Ver código fonte

RBAC: Implement generic OAuth2 authority extractor (#3740)

Roman Zabaluev 2 anos atrás
pai
commit
3ca417f64a

+ 11 - 1
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java

@@ -1,6 +1,7 @@
 package com.provectus.kafka.ui.config.auth;
 package com.provectus.kafka.ui.config.auth;
 
 
 import jakarta.annotation.PostConstruct;
 import jakarta.annotation.PostConstruct;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 import java.util.Set;
 import java.util.Set;
@@ -14,7 +15,16 @@ public class OAuthProperties {
   private Map<String, OAuth2Provider> client = new HashMap<>();
   private Map<String, OAuth2Provider> client = new HashMap<>();
 
 
   @PostConstruct
   @PostConstruct
-  public void validate() {
+  public void init() {
+    getClient().values().forEach((provider) -> {
+      if (provider.getCustomParams() == null) {
+        provider.setCustomParams(Collections.emptyMap());
+      }
+      if (provider.getScope() == null) {
+        provider.setScope(Collections.emptySet());
+      }
+    });
+
     getClient().values().forEach(this::validateProvider);
     getClient().values().forEach(this::validateProvider);
   }
   }
 
 

+ 1 - 2
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthPropertiesConverter.java

@@ -73,8 +73,7 @@ public final class OAuthPropertiesConverter {
   }
   }
 
 
   private static boolean isGoogle(OAuth2Provider provider) {
   private static boolean isGoogle(OAuth2Provider provider) {
-    return provider.getCustomParams() != null
-        && GOOGLE.equalsIgnoreCase(provider.getCustomParams().get(TYPE));
+    return GOOGLE.equalsIgnoreCase(provider.getCustomParams().get(TYPE));
   }
   }
 }
 }
 
 

+ 11 - 11
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthSecurityConfig.java

@@ -72,13 +72,13 @@ public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {
     final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();
     final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();
     return request -> delegate.loadUser(request)
     return request -> delegate.loadUser(request)
         .flatMap(user -> {
         .flatMap(user -> {
-          String providerId = request.getClientRegistration().getRegistrationId();
-          final var extractor = getExtractor(providerId, acs);
+          var provider = getProviderByProviderId(request.getClientRegistration().getRegistrationId());
+          final var extractor = getExtractor(provider, acs);
           if (extractor == null) {
           if (extractor == null) {
             return Mono.just(user);
             return Mono.just(user);
           }
           }
 
 
-          return extractor.extract(acs, user, Map.of("request", request))
+          return extractor.extract(acs, user, Map.of("request", request, "provider", provider))
               .map(groups -> new RbacOidcUser(user, groups));
               .map(groups -> new RbacOidcUser(user, groups));
         });
         });
   }
   }
@@ -88,13 +88,13 @@ public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {
     final DefaultReactiveOAuth2UserService delegate = new DefaultReactiveOAuth2UserService();
     final DefaultReactiveOAuth2UserService delegate = new DefaultReactiveOAuth2UserService();
     return request -> delegate.loadUser(request)
     return request -> delegate.loadUser(request)
         .flatMap(user -> {
         .flatMap(user -> {
-          String providerId = request.getClientRegistration().getRegistrationId();
-          final var extractor = getExtractor(providerId, acs);
+          var provider = getProviderByProviderId(request.getClientRegistration().getRegistrationId());
+          final var extractor = getExtractor(provider, acs);
           if (extractor == null) {
           if (extractor == null) {
             return Mono.just(user);
             return Mono.just(user);
           }
           }
 
 
-          return extractor.extract(acs, user, Map.of("request", request))
+          return extractor.extract(acs, user, Map.of("request", request, "provider", provider))
               .map(groups -> new RbacOAuth2User(user, groups));
               .map(groups -> new RbacOAuth2User(user, groups));
         });
         });
   }
   }
@@ -113,18 +113,18 @@ public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {
   }
   }
 
 
   @Nullable
   @Nullable
-  private ProviderAuthorityExtractor getExtractor(final String providerId, AccessControlService acs) {
-    final String provider = getProviderByProviderId(providerId);
+  private ProviderAuthorityExtractor getExtractor(final OAuthProperties.OAuth2Provider provider,
+                                                  AccessControlService acs) {
     Optional<ProviderAuthorityExtractor> extractor = acs.getOauthExtractors()
     Optional<ProviderAuthorityExtractor> extractor = acs.getOauthExtractors()
         .stream()
         .stream()
-        .filter(e -> e.isApplicable(provider))
+        .filter(e -> e.isApplicable(provider.getProvider(), provider.getCustomParams()))
         .findFirst();
         .findFirst();
 
 
     return extractor.orElse(null);
     return extractor.orElse(null);
   }
   }
 
 
