This is a tiny little wish list for the security subsystem for JSPWiki 2.3 and beyond. The principal author of the security system for 2.3, Andrew Jaquith, maintains this page. But you should feel free to add things to it. Please do not ask questions here -- this is for security API enhancement requests only. If you need to post a question, use the JSPWiki Mailing List.

Incorporate simple single signon login module#

I've created a LoginModule for Siteminder single signon.

I simply needed a way to fetch Subject Principal provided by Siteminder in the web server that forwards requests to the JSPWiki servlet container. Authentication and first pass authorization is already finished when the servlet container receives that request and the authorization information is in a request header variable.

Example header variable:

  • sm-universalid: first.last@tld

After reading the quite informative JSPWiki:Security 2.3, I managed to create a LoginModule that does the job for my environment:

  • IIS-6
  • Siteminder-5QMR5
  • jspwiki-2.4.15-beta
  • tomcat-4.1.27

I create src\com\ecyrd\jspwiki\auth\login\SiteminderLoginModule.java, compile a new jspwiki.jar and jspwiki.jks and update the server.

I modified jspwiki.jaas:

JSPWiki-custom {
  com.ecyrd.jspwiki.auth.login.SiteminderLoginModule   REQUIRED   principalHeader=sm-universalid;
};
Note: One should use container app. policy as below, but I haven't figured out how to get that to work:
JSPWiki-container {
  com.ecyrd.jspwiki.auth.login.SiteminderLoginModule   REQUIRED   principalHeader=sm-universalid;
};

JSPWiki-custom {
   com.ecyrd.jspwiki.auth.login.UserDatabaseLoginModule    REQUIRED;
};

This is the current code. It changes "first.m.last@tld" to "FirstMLast":

package com.ecyrd.jspwiki.auth.login;

import java.io.IOException;
//import java.security.Principal;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.log4j.Logger;

import com.ecyrd.jspwiki.auth.WikiPrincipal;
import com.ecyrd.jspwiki.auth.authorize.Role;

/**
 * <p>
 * Logs in a user by extracting authentication data from an HTTP request header variable.
 * Siteminder single signon (v4 and v5) uses this method. It is assumed that
 * authentication takes place in the web server, after which the request is forwarded
 * to the servlet container.
 * </p>
 * <p>
 * The HTTP request header variable used defaults to "sm-universalid". Use option
 * "principalHeader" in JAAS config to modify this:
 * <pre>
 *   JSPWiki-container {
 *     com.ecyrd.jspwiki.auth.login.SiteminderLoginModule   REQUIRED   principalHeader=sm-universalid;
 *   };
 * </pre>
 * </p>
 * <p>
 * This module must be used with a CallbackHandler that supports the following
 * Callback type:
 * </p>
 * <ol>
 * <li>{@link HttpRequestCallback} - supplies the Http request object, from
 * which the Subject Principal representation is extracted</li>
 * </ol>
 * <p>
 * After authentication, the Subject will contain principals
 * {@link com.ecyrd.jspwiki.auth.authorize.Role#ALL}
 * and {@link com.ecyrd.jspwiki.auth.authorize.Role#AUTHENTICATED}.
 * 
 * @author Andrew Jaquith
 * @author Gordon Smith
 * @version $Revision: 1.8 $ $Date: 2006/05/28 23:25:07 $
 * @since 2.3
 */
public class SiteminderLoginModule extends AbstractLoginModule
{
    protected static Logger       log      = Logger.getLogger( SiteminderLoginModule.class );

    protected static String JAAS_PRINCIPAL_HEADER = "principalHeader";
    protected static String HTTP_PRINCIPAL_HEADER_DEFAULT = "sm-universalid";

