Skip to content

Commit

Permalink
BE: RBAC: Impl Active Directory populator (#717)
Browse files Browse the repository at this point in the history
+ BE: RBAC: LDAP: Implement user subject type for LDAP & AD. Resolves #54, resolves #730
  • Loading branch information
Haarolean authored Dec 31, 2024
1 parent d093752 commit 9f79a56
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 42 deletions.
93 changes: 55 additions & 38 deletions api/src/main/java/io/kafbat/ui/config/auth/LdapSecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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;
import java.util.List;
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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -49,39 +51,51 @@ 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(),
ldapContextSource);
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
Expand All @@ -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
Expand Down Expand Up @@ -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<? extends GrantedAuthority> authorities) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ private Mono<ReactiveAdminClient> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<? extends GrantedAuthority> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -37,8 +38,11 @@ protected Set<GrantedAuthority> 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))
Expand Down

0 comments on commit 9f79a56

Please sign in to comment.