/* * ==================== * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of the Common Development * and Distribution License("CDDL") (the "License"). You may not use this file * except in compliance with the License. * * You can obtain a copy of the License at * http://IdentityConnectors.dev.java.net/legal/license.txt * See the License for the specific language governing permissions and limitations * under the License. * * When distributing the Covered Code, include this CDDL Header Notice in each file * and include the License file at identityconnectors/legal/license.txt. * If applicable, add the following below this CDDL Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * ==================== */ package org.identityconnectors.ldap.search; import static java.util.Collections.singletonList; import static org.identityconnectors.common.CollectionUtil.newCaseInsensitiveSet; import static org.identityconnectors.common.CollectionUtil.newSet; import static org.identityconnectors.common.StringUtil.isBlank; import static org.identityconnectors.ldap.LdapUtil.getStringAttrValues; import java.util.Arrays; import java.util.List; import java.util.Set; import javax.naming.NamingException; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.naming.ldap.PagedResultsControl; import org.identityconnectors.common.logging.Log; import org.identityconnectors.common.security.GuardedString; import org.identityconnectors.framework.common.objects.Attribute; import org.identityconnectors.framework.common.objects.AttributeBuilder; import org.identityconnectors.framework.common.objects.AttributeInfo; import org.identityconnectors.framework.common.objects.ConnectorObject; import org.identityconnectors.framework.common.objects.ConnectorObjectBuilder; import org.identityconnectors.framework.common.objects.Name; import org.identityconnectors.framework.common.objects.ObjectClass; import org.identityconnectors.framework.common.objects.ObjectClassInfo; import org.identityconnectors.framework.common.objects.OperationOptions; import org.identityconnectors.framework.common.objects.OperationalAttributes; import org.identityconnectors.framework.common.objects.QualifiedUid; import org.identityconnectors.framework.common.objects.ResultsHandler; import org.identityconnectors.framework.common.objects.Uid; import org.identityconnectors.ldap.GroupHelper; import org.identityconnectors.ldap.LdapConnection; import org.identityconnectors.ldap.LdapConstants; import org.identityconnectors.ldap.LdapEntry; import org.identityconnectors.ldap.schema.LdapSchemaMapping; import com.sun.jndi.ldap.ctl.VirtualListViewControl; /** * A class to perform an LDAP search against a {@link LdapConnection}. * * @author Andrei Badea */ public class LdapSearch { private static final Log log = Log.getLog(LdapSearch.class); private final LdapConnection conn; private final ObjectClass oclass; private final LdapFilter filter; private final OperationOptions options; private final GroupHelper groupHelper; private final String[] baseDNs; public static Set<String> getAttributesReturnedByDefault(LdapConnection conn, ObjectClass oclass) { if (oclass.equals(LdapSchemaMapping.ANY_OBJECT_CLASS)) { return newSet(Name.NAME); } Set<String> result = newCaseInsensitiveSet(); ObjectClassInfo oci = conn.getSchemaMapping().schema().findObjectClassInfo(oclass.getObjectClassValue()); if (oci != null) { for (AttributeInfo info : oci.getAttributeInfo()) { if (info.isReturnedByDefault()) { result.add(info.getName()); } } } return result; } public LdapSearch(LdapConnection conn, ObjectClass oclass, LdapFilter filter, OperationOptions options) { this(conn, oclass, filter, options, conn.getConfiguration().getBaseContexts()); } public LdapSearch(LdapConnection conn, ObjectClass oclass, LdapFilter filter, OperationOptions options, String... baseDNs) { this.conn = conn; this.oclass = oclass; this.filter = filter; this.options = options; this.baseDNs = baseDNs; groupHelper = new GroupHelper(conn); } /** * Performs the search and passes the resulting {@link ConnectorObject}s to * the given handler. * * @param handler * the handler. * @throws NamingException * if a JNDI exception occurs. */ public final void execute(final ResultsHandler handler) { final String[] attrsToGetOption = options.getAttributesToGet(); final Set<String> attrsToGet = getAttributesToGet(attrsToGetOption); LdapInternalSearch search = getInternalSearch(attrsToGet); search.execute(new SearchResultsHandler() { public boolean handle(String baseDN, SearchResult result) throws NamingException { return handler.handle(createConnectorObject(baseDN, result, attrsToGet, attrsToGetOption != null)); } }); } /** * Executes the query against all configured base DNs and returns the first * {@link ConnectorObject} or {@code null}. */ public final ConnectorObject getSingleResult() { final String[] attrsToGetOption = options.getAttributesToGet(); final Set<String> attrsToGet = getAttributesToGet(attrsToGetOption); final ConnectorObject[] results = new ConnectorObject[] { null }; LdapInternalSearch search = getInternalSearch(attrsToGet); search.execute(new SearchResultsHandler() { public boolean handle(String baseDN, SearchResult result) throws NamingException { results[0] = createConnectorObject(baseDN, result, attrsToGet, attrsToGetOption != null); return false; } }); return results[0]; } private LdapInternalSearch getInternalSearch(Set<String> attrsToGet) { // This is a bit tricky. If the LdapFilter has an entry DN, // we only need to look at that entry and check whether it matches // the native filter. Moreover, when looking at the entry DN // we must not throw exceptions if the entry DN does not exist or is // not valid -- just as no exceptions are thrown when the native // filter doesn't return any values. // // In the simple case when the LdapFilter has no entryDN, we // will just search over our base DNs looking for entries // matching the native filter. LdapSearchStrategy strategy; List<String> baseDNs; int searchScope; String filterEntryDN = filter != null ? filter.getEntryDN() : null; if (filterEntryDN != null) { // Would be good to check that filterEntryDN is under the configured base contexts. // However, the adapter is likely to pass entries outside the base contexts, // so not checking in order to be on the safe side. strategy = new DefaultSearchStrategy(true); baseDNs = singletonList(filterEntryDN); searchScope = SearchControls.OBJECT_SCOPE; } else { strategy = getSearchStrategy(); baseDNs = getBaseDNs(); searchScope = getLdapSearchScope(); } SearchControls controls = LdapInternalSearch.createDefaultSearchControls(); Set<String> ldapAttrsToGet = getLdapAttributesToGet(attrsToGet); controls.setReturningAttributes(ldapAttrsToGet.toArray(new String[ldapAttrsToGet.size()])); controls.setSearchScope(searchScope); String optionsFilter = LdapConstants.getSearchFilter(options); String userFilter = null; if (oclass.equals(ObjectClass.ACCOUNT)) { userFilter = conn.getConfiguration().getAccountSearchFilter(); } String nativeFilter = filter != null ? filter.getNativeFilter() : null; return new LdapInternalSearch(conn, getSearchFilter(optionsFilter, nativeFilter, userFilter), baseDNs, strategy, controls); } private Set<String> getLdapAttributesToGet(Set<String> attrsToGet) { Set<String> cleanAttrsToGet = newCaseInsensitiveSet(); cleanAttrsToGet.addAll(attrsToGet); cleanAttrsToGet.remove(LdapConstants.LDAP_GROUPS_NAME); boolean posixGroups = cleanAttrsToGet.remove(LdapConstants.POSIX_GROUPS_NAME); Set<String> result = conn.getSchemaMapping().getLdapAttributes(oclass, cleanAttrsToGet, true); if (posixGroups) { result.add(GroupHelper.getPosixRefAttribute()); } // For compatibility with the adapter, we do not ask the server for DN attributes, // such as entryDN; we compute them ourselves. Some servers might not support such attributes anyway. result.removeAll(LdapEntry.ENTRY_DN_ATTRS); return result; } /** * Creates a {@link ConnectorObject} based on the given search result. The * search result name is expected to be a relative one, thus the {@code * baseDN} parameter is needed in order to create the whole entry DN, which * is used to compute the connector object's name attribute. */ private ConnectorObject createConnectorObject(String baseDN, SearchResult result, Set<String> attrsToGet, boolean emptyAttrWhenNotFound) { LdapEntry entry = LdapEntry.create(baseDN, result); ConnectorObjectBuilder builder = new ConnectorObjectBuilder(); builder.setObjectClass(oclass); builder.setUid(conn.getSchemaMapping().createUid(oclass, entry)); builder.setName(conn.getSchemaMapping().createName(oclass, entry)); for (String attrName : attrsToGet) { Attribute attribute = null; if (LdapConstants.isLdapGroups(attrName)) { List<String> ldapGroups = groupHelper.getLdapGroups(entry.getDN().toString()); attribute = AttributeBuilder.build(LdapConstants.LDAP_GROUPS_NAME, ldapGroups); } else if (LdapConstants.isPosixGroups(attrName)) { Set<String> posixRefAttrs = getStringAttrValues(entry.getAttributes(), GroupHelper.getPosixRefAttribute()); List<String> posixGroups = groupHelper.getPosixGroups(posixRefAttrs); attribute = AttributeBuilder.build(LdapConstants.POSIX_GROUPS_NAME, posixGroups); } else if (LdapConstants.PASSWORD.is(attrName)) { attribute = AttributeBuilder.build(attrName, new GuardedString()); } else { attribute = conn.getSchemaMapping().createAttribute(oclass, attrName, entry, emptyAttrWhenNotFound); } if (attribute != null) { builder.addAttribute(attribute); } } return builder.build(); } /** * Creates a search filter which will filter to a given {@link ObjectClass}. * It will be composed of an optional filter to be applied before the object class filters, * the filters for all LDAP object classes for the given {@code ObjectClass}, and * an optional filter to be applied before the object class filters. */ private String getSearchFilter(String... optionalFilters) { StringBuilder builder = new StringBuilder(); String ocFilter = getObjectClassFilter(); int nonBlank = isBlank(ocFilter) ? 0 : 1; for (String optionalFilter : optionalFilters) { nonBlank += (isBlank(optionalFilter) ? 0 : 1); } if (nonBlank > 1) { builder.append("(&"); } appendFilter(ocFilter, builder); for (String optionalFilter : optionalFilters) { appendFilter(optionalFilter, builder); } if (nonBlank > 1) { builder.append(')'); } return builder.toString(); } private String getObjectClassFilter() { StringBuilder builder = new StringBuilder(); List<String> ldapClasses = conn.getSchemaMapping().getLdapClasses(oclass); boolean and = ldapClasses.size() > 1; if (and) { builder.append("(&"); } for (String ldapClass : ldapClasses) { builder.append("(objectClass="); builder.append(ldapClass); builder.append(')'); } if (and) { builder.append(')'); } return builder.toString(); } private static void appendFilter(String filter, StringBuilder toBuilder) { if (!isBlank(filter)) { String trimmedUserFilter = filter.trim(); boolean enclose = filter.charAt(0) != '('; if (enclose) { toBuilder.append('('); } toBuilder.append(trimmedUserFilter); if (enclose) { toBuilder.append(')'); } } } private List<String> getBaseDNs() { List<String> result; QualifiedUid container = options.getContainer(); if (container != null) { result = singletonList(LdapSearches.findEntryDN(conn, container.getObjectClass(), container.getUid())); } else { result = Arrays.asList(baseDNs); } assert result != null; return result; } private LdapSearchStrategy getSearchStrategy() { LdapSearchStrategy strategy; if (ObjectClass.ACCOUNT.equals(oclass)) { // Only consider paged strategies for accounts, just as the adapter does. boolean useBlocks = conn.getConfiguration().isUseBlocks(); boolean usePagedResultsControl = conn.getConfiguration().isUsePagedResultControl(); int pageSize = conn.getConfiguration().getBlockSize(); if (useBlocks && !usePagedResultsControl && conn.supportsControl(VirtualListViewControl.OID)) { String vlvSortAttr = conn.getConfiguration().getVlvSortAttribute(); strategy = new VlvIndexSearchStrategy(vlvSortAttr, pageSize); } else if (useBlocks && conn.supportsControl(PagedResultsControl.OID)) { strategy = new SimplePagedSearchStrategy(pageSize); } else { strategy = new DefaultSearchStrategy(false); } } else { strategy = new DefaultSearchStrategy(false); } return strategy; } private Set<String> getAttributesToGet(String[] attributesToGet) { Set<String> result; if (attributesToGet != null) { result = newCaseInsensitiveSet(); result.addAll(Arrays.asList(attributesToGet)); removeNonReadableAttributes(result); result.add(Name.NAME); } else { // This should include Name.NAME, so no need to include it explicitly. result = getAttributesReturnedByDefault(conn, oclass); } // Since Uid is not in the schema, but it is required to construct a ConnectorObject. result.add(Uid.NAME); // Our password is marked as readable because of sync(). We really can't return it from search. if (result.contains(OperationalAttributes.PASSWORD_NAME)) { log.warn("Reading passwords not supported"); // throw new ConnectorException(conn.format("readingPasswordsNotSupported", null)); } return result; } private void removeNonReadableAttributes(Set<String> attributes) { // Since the groups attributes are fake attributes, we don't want to // send them to LdapSchemaMapping. This, for example, avoid an (unlikely) // conflict with a custom attribute defined in the server schema. boolean ldapGroups = attributes.remove(LdapConstants.LDAP_GROUPS_NAME); boolean posixGroups = attributes.remove(LdapConstants.POSIX_GROUPS_NAME); conn.getSchemaMapping().removeNonReadableAttributes(oclass, attributes); if (ldapGroups) { attributes.add(LdapConstants.LDAP_GROUPS_NAME); } if (posixGroups) { attributes.add(LdapConstants.POSIX_GROUPS_NAME); } } private int getLdapSearchScope() { String scope = options.getScope(); if (OperationOptions.SCOPE_OBJECT.equals(scope)) { return SearchControls.OBJECT_SCOPE; } else if (OperationOptions.SCOPE_ONE_LEVEL.equals(scope)) { return SearchControls.ONELEVEL_SCOPE; } else if (OperationOptions.SCOPE_SUBTREE.equals(scope) || scope == null) { return SearchControls.SUBTREE_SCOPE; } else { throw new IllegalArgumentException("Invalid search scope " + scope); } } }