Author: remy.maucherat(a)jboss.com
Date: 2008-11-12 09:59:30 -0500 (Wed, 12 Nov 2008)
New Revision: 853
Modified:
trunk/java/org/apache/catalina/realm/JNDIRealm.java
trunk/webapps/docs/changelog.xml
Log:
- Port JNDI realm updates (not very useful, but does not hurt).
Modified: trunk/java/org/apache/catalina/realm/JNDIRealm.java
===================================================================
--- trunk/java/org/apache/catalina/realm/JNDIRealm.java 2008-11-12 14:57:19 UTC (rev 852)
+++ trunk/java/org/apache/catalina/realm/JNDIRealm.java 2008-11-12 14:59:30 UTC (rev 853)
@@ -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.
@@ -22,8 +22,13 @@
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.Map;
+import java.util.Set;
import javax.naming.Context;
import javax.naming.CommunicationException;
@@ -35,6 +40,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;
@@ -132,6 +138,14 @@
* 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 roles
+ * recursively by setting <code>roleRecursionLimit</code> to some positive
value.
+ * The default value is <code>0</code>, so role searches do not
recurse.</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
@@ -195,14 +209,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";
@@ -228,10 +242,21 @@
/**
- * 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,6 +319,13 @@
/**
+ * The maximum recursion depth when resolving roles recursively.
+ * By default we don't resolve roles recursively.
+ */
+ protected int roleRecursionLimit = 0;
+
+
+ /**
* The base element for role searches.
*/
protected String roleBase = "";
@@ -343,10 +375,11 @@
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
/**
@@ -461,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) {
@@ -494,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 () {
@@ -602,6 +652,28 @@
/**
+ * Return the maximum recursion depth for role searches.
+ */
+ public int getRoleRecursionLimit() {
+
+ return (this.roleRecursionLimit);
+
+ }
+
+
+ /**
+ * Set the maximum recursion depth for role searches.
+ *
+ * @param roleRecursionLimit The new recursion limit
+ */
+ public void setRoleRecursionLimit(int roleRecursionLimit) {
+
+ this.roleRecursionLimit = roleRecursionLimit;
+
+ }
+
+
+ /**
* Return the base element for role searches.
*/
public String getRoleBase() {
@@ -776,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
@@ -875,6 +969,8 @@
close(context);
// Return "not authenticated" for this request
+ if (containerLog.isDebugEnabled())
+ containerLog.debug("Returning null principal.");
return (null);
}
@@ -905,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,
@@ -938,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);
@@ -948,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));
@@ -960,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>
@@ -968,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;
@@ -986,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);
}
@@ -997,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 {
@@ -1045,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>.
@@ -1056,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)
@@ -1086,32 +1299,34 @@
// 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();
// 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);
+ String dn = getDistinguishedName(context, userBase, result);
- // 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();
-
if (containerLog.isTraceEnabled())
containerLog.trace(" entry found for " + username + " with dn
" + dn);
@@ -1333,6 +1548,69 @@
/**
+ * Add roles to a user and search for other roles containing them themselves.
+ * We search recursively with a limited depth.
+ * By default the depth is 0, and we only use direct roles.
+ * The search needs to use the distinguished role names,
+ * but to return the role names.
+ *
+ * @param depth Recursion depth, starting at zero
+ * @param context The directory context we are searching
+ * @param recursiveMap The cumulative result map of role names and DNs.
+ * @param recursiveSet The cumulative result set of role names.
+ * @param groupName The role name to add to the list.
+ * @param groupDName The distinguished name of the role.
+ *
+ * @exception NamingException if a directory server error occurs
+ */
+ private void getRolesRecursive(int depth, DirContext context, Map<String,
String> recursiveMap, Set<String> recursiveSet,
+ String groupName, String groupDName) throws
NamingException {
+ if (containerLog.isTraceEnabled())
+ containerLog.trace("Recursive search depth " + depth + " for
group '" + groupDName + " (" + groupName + ")'");
+ // Adding the given group to the result set if not already found
+ if (!recursiveSet.contains(groupDName)) {
+ recursiveSet.add(groupDName);
+ recursiveMap.put(groupDName, groupName);
+ if (depth >= roleRecursionLimit) {
+ if (roleRecursionLimit > 0)
+ containerLog.warn("Terminating recursive role search because of
recursion limit " +
+ roleRecursionLimit + ", results might be
incomplete");
+ return;
+ }
+ // Prepare the parameters for searching groups
+ String filter = roleFormat.format(new String[] { groupDName });
+ SearchControls controls = new SearchControls();
+ controls.setSearchScope(roleSubtree ? SearchControls.SUBTREE_SCOPE :
SearchControls.ONELEVEL_SCOPE);
+ controls.setReturningAttributes(new String[] { roleName });
+ if (containerLog.isTraceEnabled()) {
+ containerLog.trace("Recursive search in role base '" +
roleBase + "' for attribute '" + roleName + "'" +
+ " with filter expression '" + filter +
"'");
+ }
+ // Searching groups that assign the given group
+ NamingEnumeration results = context.search(roleBase, filter, controls);
+ if (results != null) {
+ // Iterate over the resulting groups
+ try {
+ while (results.hasMore()) {
+ SearchResult result = (SearchResult) 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) {
+ getRolesRecursive(depth+1, context, recursiveMap,
recursiveSet, name, dname);
+ }
+ }
+ } catch (PartialResultException ex) {
+ if (!adCompat)
+ throw ex;
+ }
+ }
+ }
+ }
+
+ /**
* Return a List of roles associated with the given User. Any
* roles present in the user's directory entry are supplemented by
* a directory search. If no roles are associated with this user,
@@ -1363,7 +1641,19 @@
if (list == null) {
list = new ArrayList<String>();
}
+ if (commonRole != null)
+ list.add(commonRole);
+ if (containerLog.isTraceEnabled()) {
+ if (list != null) {
+ 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));
+ } else {
+ containerLog.trace(" Found no user internal roles");
+ }
+ }
+
// Are we configured to do role searches?
if ((roleFormat == null) || (roleName == null))
return (list);
@@ -1382,26 +1672,52 @@
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 = (SearchResult) 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 (Iterator<String> i = keys.iterator(); i.hasNext();) {
+ Object k = i.next();
+ containerLog.trace( " Found direct role " + k + " ->
" + groupMap.get(k));
}
}
- return (list);
+ HashSet<String> recursiveSet = new HashSet<String>();
+ HashMap<String, String> recursiveMap = new HashMap<String,
String>();
+
+ for (Iterator<String> i = keys.iterator(); i.hasNext();) {
+ String k = i.next();
+ getRolesRecursive(0, context, recursiveMap, recursiveSet, groupMap.get(k),
k);
+ }
+
+ HashSet<String> resultSet = new HashSet<String>(list);
+ resultSet.addAll(recursiveMap.values());
+
+ if (containerLog.isTraceEnabled()) {
+ containerLog.trace(" Returning " + resultSet.size() + "
roles");
+ for (Iterator<String> i = resultSet.iterator(); i.hasNext();)
+ containerLog.trace( " Found role " + i.next());
+ }
+
+ return new ArrayList<String>(resultSet);
}
@@ -1463,9 +1779,14 @@
if (attr == null)
return (values);
NamingEnumeration e = attr.getAll();
- while(e.hasMore()) {
- String value = (String)e.next();
- values.add(value);
+ try {
+ while(e.hasMore()) {
+ String value = (String)e.next();
+ values.add(value);
+ }
+ } catch (PartialResultException ex) {
+ if (!adCompat)
+ throw ex;
}
return values;
}
@@ -1597,9 +1918,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));
}
@@ -1821,6 +2142,30 @@
}
+ /**
+ * Returns the distinguished name of a search result.
+ *
+ * @param context Our DirContext
+ * @param base The base DN
+ * @param result The search result
+ * @return String containing the distinguished name
+ */
+ protected String getDistinguishedName(DirContext context, String base, SearchResult
result)
+ throws NamingException {
+ // Get the entry's distinguished name
+ NameParser parser = context.getNameParser("");
+ Name contextName = parser.parse(context.getNameInNamespace());
+ Name baseName = parser.parse(base);
+
+ // Bugzilla 32269
+ Name entryName = parser.parse(new CompositeName(result.getName()).get(0));
+
+ Name name = contextName.addAll(baseName);
+ name = name.addAll(entryName);
+ return name.toString();
+ }
+
+
}
// ------------------------------------------------------ Private Classes
Modified: trunk/webapps/docs/changelog.xml
===================================================================
--- trunk/webapps/docs/changelog.xml 2008-11-12 14:57:19 UTC (rev 852)
+++ trunk/webapps/docs/changelog.xml 2008-11-12 14:59:30 UTC (rev 853)
@@ -26,6 +26,9 @@
<fix>
<bug>42707</bug>: Adding host aliases should be dynamic. (markt)
</fix>
+ <fix>
+ JNDI realm feature additions. (rjung)
+ </fix>
</changelog>
</subsection>
</section>