diff --git a/src/main/java/org/elasticsearch/shield/authc/active_directory/ActiveDirectoryConnection.java b/src/main/java/org/elasticsearch/shield/authc/active_directory/ActiveDirectoryConnection.java index 0a452dd0893..59f97c3b780 100644 --- a/src/main/java/org/elasticsearch/shield/authc/active_directory/ActiveDirectoryConnection.java +++ b/src/main/java/org/elasticsearch/shield/authc/active_directory/ActiveDirectoryConnection.java @@ -5,167 +5,22 @@ */ package org.elasticsearch.shield.authc.active_directory; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.collect.ImmutableList; import org.elasticsearch.common.logging.ESLogger; -import org.elasticsearch.shield.authc.ldap.LdapException; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection; -import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.*; -import java.util.List; +import javax.naming.directory.DirContext; /** * An Ldap Connection customized for active directory. */ public class ActiveDirectoryConnection extends AbstractLdapConnection { - private final String groupSearchDN; - private final int timeoutMilliseconds; - /** * This object is intended to be constructed by the LdapConnectionFactory */ - ActiveDirectoryConnection(ESLogger logger, DirContext ctx, String boundName, String groupSearchDN, int timeoutMilliseconds) { - super(logger, ctx, boundName); - this.groupSearchDN = groupSearchDN; - this.timeoutMilliseconds = timeoutMilliseconds; + ActiveDirectoryConnection(ESLogger logger, DirContext ctx, String boundName, String groupSearchDN, TimeValue timeout) { + super(logger, ctx, boundName, new ActiveDirectoryGroupsResolver(logger, groupSearchDN), timeout); } - /** - * LDAP connections should be closed to clean up resources. However, the jndi contexts have the finalize - * implemented properly so that it will clean up on garbage collection. - */ - @Override - public void close() { - try { - jndiContext.close(); - } catch (NamingException e) { - throw new ActiveDirectoryException("could not close the LDAP connection", e); - } - } - - @Override - public List groups() { - - String groupsSearchFilter = buildGroupQuery(); - logger.debug("group SID to DN search filter: [{}]", groupsSearchFilter); - - // Search for groups the user belongs to in order to get their names - //Create the search controls - SearchControls groupsSearchCtls = new SearchControls(); - - //Specify the search scope - groupsSearchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); - groupsSearchCtls.setReturningAttributes(Strings.EMPTY_ARRAY); //we only need the entry DN - groupsSearchCtls.setTimeLimit(timeoutMilliseconds); - - ImmutableList.Builder groups = ImmutableList.builder(); - try (ClosableNamingEnumeration groupsAnswer = new ClosableNamingEnumeration<>( - jndiContext.search(groupSearchDN, groupsSearchFilter, groupsSearchCtls))) { - - //Loop through the search results - while (groupsAnswer.hasMoreElements()) { - SearchResult sr = groupsAnswer.next(); - groups.add(sr.getNameInNamespace()); - } - } catch (NamingException | LdapException ne) { - throw new ActiveDirectoryException("failed to fetch AD groups", bindDn, ne); - } - List groupList = groups.build(); - if (logger.isDebugEnabled()) { - logger.debug("found these groups [{}] for userDN [{}]", groupList, this.bindDn); - } - return groupList; - } - - private String buildGroupQuery() { - StringBuilder groupsSearchFilter = new StringBuilder("(|"); - try { - SearchControls userSearchCtls = new SearchControls(); - userSearchCtls.setSearchScope(SearchControls.OBJECT_SCOPE); - - //specify the LDAP search filter to find the user in question - String userSearchFilter = "(objectClass=user)"; - String userReturnedAtts[] = { "tokenGroups" }; - userSearchCtls.setReturningAttributes(userReturnedAtts); - try (ClosableNamingEnumeration userAnswer = new ClosableNamingEnumeration<>( - jndiContext.search(authenticatedUserDn(), userSearchFilter, userSearchCtls))) { - - //Loop through the search results - while (userAnswer.hasMoreElements()) { - - SearchResult sr = userAnswer.next(); - Attributes attrs = sr.getAttributes(); - - if (attrs != null) { - try (ClosableNamingEnumeration ae = new ClosableNamingEnumeration<>(attrs.getAll())) { - while (ae.hasMore() ) { - Attribute attr = ae.next(); - for (NamingEnumeration e = attr.getAll(); e.hasMore(); ) { - byte[] sid = (byte[]) e.next(); - groupsSearchFilter.append("(objectSid="); - groupsSearchFilter.append(binarySidToStringSid(sid)); - groupsSearchFilter.append(")"); - } - groupsSearchFilter.append(")"); - } - } - } - } - } - - } catch (NamingException | LdapException ne) { - throw new ActiveDirectoryException("failed to fetch AD groups", bindDn, ne); - } - return groupsSearchFilter.toString(); - } - - @Override - public String authenticatedUserDn() { - return bindDn; - } - - /** - * To better understand what the sid is and how its string representation looks like, see - * http://blogs.msdn.com/b/alextch/archive/2007/06/18/sample-java-application-that-retrieves-group-membership-of-an-active-directory-user-account.aspx - * - * @param SID byte encoded security ID - */ - static public String binarySidToStringSid(byte[] SID) { - String strSID; - - //convert the SID into string format - - long version; - long authority; - long count; - long rid; - - strSID = "S"; - version = SID[0]; - strSID = strSID + "-" + Long.toString(version); - authority = SID[4]; - - for (int i = 0; i < 4; i++) { - authority <<= 8; - authority += SID[4 + i] & 0xFF; - } - - strSID = strSID + "-" + Long.toString(authority); - count = SID[2]; - count <<= 8; - count += SID[1] & 0xFF; - for (int j = 0; j < count; j++) { - rid = SID[11 + (j * 4)] & 0xFF; - for (int k = 1; k < 4; k++) { - rid <<= 8; - rid += SID[11 - k + (j * 4)] & 0xFF; - } - strSID = strSID + "-" + Long.toString(rid); - } - return strSID; - } } diff --git a/src/main/java/org/elasticsearch/shield/authc/active_directory/ActiveDirectoryConnectionFactory.java b/src/main/java/org/elasticsearch/shield/authc/active_directory/ActiveDirectoryConnectionFactory.java index c7fe8671ab4..3a830e3cbbc 100644 --- a/src/main/java/org/elasticsearch/shield/authc/active_directory/ActiveDirectoryConnectionFactory.java +++ b/src/main/java/org/elasticsearch/shield/authc/active_directory/ActiveDirectoryConnectionFactory.java @@ -9,12 +9,14 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.ImmutableMap; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.shield.ShieldSettingsException; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.shield.authc.ldap.LdapException; import org.elasticsearch.shield.authc.support.SecuredString; import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration; import org.elasticsearch.shield.authc.support.ldap.ConnectionFactory; +import org.elasticsearch.shield.authc.support.ldap.SearchScope; import javax.naming.Context; import javax.naming.NamingException; @@ -36,14 +38,14 @@ public class ActiveDirectoryConnectionFactory extends ConnectionFactory sharedLdapEnv; private final String userSearchDN; private final String domainName; private final String userSearchFilter; - private final int timeoutMilliseconds; - private final Boolean userSearchSubtree; + private final SearchScope userSearchScope; + private final TimeValue timeout; @Inject public ActiveDirectoryConnectionFactory(RealmConfig config) { @@ -55,8 +57,8 @@ public class ActiveDirectoryConnectionFactory extends ConnectionFactory builder = ImmutableMap.builder() @@ -90,9 +92,9 @@ public class ActiveDirectoryConnectionFactory extends ConnectionFactory results = new ClosableNamingEnumeration<>( ctx.search(userSearchDN, userSearchFilter, new Object[] { userName }, searchCtls))) { @@ -101,16 +103,16 @@ public class ActiveDirectoryConnectionFactory extends ConnectionFactory resolve(DirContext ctx, String userDn, TimeValue timeout) { + + String groupsSearchFilter = buildGroupQuery(ctx, userDn, timeout); + logger.debug("group SID to DN search filter: [{}]", groupsSearchFilter); + + // Search for groups the user belongs to in order to get their names + //Create the search controls + SearchControls groupsSearchCtls = new SearchControls(); + + //Specify the search scope + groupsSearchCtls.setSearchScope(SearchScope.SUB_TREE.scope()); + groupsSearchCtls.setReturningAttributes(Strings.EMPTY_ARRAY); //we only need the entry DN + groupsSearchCtls.setTimeLimit((int) timeout.millis()); + + ImmutableList.Builder groups = ImmutableList.builder(); + try (ClosableNamingEnumeration groupsAnswer = new ClosableNamingEnumeration<>( + ctx.search(baseDn, groupsSearchFilter, groupsSearchCtls))) { + + //Loop through the search results + while (groupsAnswer.hasMoreElements()) { + SearchResult sr = groupsAnswer.next(); + groups.add(sr.getNameInNamespace()); + } + } catch (NamingException | LdapException ne) { + throw new ActiveDirectoryException("failed to fetch AD groups", userDn, ne); + } + List groupList = groups.build(); + if (logger.isDebugEnabled()) { + logger.debug("found these groups [{}] for userDN [{}]", groupList, userDn); + } + return groupList; + + } + + static String buildGroupQuery(DirContext ctx, String userDn, TimeValue timeout) { + StringBuilder groupsSearchFilter = new StringBuilder("(|"); + try { + SearchControls userSearchCtls = new SearchControls(); + + userSearchCtls.setSearchScope(SearchControls.OBJECT_SCOPE); + userSearchCtls.setTimeLimit((int) timeout.millis()); + + userSearchCtls.setReturningAttributes(new String[] { "tokenGroups" }); + try (ClosableNamingEnumeration userAnswer = new ClosableNamingEnumeration<>( + ctx.search(userDn, "(objectClass=user)", userSearchCtls))) { + + while (userAnswer.hasMoreElements()) { + + SearchResult sr = userAnswer.next(); + Attributes attrs = sr.getAttributes(); + + if (attrs != null) { + try (ClosableNamingEnumeration ae = new ClosableNamingEnumeration<>(attrs.getAll())) { + while (ae.hasMore() ) { + Attribute attr = ae.next(); + for (NamingEnumeration e = attr.getAll(); e.hasMore(); ) { + byte[] sid = (byte[]) e.next(); + groupsSearchFilter.append("(objectSid="); + groupsSearchFilter.append(binarySidToStringSid(sid)); + groupsSearchFilter.append(")"); + } + groupsSearchFilter.append(")"); + } + } + } + } + } + + } catch (NamingException | LdapException ne) { + throw new ActiveDirectoryException("failed to fetch AD groups", userDn, ne); + } + return groupsSearchFilter.toString(); + } + + /** + * To better understand what the sid is and how its string representation looks like, see + * http://blogs.msdn.com/b/alextch/archive/2007/06/18/sample-java-application-that-retrieves-group-membership-of-an-active-directory-user-account.aspx + * + * @param SID byte encoded security ID + */ + static public String binarySidToStringSid(byte[] SID) { + String strSID; + + //convert the SID into string format + + long version; + long authority; + long count; + long rid; + + strSID = "S"; + version = SID[0]; + strSID = strSID + "-" + Long.toString(version); + authority = SID[4]; + + for (int i = 0; i < 4; i++) { + authority <<= 8; + authority += SID[4 + i] & 0xFF; + } + + strSID = strSID + "-" + Long.toString(authority); + count = SID[2]; + count <<= 8; + count += SID[1] & 0xFF; + for (int j = 0; j < count; j++) { + rid = SID[11 + (j * 4)] & 0xFF; + for (int k = 1; k < 4; k++) { + rid <<= 8; + rid += SID[11 - k + (j * 4)] & 0xFF; + } + strSID = strSID + "-" + Long.toString(rid); + } + return strSID; + } + +} diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnection.java b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnection.java index a74aa000bf3..b8db3e81cdd 100644 --- a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnection.java +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnection.java @@ -5,17 +5,11 @@ */ package org.elasticsearch.shield.authc.ldap; -import org.elasticsearch.common.Nullable; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection; -import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.*; -import java.util.LinkedList; -import java.util.List; +import javax.naming.directory.DirContext; /** * Encapsulates jndi/ldap functionality into one authenticated connection. The constructor is package scoped, assuming @@ -30,109 +24,10 @@ import java.util.List; */ public class LdapConnection extends AbstractLdapConnection { - private final int timeoutMilliseconds; - private final boolean isGroupSubTreeSearch; - private final boolean isFindGroupsByAttribute; - private final String groupSearchDN; - private final String groupAttribute = "memberOf"; - private final String userAttributeForGroupMembership; - private final String groupSearchFilter; - /** * This object is intended to be constructed by the LdapConnectionFactory */ - LdapConnection(ESLogger logger, DirContext ctx, String bindDN, int timeoutMilliseconds, boolean isFindGroupsByAttribute, boolean isGroupSubTreeSearch, - @Nullable String groupSearchFilter, @Nullable String groupSearchDN, @Nullable String userAttributeForGroupMembership) { - super(logger, ctx, bindDN); - this.isGroupSubTreeSearch = isGroupSubTreeSearch; - this.groupSearchFilter = groupSearchFilter; - this.groupSearchDN = groupSearchDN; - this.isFindGroupsByAttribute = isFindGroupsByAttribute; - this.userAttributeForGroupMembership = userAttributeForGroupMembership; - this.timeoutMilliseconds = timeoutMilliseconds; - } - - /** - * Fetches the groups that the user is a member of - * - * @return List of group membership - */ - @Override - public List groups() { - List groups = isFindGroupsByAttribute ? getGroupsFromUserAttrs() : getGroupsFromSearch(); - if (logger.isDebugEnabled()) { - logger.debug("found groups [{}] for userDN [{}]", groups, this.bindDn); - } - return groups; - } - - /** - * Fetches the groups of a user by doing a search. This could be abstracted out into a strategy class or through - * an inherited class (with groups as the template method). - * @return fully distinguished names of the roles - */ - public List getGroupsFromSearch() { - String userIdentifier = userAttributeForGroupMembership == null ? bindDn : readUserAttribute(userAttributeForGroupMembership); - if (logger.isTraceEnabled()) { - logger.trace("user identifier for group lookup is [{}]", userIdentifier); - } - List groups = new LinkedList<>(); - SearchControls search = new SearchControls(); - search.setReturningAttributes(Strings.EMPTY_ARRAY); - search.setSearchScope(this.isGroupSubTreeSearch ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE); - search.setTimeLimit(timeoutMilliseconds); - - try (ClosableNamingEnumeration results = new ClosableNamingEnumeration<>( - jndiContext.search(groupSearchDN, groupSearchFilter, new Object[] {userIdentifier}, search))) { - while (results.hasMoreElements()) { - groups.add(results.next().getNameInNamespace()); - } - } catch (NamingException | LdapException e ) { - throw new LdapException("could not search for an LDAP group", bindDn, e); - } - return groups; - } - - /** - * Fetches the groups from the user attributes (if supported). This method could later be abstracted out - * into a strategy class - * - * @return list of groups the user is a member of. - */ - public List getGroupsFromUserAttrs() { - List groupDns = new LinkedList<>(); - try { - Attributes results = jndiContext.getAttributes(bindDn, new String[] { groupAttribute }); - try (ClosableNamingEnumeration ae = new ClosableNamingEnumeration<>(results.getAll())) { - while (ae.hasMore()) { - Attribute attr = ae.next(); - for (NamingEnumeration attrEnum = attr.getAll(); attrEnum.hasMore(); ) { - Object val = attrEnum.next(); - if (val instanceof String) { - String stringVal = (String) val; - groupDns.add(stringVal); - } - } - } - } - } catch (NamingException | LdapException e) { - throw new LdapException("could not look up group attributes for user", bindDn, e); - } - return groupDns; - } - - String readUserAttribute(String userAttribute) { - try { - Attributes results = jndiContext.getAttributes(bindDn, new String[]{userAttribute}); - Attribute attribute = results.get(userAttribute); - if (results.size() == 0) { - throw new LdapException("No results returned for attribute [" + userAttribute + "]", bindDn); - } - return (String) attribute.get(); - } catch (NamingException e) { - throw new LdapException("Could not look attribute [" + userAttribute + "]", bindDn, e); - } catch (ClassCastException e) { - throw new LdapException("Returned ldap attribute [" + userAttribute + "] is not of type String", bindDn, e); - } + LdapConnection(ESLogger logger, DirContext ctx, String bindDN, AbstractLdapConnection.GroupsResolver groupsResolver, TimeValue timeout) { + super(logger, ctx, bindDN, groupsResolver, timeout); } } diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnectionFactory.java b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnectionFactory.java index 5c16f65df20..aa258a54b1d 100644 --- a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnectionFactory.java +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnectionFactory.java @@ -9,9 +9,11 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.ImmutableMap; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.shield.ShieldSettingsException; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.shield.authc.support.SecuredString; +import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection; import org.elasticsearch.shield.authc.support.ldap.ConnectionFactory; import javax.naming.Context; @@ -32,25 +34,13 @@ import java.util.Hashtable; public class LdapConnectionFactory extends ConnectionFactory { public static final String USER_DN_TEMPLATES_SETTING = "user_dn_templates"; - public static final String GROUP_SEARCH_SUBTREE_SETTING = "group_search.subtree"; - public static final String GROUP_SEARCH_BASEDN_SETTING = "group_search.base_dn"; - public static final String GROUP_SEARCH_FILTER_SETTING = "group_search.filter"; - public static final String GROUP_SEARCH_USER_ATTRIBUTE_SETTING = "group_search.user_attribute"; - - private static final String GROUP_SEARCH_DEFAULT_FILTER = "(&" + - "(|(objectclass=groupOfNames)(objectclass=groupOfUniqueNames)(objectclass=group))" + - "(|(uniqueMember={0})(member={0})))"; private final ImmutableMap sharedLdapEnv; private final String[] userDnTemplates; - protected final String groupSearchDN; - protected final boolean groupSubTreeSearch; - protected final boolean findGroupsByAttribute; - private final int timeoutMilliseconds; - private final String groupFilter; - private final String groupSearchUserAttribute; + private final AbstractLdapConnection.GroupsResolver groupResolver; + private final TimeValue timeout; - @Inject() + @Inject public LdapConnectionFactory(RealmConfig config) { super(LdapConnection.class, config); Settings settings = config.settings(); @@ -59,11 +49,10 @@ public class LdapConnectionFactory extends ConnectionFactory { throw new ShieldSettingsException("missing required LDAP setting [" + USER_DN_TEMPLATES_SETTING + "]"); } String[] ldapUrls = settings.getAsArray(URLS_SETTING); - if (ldapUrls == null) { + if (ldapUrls == null || ldapUrls.length == 0) { throw new ShieldSettingsException("missing required LDAP setting [" + URLS_SETTING + "]"); } - - timeoutMilliseconds = (int) settings.getAsTime(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT).millis(); + timeout = settings.getAsTime(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT); ImmutableMap.Builder builder = ImmutableMap.builder() .put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory") @@ -75,12 +64,8 @@ public class LdapConnectionFactory extends ConnectionFactory { configureJndiSSL(ldapUrls, builder); sharedLdapEnv = builder.build(); - groupSearchDN = settings.get(GROUP_SEARCH_BASEDN_SETTING); - findGroupsByAttribute = groupSearchDN == null; - groupSubTreeSearch = settings.getAsBoolean(GROUP_SEARCH_SUBTREE_SETTING, true); - groupFilter = settings.get(GROUP_SEARCH_FILTER_SETTING, GROUP_SEARCH_DEFAULT_FILTER); - groupSearchUserAttribute = settings.get(GROUP_SEARCH_FILTER_SETTING) == null ? - null : settings.get(GROUP_SEARCH_USER_ATTRIBUTE_SETTING); //if filter isn't set we don't want to change from using the DN + + groupResolver = groupResolver(settings); } /** @@ -104,8 +89,7 @@ public class LdapConnectionFactory extends ConnectionFactory { DirContext ctx = new InitialDirContext(ldapEnv); //return the first good connection - return new LdapConnection(connectionLogger, ctx, dn, timeoutMilliseconds, findGroupsByAttribute, groupSubTreeSearch, - groupFilter, groupSearchDN, groupSearchUserAttribute); + return new LdapConnection(connectionLogger, ctx, dn, groupResolver, timeout); } catch (NamingException e) { logger.warn("failed LDAP authentication with user template [{}] and DN [{}]", e, template, dn); @@ -126,4 +110,12 @@ public class LdapConnectionFactory extends ConnectionFactory { String escapedUsername = Rdn.escapeValue(username); return MessageFormat.format(template, escapedUsername); } + + static AbstractLdapConnection.GroupsResolver groupResolver(Settings settings) { + Settings searchSettings = settings.getAsSettings("group_search"); + if (!searchSettings.names().isEmpty()) { + return new SearchGroupsResolver(searchSettings); + } + return new UserAttributeGroupsResolver(settings); + } } diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/SearchGroupsResolver.java b/src/main/java/org/elasticsearch/shield/authc/ldap/SearchGroupsResolver.java new file mode 100644 index 00000000000..95926f7fb6d --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/SearchGroupsResolver.java @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.authc.ldap; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection; +import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration; +import org.elasticsearch.shield.authc.support.ldap.SearchScope; + +import javax.naming.NamingException; +import javax.naming.directory.*; +import java.util.LinkedList; +import java.util.List; + +/** +* +*/ +class SearchGroupsResolver implements AbstractLdapConnection.GroupsResolver { + + private static final String GROUP_SEARCH_DEFAULT_FILTER = "(&" + + "(|(objectclass=groupOfNames)(objectclass=groupOfUniqueNames)(objectclass=group))" + + "(|(uniqueMember={0})(member={0})))"; + + private final String baseDn; + private final String filter; + private final String userAttribute; + private final SearchScope scope; + + public SearchGroupsResolver(Settings settings) { + baseDn = settings.get("base_dn"); + filter = settings.get("filter", GROUP_SEARCH_DEFAULT_FILTER); + userAttribute = filter == null ? null : settings.get("user_attribute"); + scope = SearchScope.resolve(settings.get("scope"), SearchScope.SUB_TREE); + } + + public SearchGroupsResolver(String baseDn, String filter, String userAttribute, SearchScope scope) { + this.baseDn = baseDn; + this.filter = filter; + this.userAttribute = userAttribute; + this.scope = scope; + } + + @Override + public List resolve(DirContext ctx, String userDn, TimeValue timeout) { + List groups = new LinkedList<>(); + + String userId = userAttribute != null ? readUserAttribute(ctx, userDn, userDn) : userDn; + SearchControls search = new SearchControls(); + search.setReturningAttributes(Strings.EMPTY_ARRAY); + search.setSearchScope(scope.scope()); + search.setTimeLimit((int) timeout.millis()); + + try (ClosableNamingEnumeration results = new ClosableNamingEnumeration<>( + ctx.search(baseDn, filter, new Object[] { userId }, search))) { + while (results.hasMoreElements()) { + groups.add(results.next().getNameInNamespace()); + } + } catch (NamingException | LdapException e ) { + throw new LdapException("could not search for an LDAP group", userDn, e); + } + return groups; + } + + String readUserAttribute(DirContext ctx, String userDn, String userAttribute) { + try { + Attributes results = ctx.getAttributes(userDn, new String[]{userAttribute}); + Attribute attribute = results.get(userAttribute); + if (results.size() == 0) { + throw new LdapException("No results returned for attribute [" + userAttribute + "]", userDn); + } + return (String) attribute.get(); + } catch (NamingException e) { + throw new LdapException("Could not look attribute [" + userAttribute + "]", userDn, e); + } catch (ClassCastException e) { + throw new LdapException("Returned ldap attribute [" + userAttribute + "] is not of type String", userDn, e); + } + } +} diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/UserAttributeGroupsResolver.java b/src/main/java/org/elasticsearch/shield/authc/ldap/UserAttributeGroupsResolver.java new file mode 100644 index 00000000000..b9d7b1a8d2a --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/UserAttributeGroupsResolver.java @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.authc.ldap; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection; +import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import java.util.LinkedList; +import java.util.List; + +/** +* +*/ +class UserAttributeGroupsResolver implements AbstractLdapConnection.GroupsResolver { + + private final String attribute; + + public UserAttributeGroupsResolver(Settings settings) { + this(settings.get("user_group_attribute", "memberOf")); + } + + public UserAttributeGroupsResolver(String attribute) { + this.attribute = attribute; + } + + @Override + public List resolve(DirContext ctx, String userDn, TimeValue timeout) { + List groupDns = new LinkedList<>(); + try { + Attributes results = ctx.getAttributes(userDn, new String[] { attribute }); + try (ClosableNamingEnumeration ae = new ClosableNamingEnumeration<>(results.getAll())) { + while (ae.hasMore()) { + Attribute attr = ae.next(); + for (NamingEnumeration attrEnum = attr.getAll(); attrEnum.hasMore(); ) { + Object val = attrEnum.next(); + if (val instanceof String) { + String stringVal = (String) val; + groupDns.add(stringVal); + } + } + } + } + } catch (NamingException | LdapException e) { + throw new LdapException("could not look up group attributes for user", userDn, e); + } + return groupDns; + } +} diff --git a/src/main/java/org/elasticsearch/shield/authc/support/ldap/AbstractLdapConnection.java b/src/main/java/org/elasticsearch/shield/authc/support/ldap/AbstractLdapConnection.java index 87ae1770c11..32e00b42913 100644 --- a/src/main/java/org/elasticsearch/shield/authc/support/ldap/AbstractLdapConnection.java +++ b/src/main/java/org/elasticsearch/shield/authc/support/ldap/AbstractLdapConnection.java @@ -6,6 +6,7 @@ package org.elasticsearch.shield.authc.support.ldap; import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.unit.TimeValue; import javax.naming.NamingException; import javax.naming.directory.DirContext; @@ -20,6 +21,8 @@ public abstract class AbstractLdapConnection implements Closeable { protected final ESLogger logger; protected final DirContext jndiContext; protected final String bindDn; + protected final GroupsResolver groupsResolver; + protected final TimeValue timeout; /** * This object is intended to be constructed by the LdapConnectionFactory @@ -29,10 +32,12 @@ public abstract class AbstractLdapConnection implements Closeable { * outside of and be reused across all connections. We can't keep a static logger in this class * since we want the logger to be contextual (i.e. aware of the settings and its enviorment). */ - public AbstractLdapConnection(ESLogger logger, DirContext ctx, String boundName) { + public AbstractLdapConnection(ESLogger logger, DirContext ctx, String boundName, GroupsResolver groupsResolver, TimeValue timeout) { this.logger = logger; this.jndiContext = ctx; this.bindDn = boundName; + this.groupsResolver = groupsResolver; + this.timeout = timeout; } /** @@ -58,5 +63,13 @@ public abstract class AbstractLdapConnection implements Closeable { /** * @return List of fully distinguished group names */ - public abstract List groups(); + public List groups() { + return groupsResolver.resolve(jndiContext, bindDn, timeout); + } + + public static interface GroupsResolver { + + List resolve(DirContext ctx, String userDn, TimeValue timeout); + + } } diff --git a/src/main/java/org/elasticsearch/shield/authc/support/ldap/SearchScope.java b/src/main/java/org/elasticsearch/shield/authc/support/ldap/SearchScope.java new file mode 100644 index 00000000000..ad27f81ace9 --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/authc/support/ldap/SearchScope.java @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.authc.support.ldap; + +import org.elasticsearch.shield.authc.ldap.LdapException; + +import javax.naming.directory.SearchControls; + +/** + * + */ +public enum SearchScope { + + BASE(SearchControls.OBJECT_SCOPE), + ONE_LEVEL(SearchControls.ONELEVEL_SCOPE), + SUB_TREE(SearchControls.SUBTREE_SCOPE); + + private final int scope; + + SearchScope(int scope) { + this.scope = scope; + } + + public int scope() { + return scope; + } + + public static SearchScope resolve(String scope, SearchScope defaultScope) { + if (scope == null) { + return defaultScope; + } + switch (scope.toLowerCase()) { + case "base": + case "object": return BASE; + case "one_level" : return ONE_LEVEL; + case "sub_tree" : return SUB_TREE; + default: + throw new LdapException("Unknown search scope [" + scope + "]"); + } + } +}