Skip to content

Commit 9ec5d75

Browse files
fix(ldap): Allow to use any ldap attribute as user id in batch
roles/sync
1 parent 129324e commit 9ec5d75

File tree

3 files changed

+73
-74
lines changed

3 files changed

+73
-74
lines changed

fiat-ldap/src/main/java/com/netflix/spinnaker/fiat/config/LdapConfig.java

+1-3
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,8 @@ public static class ConfigProps {
5555
MessageFormat userDnPattern = new MessageFormat("uid={0},ou=users");
5656
String userSearchBase = "";
5757
String userSearchFilter;
58+
String userIdAttribute;
5859
String groupSearchFilter = "(uniqueMember={0})";
5960
String groupRoleAttributes = "cn";
60-
String groupUserAttributes = "";
61-
62-
int thresholdToUseGroupMembership = 100;
6361
}
6462
}

fiat-ldap/src/main/java/com/netflix/spinnaker/fiat/roles/ldap/LdapUserRolesProvider.java

+46-42
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,11 @@
2121
import com.netflix.spinnaker.fiat.permissions.ExternalUser;
2222
import com.netflix.spinnaker.fiat.roles.UserRolesProvider;
2323
import java.text.MessageFormat;
24-
import java.text.ParseException;
2524
import java.util.*;
25+
import java.util.function.Function;
2626
import java.util.stream.Collectors;
2727
import javax.naming.InvalidNameException;
2828
import javax.naming.Name;
29-
import javax.naming.NamingEnumeration;
3029
import javax.naming.NamingException;
3130
import javax.naming.directory.Attributes;
3231
import lombok.Setter;
@@ -100,22 +99,28 @@ public List<Role> loadRoles(ExternalUser user) {
10099
.collect(Collectors.toList());
101100
}
102101

