Writing a custom Tomcat login module (Realm)

In the project I am working on we needed to move the users from LDAP to a database table due to a switch from JBoss to Tomcat as the application server. It was a little complicated since the passwords was hashed with SHA1 in LDAP and we needed the cleartext passwords since we needed to encrypt it in another way. To avoid having users switch their password I wrote a custom login module, called a Realm, in Tomcat. It was really simple.

When logging in the login module will do the following:

  1. Check whether the username is registered in the database. If it is the user is authenticated against the database.
  2. If no username/password is found in the database we go to the LDAP server and try to authenticate the user there.
  3. If the user was authenticated using LDAP we update the database with the username and password supplied before returning a Principal.
  4. If we still haven’t authenticated we return null to indicate that we could not authenticate the user.

Simple and effective.

Writing a custom Realm

A realm is defined by the org.apache.catalina.Realm interface. I opted to extend the org.apache.catalina.realm.RealmBase class instead of implementing the interface since it meant I only had to override one method:

public class LdapDbRealm extends RealmBase {

   // accessors for realm configuration (database) *******************
   public String getDbUsername() {
      return this.pDbUsername;
   }
   public void setDbUsername(String dbUsername) {
      this.pDbUsername = dbUsername;
   }
   ...
   ...

    // accessors for realm configuration (ldap) *******************
    public String getLdapPassword() {
      return this.pLdapPassword;
   }
   public void setLdapPassword(String ldapPassword) {
      this.pLdapPassword = ldapPassword;
   }
   ...
   ...

    // methods from super class ***************************************

   /**
    * This method is the work-horse of the class. The implementation goes
    * through the follow steps:
    * <ol>
    * <li>Checks whether the username is registered in the database. If it is the
    * user is authenticated against the database.</li>
    * <li>Next we go to the LDAP server and checks whether the user can be authenticated
    * there.</li>
    * <li>If the user was authenticated using LDAP we update the database with the
    * username and password supplied before returning a Principal.</li>
    * </ol>
    *
    * @see org.apache.catalina.realm.RealmBase#authenticate(java.lang.String, java.lang.String)
    */
   public Principal authenticate(String username, String credentials) {
      ...
      ...
   }

}

The accessor methods are used to set information from the server.xml document where the Realm is configured for the Context:

<Context path="/guide" docBase="guide.war" debug="0" reloadable="true" crossContext="true">
   <Logger className="org.apache.catalina.logger.FileLogger"
      prefix="hgguide_log." suffix=".txt" verbosity="4" timestamp="true"/>

   <!-- configure login realm -->
   <Realm className="dk.horisontnet.guide.login.LdapDbRealm"
      dbDriverName="COM.ibm.db2.jdbc.app.DB2Driver"
      dbUrl="jdbc:db2:guide"
      dbUsername="db_user"
      dbPassword="password"
      ldapUsername="cn=Manager, o=Example"
      ldapPassword="password"
      ldapSearchBase="ou=Users, ou=Guide, o=Example"
      ldapUrl="ldap://localhost"
   />

   ...
   ...

</Context>

All the attributes to the <Realm /> tag should have a matching accessor pair in the Realm implementation so they can be set by Tomcat at startup. A caveat is that you need to define the Realm implementation in a MBean descriptor and add the descriptor to the ServerLifecycleListener at the top of the server.xml:

<Listener className="org.apache.catalina.mbeans.ServerLifecycleListener"
            debug="0"
            descriptors="/com/example/realm/mbean-descriptors.xml" />

The descriptor file looks like this:

<?xml version="1.0"?>
<!DOCTYPE mbeans-descriptors PUBLIC
 "-//Apache Software Foundation//DTD Model MBeans Configuration File"
 "http://jakarta.apache.org/commons/dtds/mbeans-descriptors.dtd">
<mbeans-descriptors>
   <mbean name="LdapDbRealm" className="org.apache.catalina.mbeans.ClassNameMBean"
      description="Implementation of custom Realm"
      domain="Catalina"
      group="Realm"
      type="com.example.realm.LdapDbRealm">

      <attribute name="debug"
         description="The debugging detail level for this component"
         type="int"/>

      <attribute   name="dbDriverName"
         description="The JNDI named JDBC DataSource for your database"
         type="java.lang.String"/>

      <attribute   name="dbDsName"
         description="The column in the user role table that names a role"
         type="java.lang.String"/>

      ...
      ...
   </mbean>

</mbean-descriptors>


Installation

The realm class and the descriptor is installed in Tomcat by adding it to the server.xml as described above and by copying the class files to the %CATALINA_HOME%serverclasses directory together with the descriptor file. The path in the Listener tag (see above) should match the path of the descriptor.

Custom Principal and ClassCastException problems

I created my own java.security.Principal implementation that I return from the authenticate method of Realm implementation since I need some additional information about the user. I then tought that I simply could cast the java.lang.Principal returned by the HttpServletRequest.getUserPrincipal() method to my implementation class but i kept getting java.lang.ClassCastExceptions. πŸ™

The solution could be found in the Tomcat wiki and was to use reflection. This is due to the fact that the class is loaded by different classloaders and hence are different as seen by Tomcat.

