yoavs 2004/09/21 16:29:33 Modified: catalina/src/share/org/apache/catalina/realm JAASCallbackHandler.java JAASRealm.java LocalStrings.properties webapps/docs changelog.xml Log: Bugzilla 28631: JAASRealm enhancements to allow custom user and group principal classes (and use commons-logging) Revision Changes Path 1.5 +35 -14 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.4 retrieving revision 1.5 diff -u -r1.4 -r1.5 --- JAASCallbackHandler.java 23 Jun 2004 13:51:37 -0000 1.4 +++ JAASCallbackHandler.java 21 Sep 2004 23:29:33 -0000 1.5 @@ -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,26 @@ 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 +111,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 @@ -102,11 +128,11 @@ if (callbacks[i] instanceof NameCallback) { if (realm.getContainer().getLogger().isTraceEnabled()) - realm.getContainer().getLogger().trace("Returning username " + username); + realm.getContainer().getLogger().trace(sm.getString("jaasCallback.username", username)); ((NameCallback) callbacks[i]).setName(username); } else if (callbacks[i] instanceof PasswordCallback) { if (realm.getContainer().getLogger().isTraceEnabled()) - realm.getContainer().getLogger().trace("Returning password " + password); + realm.getContainer().getLogger().trace(sm.getString("jaasCallback.password", password)); final char[] passwordcontents; if (password != null) { passwordcontents = password.toCharArray(); @@ -118,11 +144,6 @@ } else { throw new UnsupportedCallbackException(callbacks[i]); } - - } - } - - } 1.9 +177 -58 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.8 retrieving revision 1.9 diff -u -r1.8 -r1.9 --- JAASRealm.java 20 Sep 2004 15:57:55 -0000 1.8 +++ JAASRealm.java 21 Sep 2004 23:29:33 -0000 1.9 @@ -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.Map; +import java.util.List; 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,39 @@ * <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> - * </ul> - * - * @author Craig R. McClanahan - * @author Yoav Shapira + * <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 * @version $Revision$ $Date$ */ @@ -104,20 +136,20 @@ /** * 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"; /** - * Descriptive information about this Realm implementation. + * Descriptive information about this <code>Realm</code> implementation. */ protected static final String name = "JAASRealm"; @@ -125,7 +157,7 @@ /** * The list of role class names, split out for easy processing. */ - protected ArrayList roleClasses = new ArrayList(); + protected List roleClasses = new ArrayList(); /** @@ -138,7 +170,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 +191,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 +240,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,8 +249,16 @@ return (this.roleClassNames); } - public void setRoleClassNames(String roleClassNames) { - this.roleClassNames = 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(); String temp = this.roleClassNames; if (temp == null) { @@ -233,7 +280,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 +289,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 +323,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 +331,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 +343,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 +400,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); @@ -363,7 +418,53 @@ return null; } } - + + /** + * 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 +473,7 @@ /** - * Return a short name for this Realm implementation. + * Return a short name for this <code>Realm</code> implementation. */ protected String getName() { @@ -392,7 +493,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,58 +503,76 @@ /** - * 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; } /** @@ -488,7 +607,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 @@ -502,7 +621,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.7 +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.6 retrieving revision 1.7 diff -u -r1.6 -r1.7 --- LocalStrings.properties 3 Jul 2004 04:16:41 -0000 1.6 +++ LocalStrings.properties 21 Sep 2004 23:29:33 -0000 1.7 @@ -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 1.114 +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.113 retrieving revision 1.114 diff -u -r1.113 -r1.114 --- changelog.xml 21 Sep 2004 19:36:11 -0000 1.113 +++ changelog.xml 21 Sep 2004 23:29:33 -0000 1.114 @@ -48,6 +48,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>
--------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]