103-
private class UserGroupMapper implements AttributesMapper<List<Pair<String, Role>>> {
104-
public List<Pair<String, Role>> mapFromAttributes(Attributes attrs) throws NamingException {
105-
String group = attrs.get(configProps.getGroupRoleAttributes()).get().toString();
106-
Role role = new Role(group).setSource(Role.Source.LDAP);
107-
List<Pair<String, Role>> member = new ArrayList<>();
108-
for (NamingEnumeration<?> members = attrs.get(configProps.getGroupUserAttributes()).getAll();
109-
members.hasMore(); ) {
110-
try {
111-
String user =
112-
String.valueOf(configProps.getUserDnPattern().parse(members.next().toString())[0]);
113-
member.add(Pair.of(user, role));
114-
} catch (ParseException e) {
115-
e.printStackTrace();
116-
}
117-
}
118-
return member;
102+
private class RoleFullDNtoUserRoleMapper implements AttributesMapper<Pair<String, Role>> {
103+
@Override
104+
public Pair<String, Role> mapFromAttributes(Attributes attrs) throws NamingException {
105+
return Pair.of(
106+
attrs.get("distinguishedname").get().toString(),
107+
new Role(attrs.get(configProps.getGroupRoleAttributes()).get().toString())
108+
.setSource(Role.Source.LDAP));
109+
}
110+
}
111+
112+
private class UserGroupMapper implements AttributesMapper<Pair<String, Role>> {
113+
114+
private Role role;
115+
116+
public UserGroupMapper(Role role) {
117+
this.role = role;
118+
}
119+
120+
@Override
121+
public Pair<String, Role> mapFromAttributes(Attributes attrs) throws NamingException {
122+
return Pair.of(
123+
attrs.get(configProps.getUserIdAttribute()).get().toString().toLowerCase(), role);
119124
}
120125
}
121126

@@ -125,30 +130,29 @@ public Map<String, Collection<Role>> multiLoadRoles(Collection<ExternalUser> use
125130
return new HashMap<>();
126131
}
127132

128-
if (users.size() > configProps.getThresholdToUseGroupMembership()
129-
&& StringUtils.isNotEmpty(configProps.getGroupUserAttributes())) {
130-
Set<String> userIds = users.stream().map(ExternalUser::getId).collect(Collectors.toSet());
131-
return ldapTemplate
132-
.search(
133-
configProps.getGroupSearchBase(),
134-
MessageFormat.format(
135-
configProps.getGroupSearchFilter(),
136-
"*",
137-
"*"), // Passing two wildcard params like loadRoles
138-
new UserGroupMapper())
139-
.stream()
140-
.flatMap(List::stream)
141-
.filter(p -> userIds.contains(p.getKey()))
142-
.collect(
143-
Collectors.groupingBy(
144-
Pair::getKey,
145-
Collectors.mapping(Pair::getValue, Collectors.toCollection(ArrayList::new))));
146-
}
147-
148-
// ExternalUser is used here as a simple data type to hold the username/roles combination.
149-
return users.stream()
150-
.map(u -> new ExternalUser().setId(u.getId()).setExternalRoles(loadRoles(u)))
151-
.collect(Collectors.toMap(ExternalUser::getId, ExternalUser::getExternalRoles));
133+
Map<String, String> userIds =
134+
users.stream()
135+
.map(ExternalUser::getId)
136+
.collect(Collectors.toMap(String::toLowerCase, Function.identity()));
137+
return ldapTemplate
138+
.search(
139+
configProps.getGroupSearchBase(),
140+
MessageFormat.format(configProps.getGroupSearchFilter(), "*", "*"),
141+
new RoleFullDNtoUserRoleMapper())
142+
.stream()
143+
.map(
144+
r ->
145+
ldapTemplate.search(
146+
configProps.getUserSearchBase(),
147+
String.format("(&(objectCategory=user)(memberOf=%s))", r.getKey()),
148+
new UserGroupMapper(r.getValue())))
149+
.flatMap(List::stream)
150+
.filter(p -> userIds.containsKey(p.getKey()))
151+
.map(p -> Pair.of(userIds.get(p.getKey()), p.getValue()))
152+
.collect(
153+
Collectors.groupingBy(
154+
Pair::getKey,
155+
Collectors.mapping(Pair::getValue, Collectors.toCollection(ArrayList::new))));
152156
}
153157

154158
private String getUserFullDn(String userId) {

fiat-ldap/src/test/groovy/com/netflix/spinnaker/fiat/roles/ldap/LdapUserRolesProviderTest.groovy

+26-29
Original file line numberDiff line numberDiff line change
@@ -86,59 +86,56 @@ class LdapUserRolesProviderTest extends Specification {
8686
"notEmpty" |_
8787
}
8888

89-
void "multiLoadRoles should use loadRoles when groupUserAttributes is empty"() {
89+
void "multiLoadRoles should call ldap only N+1 times for groups and users in each group"() {
9090
given:
9191
def users = [externalUser("user1"), externalUser("user2")]
9292
def role1 = new Role("group1")
9393
def role2 = new Role("group2")
9494

9595
def configProps = baseConfigProps()
96-
def provider = Spy(LdapUserRolesProvider){
97-
loadRoles(_ as ExternalUser) >>> [[role1], [role2]]
98-
}.setConfigProps(configProps)
96+
configProps.groupSearchBase = "group search base"
97+
configProps.userSearchBase = "user search base"
98+
configProps.userIdAttribute = "sAMAccountName"
9999

100-
when:
101-
configProps.groupSearchBase = ""
102-
def roles = provider.multiLoadRoles(users)
100+
provider.configProps = configProps
103101

104-
then:
105-
roles == [:]
102+
provider.ldapTemplate = Mock(SpringSecurityLdapTemplate) {
103+
1 * search(configProps.groupSearchBase, *_) >> [Pair.of("group1 dn", role1) , Pair.of("group2 dn", role2)]
104+
1 * search(configProps.userSearchBase, { it.contains("group1 dn") }, _) >> [Pair.of("user1", role1)]
105+
1 * search(configProps.userSearchBase, { it.contains("group2 dn") }, _) >> [Pair.of("user2", role2)]
106+
}
106107

107108
when:
108-
configProps.groupSearchBase = "notEmpty"
109-
roles = provider.multiLoadRoles(users)
109+
def roles = provider.multiLoadRoles(users)
110110

111111
then:
112112
roles == [user1: [role1], user2: [role2]]
113113
}
114114

115-
void "multiLoadRoles should use groupUserAttributes when groupUserAttributes is not empty"() {
115+
void "multiLoadRoles should use provided user ids because ldap filters can be case insensitive"() {
116116
given:
117-
def users = [externalUser("user1"), externalUser("user2")]
117+
def users = [externalUser("User1"), externalUser("User2")]
118118
def role1 = new Role("group1")
119119
def role2 = new Role("group2")
120120

121-
def configProps = baseConfigProps().setGroupSearchBase("notEmpty").setGroupUserAttributes("member")
122-
def provider = Spy(LdapUserRolesProvider){
123-
2 * loadRoles(_) >>> [[role1], [role2]]
124-
}.setConfigProps(configProps)
125-
126-
when: "thresholdToUseGroupMembership is too high"
127-
configProps.thresholdToUseGroupMembership = 100
128-
def roles = provider.multiLoadRoles(users)
121+
def configProps = baseConfigProps()
122+
configProps.groupSearchBase = "group search base"
123+
configProps.userSearchBase = "user search base"
124+
configProps.userIdAttribute = "sAMAccountName"
129125

130-
then: "should use loadRoles"
131-
roles == [user1: [role1], user2: [role2]]
126+
provider.configProps = configProps
132127

133-
when: "users count is greater than thresholdToUseGroupMembership"
134-
configProps.thresholdToUseGroupMembership = 1
135128
provider.ldapTemplate = Mock(SpringSecurityLdapTemplate) {
136-
1 * search(*_) >> [[Pair.of("user1",role1)], [Pair.of("user2", role2)], [Pair.of("unknown", role2)]]
129+
1 * search(configProps.groupSearchBase, *_) >> [Pair.of("group1 dn", role1) , Pair.of("group2 dn", role2)]
130+
1 * search(configProps.userSearchBase, { it.contains("group1 dn") }, _) >> [Pair.of("user1", role1)]
131+
1 * search(configProps.userSearchBase, { it.contains("group2 dn") }, _) >> [Pair.of("user2", role2)]
137132
}
138-
roles = provider.multiLoadRoles(users)
139133

140-
then: "should use ldapTemplate.search method"
141-
roles == [user1: [role1], user2: [role2]]
134+
when:
135+
def roles = provider.multiLoadRoles(users)
136+
137+
then:
138+
roles == [User1: [role1], User2: [role2]]
142139
}
143140

144141
private static ExternalUser externalUser(String id) {

0 commit comments

Comments
 (0)