Beware of runtime dependencies (added 8/Feb/2006)

I have just upgraded the Tomcat instance the Realm is used in from Tomcat 4.1.x to 5.5.x which proved difficult since the RealmBase class being extended has changed. Please see here for more information. The lesson is to make sure to compile the Realm against the actual version of Tomcat you are deploying on.

Deploying Sametime

We started by dividing the users into groups:

  • Group 1: IT department and support staff
  • Group 2: Management and super users
  • Group 3: Office personnel
  • Group 4: All the rest (incl. mobile users)

We roll Sametime out in a stepwise fashion starting with group 1 and once they have been running it for 2 weeks we proceed to group 2 and so on. This is done to lessen the effect on the helpdesk and to make sure there are some internal ambassodors for the product before releasing it to the wild crowd.

To roll it out we simply created an explicit policy called /Sametime. The policy only has a Desktop Setting and it used to maintain the Sametime server name. We assigned the policy to the chosen users using the Domino Administrator instead of having to manually do it.

We have been using the IM_DISABLED notes.ini setting set to 1 to disable Sametime from loading in Notes 6.5.x. To configure Sametime without having to visit every user we created a welcome e-mail with a button users could click. Once the button was clicked and Sametime configured the documentation was shown for users to print. This had more users click the button.

The button does the following:

  • Removes the IM_DISABLED notes.ini setting (to enable the Sametime functionality)
  • Sets the IM_SHOW_STATUS notes.ini setting (show online status on names)
  • Sets the IM_ENABLE_SSO notes.ini setting (enable Sametime Single-Sign-On)
  • Sets the Sametime server in the active location document
  • Prompts the user to restart Notes

Once Notes is restarted Notes prompts the user for the HTTP password and the user is laughing. Sweet and simple.

We set the Sametime server in the active location document to make sure Sametime will kick in once Notes is restarted. To set it we utilized my LotusScript LocationDocument class:

Dim ld As New LocationDocument("")
Set ld.SametimeServer = New NotesName("sametime1/Example")
Call ld.Save()

So far we are rolling out to Group 2 and most users are happy. We have a few being concerned about the “big brother watching you” effect but that is probably just a matter of time until they see the benefits. Of cause they can also turn of Sametime, though we don’t encourge it.

The only problem we have had is with the HTTP password for users not using iNotes. This is a major headache and it is difficult to explain the customer that “Single-Sign-On” doesn’t really mean exactly that. I can understand them though. It would be really nice if Notes could just “handle” the authentication to Sametime. I mean the user has already been authenticated using the certificate in his id-file, which is way stronger than plain text passwords.

I hope Lotus is thinking about this as they continue to bundle Notes and Sametime.

IBM DB2 trigger and stored procedures woes

I accept that it is so, but I still find it strange what you can and cannot do in triggers. If anyone knows the answers to my problems with the below trigger code I would be more than happy to hear from them… πŸ™‚

Well it all started with me having to do a simple trigger that calculates a simple search rank when a row is updated. Instead of having this is the Java application I thought this was the perfect job for a trigger. A trigger is code you can have executed at specific times before or after a row of data is inserted. You have much more options than that but you get the point.

The trigger should run after update and end up updating the search_rank column with a calculated integer value. The search rank is quite simple – 1 point for a company name, 3 points for an address, 3 points for an e-mail address etc. After adding the different scores the final store should be written to a column. I immediately thought that a trigger should do the job so I wrote one (not as easy as it sounds since there were some trial and error included).

CREATE TRIGGER CALC_SEARCHRANK
AFTER UPDATE ON COMPANY
REFERENCING NEW AS N OLD AS O
FOR EACH ROW MODE DB2SQL
BEGIN ATOMIC
   -- declare temp variable and reset out variable
   declare calc_rank smallint default 0;
   declare temp_rank smallint default 0;
   declare temp_string varchar(50) default null;

   -- get value for logo
   declare cur_logo cursor for select name_1 from company_logo cl where cl.company_id=o.company_id;
   open cur_logo;
   fetch cur_logo into temp_rank;
   close cur_logo;
   set calc_rank = calc_rank + temp_rank;

   -- get value for products
   select count(*) into temp_rank from company c where c.company_id in (select company_id from company_logo cl where cl.company_id=o.company_id);
   set calc_rank = calc_rank + temp_rank;

   -- get value for company name
   select name_1 into temp_string from company where company_id=o.company_id;
   if temp_string != '' then
      set calc_rank = calc_rank + 1;
   end if;

   -- get value for zip/city
   select zipcode into temp_string from company where company_id=o.company_id;
   if temp_string != '' then
      set calc_rank = calc_rank + 2;
   end if;

   -- get value for address1
   select address_1 into temp_string from company where company_id=o.company_id;
   if temp_string != '' then
      set calc_rank = calc_rank + 2;
   end if;

   -- get value for phone
   select phone into temp_string from company where company_id=o.company_id;
   if temp_string != '' then
      set calc_rank = calc_rank + 3;
   end if;

   -- get value for fax
   select fax into temp_string from company where company_id=o.company_id;
   if temp_string != '' then
      set calc_rank = calc_rank + 2;
   end if;

   -- get value for e-mail
   select email into temp_string from company where company_id=o.company_id;
   if temp_string != '' then
      set calc_rank = calc_rank + 3;
   end if;

   -- get value for web
   select url into temp_string from company where company_id=o.company_id;
   if temp_string != '' then
      set calc_rank = calc_rank + 4;
   end if;

   -- update company table
   update company set search_rank=calc_rank where company_id=o.company_id;