-  private String getProviderByProviderId(final String providerId) {
-    return properties.getClient().get(providerId).getProvider();
+  private OAuthProperties.OAuth2Provider getProviderByProviderId(final String providerId) {
+    return properties.getClient().get(providerId);
   }
   }
 
 
 }
 }

+ 2 - 4
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/CognitoLogoutSuccessHandler.java

@@ -46,10 +46,8 @@ public class CognitoLogoutSuccessHandler implements LogoutSuccessHandler {
         .fragment(null)
         .fragment(null)
         .build();
         .build();
 
 
-    Assert.isTrue(
-        provider.getCustomParams() != null && provider.getCustomParams().containsKey("logoutUrl"),
-        "Custom params should contain 'logoutUrl'"
-    );
+    Assert.isTrue(provider.getCustomParams().containsKey("logoutUrl"),
+        "Custom params should contain 'logoutUrl'");
     final var uri = UriComponentsBuilder
     final var uri = UriComponentsBuilder
         .fromUri(URI.create(provider.getCustomParams().get("logoutUrl")))
         .fromUri(URI.create(provider.getCustomParams().get("logoutUrl")))
         .queryParam("client_id", provider.getClientId())
         .queryParam("client_id", provider.getClientId())

+ 4 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/provider/Provider.java

@@ -10,6 +10,8 @@ public enum Provider {
 
 
   OAUTH_COGNITO,
   OAUTH_COGNITO,
 
 
+  OAUTH,
+
   LDAP,
   LDAP,
   LDAP_AD;
   LDAP_AD;
 
 
@@ -22,6 +24,8 @@ public enum Provider {
     public static String GOOGLE = "google";
     public static String GOOGLE = "google";
     public static String GITHUB = "github";
     public static String GITHUB = "github";
     public static String COGNITO = "cognito";
     public static String COGNITO = "cognito";
+
+    public static String OAUTH = "oauth";
   }
   }
 
 
 }
 }

+ 2 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java

@@ -20,6 +20,7 @@ import com.provectus.kafka.ui.model.rbac.permission.TopicAction;
 import com.provectus.kafka.ui.service.rbac.extractor.CognitoAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.CognitoAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.GithubAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.GithubAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.GoogleAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.GoogleAuthorityExtractor;
+import com.provectus.kafka.ui.service.rbac.extractor.OauthAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor;
 import jakarta.annotation.PostConstruct;
 import jakarta.annotation.PostConstruct;
 import java.util.Collections;
 import java.util.Collections;
@@ -76,6 +77,7 @@ public class AccessControlService {
               case OAUTH_COGNITO -> new CognitoAuthorityExtractor();
               case OAUTH_COGNITO -> new CognitoAuthorityExtractor();
               case OAUTH_GOOGLE -> new GoogleAuthorityExtractor();
               case OAUTH_GOOGLE -> new GoogleAuthorityExtractor();
               case OAUTH_GITHUB -> new GithubAuthorityExtractor();
               case OAUTH_GITHUB -> new GithubAuthorityExtractor();
+              case OAUTH -> new OauthAuthorityExtractor();
               default -> null;
               default -> null;
             })
             })
             .filter(Objects::nonNull)
             .filter(Objects::nonNull)

+ 6 - 3
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.java

@@ -1,5 +1,8 @@
 package com.provectus.kafka.ui.service.rbac.extractor;
 package com.provectus.kafka.ui.service.rbac.extractor;
 
 
+import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.COGNITO;
+
+import com.google.common.collect.Sets;
 import com.provectus.kafka.ui.model.rbac.Role;
 import com.provectus.kafka.ui.model.rbac.Role;
 import com.provectus.kafka.ui.model.rbac.provider.Provider;
 import com.provectus.kafka.ui.model.rbac.provider.Provider;
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
@@ -18,8 +21,8 @@ public class CognitoAuthorityExtractor implements ProviderAuthorityExtractor {
   private static final String COGNITO_GROUPS_ATTRIBUTE_NAME = "cognito:groups";
   private static final String COGNITO_GROUPS_ATTRIBUTE_NAME = "cognito:groups";
 
 
   @Override
   @Override
-  public boolean isApplicable(String provider) {
-    return Provider.Name.COGNITO.equalsIgnoreCase(provider);
+  public boolean isApplicable(String provider, Map<String, String> customParams) {
+    return COGNITO.equalsIgnoreCase(provider) || COGNITO.equalsIgnoreCase(customParams.get(TYPE));
   }
   }
 
 
   @Override
   @Override
@@ -63,7 +66,7 @@ public class CognitoAuthorityExtractor implements ProviderAuthorityExtractor {
         .map(Role::getName)
         .map(Role::getName)
         .collect(Collectors.toSet());
         .collect(Collectors.toSet());
 
 
-    return Mono.just(Stream.concat(groupsByUsername.stream(), groupsByGroups.stream()).collect(Collectors.toSet()));
+    return Mono.just(Sets.union(groupsByUsername, groupsByGroups));
   }
   }
 
 
 }
 }

