Skip to content

Commit

Permalink
BE: RBAC: Implement an authorities extractor to support subject-level…
Browse files Browse the repository at this point in the history
… role matching (#3979)

Co-authored-by: Ilya Kuramshin <iliax@proton.me>
(cherry picked from commit b700ac3)
  • Loading branch information
Haarolean committed Jun 27, 2023
1 parent b0c0e06 commit 56fa824
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public class LdapProperties {
private String userFilterSearchBase;
private String userFilterSearchFilter;
private String groupFilterSearchBase;
private String groupFilterSearchFilter;
private String groupRoleAttribute;

@Value("${oauth2.ldap.activeDirectory:false}")
private boolean isActiveDirectory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
import static com.provectus.kafka.ui.config.auth.AbstractAuthSecurityConfig.AUTH_WHITELIST;

import com.provectus.kafka.ui.service.rbac.AccessControlService;
import com.provectus.kafka.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor;
import java.util.Collection;
import java.util.List;
import javax.annotation.Nullable;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
Expand Down Expand Up @@ -50,9 +52,9 @@ public class LdapSecurityConfig {

@Bean
public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource,
LdapAuthoritiesPopulator ldapAuthoritiesPopulator,
@Nullable AccessControlService acs) {
var rbacEnabled = acs != null && acs.isRbacEnabled();
LdapAuthoritiesPopulator authoritiesExtractor,
AccessControlService acs) {
var rbacEnabled = acs.isRbacEnabled();
BindAuthenticator ba = new BindAuthenticator(contextSource);
if (props.getBase() != null) {
ba.setUserDnPatterns(new String[] {props.getBase()});
Expand All @@ -67,7 +69,7 @@ public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSo
AbstractLdapAuthenticationProvider authenticationProvider;
if (!props.isActiveDirectory()) {
authenticationProvider = rbacEnabled
? new LdapAuthenticationProvider(ba, ldapAuthoritiesPopulator)
? new LdapAuthenticationProvider(ba, authoritiesExtractor)
: new LdapAuthenticationProvider(ba);
} else {
authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(),
Expand Down Expand Up @@ -97,11 +99,24 @@ public BaseLdapPathContextSource contextSource() {

@Bean
@Primary
public LdapAuthoritiesPopulator ldapAuthoritiesPopulator(BaseLdapPathContextSource contextSource) {
var authoritiesPopulator = new DefaultLdapAuthoritiesPopulator(contextSource, props.getGroupFilterSearchBase());
authoritiesPopulator.setRolePrefix("");
authoritiesPopulator.setConvertToUpperCase(false);
return authoritiesPopulator;
public DefaultLdapAuthoritiesPopulator ldapAuthoritiesExtractor(ApplicationContext context,
BaseLdapPathContextSource contextSource,
AccessControlService acs) {
var rbacEnabled = acs != null && acs.isRbacEnabled();

DefaultLdapAuthoritiesPopulator extractor;

if (rbacEnabled) {
extractor = new RbacLdapAuthoritiesExtractor(context, contextSource, props.getGroupFilterSearchBase());
} else {
extractor = new DefaultLdapAuthoritiesPopulator(contextSource, props.getGroupFilterSearchBase());
}

Optional.ofNullable(props.getGroupFilterSearchFilter()).ifPresent(extractor::setGroupSearchFilter);
extractor.setRolePrefix("");
extractor.setConvertToUpperCase(false);
extractor.setSearchSubtree(true);
return extractor;
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.provectus.kafka.ui.service.rbac.extractor;

import com.provectus.kafka.ui.config.auth.LdapProperties;
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.List;
import java.util.Map;
import java.util.Set;
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.util.Assert;

@Slf4j
public class RbacLdapAuthoritiesExtractor extends DefaultLdapAuthoritiesPopulator {

private final AccessControlService acs;
private final LdapProperties props;

public RbacLdapAuthoritiesExtractor(ApplicationContext context,
BaseLdapPathContextSource contextSource, String groupFilterSearchBase) {
super(contextSource, groupFilterSearchBase);
this.acs = context.getBean(AccessControlService.class);
this.props = context.getBean(LdapProperties.class);
}

@Override
protected Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user, String username) {
var ldapGroups = getRoles(user.getNameInNamespace(), username);

return acs.getRoles()
.stream()
.filter(r -> r.getSubjects()
.stream()
.filter(subject -> subject.getProvider().equals(Provider.LDAP))
.filter(subject -> subject.getType().equals("group"))
.anyMatch(subject -> ldapGroups.contains(subject.getValue()))
)
.map(Role::getName)
.peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
}

private Set<String> getRoles(String userDn, String username) {
var groupSearchBase = props.getGroupFilterSearchBase();
Assert.notNull(groupSearchBase, "groupSearchBase is empty");

var groupRoleAttribute = props.getGroupRoleAttribute();
if (groupRoleAttribute == null) {

groupRoleAttribute = "cn";
}

log.trace(
"Searching for roles for user [{}] with DN [{}], groupRoleAttribute [{}] and filter [{}] in search base [{}]",
username, userDn, groupRoleAttribute, getGroupSearchFilter(), groupSearchBase);

var ldapTemplate = getLdapTemplate();
ldapTemplate.setIgnoreNameNotFoundException(true);

Set<Map<String, List<String>>> userRoles = ldapTemplate.searchForMultipleAttributeValues(
groupSearchBase, getGroupSearchFilter(), new String[] {userDn, username},
new String[] {groupRoleAttribute});

return userRoles.stream()
.map(record -> record.get(getGroupRoleAttribute()).get(0))
.peek(group -> log.trace("Found LDAP group [{}] for user [{}]", group, username))
.collect(Collectors.toSet());
}

}

0 comments on commit 56fa824

Please sign in to comment.