Package com.google.enterprise.connector.ldap

Source Code of com.google.enterprise.connector.ldap.LdapHandler$LdapRule

// Copyright 2010 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.enterprise.connector.ldap;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Collections2;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.enterprise.connector.ldap.LdapConstants.AuthType;
import com.google.enterprise.connector.ldap.LdapConstants.ErrorMessages;
import com.google.enterprise.connector.ldap.LdapConstants.LdapConnectionError;
import com.google.enterprise.connector.ldap.LdapConstants.Method;
import com.google.enterprise.connector.ldap.LdapConstants.ServerType;

import java.io.IOException;
import java.net.NoRouteToHostException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.naming.AuthenticationException;
import javax.naming.AuthenticationNotSupportedException;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;
import javax.naming.ldap.Rdn;

/**
* This class encapsulates all interaction with jdni (javax.naming). No other
* ldap connector class should need to import anything from jdni. All
* javax.naming
* exceptions are wrapped in RuntimeException, so callers need to be careful to
* catch RuntimeException.
*/
public class LdapHandler implements LdapHandlerI {
  /**
   * Every object in LDAP has a "distinguished name" (DN). jndi does not treat
   * DN as an attribute, but we do. DN will always be present and will always be
   * unique.
   */
  public static final String DN_ATTRIBUTE = "dn";

  private static Logger LOG = Logger.getLogger(LdapHandler.class.getName());

  private LdapConnectionSettings ldapConnectionSettings = null;
  private String schemaKey = null;
  private Set<String> schema = null;
  private LdapRule rule = null;
  private int maxResults = 0;
  private String ldapConnectionTimeout = "-1";

  private LdapConnection connection = null;

  private static Function<String, String> toLower = new Function<String, String>() {
    @Override
    public String apply(String s) {
      return s.toLowerCase();
    }
  };

  public LdapHandler() {
  }
 
  public void setConnectionTimeout(String ldapConnectionTimeout) {
    this.ldapConnectionTimeout = ldapConnectionTimeout;
  }

  public String getConnectionTimeout() {
    return this.ldapConnectionTimeout;
  }

  @Override
  public void setQueryParameters(LdapRule rule, Set<String> schema, String schemaKey, int maxResults) {
    this.rule = rule;
    this.schemaKey = schemaKey;
    if (schema == null) {
      this.schema = null;
    } else {
      this.schema = Sets.newHashSet(Collections2.transform(schema, toLower));
    }
    this.maxResults = maxResults;
  }

  @Override
  public void setLdapConnectionSettings(LdapConnectionSettings ldapConnectionSettings) {
    this.ldapConnectionSettings = ldapConnectionSettings;
    LOG.fine("settings " + this.ldapConnectionSettings);
    connection = new LdapConnection(ldapConnectionSettings, getConnectionTimeout());
  }

  @Override
  public Map<LdapConnectionError, String> getErrors() {
    if (connection != null) {
      return connection.getErrors();
    }
    throw new IllegalStateException("Must successfully set connection config before getting error state");
  }

  /**
   * Convenience routine for setting up an LdapHandler from an LdapConnectorConfig.
   * This is expected to be called by Spring, for a production instance.
   */
  public static LdapHandlerI makeLdapHandlerFromConfig(LdapConnectorConfig ldapConnectorConfig) {
    LOG.fine("ldapConnectorConfig: " + ldapConnectorConfig);
    LdapHandlerI ldapHandler = new LdapHandler();
    LOG.fine("ldapHandler: " + ldapHandler);
    LdapConnectionSettings settings = ldapConnectorConfig.getSettings();
    LOG.fine("settings: " + settings);
    ldapHandler.setLdapConnectionSettings(settings);
    LdapRule rule = ldapConnectorConfig.getRule();
    Set<String> schema = ldapConnectorConfig.getSchema();
    String schemaKey = ldapConnectorConfig.getSchemaKey();
    ldapHandler.setQueryParameters(rule, schema, schemaKey, 0);
    return ldapHandler;
  }

  @VisibleForTesting
  LdapContext getLdapContext() {
    return connection.getLdapContext();
  }

