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

org.apache.james.user.ldap.ReadOnlyUsersLDAPRepository Maven / Gradle / Ivy

/****************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one   *
 * or more contributor license agreements.  See the NOTICE file *
 * distributed with this work for additional information        *
 * regarding copyright ownership.  The ASF licenses this file   *
 * to you 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 org.apache.james.user.ldap;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.annotation.PostConstruct;
import javax.naming.Context;
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.InitialLdapContext;
import javax.naming.ldap.LdapContext;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.james.lifecycle.api.Configurable;
import org.apache.james.lifecycle.api.LogEnabled;
import org.apache.james.user.api.UsersRepository;
import org.apache.james.user.api.UsersRepositoryException;
import org.apache.james.user.api.model.User;
import org.apache.james.user.ldap.api.LdapConstants;
import org.apache.james.util.retry.DoublingRetrySchedule;
import org.apache.james.util.retry.api.RetrySchedule;
import org.apache.james.util.retry.naming.ldap.RetryingLdapContext;

import org.slf4j.Logger;

/**
 * 

* This repository implementation serves as a bridge between Apache James and * LDAP. It allows James to authenticate users against an LDAP compliant server * such as Apache DS or Microsoft AD. It also enables role/group based access * restriction based on LDAP groups. *

*

* It is intended for organisations that already have a user-authentication and * authorisation mechanism in place, and want to leverage this when deploying * James. The assumption inherent here is that such organisations would not want * to manage user details via James, but will do so externally using whatever * mechanism provided by, or built on top off, their LDAP implementation. *

*

* Based on this assumption, this repository is strictly read-only. As a * consequence, user modification, deletion and creation requests will be * ignored when using this repository. *

*

* The following fragment of XML provides an example configuration to enable * this repository:
* *

 *  <users-store>
 *      <repository name="LDAPUsers" 
 *      class="org.apache.james.userrepository.ReadOnlyUsersLDAPRepository" 
 *      ldapHost="ldap://myldapserver:389"
 *      principal="uid=ldapUser,ou=system"
 *      credentials="password"
 *      userBase="ou=People,o=myorg.com,ou=system"
 *      userIdAttribute="uid"
 *      userObjectClass="inetOrgPerson"
 *      maxRetries="20"
 *      retryStartInterval="0"
 *      retryMaxInterval="30"
 *      retryIntervalScale="1000"
 *  </users-store>
 * 
* *
* * Its constituent attributes are defined as follows: *
    *
  • ldapHost: The URL of the LDAP server to connect to.
  • *
  • * principal: (optional) The name (DN) of the user with which to * initially bind to the LDAP server.
  • *
  • * credentials: (optional) The password with which to initially bind to * the LDAP server.
  • *
  • * userBase:The context within which to search for user entities.
  • *
  • * userIdAttribute:The name of the LDAP attribute which holds user ids. * For example "uid" for Apache DS, or "sAMAccountName" for * Microsoft Active Directory.
  • *
  • * userObjectClass:The objectClass value for user nodes below the * userBase. For example "inetOrgPerson" for Apache DS, or * "user" for Microsoft Active Directory.
  • ** *
  • * maxRetries: (optional, default = 0) The maximum number of times to * retry a failed operation. -1 means retry forever.
  • *
  • * retryStartInterval: (optional, default = 0) The interval in * milliseconds to wait before the first retry. If > 0, subsequent retries are * made at double the proceeding one up to the retryMaxInterval described * below. If = 0, the next retry is 1 and subsequent retries proceed as above.
  • *
  • * retryMaxInterval: (optional, default = 60) The maximum interval in * milliseconds to wait between retries
  • *
  • * retryIntervalScale: (optional, default = 1000) The amount by which to * multiply each retry interval. The default value of 1000 (milliseconds) is 1 * second, so the default retryMaxInterval of 60 is 60 seconds, or 1 * minute. *
*

*

* Example Schedules *

    *
  • * Retry after 1000 milliseconds, doubling the interval for each retry up to * 30000 milliseconds, subsequent retry intervals are 30000 milliseconds until * 10 retries have been attempted, after which the Exception * causing the fault is thrown: *
      *
    • maxRetries = 10 *
    • retryStartInterval = 1000 *
    • retryMaxInterval = 30000 *
    • retryIntervalScale = 1 *
    *
  • * Retry immediately, then retry after 1 * 1000 milliseconds, doubling the * interval for each retry up to 30 * 1000 milliseconds, subsequent retry * intervals are 30 * 1000 milliseconds until 20 retries have been attempted, * after which the Exception causing the fault is thrown: *
      *
    • maxRetries = 20 *
    • retryStartInterval = 0 *
    • retryMaxInterval = 30 *
    • retryIntervalScale = 1000 *
    *
  • * Retry after 5000 milliseconds, subsequent retry intervals are 5000 * milliseconds. Retry forever: *
      *
    • maxRetries = -1 *
    • retryStartInterval = 5000 *
    • retryMaxInterval = 5000 *
    • retryIntervalScale = 1 *
    *
*

* *

* In order to enable group/role based access restrictions, you can use the * "<restriction>" configuration element. An example of this is * shown below:
* *

 * <restriction
 * 	memberAttribute="uniqueMember">
 * 		<group>cn=PermanentStaff,ou=Groups,o=myorg.co.uk,ou=system</group>
 *        	<group>cn=TemporaryStaff,ou=Groups,o=myorg.co.uk,ou=system</group>
 * </restriction>
 * 
* * Its constituent attributes and elements are defined as follows: *
    *
  • * memberAttribute: The LDAP attribute whose values indicate the DNs of * the users which belong to the group or role.
  • *
  • * group: A valid group or role DN. A user is only authenticated * (permitted access) if they belong to at least one of the groups listed under * the "<restriction>" sections.
  • *
*

* *

* The following parameters may be used to adjust the underlying * com.sun.jndi.ldap.LdapCtxFactory. See LDAP Naming Service Provider for the Java Naming and Directory InterfaceTM * (JNDI) : Provider-specific Properties for details. *

    *
  • * useConnectionPool: (optional, default = true) Sets property * com.sun.jndi.ldap.connect.pool to the specified boolean value *
  • * connectionTimeout: (optional) Sets property * com.sun.jndi.ldap.connect.timeout to the specified integer value *
  • * readTimeout: (optional) Sets property * com.sun.jndi.ldap.read.timeout to the specified integer value. * Applicable to Java 6 and above. *
* * @see ReadOnlyLDAPUser * @see ReadOnlyLDAPGroupRestriction * */ public class ReadOnlyUsersLDAPRepository implements UsersRepository, Configurable, LogEnabled { // The name of the factory class which creates the initial context // for the LDAP service provider private static final String INITIAL_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; private static final String PROPERTY_NAME_CONNECTION_POOL = "com.sun.jndi.ldap.connect.pool"; private static final String PROPERTY_NAME_CONNECT_TIMEOUT = "com.sun.jndi.ldap.connect.timeout"; private static final String PROPERTY_NAME_READ_TIMEOUT = "com.sun.jndi.ldap.read.timeout"; /** * The URL of the LDAP server against which users are to be authenticated. * Note that users are actually authenticated by binding against the LDAP * server using the users "dn" and "credentials".The * value of this field is taken from the value of the configuration * attribute "ldapHost". */ private String ldapHost; /** * The value of this field is taken from the configuration attribute * "userIdAttribute". This is the LDAP attribute type which holds * the userId value. Note that this is not the same as the email address * attribute. */ private String userIdAttribute; /** * The value of this field is taken from the configuration attribute * "userObjectClass". This is the LDAP object class to use in the * search filter for user nodes under the userBase value. */ private String userObjectClass; /** * This is the LDAP context/sub-context within which to search for user * entities. The value of this field is taken from the configuration * attribute "userBase". */ private String userBase; /** * The user with which to initially bind to the LDAP server. The value of * this field is taken from the configuration attribute * "principal". */ private String principal; /** * The password/credentials with which to initially bind to the LDAP server. * The value of this field is taken from the configuration attribute * "credentials". */ private String credentials; /** * Encapsulates the information required to restrict users to LDAP groups or * roles. This object is populated from the contents of the configuration * element <restriction>. */ private ReadOnlyLDAPGroupRestriction restriction; /** * The context for the LDAP server. This is the connection that is built * from the configuration attributes "ldapHost", * "principal" and "credentials". */ private LdapContext ldapContext; // Use a connection pool. Default is true. private boolean useConnectionPool = true; // The connection timeout in milliseconds. // A value of less than or equal to zero means to use the network protocol's // (i.e., TCP's) timeout value. private int connectionTimeout = -1; // The LDAP read timeout in milliseconds. private int readTimeout = -1; // The schedule for retry attempts private RetrySchedule schedule = null; // Maximum number of times to retry a connection attempts. Default is no // retries. private int maxRetries = 0; private Logger log; /** * Creates a new instance of ReadOnlyUsersLDAPRepository. * */ public ReadOnlyUsersLDAPRepository() { super(); } /** * Extracts the parameters required by the repository instance from the * James server configuration data. The fields extracted include * {@link #ldapHost}, {@link #userIdAttribute}, {@link #userBase}, * {@link #principal}, {@link #credentials} and {@link #restriction}. * * @param configuration * An encapsulation of the James server configuration data. */ public void configure(HierarchicalConfiguration configuration) throws ConfigurationException { ldapHost = configuration.getString("[@ldapHost]", ""); principal = configuration.getString("[@principal]", ""); credentials = configuration.getString("[@credentials]", ""); userBase = configuration.getString("[@userBase]"); userIdAttribute = configuration.getString("[@userIdAttribute]"); userObjectClass = configuration.getString("[@userObjectClass]"); // Default is to use connection pooling useConnectionPool = configuration.getBoolean("[@useConnectionPool]", true); connectionTimeout = configuration.getInt("[@connectionTimeout]", -1); readTimeout = configuration.getInt("[@readTimeout]", -1); // Default maximum retries is 1, which allows an alternate connection to // be found in a multi-homed environment maxRetries = configuration.getInt("[@maxRetries]", 1); // Default retry start interval is 0 second long retryStartInterval = configuration.getLong("[@retryStartInterval]", 0); // Default maximum retry interval is 60 seconds long retryMaxInterval = configuration.getLong("[@retryMaxInterval]", 60); int scale = configuration.getInt("[@retryIntervalScale]", 1000); // seconds schedule = new DoublingRetrySchedule(retryStartInterval, retryMaxInterval, scale); HierarchicalConfiguration restrictionConfig = null; // Check if we have a restriction we can use // See JAMES-1204 if (configuration.containsKey("restriction[@memberAttribute]")) { restrictionConfig = configuration.configurationAt("restriction"); } restriction = new ReadOnlyLDAPGroupRestriction(restrictionConfig); } /** * Initialises the user-repository instance. It will create a connection to * the LDAP host using the supplied configuration. * * @throws Exception * If an error occurs authenticating or connecting to the * specified LDAP host. */ @PostConstruct public void init() throws Exception { if (log.isDebugEnabled()) { log.debug(new StringBuilder(128). append(this.getClass().getName()). append(".init()"). append('\n'). append("LDAP host: "). append(ldapHost). append('\n'). append("User baseDN: "). append(userBase). append('\n'). append("userIdAttribute: "). append(userIdAttribute). append('\n'). append("Group restriction: "). append(restriction). append('\n'). append("UseConnectionPool: "). append(useConnectionPool). append('\n'). append("connectionTimeout: "). append(connectionTimeout). append('\n'). append("readTimeout: "). append(readTimeout). append('\n'). append("retrySchedule: "). append(schedule). append('\n'). append("maxRetries: "). append(maxRetries). append('\n'). toString()); } // Setup the initial LDAP context updateLdapContext(); } /** * Answer the LDAP context used to connect with the LDAP server. * * @return an LdapContext * @throws NamingException */ protected LdapContext getLdapContext() throws NamingException { if (null == ldapContext) { updateLdapContext(); } return ldapContext; } protected void updateLdapContext() throws NamingException { ldapContext = computeLdapContext(); } /** * Answers a new LDAP/JNDI context using the specified user credentials. * * @return an LDAP directory context * @throws NamingException * Propagated from underlying LDAP communication API. */ protected LdapContext computeLdapContext() throws NamingException { return new RetryingLdapContext(schedule, maxRetries, log) { @Override public Context newDelegate() throws NamingException { return new InitialLdapContext(getContextEnvironment(), null); } }; } protected Properties getContextEnvironment() { final Properties props = new Properties(); props.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY); props.put(Context.PROVIDER_URL, null == ldapHost ? "" : ldapHost); if (null == credentials || credentials.isEmpty()) { props.put(Context.SECURITY_AUTHENTICATION, LdapConstants.SECURITY_AUTHENTICATION_NONE); } else { props.put(Context.SECURITY_AUTHENTICATION, LdapConstants.SECURITY_AUTHENTICATION_SIMPLE); props.put(Context.SECURITY_PRINCIPAL, null == principal ? "" : principal); props.put(Context.SECURITY_CREDENTIALS, credentials); } // The following properties are specific to com.sun.jndi.ldap.LdapCtxFactory props.put(PROPERTY_NAME_CONNECTION_POOL, Boolean.toString(useConnectionPool)); if (connectionTimeout > -1) { props.put(PROPERTY_NAME_CONNECT_TIMEOUT, Integer.toString(connectionTimeout)); } if (readTimeout > -1) { props.put(PROPERTY_NAME_READ_TIMEOUT, Integer.toString(readTimeout)); } return props; } /** * Indicates if the user with the specified DN can be found in the group * membership map-as encapsulated by the specified parameter map. * * @param userDN * The DN of the user to search for. * @param groupMembershipList * A map containing the entire group membership lists for the * configured groups. This is organised as a map of * * "<groupDN>=<[userDN1,userDN2,...,userDNn]>" * pairs. In essence, each groupDN string is * associated to a list of userDNs. * @return True if the specified userDN is associated with at * least one group in the parameter map, and False * otherwise. */ private boolean userInGroupsMembershipList(String userDN, Map> groupMembershipList) { boolean result = false; Collection> memberLists = groupMembershipList.values(); Iterator> memberListsIterator = memberLists.iterator(); while (memberListsIterator.hasNext() && !result) { Collection groupMembers = memberListsIterator.next(); result = groupMembers.contains(userDN); } return result; } /** * Gets all the user entities taken from the LDAP server, as taken from the * search-context given by the value of the attribute {@link #userBase}. * * @return A set containing all the relevant users found in the LDAP * directory. * @throws NamingException * Propagated from the LDAP communication layer. */ private Set getAllUsersFromLDAP() throws NamingException { Set result = new HashSet(); SearchControls sc = new SearchControls(); sc.setSearchScope(SearchControls.SUBTREE_SCOPE); sc.setReturningAttributes(new String[] { "distinguishedName" }); NamingEnumeration sr = ldapContext.search(userBase, "(objectClass=" + userObjectClass + ")", sc); while (sr.hasMore()) { SearchResult r = sr.next(); result.add(r.getNameInNamespace()); } return result; } /** * Extract the user attributes for the given collection of userDNs, and * encapsulates the user list as a collection of {@link ReadOnlyLDAPUser}s. * This method delegates the extraction of a single user's details to the * method {@link #buildUser(String)}. * * @param userDNs * The distinguished-names (DNs) of the users whose information * is to be extracted from the LDAP repository. * @return A collection of {@link ReadOnlyLDAPUser}s as taken from the LDAP * server. * @throws NamingException * Propagated from the underlying LDAP communication layer. */ private Collection buildUserCollection(Collection userDNs) throws NamingException { List results = new ArrayList(); Iterator userDNIterator = userDNs.iterator(); while (userDNIterator.hasNext()) { ReadOnlyLDAPUser user = buildUser(userDNIterator.next()); results.add(user); } return results; } /** * Given a userDN, this method retrieves the user attributes from the LDAP * server, so as to extract the items that are of interest to James. * Specifically it extracts the userId, which is extracted from the LDAP * attribute whose name is given by the value of the field * {@link #userIdAttribute}. * * @param userDN * The distinguished-name of the user whose details are to be * extracted from the LDAP repository. * @return A {@link ReadOnlyLDAPUser} instance which is initialized with the * userId of this user and ldap connection information with which * the userDN and attributes were obtained. * @throws NamingException * Propagated by the underlying LDAP communication layer. */ private ReadOnlyLDAPUser buildUser(String userDN) throws NamingException { SearchControls sc = new SearchControls(); sc.setSearchScope(SearchControls.OBJECT_SCOPE); sc.setReturningAttributes(new String[] { userIdAttribute }); sc.setCountLimit(1); StringBuilder builderFilter = new StringBuilder("(objectClass="); builderFilter.append(userObjectClass); builderFilter.append(")"); NamingEnumeration sr = ldapContext.search(userDN, builderFilter.toString(), sc); if (!sr.hasMore()) return null; Attributes userAttributes = sr.next().getAttributes(); Attribute userName = userAttributes.get(userIdAttribute); if (!restriction.isActivated() || userInGroupsMembershipList(userDN, restriction .getGroupMembershipLists(ldapContext))) return new ReadOnlyLDAPUser(userName.get().toString(), userDN, ldapContext); return null; } /** * @see UsersRepository#contains(java.lang.String) */ public boolean contains(String name) throws UsersRepositoryException { if (getUserByName(name) != null) { return true; } return false; } /* * TODO Should this be deprecated? At least the method isn't declared in the * interface anymore * * @see UsersRepository#containsCaseInsensitive(java.lang.String) */ public boolean containsCaseInsensitive(String name) throws UsersRepositoryException { if (getUserByNameCaseInsensitive(name) != null) { return true; } return false; } /** * @see UsersRepository#countUsers() */ public int countUsers() throws UsersRepositoryException { try { return getValidUsers().size(); } catch (NamingException e) { log.error("Unable to retrieve user count from ldap", e); throw new UsersRepositoryException("Unable to retrieve user count from ldap", e); } } /* * TODO Should this be deprecated? At least the method isn't declared in the * interface anymore * * @see UsersRepository#getRealName(java.lang.String) */ public String getRealName(String name) throws UsersRepositoryException { User u = getUserByNameCaseInsensitive(name); if (u != null) { return u.getUserName(); } return null; } /** * @see UsersRepository#getUserByName(java.lang.String) */ public User getUserByName(String name) throws UsersRepositoryException { try { return buildUser(userIdAttribute + "=" + name + "," + userBase); } catch (NamingException e) { log.error("Unable to retrieve user from ldap", e); throw new UsersRepositoryException("Unable to retrieve user from ldap", e); } } /* * TODO Should this be deprecated? At least the method isn't declared in the * interface anymore * * @see UsersRepository#getUserByNameCaseInsensitive(java.lang.String) */ public User getUserByNameCaseInsensitive(String name) throws UsersRepositoryException { try { Iterator userIt = buildUserCollection(getValidUsers()).iterator(); while (userIt.hasNext()) { ReadOnlyLDAPUser u = userIt.next(); if (u.getUserName().equalsIgnoreCase(name)) { return u; } } } catch (NamingException e) { log.error("Unable to retrieve user from ldap", e); throw new UsersRepositoryException("Unable to retrieve user from ldap", e); } return null; } /** * @see UsersRepository#list() */ public Iterator list() throws UsersRepositoryException { List result = new ArrayList(); try { Iterator userIt = buildUserCollection(getValidUsers()).iterator(); while (userIt.hasNext()) { result.add(userIt.next().getUserName()); } } catch (NamingException namingException) { throw new UsersRepositoryException( "Unable to retrieve users list from LDAP due to unknown naming error.", namingException); } return result.iterator(); } private Collection getValidUsers() throws NamingException { Set userDNs = getAllUsersFromLDAP(); Collection validUserDNs; if (restriction.isActivated()) { Map> groupMembershipList = restriction .getGroupMembershipLists(ldapContext); validUserDNs = new ArrayList(); Iterator userDNIterator = userDNs.iterator(); String userDN; while (userDNIterator.hasNext()) { userDN = userDNIterator.next(); if (userInGroupsMembershipList(userDN, groupMembershipList)) validUserDNs.add(userDN); } } else { validUserDNs = userDNs; } return validUserDNs; } /** * @see UsersRepository#removeUser(java.lang.String) */ public void removeUser(String name) throws UsersRepositoryException { log.warn("This user-repository is read-only. Modifications are not permitted."); throw new UsersRepositoryException( "This user-repository is read-only. Modifications are not permitted."); } /** * @see UsersRepository#test(java.lang.String, java.lang.String) */ public boolean test(String name, String password) throws UsersRepositoryException { User u = getUserByName(name); if (u != null) { return u.verifyPassword(password); } return false; } /** * @see UsersRepository#addUser(java.lang.String, java.lang.String) */ public void addUser(String username, String password) throws UsersRepositoryException { log.error("This user-repository is read-only. Modifications are not permitted."); throw new UsersRepositoryException( "This user-repository is read-only. Modifications are not permitted."); } /** * @see UsersRepository#updateUser(org.apache.james.api.user.User) */ public void updateUser(User user) throws UsersRepositoryException { log.error("This user-repository is read-only. Modifications are not permitted."); throw new UsersRepositoryException( "This user-repository is read-only. Modifications are not permitted."); } /** * @see org.apache.james.lifecycle.api.LogEnabled#setLog(org.slf4j.Logger) */ public void setLog(Logger log) { this.log = log; } /** * VirtualHosting not supported */ public boolean supportVirtualHosting() throws UsersRepositoryException { return false; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy