/*
* $Header: /home/cvspublic/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/realm/JNDIRealm.java,v 1.11 2003/01/11 01:47:13 glenn Exp $
* $Revision: 1.11 $
* $Date: 2003/01/11 01:47:13 $
*
* ====================================================================
* The Apache Software License, Version 1.1
*
* Copyright (c) 1999-2002 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution, if
* any, must include the following acknowlegement:
* "This product includes software developed by the
* Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowlegement may appear in the software itself,
* if and wherever such third-party acknowlegements normally appear.
*
* 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
* Foundation" must not be used to endorse or promote products derived
* from this software without prior written permission. For written
* permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache"
* nor may "Apache" appear in their names without prior written
* permission of the Apache Group.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
*
Implementation of Realm that works with a directory * server accessed via the Java Naming and Directory Interface (JNDI) APIs. * The following constraints are imposed on the data structure in the * underlying directory server:
*DirContext
that is accessed
* via the connectionURL
property.connectURL
* an attempt will be made to use the alternateURL
if it
* exists.userPattern
property.userPattern
property is not
* specified, a unique element can be located by searching the directory
* context. In this case:
* userSearch
pattern specifies the search filter
* after substitution of the username.userBase
property can be set to the element that
* is the base of the subtree containing users. If not specified,
* the search base is the top-level context.userSubtree
property can be set to
* true
if you wish to search the entire subtree of the
* directory context. The default value of false
* requests a search of only the current level.userPassword
property is not specified.userPassword
* property is specified, in which case:
* userPassword
property.
* RealmBase.digest()
method (using the standard digest
* support included in RealmBase
).
* RealmBase.digest()
) are equal to the retrieved value
* for the user password attribute.DirContext
that is accessed via the
* connectionURL
property. This element has the following
* characteristics:
* roleSearch
* property.roleSearch
pattern optionally includes pattern
* replacements "{0}" for the distinguished name, and/or "{1}" for
* the username, of the authenticated user for which roles will be
* retrieved.roleBase
property can be set to the element that
* is the base of the search for matching roles. If not specified,
* the entire context will be searched.roleSubtree
property can be set to
* true
if you wish to search the entire subtree of the
* directory context. The default value of false
* requests a search of only the current level.roleName
property) containing the name of the
* role represented by this element.userRoleName
property.<security-role-ref>
element in
* the web application deployment descriptor allows applications to refer
* to roles programmatically by names other than those used in the
* directory server itself.TODO - Support connection pooling (including message
* format objects) so that authenticate()
does not have to be
* synchronized.
userSearch
.
*/
protected MessageFormat userSearchFormat = null;
/**
* Should we search the entire subtree for matching users?
*/
protected boolean userSubtree = false;
/**
* The attribute name used to retrieve the user password.
*/
protected String userPassword = null;
/**
* The message format used to form the distinguished name of a
* user, with "{0}" marking the spot where the specified username
* goes.
*/
protected String userPattern = null;
/**
* The MessageFormat object associated with the current
* userPattern
.
*/
protected MessageFormat userPatternFormat = null;
/**
* The base element for role searches.
*/
protected String roleBase = "";
/**
* The MessageFormat object associated with the current
* roleSearch
.
*/
protected MessageFormat roleFormat = null;
/**
* The name of an attribute in the user's entry containing
* roles for that user
*/
protected String userRoleName = null;
/**
* The name of the attribute containing roles held elsewhere
*/
protected String roleName = null;
/**
* The message format used to select roles for a user, with "{0}" marking
* the spot where the distinguished name of the user goes.
*/
protected String roleSearch = null;
/**
* Should we search the entire subtree for matching memberships?
*/
protected boolean roleSubtree = false;
/**
* An alternate URL, to which, we should connect if connectionURL fails.
*/
protected String alternateURL;
/**
* The number of connection attempts. If greater than zero we use the
* alternate url.
*/
protected int connectionAttempt = 0;
// ------------------------------------------------------------- Properties
/**
* Return the type of authentication to use.
*/
public String getAuthentication() {
return authentication;
}
/**
* Set the type of authentication to use.
*
* @param authentication The authentication
*/
public void setAuthentication(String authentication) {
this.authentication = authentication;
}
/**
* Return the connection username for this Realm.
*/
public String getConnectionName() {
return (this.connectionName);
}
/**
* Set the connection username for this Realm.
*
* @param connectionName The new connection username
*/
public void setConnectionName(String connectionName) {
this.connectionName = connectionName;
}
/**
* Return the connection password for this Realm.
*/
public String getConnectionPassword() {
return (this.connectionPassword);
}
/**
* Set the connection password for this Realm.
*
* @param connectionPassword The new connection password
*/
public void setConnectionPassword(String connectionPassword) {
this.connectionPassword = connectionPassword;
}
/**
* Return the connection URL for this Realm.
*/
public String getConnectionURL() {
return (this.connectionURL);
}
/**
* Set the connection URL for this Realm.
*
* @param connectionURL The new connection URL
*/
public void setConnectionURL(String connectionURL) {
this.connectionURL = connectionURL;
}
/**
* Return the JNDI context factory for this Realm.
*/
public String getContextFactory() {
return (this.contextFactory);
}
/**
* Set the JNDI context factory for this Realm.
*
* @param contextFactory The new context factory
*/
public void setContextFactory(String contextFactory) {
this.contextFactory = contextFactory;
}
/**
* Return the protocol to be used.
*/
public String getProtocol() {
return protocol;
}
/**
* Set the protocol for this Realm.
*
* @param protocol The new protocol.
*/
public void setProtocol(String protocol) {
this.protocol = protocol;
}
/**
* Returns the current settings for handling JNDI referrals.
*/
public String getReferrals () {
return referrals;
}
/**
* How do we handle JNDI referrals? ignore, follow, or throw
* (see javax.naming.Context.REFERRAL for more information).
*/
public void setReferrals (String referrals) {
this.referrals = referrals;
}
/**
* Return the base element for user searches.
*/
public String getUserBase() {
return (this.userBase);
}
/**
* Set the base element for user searches.
*
* @param userBase The new base element
*/
public void setUserBase(String userBase) {
this.userBase = userBase;
}
/**
* Return the message format pattern for selecting users in this Realm.
*/
public String getUserSearch() {
return (this.userSearch);
}
/**
* Set the message format pattern for selecting users in this Realm.
*
* @param userSearch The new user search pattern
*/
public void setUserSearch(String userSearch) {
this.userSearch = userSearch;
if (userSearch == null)
userSearchFormat = null;
else
userSearchFormat = new MessageFormat(userSearch);
}
/**
* Return the "search subtree for users" flag.
*/
public boolean getUserSubtree() {
return (this.userSubtree);
}
/**
* Set the "search subtree for users" flag.
*
* @param userSubtree The new search flag
*/
public void setUserSubtree(boolean userSubtree) {
this.userSubtree = userSubtree;
}
/**
* Return the user role name attribute name for this Realm.
*/
public String getUserRoleName() {
return userRoleName;
}
/**
* Set the user role name attribute name for this Realm.
*
* @param userRoleName The new userRole name attribute name
*/
public void setUserRoleName(String userRoleName) {
this.userRoleName = userRoleName;
}
/**
* Return the base element for role searches.
*/
public String getRoleBase() {
return (this.roleBase);
}
/**
* Set the base element for role searches.
*
* @param roleBase The new base element
*/
public void setRoleBase(String roleBase) {
this.roleBase = roleBase;
}
/**
* Return the role name attribute name for this Realm.
*/
public String getRoleName() {
return (this.roleName);
}
/**
* Set the role name attribute name for this Realm.
*
* @param roleName The new role name attribute name
*/
public void setRoleName(String roleName) {
this.roleName = roleName;
}
/**
* Return the message format pattern for selecting roles in this Realm.
*/
public String getRoleSearch() {
return (this.roleSearch);
}
/**
* Set the message format pattern for selecting roles in this Realm.
*
* @param roleSearch The new role search pattern
*/
public void setRoleSearch(String roleSearch) {
this.roleSearch = roleSearch;
if (roleSearch == null)
roleFormat = null;
else
roleFormat = new MessageFormat(roleSearch);
}
/**
* Return the "search subtree for roles" flag.
*/
public boolean getRoleSubtree() {
return (this.roleSubtree);
}
/**
* Set the "search subtree for roles" flag.
*
* @param roleSubtree The new search flag
*/
public void setRoleSubtree(boolean roleSubtree) {
this.roleSubtree = roleSubtree;
}
/**
* Return the password attribute used to retrieve the user password.
*/
public String getUserPassword() {
return (this.userPassword);
}
/**
* Set the password attribute used to retrieve the user password.
*
* @param userPassword The new password attribute
*/
public void setUserPassword(String userPassword) {
this.userPassword = userPassword;
}
/**
* Return the message format pattern for selecting users in this Realm.
*/
public String getUserPattern() {
return (this.userPattern);
}
/**
* Set the message format pattern for selecting users in this Realm.
*
* @param userPattern The new user pattern
*/
public void setUserPattern(String userPattern) {
this.userPattern = userPattern;
if (userPattern == null)
userPatternFormat = null;
else
userPatternFormat = new MessageFormat(userPattern);
}
/**
* Getter for property alternateURL.
*
* @return Value of property alternateURL.
*/
public String getAlternateURL() {
return this.alternateURL;
}
/**
* Setter for property alternateURL.
*
* @param alternateURL New value of property alternateURL.
*/
public void setAlternateURL(String alternateURL) {
this.alternateURL = alternateURL;
}
// ---------------------------------------------------------- Realm Methods
/**
* Return the Principal associated with the specified username and
* credentials, if there is one; otherwise return null
.
*
* If there are any errors with the JDBC connection, executing
* the query or anything we return null (don't authenticate). This
* event is also logged, and the connection will be closed so that
* a subsequent request will automatically re-open it.
*
* @param username Username of the Principal to look up
* @param credentials Password or other credentials to use in
* authenticating this username
*/
public Principal authenticate(String username, String credentials) {
DirContext context = null;
Principal principal = null;
try {
// Ensure that we have a directory context available
context = open();
// Occassionally the directory context will timeout. Try one more
// time before giving up.
try {
// Authenticate the specified username if possible
principal = authenticate(context, username, credentials);
} catch (CommunicationException e) {
// If not a "Socket closed." error then rethrow.
if (e.getMessage().indexOf("Socket closed") < 0)
throw(e);
// log the exception so we know it's there.
log(sm.getString("jndiRealm.exception"), e);
// close the connection so we know it will be reopened.
if (context != null)
close(context);
// open a new directory context.
context = open();
// Try the authentication again.
principal = authenticate(context, username, credentials);
}
// Release this context
release(context);
// Return the authenticated Principal (if any)
return (principal);
} catch (NamingException e) {
// Log the problem for posterity
log(sm.getString("jndiRealm.exception"), e);
// Close the connection so that it gets reopened next time
if (context != null)
close(context);
// Return "not authenticated" for this request
return (null);
}
}
// -------------------------------------------------------- Package Methods
// ------------------------------------------------------ Protected Methods
/**
* Return the Principal associated with the specified username and
* credentials, if there is one; otherwise return null
.
*
* @param context The directory context
* @param username Username of the Principal to look up
* @param credentials Password or other credentials to use in
* authenticating this username
*
* @exception NamingException if a directory server error occurs
*/
public synchronized Principal authenticate(DirContext context,
String username,
String credentials)
throws NamingException {
if (username == null || username.equals("")
|| credentials == null || credentials.equals(""))
return (null);
List roles = null;
if ( userPassword == null )
{
// Bind to the directory to authenticate (and obtain roles).
roles = bindAsUser(context, username, credentials);
if ( debug >= 2 )
log(sm.getString(((roles != null) ? "jndiRealm.authenticateSuccess" : "jndiRealm.authenticateFailure"),
username));
if ( roles == null )
return(null);
}
else
{
// Retrieve user information
User user = getUser(context, username);
if (user == null)
return (null);
boolean validated = compareCredentials(context, user, credentials);
if (debug >= 2) {
if (validated) {
log(sm.getString("jndiRealm.authenticateSuccess",
user.username));
} else {
log(sm.getString("jndiRealm.authenticateFailure",
user.username));
}
}
// Check the user's credentials
if (!validated)
return (null);
// Search for additional roles
roles = getRoles(context, user);
}
// Create and return a suitable Principal for this user
return (new GenericPrincipal(this, username, credentials, roles));
}
/**
* Return a User object containing information about the user
* with the specified username, if found in the directory;
* otherwise return null
.
*
* If the userPassword
configuration attribute is
* specified, the value of that attribute is retrieved from the
* user's directory entry. If the userRoleName
* configuration attribute is specified, all values of that
* attribute are retrieved from the directory entry.
*
* @param context The directory context
* @param username Username to be looked up
*
* @exception NamingException if a directory server error occurs
*/
protected User getUser(DirContext context, String username)
throws NamingException {
User user = null;
// Get attributes to retrieve from user entry
ArrayList list = new ArrayList();
if (userPassword != null)
list.add(userPassword);
if (userRoleName != null)
list.add(userRoleName);
String[] attrIds = new String[list.size()];
list.toArray(attrIds);
// Use pattern or search for user entry
if (userPatternFormat != null) {
user = getUserByPattern(context, username, attrIds);
} else {
user = getUserBySearch(context, username, attrIds);
}
return user;
}
/**
* Use the UserPattern
configuration attribute to
* locate the directory entry for the user with the specified
* username and return a User object; otherwise return
* null
.
*
* @param context The directory context
* @param username The username
* @param attrIds String[]containing names of attributes to
* retrieve.
*
* @exception NamingException if a directory server error occurs
*/
protected User getUserByPattern(DirContext context,
String username,
String[] attrIds)
throws NamingException {
if (debug >= 2)
log("lookupUser(" + username + ")");
if (username == null || userPatternFormat == null)
return (null);
// Form the dn from the user pattern
String dn = userPatternFormat.format(new String[] { username });
if (debug >= 3) {
log(" dn=" + dn);
}
// Return if no attributes to retrieve
if (attrIds == null || attrIds.length == 0)
return new User(username, dn, null, null);
// Get required attributes from user entry
Attributes attrs = null;
try {
attrs = context.getAttributes(dn, attrIds);
} catch (NameNotFoundException e) {
return (null);
}
if (attrs == null)
return (null);
// Retrieve value of userPassword
String password = null;
if (userPassword != null)
password = getAttributeValue(userPassword, attrs);
// Retrieve values of userRoleName attribute
ArrayList roles = null;
if (userRoleName != null)
roles = addAttributeValues(userRoleName, attrs, roles);
return new User(username, dn, password, roles);
}
/**
* Search the directory to return a User object containing
* information about the user with the specified username, if
* found in the directory; otherwise return null
.
*
* @param context The directory context
* @param username The username
* @param attrIds String[]containing names of attributes to retrieve.
*
* @exception NamingException if a directory server error occurs
*/
protected User getUserBySearch(DirContext context,
String username,
String[] attrIds)
throws NamingException {
if (username == null || userSearchFormat == null)
return (null);
// Form the search filter
String filter = userSearchFormat.format(new String[] { username });
// Set up the search controls
SearchControls constraints = new SearchControls();
if (userSubtree) {
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
}
else {
constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
}
// Specify the attributes to be retrieved
if (attrIds == null)
attrIds = new String[0];
constraints.setReturningAttributes(attrIds);
if (debug > 3) {
log(" Searching for " + username);
log(" base: " + userBase + " filter: " + filter);
}
NamingEnumeration results =
context.search(userBase, filter, constraints);
// Fail if no entries found
if (results == null || !results.hasMore()) {
if (debug > 2) {
log(" username not found");
}
return (null);
}
// Get result for the first entry found
SearchResult result = (SearchResult)results.next();
// Check no further entries were found
if (results.hasMore()) {
log("username " + username + " has multiple entries");
return (null);
}
// Get the entry's distinguished name
NameParser parser = context.getNameParser("");
Name contextName = parser.parse(context.getNameInNamespace());
Name baseName = parser.parse(userBase);
Name entryName = parser.parse(result.getName());
Name name = contextName.addAll(baseName);
name = name.addAll(entryName);
String dn = name.toString();
if (debug > 2)
log(" entry found for " + username + " with dn " + dn);
// Get the entry's attributes
Attributes attrs = result.getAttributes();
if (attrs == null)
return null;
// Retrieve value of userPassword
String password = null;
if (userPassword != null)
password = getAttributeValue(userPassword, attrs);
// Retrieve values of userRoleName attribute
ArrayList roles = null;
if (userRoleName != null)
roles = addAttributeValues(userRoleName, attrs, roles);
return new User(username, dn, password, roles);
}
/**
* Check whether the credentials presented by the user match those
* retrieved from the directory.
*
* @param context The directory context
* @param user The User to be authenticated
* @param credentials Authentication credentials
*
* @exception NamingException if a directory server error occurs
*/
protected boolean compareCredentials(DirContext context,
User info,
String credentials)
throws NamingException {
if (info == null || credentials == null)
return (false);
String password = info.password;
if (password == null)
return (false);
// Validate the credentials specified by the user
if (debug >= 3)
log(" validating credentials");
boolean validated = false;
if (hasMessageDigest()) {
// Hex hashes should be compared case-insensitive
validated = (digest(credentials).equalsIgnoreCase(password));
} else
validated = (digest(credentials).equals(password));
return (validated);
}
/**
* 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,
* a zero-length List is returned. If the user is not validated,
* returns null
.
*
* @param context The directory context
* @param username Username of the Principal to look up
* @param credentials Password or other credentials to use in
* authenticating this username
*
* @exception NamingException if a directory server error occurs
*/
protected List bindAsUser(DirContext context,
String username,
String credentials)
throws NamingException {
if (username == null || username.equals("")
|| credentials == null || credentials.equals(""))
return (null);
ArrayList roles = null;
// Bind to the directory to authenticate and obtain roles.
String dn = null;
// Use pattern or search for user entry
if (userPatternFormat != null) {
if (debug >= 2)
log("lookupUser(" + username + ")");
// Form the dn from the user pattern
dn = userPatternFormat.format(new String[] { username });
if (debug >= 3) {
log(" dn=" + dn);
}
} else {
if (userSearchFormat == null)
return (null);
// Form the search filter
String filter = userSearchFormat.format(new String[] { username });
// Set up the search controls
SearchControls constraints = new SearchControls();
if (userSubtree) {
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
}
else {
constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
}
constraints.setReturningAttributes(new String[0]);
if (debug > 3) {
log(" Searching for " + username);
log(" base: " + userBase + " filter: " + filter);
}
NamingEnumeration results =
context.search(userBase, filter, constraints);
// Fail if no entries found
if (results == null || !results.hasMore()) {
if (debug > 2) {
log(" username not found");
}
return(null);
}
// Get result for the first entry found
SearchResult result = (SearchResult)results.next();
// Check no further entries were found
if (results.hasMore()) {
log("username " + username + " has multiple entries");
return (null);
}
// Get the entry's distinguished name
NameParser parser = context.getNameParser("");
Name contextName = parser.parse(context.getNameInNamespace());
Name baseName = parser.parse(userBase);
Name entryName = parser.parse(result.getName());
Name name = contextName.addAll(baseName);
name = name.addAll(entryName);
dn = name.toString();
if (debug > 2)
log(" entry found for " + username + " with dn " + dn);
}
if (dn == null)
return (null);
// Validate the credentials specified by the user
if (debug >= 3) {
log(" validating credentials by binding as the user");
}
// Set up security environment to bind as the user
context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
// Elicit an LDAP bind operation
try {
if (debug > 2) {
log(" binding as " + dn);
}
ArrayList list = new ArrayList();
if (userRoleName != null)
list.add(userRoleName);
String[] attrIds = new String[list.size()];
list.toArray(attrIds);
Attributes attrs = context.getAttributes("", attrIds);
if (attrs != null)
{
// Retrieve values of userRoleName attribute
if (userRoleName != null)
roles = addAttributeValues(userRoleName, attrs, roles);
}
// Search for additional roles.
roles = (ArrayList)getRoles(context, new User(username, dn, null, roles));
// To indicate success roles must be non-null.
if ( roles == null )
roles = new ArrayList();
}
catch (AuthenticationException e) {
if (debug > 2) {
log(" bind attempt failed");
}
}
// 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 (roles);
}
/**
* 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,
* a zero-length List is returned.
*
* @param context The directory context we are searching
* @param user The User to be checked
*
* @exception NamingException if a directory server error occurs
*/
protected List getRoles(DirContext context, User user)
throws NamingException {
if (user == null)
return (null);
String dn = user.dn;
String username = user.username;
if (dn == null || username == null)
return (null);
if (debug >= 2)
log(" getRoles(" + dn + ")");
// Start with roles retrieved from the user entry
ArrayList list = user.roles;
if (list == null) {
list = new ArrayList();
}
// 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[] { dn, username });
SearchControls controls = new SearchControls();
if (roleSubtree)
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
else
controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
controls.setReturningAttributes(new String[] {roleName});
// Perform the configured search and process the results
if (debug >= 3) {
log(" Searching role base '" + roleBase + "' for attribute '" +
roleName + "'");
log(" With filter expression '" + filter + "'");
}
NamingEnumeration 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);
}
// Return the augmented list of roles
if (debug >= 2) {
log(" Returning " + list.size() + " roles");
for (int i=0; i