Skip to content

Commit c1ecbdc

Browse files
krasilnikov-dmitriyDmitry Krasilnikov
authored and
Dmitry Krasilnikov
committed
Allow to use any ldap attribute as user id for batch role sync
1 parent 5250764 commit c1ecbdc

File tree

7 files changed

+476
-2
lines changed

7 files changed

+476
-2
lines changed

fiat-ldap-v2/fiat-ldap-v2.gradle

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2017 Google, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License")
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
dependencies {
18+
implementation project(":fiat-roles")
19+
implementation project(":fiat-core")
20+
21+
implementation "org.apache.commons:commons-lang3"
22+
implementation "org.springframework.boot:spring-boot-autoconfigure"
23+
implementation "org.springframework.security:spring-security-ldap"
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2017 Google, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License")
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.spinnaker.fiat.config;
18+
19+
import java.text.MessageFormat;
20+
import lombok.Data;
21+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
22+
import org.springframework.boot.context.properties.ConfigurationProperties;
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
26+
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
27+
28+
@Configuration
29+
@ConditionalOnProperty(value = "auth.group-membership.service", havingValue = "ldapv2")
30+
public class LdapConfigV2 {
31+
32+
@Bean
33+
public SpringSecurityLdapTemplate springSecurityLdapTemplate(ConfigProps configProps) {
34+
DefaultSpringSecurityContextSource contextSource =
35+
new DefaultSpringSecurityContextSource(configProps.url);
36+
contextSource.setUserDn(configProps.managerDn);
37+
contextSource.setPassword(configProps.managerPassword);
38+
contextSource.afterPropertiesSet();
39+
SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(contextSource);
40+
template.setIgnorePartialResultException(configProps.isIgnorePartialResultException());
41+
return template;
42+
}
43+
44+
@Data
45+
@Configuration
46+
@ConfigurationProperties("auth.group-membership.ldapv2")
47+
public static class ConfigProps {
48+
String url;
49+
String managerDn;
50+
String managerPassword;
51+
52+
String groupSearchBase = "";
53+
MessageFormat userDnPattern = new MessageFormat("uid={0},ou=users");
54+
String userSearchBase = "";
55+
String userIdAttributes = "";
56+
String userSearchFilter;
57+
String groupSearchFilter = "(uniqueMember={0})";
58+
String groupRoleAttributes = "cn";
59+
String groupUserAttributes = "";
60+
61+
int thresholdToUseGroupMembership = 100;
62+
63+
boolean ignorePartialResultException = false;
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
* Copyright 2017 Google, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License")
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.spinnaker.fiat.roles.ldap;
18+
19+
import com.netflix.spinnaker.fiat.config.LdapConfigV2;
20+
import com.netflix.spinnaker.fiat.model.resources.Role;
21+
import com.netflix.spinnaker.fiat.permissions.ExternalUser;
22+
import com.netflix.spinnaker.fiat.roles.UserRolesProvider;
23+
import java.text.MessageFormat;
24+
import java.util.*;
25+
import java.util.stream.Collectors;
26+
import javax.naming.InvalidNameException;
27+
import javax.naming.Name;
28+
import javax.naming.NamingEnumeration;
29+
import javax.naming.NamingException;
30+
import javax.naming.directory.Attribute;
31+
import javax.naming.directory.Attributes;
32+
import javax.naming.ldap.LdapName;
33+
import lombok.Setter;
34+
import lombok.extern.slf4j.Slf4j;
35+
import org.apache.commons.lang3.StringUtils;
36+
import org.apache.commons.lang3.tuple.Pair;
37+
import org.springframework.beans.factory.annotation.Autowired;
38+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
39+
import org.springframework.dao.IncorrectResultSizeDataAccessException;
40+
import org.springframework.ldap.core.AttributesMapper;
41+
import org.springframework.ldap.core.DirContextOperations;
42+
import org.springframework.ldap.core.DistinguishedName;
43+
import org.springframework.ldap.support.LdapEncoder;
44+
import org.springframework.security.ldap.LdapUtils;
45+
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
46+
import org.springframework.stereotype.Component;
47+
48+
@Slf4j
49+
@Component
50+
@ConditionalOnProperty(value = "auth.group-membership.service", havingValue = "ldapv2")
51+
public class LdapUserRolesProviderV2 implements UserRolesProvider {
52+
53+
@Setter private SpringSecurityLdapTemplate ldapTemplate;
54+
55+
@Setter private LdapConfigV2.ConfigProps configProps;
56+
57+
public LdapUserRolesProviderV2(
58+
@Autowired SpringSecurityLdapTemplate ldapTemplate,
59+
@Autowired LdapConfigV2.ConfigProps configProps) {
60+
this.ldapTemplate = ldapTemplate;
61+
this.configProps = configProps;
62+
}
63+
64+
@Override
65+
public List<Role> loadRoles(ExternalUser user) {
66+
String userId = user.getId();
67+
68+
log.debug("loadRoles for user {}", userId);
69+
if (StringUtils.isEmpty(configProps.getGroupSearchBase())) {
70+
return new ArrayList<>();
71+
}
72+
73+
String fullUserDn = getUserFullDn(userId);
74+
75+
if (fullUserDn == null) {
76+
// Likely a service account
77+
log.debug("fullUserDn is null for {}", userId);
78+
return new ArrayList<>();
79+
}
80+
81+
String[] params = new String[] {fullUserDn, userId};
82+
log.debug("Searching for groups using \ngroupSearchBase: {}\ngroupSearchFilter: {}\nparams: {}\ngroupRoleAttributes: {}",
83+
configProps.getGroupSearchBase(),
84+
configProps.getGroupSearchFilter(),
85+
StringUtils.join(params, " :: "),
86+
configProps.getGroupRoleAttributes());
87+
88+
89+
// Copied from org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator.
90+
Set<String> userRoles =
91+
ldapTemplate.searchForSingleAttributeValues(
92+
configProps.getGroupSearchBase(),
93+
configProps.getGroupSearchFilter(),
94+
params,
95+
configProps.getGroupRoleAttributes());
96+
97+
log.debug("Got roles for user {}: {}", userId, userRoles);
98+
return userRoles.stream()
99+
.map(role -> new Role(role).setSource(Role.Source.LDAP))
100+
.collect(Collectors.toList());
101+
}
102+
103+
private class RoleFullDNtoUserRoleMapper implements AttributesMapper<Role> {
104+
@Override
105+
public Role mapFromAttributes(Attributes attrs) throws NamingException {
106+
return new Role(attrs.get(configProps.getGroupRoleAttributes()).get().toString())
107+
.setSource(Role.Source.LDAP);
108+
}
109+
}
110+
111+
private class UserRoleMapper implements AttributesMapper<Pair<String, Collection<Role>>> {
112+
@Override
113+
public Pair<String, Collection<Role>> mapFromAttributes(Attributes attrs)
114+
throws NamingException {
115+
List<Role> roles = new ArrayList<>();
116+
Attribute memberOfAttribute = attrs.get("memberOf");
117+
if (memberOfAttribute != null) {
118+
for (NamingEnumeration<?> memberOf = memberOfAttribute.getAll(); memberOf.hasMore(); ) {
119+
String roleDN = memberOf.next().toString();
120+
LdapName ln = org.springframework.ldap.support.LdapUtils.newLdapName(roleDN);
121+
String role =
122+
org.springframework.ldap.support.LdapUtils.getStringValue(
123+
ln, configProps.getGroupRoleAttributes());
124+
roles.add(new Role(role).setSource(Role.Source.LDAP));
125+
}
126+
}
127+
128+
return Pair.of(
129+
attrs.get(configProps.getUserIdAttributes()).get().toString().toLowerCase(), roles);
130+
}
131+
}
132+
133+
@Override
134+
public Map<String, Collection<Role>> multiLoadRoles(Collection<ExternalUser> users) {
135+
if (StringUtils.isEmpty(configProps.getGroupSearchBase())) {
136+
return new HashMap<>();
137+
}
138+
StringBuilder filter = new StringBuilder();
139+
filter.append("(|");
140+
users.forEach(
141+
u -> filter.append(MessageFormat.format(configProps.getUserSearchFilter(), u.getId())));
142+
filter.append(")");
143+
144+
Map<String, List<ExternalUser>> userIds =
145+
users.stream().collect(Collectors.groupingBy(e -> e.getId().toLowerCase()));
146+
List<Role> roles =
147+
ldapTemplate.search(
148+
configProps.getGroupSearchBase(),
149+
MessageFormat.format(configProps.getGroupSearchFilter(), "*", "*"),
150+
new RoleFullDNtoUserRoleMapper());
151+
152+
return ldapTemplate
153+
.search(configProps.getUserSearchBase(), filter.toString(), new UserRoleMapper())
154+
.stream()
155+
.flatMap(
156+
p -> {
157+
List<ExternalUser> sameUsers = userIds.get(p.getKey().toLowerCase());
158+
return sameUsers.stream()
159+
.flatMap(it -> p.getValue().stream().map(role -> Pair.of(it.getId(), role)));
160+
})
161+
.filter(p -> roles.contains(p.getValue()))
162+
.collect(
163+
Collectors.groupingBy(
164+
Pair::getKey,
165+
Collectors.mapping(Pair::getValue, Collectors.toCollection(ArrayList::new))));
166+
}
167+
168+
private String getUserFullDn(String userId) {
169+
String rootDn = LdapUtils.parseRootDnFromUrl(configProps.getUrl());
170+
DistinguishedName root = new DistinguishedName(rootDn);
171+
log.debug("Root DN: {}", root.toString());
172+
173+
String[] formatArgs = new String[] {LdapEncoder.nameEncode(userId)};
174+
175+
String partialUserDn;
176+
if (!StringUtils.isEmpty(configProps.getUserSearchFilter())) {
177+
try {
178+
DirContextOperations res =
179+
ldapTemplate.searchForSingleEntry(
180+
configProps.getUserSearchBase(), configProps.getUserSearchFilter(), formatArgs);
181+
partialUserDn = res.getDn().toString();
182+
} catch (IncorrectResultSizeDataAccessException e) {
183+
log.error("Unable to find a single user entry", e);
184+
return null;
185+
}
186+
} else {
187+
partialUserDn = configProps.getUserDnPattern().format(formatArgs);
188+
}
189+
190+
DistinguishedName user = new DistinguishedName(partialUserDn);
191+
log.debug("User portion: {}", user.toString());
192+
193+
try {
194+
Name fullUser = root.addAll(user);
195+
log.debug("Full user DN: {}", fullUser.toString());
196+
return fullUser.toString();
197+
} catch (InvalidNameException ine) {
198+
log.error("Could not assemble full userDn", ine);
199+
}
200+
return null;
201+
}
202+
}

0 commit comments

Comments
 (0)