+ 4 - 2
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GithubAuthorityExtractor.java

@@ -1,5 +1,7 @@
 package com.provectus.kafka.ui.service.rbac.extractor;
 package com.provectus.kafka.ui.service.rbac.extractor;
 
 
+import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.GITHUB;
+
 import com.provectus.kafka.ui.model.rbac.Role;
 import com.provectus.kafka.ui.model.rbac.Role;
 import com.provectus.kafka.ui.model.rbac.provider.Provider;
 import com.provectus.kafka.ui.model.rbac.provider.Provider;
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
@@ -28,8 +30,8 @@ public class GithubAuthorityExtractor implements ProviderAuthorityExtractor {
   private static final String DUMMY = "dummy";
   private static final String DUMMY = "dummy";
 
 
   @Override
   @Override
-  public boolean isApplicable(String provider) {
-    return Provider.Name.GITHUB.equalsIgnoreCase(provider);
+  public boolean isApplicable(String provider, Map<String, String> customParams) {
+    return GITHUB.equalsIgnoreCase(provider) || GITHUB.equalsIgnoreCase(customParams.get(TYPE));
   }
   }
 
 
   @Override
   @Override

+ 8 - 8
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GoogleAuthorityExtractor.java

@@ -1,13 +1,14 @@
 package com.provectus.kafka.ui.service.rbac.extractor;
 package com.provectus.kafka.ui.service.rbac.extractor;
 
 
+import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.GOOGLE;
+
+import com.google.common.collect.Sets;
 import com.provectus.kafka.ui.model.rbac.Role;
 import com.provectus.kafka.ui.model.rbac.Role;
 import com.provectus.kafka.ui.model.rbac.provider.Provider;
 import com.provectus.kafka.ui.model.rbac.provider.Provider;
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
-import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.Set;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
 import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
@@ -19,8 +20,8 @@ public class GoogleAuthorityExtractor implements ProviderAuthorityExtractor {
   public static final String EMAIL_ATTRIBUTE_NAME = "email";
   public static final String EMAIL_ATTRIBUTE_NAME = "email";
 
 
   @Override
   @Override
-  public boolean isApplicable(String provider) {
-    return Provider.Name.GOOGLE.equalsIgnoreCase(provider);
+  public boolean isApplicable(String provider, Map<String, String> customParams) {
+    return GOOGLE.equalsIgnoreCase(provider) || GOOGLE.equalsIgnoreCase(customParams.get(TYPE));
   }
   }
 
 
   @Override
   @Override
@@ -52,7 +53,7 @@ public class GoogleAuthorityExtractor implements ProviderAuthorityExtractor {
       return Mono.just(groupsByUsername);
       return Mono.just(groupsByUsername);
     }
     }
 
 
-    List<String> groupsByDomain = acs.getRoles()
+    Set<String> groupsByDomain = acs.getRoles()
         .stream()
         .stream()
         .filter(r -> r.getSubjects()
         .filter(r -> r.getSubjects()
             .stream()
             .stream()
@@ -60,10 +61,9 @@ public class GoogleAuthorityExtractor implements ProviderAuthorityExtractor {
             .filter(s -> s.getType().equals("domain"))
             .filter(s -> s.getType().equals("domain"))
             .anyMatch(s -> s.getValue().equals(domain)))
             .anyMatch(s -> s.getValue().equals(domain)))
         .map(Role::getName)
         .map(Role::getName)
-        .toList();
+        .collect(Collectors.toSet());
 
 
-    return Mono.just(Stream.concat(groupsByUsername.stream(), groupsByDomain.stream())
-        .collect(Collectors.toSet()));
+    return Mono.just(Sets.union(groupsByUsername, groupsByDomain));
   }
   }
 
 
 }
 }

+ 85 - 3
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/OauthAuthorityExtractor.java

@@ -1,22 +1,44 @@
 package com.provectus.kafka.ui.service.rbac.extractor;
 package com.provectus.kafka.ui.service.rbac.extractor;
 
 
+import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.OAUTH;
+
+import com.google.common.collect.Sets;
+import com.provectus.kafka.ui.config.auth.OAuthProperties;
+import com.provectus.kafka.ui.model.rbac.Role;
+import com.provectus.kafka.ui.model.rbac.provider.Provider;
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.Set;
 import java.util.Set;
