yoavs 2004/09/21 16:01:50 Modified: catalina/src/share/org/apache/catalina/realm Tag: TOMCAT_5_0 JAASCallbackHandler.java JAASRealm.java LocalStrings.properties webapps/docs Tag: TOMCAT_5_0 changelog.xml Log: Bugzilla 28631: JAASRealm enhancements to allow custom user and group principal classes (and use commons-logging) Revision Changes Path No revision No revision 1.3.2.1 +42 -11 jakarta-tomcat-catalina/catalina/src/share/org/apache/catalina/realm/JAASCallbackHandler.java Index: JAASCallbackHandler.java =================================================================== RCS file: /home/cvs/jakarta-tomcat-catalina/catalina/src/share/org/apache/catalina/realm/JAASCallbackHandler.java,v retrieving revision 1.3 retrieving revision 1.3.2.1 diff -u -r1.3 -r1.3.2.1 --- JAASCallbackHandler.java 29 Feb 2004 12:38:47 -0000 1.3 +++ JAASCallbackHandler.java 21 Sep 2004 23:01:50 -0000 1.3.2.1 @@ -25,25 +25,38 @@ import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; +import org.apache.catalina.util.StringManager; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; /** - * <p>Implementation of the JAAS <strong>CallbackHandler</code> interface, + * <p>Implementation of the JAAS <code>CallbackHandler</code> interface, * used to negotiate delivery of the username and credentials that were * specified to our constructor. No interaction with the user is required * (or possible).</p> * + * <p>This <code>CallbackHandler</code> will pre-digest the supplied + * password, if required by the <code><Realm></code> element in + * <code>server.xml</code>.</p> + * <p>At present, <code>JAASCallbackHandler</code> knows how to handle callbacks of + * type <code>javax.security.auth.callback.NameCallback</code> and + * <code>javax.security.auth.callback.PasswordCallback</code>.</p> + * * @author Craig R. McClanahan + * @author Andrew R. Jaquith * @version $Revision$ $Date$ */ public class JAASCallbackHandler implements CallbackHandler { - + private static Log log = LogFactory.getLog(JAASCallbackHandler.class); // ------------------------------------------------------------ Constructor /** * Construct a callback handler configured with the specified values. + * Note that if the <code>JAASRealm</code> instance specifies digested passwords, + * the <code>password</code> parameter will be pre-digested here. * * @param realm Our associated JAASRealm instance * @param username Username to be authenticated with @@ -55,13 +68,27 @@ super(); this.realm = realm; this.username = username; - this.password = password; + if (realm.hasMessageDigest()) { + this.password = realm.digest(password); + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasCallback.digestpassword", password, this.password)); + } + } + else { + this.password = password; + } } // ----------------------------------------------------- Instance Variables + /** + * The string manager for this package. + */ + protected static final StringManager sm = + StringManager.getManager(Constants.Package); + /** * The password to be authenticated with. @@ -85,11 +112,11 @@ /** - * Retrieve the information requested in the provided Callbacks. This - * implementation only recognizes <code>NameCallback</code> and + * Retrieve the information requested in the provided <code>Callbacks</code>. + * This implementation only recognizes <code>NameCallback</code> and * <code>PasswordCallback</code> instances. * - * @param callbacks The set of callbacks to be processed + * @param callbacks The set of <code>Callback</code>s to be processed * * @exception IOException if an input/output error occurs * @exception UnsupportedCallbackException if the login method requests @@ -101,12 +128,16 @@ for (int i = 0; i < callbacks.length; i++) { if (callbacks[i] instanceof NameCallback) { - if (realm.getDebug() >= 3) - realm.log("Returning username " + username); + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasCallback.username", username)); + } + ((NameCallback) callbacks[i]).setName(username); } else if (callbacks[i] instanceof PasswordCallback) { - if (realm.getDebug() >= 3) - realm.log("Returning password " + password); + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasCallback.password", password)); + } + final char[] passwordcontents; if (password != null) { passwordcontents = password.toCharArray(); 1.6.2.3 +171 -50 jakarta-tomcat-catalina/catalina/src/share/org/apache/catalina/realm/JAASRealm.java Index: JAASRealm.java =================================================================== RCS file: /home/cvs/jakarta-tomcat-catalina/catalina/src/share/org/apache/catalina/realm/JAASRealm.java,v retrieving revision 1.6.2.2 retrieving revision 1.6.2.3 diff -u -r1.6.2.2 -r1.6.2.3 --- JAASRealm.java 20 Sep 2004 16:11:47 -0000 1.6.2.2 +++ JAASRealm.java 21 Sep 2004 23:01:50 -0000 1.6.2.3 @@ -22,7 +22,10 @@ import java.security.acl.Group; import java.util.ArrayList; import java.util.Enumeration; +import java.util.HashMap; import java.util.Iterator; +import java.util.List; +import java.util.Map; import javax.security.auth.Subject; import javax.security.auth.login.AccountExpiredException; @@ -62,7 +65,7 @@ * this Realm:</p> * <ul> * <li>The JAAS <code>LoginModule</code> is assumed to return a - * <code>Subject with at least one <code>Principal</code> instance + * <code>Subject</code> with at least one <code>Principal</code> instance * representing the user himself or herself, and zero or more separate * <code>Principals</code> representing the security roles authorized * for this user.</li> @@ -87,10 +90,40 @@ * <li>It is a configuration error for the JAAS login method to return a * validated <code>Subject</code> without a <code>Principal</code> that * matches the "user classes" list.</li> + * <li>By default, the enclosing Container's name serves as the + * application name used to obtain the JAAS LoginContext ("Catalina" in + * a default installation). Tomcat must be able to find an application + * with this name in the JAAS configuration file. Here is a hypothetical + * JAAS configuration file entry for a database-oriented login module that uses + * a Tomcat-managed JNDI database resource: + * <blockquote><pre>Catalina { +org.foobar.auth.DatabaseLoginModule REQUIRED + JNDI_RESOURCE=jdbc/AuthDB + USER_TABLE=users + USER_ID_COLUMN=id + USER_NAME_COLUMN=name + USER_CREDENTIAL_COLUMN=password + ROLE_TABLE=roles + ROLE_NAME_COLUMN=name + PRINCIPAL_FACTORY=org.foobar.auth.impl.SimplePrincipalFactory; +};</pre></blockquote></li> + * <li>To set the JAAS configuration file + * location, set the <code>CATALINA_OPTS</code> environment variable + * similar to the following: +<blockquote><code>CATALINA_OPTS="-Djava.security.auth.login.config=$CATALINA_HOME/conf/jaas.config"</code></blockquote> + * </li> + * <li>As part of the login process, JAASRealm registers its own <code>CallbackHandler</code>, + * called (unsurprisingly) <code>JAASCallbackHandler</code>. This handler supplies the + * HTTP requests's username and credentials to the user-supplied <code>LoginModule</code></li> + * <li>As with other <code>Realm</code> implementations, digested passwords are supported if + * the <code><Realm></code> element in <code>server.xml</code> contains a + * <code>digest</code> attribute; <code>JAASCallbackHandler</code> will digest the password + * prior to passing it back to the <code>LoginModule</code></li> * </ul> * * @author Craig R. McClanahan * @author Yoav Shapira + * @author Andrew R. Jaquith * @version $Revision$ $Date$ */ @@ -104,13 +137,13 @@ /** * The application name passed to the JAAS <code>LoginContext</code>, - * which uses it to select the set of relevant <code>LoginModules</code>. + * which uses it to select the set of relevant <code>LoginModule</code>s. */ protected String appName = null; /** - * Descriptive information about this Realm implementation. + * Descriptive information about this <code>Realm</code> implementation. */ protected static final String info = "org.apache.catalina.realm.JAASRealm/1.0"; @@ -125,7 +158,7 @@ /** * The list of role class names, split out for easy processing. */ - protected ArrayList roleClasses = new ArrayList(); + protected List roleClasses = new ArrayList(); /** @@ -138,7 +171,14 @@ /** * The set of user class names, split out for easy processing. */ - protected ArrayList userClasses = new ArrayList(); + protected List userClasses = new ArrayList(); + + /** + * Map associating each user <code>Principal</code> object + * with an array of role <code>Principal</code>s. + * This Map is read when <code>hasRole</code> is called. + */ + protected Map roleMap = new HashMap(); /** * Whether to use context ClassLoader or default ClassLoader. @@ -152,15 +192,15 @@ /** - * setter for the appName member variable - * @deprecated JAAS should use the Engine ( domain ) name and webpp/host overrides + * setter for the <code>appName</code> member variable + * @deprecated JAAS should use the <code>Engine</code> (domain) name and webpp/host overrides */ public void setAppName(String name) { appName = name; } /** - * getter for the appName member variable + * getter for the <code>appName</code> member variable */ public String getAppName() { return appName; @@ -201,7 +241,7 @@ } /** - * Comma-delimited list of <code>javax.security.Principal</code> classes + * Comma-delimited list of <code>java.security.Principal</code> classes * that represent security roles. */ protected String roleClassNames = null; @@ -210,6 +250,14 @@ return (this.roleClassNames); } + /** + * Sets the list of comma-delimited classes that represent + * roles. The classes in the list must implement <code>java.security.Principal</code>. + * When this accessor is called (for example, by a <code>Digester</code> + * instance parsing the + * configuration file), it will parse the class names and store the resulting + * string(s) into the <code>ArrayList</code> field </code>roleClasses</code>. + */ public void setRoleClassNames(String roleClassNames) { this.roleClassNames = roleClassNames; roleClasses.clear(); @@ -233,7 +281,7 @@ /** - * Comma-delimited list of <code>javax.security.Principal</code> classes + * Comma-delimited list of <code>java.security.Principal</code> classes * that represent individual users. */ protected String userClassNames = null; @@ -242,6 +290,14 @@ return (this.userClassNames); } + /** + * Sets the list of comma-delimited classes that represent individual + * users. The classes in the list must implement <code>java.security.Principal</code>. + * When this accessor is called (for example, by a <code>Digester</code> + * instance parsing the + * configuration file), it will parse the class names and store the resulting + * string(s) into the <code>ArrayList</code> field </code>userClasses</code>. + */ public void setUserClassNames(String userClassNames) { this.userClassNames = userClassNames; userClasses.clear(); @@ -268,7 +324,7 @@ /** - * Return the Principal associated with the specified username and + * Return the <code>Principal</code> associated with the specified username and * credentials, if there is one; otherwise return <code>null</code>. * * If there are any errors with the JDBC connection, executing @@ -276,7 +332,7 @@ * 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 username Username of the <code>Principal</code> to look up * @param credentials Password or other credentials to use in * authenticating this username */ @@ -288,7 +344,7 @@ if( appName==null ) appName="Tomcat"; if( log.isDebugEnabled()) - log.debug("Authenticating " + appName + " " + username); + log.debug(sm.getString("jaasRealm.beginLogin", username, appName)); // What if the LoginModule is in the container class loader ? ClassLoader ocl = null; @@ -345,7 +401,7 @@ } if( log.isDebugEnabled()) - log.debug("Getting principal " + subject); + log.debug(sm.getString("jaasRealm.loginContextCreated", username)); // Return the appropriate Principal for this authenticated Subject Principal principal = createPrincipal(username, subject); @@ -364,6 +420,53 @@ } } + /** + * Returns <code>true</code> if the specified user <code>Principal</code> has the specified + * security role, within the context of this <code>Realm</code>; otherwise return + * <code>false</code>. This will be true when + * an associated role <code>Principal</code> can be found whose <code>getName</code> + * method returns a <code>String</code> equalling the specified role. + * @param principal <code>Principal</code> for whom the role is to be checked + * @param role Security role to be checked + */ + public boolean hasRole(Principal principal, String role) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.isInRole.start", principal.getName(), role)); + } + + if ((principal == null) || (role == null) || + (roleMap.get(principal) == null)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.isInRole.noPrincipalOrRole")); + } + return false; + } + + List roles = (List)roleMap.get(principal); + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.isInRole.principalCached", String.valueOf(roles.size()))); + } + + for (Iterator it = roles.iterator(); it.hasNext();) { + Principal possessedRole = (Principal)it.next(); + String possessedRoleName = possessedRole.getName(); + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.isInRole.possessesRole", possessedRole.getName())); + } + + if (possessedRoleName.equals(role)) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.isInRole.match")); + } + return true; + } + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.isInRole.noMatch")); + } + return false; + } + // -------------------------------------------------------- Package Methods @@ -372,7 +475,7 @@ /** - * Return a short name for this Realm implementation. + * Return a short name for this <code>Realm</code> implementation. */ protected String getName() { @@ -392,7 +495,7 @@ /** - * Return the Principal associated with the given user name. + * Return the <code>Principal</code> associated with the given user name. */ protected Principal getPrincipal(String username) { @@ -402,59 +505,77 @@ /** - * Construct and return a <code>java.security.Principal</code> instance - * representing the authenticated user for the specified Subject. If no - * such Principal can be constructed, return <code>null</code>. - * - * @param subject The Subject representing the logged in user + * Identify and return a <code>java.security.Principal</code> instance + * representing the authenticated user for the specified <code>Subject</code>. + * The Principal is constructed by scanning the list of Principals returned + * by the JAASLoginModule. The first <code>Principal</code> object that matches + * one of the class names supplied as a "user class" is the user Principal. + * This object is returned to tha caller. + * Any remaining principal objects returned by the LoginModules are mapped to + * roles, but only if their respective classes match one of the "role class" classes. + * If a user Principal cannot be constructed, return <code>null</code>. + * @param subject The <code>Subject</code> representing the logged-in user */ protected Principal createPrincipal(String username, Subject subject) { // Prepare to scan the Principals for this Subject String password = null; // Will not be carried forward - ArrayList roles = new ArrayList(); + + List roles = new ArrayList(); + Principal userPrincipal = null; // Scan the Principals for this Subject Iterator principals = subject.getPrincipals().iterator(); while (principals.hasNext()) { Principal principal = (Principal) principals.next(); - // No need to look further - that's our own stuff - if( principal instanceof GenericPrincipal ) { - if( log.isDebugEnabled() ) - log.debug("Found old GenericPrincipal " + principal ); - return principal; - } String principalClass = principal.getClass().getName(); - if( log.isDebugEnabled() ) - log.info("Principal: " + principalClass + " " + principal); - if (userClasses.contains(principalClass)) { - // Override the default - which is the original user, accepted by - // the friendly LoginManager - username = principal.getName(); + if( log.isDebugEnabled() ) { + log.debug(sm.getString("jaasRealm.checkPrincipal", principal, principalClass)); } + + if (userPrincipal == null && userClasses.contains(principalClass)) { + userPrincipal = principal; + if( log.isDebugEnabled() ) { + log.debug(sm.getString("jaasRealm.userPrincipalSuccess", principal.getName())); + } + } + if (roleClasses.contains(principalClass)) { roles.add(principal.getName()); } - // Same as Jboss - that's a pretty clean solution - if( (principal instanceof Group) && - "Roles".equals( principal.getName())) { - Group grp=(Group)principal; - Enumeration en=grp.members(); - while( en.hasMoreElements() ) { - Principal roleP=(Principal)en.nextElement(); - roles.add( roleP.getName()); - } + if (roleClasses.contains(principalClass)) { + roles.add(principal); + if( log.isDebugEnabled() ) { + log.debug(sm.getString("jaasRealm.rolePrincipalAdd", principal.getName())); + } } } - // Create the resulting Principal for our authenticated user - if (username != null) { - return (new GenericPrincipal(this, username, password, roles)); + // Print failure message if needed + if (userPrincipal == null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.userPrincipalFailure")); + log.debug(sm.getString("jaasRealm.rolePrincipalFailure")); + } } else { - return (null); + if (roles.size() == 0) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.rolePrincipalFailure")); + } + } else { + roleMap.put(userPrincipal, roles); + if (log.isDebugEnabled()) { + log.debug(sm.getString("jaasRealm.rolePrincipalSuccess", String.valueOf(roles.size()))); + log.debug(sm.getString("jaasRealm.cachePrincipal", userPrincipal.getName(), String.valueOf(roles.size()))); + } + } } + + // Return the resulting Principal for our authenticated user + return userPrincipal; } + /** * Ensure the given name is legal for JAAS configuration. * Added for Bugzilla 30869, made protected for easy customization @@ -487,7 +608,7 @@ /** * - * Prepare for active use of the public methods of this Component. + * Prepare for active use of the public methods of this <code>Component</code>. * * @exception LifecycleException if this component detects a fatal error * that prevents it from being started @@ -501,7 +622,7 @@ /** - * Gracefully shut down active use of the public methods of this Component. + * Gracefully shut down active use of the public methods of this <code>Component</code>. * * @exception LifecycleException if this component detects a fatal error * that needs to be reported 1.5.2.1 +27 -6 jakarta-tomcat-catalina/catalina/src/share/org/apache/catalina/realm/LocalStrings.properties Index: LocalStrings.properties =================================================================== RCS file: /home/cvs/jakarta-tomcat-catalina/catalina/src/share/org/apache/catalina/realm/LocalStrings.properties,v retrieving revision 1.5 retrieving revision 1.5.2.1 diff -u -r1.5 -r1.5.2.1 --- LocalStrings.properties 12 Dec 2003 22:37:58 -0000 1.5 +++ LocalStrings.properties 21 Sep 2004 23:01:50 -0000 1.5.2.1 @@ -4,12 +4,33 @@ # package org.apache.catalina.realm -jaasRealm.accountExpired=Username {0} NOT authenticated due to expired account -jaasRealm.authenticateSuccess=Username {0} successfully authenticated -jaasRealm.credentialExpired=Username {0} NOT authenticated due to expired credential -jaasRealm.failedLogin=Username {0} NOT authenticated due to failed login -jaasRealm.loginException=Login exception authenticating username {0} +jaasRealm.beginLogin=JAASRealm login requested for username "{0}" using LoginContext for application "{1}" +jaasRealm.accountExpired=Username "{0}" NOT authenticated due to expired account +jaasRealm.authenticateFailure=Username "{0}" NOT successfully authenticated +jaasRealm.authenticateSuccess=Username "{0}" successfully authenticated as Principal "{1}" -- Subject was created too +jaasRealm.credentialExpired=Username "{0}" NOT authenticated due to expired credential +jaasRealm.failedLogin=Username "{0}" NOT authenticated due to failed login +jaasRealm.loginException=Login exception authenticating username "{0}" jaasRealm.unexpectedError=Unexpected error +jaasRealm.loginContextCreated=JAAS LoginContext created for username "{0}" +jaasRealm.userPrincipalSuccess=Subject for username "{0}" returned user Principal "{1}" +jaasRealm.userPrincipalFailure=Subject for username "{0}" did not return a valid user Principal +jaasRealm.cachePrincipal=User Principal "{0}" cached; has {1} role Principal(s) +jaasRealm.checkPrincipal=Checking Principal "{0}" [{1}] +jaasRealm.userPrincipalSuccess=Principal "{0}" is a valid user class. We will use this as the user Principal. +jaasRealm.userPrincipalFailure=No valid user Principal found +jaasRealm.rolePrincipalAdd=Adding role Principal "{0}" to this user Principal\'s roles +jaasRealm.rolePrincipalSuccess={0} role Principal(s) found +jaasRealm.rolePrincipalFailure=No valid role Principals found. +jaasRealm.isInRole.start=Checking if user Principal "{0}" possesses role "{1}" +jaasRealm.isInRole.noPrincipalOrRole=No roles Principals found. User Principal or Subject is null, or user Principal not in cache +jaasRealm.isInRole.principalCached=User Principal has {0} role Principal(s) +jaasRealm.isInRole.possessesRole=User Principal has a role Principal called "{0}" +jaasRealm.isInRole.match=Matching role Principal found. +jaasRealm.isInRole.noMatch=Matching role Principal NOT found. +jaasCallback.digestpassword=Digested password "{0}" as "{1}" +jaasCallback.username=Returned username "{0}" +jaasCallback.password=Returned password "{0}" jdbcRealm.authenticateFailure=Username {0} NOT successfully authenticated jdbcRealm.authenticateSuccess=Username {0} successfully authenticated jdbcRealm.close=Exception closing database connection No revision No revision 1.70.2.40 +3 -0 jakarta-tomcat-catalina/webapps/docs/changelog.xml Index: changelog.xml =================================================================== RCS file: /home/cvs/jakarta-tomcat-catalina/webapps/docs/changelog.xml,v retrieving revision 1.70.2.39 retrieving revision 1.70.2.40 diff -u -r1.70.2.39 -r1.70.2.40 --- changelog.xml 21 Sep 2004 19:45:08 -0000 1.70.2.39 +++ changelog.xml 21 Sep 2004 23:01:50 -0000 1.70.2.40 @@ -79,6 +79,9 @@ <fix> <bug>31277</bug>: Clarified automatic application deployment section of Host configuration page. (yoavs) </fix> + <fix> + <bug>28631</bug>: JAASRealm enhancements to support custom user, role class names, use Commons-Logging. (yoavs) + </fix> </changelog> </subsection> <subsection name="Webapps">
--------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]