Skip to content

Commit 56fa824

Browse files
committed
BE: RBAC: Implement an authorities extractor to support subject-level role matching (#3979)
Co-authored-by: Ilya Kuramshin <[email protected]> (cherry picked from commit b700ac3)
1 parent b0c0e06 commit 56fa824

File tree

3 files changed

+105
-10
lines changed

3 files changed

+105
-10
lines changed

kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapProperties.java

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public class LdapProperties {
1515
private String userFilterSearchBase;
1616
private String userFilterSearchFilter;
1717
private String groupFilterSearchBase;
18+
private String groupFilterSearchFilter;
19+
private String groupRoleAttribute;
1820

1921
@Value("${oauth2.ldap.activeDirectory:false}")
2022
private boolean isActiveDirectory;

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

+25-10
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
import static com.provectus.kafka.ui.config.auth.AbstractAuthSecurityConfig.AUTH_WHITELIST;
44

55
import com.provectus.kafka.ui.service.rbac.AccessControlService;
6+
import com.provectus.kafka.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor;
67
import java.util.Collection;
78
import java.util.List;
8-
import javax.annotation.Nullable;
9+
import java.util.Optional;
910
import lombok.RequiredArgsConstructor;
1011
import lombok.extern.slf4j.Slf4j;
1112
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1213
import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;
1314
import org.springframework.boot.context.properties.EnableConfigurationProperties;
15+
import org.springframework.context.ApplicationContext;
1416
import org.springframework.context.annotation.Bean;
1517
import org.springframework.context.annotation.Configuration;
1618
import org.springframework.context.annotation.Import;
@@ -50,9 +52,9 @@ public class LdapSecurityConfig {
5052

5153
@Bean
5254
public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource,
53-
LdapAuthoritiesPopulator ldapAuthoritiesPopulator,
54-
@Nullable AccessControlService acs) {
55-
var rbacEnabled = acs != null && acs.isRbacEnabled();
55+
LdapAuthoritiesPopulator authoritiesExtractor,
56+
AccessControlService acs) {
57+
var rbacEnabled = acs.isRbacEnabled();
5658
BindAuthenticator ba = new BindAuthenticator(contextSource);
5759
if (props.getBase() != null) {
5860
ba.setUserDnPatterns(new String[] {props.getBase()});
@@ -67,7 +69,7 @@ public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSo
6769
AbstractLdapAuthenticationProvider authenticationProvider;
6870
if (!props.isActiveDirectory()) {
6971
authenticationProvider = rbacEnabled
70-
? new LdapAuthenticationProvider(ba, ldapAuthoritiesPopulator)
72+
? new LdapAuthenticationProvider(ba, authoritiesExtractor)
7173
: new LdapAuthenticationProvider(ba);
7274
} else {
7375
authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(),
@@ -97,11 +99,24 @@ public BaseLdapPathContextSource contextSource() {
9799

98100
@Bean
99101
@Primary
100-
public LdapAuthoritiesPopulator ldapAuthoritiesPopulator(BaseLdapPathContextSource contextSource) {
101-
var authoritiesPopulator = new DefaultLdapAuthoritiesPopulator(contextSource, props.getGroupFilterSearchBase());
102-
authoritiesPopulator.setRolePrefix("");
103-
authoritiesPopulator.setConvertToUpperCase(false);
104-
return authoritiesPopulator;
102+
public DefaultLdapAuthoritiesPopulator ldapAuthoritiesExtractor(ApplicationContext context,
103+
BaseLdapPathContextSource contextSource,
104+
AccessControlService acs) {
105+
var rbacEnabled = acs != null && acs.isRbacEnabled();
106+
107+
DefaultLdapAuthoritiesPopulator extractor;
108+
109+
if (rbacEnabled) {
110+
extractor = new RbacLdapAuthoritiesExtractor(context, contextSource, props.getGroupFilterSearchBase());
111+
} else {
112+
extractor = new DefaultLdapAuthoritiesPopulator(contextSource, props.getGroupFilterSearchBase());
113+
}
114+
115+
Optional.ofNullable(props.getGroupFilterSearchFilter()).ifPresent(extractor::setGroupSearchFilter);
116+
extractor.setRolePrefix("");
117+
extractor.setConvertToUpperCase(false);
118+
extractor.setSearchSubtree(true);
119+
return extractor;
105120
}
106121

107122
@Bean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.provectus.kafka.ui.service.rbac.extractor;
2+
3+
import com.provectus.kafka.ui.config.auth.LdapProperties;
4+
import com.provectus.kafka.ui.model.rbac.Role;
5+
import com.provectus.kafka.ui.model.rbac.provider.Provider;
6+
import com.provectus.kafka.ui.service.rbac.AccessControlService;
7+
import java.util.List;
8+
import java.util.Map;
9+
import java.util.Set;
10+
import java.util.stream.Collectors;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.context.ApplicationContext;
13+
import org.springframework.ldap.core.DirContextOperations;
14+
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
15+
import org.springframework.security.core.GrantedAuthority;
16+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
17+
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
18+
import org.springframework.util.Assert;
19+
20+
@Slf4j
21+
public class RbacLdapAuthoritiesExtractor extends DefaultLdapAuthoritiesPopulator {
22+
23+
private final AccessControlService acs;
24+
private final LdapProperties props;
25+
26+
public RbacLdapAuthoritiesExtractor(ApplicationContext context,
27+
BaseLdapPathContextSource contextSource, String groupFilterSearchBase) {
28+
super(contextSource, groupFilterSearchBase);
29+
this.acs = context.getBean(AccessControlService.class);
30+
this.props = context.getBean(LdapProperties.class);
31+
}
32+
33+
@Override
34+
protected Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user, String username) {
35+
var ldapGroups = getRoles(user.getNameInNamespace(), username);
36+
37+
return acs.getRoles()
38+
.stream()
39+
.filter(r -> r.getSubjects()
40+
.stream()
41+
.filter(subject -> subject.getProvider().equals(Provider.LDAP))
42+
.filter(subject -> subject.getType().equals("group"))
43+
.anyMatch(subject -> ldapGroups.contains(subject.getValue()))
44+
)
45+
.map(Role::getName)
46+
.peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username))
47+
.map(SimpleGrantedAuthority::new)
48+
.collect(Collectors.toSet());
49+
}
50+
51+
private Set<String> getRoles(String userDn, String username) {
52+
var groupSearchBase = props.getGroupFilterSearchBase();
53+
Assert.notNull(groupSearchBase, "groupSearchBase is empty");
54+
55+
var groupRoleAttribute = props.getGroupRoleAttribute();
56+
if (groupRoleAttribute == null) {
57+
58+
groupRoleAttribute = "cn";
59+
}
60+
61+
log.trace(
62+
"Searching for roles for user [{}] with DN [{}], groupRoleAttribute [{}] and filter [{}] in search base [{}]",
63+
username, userDn, groupRoleAttribute, getGroupSearchFilter(), groupSearchBase);
64+
65+
var ldapTemplate = getLdapTemplate();
66+
ldapTemplate.setIgnoreNameNotFoundException(true);
67+
68+
Set<Map<String, List<String>>> userRoles = ldapTemplate.searchForMultipleAttributeValues(
69+
groupSearchBase, getGroupSearchFilter(), new String[] {userDn, username},
70+
new String[] {groupRoleAttribute});
71+
72+
return userRoles.stream()
73+
.map(record -> record.get(getGroupRoleAttribute()).get(0))
74+
.peek(group -> log.trace("Found LDAP group [{}] for user [{}]", group, username))
75+
.collect(Collectors.toSet());
76+
}
77+
78+
}

0 commit comments

Comments
 (0)