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 [EMAIL PROTECTED]
   *
   * 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>&lt;security-role-ref&gt;</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);
  
      }
  
  
  
  }
  
  
  

Reply via email to