From 9ad020e36d7dba6e9e2fdd2e5b5276e728de4bd3 Mon Sep 17 00:00:00 2001 From: Fiete Ostkamp Date: Tue, 14 May 2024 13:38:17 +0200 Subject: Make rbac excluded endpoints configurable - introduce bff.rbac.endpoints-excluded config - add some performance improvements for role checking - resolve compilation warning related to missing swagger dependency Issue-ID: PORTALNG-100 Change-Id: I38ac942f0731a3297a797a09402f20aa6efc3b58 Signed-off-by: Fiete Ostkamp --- app/build.gradle | 1 + .../main/resources/application-access-control.yml | 42 +++++++++++----------- app/src/main/resources/application.yml | 4 +++ .../org/onap/portalng/bff/BaseIntegrationTest.java | 2 +- .../idtoken/IdTokenExchangeFilterFunctionTest.java | 7 ++-- .../test/resources/application-access-control.yml | 21 ----------- app/src/test/resources/application.yml | 15 ++++---- app/src/test/resources/logback-spring.xml | 18 ---------- build.gradle | 1 + lib/build.gradle | 1 + .../org/onap/portalng/bff/config/BeansConfig.java | 5 --- .../org/onap/portalng/bff/config/BffConfig.java | 8 ++--- .../bff/config/IdTokenExchangeFilterFunction.java | 35 +++++++++--------- .../onap/portalng/bff/config/SecurityConfig.java | 10 ++++-- 14 files changed, 70 insertions(+), 100 deletions(-) delete mode 100644 app/src/test/resources/application-access-control.yml delete mode 100644 app/src/test/resources/logback-spring.xml diff --git a/app/build.gradle b/app/build.gradle index 4305de0..ed30630 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,6 +41,7 @@ dependencies { implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' implementation "org.zalando:problem-spring-webflux:$problemSpringVersion" implementation "org.zalando:jackson-datatype-problem:$problemVersion" + implementation "io.swagger.core.v3:swagger-annotations:$swaggerV3Version" implementation "org.mapstruct:mapstruct:$mapStructVersion" annotationProcessor "org.mapstruct:mapstruct-processor:$mapStructVersion" diff --git a/app/src/main/resources/application-access-control.yml b/app/src/main/resources/application-access-control.yml index 4da29f1..6fda781 100644 --- a/app/src/main/resources/application-access-control.yml +++ b/app/src/main/resources/application-access-control.yml @@ -1,21 +1,21 @@ -bff.access-control: - ACTIONS_CREATE: [ portal_admin, portal_designer, portal_operator ] - ACTIONS_GET: [ portal_admin, portal_designer, portal_operator ] - ACTIONS_LIST: [ portal_admin, portal_designer, portal_operator ] - ACTIVE_ALARM_LIST: [portal_admin, portal_designer, portal_operator] - KEY_ENCRYPT_BY_USER: [portal_admin, portal_designer, portal_operator] - KEY_ENCRYPT_BY_VALUE: [portal_admin, portal_designer, portal_operator] - PREFERENCES_CREATE: [portal_admin, portal_designer, portal_operator] - PREFERENCES_GET: [portal_admin, portal_designer, portal_operator] - PREFERENCES_UPDATE: [portal_admin, portal_designer, portal_operator] - ROLE_LIST: ["*"] - USER_CREATE: [portal_admin, portal_designer, portal_operator] - USER_DELETE: [portal_admin, portal_designer, portal_operator] - USER_GET: [portal_admin, portal_designer, portal_operator] - USER_LIST_AVAILABLE_ROLES: [portal_admin, portal_designer, portal_operator] - USER_LIST_ROLES: [portal_admin, portal_designer, portal_operator] - USER_LIST: [portal_admin, portal_designer, portal_operator] - USER_UPDATE_PASSWORD: [portal_admin, portal_designer, portal_operator] - USER_UPDATE_ROLES: [portal_admin, portal_designer, portal_operator] - USER_UPDATE: [portal_admin, portal_designer, portal_operator] - +bff: + access-control: + ACTIONS_CREATE: [ portal_admin, portal_designer, portal_operator ] + ACTIONS_GET: [ portal_admin, portal_designer, portal_operator ] + ACTIONS_LIST: [ portal_admin, portal_designer, portal_operator ] + ACTIVE_ALARM_LIST: [portal_admin, portal_designer, portal_operator] + KEY_ENCRYPT_BY_USER: [portal_admin, portal_designer, portal_operator] + KEY_ENCRYPT_BY_VALUE: [portal_admin, portal_designer, portal_operator] + PREFERENCES_CREATE: [portal_admin, portal_designer, portal_operator] + PREFERENCES_GET: [portal_admin, portal_designer, portal_operator] + PREFERENCES_UPDATE: [portal_admin, portal_designer, portal_operator] + ROLE_LIST: ["*"] + USER_CREATE: [portal_admin, portal_designer, portal_operator] + USER_DELETE: [portal_admin, portal_designer, portal_operator] + USER_GET: [portal_admin, portal_designer, portal_operator] + USER_LIST_AVAILABLE_ROLES: [portal_admin, portal_designer, portal_operator] + USER_LIST_ROLES: [portal_admin, portal_designer, portal_operator] + USER_LIST: [portal_admin, portal_designer, portal_operator] + USER_UPDATE_PASSWORD: [portal_admin, portal_designer, portal_operator] + USER_UPDATE_ROLES: [portal_admin, portal_designer, portal_operator] + USER_UPDATE: [portal_admin, portal_designer, portal_operator] diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml index 367b33c..a99ff0b 100644 --- a/app/src/main/resources/application.yml +++ b/app/src/main/resources/application.yml @@ -52,4 +52,8 @@ bff: preferences-url: ${PREFERENCES_URL} history-url: ${HISTORY_URL} keycloak-url: ${KEYCLOAK_URL} + endpoints: + unauthenticated: /api-docs.html, /api.yaml, /webjars/**, /actuator/** + rbac: + endpoints-excluded: /actuator/**, **/actuator/**, */actuator/**, /**/actuator/**, /*/actuator/** diff --git a/app/src/test/java/org/onap/portalng/bff/BaseIntegrationTest.java b/app/src/test/java/org/onap/portalng/bff/BaseIntegrationTest.java index 1311ac7..528568d 100644 --- a/app/src/test/java/org/onap/portalng/bff/BaseIntegrationTest.java +++ b/app/src/test/java/org/onap/portalng/bff/BaseIntegrationTest.java @@ -52,8 +52,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.http.MediaType; /** Base class for all tests that has the common config including port, realm, logging and auth. */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureWireMock(port = 0) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public abstract class BaseIntegrationTest { @TestConfiguration diff --git a/app/src/test/java/org/onap/portalng/bff/idtoken/IdTokenExchangeFilterFunctionTest.java b/app/src/test/java/org/onap/portalng/bff/idtoken/IdTokenExchangeFilterFunctionTest.java index cb6694a..b7491f2 100644 --- a/app/src/test/java/org/onap/portalng/bff/idtoken/IdTokenExchangeFilterFunctionTest.java +++ b/app/src/test/java/org/onap/portalng/bff/idtoken/IdTokenExchangeFilterFunctionTest.java @@ -30,6 +30,7 @@ import java.util.UUID; import org.junit.jupiter.api.Test; import org.onap.portalng.bff.BaseIntegrationTest; import org.onap.portalng.bff.config.IdTokenExchangeFilterFunction; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; @@ -41,10 +42,10 @@ import reactor.core.publisher.Mono; class IdTokenExchangeFilterFunctionTest extends BaseIntegrationTest { + @Autowired IdTokenExchangeFilterFunction filterFunction; + @Test void idTokenIsCorrectlyPropagated() { - final IdTokenExchangeFilterFunction filterFunction = new IdTokenExchangeFilterFunction(); - final String idToken = UUID.randomUUID().toString(); final ServerWebExchange serverWebExchange = MockServerWebExchange.builder( @@ -72,8 +73,6 @@ class IdTokenExchangeFilterFunctionTest extends BaseIntegrationTest { @Test void exceptionIsThrownWhenIdTokenIsMissingInRequest() { - final IdTokenExchangeFilterFunction filterFunction = new IdTokenExchangeFilterFunction(); - final ServerWebExchange serverWebExchange = MockServerWebExchange.builder(MockServerHttpRequest.get("http://localhost:8000")).build(); diff --git a/app/src/test/resources/application-access-control.yml b/app/src/test/resources/application-access-control.yml deleted file mode 100644 index 6fda781..0000000 --- a/app/src/test/resources/application-access-control.yml +++ /dev/null @@ -1,21 +0,0 @@ -bff: - access-control: - ACTIONS_CREATE: [ portal_admin, portal_designer, portal_operator ] - ACTIONS_GET: [ portal_admin, portal_designer, portal_operator ] - ACTIONS_LIST: [ portal_admin, portal_designer, portal_operator ] - ACTIVE_ALARM_LIST: [portal_admin, portal_designer, portal_operator] - KEY_ENCRYPT_BY_USER: [portal_admin, portal_designer, portal_operator] - KEY_ENCRYPT_BY_VALUE: [portal_admin, portal_designer, portal_operator] - PREFERENCES_CREATE: [portal_admin, portal_designer, portal_operator] - PREFERENCES_GET: [portal_admin, portal_designer, portal_operator] - PREFERENCES_UPDATE: [portal_admin, portal_designer, portal_operator] - ROLE_LIST: ["*"] - USER_CREATE: [portal_admin, portal_designer, portal_operator] - USER_DELETE: [portal_admin, portal_designer, portal_operator] - USER_GET: [portal_admin, portal_designer, portal_operator] - USER_LIST_AVAILABLE_ROLES: [portal_admin, portal_designer, portal_operator] - USER_LIST_ROLES: [portal_admin, portal_designer, portal_operator] - USER_LIST: [portal_admin, portal_designer, portal_operator] - USER_UPDATE_PASSWORD: [portal_admin, portal_designer, portal_operator] - USER_UPDATE_ROLES: [portal_admin, portal_designer, portal_operator] - USER_UPDATE: [portal_admin, portal_designer, portal_operator] diff --git a/app/src/test/resources/application.yml b/app/src/test/resources/application.yml index 3e423e4..04e6a57 100644 --- a/app/src/test/resources/application.yml +++ b/app/src/test/resources/application.yml @@ -1,7 +1,6 @@ -logging: - level: - org.springframework.web: TRACE - +management: + tracing: + enabled: false spring: profiles: include: @@ -22,12 +21,14 @@ spring: resourceserver: jwt: jwk-set-uri: http://localhost:${wiremock.server.port}/realms/ONAP/protocol/openid-connect/certs - jackson: - serialization: - FAIL_ON_EMPTY_BEANS: false bff: realm: ONAP preferences-url: http://localhost:${wiremock.server.port} history-url: http://localhost:${wiremock.server.port} keycloak-url: http://localhost:${wiremock.server.port} + endpoints: + unauthenticated: /api-docs.html, /api.yaml, /webjars/**, /actuator/** + rbac: + endpoints-excluded: /actuator/**, **/actuator/**, */actuator/**, /**/actuator/**, /*/actuator/** + diff --git a/app/src/test/resources/logback-spring.xml b/app/src/test/resources/logback-spring.xml deleted file mode 100644 index 45bd7e2..0000000 --- a/app/src/test/resources/logback-spring.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - ${LOGBACK_LEVEL:-info} - - - ${CONSOLE_LOG_PATTERN} - utf8 - - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index bb57241..3e3b47d 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ ext { logbackVersion = '7.4' lombokVersion = '1.18.28' micrometerVersion = '1.1.4' + swaggerV3Version = '2.2.21' // app wiremockVersion = '4.0.4' diff --git a/lib/build.gradle b/lib/build.gradle index adb82ea..44b2920 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation "org.mapstruct:mapstruct:$mapStructVersion" implementation "org.mapstruct.extensions.spring:mapstruct-spring-annotations:$mapStructExtensionsVersion" implementation "org.mapstruct.extensions.spring:mapstruct-spring-extensions:$mapStructExtensionsVersion" + implementation "io.swagger.core.v3:swagger-annotations:$swaggerV3Version" implementation(platform("io.micrometer:micrometer-tracing-bom:$micrometerVersion")) implementation("io.micrometer:micrometer-tracing") diff --git a/lib/src/main/java/org/onap/portalng/bff/config/BeansConfig.java b/lib/src/main/java/org/onap/portalng/bff/config/BeansConfig.java index a23ac0c..f64da12 100644 --- a/lib/src/main/java/org/onap/portalng/bff/config/BeansConfig.java +++ b/lib/src/main/java/org/onap/portalng/bff/config/BeansConfig.java @@ -74,11 +74,6 @@ public class BeansConfig { return oauth2Filter; } - @Bean(name = ID_TOKEN_EXCHANGE_FILTER_FUNCTION) - ExchangeFilterFunction idTokenExchangeFilterFunction() { - return new IdTokenExchangeFilterFunction(); - } - @Bean(name = ERROR_HANDLING_EXCHANGE_FILTER_FUNCTION) ExchangeFilterFunction errorHandlingExchangeFilterFunction() { return ExchangeFilterFunction.ofResponseProcessor( diff --git a/lib/src/main/java/org/onap/portalng/bff/config/BffConfig.java b/lib/src/main/java/org/onap/portalng/bff/config/BffConfig.java index 5bc618c..3fada84 100644 --- a/lib/src/main/java/org/onap/portalng/bff/config/BffConfig.java +++ b/lib/src/main/java/org/onap/portalng/bff/config/BffConfig.java @@ -24,8 +24,8 @@ package org.onap.portalng.bff.config; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import java.util.List; import java.util.Map; +import java.util.Set; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.zalando.problem.Problem; @@ -37,8 +37,8 @@ import reactor.core.publisher.Mono; * urls. */ @Valid -@ConfigurationProperties("bff") @Data +@ConfigurationProperties("bff") public class BffConfig { @NotBlank private final String realm; @@ -46,9 +46,9 @@ public class BffConfig { @NotBlank private final String historyUrl; @NotBlank private final String keycloakUrl; - @NotNull private final Map> accessControl; + @NotNull private final Map> accessControl; - public Mono> getRoles(String method) { + public Mono> getRoles(String method) { return Mono.just(accessControl) .map(control -> control.get(method)) .onErrorResume( diff --git a/lib/src/main/java/org/onap/portalng/bff/config/IdTokenExchangeFilterFunction.java b/lib/src/main/java/org/onap/portalng/bff/config/IdTokenExchangeFilterFunction.java index d747f3a..26db78d 100644 --- a/lib/src/main/java/org/onap/portalng/bff/config/IdTokenExchangeFilterFunction.java +++ b/lib/src/main/java/org/onap/portalng/bff/config/IdTokenExchangeFilterFunction.java @@ -23,9 +23,10 @@ package org.onap.portalng.bff.config; import com.nimbusds.jwt.JWTParser; import java.text.ParseException; -import java.util.Collections; import java.util.List; -import java.util.Optional; +import java.util.Set; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; @@ -37,27 +38,32 @@ import org.zalando.problem.Status; import reactor.core.Exceptions; import reactor.core.publisher.Mono; +@Component public class IdTokenExchangeFilterFunction implements ExchangeFilterFunction { public static final String X_AUTH_IDENTITY_HEADER = "X-Auth-Identity"; public static final String CLAIM_NAME_ROLES = "roles"; - private static final List EXCLUDED_PATHS_PATTERNS = - List.of( - "/actuator/**", "**/actuator/**", "*/actuator/**", "/**/actuator/**", "/*/actuator/**"); + private final List rbacExcludedPatterns; private static final Mono serverWebExchangeFromContext = Mono.deferContextual(Mono::just) .filter(context -> context.hasKey(ServerWebExchange.class)) .map(context -> context.get(ServerWebExchange.class)); + private final AntPathMatcher antPathMatcher = new AntPathMatcher(); + + public IdTokenExchangeFilterFunction( + @Value("${bff.rbac.endpoints-excluded}") List rbacExcludedPatterns) { + this.rbacExcludedPatterns = rbacExcludedPatterns; + } + @Override public Mono filter(ClientRequest request, ExchangeFunction next) { boolean shouldNotFilter = - EXCLUDED_PATHS_PATTERNS.stream() + rbacExcludedPatterns.stream() .anyMatch( - excludedPath -> - new AntPathMatcher().match(excludedPath, request.url().getRawPath())); + excludedPath -> antPathMatcher.match(excludedPath, request.url().getRawPath())); if (shouldNotFilter) { return next.exchange(request).switchIfEmpty(Mono.defer(() -> next.exchange(request))); } @@ -86,7 +92,7 @@ public class IdTokenExchangeFilterFunction implements ExchangeFilterFunction { } public static Mono validateAccess( - ServerWebExchange exchange, List rolesListForMethod) { + ServerWebExchange exchange, Set rolesListForMethod) { return extractRoles(exchange) .map(roles -> roles.stream().anyMatch(rolesListForMethod::contains)) @@ -110,16 +116,13 @@ public class IdTokenExchangeFilterFunction implements ExchangeFilterFunction { .map( jwt -> { try { - return Optional.of(jwt.getJWTClaimsSet()); + return jwt.getJWTClaimsSet().getClaim(CLAIM_NAME_ROLES); } catch (ParseException e) { throw Exceptions.propagate(e); } }) - .map( - optionalClaimsSet -> - optionalClaimsSet - .map(claimsSet -> claimsSet.getClaim(CLAIM_NAME_ROLES)) - .map(obj -> (List) obj)) - .map(roles -> roles.orElse(Collections.emptyList())); + .filter(List.class::isInstance) + .map(roles -> (List) roles) + .switchIfEmpty(Mono.just(List.of())); } } diff --git a/lib/src/main/java/org/onap/portalng/bff/config/SecurityConfig.java b/lib/src/main/java/org/onap/portalng/bff/config/SecurityConfig.java index 2a0f701..94c87bd 100644 --- a/lib/src/main/java/org/onap/portalng/bff/config/SecurityConfig.java +++ b/lib/src/main/java/org/onap/portalng/bff/config/SecurityConfig.java @@ -21,9 +21,9 @@ package org.onap.portalng.bff.config; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; @@ -34,9 +34,13 @@ import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2Autho import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; import org.springframework.security.web.server.SecurityWebFilterChain; -@EnableWebFluxSecurity @Configuration +@EnableWebFluxSecurity public class SecurityConfig { + + @Value("${bff.endpoints.unauthenticated}") + private String[] unauthenticatedEndpoints; + @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http.httpBasic() @@ -48,7 +52,7 @@ public class SecurityConfig { .cors() .and() .authorizeExchange() - .pathMatchers(HttpMethod.GET, "/api-docs.html", "/api.yaml", "/webjars/**", "/actuator/**") + .pathMatchers(unauthenticatedEndpoints) .permitAll() .anyExchange() .authenticated() -- cgit