/*
* JBoss, Home of Professional Open Source.
* Copyright 2013, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.domain.management.security;
import static org.jboss.as.domain.management.logging.DomainManagementLogger.SECURITY_LOGGER;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.NameNotFoundException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapReferralException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import org.jboss.as.domain.management.security.BaseLdapGroupSearchResource.GroupName;
/**
* Factory for supplying LDAP searches for groups.
*
* @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
*/
public class LdapGroupSearcherFactory {
private static final int searchTimeLimit = 10000;
private static final String PARSE_ROLES_FROM_DN = "org.jboss.as.domain.management.security.parseGroupNameFromLdapDN";
static LdapSearcher<LdapEntry[], LdapEntry> createForGroupToPrincipal(final String baseDn, final String groupDnAttribute,
final String groupNameAttribute, final String principalAttribute, final boolean recursive,
final GroupName searchBy, final boolean preferOriginalConnection) {
return new GroupToPrincipalSearcher(baseDn, groupDnAttribute, groupNameAttribute, principalAttribute, recursive, searchBy, preferOriginalConnection);
}
static LdapSearcher<LdapEntry[], LdapEntry> createForPrincipalToGroup(final String groupAttribute, final String groupNameAttribute,
final boolean preferOriginalConnection, final boolean skipMissingGroups, final boolean usingSimpleNames) {
return new PrincipalToGroupSearcher(groupAttribute, groupNameAttribute, preferOriginalConnection, skipMissingGroups, usingSimpleNames);
}
private static SearchControls createSearchControl(final boolean recursive, final String[] attributes) {
if (SECURITY_LOGGER.isTraceEnabled()) {
SECURITY_LOGGER.tracef("createSearchControl recursive=%b, attributes=%s", recursive, Arrays.toString(attributes));
}
// 2 - Search to identify the DN of the user connecting
SearchControls searchControls = new SearchControls();
if (recursive) {
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
} else {
searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
}
searchControls.setReturningAttributes(attributes);
searchControls.setTimeLimit(searchTimeLimit);
return searchControls;
}
private static String[] createArray(final String... elements) {
ArrayList<String> elementList = new ArrayList<String>(elements.length);
for (String current : elements) {
if (current != null) {
elementList.add(current);
}
}
return elementList.toArray(new String[elementList.size()]);
}
private static class GroupToPrincipalSearcher implements LdapSearcher<LdapEntry[], LdapEntry> {
private final String baseDn;
private final String groupDnAttribute;
private final String groupNameAttribute;
private final String[] attributeArray;
private final String filterString;
private final boolean recursive;
private final GroupName searchBy;
private final boolean preferOriginalConnection;
private GroupToPrincipalSearcher(final String baseDn, final String groupDnAttribute, final String groupNameAttribute,
final String principalAttribute, final boolean recursive, final GroupName searchBy, final boolean preferOriginalConnection) {
this.baseDn = baseDn;
this.groupDnAttribute = groupDnAttribute;
this.groupNameAttribute = groupNameAttribute;
this.attributeArray = createArray(groupDnAttribute, groupNameAttribute);
this.filterString = String.format("(%s={0})", principalAttribute);
this.recursive = recursive;
this.searchBy = searchBy;
this.preferOriginalConnection = preferOriginalConnection;
if (SECURITY_LOGGER.isTraceEnabled()) {
SECURITY_LOGGER.tracef("GroupToPrincipalSearcher baseDn=%s", baseDn);
SECURITY_LOGGER.tracef("GroupToPrincipalSearcher groupDnAttribute=%s", groupDnAttribute);
SECURITY_LOGGER.tracef("GroupToPrincipalSearcher groupNameAttribute=%s", groupNameAttribute);
SECURITY_LOGGER.tracef("GroupToPrincipalSearcher attributeArray=%s", Arrays.toString(attributeArray));
SECURITY_LOGGER.tracef("GroupToPrincipalSearcher filterString=%s", filterString);
SECURITY_LOGGER.tracef("GroupToPrincipalSearcher recursive=%b", recursive);
SECURITY_LOGGER.tracef("GroupToPrincipalSearcher searchBy=%s", searchBy);
SECURITY_LOGGER.tracef("GroupToPrincipalSearcher preferOriginalConnection=%b", preferOriginalConnection);
}
}
@Override
public LdapEntry[] search(LdapConnectionHandler originalConnectionHandler, LdapEntry entry) throws IOException, NamingException {
SearchControls searchControls = createSearchControl(recursive, attributeArray); // TODO - Can we create this in
// advance?
Set<LdapEntry> foundEntries = new HashSet<LdapEntry>();
LdapConnectionHandler connectionHandler = originalConnectionHandler;
URI referralAddress = null;
if ((referralAddress = entry.getReferralUri()) != null && preferOriginalConnection==false) {
connectionHandler = connectionHandler.findForReferral(referralAddress);
if (connectionHandler == null) {
SECURITY_LOGGER.tracef("Unable to obtain connection handler for referral URI %s", referralAddress);
return foundEntries.toArray(new LdapEntry[foundEntries.size()]);
}
} else {
// Be sure it is back to null to prevent leaking into LdapEntry for results that may be used for an iterative search.
referralAddress = null;
}
Object[] searchParameter = getSearchParameter(entry);
boolean trace = SECURITY_LOGGER.isTraceEnabled();
if (trace) {
SECURITY_LOGGER.tracef("Performing search baseDn=%s, filterString=%s, searchParameter=%s", baseDn,
filterString, Arrays.toString(searchParameter));
}
NamingEnumeration<SearchResult> searchResults = connectionHandler.getConnection().search(baseDn, filterString, searchParameter, searchControls);
if (trace && searchResults.hasMore() == false) {
SECURITY_LOGGER.trace("No search results found.");
}
while (searchResults.hasMore()) {
SearchResult current = searchResults.next();
Attributes attributes = current.getAttributes();
if (attributes != null) {
LdapEntry newEntry = convertToLdapEntry(current, attributes, referralAddress);
SECURITY_LOGGER.tracef("Adding %s", newEntry);
foundEntries.add(newEntry);
} else {
SECURITY_LOGGER.tracef("No attributes found for %s", current);
}
}
return foundEntries.toArray(new LdapEntry[foundEntries.size()]);
}
private LdapEntry convertToLdapEntry(SearchResult searchResult, Attributes attributes, final URI referralAddress) throws NamingException {
String simpleName = null;
String distinguishedName = null;
if (groupNameAttribute != null) {
SECURITY_LOGGER.tracef("Getting groupNameAttribute=%s", groupNameAttribute);
Attribute groupNameAttr = attributes.get(groupNameAttribute);
if (groupNameAttr != null) {
simpleName = (String) groupNameAttr.get();
}
}
if (groupDnAttribute != null) {
if ("dn".equals(groupDnAttribute)) {
SECURITY_LOGGER.trace("Obtaining dn using getNameInNamespace()");
distinguishedName = searchResult.getNameInNamespace();
} else {
SECURITY_LOGGER.tracef("Getting groupDnAttribute=%s", groupDnAttribute);
Attribute groupDnAttr = attributes.get(groupDnAttribute);
if (groupDnAttr != null) {
distinguishedName = (String) groupDnAttr.get();
}
}
}
return new LdapEntry(simpleName, distinguishedName, referralAddress);
}
private Object[] getSearchParameter(final LdapEntry entry) {
switch (searchBy) {
case SIMPLE:
return new String[] { entry.getSimpleName() };
default:
return new String[] { entry.getDistinguishedName() };
}
}
}
private static class PrincipalToGroupSearcher implements LdapSearcher<LdapEntry[], LdapEntry> {
private final String groupAttribute; // The attribute on the principal that references the group it is a member of.
private final String groupNameAttribute; // The attribute on the group that is it's simple name.
private final boolean preferOriginalConnection; // After a referral should we still prefer the original connection?
private final boolean skipMissingGroups;
private final boolean usingSimpleNames; // If set to true the groups are going to be mapped using a simple name so essential all entries have one.
private PrincipalToGroupSearcher(final String groupAttribute, final String groupNameAttribute, final boolean preferOriginalConnection,
final boolean skipMissingGroups, final boolean usingSimpleName) {
this.groupAttribute = groupAttribute;
this.groupNameAttribute = groupNameAttribute;
this.preferOriginalConnection = preferOriginalConnection;
this.skipMissingGroups = skipMissingGroups;
this.usingSimpleNames = usingSimpleName;
if (SECURITY_LOGGER.isTraceEnabled()) {
SECURITY_LOGGER.tracef("PrincipalToGroupSearcher groupAttribute=%s", groupAttribute);
SECURITY_LOGGER.tracef("PrincipalToGroupSearcher groupNameAttribute=%s", groupNameAttribute);
SECURITY_LOGGER.tracef("PrincipalToGroupSearcher preferOriginalConnection=%b", preferOriginalConnection);
SECURITY_LOGGER.tracef("PrincipalToGroupSearcher skipMissingGroups=%b", skipMissingGroups);
SECURITY_LOGGER.tracef("PrincipalToGroupSearcher usingSimpleNames=%b", usingSimpleNames);
}
}
@Override
public LdapEntry[] search(LdapConnectionHandler originalConnectionHandler, LdapEntry entry) throws IOException, NamingException {
Set<LdapEntry> foundEntries = new HashSet<LdapEntry>();
LdapConnectionHandler connectionHandler = originalConnectionHandler;
URI originalReferralAddress = null;
if ((originalReferralAddress = entry.getReferralUri()) != null) {
// To load the list of groups references we will always need to have followed the referral.
connectionHandler = connectionHandler.findForReferral(originalReferralAddress);
if (connectionHandler == null) {
SECURITY_LOGGER.tracef("Unable to obtain connection handler for referral URI %s", originalReferralAddress);
return foundEntries.toArray(new LdapEntry[foundEntries.size()]);
}
}
DirContext dirContext = connectionHandler.getConnection();
// Load the list of group - before reaching this point any referrals should have already been followed so we
// do not prepare to follow a referral here.
Attributes groups = dirContext.getAttributes(entry.getDistinguishedName(), new String[] { groupAttribute });
Attribute groupRef = groups.get(groupAttribute);
if (preferOriginalConnection) {
// If needed reset the connection handler back to the original in
// preparation for loading the actual groups.
connectionHandler = originalConnectionHandler;
originalReferralAddress = null;
}
boolean shouldParseGroupFromDN = Boolean.valueOf(SecurityActions.getSystemProperty(PARSE_ROLES_FROM_DN, null));
if (groupRef != null && groupRef.size() > 0) {
NamingEnumeration<String> groupRefValues = (NamingEnumeration<String>) groupRef.getAll();
while (groupRefValues.hasMore()) {
String distingushedName = groupRefValues.next().replace("\\", "\\\\").replace("/", "\\/");
SECURITY_LOGGER.tracef("Group found with distinguishedName=%s", distingushedName);
LdapConnectionHandler groupLoadHandler = connectionHandler;
URI groupReferralAddress = originalReferralAddress;
if (shouldParseGroupFromDN) {
// skip extra ldap search and instead parse group from DN. similar to parseRoleNameFromDN in LdapExtLoginModule
LdapEntry parsedGroup = parseRole(distingushedName, groupNameAttribute, groupReferralAddress);
if (parsedGroup != null) {
SECURITY_LOGGER.tracef("Parsed group %s for group with distringuishedName=%s", parsedGroup.getSimpleName(), parsedGroup.getDistinguishedName());
foundEntries.add(parsedGroup);
} else {
SECURITY_LOGGER.tracef("Failed to parse %s from distinguishedName=%s", groupNameAttribute, distingushedName);
}
continue;
}
boolean retry = false;
String simpleName = null;
do {
retry = false;
try {
dirContext = groupLoadHandler.getConnection();
// Load the Name
Attributes groupNameAttrs = dirContext.getAttributes(distingushedName,
groupNameAttribute != null ? new String[] { groupNameAttribute } : new String[] {});
if (groupNameAttribute != null) {
Attribute groupNameAttr = groupNameAttrs.get(groupNameAttribute);
if (groupNameAttr != null) {
simpleName = (String) groupNameAttr.get();
SECURITY_LOGGER.tracef("simpleName %s loaded for group with distinguishedName=%s", simpleName,
distingushedName);
} else {
SECURITY_LOGGER.tracef("attribute %s not loaded for group with distinguishedName=%s", groupNameAttr,
distingushedName);
}
} else {
SECURITY_LOGGER.trace("No groupNameAttribute to load simpleName");
}
if (simpleName == null && usingSimpleNames) {
if (skipMissingGroups == false) {
throw SECURITY_LOGGER.unableToLoadSimpleNameForGroup(distingushedName);
}
} else {
foundEntries.add(new LdapEntry(simpleName, distingushedName, groupReferralAddress));
}
} catch (NameNotFoundException e) {
SECURITY_LOGGER.tracef("Failed to query roleNameAttrName: %s", e.getMessage());
if (skipMissingGroups == false) {
throw e;
}
} catch (LdapReferralException e) {
Object info = e.getReferralInfo();
try {
URI fullUri = new URI(info.toString());
groupReferralAddress = new URI(fullUri.getScheme(), null, fullUri.getHost(), fullUri.getPort(),
null, null, null);
distingushedName = fullUri.getPath().substring(1);
SECURITY_LOGGER.tracef("Received referral with address '%s' for dn '%s'",
groupReferralAddress.toString(), distingushedName);
groupLoadHandler = groupLoadHandler.findForReferral(groupReferralAddress);
if (groupLoadHandler == null) {
SECURITY_LOGGER.tracef("Unable to follow referral to '%s'", fullUri);
}
retry = true;
} catch (URISyntaxException ue) {
SECURITY_LOGGER.tracef("Unable to construct URI from referral: %s", info);
}
}
} while (retry);
}
} else {
SECURITY_LOGGER.tracef("No groups found for %s", entry);
}
return foundEntries.toArray(new LdapEntry[foundEntries.size()]);
}
private LdapEntry parseRole(String dn, String groupNameAttribute, URI groupReferralAddress) {
try {
LdapName ldapName = new LdapName(Rdn.unescapeValue(dn).toString());
for (int i = ldapName.size() - 1; i >= 0; i--) {
String rdnString = ldapName.get(i);
Rdn rdn = new Rdn(rdnString);
Attribute attr = rdn.toAttributes().get(groupNameAttribute);
if (attr != null) {
Object value = attr.get();
if (value != null) {
return new LdapEntry( (value instanceof byte[]) ? new String((byte[]) value, StandardCharsets.UTF_8) : value.toString(), dn, groupReferralAddress);
}
}
}
} catch (NamingException e) {
SECURITY_LOGGER.tracef("Unable to parse role from DN (%s): %s", dn, e.getMessage());
}
return null;
}
}
}