浏览代码

Implement LDAP support for RBAC

Roman Zabaluev 2 年之前
父节点
当前提交
2c902aa6c5

+ 54 - 3
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java

@@ -1,13 +1,18 @@
 package com.provectus.kafka.ui.config.auth;
 
+import com.provectus.kafka.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor;
+import java.util.Collection;
 import java.util.List;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;
+import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Primary;
+import org.springframework.ldap.core.DirContextOperations;
 import org.springframework.ldap.core.support.BaseLdapPathContextSource;
 import org.springframework.ldap.core.support.LdapContextSource;
 import org.springframework.security.authentication.AuthenticationManager;
@@ -16,12 +21,17 @@ import org.springframework.security.authentication.ReactiveAuthenticationManager
 import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
 import org.springframework.security.ldap.authentication.BindAuthenticator;
 import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
 import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
 import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
 import org.springframework.security.ldap.search.LdapUserSearch;
+import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
+import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
+import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 
 @Configuration
@@ -31,7 +41,7 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
 @Slf4j
 public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
 
-  @Value("${spring.ldap.urls}")
+  @Value("${spring.ldap.urls}") // TODO properties
   private String ldapUrls;
   @Value("${spring.ldap.dn.pattern:#{null}}")
   private String ldapUserDnPattern;
@@ -50,7 +60,8 @@ public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
   private String activeDirectoryDomain;
 
   @Bean
-  public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) {
+  public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource,
+                                                             ApplicationContext context) {
     BindAuthenticator ba = new BindAuthenticator(contextSource);
     if (ldapUserDnPattern != null) {
       ba.setUserDnPatterns(new String[] {ldapUserDnPattern});
@@ -63,7 +74,8 @@ public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
 
     AbstractLdapAuthenticationProvider authenticationProvider;
     if (!isActiveDirectory) {
-      authenticationProvider = new LdapAuthenticationProvider(ba);
+      authenticationProvider = new LdapAuthenticationProvider(ba, authoritiesPopulator(context));
+      authenticationProvider.setUserDetailsContextMapper(userDetailsContextMapper());
     } else {
       authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(activeDirectoryDomain, ldapUrls);
       authenticationProvider.setUseAuthenticationRequestCredentials(true);
@@ -75,6 +87,7 @@ public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
   }
 
   @Bean
+  @Primary
   public BaseLdapPathContextSource contextSource() {
     LdapContextSource ctx = new LdapContextSource();
     ctx.setUrl(ldapUrls);
@@ -109,5 +122,43 @@ public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
         .build();
   }
 
+  @Bean
+  public UserDetailsContextMapper userDetailsContextMapper() {
+    return new LdapUserDetailsMapper() {
+      @Override
+      public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> authorities) {
+        UserDetails userDetails = super.mapUserFromContext(ctx, username, authorities);
+        return new RbacLdapUser(userDetails);
+      }
+    };
+  }
+
+  @Bean
+  LdapAuthoritiesPopulator authoritiesPopulator(ApplicationContext context) {
+    RbacLdapAuthoritiesExtractor extractor = new RbacLdapAuthoritiesExtractor(context);
+    extractor.setRolePrefix("");
+    return extractor;
+  }
+
+/*  @Autowired
+  public void configure(AuthenticationManagerBuilder auth) throws Exception {
+    var a = auth
+        .ldapAuthentication()
+        .userDnPatterns("uid={0},ou=people")
+        .groupSearchBase("ou=groups")
+        .contextSource()
+        .url()
+        .managerDn()
+        .managerPassword()
+        .and()
+//        .passwordCompare()
+//        .passwordEncoder(new BCryptPasswordEncoder())
+//        .passwordAttribute("userPassword");
+    ;
+    if (isActiveDirectory) {
+      a.authenticationProvider()
+    }
+  }*/
+
 }
 

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

@@ -115,7 +115,7 @@ public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {
   @Nullable
   private ProviderAuthorityExtractor getExtractor(final String providerId, AccessControlService acs) {
     final String provider = getProviderByProviderId(providerId);
-    Optional<ProviderAuthorityExtractor> extractor = acs.getExtractors()
+    Optional<ProviderAuthorityExtractor> extractor = acs.getOauthExtractors()
         .stream()
         .filter(e -> e.isApplicable(provider))
         .findFirst();

+ 60 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacLdapUser.java

@@ -0,0 +1,60 @@
+package com.provectus.kafka.ui.config.auth;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+public class RbacLdapUser implements UserDetails, RbacUser {
+
+  private final UserDetails userDetails;
+
+  public RbacLdapUser(UserDetails userDetails) {
+    this.userDetails = userDetails;
+  }
+
+  @Override
+  public String name() {
+    return userDetails.getUsername();
+  }
+
+  @Override
+  public Collection<String> groups() {
+    return userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
+  }
+
+  @Override
+  public Collection<? extends GrantedAuthority> getAuthorities() {
+    return userDetails.getAuthorities();
+  }
+
+  @Override
+  public String getPassword() {
+    return userDetails.getPassword();
+  }
+
+  @Override
+  public String getUsername() {
+    return userDetails.getUsername();
+  }
+
+  @Override
+  public boolean isAccountNonExpired() {
+    return userDetails.isAccountNonExpired();
+  }
+
+  @Override
+  public boolean isAccountNonLocked() {
+    return userDetails.isAccountNonLocked();
+  }
+
+  @Override
+  public boolean isCredentialsNonExpired() {
+    return userDetails.isCredentialsNonExpired();
+  }
+
+  @Override
+  public boolean isEnabled() {
+    return userDetails.isEnabled();
+  }
+}

+ 13 - 9
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java

@@ -18,10 +18,10 @@ 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.GithubAuthorityExtractor;
 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.ProviderAuthorityExtractor;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.regex.Pattern;
@@ -33,6 +33,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.core.env.Environment;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.core.context.ReactiveSecurityContextHolder;
 import org.springframework.security.core.context.SecurityContext;
@@ -49,10 +50,11 @@ public class AccessControlService {
 
   @Nullable
   private final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository;
+  private final RoleBasedAccessControlProperties properties;
+  private final Environment environment;
 
   private boolean rbacEnabled = false;
-  private Set<ProviderAuthorityExtractor> extractors = Collections.emptySet();
-  private final RoleBasedAccessControlProperties properties;
+  private Set<ProviderAuthorityExtractor> oauthExtractors = Collections.emptySet();
 
   @PostConstruct
   public void init() {
@@ -62,7 +64,7 @@ public class AccessControlService {
     }
     rbacEnabled = true;
 
-    this.extractors = properties.getRoles()
+    this.oauthExtractors = properties.getRoles()
         .stream()
         .map(role -> role.getSubjects()
             .stream()
@@ -72,14 +74,16 @@ public class AccessControlService {
               case OAUTH_COGNITO -> new CognitoAuthorityExtractor();
               case OAUTH_GOOGLE -> new GoogleAuthorityExtractor();
               case OAUTH_GITHUB -> new GithubAuthorityExtractor();
-              case LDAP, LDAP_AD -> new LdapAuthorityExtractor(ldapTemplate); // TODO do we need a separate one for AD?
+              default -> null;
             })
+            .filter(Objects::nonNull)
             .collect(Collectors.toSet()))
         .flatMap(Set::stream)
         .collect(Collectors.toSet());
 
-    if ((clientRegistrationRepository == null || !clientRegistrationRepository.iterator().hasNext())
-        && !properties.getRoles().isEmpty()) {
+    if (!properties.getRoles().isEmpty()
+        && "oauth2".equalsIgnoreCase(environment.getProperty("auth.type"))
+        && (clientRegistrationRepository == null || !clientRegistrationRepository.iterator().hasNext())) {
       log.error("Roles are configured but no authentication methods are present. Authentication might fail.");
     }
   }
@@ -341,8 +345,8 @@ public class AccessControlService {
     return isAccessible(Resource.KSQL, null, user, context, requiredActions);
   }
 
-  public Set<ProviderAuthorityExtractor> getExtractors() {
-    return extractors;
+  public Set<ProviderAuthorityExtractor> getOauthExtractors() {
+    return oauthExtractors;
   }
 
   public List<Role> getRoles() {

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

@@ -1,23 +0,0 @@
-package com.provectus.kafka.ui.service.rbac.extractor;
-
-import com.provectus.kafka.ui.service.rbac.AccessControlService;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Set;
-import lombok.extern.slf4j.Slf4j;
-import reactor.core.publisher.Mono;
-
-@Slf4j
-public class LdapAuthorityExtractor implements ProviderAuthorityExtractor {
-
-  @Override
-  public boolean isApplicable(String provider) {
-    return false; // TODO #2752
-  }
-
-  @Override
-  public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {
-    return Mono.just(Collections.emptySet()); // TODO #2752
-  }
-
-}

+ 67 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java

@@ -0,0 +1,67 @@
+package com.provectus.kafka.ui.service.rbac.extractor;
+
+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 java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationContext;
+import org.springframework.ldap.core.DirContextOperations;
+import org.springframework.ldap.core.support.BaseLdapPathContextSource;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
+import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
+
+@Slf4j
+public class RbacLdapAuthoritiesExtractor extends DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator {
+
+  private final AccessControlService acs;
+
+  private final Function<Map<String, List<String>>, GrantedAuthority> authorityMapper = (record) -> {
+    String role = record.get(getGroupRoleAttribute()).get(0);
+    return new SimpleGrantedAuthority(getRolePrefix() + role);
+  };
+
+  public RbacLdapAuthoritiesExtractor(ApplicationContext context) {
+    super(context.getBean(BaseLdapPathContextSource.class), null);
+    this.acs = context.getBean(AccessControlService.class);
+  }
+
+  @Override
+  public Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user, String username) {
+    return acs.getRoles()
+        .stream()
+        .map(Role::getSubjects)
+        .flatMap(List::stream)
+        .filter(s -> s.getProvider().equals(Provider.LDAP))
+        .filter(s -> s.getType().equals("group"))
+        .flatMap(subject -> getRoles(subject.getValue(), user.getNameInNamespace(), username).stream())
+        .collect(Collectors.toSet());
+  }
+
+  private Set<GrantedAuthority> getRoles(String search, String userDn, String username) {
+    if (search == null) {
+      return new HashSet<>();
+    }
+
+    log.trace("Searching for roles for user [{}] with DN [{}] and filter [{}] in search base [{}]",
+        username, userDn, getGroupSearchFilter(), search);
+
+    Set<Map<String, List<String>>> userRoles = getLdapTemplate().searchForMultipleAttributeValues(
+        search, getGroupSearchFilter(), new String[] {userDn, username},
+        new String[] {getGroupRoleAttribute()});
+
+    log.debug("Found roles from search [{}]", userRoles);
+
+    return userRoles.stream()
+        .map(authorityMapper)
+        .collect(Collectors.toSet());
+  }
+
+}