Skip to content

Commit 63c9743

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

File tree

3 files changed

+52
-86
lines changed

3 files changed

+52
-86
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

+40-43
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,10 @@
2020
import com.netflix.spinnaker.fiat.model.resources.Role;
2121
import com.netflix.spinnaker.fiat.permissions.ExternalUser;
2222
import com.netflix.spinnaker.fiat.roles.UserRolesProvider;
23-
import java.text.MessageFormat;
24-
import java.text.ParseException;
2523
import java.util.*;
2624
import java.util.stream.Collectors;
2725
import javax.naming.InvalidNameException;
2826
import javax.naming.Name;
29-
import javax.naming.NamingEnumeration;
3027
import javax.naming.NamingException;
3128
import javax.naming.directory.Attributes;
3229
import lombok.Setter;
@@ -100,22 +97,27 @@ public List<Role> loadRoles(ExternalUser user) {
10097
.collect(Collectors.toList());
10198
}
10299

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;
100+
private class RoleFullDNtoUserRoleMapper implements AttributesMapper<Pair<String, Role>> {
101+
@Override
102+
public Pair<String, Role> mapFromAttributes(Attributes attrs) throws NamingException {
103+
return Pair.of(
104+
attrs.get("distinguishedname").get().toString(),
105+
new Role(attrs.get(configProps.getGroupRoleAttributes()).get().toString())
106+
.setSource(Role.Source.LDAP));
107+
}
108+
}
109+
110+
private class UserGroupMapper implements AttributesMapper<Pair<String, Role>> {
111+
112+
private Role role;
113+
114+
public UserGroupMapper(Role role) {
115+
this.role = role;
116+
}
117+
118+
@Override
119+
public Pair<String, Role> mapFromAttributes(Attributes attrs) throws NamingException {
120+
return Pair.of(attrs.get(configProps.getUserIdAttribute()).get().toString(), role);
119121
}
120122
}
121123

@@ -125,30 +127,25 @@ public Map<String, Collection<Role>> multiLoadRoles(Collection<ExternalUser> use
125127
return new HashMap<>();
126128
}
127129

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));
130+
Set<String> userIds = users.stream().map(ExternalUser::getId).collect(Collectors.toSet());
131+
return ldapTemplate
132+
.search(
133+
configProps.getGroupSearchBase(),
134+
configProps.getGroupSearchFilter(),
135+
new RoleFullDNtoUserRoleMapper())
136+
.stream()
137+
.map(
138+
r ->
139+
ldapTemplate.search(
140+
configProps.getUserSearchBase(),
141+
String.format("(&(objectCategory=user)(memberOf=%s))", r.getKey()),
142+
new UserGroupMapper(r.getValue())))
143+
.flatMap(List::stream)
144+
.filter(p -> userIds.contains(p.getKey()))
145+
.collect(
146+
Collectors.groupingBy(
147+
Pair::getKey,
148+
Collectors.mapping(Pair::getValue, Collectors.toCollection(ArrayList::new))));
152149
}
153150

154151
private String getUserFullDn(String userId) {

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

+11-40
Original file line numberDiff line numberDiff line change
@@ -86,58 +86,29 @@ 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)
110-
111-
then:
112-
roles == [user1: [role1], user2: [role2]]
113-
}
114-
115-
void "multiLoadRoles should use groupUserAttributes when groupUserAttributes is not empty"() {
116-
given:
117-
def users = [externalUser("user1"), externalUser("user2")]
118-
def role1 = new Role("group1")
119-
def role2 = new Role("group2")
120-
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
128109
def roles = provider.multiLoadRoles(users)
129110

130-
then: "should use loadRoles"
131-
roles == [user1: [role1], user2: [role2]]
132-
133-
when: "users count is greater than thresholdToUseGroupMembership"
134-
configProps.thresholdToUseGroupMembership = 1
135-
provider.ldapTemplate = Mock(SpringSecurityLdapTemplate) {
136-
1 * search(*_) >> [[Pair.of("user1",role1)], [Pair.of("user2", role2)], [Pair.of("unknown", role2)]]
137-
}
138-
roles = provider.multiLoadRoles(users)
139-
140-
then: "should use ldapTemplate.search method"
111+
then:
141112
roles == [user1: [role1], user2: [role2]]
142113
}
143114

0 commit comments

Comments
 (0)