Author: markt Date: Mon Sep 7 12:45:58 2009 New Revision: 812115 URL: http://svn.apache.org/viewvc?rev=812115&view=rev Log: Apply AD improvements
Modified: tomcat/tc6.0.x/trunk/STATUS.txt tomcat/tc6.0.x/trunk/java/org/apache/catalina/realm/JNDIRealm.java tomcat/tc6.0.x/trunk/webapps/docs/changelog.xml Modified: tomcat/tc6.0.x/trunk/STATUS.txt URL: http://svn.apache.org/viewvc/tomcat/tc6.0.x/trunk/STATUS.txt?rev=812115&r1=812114&r2=812115&view=diff ============================================================================== --- tomcat/tc6.0.x/trunk/STATUS.txt (original) +++ tomcat/tc6.0.x/trunk/STATUS.txt Mon Sep 7 12:45:58 2009 @@ -144,17 +144,6 @@ +1: kkolinko, markt, rjung, funkman -1: -* Port Active Directory improvements to JNDIREalm from trunk - Patch testing successfully by willing volunteer on the users list - http://people.apache.org/~markt/patches/2009-08-06-ADforJNDIRealm.patch - +1: markt, kkolinko, funkman - -1: - kkolinko: ( - There are several (two) places with a loop printing containerLog.debug( - "Found role: " + it.next()); It would be better to prepare the whole string - of roles and print it at once. - ) - * Port TLD processing improvements from trunk There have been quite a few changes to TLD processing and they are tightly coupled. Therefore, this proposal is a series of patches and the patches Modified: tomcat/tc6.0.x/trunk/java/org/apache/catalina/realm/JNDIRealm.java URL: http://svn.apache.org/viewvc/tomcat/tc6.0.x/trunk/java/org/apache/catalina/realm/JNDIRealm.java?rev=812115&r1=812114&r2=812115&view=diff ============================================================================== --- tomcat/tc6.0.x/trunk/java/org/apache/catalina/realm/JNDIRealm.java (original) +++ tomcat/tc6.0.x/trunk/java/org/apache/catalina/realm/JNDIRealm.java Mon Sep 7 12:45:58 2009 @@ -5,9 +5,9 @@ * 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. @@ -24,8 +24,12 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.Hashtable; +import java.util.Iterator; import java.util.List; +import java.util.Set; import javax.naming.Context; import javax.naming.CommunicationException; @@ -37,6 +41,7 @@ import javax.naming.NameParser; import javax.naming.Name; import javax.naming.AuthenticationException; +import javax.naming.PartialResultException; import javax.naming.ServiceUnavailableException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; @@ -134,6 +139,15 @@ * in the user's element whose name is configured by the * <code>userRoleName</code> property.</li> * + * <li>A default role can be assigned to each user that was successfully + * authenticated by setting the <code>commonRole</code> property to the + * name of this role. The role doesn't have to exist in the directory.</li> + * + * <li>If the directory server contains nested roles, you can search for them + * by setting <code>roleNested</code> to <code>true</code>. + * The default value is <code>false</code>, so role searches will not find + * nested roles.</li> + * * <li>Note that the standard <code><security-role-ref></code> element in * the web application deployment descriptor allows applications to refer * to roles programmatically by names other than those used in the @@ -197,14 +211,14 @@ */ protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory"; - + /** * How aliases should be dereferenced during search operations. */ protected String derefAliases = null; /** - * Constant that holds the name of the environment property for specifying + * Constant that holds the name of the environment property for specifying * the manner in which aliases should be dereferenced. */ public final static String DEREF_ALIASES = "java.naming.ldap.derefAliases"; @@ -230,9 +244,20 @@ /** - * How should we handle referrals? Microsoft Active Directory can't handle - * the default case, so an application authenticating against AD must - * set referrals to "follow". + * Should we ignore PartialResultExceptions when iterating over NamingEnumerations? + * Microsoft Active Directory often returns referrals, which lead + * to PartialResultExceptions. Unfortunately there's no stable way to detect, + * if the Exceptions really come from an AD referral. + * Set to true to ignore PartialResultExceptions. + */ + protected boolean adCompat = false; + + + /** + * How should we handle referrals? Microsoft Active Directory often returns + * referrals. If you need to follow them set referrals to "follow". + * Caution: if your DNS is not part of AD, the LDAP client lib might try + * to resolve your domain name in DNS to find another LDAP server. */ protected String referrals = null; @@ -294,7 +319,6 @@ */ protected MessageFormat[] userPatternFormatArray = null; - /** * The base element for role searches. */ @@ -332,6 +356,12 @@ * Should we search the entire subtree for matching memberships? */ protected boolean roleSubtree = false; + + /** + * Should we look for nested group in order to determine roles? + */ + protected boolean roleNested = false; + /** * An alternate URL, to which, we should connect if connectionURL fails. @@ -345,9 +375,10 @@ protected int connectionAttempt = 0; /** - * The current user pattern to be used for lookup and binding of a user. + * Add this role to every authenticated user */ - protected int curUserPattern = 0; + protected String commonRole = null; + // ------------------------------------------------------------- Properties @@ -463,11 +494,11 @@ */ public java.lang.String getDerefAliases() { return derefAliases; - } - + } + /** * Set the value for derefAliases to be used when searching the directory. - * + * * @param derefAliases New value of property derefAliases. */ public void setDerefAliases(java.lang.String derefAliases) { @@ -496,6 +527,23 @@ /** + * Returns the current settings for handling PartialResultExceptions + */ + public boolean getAdCompat () { + return adCompat; + } + + + /** + * How do we handle PartialResultExceptions? + * True: ignore all PartialResultExceptions. + */ + public void setAdCompat (boolean adCompat) { + this.adCompat = adCompat; + } + + + /** * Returns the current settings for handling JNDI referrals. */ public String getReferrals () { @@ -693,6 +741,28 @@ this.roleSubtree = roleSubtree; } + + /** + * Return the "The nested group search flag" flag. + */ + public boolean getRoleNested() { + + return (this.roleNested); + + } + + + /** + * Set the "search subtree for roles" flag. + * + * @param roleNested The nested group search flag + */ + public void setRoleNested(boolean roleNested) { + + this.roleNested = roleNested; + + } + /** @@ -778,6 +848,28 @@ } + /** + * Return the common role + */ + public String getCommonRole() { + + return commonRole; + + } + + + /** + * Set the common role + * + * @param commonRole The common role + */ + public void setCommonRole(String commonRole) { + + this.commonRole = commonRole; + + } + + // ---------------------------------------------------------- Realm Methods @@ -877,6 +969,8 @@ close(context); // Return "not authenticated" for this request + if (containerLog.isDebugEnabled()) + containerLog.debug("Returning null principal."); return (null); } @@ -907,21 +1001,30 @@ throws NamingException { if (username == null || username.equals("") - || credentials == null || credentials.equals("")) + || credentials == null || credentials.equals("")) { + if (containerLog.isDebugEnabled()) + containerLog.debug("username null or empty: returning null principal."); return (null); + } if (userPatternArray != null) { - for (curUserPattern = 0; + for (int curUserPattern = 0; curUserPattern < userPatternFormatArray.length; curUserPattern++) { // Retrieve user information - User user = getUser(context, username); + User user = getUser(context, username, credentials, curUserPattern); if (user != null) { try { // Check the user's credentials if (checkCredentials(context, user, credentials)) { // Search for additional roles List<String> roles = getRoles(context, user); + if (containerLog.isDebugEnabled()) { + Iterator<String> it = roles.iterator(); + while (it.hasNext()) { + containerLog.debug("Found role: " + it.next()); + } + } return (new GenericPrincipal(this, username, credentials, @@ -940,7 +1043,7 @@ return null; } else { // Retrieve user information - User user = getUser(context, username); + User user = getUser(context, username, credentials); if (user == null) return (null); @@ -950,6 +1053,12 @@ // Search for additional roles List<String> roles = getRoles(context, user); + if (containerLog.isDebugEnabled()) { + Iterator<String> it = roles.iterator(); + while (it.hasNext()) { + containerLog.debug("Found role: " + it.next()); + } + } // Create and return a suitable Principal for this user return (new GenericPrincipal(this, username, credentials, roles)); @@ -962,6 +1071,45 @@ * with the specified username, if found in the directory; * otherwise return <code>null</code>. * + * @param context The directory context + * @param username Username to be looked up + * + * @exception NamingException if a directory server error occurs + * + * @see #getUser(DirContext, String, String, int) + */ + protected User getUser(DirContext context, String username) + throws NamingException { + + return getUser(context, username, null, -1); + } + + + /** + * Return a User object containing information about the user + * with the specified username, if found in the directory; + * otherwise return <code>null</code>. + * + * @param context The directory context + * @param username Username to be looked up + * @param credentials User credentials (optional) + * + * @exception NamingException if a directory server error occurs + * + * @see #getUser(DirContext, String, int) + */ + protected User getUser(DirContext context, String username, String credentials) + throws NamingException { + + return getUser(context, username, credentials, -1); + } + + + /** + * Return a User object containing information about the user + * with the specified username, if found in the directory; + * otherwise return <code>null</code>. + * * If the <code>userPassword</code> configuration attribute is * specified, the value of that attribute is retrieved from the * user's directory entry. If the <code>userRoleName</code> @@ -970,10 +1118,13 @@ * * @param context The directory context * @param username Username to be looked up + * @param credentials User credentials (optional) + * @param curUserPattern Index into userPatternFormatArray * * @exception NamingException if a directory server error occurs */ - protected User getUser(DirContext context, String username) + protected User getUser(DirContext context, String username, + String credentials, int curUserPattern) throws NamingException { User user = null; @@ -988,8 +1139,8 @@ list.toArray(attrIds); // Use pattern or search for user entry - if (userPatternFormatArray != null) { - user = getUserByPattern(context, username, attrIds); + if (userPatternFormatArray != null && curUserPattern >= 0) { + user = getUserByPattern(context, username, credentials, attrIds, curUserPattern); } else { user = getUserBySearch(context, username, attrIds); } @@ -999,29 +1150,24 @@ /** - * Use the <code>UserPattern</code> configuration attribute to - * locate the directory entry for the user with the specified - * username and return a User object; otherwise return - * <code>null</code>. + * Use the distinguished name to locate the directory + * entry for the user with the specified username and + * return a User object; otherwise return <code>null</code>. * * @param context The directory context * @param username The username * @param attrIds String[]containing names of attributes to + * @param dn Distinguished name of the user * retrieve. * * @exception NamingException if a directory server error occurs */ protected User getUserByPattern(DirContext context, - String username, - String[] attrIds) + String username, + String[] attrIds, + String dn) throws NamingException { - if (username == null || userPatternFormatArray[curUserPattern] == null) - return (null); - - // Form the dn from the user pattern - String dn = userPatternFormatArray[curUserPattern].format(new String[] { username }); - // Get required attributes from user entry Attributes attrs = null; try { @@ -1047,6 +1193,71 @@ /** + * Use the <code>UserPattern</code> configuration attribute to + * locate the directory entry for the user with the specified + * username and return a User object; otherwise return + * <code>null</code>. + * + * @param context The directory context + * @param username The username + * @param credentials User credentials (optional) + * @param attrIds String[]containing names of attributes to + * @param curUserPattern Index into userPatternFormatArray + * + * @exception NamingException if a directory server error occurs + * @see #getUserByPattern(DirContext, String, String[], String) + */ + protected User getUserByPattern(DirContext context, + String username, + String credentials, + String[] attrIds, + int curUserPattern) + throws NamingException { + + User user = null; + + if (username == null || userPatternFormatArray[curUserPattern] == null) + return (null); + + // Form the dn from the user pattern + String dn = userPatternFormatArray[curUserPattern].format(new String[] { username }); + + try { + user = getUserByPattern(context, username, attrIds, dn); + } catch (NameNotFoundException e) { + return (null); + } catch (NamingException e) { + // If the getUserByPattern() call fails, try it again with the + // credentials of the user that we're searching for + try { + // Set up security environment to bind as the user + context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); + context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials); + + user = getUserByPattern(context, username, attrIds, dn); + } finally { + // Restore the original security environment + if (connectionName != null) { + context.addToEnvironment(Context.SECURITY_PRINCIPAL, + connectionName); + } else { + context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); + } + + if (connectionPassword != null) { + context.addToEnvironment(Context.SECURITY_CREDENTIALS, + connectionPassword); + } + else { + context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); + } + } + } + return user; + } + + + /** * Search the directory to return a User object containing * information about the user with the specified username, if * found in the directory; otherwise return <code>null</code>. @@ -1058,8 +1269,8 @@ * @exception NamingException if a directory server error occurs */ protected User getUserBySearch(DirContext context, - String username, - String[] attrIds) + String username, + String[] attrIds) throws NamingException { if (username == null || userSearchFormat == null) @@ -1083,36 +1294,38 @@ attrIds = new String[0]; constraints.setReturningAttributes(attrIds); - NamingEnumeration results = + NamingEnumeration<SearchResult> results = context.search(userBase, filter, constraints); // Fail if no entries found - if (results == null || !results.hasMore()) { - return (null); + try { + if (results == null || !results.hasMore()) { + return (null); + } + } catch (PartialResultException ex) { + if (!adCompat) + throw ex; + else + return (null); } // Get result for the first entry found - SearchResult result = (SearchResult)results.next(); + SearchResult result = results.next(); // Check no further entries were found - if (results.hasMore()) { - if(containerLog.isInfoEnabled()) - containerLog.info("username " + username + " has multiple entries"); - return (null); + try { + if (results.hasMore()) { + if(containerLog.isInfoEnabled()) + containerLog.info("username " + username + " has multiple entries"); + return (null); + } + } catch (PartialResultException ex) { + if (!adCompat) + throw ex; } - // Get the entry's distinguished name - NameParser parser = context.getNameParser(""); - Name contextName = parser.parse(context.getNameInNamespace()); - Name baseName = parser.parse(userBase); - - // Bugzilla 32269 - Name entryName = parser.parse(new CompositeName(result.getName()).get(0)); - - Name name = contextName.addAll(baseName); - name = name.addAll(entryName); - String dn = name.toString(); + String dn = getDistinguishedName(context, userBase, result); if (containerLog.isTraceEnabled()) containerLog.trace(" entry found for " + username + " with dn " + dn); @@ -1333,7 +1546,6 @@ return (validated); } - /** * Return a List of roles associated with the given User. Any * roles present in the user's directory entry are supplemented by @@ -1365,11 +1577,19 @@ if (list == null) { list = new ArrayList<String>(); } + if (commonRole != null) + list.add(commonRole); + + if (containerLog.isTraceEnabled()) { + containerLog.trace(" Found " + list.size() + " user internal roles"); + for (int i=0; i<list.size(); i++) + containerLog.trace( " Found user internal role " + list.get(i)); + } // Are we configured to do role searches? if ((roleFormat == null) || (roleName == null)) return (list); - + // Set up parameters for an appropriate search String filter = roleFormat.format(new String[] { doRFC2254Encoding(dn), username }); SearchControls controls = new SearchControls(); @@ -1380,30 +1600,86 @@ controls.setReturningAttributes(new String[] {roleName}); // Perform the configured search and process the results - NamingEnumeration results = + NamingEnumeration<SearchResult> results = context.search(roleBase, filter, controls); if (results == null) return (list); // Should never happen, but just in case ... - while (results.hasMore()) { - SearchResult result = (SearchResult) results.next(); - Attributes attrs = result.getAttributes(); - if (attrs == null) - continue; - list = addAttributeValues(roleName, attrs, list); - } + HashMap<String, String> groupMap = new HashMap<String, String>(); + try { + while (results.hasMore()) { + SearchResult result = results.next(); + Attributes attrs = result.getAttributes(); + if (attrs == null) + continue; + String dname = getDistinguishedName(context, roleBase, result); + String name = getAttributeValue(roleName, attrs); + if (name != null && dname != null) { + groupMap.put(dname, name); + } + } + } catch (PartialResultException ex) { + if (!adCompat) + throw ex; + } + Set<String> keys = groupMap.keySet(); if (containerLog.isTraceEnabled()) { - if (list != null) { - containerLog.trace(" Returning " + list.size() + " roles"); - for (int i=0; i<list.size(); i++) - containerLog.trace( " Found role " + list.get(i)); - } else { - containerLog.trace(" getRoles about to return null "); + containerLog.trace(" Found " + keys.size() + " direct roles"); + for (String key: keys) { + containerLog.trace( " Found direct role " + key + " -> " + groupMap.get(key)); } } - return (list); + // if nested group search is enabled, perform searches for nested groups until no new group is found + if (getRoleNested()) { + + // The following efficient algorithm is known as memberOf Algorithm, as described in "Practices in + // Directory Groups". It avoids group slurping and handles cyclic group memberships as well. + // See http://middleware.internet2.edu/dir/ for details + + Set<String> newGroupDNs = new HashSet<String>(groupMap.keySet()); + while (!newGroupDNs.isEmpty()) { + Set<String> newThisRound = new HashSet<String>(); // Stores the groups we find in this iteration + + for (String groupDN : newGroupDNs) { + filter = roleFormat.format(new String[] { groupDN }); + + if (containerLog.isTraceEnabled()) { + containerLog.trace("Perform a nested group search with base "+ roleBase + " and filter " + filter); + } + + results = context.search(roleBase, filter, controls); + + try { + while (results.hasMore()) { + SearchResult result = results.next(); + Attributes attrs = result.getAttributes(); + if (attrs == null) + continue; + String dname = getDistinguishedName(context, roleBase, result); + String name = getAttributeValue(roleName, attrs); + if (name != null && dname != null && !groupMap.keySet().contains(dname)) { + groupMap.put(dname, name); + newThisRound.add(dname); + + if (containerLog.isTraceEnabled()) { + containerLog.trace(" Found nested role " + dname + " -> " + name); + } + + } + } + } catch (PartialResultException ex) { + if (!adCompat) + throw ex; + } + } + + newGroupDNs = newThisRound; + } + } + + return new ArrayList<String>(groupMap.values()); } @@ -1464,10 +1740,15 @@ Attribute attr = attrs.get(attrId); if (attr == null) return (values); - NamingEnumeration e = attr.getAll(); - while(e.hasMore()) { - String value = (String)e.next(); - values.add(value); + NamingEnumeration<?> e = attr.getAll(); + try { + while(e.hasMore()) { + String value = (String)e.next(); + values.add(value); + } + } catch (PartialResultException ex) { + if (!adCompat) + throw ex; } return values; } @@ -1599,9 +1880,9 @@ protected synchronized Principal getPrincipal(DirContext context, String username) throws NamingException { - + User user = getUser(context, username); - + return new GenericPrincipal(this, user.username, user.password , getRoles(context, user)); } @@ -1650,7 +1931,7 @@ * * @return java.util.Hashtable the configuration for the directory context. */ - protected Hashtable getDirectoryContextEnvironment() { + protected Hashtable<String,String> getDirectoryContextEnvironment() { Hashtable<String,String> env = new Hashtable<String,String>(); @@ -1689,7 +1970,7 @@ */ protected void release(DirContext context) { - ; // NO-OP since we are not pooling anything + // NO-OP since we are not pooling anything } @@ -1773,7 +2054,7 @@ startingPoint = endParenLoc+1; startParenLoc = userPatternString.indexOf('(', startingPoint); } - return (String[])pathList.toArray(new String[] {}); + return pathList.toArray(new String[] {}); } return null; Modified: tomcat/tc6.0.x/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/tc6.0.x/trunk/webapps/docs/changelog.xml?rev=812115&r1=812114&r2=812115&view=diff ============================================================================== --- tomcat/tc6.0.x/trunk/webapps/docs/changelog.xml (original) +++ tomcat/tc6.0.x/trunk/webapps/docs/changelog.xml Mon Sep 7 12:45:58 2009 @@ -155,6 +155,11 @@ Correct JDBC driver de-registration on web application stop and fix NPE that is exposed by the fix. (markt) </fix> + <update> + Various JNDI realm improvements for Active Directory. These include the + ability to specify a default role, optional handling for nested roles + and an option to ignore PartialResultExceptions (markt). + </update> </changelog> </subsection> <subsection name="Coyote"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org