    /**
     * Logs in the user.
     * @see javax.security.auth.spi.LoginModule#login()
     */
    public boolean login() throws LoginException
    {
        // Make a Principal based on the appropriate request header variable.
        HttpRequestCallback requestCallback = new HttpRequestCallback();
        Callback[] callbacks = new Callback[] { requestCallback };

        try
        {
            String principalHeaderName = HTTP_PRINCIPAL_HEADER_DEFAULT;
            // Pick up Principal header variable name if provided as an option in JAAS config.
            if ( m_options.containsKey( JAAS_PRINCIPAL_HEADER ) ) {
                principalHeaderName = ( String ) m_options.get( JAAS_PRINCIPAL_HEADER );
            }

            // Extract Principal representation out of the request directly.

            m_handler.handle( callbacks );
            HttpServletRequest request = requestCallback.getRequest();
            if ( request == null )
            {
                throw new LoginException( "No Http request supplied." );
            }

            HttpSession session = request.getSession(false);
            String sid = (session == null) ? NULL : session.getId(); 

            String principalString = request.getHeader( principalHeaderName );
            if ( principalString == null )
            {
                if ( log.isDebugEnabled() ) {
                    log.debug( "Principal request var \"" + principalHeaderName + "\" not found for session ID=" + sid );
                }
                throw new FailedLoginException( "No user Principal found" );
            }
            if ( log.isDebugEnabled() ) {
                log.debug( "Request Principal string = " + principalString );
            }

            // Modify Principal string for wiki.

            // Extract username if Principal is email address.
            int indexPos = principalString.indexOf( '@' );
            if ( indexPos != -1 ) {
                principalString = principalString.substring( 0, indexPos );
            }

            // Split username on '.' or '_'.
            String [] words = principalString.split( "\\.|_" );

            // Change username to UpperCamelCase.
            String wikiPrincipalString = "";
            for ( int wordsI = 0; wordsI < words.length; wordsI++ ) {
                String word = words[wordsI];
                if(word.length() == 1) {
                    wikiPrincipalString += word.toUpperCase();
                }
                else {
                    wikiPrincipalString += word.substring( 0, 1 ).toUpperCase() + word.substring(1);
                }
            }
            if ( log.isDebugEnabled() ) {
                log.debug( "Wiki Principal string = " + wikiPrincipalString );
            }

            WikiPrincipal principal = new WikiPrincipal( wikiPrincipalString );
            if ( log.isDebugEnabled() ) {
                log.debug( "Adding Principals " + principal.getName() + ",Role.AUTHENTICATED,Role.ALL - sid=" + sid );
            }

            // If login succeeds, commit these principals/roles.
            m_principals.add( principal );
            m_principals.add( Role.AUTHENTICATED );
            m_principals.add( Role.ALL );
            
            // If login succeeds, overwrite these principals/roles.
            m_principalsToOverwrite.add( WikiPrincipal.GUEST );
            
            // If login fails, remove these roles.
            m_principalsToRemove.add( Role.ANONYMOUS );

            return true;
        }
        catch( IOException e )
        {
            log.error( "IOException: " + e.getMessage() );
            return false;
        }
        catch( UnsupportedCallbackException e )
        {
            log.error( "UnsupportedCallbackException: " + e.getMessage() );
            return false;
        }
    }

}

Consolidated ACL permissions for grants to same Principal#

Ebu points out that if you define something like this for an ACL:
  ALLOW view ebu
  ALLOW delete ebu
...for a particular page, the parser in DefaultAclManager will build two PagePermission objects for Principal ebu and store them in the AclEntry. Since it's entirely possible for someone to write an acl like that, would it make sense to collapse a single principal's actions (permissions) to the single most relevant one within an AclEntry?

ARJ comments: The code for this is not difficult: when ACLs are parsed or modified, iterate through ACL entries 1..n (backwards) and see if each ACL entry is implied by, or implies, any other. If so, discard the implied ACL. This can probably wait until 2.5 and the long-awaited metadata refactoring.

PagePermissions tag#

Writes a little string that says "You may edit this page" or something else that indicates what privileges the user has for the current page.

More public methods for user and group managers#

From Ebu:
UserDataBase:
  List<Principal> getUsers()
  List<String> getUserIdentifiers()

    List all users.
    The latter is useful e.g. when editing groups or displaying per-user access information
    on a custom JSP page. The former might be useful for Java code; haven't needed this yet.
    I use the user's login as identifier, as opposed to his WikiName, by the way.

  boolean exists( String userIdentifier )

    Trying to decide whether this is necessary. We have the various findBy*, maybe this is
    sufficient. We have the equivalent in GroupManager, though.

GroupManager:

  List<Group> getGroups()
  List<String> getGroupNames()

    List all groups.
    The latter form would be useful, again, in building an ACL management interface.

AclImpl (or utility class?):

  List<PagePermission> getUserPermissions( WikiPage, Principal )
    Get the permissions of the given user for the given page.
    Should _not_ add default policy permissions.

  List<Principal> getUsersByPermission( WikiPage, PagePermission )
    List all principals that have the specified permission for the page.

  void removeUserPermissions( WikiPage, Principal )
    Remove the permissions of this user from this page, without affecting any
    other ACL entries

    All of these are useful when presenting ACLs in a usable GUI form, and
    applying the incoming changes.
