diff --git a/src/main/java/io/github/opensabre/sysadmin/internaltoken/config/InternalTokenManagementSecurityConfiguration.java b/src/main/java/io/github/opensabre/sysadmin/internaltoken/config/InternalTokenManagementSecurityConfiguration.java index e384802..bbf66e3 100644 --- a/src/main/java/io/github/opensabre/sysadmin/internaltoken/config/InternalTokenManagementSecurityConfiguration.java +++ b/src/main/java/io/github/opensabre/sysadmin/internaltoken/config/InternalTokenManagementSecurityConfiguration.java @@ -14,6 +14,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter; import org.springframework.security.web.SecurityFilterChain; +import java.util.stream.Stream; /** * Verifies external JWTs at the first application and protects the key control plane. @@ -34,8 +35,12 @@ public SecurityFilterChain sysadminSecurityFilterChain( session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authorize -> authorize .requestMatchers(MANAGEMENT_PATH).hasAuthority("ADMIN") + .requestMatchers("/actuator/internalTokenKeyStatus") + .hasAuthority(ActuatorMonitoringAccess.AUTHORITY) .requestMatchers(ActuatorMonitoringAccess.metricPathArray()) .hasAuthority(ActuatorMonitoringAccess.AUTHORITY) + .requestMatchers("/actuator/**") + .hasAuthority("SCOPE_actuator.read") .anyRequest().permitAll()) .oauth2ResourceServer(resourceServer -> resourceServer .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))) @@ -51,9 +56,16 @@ public SecurityFilterChain sysadminSecurityFilterChain( authoritiesConverter.setAuthorityPrefix(""); authoritiesConverter.setAuthoritiesClaimName("roles"); + JwtGrantedAuthoritiesConverter scopeAuthoritiesConverter = + new JwtGrantedAuthoritiesConverter(); + JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter(); - authenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); + authenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> + Stream.concat(authoritiesConverter.convert(jwt).stream(), + scopeAuthoritiesConverter.convert(jwt).stream()) + .distinct() + .toList()); return authenticationConverter; } } diff --git a/src/main/java/io/github/opensabre/sysadmin/internaltoken/service/DiscoveryInternalTokenInstanceVersionVerifier.java b/src/main/java/io/github/opensabre/sysadmin/internaltoken/service/DiscoveryInternalTokenInstanceVersionVerifier.java index 0d0b4c6..6ad59a4 100644 --- a/src/main/java/io/github/opensabre/sysadmin/internaltoken/service/DiscoveryInternalTokenInstanceVersionVerifier.java +++ b/src/main/java/io/github/opensabre/sysadmin/internaltoken/service/DiscoveryInternalTokenInstanceVersionVerifier.java @@ -1,12 +1,15 @@ package io.github.opensabre.sysadmin.internaltoken.service; import io.github.opensabre.sysadmin.internaltoken.config.InternalTokenKeyManagementProperties; +import io.github.opensabre.security.actuator.ActuatorMonitoringTokenIssuer; +import io.github.opensabre.security.token.InternalTokenConstants; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; import java.time.Instant; +import java.net.URI; import java.util.ArrayList; import java.util.List; @@ -15,18 +18,19 @@ public class DiscoveryInternalTokenInstanceVersionVerifier implements InternalTokenInstanceVersionVerifier { - private static final String STATUS_PATH = "/actuator/internalTokenKeyStatus"; - private final DiscoveryClient discoveryClient; private final RestClient restClient; private final InternalTokenKeyManagementProperties properties; + private final ActuatorMonitoringTokenIssuer tokenIssuer; public DiscoveryInternalTokenInstanceVersionVerifier( DiscoveryClient discoveryClient, - InternalTokenKeyManagementProperties properties) { + InternalTokenKeyManagementProperties properties, + ActuatorMonitoringTokenIssuer tokenIssuer) { this.discoveryClient = discoveryClient; this.restClient = RestClient.create(); this.properties = properties; + this.tokenIssuer = tokenIssuer; } @Override @@ -41,7 +45,8 @@ public void requireAllInstances(long configVersion, String activeKeyId) { for (ServiceInstance instance : instances) { try { InstanceRefreshStatus status = restClient.get() - .uri(instance.getUri().resolve(STATUS_PATH)) + .uri(statusUri(instance)) + .header(InternalTokenConstants.HEADER, tokenIssuer.issue(application)) .retrieve() .body(InstanceRefreshStatus.class); if (status == null || !status.successful() @@ -60,6 +65,21 @@ public void requireAllInstances(long configVersion, String activeKeyId) { } } + static URI statusUri(ServiceInstance instance) { + var metadata = instance.getMetadata(); + String scheme = metadata.getOrDefault("management.scheme", instance.isSecure() ? "https" : "http"); + String host = metadata.getOrDefault("management.host", instance.getHost()); + String port = metadata.getOrDefault("management.port", Integer.toString(instance.getPort())); + String path = metadata.getOrDefault("management.path", "/actuator"); + if (!path.startsWith("/")) { + throw new IllegalArgumentException("Actuator management path must start with /"); + } + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + return URI.create(scheme + "://" + host + ":" + port + path + "/internalTokenKeyStatus"); + } + record InstanceRefreshStatus( long configVersion, String activeKeyId, diff --git a/src/test/java/io/github/opensabre/sysadmin/internaltoken/service/DiscoveryInternalTokenInstanceVersionVerifierTest.java b/src/test/java/io/github/opensabre/sysadmin/internaltoken/service/DiscoveryInternalTokenInstanceVersionVerifierTest.java new file mode 100644 index 0000000..9c7415e --- /dev/null +++ b/src/test/java/io/github/opensabre/sysadmin/internaltoken/service/DiscoveryInternalTokenInstanceVersionVerifierTest.java @@ -0,0 +1,19 @@ +package io.github.opensabre.sysadmin.internaltoken.service; + +import org.junit.jupiter.api.Test; +import org.springframework.cloud.client.DefaultServiceInstance; + +import static org.assertj.core.api.Assertions.assertThat; + +class DiscoveryInternalTokenInstanceVersionVerifierTest { + + @Test + void usesDiscoveredManagementAddressInsteadOfBusinessPort() { + var instance = new DefaultServiceInstance("id", "base-gateway", "business-host", 8000, false); + instance.getMetadata().put("management.host", "management-host"); + instance.getMetadata().put("management.port", "18080"); + + assertThat(DiscoveryInternalTokenInstanceVersionVerifier.statusUri(instance)) + .hasToString("http://management-host:18080/actuator/internalTokenKeyStatus"); + } +}