Skip to content
Permalink
Browse files

Merge pull request #26 from dwnusbaum/JENKINS-21784

[JENKINS-21784] Add support for querying group membership
  • Loading branch information
andresrc committed Jan 30, 2018
2 parents a62fbf7 + 0cf50cf commit e0e794eca6bf5cb2656ac465a274ce314da57f08
@@ -44,6 +44,7 @@
import jenkins.security.plugins.ldap.FromGroupSearchLDAPGroupMembershipStrategy;
import jenkins.security.plugins.ldap.LDAPConfiguration;
import jenkins.security.plugins.ldap.LDAPGroupMembershipStrategy;
import jenkins.security.plugins.ldap.LDAPExtendedTemplate;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.acegisecurity.AcegiSecurityException;
@@ -56,6 +57,7 @@
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.ldap.InitialDirContextFactory;
import org.acegisecurity.ldap.LdapDataAccessException;
import org.acegisecurity.ldap.LdapEntryMapper;
import org.acegisecurity.ldap.LdapUserSearch;
import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
@@ -88,6 +90,7 @@
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
import javax.naming.ldap.Control;
import javax.naming.ldap.LdapName;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
@@ -393,7 +396,7 @@ group target (CN is a reasonable default)
/**
* The group details cache.
*/
private transient Map<String,CacheEntry<Set<String>>> groupDetailsCache = null;
private transient Map<String,CacheEntry<GroupDetailsImpl>> groupDetailsCache = null;