end
@

But that wasn’t good enough. I kept getting errors when trying to add the trigger to the database:

DB21034E  The command was processed as an SQL statement because it was not a
valid Command Line Processor command.  During SQL processing it returned:
SQL0104N  An unexpected token "for" was found following "lare cur_logo
cursor".  Expected tokens may include:
"".  LINE NUMBER=12.  SQLSTATE=42601

After a LOT of trying different stuff out and searching on Google I have found out there is something called dynamic statements and what have you. Apparently you cannot do something like the above – I’ll have to look more deeply into why not. I would like to understand.

Since I hadn’t solved the problem I set out to solve yet I turned to trying my SQL out as a stored procedure. This worked better. It only took me a couple of minutes to convert the SQL into a procedure and load the procedure into my database.

CREATE PROCEDURE CALCSEARCHRANK(IN id INT)
LANGUAGE SQL
DYNAMIC RESULT SETS 0
MODIFIES SQL DATA
BEGIN
   -- declare temp variable and reset out variable
   declare calc_rank smallint default 0;
   declare temp_rank smallint default 0;
   declare temp_string varchar(50) default null;

   -- get value for logo
   select count(*) into temp_rank from company_logo cl where cl.company_id=id;
   set calc_rank = calc_rank + temp_rank;

   ... (same as above)
   ... (same as above)
   ... (same as above)

   -- update company table
   update company set search_rank=calc_rank where company_id=id;
END @

The stored procedure worked but I really would like to have the code called automatically hence the need for the trigger. After a couple of hours trying to call my procedure from my trigger I finally gave up. Apparently calling stored procedures from triggers is only supported from DB2 v.8.2 and up and we’re running DB2 v.7.2 due to some legacy stuff. Bummer!

So now I am basically back where I started and I have implemented the updating of the search rank in the application instead in the database. The only thing left to do is the updating of the existing records but that should be simple (famous last words)) using the stored procedure I ended up writing. The only issue there is installing the stored procedure in DB2 on the Linux production box.

I am having an issue with this on Linux where DB2 keeps complaining about not be able to load the db2udp library but I might have found the error. Apparently you need to install the Application Development package in order for you to install stored procedures.

This is a logical prerequisite I think… πŸ™‚

JBoss stability problems

We are having some problems with a JBoss 3.2.6 instance running out of memory (java.lang.OutOfMemoryError) with an error message about not being able to create native threads. First of I though it was memory for the instance so I added a bunch of more memory that that didn’t seem to fix the problem. I poked a little around on Google and found JBoss issue 1369 which is basically what we are experiencing. For the moment I have tried disabling the UIL2 (Universal Invocation Layer 2) and just leaving the JVM Invocation Layer (jvm-il) to see if that solves the problem. Only using the jvm-il should solve the need to using separate threads etc.

We are crossing our fingers.

Domino 7 feature: Rename reversion approval

Quoted from “New features in Lotus Domino 7.0 Beta 4” on developerWorks:

The administration process (also known as AdminP) no longer automatically reverts name changes. It now requires the administrator to either approve or reject the name change reversion. To provide uninterrupted access to a user’s databases while a name change is in progress, there is a period of time in which both the old and new names are allowed access to the systems and databases. By default, this period is 21 days, but you can set it to any whole day value from 14 to 60 when the rename is performed. At the end of this period, the old name will no longer be supported.

In some situations (for example, when the user is away for an extended period and cannot accept the name change), the old name must remain active and the new name abandoned. In such cases, the name change needs to be reverted to provide continued access for the user. In earlier releases, the reversion was performed automatically at expiration time. With the addition of the new approval process, the administrator can now approve or reject a name change reversion.

New version of Ultimate Dropdown Menu released…

For a web application we are hosting for some customers we use the Ultimate Dropdown Menu which is a CSS formatted menu product. The UDM is a third-party product – not something we did at my company.

The primary reason that we are using it is that it allows the developer to set a shortcut key so the users can access and use the menu using only the keyboard. This is a must for a web application that users use each day as their primary tool for data entry. Another reason is that the license is cheap! At 70 USD per server it is very competitive – you can’t even code a decent stylesheet for that amount of money.

The menu had one short coming though. The menu is based on unordered HTML lists (<ul>) which may be nested to desired level of nesting and we needed to add menu items to the menu dynamically using the DOM (Document Object Model) through JavaScript. However the menu API was missing a way to have it rebuild its internal state and make it update dynamically. This short coming has been solved in the new 4.43 release.


The Load XML extension is only possible because of a new public method called um.refresh, which re-initialises the navigation tree as though the page had been reloaded – many thanks to Mikkel Heisterberg for suggesting this.