ARJ comments: some of these are very good ideas indeed.

Auditing features#

From Ebu again: A WikiAuditor interface, or something equivalent. WikiStatistics? Certain things, such as page edits, could be sorted by user, which could help keep track of spamming. (Your custom implementation could send you email the moment it notices too many edits from the same user in a short time span, etc.)
  void auditAuthEvent( AuthorizationEvent )
    Makes note of login, logout; could store login time, source, ...

  List<Principal> getActiveUsers()
    Returns a list of currently logged in users

  void auditUserOperation( UserOperation )
    Makes note of added/removed user

  void auditPageOperation( WikiPage, PageOperation )
    Makes note of page add/edit/delete/attach, ACL changes
ARJ comments: auditing has been on my mind for a while. I think this is a 2.5 item, though. I think it's highly likely that we do this a bit differently, though. The pattern I'm thinking of is more of a classic Listener pattern for generic events (a la Swing), some of which will be security related, and some of which will not be.

Session statistics#

Because JSPWiki 2.3 now has this cool concept of a WikiSession, and we keep a cache of these in memory so they can be associated with HttpSessions, it would be a trivial matter to add some session statistics. We could certainly count the number of active wiki sessions, and even enumerate the names of the users if we wanted. Public methods could feed some web-tier tags that printed out, for instance, the number of logged-in users v. the number of guests.

Password quality#

Password length and composition is not enforced. A minimum length & complexity requirement should be in place; e.g.,, must be 8 characters and include at least one number, which can't be the last character.

Trusted code boundaries#

At present JSPWiki does not have any internal security domain boundaries -- all classes inside jspwiki.jar are equally trusted. But certain operations are sensitive, like modifying pages and reading user account information. Thus, in a future version of JSPWiki (2.5) we will likely add additional permissions for user database access and other restricted actions. These will NOT be granted to users, but to jspwiki.jar. In addition, we will likely add a convenience method to AuthorizationManager similar to Subject.doAs() that permits code to run with trusted privileges. Likely method signature: AuthorizationManager.doAs( WikiSession, PrivilegedAction ). The PrivilegedAction would be a PrivilegedAction anonymous class enclosing the privileged code. UI-tier classes that call, for example, the WikiEngine.getPage(String) method (such as JSPs) would enclose this code inside a PrivilegedAction block and pass it to AuthorizationManager.doAs along with the current WikiSession. The getPage method would check to see if the caller possessed the PagePermission, "(page)", "read" permission.

Here's how a typical JSP scriptlet would "safely" retrieve a page from WikiEngine:

final String page = request.getParameter( "page" );
try
{
  AuthorizationMananger.doAs( wikiSession, new PrivilegedAction()
    {
      public Object run()
      {
        engine.getPage( page );
        return null;
      }
   } );
}
catch( WikiSecurityException e )
{
  log.fatal( "Oooo. You've tried to get a page you're not entitled to...");
} 

...and then in WikiEngine.getPage we'd have a permission-checking code block that looks similar to this:

try
{
  java.security.AccessController.checkPermission( new PagePermission( wikiPage, "read" );
}
catch ( AccessControlException e )
{
  throw new WikiSecurityException( "You can't read page " + page );
} 

The benefit of this approach is that we can start to wall off the sensitive code, without having to change lots of method signatures to include lots of WikiSessions.


I don't know if this would fall under Security or not, but it would be nice to have a special bit of markup that would automatically insert the current list of users in the database into a page, thus allowing easy creation of a user directory.

Some notes:

  • Clearly that is a piece of functionality that would need to be able to be turned "on or off" since some sites would NOT want to provided users with that capability.
  • The markup could specify whether a list of all users is inserted, or whether only users added within the last "n" days are inserted.
  • The markup could perhaps specify that the list of users should be ordered by name, by ascending/descending by date added, or date of last post. Different pages could then be created based on these different views of the user database.

--Phill Conrad, 07-Jun-2006


It would be nice to have the concept of spaces. This way if one group has access to space XYZ and a user of that group creates a page then the security is already set for that page. This may bring up problems with the way the pages are stored etc., but it might be a nice thought to entertain.

--Chris Rohr, 20-Jun-2006

Add new attachment

Only authorized users are allowed to upload new attachments.
« This page (revision-32) was last changed on 28-Jul-2006 23:51 by null