Menu

DataSourceRealm - Anyone?

Help
2005-12-19
2013-04-15
  • Christopher Schultz

    Has anyone successfully used CatalinaRealmAdapter with a DataSourceRealm?

    [ I posted this same question in October and got no responses - http://sourceforge.net/forum/forum.php?thread_id=1374325&forum_id=200425. Someone else posted in September with no useful followups - http://sourceforge.net/forum/forum.php?thread_id=1339224&forum_id=200425. Someone must know what's going on!? ]

    I searched the forums and google and can't find any guidance. My configuration looks like this:

    <realm className=&quot;org.securityfilter.realm.catalina.CatalinaRealmAdapter&quot; />

    <realm className=&quot;org.apache.catalina.realm.DataSourceRealm&quot;&gt;
    &lt;realm-param name=&quot;dataSourceName&quot; value=&quot;jdbc/datasource&quot; /&gt;
    &lt;realm-param name=&quot;userTable&quot; value=&quot;user&quot; /&gt;
    &lt;realm-param name=&quot;userRoleTable&quot; value=&quot;user_role&quot; /&gt;
    &lt;realm-param name=&quot;userNameCol&quot; value=&quot;username&quot; /&gt;
    &lt;realm-param name=&quot;userCredCol&quot; value=&quot;password_md5&quot; /&gt;
    &lt;realm-param name=&quot;roleNameCol&quot; value=&quot;rolename&quot; /&gt;
    &lt;realm-param name=&quot;digest&quot; value=&quot;MD5&quot; /&gt;
    &lt;realm-param name=&quot;debug&quot; value=&quot;99&quot; /&gt;
    &lt;/realm&gt;

    Before trying securityfilter, I was using this exact DataSourceRealm configuration successfully with Tomcat's built-in container-based auth.

    I have a &lt;Resource&gt; defined in my &lt;GlobalNamingResources&gt; in server.xml and a &lt;Realm&gt; (DataSourceRealm) defined in my &lt;Context&gt; which uses the aforementioned data source. This will correctly authenticate my users.

    To switch to securityfilter, I made no changes to server.xml, moved my security config from web.xml to securityfilter-config.xml, and configured my &lt;realm&gt;s in securityfilter-config.xml (I think) appropriately.

    When DataSourceRealm didn't work, I switched to JDBCRealm for securityfilter, which works. This leads me to believe that I have a problem only with my DataSourceRealm configuration vis-a-vis securityfilter.

    Could anyone who has this working give me some advice? I really would love to get securityfilter working.

    I get this exception when authentication is attempted:
          
          DataSourceRealm[null]: Exception performing authentication
          java.lang.NullPointerException
          at org.apache.catalina.realm.DataSourceRealm.open(DataSourceRealm.java:420)
          at org.apache.catalina.realm.DataSourceRealm.authenticate(DataSourceRealm.java:269)
          at org.securityfilter.realm.catalina.CatalinaRealmAdapter.authenticate(CatalinaRealmAdapter.java:95)
          at org.securityfilter.authenticator.FormAuthenticator.processLogin(FormAuthenticator.java:178)
          at org.securityfilter.filter.SecurityFilter.doFilter(SecurityFilter.java:138)

    Thanks,
    -chris

     
    • Christopher Schultz

      I've investigated this problem and I believe I understand the problem and have a solution.

      At first, I thought the NPE from the parent message referred to the JNDI DataSource being null. Instead, the StandardServer object is null, instead.

      StandardServer looks like it's being used in a singleton-like way, except that it doesn't automatically create itself when you call the static getServer() method.

      Here is what I have found:

      The problem is that, in order to use the Tomcat (i.e. catalina) realms in your own application, you have to copy $TOMCAT_HOME/server/lib/catalina.jar into your webapp's own lib directory. Otherwise, you don't have access to these internal classes; Tomcat shields your application from all of that stuff through it's use of classloaders in order to enforce separation between the webapps and the container.

      By adding catalina.jar to your webapp, you are  making those classes available for your use within the webapp. Unfortunately, due to class loading semantics, the DataSourceRealm class used within our application is not the same as the DataSourceRealm class available to Tomcat (they have been loaded by different ClassLoaders, so they are distinct classes). Tomcat's DataSourceRealm does not use standard JNDI access methods... they use their own internal API which uses a singleton (or something that looks like one) to gain access to the "StandardServer" object (see DataSourceRealm.java:419 in Tomcat 4.1.31).

      It makes sense that Tomcat code would use their own internal objects to access this data because realms actually use the global naming stuff instead of those JNDI datasources that are available to the application (for example, it is possible to use a DataSourceRealm for a webapp that does not have access to any JNDI datasources).

      The call to StandardService.getServer() returns NULL because the server has not been initialized; the initialized server is actually hidden by the ClassLoader from your own application.  :(

      So, that's why it happens. This is what I did to fix it: I wrote my own implementation of the org.securityfilter.realm.SecurityRealmInterface interface which is based upon the code from  Tomcat's DataSourceRealm and some other things.

      Below, you can find the source to my own implementation of DataSourceRealm as well as SimplePrincipal.

      I hope the helps those who want to use JNDI DataSources with securityfilter.

      -chris

      ===== DataSourceRealm.java ====
      import java.security.NoSuchAlgorithmException;
      import java.security.Principal;
      import java.sql.Connection;
      import java.sql.PreparedStatement;
      import java.sql.ResultSet;
      import java.sql.SQLException;
      import java.util.ArrayList;

      import javax.naming.InitialContext;
      import javax.naming.Context;
      import javax.naming.NamingException;
      import javax.sql.DataSource;

      import org.apache.log4j.Logger;

      import org.securityfilter.realm.SecurityRealmInterface;

      import org.childhealthcare.tools.security.Digester;

      /**
      * A JNDI DataSource realm for use with SecurityFilter.
      *
      * @author Chris Schultz
      */
      public class DataSourceRealm
          implements SecurityRealmInterface
      {
          /**
           * The logger for this class.
           */
          private static final Logger logger = Logger.getLogger(DataSourceRealm.class);

          /**
           * Authenticate a user.
           *
           * @param username The username of the user to authenticate.
           * @param credentials The plaintext password, as entered by the user.
           *
           * @return A Principal object representing the user if successful,
           *         null otherwise.
           */
          public Principal authenticate(String username, String credentials)
          {
              Connection conn = null;

              try
          {
                  // Ensure that we have an open database connection
                  conn = getConnection();

                  if (null == conn)
              {
              logger.error("Could not open JDBC connection");

                      // If the db connection open fails, return "not authenticated"
                      return null;
                  }

                  // Acquire a Principal object for this user
              return authenticate(conn,
                      username,
                      credentials);
              }
          catch (NamingException ne)
          {
              logger.error("Cannot authenticate", ne);

                  return null;
          }
          catch (SQLException sqle)
          {
              logger.error("Cannot authenticate", sqle);

                  return null;
              }
          catch (NoSuchAlgorithmException nsae)
          {
              logger.error("Nad hash algorithm '" + digest + "'", nsae);

                  return null;
          }
          finally
          {
                  if (conn != null)
              try { conn.close(); } catch(SQLException sqle)
              { logger.error("Cannot close Connection", sqle); }
          }
          }

          /**
           * Return the Principal associated with the specified username and
           * credentials, if there is one; otherwise return <code>null</code>.
           *
           * @param dbConnection The database connection to be used
           * @param username Username of the Principal to look up
           * @param credentials Password or other credentials to use in
           *  authenticating this username
           *
           * @exception SQLException if a database error occurs
           */
          private Principal authenticate(Connection conn,
                         String username,
                         String credentials)
              throws SQLException, NoSuchAlgorithmException
          {
              PreparedStatement ps = null;
              ResultSet rs = null;

          // Perform a message digest if necessary
          if(null != this.digest)
          {
              if(logger.isDebugEnabled())
              logger.debug("Digesting credentials using " + this.digest);

              credentials = Digester.digest(credentials, this.digest);
          }

              try
          {
              // First, get the user record.
              ps = conn.prepareStatement(getUserSelectStatement());
              ps.setString(1, username);
              ps.setString(2, credentials);

              rs = ps.executeQuery();

              if(!rs.next())
              return null; // No such user

              rs.close(); rs = null;
              ps.close(); ps = null;

              // Now, get the user's roles
              ArrayList roles = new ArrayList();
              ps = conn.prepareStatement(getRoleSelect());
              ps.setString(1, username);

              rs = ps.executeQuery();

              while(rs.next())
              roles.add(rs.getString("rolename"));

              return new SimplePrincipal(username, roles);
          }
          finally
          {
              if(null != rs)
              try { rs.close(); } catch (SQLException sqle)
              { logger.error("Could not close ResultSet", sqle); }

              if(null != ps)
              try { ps.close(); } catch (SQLException sqle)
              { logger.error("Could not close PreparedStatement", sqle); }
          }
          }

          /**
           * Return <code>true</code> if the specified Principal has the specified
           * security role, within the context of this Realm; otherwise return
           * <code>false</code>.  This method can be overridden by Realm
           * implementations, but the default is adequate when an instance of
           * <code>GenericPrincipal</code> is used to represent authenticated
           * Principals from this Realm.
           *
           * @param principal Principal for whom the role is to be checked
           * @param role Security role to be checked
           */
          /**
           * Test for role membership.
           *
           * Use Principal.getName() to get the username from the principal object.
           *
           * @param principal Principal object representing a user
           * @param rolename name of a role to test for membership
           *
           * @return true if the user is in the role, false otherwise
           */
          public boolean isUserInRole(Principal principal, String rolename)
          {
              if ((principal == null) || (rolename == null) ||
                  !(principal instanceof SimplePrincipal))
                  return (false);

              SimplePrincipal p = (SimplePrincipal)principal;

          return p.isInRole(rolename);
          }

          /**
           * The name of the JNDI JDBC DataSource
           */
          protected String dataSourceName;

          /**
           * The table that holds user data.
           */
          protected String userTable;

          /**
           * The column in the user table that holds the user's name
           */
          protected String userNameCol;

          /**
           * The column in the user table that holds the user's credentials
           * (i.e. password).
           */
          protected String userCredCol;

          /**
           * The table that holds the relation between user's and roles
           */
          protected String userRoleTable;

          /**
           * The column in the user role table that names a role
           */
          protected String roleNameCol;

          private String digest;

          /**
           * Return the name of the JNDI JDBC DataSource.
           *
           */
          public String getDataSourceName() {
              return dataSourceName;
          }

          /**
           * Set the name of the JNDI JDBC DataSource.
           *
           * @param dataSourceName the name of the JNDI JDBC DataSource
           */
          public void setDataSourceName( String dataSourceName) {
          this.dataSourceName = dataSourceName;
          }

          /**
           * Return the column in the user role table that names a role.
           *
           */
          public String getRoleNameCol() {
              return roleNameCol;
          }

          /**
           * Set the column in the user role table that names a role.
           *
           * @param roleNameCol The column name
           */
          public void setRoleNameCol( String roleNameCol ) {
              this.roleNameCol = roleNameCol;
          }

          /**
           * Return the column in the user table that holds the user's credentials.
           *
           */
          public String getUserCredCol() {
              return userCredCol;
          }

          /**
           * Set the column in the user table that holds the user's credentials.
           *
           * @param userCredCol The column name
           */
          public void setUserCredCol( String userCredCol ) {
          this.userCredCol = userCredCol;
          }

          /**
           * Return the column in the user table that holds the user's name.
           *
           */
          public String getUserNameCol() {
              return userNameCol;
          }

          /**
           * Set the column in the user table that holds the user's name.
           *
           * @param userNameCol The column name
           */
          public void setUserNameCol( String userNameCol ) {
          this.userNameCol = userNameCol;
          }

          /**
           * Return the table that holds the relation between user's and roles.
           *
           */
          public String getUserRoleTable() {
              return userRoleTable;
          }

          /**
           * Set the table that holds the relation between user's and roles.
           *
           * @param userRoleTable The table name
           */
          public void setUserRoleTable( String userRoleTable ) {
              this.userRoleTable = userRoleTable;
          }

          /**
           * Return the table that holds user data..
           *
           */
          public String getUserTable() {
              return userTable;
          }

          /**
           * Set the table that holds user data.
           *
           * @param userTable The table name
           */
          public void setUserTable( String userTable ) {
          this.userTable = userTable;
          }

          public void setDigest(String digest)
          {
          this.digest = digest;
          }

          public String getDigest()
          {
          return this.digest;
          }

          /**
           * Open the specified database connection.
           *
           * @return Connection to the database
           */
          private Connection getConnection()
          throws NamingException, SQLException
          {
          Context context = new InitialContext();

          DataSource ds = (DataSource)context.lookup(dataSourceName);

          if(null == ds)
              throw new NamingException("Found no DataSource for '"
                            + dataSourceName + "'");

          return ds.getConnection();
          }

          private String _userSelect;
          private String _roleSelect;
          private synchronized String getUserSelectStatement()
          {
          if(null == _userSelect)
              _userSelect = "SELECT " + userNameCol
              + " FROM " + userTable
              + " WHERE "
              + userNameCol + "=? AND "
              + userCredCol + "=?";

          return _userSelect;
          }

          private synchronized String getRoleSelect()
          {
          if(null == _roleSelect)
              _roleSelect = "SELECT " + roleNameCol
              + " FROM " + userRoleTable
              + " WHERE "
              + userNameCol + "=?";

          return _roleSelect;
          }
      }

      ==== SimplePrincipal.java ====
      package org.childhealthcare.resource.util;

      import java.io.Serializable;
      import java.security.Principal;
      import java.util.Collection;
      import java.util.Collections;

      /**
      * Principal - a simple, serializable Principal.
      *
      * @author Chris Schultz
      * @version $Revision: 1.4 $ $Date: 2003/01/06 00:17:25 $
      */
      public class SimplePrincipal
          implements Principal, Serializable
      {
          private String _name;
          private Collection _roles;

          SimplePrincipal(String name, Collection roles) {
          _name = name;

          _roles = roles;
          }

          /**
           * Returns the name of this principal.
           *
           * @return the name of this principal.
           */
          public String getName() {
          return _name;
          }

          public boolean isInRole(String rolename)
          {
          return (_roles != null && _roles.contains(rolename));
          }

          /**
           * Compares this principal to the specified object.
           *
           * @param obj object to compare with.
           *
           * @return true if the object passed in is a SimplePrincipal with the same name.
           */
          public boolean equals(Object o)
          {
          return (o instanceof Principal)
              && _name.equals(((Principal)o).getName());
          }

          /**
           * Returns a string representation of this principal.
           *
           * @return a string representation of this principal.
           */
          public String toString() {
          return "Principal[name = \'" + _name + "\']";
          }

          /**
           * Returns a hashcode for this principal.
           *
           * @return a hashcode for this principal.
           */
          public int hashCode() {
          return _name.hashCode();
          }
      }

       
      • Torgeir Veimo

        Torgeir Veimo - 2006-02-15

        Did you try putting securityfilter.jar and your realm implementation into server/lib instead of in your WEB-INF/lib directory?

         
      • Ilja S.

        Ilja S. - 2006-06-13

        Hi
        Just looking at your solution with custom DataSource Realm.
        I have created 2 classes from source you provided here. Packed them in one jar. Then I put it in WEB-INF/lib along with securityfilter.jar. In securityfilter config point Realm to your implementation.
        But this seems not working for me. I have tried to put it in server/lib, common/lib etc, but in most cases geting ClassCastException during server startup or classnotfoundexception (DataSourceRealm)
        Any ideas?
        Thx in advance..

         
        • Christopher Schultz

          Make sure that your package declarations match the directory structure within your JAR file.

          The files I posted have mismatching package declarations (I forgot to delete it from the second one).

          Hope that helps,
          -chris

           
          • Ilja S.

            Ilja S. - 2006-06-15

            Hi
            Thx for reply, finally (spent 1 day fightnig with this) found a reason. Your custom datasource is really working. Problems were: 1. since your datasource implements securityfilter's interface, no need for catalina adapter in config file. 2. I use Jbuilder and for some mysterious problem, when I ran a project, package with datasource and principal was not copied to web-inf dir (this cost me half a day).

            But now everething fine, Thank you very much for this implementation!
            P.S. You use some custom Digester class which source code you did not provided :) But that is not really big problem.
            Btw why you did not used digest methods from original Catalina's RealmBase class? I mean its' source code is available, you could adopt it...

             

Log in to post a comment.

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.