Pārlūkot izejas kodu

RBAC: Implement generic OAuth2 authority extractor. Resolves #2844

Roman Zabaluev 2 gadi atpakaļ
vecāks
revīzija
ae5985eddc

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

@@ -2,6 +2,7 @@ package com.provectus.kafka.ui.config.auth;
 
 
 import jakarta.annotation.PostConstruct;
 import jakarta.annotation.PostConstruct;
 import java.util.HashMap;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Map;
 import java.util.Set;
 import java.util.Set;
 import lombok.Data;
 import lombok.Data;
@@ -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(new HashMap<>());
+      }
+      if (provider.getScope() == null) {
+        provider.setScope(new HashSet<>());
+      }
+    });
+
     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));
   }
   }
 }
 }
 
 

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

@@ -114,17 +114,17 @@ public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {
 
 
   @Nullable
   @Nullable
   private ProviderAuthorityExtractor getExtractor(final String providerId, AccessControlService acs) {
   private ProviderAuthorityExtractor getExtractor(final String providerId, AccessControlService acs) {
-    final String provider = getProviderByProviderId(providerId);
+    final var provider = getProviderByProviderId(providerId);
     Optional<ProviderAuthorityExtractor> extractor = acs.getExtractors()
     Optional<ProviderAuthorityExtractor> extractor = acs.getExtractors()
         .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.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.LdapAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.LdapAuthorityExtractor;
+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;
@@ -71,6 +72,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();
               case LDAP, LDAP_AD -> new LdapAuthorityExtractor();
               case LDAP, LDAP_AD -> new LdapAuthorityExtractor();
             }).collect(Collectors.toSet()))
             }).collect(Collectors.toSet()))
         .flatMap(Set::stream)
         .flatMap(Set::stream)

+ 4 - 2
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.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.COGNITO;
+
 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 +20,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

+ 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

+ 4 - 2
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GoogleAuthorityExtractor.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.GOOGLE;
+
 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;
@@ -19,8 +21,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

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

@@ -11,7 +11,7 @@ import reactor.core.publisher.Mono;
 public class LdapAuthorityExtractor implements ProviderAuthorityExtractor {
 public class LdapAuthorityExtractor implements ProviderAuthorityExtractor {
 
 
   @Override
   @Override
-  public boolean isApplicable(String provider) {
+  public boolean isApplicable(String provider, Map<String, String> params) {
     return false; // TODO #2752
     return false; // TODO #2752
   }
   }
 
 

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

@@ -1,8 +1,19 @@
 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.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.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 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;
@@ -11,12 +22,14 @@ import reactor.core.publisher.Mono;
 public class OauthAuthorityExtractor implements ProviderAuthorityExtractor {
 public class OauthAuthorityExtractor implements ProviderAuthorityExtractor {
 
 
   @Override
   @Override
-  public boolean isApplicable(String provider) {
-    return false; // TODO #2844
+  public boolean isApplicable(String provider, Map<String, String> customParams) {
+    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.debug("Extracting OAuth2 user authorities");
+
     DefaultOAuth2User principal;
     DefaultOAuth2User principal;
     try {
     try {
       principal = (DefaultOAuth2User) value;
       principal = (DefaultOAuth2User) value;
@@ -25,7 +38,52 @@ public class OauthAuthorityExtractor implements ProviderAuthorityExtractor {
       throw new RuntimeException();
       throw new RuntimeException();
     }
     }
 
 
-    return Mono.just(Set.of(principal.getName())); // TODO #2844
+    Set<String> groupsByUsername = 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> groupsByGroupField = acs.getRoles()
+        .stream()
+        .filter(role -> role.getSubjects()
+            .stream()
+            .filter(s -> s.getProvider().equals(Provider.OAUTH))
+            .filter(s -> s.getType().equals("groupsfield"))
+            .anyMatch(subject -> convertGroups(principal.getAttribute(subject.getValue())).contains(role.getName()))
+        )
+        //subject.getValue()
+        .map(Role::getName)
+        .collect(Collectors.toSet());
+
+    return Mono.just(Stream.concat(groupsByUsername.stream(), groupsByGroupField.stream()).collect(Collectors.toSet()));
+  }
+
+  @SuppressWarnings("unchecked")
+  private Collection<String> convertGroups(Object groups) {
+    try {
+      if ((groups instanceof List<?>) || (groups instanceof Set<?>)) {
+        log.trace("The field is either a set or a list, returning as is");
+        return (Collection<String>) groups;
+      }
+
+      if (!(groups instanceof String)) {
+        log.debug("The field is not a string, skipping");
+        return Collections.emptySet();
+      }
+
+      log.trace("Trying to deserialize the field");
+      //@formatter:off
+      return new ObjectMapper().readValue((String) groups, new TypeReference<>() {});
+      //@formatter:on
+    } catch (Exception e) {
+      log.error("Error deserializing field", e);
+      return Collections.emptySet();
+    }
   }
   }
 
 
 }
 }

+ 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

@@ -224,7 +224,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);