@Deprecated @Restricted(NoExternalUse.class)
private transient Map<String,String> extraEnvVars;
@@ -841,50 +844,71 @@ public LdapUserDetails updateUserDetails(LdapUserDetails d) {

@Override
public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException {
return loadGroupByGroupname(groupname, false);
}

@Override
public GroupDetails loadGroupByGroupname(String groupname, boolean fetchMembers) throws UsernameNotFoundException, DataAccessException {
groupname = fixGroupname(groupname);
Set<String> cachedGroups;
GroupDetailsImpl cachedGroup;
if (cache != null) {
final CacheEntry<Set<String>> cached;
final CacheEntry<GroupDetailsImpl> cached;
synchronized (this) {
cached = groupDetailsCache != null ? groupDetailsCache.get(groupname) : null;
}
if (cached != null && cached.isValid()) {
cachedGroups = cached.getValue();
GroupDetailsImpl cachedValue = cached.getValue();
if (!fetchMembers || cachedValue.getMembers() != null) {
cachedGroup = cachedValue;
} else {
cachedGroup = null;
}
} else {
cachedGroups = null;
cachedGroup = null;
}
} else {
cachedGroups = null;
cachedGroup = null;
}

// TODO: obtain a DN instead so that we can obtain multiple attributes later

final Set<String> groups = cachedGroups != null
? cachedGroups
: searchForGroupName(groupname);
if (cache != null && cachedGroups == null && !groups.isEmpty()) {
final GroupDetailsImpl group = cachedGroup != null
? cachedGroup
: searchForGroupName(groupname, fetchMembers);
if (cache != null && cachedGroup == null) {
synchronized (this) {
if (groupDetailsCache == null) {
groupDetailsCache = new CacheMap<String, Set<String>>(cache.getSize());
groupDetailsCache = new CacheMap<>(cache.getSize());
}
groupDetailsCache.put(groupname, new CacheEntry<Set<String>>(cache.getTtl(), groups));
groupDetailsCache.put(groupname, new CacheEntry<>(cache.getTtl(), group));
}
}

if(groups.isEmpty())
throw new UsernameNotFoundException(groupname);

return new GroupDetailsImpl(fixGroupname(groups.iterator().next()));
return group;
}

private Set<String> searchForGroupName(String groupname) {
Set<String> groups = new TreeSet<>();
private @Nonnull GroupDetailsImpl searchForGroupName(String groupname, boolean fetchMembers) throws UsernameNotFoundException, DataAccessException {
for (LDAPConfiguration conf : configurations) {
String searchBase = conf.getGroupSearchBase() != null ? conf.getGroupSearchBase() : "";
String searchFilter = conf.getGroupSearchFilter() != null ? conf.getGroupSearchFilter() : GROUP_SEARCH;
groups.addAll(conf.getLdapTemplate().searchForSingleAttributeValues(searchBase, searchFilter, new String[]{groupname}, "cn"));
try {
String searchBase = conf.getGroupSearchBase() != null ? conf.getGroupSearchBase() : "";
String searchFilter = conf.getGroupSearchFilter() != null ? conf.getGroupSearchFilter() : GROUP_SEARCH;
LDAPExtendedTemplate template = conf.getLdapTemplate();
GroupDetailsImpl groupDetails = (GroupDetailsImpl)template.searchForFirstEntry(searchBase, searchFilter,
new Object[]{groupname}, new String[]{}, new GroupDetailsMapper());
if (groupDetails != null) {
if (fetchMembers) {
Set<String> members = conf.getGroupMembershipStrategy().getGroupMembers(groupDetails.getDn(), conf);
groupDetails = new GroupDetailsImpl(groupDetails.getDn(), groupDetails.getName(), members);
}
return groupDetails;
}
} catch (DataAccessException e) {
LOGGER.log(Level.WARNING,
String.format("Failed communication with ldap server %s (%s)",
conf.getId(), conf.getServer()),
e);
throw e;
}
}
return groups;
throw new UsernameNotFoundException(groupname);
}

private static String fixGroupname(String groupname) {
@@ -902,15 +926,42 @@ public DescriptorImpl getDescriptor() {

private static class GroupDetailsImpl extends GroupDetails {

private String name;
private final String dn;
private final String name;
private final Set<String> members;

public GroupDetailsImpl(String name) {
public GroupDetailsImpl(String dn, String name) {
this(dn, name, null);
}

public GroupDetailsImpl(String dn, String name, Set<String> members) {
this.dn = dn;
this.name = name;
this.members = members;
}

public String getDn() {
return dn;
}

@Override
public String getName() {
return name;
}

@Override
public Set<String> getMembers() {
return members;
}
}

private static class GroupDetailsMapper implements LdapEntryMapper {
@Override
public GroupDetailsImpl mapAttributes(String dn, Attributes attributes) throws NamingException {
LdapName name = new LdapName(dn);
String groupName = fixGroupname(String.valueOf(name.getRdn(name.size() - 1).getValue()));
return new GroupDetailsImpl(dn, groupName);
}
}

private class LDAPAuthenticationManager implements AuthenticationManager {
@@ -25,18 +25,31 @@

import hudson.Extension;
import hudson.security.LDAPSecurityRealm;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.InvalidNameException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.ldap.LdapName;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.ldap.LdapEntryMapper;
import org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator;
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.springframework.dao.DataAccessException;

/**
* Traditional strategy.
* @since 1.10
*/
public class FromGroupSearchLDAPGroupMembershipStrategy extends LDAPGroupMembershipStrategy {

private static final Logger LOGGER = Logger.getLogger(FromGroupSearchLDAPGroupMembershipStrategy.class.getName());
/**
* The search filter to apply to groups. Only those groups matching this criteria will be considered as groups
* that the user belongs to.
@@ -68,6 +81,13 @@ public void setAuthoritiesPopulator(LdapAuthoritiesPopulator authoritiesPopulato
return getAuthoritiesPopulator().getGrantedAuthorities(ldapUser);
}

@Override
public Set<String> getGroupMembers(String groupDn, LDAPConfiguration conf) throws DataAccessException {
LDAPExtendedTemplate template = conf.getLdapTemplate();
String[] memberAttributes = { "member", "uniqueMember", "memberUid" };
return (Set<String>) template.retrieveEntry(groupDn, new GroupMembersMapper(), memberAttributes);
}

@Extension
public static class DescriptorImpl extends LDAPGroupMembershipStrategyDescriptor {

@@ -76,4 +96,41 @@ public String getDisplayName() {
return Messages.FromGroupSearchLDAPGroupMembershipStrategy_DisplayName();
}
}

/**
* Maps member attributes in groups to a set of member names.
*/
private static class GroupMembersMapper implements LdapEntryMapper {
@Override
public Set<String> mapAttributes(String dn, Attributes attributes) throws NamingException {
NamingEnumeration<?> enumeration;
boolean expectingUidInsteadOfDn = false;
if (attributes.get("member") != null) {
enumeration = attributes.get("member").getAll();
} else if (attributes.get("uniqueMember") != null) {
enumeration = attributes.get("uniqueMember").getAll();
} else if (attributes.get("memberUid") != null) {
enumeration = attributes.get("memberUid").getAll();
expectingUidInsteadOfDn = true;
} else {
LOGGER.log(Level.FINEST, "No members for {0}", dn);
return Collections.emptySet();
}
Set<String> members = new TreeSet<>();
while (enumeration.hasMore()) {
String memberDn = String.valueOf(enumeration.next());
if (expectingUidInsteadOfDn) {
members.add(memberDn);
} else {
try {
LdapName memberName = new LdapName(memberDn);
members.add(String.valueOf(memberName.getRdn(memberName.size() - 1).getValue()));
} catch (InvalidNameException e) {
LOGGER.log(Level.FINEST, "Expecting DN but found {0}", memberDn);
}
}
}
return members;
}
}
}
@@ -28,9 +28,11 @@
import java.util.Set;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.ldap.LdapEntryMapper;
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.springframework.dao.DataAccessException;

import javax.naming.InvalidNameException;
import javax.naming.NamingException;
@@ -39,6 +41,7 @@
import javax.naming.ldap.LdapName;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.LogRecord;
@@ -51,6 +54,7 @@
public class FromUserRecordLDAPGroupMembershipStrategy extends LDAPGroupMembershipStrategy {

private static final Logger LOGGER = Logger.getLogger(FromUserRecordLDAPGroupMembershipStrategy.class.getName());
private static final String USER_SEARCH_FILTER = "({0}={1})";
private final String attributeName;

@DataBoundConstructor
@@ -116,6 +120,26 @@ public String getAttributeName() {
return result.toArray(new GrantedAuthority[result.size()]);
}

@Override
public Set<String> getGroupMembers(String groupDn, LDAPConfiguration conf) throws DataAccessException {
LDAPExtendedTemplate template = conf.getLdapTemplate();
String searchBase = conf.getUserSearchBase() != null ? conf.getUserSearchBase() : "";
String[] filterArgs = { getAttributeName(), groupDn };
return new HashSet<>((List<String>)template.searchForAllEntries(searchBase, USER_SEARCH_FILTER,
filterArgs, new String[]{}, new UserRecordMapper()));
}

/**
* Maps users records to names.
*/
private static class UserRecordMapper implements LdapEntryMapper {
@Override
public String mapAttributes(String dn, Attributes attributes) throws NamingException {
LdapName name = new LdapName(dn);
return String.valueOf(name.getRdn(name.size() - 1).getValue());
}
}

@Extension
public static class DescriptorImpl extends LDAPGroupMembershipStrategyDescriptor {

@@ -135,7 +135,7 @@
/**
* Set in {@link #createApplicationContext(LDAPSecurityRealm, boolean)}
*/
private transient LdapTemplate ldapTemplate;
private transient LDAPExtendedTemplate ldapTemplate;
private transient String id;

@DataBoundConstructor
@@ -585,7 +585,7 @@ public WebApplicationContext createApplicationContext(LDAPSecurityRealm realm, b
}
WebApplicationContext appContext = builder.createApplicationContext();

ldapTemplate = new LdapTemplate(SecurityRealm.findBean(InitialDirContextFactory.class, appContext));
ldapTemplate = new LDAPExtendedTemplate(SecurityRealm.findBean(InitialDirContextFactory.class, appContext));

if (groupMembershipStrategy != null) {
groupMembershipStrategy.setAuthoritiesPopulator(SecurityRealm.findBean(LdapAuthoritiesPopulator.class, appContext));
@@ -595,7 +595,7 @@ public WebApplicationContext createApplicationContext(LDAPSecurityRealm realm, b
}

@Restricted(NoExternalUse.class)
public LdapTemplate getLdapTemplate() {
public LDAPExtendedTemplate getLdapTemplate() {
return ldapTemplate;
}

0 comments on commit e0e794e

Please sign in to comment.
You can’t perform that action at this time.