You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by cr...@apache.org on 2001/04/13 23:18:43 UTC
cvs commit: jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/realm JNDIRealm.java LocalStrings.properties
craigmcc 01/04/13 14:18:43
Modified: catalina/src/share/org/apache/catalina/realm
LocalStrings.properties
Added: catalina/src/share/org/apache/catalina/realm JNDIRealm.java
Log:
Initial version of a Realm implementation for Tomcat 4.0 that utilizes a
directory server (accessed via JNDI) to perform user authentication and
access control for container-managed security. This code is based in
large part on the proposed code (on TOMCAT-DEV) by John Holman, with some
additions and refactoring by me.
Use of this realm would be configured in "conf/server.xml" by an entry
like this to connect to an LDAP server on the same host that Tomcat is
running on:
<Realm className="org.apache.catalina.realm.JNDIRealm"
connectionName="admin-username"
connectionPassword="admin-password"
connectionURL="ldap://localhost:389"
userPattern="cn={0},dc=mycompany,dc=com"
userPassword="userPassword"
roleBase="dc=groups,dc=mycompany,dc=com"
roleName="cn"
roleSearch="(|(uniqueMember={0})(member={0}))"
roleSubtree="false"
/>
TODO: Update the configuration documentation to describe the above.
TODO: Support an operational mode where the Realm attempts to bind to the
directory server using the user's username and password (instead of a
system administrator username and password). This is a different enough
style that it probably should be a separate implementation class.
TODO: Support connection pooling (for both this and JDBCRealm) so that
the authenticate() method does not have to be synchronized.
Revision Changes Path
1.4 +6 -1 jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/realm/LocalStrings.properties
Index: LocalStrings.properties
===================================================================
RCS file: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/realm/LocalStrings.properties,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -r1.3 -r1.4
--- LocalStrings.properties 2001/04/11 01:46:09 1.3
+++ LocalStrings.properties 2001/04/13 21:18:42 1.4
@@ -1,4 +1,4 @@
-# $Id: LocalStrings.properties,v 1.3 2001/04/11 01:46:09 craigmcc Exp $
+# $Id: LocalStrings.properties,v 1.4 2001/04/13 21:18:42 craigmcc Exp $
# language
@@ -9,6 +9,11 @@
jdbcRealm.close=Exception closing database connection
jdbcRealm.exception=Exception performing authentication
jdbcRealm.open=Exception opening database connection
+jndiRealm.authenticateFailure=Username {0} NOT successfully authenticated
+jndiRealm.authenticateSuccess=Username {0} successfully authenticated
+jndiRealm.close=Exception closing directory server connection
+jndiRealm.exception=Exception performing authentication
+jndiRealm.open=Exception opening directory server connection
memoryRealm.authenticateFailure=Username {0} NOT successfully authenticated
memoryRealm.authenticateSuccess=Username {0} successfully authenticated
memoryRealm.loadExist=Memory database file {0} cannot be read
1.1 jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/realm/JNDIRealm.java
Index: JNDIRealm.java
===================================================================
/*
* The Apache Software License, Version 1.1
*
* Copyright (c) 1999 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
* <http://www.apache.org/>.
*
* [Additional notices, if required by prior licensing conditions]
*
*/
package org.apache.catalina.realm;
import java.security.Principal;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Realm;
import org.apache.catalina.util.StringManager;
/**
* <p>Implementation of <strong>Realm</strong> 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:</p>
* <ul>
* <li>Each user that can be authenticated is represented by an individual
* element in the top level <code>DirContext</code> that is accessed
* via the <code>connectionURL</code> property. This element has the
* following characteristics:
* <ul>
* <li>The distinguished name (<code>dn</code>) attribute of this element
* contains the username that is being presented for authentication.
* </li>
* <li>The distinguished name can be represented by a pattern passed to
* an instance of <code>MessageFormat</code>, where the string "{0}"
* in the pattern is replaced by the username being presented.</li>
* <li>The element for this user contains an attribute named by the
* <code>userPassword</code> property. The value of this attribute
* is retrieved for use in authentication.</li>
* <li>The value of the user password attribute is either a cleartext
* String, or the result of passing a cleartext String through the
* <code>RealmBase.digest()</code> method (using the standard digest
* support included in <code>RealmBase</code>).
* <li>The user is considered to be authenticated if the presented
* credentials (after being passed through
* <code>RealmBase.digest()</code>) are equal to the retrieved value
* for the user password attribute.</li>
* </ul></li>
* <li>Each group of users that has been assigned a particular role is
* represented by an individual element in the top level
* <code>DirContext</code> that is accessed via the
* <code>connectionURL</code> property. This element has the following
* characteristics:
* <ul>
* <li>The set of all possible groups of interest can be selected by a
* search pattern configured by the <code>roleSearch</code>
* property.</li>
* <li>The <code>roleSearch</code> 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.</li>
* <li>The <code>roleBase</code> 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.</li>
* <li>The <code>roleSubtree</code> property can be set to
* <code>true</code> if you wish to search the entire subtree of the
* directory context. The default value of <code>false</code>
* requests a search of only the current level.</li>
* <li>The element includes an attribute (whose name is configured by
* the <code>roleName</code> property) containing the name of the
* role represented by this element.</li>
* </ul></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
* directory server itself.</li>
* </ul>
*
* <p><strong>TODO</strong> - Support connection pooling (including message
* format objects) so that <code>authenticate()</code> does not have to be
* synchronized.</p>
*
* @author John Holman
* @author Craig R. McClanahan
* @version $Revision: 1.1 $ $Date: 2001/04/13 21:18:42 $
*/
public class JNDIRealm extends RealmBase {
// ----------------------------------------------------- Instance Variables
/**
* The connection username for the server we will contact.
*/
protected String connectionName = null;
/**
* The connection password for the server we will contact.
*/
protected String connectionPassword = null;
/**
* The connection URL for the server we will contact.
*/
protected String connectionURL = null;
/**
* The directory context linking us to our directory server.
*/
protected DirContext context = null;
/**
* The JNDI context factory used to acquire our InitialContext. By
* default, assumes use of an LDAP server using the standard JNDI LDAP
* provider.
*/
protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
/**
* Descriptive information about this Realm implementation.
*/
protected static final String info =
"org.apache.catalina.realm.JNDIRealm/1.0";
/**
* Descriptive information about this Realm implementation.
*/
protected static final String name = "JNDIRealm";
/**
* The base element for role searches.
*/
protected String roleBase = "";
/**
* The MessageFormat object associated with the current
* <code>roleSearch</code>.
*/
protected MessageFormat roleFormat = null;
/**
* The name of the attribute containing the role name.
*/
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;
/**
* The MessageFormat object associated with the current
* <code>userPattern</code>.
*/
protected MessageFormat userFormat = null;
/**
* The attribute name used to retrieve the user password.
*/
protected String userPassword[] = null;
/**
* The message format used to select a user, with "{0}" marking the
* spot where the specified username goes.
*/
protected String userPattern = null;
// ------------------------------------------------------------- Properties
/**
* 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 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() {
if (this.roleName != null)
return (this.roleName[0]);
else
return (null);
}
/**
* Set the role name attribute name for this Realm.
*
* @param roleName The new role name attribute name
*/
public void setRoleName(String roleName) {
if (roleName != null)
this.roleName = new String[] { roleName };
else
this.roleName = null;
}
/**
* 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() {
if (this.userPassword != null)
return (this.userPassword[0]);
else
return (null);
}
/**
* Set the password attribute used to retrieve the user password.
*
* @param userPassword The new password attribute
*/
public void setUserPassword(String userPassword) {
if (userPassword != null)
this.userPassword = new String[] { userPassword };
else
this.userPassword = null;
}
/**
* 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)
userFormat = null;
else
userFormat = new MessageFormat(userPattern);
}
// ---------------------------------------------------------- Realm Methods
/**
* Return the Principal 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
* 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;
try {
// Ensure that we have a directory context available
context = open();
// Authenticate the specified username if possible
Principal 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 <code>null</code>.
*
* @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 {
// Authenticate the specified username if possible
String dn = getUserDN(context, username, credentials);
if (dn == null)
return (null);
// Look up the associated roles
List roles = getRoles(context, username, dn);
// Create and return a suitable Principal for this user
return (new GenericPrincipal(this, username, credentials, roles));
}
/**
* Close any open connection to the directory server for this Realm.
*
* @param context The directory context to be closed
*/
protected void close(DirContext context) {
// Do nothing if there is no opened connection
if (context == null)
return;
// Close our opened connection
try {
if (debug >= 1)
log("Closing directory context");
context.close();
} catch (NamingException e) {
log(sm.getString("jndiRealm.close"), e);
}
this.context = null;
}
/**
* Return a short name for this Realm implementation.
*/
protected String getName() {
return (this.name);
}
/**
* Return the password associated with the given principal's user name.
*/
protected String getPassword(String username) {
return (null);
}
/**
* Return the Principal associated with the given user name.
*/
protected Principal getPrincipal(String username) {
return (null);
}
/**
* Return a List of roles associated with the user with the specified
* distinguished name. If no roles are associated with this user, a
* zero-length List is returned.
*
* @param context The directory context we are searching
* @param username The username of the user to be checked
* @param dn Distinguished name of the user to be checked
*
* @exception NamingException if a directory server error occurs
*/
protected List getRoles(DirContext context,
String username, String dn)
throws NamingException {
if (debug >= 2)
log("getRoles(" + dn + ")");
// Are we configured to do role searches?
ArrayList list = new ArrayList();
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(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;
Attribute attr = attrs.get(roleName[0]);
if (attr != null) {
String role = (String) attr.get();
if (debug >= 3)
log(" Found role '" + role + "'");
list.add(role);
}
}
// Return the completed list of roles
if (debug >= 2)
log(" Returning " + list.size() + " roles");
return (list);
}
/**
* Return the distinguished name of an authenticated user (if successful)
* or <code>null</code> if authentication is unsuccessful.
*
* @param context The directory context we are accessing
* @param username Username to be authenticated
* @param credentials Authentication credentials
*
* @exception NamingException if a directory server error occurs
*/
protected String getUserDN(DirContext context,
String username, String credentials)
throws NamingException {
if (debug >= 2)
log("getUserDN(" + username + ")");
if (username == null)
return (null);
if ((userFormat == null) || (userPassword == null))
return (null);
// Retrieve the user password attribute for this user
String dn = userFormat.format(new String[] { username });
if (debug >= 3)
log(" dn=" + dn);
Attributes attrs = null;
try {
attrs = context.getAttributes(dn, userPassword);
} catch (NameNotFoundException e) {
return (null);
}
if (attrs == null)
return (null);
if (debug >= 3)
log(" retrieving attribute " + userPassword[0]);
Attribute attr = attrs.get(userPassword[0]);
if (attr == null)
return (null);
if (debug >= 3)
log(" retrieving value");
Object value = attr.get();
if (value == null)
return (null);
String valueString = null;
if (value instanceof byte[])
valueString = new String((byte[]) value);
else
valueString = value.toString();
// Validate the credentials specified by the user
if (debug >= 3)
log(" validating credentials");
if (digest(credentials).equals(valueString)) {
if (debug >= 2)
log(sm.getString("jndiRealm.authenticateSuccess",
username));
} else {
if (debug >= 2)
log(sm.getString("jndiRealm.authenticateFailure",
username));
return (null);
}
return (dn);
}
/**
* Open (if necessary) and return a connection to the configured
* directory server for this Realm.
*
* @exception NamingException if a directory server error occurs
*/
protected DirContext open() throws NamingException {
// Do nothing if there is a directory server connection already open
if (context != null)
return (context);
// Establish a connection and retrieve the initial context
if (debug >= 1)
log("Connecting to URL " + connectionURL);
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);
if (connectionName != null)
env.put(Context.SECURITY_PRINCIPAL, connectionName);
if (connectionPassword != null)
env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
if (connectionURL != null)
env.put(Context.PROVIDER_URL, connectionURL);
context = new InitialDirContext(env);
return (context);
}
/**
* Release our use of this connection so that it can be recycled.
*
* @param context The directory context to release
*/
protected void release(DirContext context) {
; // NO-OP since we are not pooling anything
}
// ------------------------------------------------------ Lifecycle Methods
/**
* Prepare for active use of the public methods of this Component.
*
* @exception IllegalStateException if this component has already been
* started
* @exception LifecycleException if this component detects a fatal error
* that prevents it from being started
*/
public void start() throws LifecycleException {
// Validate that we can open our connection
try {
open();
} catch (NamingException e) {
throw new LifecycleException(sm.getString("jndiRealm.open"), e);
}
// Perform normal superclass initialization
super.start();
}
/**
* Gracefully shut down active use of the public methods of this Component.
*
* @exception IllegalStateException if this component has not been
* started
* @exception LifecycleException if this component detects a fatal error
* that needs to be reported
*/
public void stop() throws LifecycleException {
// Perform normal superclass finalization
super.stop();
// Close any open directory server connection
close(this.context);
}
}