| /* |
| * 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.lenya.ac.ldap; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.util.Hashtable; |
| import java.util.Properties; |
| |
| import javax.naming.AuthenticationException; |
| 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.DirContext; |
| import javax.naming.directory.SearchControls; |
| import javax.naming.directory.SearchResult; |
| import javax.naming.ldap.InitialLdapContext; |
| |
| import org.apache.avalon.framework.configuration.Configuration; |
| import org.apache.avalon.framework.configuration.ConfigurationException; |
| import org.apache.avalon.framework.configuration.DefaultConfiguration; |
| import org.apache.commons.logging.Log; |
| import org.apache.lenya.ac.AccessControlException; |
| import org.apache.lenya.ac.ItemManager; |
| import org.apache.lenya.ac.file.FileUser; |
| |
| import com.sun.jndi.ldap.LdapCtxFactory; |
| import com.sun.jndi.ldap.LdapURL; |
| |
| /** |
| * LDAP user. |
| * @version $Id$ |
| */ |
| public class LDAPUser extends FileUser { |
| /** |
| * |
| */ |
| private static final long serialVersionUID = 1L; |
| |
| private Properties defaultProperties = null; |
| |
| /** |
| * <code>LDAP_ID</code> The LDAP id |
| */ |
| public static final String LDAP_ID = "ldapid"; |
| private static String LDAP_PROPERTIES_FILE = "ldap.properties"; |
| private static String PROVIDER_URL_PROP = "provider-url"; |
| private static String MGR_DN_PROP = "mgr-dn"; |
| private static String MGR_PW_PROP = "mgr-pw"; |
| private static String KEY_STORE_PROP = "key-store"; |
| private static String SECURITY_PROTOCOL_PROP = "security-protocol"; |
| private static String SECURITY_AUTHENTICATION_PROP = "security-authentication"; |
| private static String USR_ATTR_PROP = "usr-attr"; |
| private static String USR_ATTR_DEFAULT = "uid"; |
| private static String USR_NAME_ATTR_PROP = "usr-name-attr"; |
| private static String USR_NAME_ATTR_DEFAULT = "gecos"; |
| private static String USR_BRANCH_PROP = "usr-branch"; |
| private static String USR_BRANCH_DEFAULT = "ou=People"; |
| private static String USR_AUTH_TYPE_PROP = "usr-authentication"; |
| private static String USR_AUTH_TYPE_DEFAULT = "simple"; |
| private static String BASE_DN_PROP = "base-dn"; |
| private static String DOMAIN_NAME_PROP = "domain-name"; |
| private static String HANDLE_REFERRALS_PROP = "handle-referrals"; |
| private static String HANDLE_REFERRALS_DEFAULT = "ignore"; |
| |
| private String ldapId; |
| private String ldapName; |
| |
| // deprecated: for backwards compatibility only ! |
| private static String PARTIAL_USER_DN_PROP = "partial-user-dn"; |
| |
| /** |
| * Creates a new LDAPUser object. |
| * @param itemManager The item manager. |
| * @param logger The logger. |
| */ |
| public LDAPUser(ItemManager itemManager, Log logger) { |
| super(itemManager, logger); |
| } |
| |
| /** |
| * Create an LDAPUser |
| * @param itemManager The item manager. |
| * @param logger The logger. |
| * @param id user id of LDAPUser |
| * @param email of LDAPUser |
| * @param _ldapId of LDAPUser |
| * @param _logger The logger. |
| * @throws ConfigurationException if the properties could not be read |
| */ |
| public LDAPUser(ItemManager itemManager, Log logger, String id, String email, |
| String _ldapId, Log _logger) throws ConfigurationException { |
| super(itemManager, logger, id, null, email, null); |
| this.ldapId = _ldapId; |
| initialize(); |
| } |
| |
| /** |
| * Create a new LDAPUser from a configuration |
| * @param config the <code>Configuration</code> specifying the user |
| * details |
| * @throws ConfigurationException if the user could not be instantiated |
| */ |
| public void configure(Configuration config) throws ConfigurationException { |
| super.configure(config); |
| this.ldapId = config.getChild(LDAP_ID).getValue(); |
| |
| initialize(); |
| } |
| |
| /** |
| * Checks if a user exists. |
| * @param _ldapId The LDAP id. |
| * @return A boolean value indicating whether the user is found in the |
| * directory |
| * @throws AccessControlException when an error occurs. |
| */ |
| public boolean existsUser(String _ldapId) throws AccessControlException { |
| |
| if (getLogger().isDebugEnabled()) |
| getLogger().debug("existsUser() checking id " + _ldapId); |
| |
| boolean exists = false; |
| |
| try { |
| readProperties(); |
| SearchResult entry = getDirectoryEntry(_ldapId); |
| |
| exists = (entry != null); |
| } catch (final IOException e) { |
| if (getLogger().isDebugEnabled()) |
| getLogger().debug("existsUser() for id " + _ldapId + " got exception: " + e); |
| throw new AccessControlException("Exception during search: ", e); |
| } catch (final NamingException e) { |
| if (getLogger().isDebugEnabled()) |
| getLogger().debug("existsUser() for id " + _ldapId + " got exception: " + e); |
| throw new AccessControlException("Exception during search: ", e); |
| } |
| |
| return exists; |
| } |
| |
| /** |
| * Initializes this user. The current (already authenticated) ldapId is |
| * queried in the directory, in order to retrieve additional information, |
| * such as the user name. In current implementation, only the user name is |
| * actually retrieved, but other attributes may be used in the future (such |
| * as groups ?) TODO: should the code be changed to not throw an exception |
| * when something goes wrong ? After all, it's only used to get additional |
| * info for display? This is a design decision, I'm not sure what's best. |
| * @throws ConfigurationException when something went wrong. |
| */ |
| protected void initialize() throws ConfigurationException { |
| |
| try { |
| if (getLogger().isDebugEnabled()) |
| getLogger().debug("initialize() getting entry ..."); |
| |
| SearchResult entry = getDirectoryEntry(this.ldapId); |
| if (entry != null) { |
| StringBuffer name = new StringBuffer(); |
| /* users full name */ |
| String usrNameAttr = defaultProperties.getProperty(USR_NAME_ATTR_PROP, |
| USR_NAME_ATTR_DEFAULT); |
| |
| if (getLogger().isDebugEnabled()) |
| getLogger().debug( |
| "initialize() got entry, going to look for attribute " + usrNameAttr |
| + " in entry, which is: " + entry); |
| |
| Attributes attributes = entry.getAttributes(); |
| if (attributes != null) { |
| Attribute userName = attributes.get(usrNameAttr); |
| if (userName != null) name.append((String) userName.get()); |
| } |
| |
| this.ldapName = name.toString(); |
| if (getLogger().isDebugEnabled()) |
| getLogger().debug("initialize() set name to " + this.ldapName); |
| } else { |
| this.ldapName = ""; |
| } |
| } catch (final NamingException e1) { |
| throw new ConfigurationException("Could not read properties", e1); |
| } catch (final IOException e1) { |
| throw new ConfigurationException("Could not read properties", e1); |
| } |
| } |
| |
| /** |
| * @see org.apache.lenya.ac.file.FileUser#createConfiguration() |
| */ |
| protected Configuration createConfiguration() { |
| DefaultConfiguration config = (DefaultConfiguration) super.createConfiguration(); |
| |
| // add ldap_id node |
| DefaultConfiguration child = new DefaultConfiguration(LDAP_ID); |
| child.setValue(this.ldapId); |
| config.addChild(child); |
| |
| return config; |
| } |
| |
| /** |
| * Get the ldap id |
| * @return the ldap id |
| */ |
| public String getLdapId() { |
| return this.ldapId; |
| } |
| |
| /** |
| * Set the ldap id |
| * @param string the new ldap id |
| */ |
| public void setLdapId(String string) { |
| this.ldapId = string; |
| } |
| |
| /** |
| * Authenticate a user against the directory. The principal to be |
| * authenticated is either constructed by use of the configured properties, |
| * or by lookup of this ID in the directory. This principal then attempts to |
| * authenticate against the directory with the provided password. |
| * @see org.apache.lenya.ac.User#authenticate(java.lang.String) |
| */ |
| public boolean authenticate(String password) { |
| |
| boolean authenticated = false; |
| String principal = ""; |
| Context ctx = null; |
| |
| try { |
| principal = getPrincipal(); |
| |
| if (getLogger().isDebugEnabled()) |
| getLogger().debug("Authenticating with principal [" + principal + "]"); |
| |
| ctx = bind(principal, password, defaultProperties.getProperty(USR_AUTH_TYPE_PROP, |
| USR_AUTH_TYPE_DEFAULT)); |
| authenticated = true; |
| close(ctx); |
| if (getLogger().isDebugEnabled()) |
| getLogger().debug("Context closed."); |
| } catch (IOException e) { |
| getLogger().warn("authenticate handling IOException, check your setup: " + e); |
| } catch (AuthenticationException e) { |
| getLogger().info("authenticate failed for principal " + principal + ", exception " + e); |
| } catch (NamingException e) { |
| // log this failure |
| if (getLogger().isInfoEnabled()) { |
| getLogger().info("Bind for user " + principal + " to Ldap server failed: ", e); |
| } |
| } |
| |
| return authenticated; |
| |
| } |
| |
| /** |
| * @see org.apache.lenya.ac.Item#getName() |
| */ |
| public String getName() { |
| return this.ldapName; |
| } |
| |
| /** |
| * LDAP Users fetch their name information from the LDAP server, so we don't |
| * store it locally. Since we only have read access we basically can't set |
| * the name, i.e. any request to change the name is ignored. |
| * @param string is ignored |
| */ |
| public void setName(String string) { |
| // we do not have write access to LDAP, so we ignore |
| // change request to the name. |
| } |
| |
| /** |
| * The LDAPUser doesn't store any passwords as they are handled by LDAP |
| * @param plainTextPassword is ignored |
| */ |
| public void setPassword(String plainTextPassword) { |
| setEncryptedPassword(null); |
| } |
| |
| /** |
| * The LDAPUser doesn't store any passwords as they are handled by LDAP |
| * @param encryptedPassword is ignored |
| */ |
| protected void setEncryptedPassword(String encryptedPassword) { |
| encryptedPassword = null; |
| } |
| |
| /** |
| * The LDAPUser doesn't change any passwords as they are handled by LDAP |
| * @return always returns false |
| */ |
| public boolean canChangePassword() { |
| return false; |
| } |
| |
| /** |
| * Connect to the LDAP server |
| * @param principal the principal string for the LDAP connection |
| * @param credentials the credentials for the LDAP connection |
| * @param authMethod the authentication method |
| * @return a <code>DirContext</code> |
| * @throws NamingException if there are problems establishing the Ldap |
| * connection |
| */ |
| private DirContext bind(String principal, String credentials, String authMethod) |
| throws NamingException { |
| |
| if (getLogger().isInfoEnabled()) |
| getLogger().info("Binding principal: [" + principal + "]"); |
| |
| Hashtable env = new Hashtable(); |
| |
| System.setProperty("javax.net.ssl.trustStore", getConfigurationUri() |
| + File.separator + defaultProperties.getProperty(KEY_STORE_PROP)); |
| |
| env.put(Context.INITIAL_CONTEXT_FACTORY, LdapCtxFactory.class.getName()); |
| |
| String prop = defaultProperties.getProperty(PROVIDER_URL_PROP); |
| if (prop == null) |
| throw new RuntimeException("LDAP configuration error: property " + PROVIDER_URL_PROP |
| + " is not set in property file " + LDAP_PROPERTIES_FILE); |
| env.put(Context.PROVIDER_URL, prop); |
| |
| prop = defaultProperties.getProperty(SECURITY_PROTOCOL_PROP); |
| if (prop == null) |
| throw new RuntimeException("LDAP configuration error: property " |
| + SECURITY_PROTOCOL_PROP + " is not set in property file " |
| + LDAP_PROPERTIES_FILE); |
| env.put(Context.SECURITY_PROTOCOL, prop); |
| |
| env.put(Context.SECURITY_AUTHENTICATION, authMethod); |
| if (authMethod != null && !authMethod.equals("none")) { |
| env.put(Context.SECURITY_PRINCIPAL, principal); |
| env.put(Context.SECURITY_CREDENTIALS, credentials); |
| } |
| env.put(Context.REFERRAL, defaultProperties.getProperty(HANDLE_REFERRALS_PROP, HANDLE_REFERRALS_DEFAULT)); |
| |
| DirContext ctx = new InitialLdapContext(env, null); |
| |
| if (getLogger().isInfoEnabled()) |
| getLogger().info("Finished binding principal."); |
| |
| return ctx; |
| } |
| |
| /** |
| * Close the connection to the LDAP server |
| * @param ctx the context that was returned from the bind |
| * @throws NamingException if there is a problem communicating to the LDAP |
| * server |
| */ |
| private void close(Context ctx) throws NamingException { |
| if (ctx != null) |
| ctx.close(); |
| } |
| |
| /** |
| * Read the properties |
| * @throws IOException if the properties cannot be found. |
| */ |
| private void readProperties() throws IOException { |
| // create and load default properties |
| File propertiesFile = new File(getConfigurationUri(), LDAP_PROPERTIES_FILE); |
| |
| if (defaultProperties == null) { |
| defaultProperties = new Properties(); |
| |
| FileInputStream in = null; |
| try { |
| in = new FileInputStream(propertiesFile); |
| defaultProperties.load(in); |
| } finally { |
| if (in != null) { |
| in.close(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Wrapping of the decision whether a recursive search is wanted or not. |
| * Implementation: If the USR_BRANCH_PROP is present, this is the new style |
| * of configuration (starting Lenya 1.2.2); if it has a value, then a |
| * specific branch is wanted: no recursive search. If the property is |
| * present, but has no value, search recursively. |
| * @return Recursive search |
| */ |
| private boolean isSubtreeSearch() { |
| boolean recurse = false; |
| String usrBranchProp = defaultProperties.getProperty(USR_BRANCH_PROP); |
| if (usrBranchProp != null) |
| if (usrBranchProp.trim().length() == 0) |
| recurse = true; |
| |
| return recurse; |
| } |
| |
| private SearchResult getDirectoryEntry(String userId) throws NamingException, IOException { |
| DirContext context = null; |
| String searchFilter = ""; |
| String objectName = ""; |
| boolean recursiveSearch; |
| SearchResult result = null; |
| |
| try { |
| readProperties(); |
| |
| context = bind(defaultProperties.getProperty(MGR_DN_PROP), defaultProperties |
| .getProperty(MGR_PW_PROP), defaultProperties |
| .getProperty(SECURITY_AUTHENTICATION_PROP)); |
| |
| // Get search information and user attribute from properties |
| // provide defaults if not present (backward compatibility) |
| String userAttribute = defaultProperties.getProperty(USR_ATTR_PROP, USR_ATTR_DEFAULT); |
| searchFilter = "(" + userAttribute + "=" + userId + ")"; |
| SearchControls scope = new SearchControls(); |
| |
| recursiveSearch = isSubtreeSearch(); |
| if (recursiveSearch) { |
| scope.setSearchScope(SearchControls.SUBTREE_SCOPE); |
| objectName = defaultProperties.getProperty(PROVIDER_URL_PROP); |
| } else { |
| scope.setSearchScope(SearchControls.ONELEVEL_SCOPE); |
| objectName = defaultProperties.getProperty(USR_BRANCH_PROP, USR_BRANCH_DEFAULT); |
| } |
| |
| if (getLogger().isDebugEnabled()) |
| getLogger().debug( |
| "searching object " + objectName + " filtering with " + searchFilter |
| + ", recursive search ? " + recursiveSearch); |
| |
| NamingEnumeration results = context.search(objectName, searchFilter, scope); |
| if (results != null && results.hasMore()) |
| result = (SearchResult) results.next(); |
| |
| // sanity check: if more than one entry is returned |
| // for a user-id, then the directory is probably flawed, |
| // so it would be nice to warn the administrator. |
| // |
| // This block is commented out for now, because of possible |
| // side-effects, such as unexpected exceptions. |
| // try { |
| // if (results.hasMore()) { |
| // getLogger().warn("Found more than one entry in the directory for |
| // user " + userId + ". You probably should deactivate recursive |
| // searches. The first entry was used as a work-around."); |
| // } |
| // } |
| // catch (javax.naming.PartialResultException e) { |
| // if (getLogger().isDebugEnabled()) |
| // getLogger().debug("Catching and ignoring PartialResultException, |
| // as this means LDAP server does not support our sanity check"); |
| // } |
| |
| } catch (NamingException e) { |
| if (getLogger().isDebugEnabled()) |
| getLogger().debug( |
| "NamingException caught when searching on objectName = " + objectName |
| + " and searchFilter=" + searchFilter |
| + ", this exception will be propagated: " + e); |
| throw e; |
| } finally { |
| try { |
| if (context != null) { |
| close(context); |
| } |
| } catch (NamingException e) { |
| getLogger().warn("this should not happen: exception closing context " + e); |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Encapsulation of the creation of a principal: we need to distinguish |
| * three cases, in order to support different modes of using a directory. |
| * The first is the use of a domain-name (requirement of MS Active |
| * Directory): if this property is set, this is used to construct the |
| * principal. The second case is where a user-id is somewhere in a domain, |
| * but not in a specific branch: in this case, a subtree search is performed |
| * to retrieve the complete path. The third case is where a specific branch |
| * of the directory is to be used; this is the case where usr-branch is set |
| * to a value. In this case, this branch is used to construct the principal. |
| * @return The principal |
| * @throws IOException |
| * @throws NamingException |
| */ |
| private String getPrincipal() throws IOException, NamingException { |
| |
| String principal; |
| |
| // 1. Check if domain-name is to be supported |
| String domainProp = defaultProperties.getProperty(DOMAIN_NAME_PROP); |
| if (domainProp != null && domainProp.trim().length() > 0) { |
| principal = domainProp + "\\" + getLdapId(); |
| } else { |
| if (isSubtreeSearch()) { |
| // 2. Principal is constructed from directory entry |
| SearchResult entry = getDirectoryEntry(getLdapId()); |
| principal = entry.getName(); |
| if (entry.isRelative()){ |
| if (principal.length() > 0) |
| principal = principal + "," + defaultProperties.getProperty(BASE_DN_PROP); |
| } else { |
| // if the item is found following a referral an URL string is |
| // returned which can not be used as principal |
| LdapURL ldapurl = new LdapURL(principal); |
| principal = ldapurl.getDN(); |
| } |
| } else |
| // 3. Principal is constructed from properties |
| principal = constructPrincipal(getLdapId()); |
| } |
| |
| return principal; |
| } |
| |
| /** |
| * Construct the principal for a user, by using the given userId along with |
| * the configured properties. |
| * @param userId The user id |
| * @return The principal |
| */ |
| private String constructPrincipal(String userId) { |
| StringBuffer principal = new StringBuffer(); |
| principal.append(defaultProperties.getProperty(USR_ATTR_PROP, USR_ATTR_DEFAULT)) |
| .append("=").append(userId).append(","); |
| |
| String baseDn = defaultProperties.getProperty(BASE_DN_PROP); |
| if (baseDn != null && baseDn.length() > 0) { |
| // USR_BRANCH_PROP may be empty, so only append when not-empty |
| String usrBranch = defaultProperties.getProperty(USR_BRANCH_PROP); |
| if (usrBranch != null) { |
| if (usrBranch.trim().length() > 0) |
| principal.append(usrBranch).append(","); |
| } else |
| principal.append(USR_BRANCH_DEFAULT).append(","); |
| |
| principal.append(defaultProperties.getProperty(BASE_DN_PROP)); |
| } else { |
| // try for backwards compatibility of ldap properties |
| getLogger().warn( |
| "getPrincipal() read a deprecated format in ldap properties, please update"); |
| principal.append(defaultProperties.getProperty(PARTIAL_USER_DN_PROP)); |
| } |
| |
| if (getLogger().isDebugEnabled()) |
| getLogger().debug("getPrincipal() returning " + principal.toString()); |
| |
| return principal.toString(); |
| } |
| |
| } |