From 9f79a56d42f88feae7f1d3b020b598427d3ea1b2 Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Tue, 31 Dec 2024 08:55:25 +0400 Subject: [PATCH] BE: RBAC: Impl Active Directory populator (#717) + BE: RBAC: LDAP: Implement user subject type for LDAP & AD. Resolves #54, resolves #730 --- .../ui/config/auth/LdapSecurityConfig.java | 93 +++++++++++-------- .../ui/service/AdminClientServiceImpl.java | 2 +- ...acActiveDirectoryAuthoritiesExtractor.java | 50 ++++++++++ .../RbacLdapAuthoritiesExtractor.java | 10 +- 4 files changed, 113 insertions(+), 42 deletions(-) create mode 100644 api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java diff --git a/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java b/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java index 4d89a9568..9b1445507 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java @@ -1,6 +1,7 @@ package io.kafbat.ui.config.auth; import io.kafbat.ui.service.rbac.AccessControlService; +import io.kafbat.ui.service.rbac.extractor.RbacActiveDirectoryAuthoritiesExtractor; import io.kafbat.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor; import io.kafbat.ui.util.StaticFileWebFilter; import java.util.Collection; @@ -8,6 +9,7 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; @@ -17,7 +19,6 @@ 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; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter; @@ -29,10 +30,11 @@ 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.NullLdapAuthoritiesPopulator; import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider; +import org.springframework.security.ldap.authentication.ad.DefaultActiveDirectoryAuthoritiesPopulator; import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; import org.springframework.security.ldap.search.LdapUserSearch; -import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; import org.springframework.security.web.server.SecurityWebFilterChain; @@ -49,14 +51,43 @@ public class LdapSecurityConfig extends AbstractAuthSecurityConfig { private final LdapProperties props; @Bean - public ReactiveAuthenticationManager authenticationManager(LdapContextSource ldapContextSource, - LdapAuthoritiesPopulator authoritiesExtractor, - AccessControlService acs) { + public ReactiveAuthenticationManager authenticationManager(AbstractLdapAuthenticationProvider authProvider) { + return new ReactiveAuthenticationManagerAdapter(new ProviderManager(List.of(authProvider))); + } + + @Bean + public AbstractLdapAuthenticationProvider authenticationProvider(LdapAuthoritiesPopulator authoritiesExtractor, + @Autowired(required = false) BindAuthenticator ba, + AccessControlService acs) { var rbacEnabled = acs.isRbacEnabled(); + + AbstractLdapAuthenticationProvider authProvider; + + if (!props.isActiveDirectory()) { + authProvider = new LdapAuthenticationProvider(ba, authoritiesExtractor); + } else { + authProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(), + props.getUrls()); + authProvider.setUseAuthenticationRequestCredentials(true); + ((ActiveDirectoryLdapAuthenticationProvider) authProvider).setAuthoritiesPopulator(authoritiesExtractor); + } + + if (rbacEnabled) { + authProvider.setUserDetailsContextMapper(new RbacUserDetailsMapper()); + } + + return authProvider; + } + + @Bean + @ConditionalOnProperty(value = "oauth2.ldap.activeDirectory", havingValue = "false") + public BindAuthenticator ldapBindAuthentication(LdapContextSource ldapContextSource) { BindAuthenticator ba = new BindAuthenticator(ldapContextSource); + if (props.getBase() != null) { ba.setUserDnPatterns(new String[] {props.getBase()}); } + if (props.getUserFilterSearchFilter() != null) { LdapUserSearch userSearch = new FilterBasedLdapUserSearch(props.getUserFilterSearchBase(), props.getUserFilterSearchFilter(), @@ -64,24 +95,7 @@ public ReactiveAuthenticationManager authenticationManager(LdapContextSource lda ba.setUserSearch(userSearch); } - AbstractLdapAuthenticationProvider authenticationProvider; - if (!props.isActiveDirectory()) { - authenticationProvider = rbacEnabled - ? new LdapAuthenticationProvider(ba, authoritiesExtractor) - : new LdapAuthenticationProvider(ba); - } else { - authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(), - props.getUrls()); // TODO Issue #3741 - authenticationProvider.setUseAuthenticationRequestCredentials(true); - } - - if (rbacEnabled) { - authenticationProvider.setUserDetailsContextMapper(new UserDetailsMapper()); - } - - AuthenticationManager am = new ProviderManager(List.of(authenticationProvider)); - - return new ReactiveAuthenticationManagerAdapter(am); + return ba; } @Bean @@ -95,24 +109,27 @@ public LdapContextSource ldapContextSource() { } @Bean - public DefaultLdapAuthoritiesPopulator ldapAuthoritiesExtractor(ApplicationContext context, - BaseLdapPathContextSource contextSource, - AccessControlService acs) { - var rbacEnabled = acs != null && acs.isRbacEnabled(); + public LdapAuthoritiesPopulator authoritiesExtractor(ApplicationContext ctx, + BaseLdapPathContextSource ldapCtx, + AccessControlService acs) { + if (!props.isActiveDirectory()) { + if (!acs.isRbacEnabled()) { + return new NullLdapAuthoritiesPopulator(); + } - DefaultLdapAuthoritiesPopulator extractor; + var extractor = new RbacLdapAuthoritiesExtractor(ctx, ldapCtx, props.getGroupFilterSearchBase()); - if (rbacEnabled) { - extractor = new RbacLdapAuthoritiesExtractor(context, contextSource, props.getGroupFilterSearchBase()); + Optional.ofNullable(props.getGroupFilterSearchFilter()).ifPresent(extractor::setGroupSearchFilter); + extractor.setRolePrefix(""); + extractor.setConvertToUpperCase(false); + extractor.setSearchSubtree(true); + + return extractor; } else { - extractor = new DefaultLdapAuthoritiesPopulator(contextSource, props.getGroupFilterSearchBase()); + return acs.isRbacEnabled() + ? new RbacActiveDirectoryAuthoritiesExtractor(ctx) + : new DefaultActiveDirectoryAuthoritiesPopulator(); } - - Optional.ofNullable(props.getGroupFilterSearchFilter()).ifPresent(extractor::setGroupSearchFilter); - extractor.setRolePrefix(""); - extractor.setConvertToUpperCase(false); - extractor.setSearchSubtree(true); - return extractor; } @Bean @@ -142,7 +159,7 @@ public SecurityWebFilterChain configureLdap(ServerHttpSecurity http) { return builder.build(); } - private static class UserDetailsMapper extends LdapUserDetailsMapper { + private static class RbacUserDetailsMapper extends LdapUserDetailsMapper { @Override public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection authorities) { diff --git a/api/src/main/java/io/kafbat/ui/service/AdminClientServiceImpl.java b/api/src/main/java/io/kafbat/ui/service/AdminClientServiceImpl.java index e3613c94e..6c018ba31 100644 --- a/api/src/main/java/io/kafbat/ui/service/AdminClientServiceImpl.java +++ b/api/src/main/java/io/kafbat/ui/service/AdminClientServiceImpl.java @@ -53,7 +53,7 @@ private Mono createAdminClient(KafkaCluster cluster) { return AdminClient.create(properties); }).flatMap(ac -> ReactiveAdminClient.create(ac).doOnError(th -> ac.close())) .onErrorMap(th -> new IllegalStateException( - "Error while creating AdminClient for Cluster " + cluster.getName(), th)); + "Error while creating AdminClient for the cluster " + cluster.getName(), th)); } @Override diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java new file mode 100644 index 000000000..cefef5a7e --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java @@ -0,0 +1,50 @@ +package io.kafbat.ui.service.rbac.extractor; + +import io.kafbat.ui.model.rbac.Role; +import io.kafbat.ui.model.rbac.provider.Provider; +import io.kafbat.ui.service.rbac.AccessControlService; +import java.util.Collection; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.ldap.authentication.ad.DefaultActiveDirectoryAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; + +@Slf4j +public class RbacActiveDirectoryAuthoritiesExtractor implements LdapAuthoritiesPopulator { + + private final DefaultActiveDirectoryAuthoritiesPopulator populator = new DefaultActiveDirectoryAuthoritiesPopulator(); + private final AccessControlService acs; + + public RbacActiveDirectoryAuthoritiesExtractor(ApplicationContext context) { + this.acs = context.getBean(AccessControlService.class); + } + + @Override + public Collection getGrantedAuthorities(DirContextOperations userData, String username) { + var adGroups = populator.getGrantedAuthorities(userData, username) + .stream() + .map(GrantedAuthority::getAuthority) + .peek(group -> log.trace("Found AD group [{}] for user [{}]", group, username)) + .collect(Collectors.toSet()); + + return acs.getRoles() + .stream() + .filter(r -> r.getSubjects() + .stream() + .filter(subject -> subject.getProvider().equals(Provider.LDAP_AD)) + .anyMatch(subject -> switch (subject.getType()) { + case "user" -> username.equalsIgnoreCase(subject.getValue()); + case "group" -> adGroups.contains(subject.getValue()); + default -> false; + }) + ) + .map(Role::getName) + .peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username)) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + } +} diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java index 3282ab1e2..261b30cfe 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java @@ -19,7 +19,8 @@ public class RbacLdapAuthoritiesExtractor extends NestedLdapAuthoritiesPopulator private final AccessControlService acs; public RbacLdapAuthoritiesExtractor(ApplicationContext context, - BaseLdapPathContextSource contextSource, String groupFilterSearchBase) { + BaseLdapPathContextSource contextSource, + String groupFilterSearchBase) { super(contextSource, groupFilterSearchBase); this.acs = context.getBean(AccessControlService.class); } @@ -37,8 +38,11 @@ protected Set getAdditionalRoles(DirContextOperations user, St .filter(r -> r.getSubjects() .stream() .filter(subject -> subject.getProvider().equals(Provider.LDAP)) - .filter(subject -> subject.getType().equals("group")) - .anyMatch(subject -> ldapGroups.contains(subject.getValue())) + .anyMatch(subject -> switch (subject.getType()) { + case "user" -> username.equalsIgnoreCase(subject.getValue()); + case "group" -> ldapGroups.contains(subject.getValue()); + default -> false; + }) ) .map(Role::getName) .peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username))