+import java.util.stream.Collectors;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
 import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.util.Assert;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
 @Slf4j
 @Slf4j
 public class OauthAuthorityExtractor implements ProviderAuthorityExtractor {
 public class OauthAuthorityExtractor implements ProviderAuthorityExtractor {
 
 
+  public static final String ROLES_FIELD_PARAM_NAME = "roles-field";
+
   @Override
   @Override
-  public boolean isApplicable(String provider) {
-    return false; // TODO #2844
+  public boolean isApplicable(String provider, Map<String, String> customParams) {
+    var containsRolesFieldNameParam = customParams.containsKey(ROLES_FIELD_PARAM_NAME);
+    if (!containsRolesFieldNameParam) {
+      log.debug("Provider [{}] doesn't contain a roles field param name, mapping won't be performed", provider);
+      return false;
+    }
+
+    return OAUTH.equalsIgnoreCase(provider) || OAUTH.equalsIgnoreCase(customParams.get(TYPE));
   }
   }
 
 
   @Override
   @Override
   public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {
   public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {
+    log.trace("Extracting OAuth2 user authorities");
+
     DefaultOAuth2User principal;
     DefaultOAuth2User principal;
     try {
     try {
       principal = (DefaultOAuth2User) value;
       principal = (DefaultOAuth2User) value;
@@ -25,7 +47,67 @@ public class OauthAuthorityExtractor implements ProviderAuthorityExtractor {
       throw new RuntimeException();
       throw new RuntimeException();
     }
     }
 
 
-    return Mono.just(Set.of(principal.getName())); // TODO #2844
+    var provider = (OAuthProperties.OAuth2Provider) additionalParams.get("provider");
+    Assert.notNull(provider, "provider is null");
+    var rolesFieldName = provider.getCustomParams().get(ROLES_FIELD_PARAM_NAME);
+
+    Set<String> rolesByUsername = acs.getRoles()
+        .stream()
+        .filter(r -> r.getSubjects()
+            .stream()
+            .filter(s -> s.getProvider().equals(Provider.OAUTH))
+            .filter(s -> s.getType().equals("user"))
+            .anyMatch(s -> s.getValue().equals(principal.getName())))
+        .map(Role::getName)
+        .collect(Collectors.toSet());
+
+    Set<String> rolesByRolesField = acs.getRoles()
+        .stream()
+        .filter(role -> role.getSubjects()
+            .stream()
+            .filter(s -> s.getProvider().equals(Provider.OAUTH))
+            .filter(s -> s.getType().equals("role"))
+            .anyMatch(subject -> {
+              var roleName = subject.getValue();
+              var principalRoles = convertRoles(principal.getAttribute(rolesFieldName));
+              var roleMatched = principalRoles.contains(roleName);
+
+              if (roleMatched) {
+                log.debug("Assigning role [{}] to user [{}]", roleName, principal.getName());
+              } else {
+                log.trace("Role [{}] not found in user [{}] roles", roleName, principal.getName());
+              }
+
+              return roleMatched;
+            })
+        )
+        .map(Role::getName)
+        .collect(Collectors.toSet());
+
+    return Mono.just(Sets.union(rolesByUsername, rolesByRolesField));
+  }
+
+  @SuppressWarnings("unchecked")
+  private Collection<String> convertRoles(Object roles) {
+    if (roles == null) {
+      log.debug("Param missing from attributes, skipping");
+      return Collections.emptySet();
+    }
+
+    if ((roles instanceof List<?>) || (roles instanceof Set<?>)) {
+      log.trace("The field is either a set or a list, returning as is");
+      return (Collection<String>) roles;
+    }
+
+    if (!(roles instanceof String)) {
+      log.debug("The field is not a string, skipping");
+      return Collections.emptySet();
+    }
+
+    log.trace("Trying to deserialize the field value [{}] as a string", roles);
+
+    return Arrays.stream(((String) roles).split(","))
+        .collect(Collectors.toSet());
   }
   }
 
 
 }
 }

+ 3 - 1
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/ProviderAuthorityExtractor.java

@@ -7,7 +7,9 @@ import reactor.core.publisher.Mono;
 
 
 public interface ProviderAuthorityExtractor {
 public interface ProviderAuthorityExtractor {
 
 
-  boolean isApplicable(String provider);
+  String TYPE = "type";
+
+  boolean isApplicable(String provider, Map<String, String> customParams);
 
 
   Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams);
   Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams);
 
 

+ 1 - 1
kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/DynamicConfigOperations.java

@@ -230,7 +230,7 @@ public class DynamicConfigOperations {
 
 
       Optional.ofNullable(auth)
       Optional.ofNullable(auth)
           .flatMap(a -> Optional.ofNullable(a.oauth2))
           .flatMap(a -> Optional.ofNullable(a.oauth2))
-          .ifPresent(OAuthProperties::validate);
+          .ifPresent(OAuthProperties::init);
 
 
       Optional.ofNullable(webclient)
       Optional.ofNullable(webclient)
           .ifPresent(WebclientProperties::validate);
           .ifPresent(WebclientProperties::validate);