Make rbac excluded endpoints configurable 91/137891/8 newdelhi
authorFiete Ostkamp <Fiete.Ostkamp@telekom.de>
Tue, 14 May 2024 11:38:17 +0000 (13:38 +0200)
committerFiete Ostkamp <Fiete.Ostkamp@telekom.de>
Thu, 16 May 2024 12:07:00 +0000 (14:07 +0200)
- 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 <Fiete.Ostkamp@telekom.de>
14 files changed:
app/build.gradle
app/src/main/resources/application-access-control.yml
app/src/main/resources/application.yml
app/src/test/java/org/onap/portalng/bff/BaseIntegrationTest.java
app/src/test/java/org/onap/portalng/bff/idtoken/IdTokenExchangeFilterFunctionTest.java
app/src/test/resources/application-access-control.yml [deleted file]
app/src/test/resources/application.yml
app/src/test/resources/logback-spring.xml [deleted file]
build.gradle
lib/build.gradle
lib/src/main/java/org/onap/portalng/bff/config/BeansConfig.java
lib/src/main/java/org/onap/portalng/bff/config/BffConfig.java
lib/src/main/java/org/onap/portalng/bff/config/IdTokenExchangeFilterFunction.java
lib/src/main/java/org/onap/portalng/bff/config/SecurityConfig.java

index 4305de0..ed30630 100644 (file)
@@ -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"
index 4da29f1..6fda781 100644 (file)
@@ -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]
index 367b33c..a99ff0b 100644 (file)
@@ -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/**
 
index 1311ac7..528568d 100644 (file)
@@ -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
index cb6694a..b7491f2 100644 (file)
@@ -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 (file)
index 6fda781..0000000
+++ /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]
index 3e423e4..04e6a57 100644 (file)
@@ -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 (file)
index 45bd7e2..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<configuration scan="true">
-    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
-
-    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
-        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
-            <level>${LOGBACK_LEVEL:-info}</level>
-        </filter>
-        <encoder>
-            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
-            <charset>utf8</charset>
-        </encoder>
-    </appender>
-
-    <root level="all">
-        <appender-ref ref="stdout"/>
-    </root>
-</configuration>
\ No newline at end of file
index bb57241..3e3b47d 100644 (file)
@@ -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'
index adb82ea..44b2920 100644 (file)
@@ -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")
index a23ac0c..f64da12 100644 (file)
@@ -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(
index 5bc618c..3fada84 100644 (file)
@@ -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<String, List<String>> accessControl;
+  @NotNull private final Map<String, Set<String>> accessControl;
 
-  public Mono<List<String>> getRoles(String method) {
+  public Mono<Set<String>> getRoles(String method) {
     return Mono.just(accessControl)
         .map(control -> control.get(method))
         .onErrorResume(
index d747f3a..26db78d 100644 (file)
@@ -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<String> EXCLUDED_PATHS_PATTERNS =
-      List.of(
-          "/actuator/**", "**/actuator/**", "*/actuator/**", "/**/actuator/**", "/*/actuator/**");
+  private final List<String> rbacExcludedPatterns;
 
   private static final Mono<ServerWebExchange> 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<String> rbacExcludedPatterns) {
+    this.rbacExcludedPatterns = rbacExcludedPatterns;
+  }
+
   @Override
   public Mono<ClientResponse> 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<Void> validateAccess(
-      ServerWebExchange exchange, List<String> rolesListForMethod) {
+      ServerWebExchange exchange, Set<String> 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<String>) obj))
-        .map(roles -> roles.orElse(Collections.emptyList()));
+        .filter(List.class::isInstance)
+        .map(roles -> (List<String>) roles)
+        .switchIfEmpty(Mono.just(List.<String>of()));
   }
 }
index 2a0f701..94c87bd 100644 (file)
@@ -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()