  /**
   * Executes a rule. Note: the implementation of this class is based on GADS.
   * Note: execute should only be called once. To execute again, create a new
   * LdapHandler and execute that.
   *
   * @return a Map of results. The map is sorted by the schemaKey
   *         specified in the constructor. Each result is a Multimap of Strings
   *         to Strings, keyed by attributes in the schema. Results are
   *         Multimaps because ldap can store multiple values with an attribute,
   *         although in practice this is rare (except for a few attributes,
   *         like email aliases).
   */
  @Override
  public Map<String, Multimap<String, String>> get() {
    LOG.fine("entering get " + ldapConnectionSettings);

    if (ldapConnectionSettings == null) {
      throw new IllegalStateException("Must successfully set LdapConnectionSettings before get");
    }

    connection = new LdapConnection(ldapConnectionSettings, getConnectionTimeout());

    LOG.fine("connection:" + connection);

    SortedMap<String, Multimap<String, String>> result =
        new TreeMap<String, Multimap<String, String>>();

    LdapContext ctx = connection.getLdapContext();

    LOG.fine("ctx:" + ctx);

    if (ctx == null) {
      Map<LdapConnectionError, String> errors = connection.getErrors();
      if (errors != null
          && errors.containsKey(LdapConnectionError.CommunicationException)) {
        throw new LdapTransientException(
            errors.get(LdapConnectionError.CommunicationException));
      } else {
        throw new IllegalStateException(
            ErrorMessages.UNKNOWN_CONNECTION_ERROR.toString());
      }
    }

    NamingEnumeration<SearchResult> ldapResults = null;
    int resultCount = 0;
    byte[] cookie = null;
    try {     
     
      do {
        SearchControls controls = makeControls(rule, schema);

        LOG.info("Ldap search begin");
        ldapResults = ctx.search("", // Filter is always relative to our base dn
            rule.getFilter(), controls);

        // Process results.
        while (ldapResults.hasMore()) {
          resultCount++;

          Multimap<String, String> thisResult = ArrayListMultimap.create();

          SearchResult searchResult = ldapResults.next();
          Attributes attributes = searchResult.getAttributes();

          // We don't see our DN as a normal attribute, we have to ask for it
          // separately.
          String canonicalDn = canonicalDn(searchResult.getNameInNamespace());
          thisResult.put(DN_ATTRIBUTE, canonicalDn);

          if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("ldap search result " + resultCount + " dn " + canonicalDn);
          }

          // Add all our attributes to this result object
          handleAttrs(thisResult, searchResult, result, attributes);

          String keyValue = getFirst(schemaKey, thisResult);
          if (keyValue == null) {
            LOG.warning("Ldap result" + canonicalDn +
                " is missing schema key attribute " + schemaKey + ": skipping");
          } else {
            result.put(keyValue, thisResult);
          }
          if (maxResults > 0 && resultCount >= maxResults) {
            break;
          }
        }

        if (LOG.isLoggable(Level.INFO)) {
          LOG.info("ldap search intermediate result count " + resultCount);
        }

        // Examine the paged results control response
        // This may be null if the server does not support paged results
        Control[] pagedControls = ctx.getResponseControls();
        if (controls != null && pagedControls != null) {
          for (int i = 0; i < pagedControls.length; i++) {
            if (pagedControls[i] instanceof PagedResultsResponseControl) {
              PagedResultsResponseControl prrc =
                  (PagedResultsResponseControl) pagedControls[i];
              cookie = prrc.getCookie();
            } else {
              // Handle other response controls (if any)
            }
          }
        }
        // Re-activate paged results
        // Note: this code is from GADS
        // TODO: decide whether this is really needed for the ldap connector
        ctx.setRequestControls(new Control[] {new PagedResultsControl(LdapConnection.PAGESIZE,
            cookie, Control.NONCRITICAL)});
      } while (!shouldStop(cookie));

    } catch (CommunicationException e) {
      throw new LdapTransientException(e);
    } catch (NameNotFoundException e) {
      throw new IllegalStateException(e);
    } catch (NamingException e) {
      throw new IllegalStateException(e);
    } catch (IOException e) {
      throw new IllegalStateException(e);
    } finally {
      // Clean up everything.
      if (ldapResults != null) {
        try {
          ldapResults.close();
        } catch (Exception e) {
          LOG.log(Level.WARNING, "ldap_connection_cleanup_error_on_results", e);
        }
      }
      if (ctx != null) {
        try {
          ctx.close();
        } catch (Exception e) {
          LOG.log(Level.WARNING, "ldap_connection_cleanup_error_on_context", e);
        }
      }
      connection = null;
    }
    LOG.info("ldap search final result count " + resultCount);
    return result;
  }

  private static boolean shouldStop(byte[] cookie) {
    boolean shouldStop = (cookie == null);
    if (shouldStop) {
      LOG.fine("No more paged results - stopping.");
    } else {
      LOG.fine("Looking for another page");
    }
    return shouldStop;
  }

  private String getFirst(String key, Multimap<String, String> m) {
    for (String value : m.get(key)) {
      return value;
    }
    return null;
  }

  private void handleAttrs(Multimap<String, String> thisResult,
      SearchResult searchResult, SortedMap<String, Multimap<String, String>> result,
      Attributes attributes) throws NamingException {

    NamingEnumeration<? extends Attribute> allAttrs = attributes.getAll();

    while (allAttrs.hasMore()) {
      Attribute attr = allAttrs.next();
      String attrName = attr.getID().toLowerCase();
      // treat a null schema by returning all attributes
      // otherwise only return attributes in the schema
      if (schema == null || schema.contains(attrName)) {
        // Add each attribute value (most only have one)
        for (int i = 0; i < attr.size(); i++) {
          Object attributeValue = attr.get(i);
          if (attributeValue instanceof String) {
            String value = (String) attr.get(i);
            thisResult.put(attrName, value);
          } else if (attributeValue.getClass().isAssignableFrom(byte[].class)) {
            // skip this attribute - we only deal with Strings
            // This means we can't deal with encrypted strings (e.g. passwords)
            // or byte arrays (photos)
            // TODO: maybe report this?
          }
        }
      }
    }
  }

  private SearchControls makeControls(LdapRule rule, Set<String> allNotableAttributes) {
    SearchControls controls = new SearchControls();
    // Set scope as appropriate from the rule.
    if (rule.getScope() == LdapRule.Scope.SUBTREE) {
      controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
    } else if (rule.getScope() == LdapRule.Scope.OBJECT) {
      controls.setSearchScope(SearchControls.OBJECT_SCOPE);
    } else if (rule.getScope() == LdapRule.Scope.ONELEVEL) {
      controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
    } else {
      throw new RuntimeException("ldap_search_invalid_rule_scope " + rule.getScope());
    }
    if (allNotableAttributes == null || allNotableAttributes.size() == 0) {
      // Return all attributes
      controls.setReturningAttributes(null);
    } else {
      // Only return our specified attributes.
      String[] returnAttrs = new String[0];
      returnAttrs = allNotableAttributes.toArray(returnAttrs);
      controls.setReturningAttributes(returnAttrs);
    }
    return controls;
  }

  /**
   * Return a canonical form for the DN: DN lower-cased.
   * spaces around commas deleted
   * any \{2 hexdigits} sequences replaced with that byte (other than slash)
   * Note: this code is from GADS
   * TODO: decide whether this is really needed for the ldap connector
   */
  public static String canonicalDn(String origDn) {
    if (origDn == null) {
      return null;
    }
    origDn = origDn.toLowerCase();
    try {
      return Rdn.unescapeValue(origDn).toString().replaceAll("/", "%2F");
    } catch (IllegalArgumentException e) {
      LOG.log(Level.INFO, "Potentially invalid LDAP DN found: " + origDn, e);
    }
    // we only do this if the Rdn parsing above threw an exception
    return origDn.replaceAll(" *, *", ",").replaceAll("/", "%2F");
  }

  /**
   * A connection to an Ldap Server
   */
  private static class LdapConnection {

    private static final String COM_SUN_JNDI_LDAP_LDAP_CTX_FACTORY =
        "com.sun.jndi.ldap.LdapCtxFactory";
    private static final String COM_SUN_JNDI_LDAP_CONNECT_TIMEOUT =
        "com.sun.jndi.ldap.connect.timeout";
   
    private final LdapConnectionSettings settings;
    private LdapContext ldapContext = null;
    private final Map<LdapConnectionError, String> errors;
    private String connectionTimeOut;

    public static final int PAGESIZE = 1000;
   
    public LdapConnection(LdapConnectionSettings ldapConnectionSettings, String connectionTimeOut) {
      LOG.fine("Configuring LdapConnection with settings: " + ldapConnectionSettings);
      this.settings = ldapConnectionSettings;
      this.errors = Maps.newHashMap();
      this.connectionTimeOut = connectionTimeOut;
      Hashtable<String, String> env = configureLdapEnvironment();
      ldapContext = makeContext(env, PAGESIZE);
    }

    public LdapContext getLdapContext() {
      return ldapContext;
    }

    public Map<LdapConnectionError, String> getErrors() {
      return errors;
    }

    private LdapContext makeContext(Hashtable<String, String> env, int pageSize) {
      LdapContext ctx = null;
      try {
        ctx = new InitialLdapContext(env, null);
      } catch (CommunicationException e) {
        LOG.info("Communication error : " + e.toString());

        if (e.getCause() instanceof NoRouteToHostException) {
          errors.put(LdapConnectionError.CommunicationException,
              e.getCause().getMessage());
        } else if (e.getCause() instanceof SocketTimeoutException) {
          errors.put(LdapConnectionError.CommunicationExceptionTimeout, e.getCause().getMessage());
        } else if (e.getCause() instanceof UnknownHostException) {
          errors.put(LdapConnectionError.CommunicationExceptionUnknownhost, e.getCause()
              .getMessage());
        } else {
          errors.put(LdapConnectionError.CommunicationException, e.getMessage());
        }
      } catch (AuthenticationNotSupportedException e) {
        errors.put(LdapConnectionError.AuthenticationNotSupported, e.getMessage());
      } catch (AuthenticationException e) {
        errors.put(LdapConnectionError.AuthenticationException, e.getMessage());
      } catch (NamingException e) {
        errors.put(LdapConnectionError.NamingException, e.getMessage());
      }
      if (ctx == null) {
        return null;
      }
      try {
        ctx.setRequestControls(new Control[] {new PagedResultsControl(pageSize,
            Control.NONCRITICAL)});
      } catch (NamingException e) {
        errors.put(LdapConnectionError.NamingException, e.getMessage());
      } catch (IOException e) {
        errors.put(LdapConnectionError.IOException, e.getMessage());
      }
      return ctx;
    }

    private String makeLdapUrl() {
      String url;
      Method connectMethod =
          settings.getConnectMethod();
      if (connectMethod == Method.SSL) {
        url = "ldaps://"; //$NON-NLS-1$
      } else {
        url = "ldap://"; //$NON-NLS-1$
      }

      // Construct the full URL
      url = url + settings.getHostname();
      if (settings.getPort() > 0) {
        url = url + ":" + settings.getPort();
      }
      url = url + "/";

      if (settings.getBaseDN() != null) {
        url = url + encodeBaseDN(settings.getBaseDN());
      }
      return url;
    }

    /**
     * Initialize the Hashtable used to create an initial LDAP Context. Note
     * that we specifically require a Hashtable rather than a HashMap as the
     * parameter type in the InitialLDAPContext constructor
     *
     * @return initialized Hashtable suitable for constructing an
     *         InitiaLdaplContext
     */
    private Hashtable<String, String> configureLdapEnvironment() {
      Hashtable<String, String> env = new Hashtable<String, String>();

      // Use the built-in LDAP support.
      env.put(Context.INITIAL_CONTEXT_FACTORY, COM_SUN_JNDI_LDAP_LDAP_CTX_FACTORY);

      // property to indicate to the server how to handle referrals
      env.put(Context.REFERRAL, "follow");

      // force the following attributes to be returned as binary data
      env.put("java.naming.ldap.attributes.binary", "objectGUID objectSid");
     
      // Specify connection timeout, value of zero or less means use networks timeout value
      env.put(COM_SUN_JNDI_LDAP_CONNECT_TIMEOUT, connectionTimeOut);   

      // Set our authentication settings.
      AuthType authType = settings.getAuthType();
      if (authType == AuthType.SIMPLE) {
        env.put(Context.SECURITY_AUTHENTICATION, authType.toString()
            .toLowerCase());
        env.put(Context.SECURITY_PRINCIPAL, settings.getUsername());
        env.put(Context.SECURITY_CREDENTIALS, settings.getPassword());
        LOG.info("Using simple authentication.");
      } else {
        if (authType != AuthType.ANONYMOUS) {
          LOG.warning("Unknown authType - falling back to anonymous.");
        } else {
          LOG.info("Using anonymous authentication.");
        }
        env.put(Context.SECURITY_AUTHENTICATION, "none"); //$NON-NLS-1$
      }
      env.put(Context.PROVIDER_URL, makeLdapUrl());
      return env;
    }

    /**
     * We have to do some simple, naive escaping of the base DN. We CANNOT use
     * normal URL escaping, as '+' is not handled properly by the JNDI backend.
     */
    private String encodeBaseDN(String origValue) {
      origValue = origValue.replace(" ", "%20");
      return origValue;
    }

  }

  /**
   * Configuration for an ldap connection. Immutable, static data class.
   */
  public static class LdapConnectionSettings {

    private final String hostname;
    private final int port;
    private final AuthType authType;
    private final String username;
    private final String password;
    private final Method connectMethod;
    private final String baseDN;
    private final ServerType serverType;

    public LdapConnectionSettings(Method connectMethod, String hostname,
        int port, String baseDN, AuthType authType, String username, String password) {
      this.authType = authType;
      this.baseDN = baseDN;
      this.connectMethod = connectMethod;
      this.hostname = hostname;
      this.password = password;
      this.port = port;
      this.serverType = ServerType.GENERIC;
      this.username = username;
    }

    public LdapConnectionSettings(Method connectMethod, String hostname,
        int port, String baseDN) {
      this.authType = AuthType.ANONYMOUS;
      this.baseDN = baseDN;
      this.connectMethod = connectMethod;
      this.hostname = hostname;
      this.password = null;
      this.port = port;
      this.serverType = ServerType.GENERIC;
      this.username = null;
    }

    @Override
    public String toString() {
      String displayPassword;
      if (password == null) {
        displayPassword = "null";
      } else if (password.length() < 1) {
        displayPassword = "<empty>";
      } else {
        displayPassword = "####";
      }
      return "LdapConnectionSettings [authType=" + authType + ", baseDN=" + baseDN
          + ", connectMethod=" + connectMethod + ", hostname=" + hostname + ", password="
          + displayPassword + ", port=" + port + ", serverType=" + serverType + ", username=" + username
          + "]";
    }

    public AuthType getAuthType() {
      return authType;
    }

    public String getBaseDN() {
      return baseDN;
    }

    public Method getConnectMethod() {
      return connectMethod;
    }

    public String getHostname() {
      return hostname;
    }

    public String getPassword() {
      return password;
    }

    public int getPort() {
      return port;
    }

    public ServerType getServerType() {
      return serverType;
    }

    public String getUsername() {
      return username;
    }
  }

  /**
   * Configuration for an ldap rule (query). Immutable, static data class.
   */
  public static class LdapRule {
    public enum Scope {
      SUBTREE, ONELEVEL, OBJECT
    }

    private final Scope scope;
    private final String filter;

    public LdapRule(Scope scope, String filter) {
      this.scope = scope;
      this.filter = filter;
    }

    public Scope getScope() {
      return scope;
    }

    public String getFilter() {
      return filter;
    }
  }
}
TOP

Related Classes of com.google.enterprise.connector.ldap.LdapHandler$LdapRule

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.