All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.connid.bundles.ldap.search.LdapSearch Maven / Gradle / Ivy

The newest version!
/**
 * ====================
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved.
 * Copyright 2011-2013 Tirasa. 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 https://oss.oracle.com/licenses/CDDL
 * 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 https://oss.oracle.com/licenses/CDDL.
 * 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.connid.bundles.ldap.search;

import static java.util.Collections.singletonList;
import static org.connid.bundles.ldap.commons.LdapUtil.getStringAttrValues;
import static org.identityconnectors.common.CollectionUtil.newCaseInsensitiveSet;
import static org.identityconnectors.common.CollectionUtil.newSet;
import static org.identityconnectors.common.StringUtil.isBlank;

import com.sun.jndi.ldap.ctl.VirtualListViewControl;
import java.util.ArrayList;
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.connid.bundles.ldap.LdapConnection;
import org.connid.bundles.ldap.commons.GroupHelper;
import org.connid.bundles.ldap.commons.LdapConstants;
import org.connid.bundles.ldap.commons.LdapEntry;
import org.connid.bundles.ldap.commons.StatusManagement;
import org.connid.bundles.ldap.schema.LdapSchemaMapping;
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;

/**
 * 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 getAttributesReturnedByDefault(
            final LdapConnection conn, final ObjectClass oclass) {
        if (oclass.equals(LdapSchemaMapping.ANY_OBJECT_CLASS)) {
            return newSet(Name.NAME);
        }
        Set 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(
            final LdapConnection conn,
            final ObjectClass oclass,
            final LdapFilter filter,
            final OperationOptions options) {

        this(conn, oclass, filter, options, conn.getConfiguration().getBaseContexts());
    }

    public LdapSearch(
            final LdapConnection conn,
            final ObjectClass oclass,
            final LdapFilter filter,
            final OperationOptions options,
            final 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 attrsToGet = getAttributesToGet(attrsToGetOption);

        final LdapInternalSearch search = getInternalSearch(attrsToGet);

        search.execute(new SearchResultsHandler() {

            @Override
            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 attrsToGet = getAttributesToGet(attrsToGetOption);
        final ConnectorObject[] results = new ConnectorObject[]{null};

        final LdapInternalSearch search = getInternalSearch(attrsToGet);

        search.execute(new SearchResultsHandler() {

            @Override
            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(final Set 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 dns;
        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);
            dns = singletonList(filterEntryDN);
            searchScope = SearchControls.OBJECT_SCOPE;
        } else {
            strategy = getSearchStrategy();
            dns = getBaseDNs();
            searchScope = getLdapSearchScope();
        }

        SearchControls controls = LdapInternalSearch.createDefaultSearchControls();
        Set 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),
                dns, strategy, controls);
    }

    private Set getLdapAttributesToGet(final Set attrsToGet) {
        final Set cleanAttrsToGet = newCaseInsensitiveSet();

        cleanAttrsToGet.addAll(attrsToGet);
        cleanAttrsToGet.remove(LdapConstants.LDAP_GROUPS_NAME);

        final boolean posixGroups = cleanAttrsToGet.remove(LdapConstants.POSIX_GROUPS_NAME);

        final Set result = conn.getSchemaMapping().getLdapAttributes(oclass, cleanAttrsToGet, true);

        if (posixGroups) {
            result.add(GroupHelper.getPosixRefAttribute());
        }

        // Add attributes needed to define the entity status.
        // This attributes won't be attached to the connector object.
        result.addAll(StatusManagement.getInstance(
                conn.getConfiguration().getStatusManagementClass()).getOperationalAttributes());

        // 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(
            final String baseDN,
            final SearchResult result,
            final Set attrsToGet,
            final boolean emptyAttrWhenNotFound) {

        final LdapEntry entry = LdapEntry.create(baseDN, result);

        final ConnectorObjectBuilder builder = new ConnectorObjectBuilder();
        builder.setObjectClass(oclass);
        builder.setUid(conn.getSchemaMapping().createUid(oclass, entry));
        builder.setName(conn.getSchemaMapping().createName(oclass, entry));

        final List ldapGroups = new ArrayList();
        final List posixGroups = new ArrayList();

        for (String attrName : attrsToGet) {
            Attribute attribute = null;
            if (LdapConstants.isLdapGroups(attrName)) {
                ldapGroups.addAll(groupHelper.getLdapGroups(entry.getDN().toString()));

                attribute = AttributeBuilder.build(LdapConstants.LDAP_GROUPS_NAME, ldapGroups);
            } else if (LdapConstants.isPosixGroups(attrName)) {
                final Set posixRefAttrs =
                        getStringAttrValues(entry.getAttributes(), GroupHelper.getPosixRefAttribute());

                posixGroups.addAll(groupHelper.getPosixGroups(posixRefAttrs));

                attribute = AttributeBuilder.build(LdapConstants.POSIX_GROUPS_NAME, posixGroups);
            } else if (LdapConstants.PASSWORD.is(attrName)
                && !conn.getConfiguration().getRetrievePasswordsWithSearch()) {
                attribute = AttributeBuilder.build(attrName, new GuardedString());
            } else {
                attribute = conn.getSchemaMapping().createAttribute(oclass, attrName, entry, emptyAttrWhenNotFound);
            }

            if (attribute != null) {
                builder.addAttribute(attribute);
            }
        }

        final Boolean status = StatusManagement.getInstance(
                conn.getConfiguration().getStatusManagementClass()).
                getStatus(result.getAttributes(), posixGroups, ldapGroups);

        if (status != null) {
            builder.addAttribute(AttributeBuilder.buildEnabled(status));
        }

        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(final 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 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)) {
            final String trimmedUserFilter = filter.trim();
            final boolean enclose = filter.charAt(0) != '(';

            if (enclose) {
                toBuilder.append('(');
            }

            toBuilder.append(trimmedUserFilter);

            if (enclose) {
                toBuilder.append(')');
            }
        }
    }

    private List getBaseDNs() {
        List result;

        final 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 getAttributesToGet(final String[] attributesToGet) {
        Set 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 (!conn.getConfiguration().getRetrievePasswordsWithSearch()
            && result.contains(OperationalAttributes.PASSWORD_NAME)) {
            LOG.warn("Reading passwords not supported");
        }

        return result;
    }

    private void removeNonReadableAttributes(final Set 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);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy