001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.lenya.ac.ldap;
020:
021: import java.io.File;
022: import java.io.FileInputStream;
023: import java.io.IOException;
024: import java.util.Hashtable;
025: import java.util.Properties;
026:
027: import javax.naming.AuthenticationException;
028: import javax.naming.Context;
029: import javax.naming.NamingEnumeration;
030: import javax.naming.NamingException;
031: import javax.naming.directory.Attribute;
032: import javax.naming.directory.Attributes;
033: import javax.naming.directory.DirContext;
034: import javax.naming.directory.SearchControls;
035: import javax.naming.directory.SearchResult;
036: import javax.naming.ldap.InitialLdapContext;
037:
038: import org.apache.avalon.framework.configuration.Configuration;
039: import org.apache.avalon.framework.configuration.ConfigurationException;
040: import org.apache.avalon.framework.configuration.DefaultConfiguration;
041: import org.apache.avalon.framework.logger.Logger;
042: import org.apache.lenya.ac.AccessControlException;
043: import org.apache.lenya.ac.ItemManager;
044: import org.apache.lenya.ac.file.FileUser;
045:
046: import com.sun.jndi.ldap.LdapCtxFactory;
047: import com.sun.jndi.ldap.LdapURL;
048:
049: /**
050: * LDAP user.
051: * @version $Id: LDAPUser.java 580116 2007-09-27 18:02:21Z rfrovarp $
052: */
053: public class LDAPUser extends FileUser {
054: /**
055: *
056: */
057: private static final long serialVersionUID = 1L;
058:
059: private Properties defaultProperties = null;
060:
061: /**
062: * <code>LDAP_ID</code> The LDAP id
063: */
064: public static final String LDAP_ID = "ldapid";
065: private static String LDAP_PROPERTIES_FILE = "ldap.properties";
066: private static String PROVIDER_URL_PROP = "provider-url";
067: private static String MGR_DN_PROP = "mgr-dn";
068: private static String MGR_PW_PROP = "mgr-pw";
069: private static String KEY_STORE_PROP = "key-store";
070: private static String SECURITY_PROTOCOL_PROP = "security-protocol";
071: private static String SECURITY_AUTHENTICATION_PROP = "security-authentication";
072: private static String USR_ATTR_PROP = "usr-attr";
073: private static String USR_ATTR_DEFAULT = "uid";
074: private static String USR_NAME_ATTR_PROP = "usr-name-attr";
075: private static String USR_NAME_ATTR_DEFAULT = "gecos";
076: private static String USR_BRANCH_PROP = "usr-branch";
077: private static String USR_BRANCH_DEFAULT = "ou=People";
078: private static String USR_AUTH_TYPE_PROP = "usr-authentication";
079: private static String USR_AUTH_TYPE_DEFAULT = "simple";
080: private static String BASE_DN_PROP = "base-dn";
081: private static String DOMAIN_NAME_PROP = "domain-name";
082: private static String HANDLE_REFERRALS_PROP = "handle-referrals";
083: private static String HANDLE_REFERRALS_DEFAULT = "ignore";
084:
085: private String ldapId;
086: private String ldapName;
087:
088: // deprecated: for backwards compatibility only !
089: private static String PARTIAL_USER_DN_PROP = "partial-user-dn";
090:
091: /**
092: * Creates a new LDAPUser object.
093: * @param itemManager The item manager.
094: * @param logger The logger.
095: */
096: public LDAPUser(ItemManager itemManager, Logger logger) {
097: super (itemManager, logger);
098: }
099:
100: /**
101: * Create an LDAPUser
102: * @param itemManager The item manager.
103: * @param logger The logger.
104: * @param id user id of LDAPUser
105: * @param email of LDAPUser
106: * @param _ldapId of LDAPUser
107: * @param _logger The logger.
108: * @throws ConfigurationException if the properties could not be read
109: */
110: public LDAPUser(ItemManager itemManager, Logger logger, String id,
111: String email, String _ldapId, Logger _logger)
112: throws ConfigurationException {
113: super (itemManager, logger, id, null, email, null);
114: this .ldapId = _ldapId;
115: initialize();
116: }
117:
118: /**
119: * Create a new LDAPUser from a configuration
120: * @param config the <code>Configuration</code> specifying the user
121: * details
122: * @throws ConfigurationException if the user could not be instantiated
123: */
124: public void configure(Configuration config)
125: throws ConfigurationException {
126: super .configure(config);
127: this .ldapId = config.getChild(LDAP_ID).getValue();
128:
129: initialize();
130: }
131:
132: /**
133: * Checks if a user exists.
134: * @param _ldapId The LDAP id.
135: * @return A boolean value indicating whether the user is found in the
136: * directory
137: * @throws AccessControlException when an error occurs.
138: */
139: public boolean existsUser(String _ldapId)
140: throws AccessControlException {
141:
142: if (getLogger().isDebugEnabled())
143: getLogger().debug("existsUser() checking id " + _ldapId);
144:
145: boolean exists = false;
146:
147: try {
148: readProperties();
149: SearchResult entry = getDirectoryEntry(_ldapId);
150:
151: exists = (entry != null);
152: } catch (final IOException e) {
153: if (getLogger().isDebugEnabled())
154: getLogger().debug(
155: "existsUser() for id " + _ldapId
156: + " got exception: " + e);
157: throw new AccessControlException(
158: "Exception during search: ", e);
159: } catch (final NamingException e) {
160: if (getLogger().isDebugEnabled())
161: getLogger().debug(
162: "existsUser() for id " + _ldapId
163: + " got exception: " + e);
164: throw new AccessControlException(
165: "Exception during search: ", e);
166: }
167:
168: return exists;
169: }
170:
171: /**
172: * Initializes this user. The current (already authenticated) ldapId is
173: * queried in the directory, in order to retrieve additional information,
174: * such as the user name. In current implementation, only the user name is
175: * actually retrieved, but other attributes may be used in the future (such
176: * as groups ?) TODO: should the code be changed to not throw an exception
177: * when something goes wrong ? After all, it's only used to get additional
178: * info for display? This is a design decision, I'm not sure what's best.
179: * @throws ConfigurationException when something went wrong.
180: */
181: protected void initialize() throws ConfigurationException {
182:
183: try {
184: if (getLogger().isDebugEnabled())
185: getLogger().debug("initialize() getting entry ...");
186:
187: SearchResult entry = getDirectoryEntry(this .ldapId);
188: if (entry != null) {
189: StringBuffer name = new StringBuffer();
190: /* users full name */
191: String usrNameAttr = defaultProperties.getProperty(
192: USR_NAME_ATTR_PROP, USR_NAME_ATTR_DEFAULT);
193:
194: if (getLogger().isDebugEnabled())
195: getLogger().debug(
196: "initialize() got entry, going to look for attribute "
197: + usrNameAttr
198: + " in entry, which is: " + entry);
199:
200: Attributes attributes = entry.getAttributes();
201: if (attributes != null) {
202: Attribute userName = attributes.get(usrNameAttr);
203: if (userName != null)
204: name.append((String) userName.get());
205: }
206:
207: this .ldapName = name.toString();
208: if (getLogger().isDebugEnabled())
209: getLogger()
210: .debug(
211: "initialize() set name to "
212: + this .ldapName);
213: } else {
214: this .ldapName = "";
215: }
216: } catch (final NamingException e1) {
217: throw new ConfigurationException(
218: "Could not read properties", e1);
219: } catch (final IOException e1) {
220: throw new ConfigurationException(
221: "Could not read properties", e1);
222: }
223: }
224:
225: /**
226: * @see org.apache.lenya.ac.file.FileUser#createConfiguration()
227: */
228: protected Configuration createConfiguration() {
229: DefaultConfiguration config = (DefaultConfiguration) super
230: .createConfiguration();
231:
232: // add ldap_id node
233: DefaultConfiguration child = new DefaultConfiguration(LDAP_ID);
234: child.setValue(this .ldapId);
235: config.addChild(child);
236:
237: return config;
238: }
239:
240: /**
241: * Get the ldap id
242: * @return the ldap id
243: */
244: public String getLdapId() {
245: return this .ldapId;
246: }
247:
248: /**
249: * Set the ldap id
250: * @param string the new ldap id
251: */
252: public void setLdapId(String string) {
253: this .ldapId = string;
254: }
255:
256: /**
257: * Authenticate a user against the directory. The principal to be
258: * authenticated is either constructed by use of the configured properties,
259: * or by lookup of this ID in the directory. This principal then attempts to
260: * authenticate against the directory with the provided password.
261: * @see org.apache.lenya.ac.User#authenticate(java.lang.String)
262: */
263: public boolean authenticate(String password) {
264:
265: boolean authenticated = false;
266: String principal = "";
267: Context ctx = null;
268:
269: try {
270: principal = getPrincipal();
271:
272: if (getLogger().isDebugEnabled())
273: getLogger().debug(
274: "Authenticating with principal [" + principal
275: + "]");
276:
277: ctx = bind(principal, password, defaultProperties
278: .getProperty(USR_AUTH_TYPE_PROP,
279: USR_AUTH_TYPE_DEFAULT));
280: authenticated = true;
281: close(ctx);
282: if (getLogger().isDebugEnabled())
283: getLogger().debug("Context closed.");
284: } catch (IOException e) {
285: getLogger().warn(
286: "authenticate handling IOException, check your setup: "
287: + e);
288: } catch (AuthenticationException e) {
289: getLogger().info(
290: "authenticate failed for principal " + principal
291: + ", exception " + e);
292: } catch (NamingException e) {
293: // log this failure
294: if (getLogger().isInfoEnabled()) {
295: getLogger().info(
296: "Bind for user " + principal
297: + " to Ldap server failed: ", e);
298: }
299: }
300:
301: return authenticated;
302:
303: }
304:
305: /**
306: * @see org.apache.lenya.ac.Item#getName()
307: */
308: public String getName() {
309: return this .ldapName;
310: }
311:
312: /**
313: * LDAP Users fetch their name information from the LDAP server, so we don't
314: * store it locally. Since we only have read access we basically can't set
315: * the name, i.e. any request to change the name is ignored.
316: * @param string is ignored
317: */
318: public void setName(String string) {
319: // we do not have write access to LDAP, so we ignore
320: // change request to the name.
321: }
322:
323: /**
324: * The LDAPUser doesn't store any passwords as they are handled by LDAP
325: * @param plainTextPassword is ignored
326: */
327: public void setPassword(String plainTextPassword) {
328: setEncryptedPassword(null);
329: }
330:
331: /**
332: * The LDAPUser doesn't store any passwords as they are handled by LDAP
333: * @param encryptedPassword is ignored
334: */
335: protected void setEncryptedPassword(String encryptedPassword) {
336: encryptedPassword = null;
337: }
338:
339: /**
340: * The LDAPUser doesn't change any passwords as they are handled by LDAP
341: * @return always returns false
342: */
343: public boolean canChangePassword() {
344: return false;
345: }
346:
347: /**
348: * Connect to the LDAP server
349: * @param principal the principal string for the LDAP connection
350: * @param credentials the credentials for the LDAP connection
351: * @param authMethod the authentication method
352: * @return a <code>DirContext</code>
353: * @throws NamingException if there are problems establishing the Ldap
354: * connection
355: */
356: private DirContext bind(String principal, String credentials,
357: String authMethod) throws NamingException {
358:
359: if (getLogger().isInfoEnabled())
360: getLogger().info("Binding principal: [" + principal + "]");
361:
362: Hashtable env = new Hashtable();
363:
364: System
365: .setProperty("javax.net.ssl.trustStore",
366: getConfigurationDirectory().getAbsolutePath()
367: + File.separator
368: + defaultProperties
369: .getProperty(KEY_STORE_PROP));
370:
371: env.put(Context.INITIAL_CONTEXT_FACTORY, LdapCtxFactory.class
372: .getName());
373:
374: String prop = defaultProperties.getProperty(PROVIDER_URL_PROP);
375: if (prop == null)
376: throw new RuntimeException(
377: "LDAP configuration error: property "
378: + PROVIDER_URL_PROP
379: + " is not set in property file "
380: + LDAP_PROPERTIES_FILE);
381: env.put(Context.PROVIDER_URL, prop);
382:
383: prop = defaultProperties.getProperty(SECURITY_PROTOCOL_PROP);
384: if (prop == null)
385: throw new RuntimeException(
386: "LDAP configuration error: property "
387: + SECURITY_PROTOCOL_PROP
388: + " is not set in property file "
389: + LDAP_PROPERTIES_FILE);
390: env.put(Context.SECURITY_PROTOCOL, prop);
391:
392: env.put(Context.SECURITY_AUTHENTICATION, authMethod);
393: if (authMethod != null && !authMethod.equals("none")) {
394: env.put(Context.SECURITY_PRINCIPAL, principal);
395: env.put(Context.SECURITY_CREDENTIALS, credentials);
396: }
397: env.put(Context.REFERRAL, defaultProperties.getProperty(
398: HANDLE_REFERRALS_PROP, HANDLE_REFERRALS_DEFAULT));
399:
400: DirContext ctx = new InitialLdapContext(env, null);
401:
402: if (getLogger().isInfoEnabled())
403: getLogger().info("Finished binding principal.");
404:
405: return ctx;
406: }
407:
408: /**
409: * Close the connection to the LDAP server
410: * @param ctx the context that was returned from the bind
411: * @throws NamingException if there is a problem communicating to the LDAP
412: * server
413: */
414: private void close(Context ctx) throws NamingException {
415: if (ctx != null)
416: ctx.close();
417: }
418:
419: /**
420: * Read the properties
421: * @throws IOException if the properties cannot be found.
422: */
423: private void readProperties() throws IOException {
424: // create and load default properties
425: File propertiesFile = new File(getConfigurationDirectory(),
426: LDAP_PROPERTIES_FILE);
427:
428: if (defaultProperties == null) {
429: defaultProperties = new Properties();
430:
431: FileInputStream in = null;
432: try {
433: in = new FileInputStream(propertiesFile);
434: defaultProperties.load(in);
435: } finally {
436: if (in != null) {
437: in.close();
438: }
439: }
440: }
441: }
442:
443: /**
444: * Wrapping of the decision whether a recursive search is wanted or not.
445: * Implementation: If the USR_BRANCH_PROP is present, this is the new style
446: * of configuration (starting Lenya 1.2.2); if it has a value, then a
447: * specific branch is wanted: no recursive search. If the property is
448: * present, but has no value, search recursively.
449: * @return Recursive search
450: */
451: private boolean isSubtreeSearch() {
452: boolean recurse = false;
453: String usrBranchProp = defaultProperties
454: .getProperty(USR_BRANCH_PROP);
455: if (usrBranchProp != null)
456: if (usrBranchProp.trim().length() == 0)
457: recurse = true;
458:
459: return recurse;
460: }
461:
462: private SearchResult getDirectoryEntry(String userId)
463: throws NamingException, IOException {
464: DirContext context = null;
465: String searchFilter = "";
466: String objectName = "";
467: boolean recursiveSearch;
468: SearchResult result = null;
469:
470: try {
471: readProperties();
472:
473: context = bind(defaultProperties.getProperty(MGR_DN_PROP),
474: defaultProperties.getProperty(MGR_PW_PROP),
475: defaultProperties
476: .getProperty(SECURITY_AUTHENTICATION_PROP));
477:
478: // Get search information and user attribute from properties
479: // provide defaults if not present (backward compatibility)
480: String userAttribute = defaultProperties.getProperty(
481: USR_ATTR_PROP, USR_ATTR_DEFAULT);
482: searchFilter = "(" + userAttribute + "=" + userId + ")";
483: SearchControls scope = new SearchControls();
484:
485: recursiveSearch = isSubtreeSearch();
486: if (recursiveSearch) {
487: scope.setSearchScope(SearchControls.SUBTREE_SCOPE);
488: objectName = defaultProperties
489: .getProperty(PROVIDER_URL_PROP);
490: } else {
491: scope.setSearchScope(SearchControls.ONELEVEL_SCOPE);
492: objectName = defaultProperties.getProperty(
493: USR_BRANCH_PROP, USR_BRANCH_DEFAULT);
494: }
495:
496: if (getLogger().isDebugEnabled())
497: getLogger().debug(
498: "searching object " + objectName
499: + " filtering with " + searchFilter
500: + ", recursive search ? "
501: + recursiveSearch);
502:
503: NamingEnumeration results = context.search(objectName,
504: searchFilter, scope);
505: if (results != null && results.hasMore())
506: result = (SearchResult) results.next();
507:
508: // sanity check: if more than one entry is returned
509: // for a user-id, then the directory is probably flawed,
510: // so it would be nice to warn the administrator.
511: //
512: // This block is commented out for now, because of possible
513: // side-effects, such as unexpected exceptions.
514: // try {
515: // if (results.hasMore()) {
516: // getLogger().warn("Found more than one entry in the directory for
517: // user " + userId + ". You probably should deactivate recursive
518: // searches. The first entry was used as a work-around.");
519: // }
520: // }
521: // catch (javax.naming.PartialResultException e) {
522: // if (getLogger().isDebugEnabled())
523: // getLogger().debug("Catching and ignoring PartialResultException,
524: // as this means LDAP server does not support our sanity check");
525: // }
526:
527: } catch (NamingException e) {
528: if (getLogger().isDebugEnabled())
529: getLogger()
530: .debug(
531: "NamingException caught when searching on objectName = "
532: + objectName
533: + " and searchFilter="
534: + searchFilter
535: + ", this exception will be propagated: "
536: + e);
537: throw e;
538: } finally {
539: try {
540: if (context != null) {
541: close(context);
542: }
543: } catch (NamingException e) {
544: getLogger().warn(
545: "this should not happen: exception closing context "
546: + e);
547: }
548: }
549: return result;
550: }
551:
552: /**
553: * Encapsulation of the creation of a principal: we need to distinguish
554: * three cases, in order to support different modes of using a directory.
555: * The first is the use of a domain-name (requirement of MS Active
556: * Directory): if this property is set, this is used to construct the
557: * principal. The second case is where a user-id is somewhere in a domain,
558: * but not in a specific branch: in this case, a subtree search is performed
559: * to retrieve the complete path. The third case is where a specific branch
560: * of the directory is to be used; this is the case where usr-branch is set
561: * to a value. In this case, this branch is used to construct the principal.
562: * @return The principal
563: * @throws IOException
564: * @throws NamingException
565: */
566: private String getPrincipal() throws IOException, NamingException {
567:
568: String principal;
569:
570: // 1. Check if domain-name is to be supported
571: String domainProp = defaultProperties
572: .getProperty(DOMAIN_NAME_PROP);
573: if (domainProp != null && domainProp.trim().length() > 0) {
574: principal = domainProp + "\\" + getLdapId();
575: } else {
576: if (isSubtreeSearch()) {
577: // 2. Principal is constructed from directory entry
578: SearchResult entry = getDirectoryEntry(getLdapId());
579: principal = entry.getName();
580: if (entry.isRelative()) {
581: if (principal.length() > 0)
582: principal = principal
583: + ","
584: + defaultProperties
585: .getProperty(BASE_DN_PROP);
586: } else {
587: // if the item is found following a referral an URL string is
588: // returned which can not be used as principal
589: LdapURL ldapurl = new LdapURL(principal);
590: principal = ldapurl.getDN();
591: }
592: } else
593: // 3. Principal is constructed from properties
594: principal = constructPrincipal(getLdapId());
595: }
596:
597: return principal;
598: }
599:
600: /**
601: * Construct the principal for a user, by using the given userId along with
602: * the configured properties.
603: * @param userId The user id
604: * @return The principal
605: */
606: private String constructPrincipal(String userId) {
607: StringBuffer principal = new StringBuffer();
608: principal.append(
609: defaultProperties.getProperty(USR_ATTR_PROP,
610: USR_ATTR_DEFAULT)).append("=").append(userId)
611: .append(",");
612:
613: String baseDn = defaultProperties.getProperty(BASE_DN_PROP);
614: if (baseDn != null && baseDn.length() > 0) {
615: // USR_BRANCH_PROP may be empty, so only append when not-empty
616: String usrBranch = defaultProperties
617: .getProperty(USR_BRANCH_PROP);
618: if (usrBranch != null) {
619: if (usrBranch.trim().length() > 0)
620: principal.append(usrBranch).append(",");
621: } else
622: principal.append(USR_BRANCH_DEFAULT).append(",");
623:
624: principal.append(defaultProperties
625: .getProperty(BASE_DN_PROP));
626: } else {
627: // try for backwards compatibility of ldap properties
628: getLogger()
629: .warn(
630: "getPrincipal() read a deprecated format in ldap properties, please update");
631: principal.append(defaultProperties
632: .getProperty(PARTIAL_USER_DN_PROP));
633: }
634:
635: if (getLogger().isDebugEnabled())
636: getLogger().debug(
637: "getPrincipal() returning " + principal.toString());
638:
639: return principal.toString();
640: }
641:
642: }
|