You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by aj...@apache.org on 2008/02/13 06:54:24 UTC

svn commit: r627255 [12/41] - in /incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src: ./ com/ com/ecyrd/ com/ecyrd/jspwiki/ com/ecyrd/jspwiki/action/ com/ecyrd/jspwiki/attachment/ com/ecyrd/jspwiki/auth/ com/ecyrd/jspwiki/auth/acl/ com/ecyrd/jspwiki...

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/SecurityVerifier.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/SecurityVerifier.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/SecurityVerifier.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/SecurityVerifier.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,951 @@
+/*
+ JSPWiki - a JSP-based WikiWiki clone.
+
+ Copyright (C) 2001-2005 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+package com.ecyrd.jspwiki.auth;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.*;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.log4j.Logger;
+import org.freshcookies.security.policy.PolicyReader;
+import org.jdom.JDOMException;
+
+import com.ecyrd.jspwiki.InternalWikiException;
+import com.ecyrd.jspwiki.WikiEngine;
+import com.ecyrd.jspwiki.WikiException;
+import com.ecyrd.jspwiki.WikiSession;
+import com.ecyrd.jspwiki.auth.authorize.*;
+import com.ecyrd.jspwiki.auth.permissions.AllPermission;
+import com.ecyrd.jspwiki.auth.permissions.GroupPermission;
+import com.ecyrd.jspwiki.auth.permissions.PermissionFactory;
+import com.ecyrd.jspwiki.auth.permissions.WikiPermission;
+import com.ecyrd.jspwiki.auth.user.DefaultUserProfile;
+import com.ecyrd.jspwiki.auth.user.UserDatabase;
+import com.ecyrd.jspwiki.auth.user.UserProfile;
+
+/**
+ * Helper class for verifying JSPWiki's security configuration. Invoked by
+ * <code>admin/SecurityConfig.jsp</code>.
+ * @author Andrew Jaquith
+ * @since 2.4
+ */
+public final class SecurityVerifier
+{
+    private static final long     serialVersionUID             = -3859563355089169941L;
+
+    private WikiEngine            m_engine;
+
+    private File                  m_jaasConfig                 = null;
+
+    private boolean               m_isJaasConfigured           = false;
+
+    private boolean               m_isSecurityPolicyConfigured = false;
+
+    private Principal[]           m_policyPrincipals           = new Principal[0];
+
+    private WikiSession           m_session;
+
+    /** Message prefix for errors. */
+    public static final String    ERROR                        = "Error.";
+
+    /** Message prefix for warnings. */
+    public static final String    WARNING                      = "Warning.";
+
+    /** Message prefix for information messages. */
+    public static final String    INFO                         = "Info.";
+
+    /** Message topic for policy errors. */
+    public static final String    ERROR_POLICY                 = "Error.Policy";
+
+    /** Message topic for policy warnings. */
+    public static final String    WARNING_POLICY               = "Warning.Policy";
+
+    /** Message topic for policy information messages. */
+    public static final String    INFO_POLICY                  = "Info.Policy";
+
+    /** Message topic for JAAS errors. */
+    public static final String    ERROR_JAAS                   = "Error.Jaas";
+
+    /** Message topic for JAAS warnings. */
+    public static final String    WARNING_JAAS                 = "Warning.Jaas";
+
+    /** Message topic for role-checking errors. */
+    public static final String    ERROR_ROLES                  = "Error.Roles";
+
+    /** Message topic for role-checking information messages. */
+    public static final String    INFO_ROLES                   = "Info.Roles";
+
+    /** Message topic for user database errors. */
+    public static final String    ERROR_DB                     = "Error.UserDatabase";
+
+    /** Message topic for user database warnings. */
+    public static final String    WARNING_DB                   = "Warning.UserDatabase";
+
+    /** Message topic for user database information messages. */
+    public static final String    INFO_DB                      = "Info.UserDatabase";
+
+    /** Message topic for group database errors. */
+    public static final String    ERROR_GROUPS                 = "Error.GroupDatabase";
+
+    /** Message topic for group database warnings. */
+    public static final String    WARNING_GROUPS               = "Warning.GroupDatabase";
+
+    /** Message topic for group database information messages. */
+    public static final String    INFO_GROUPS                  = "Info.GroupDatabase";
+
+    /** Message topic for JAAS information messages. */
+    public static final String    INFO_JAAS                    = "Info.Jaas";
+
+    private static final String[] CONTAINER_ACTIONS            = new String[]
+                                                               { "View pages", "Comment on existing pages",
+            "Edit pages", "Upload attachments", "Create a new group", "Rename an existing page", "Delete pages" };
+
+    private static final String[] CONTAINER_JSPS               = new String[]
+                                                               { "/Wiki.jsp", "/Comment.jsp", "/Edit.jsp",
+            "/Upload.jsp", "/NewGroup.jsp", "/Rename.jsp", "/Delete.jsp" };
+
+    private static final String   BG_GREEN                     = "bgcolor=\"#c0ffc0\"";
+
+    private static final String   BG_RED                       = "bgcolor=\"#ffc0c0\"";
+
+    private static final Logger LOG                          = Logger.getLogger( SecurityVerifier.class.getName() );
+
+    /**
+     * Constructs a new SecurityVerifier for a supplied WikiEngine and WikiSession.
+     * @param engine the wiki engine
+     * @param session the wiki session (typically, that of an administrator)
+     */
+    public SecurityVerifier( WikiEngine engine, WikiSession session )
+    {
+        super();
+        m_engine = engine;
+        m_session = session;
+        m_session.clearMessages();
+        verifyJaas();
+        verifyPolicy();
+        try
+        {
+            verifyPolicyAndContainerRoles();
+        }
+        catch ( WikiException e )
+        {
+            m_session.addMessage( ERROR_ROLES, e.getMessage() );
+        }
+        verifyGroupDatabase();
+        verifyUserDatabase();
+    }
+
+    /**
+     * Returns an array of unique Principals from the JSPWIki security policy
+     * file. This array will be zero-length if the policy file was not
+     * successfully located, or if the file did not specify any Principals in
+     * the policy.
+     * @return the array of principals
+     */
+    public final Principal[] policyPrincipals()
+    {
+        return m_policyPrincipals;
+    }
+
+    /**
+     * Formats and returns an HTML table containing sample permissions and what
+     * roles are allowed to have them. This method will throw an
+     * {@link IllegalStateException} if the authorizer is not of type
+     * {@link com.ecyrd.jspwiki.auth.authorize.WebContainerAuthorizer}
+     * @return the formatted HTML table containing the result of the tests
+     */
+    public final String policyRoleTable()
+    {
+        Principal[] roles = m_policyPrincipals;
+        String wiki = m_engine.getApplicationName();
+
+        String[] pages = new String[]
+        { "Main", "Index", "GroupTest", "GroupAdmin" };
+        String[] pageActions = new String[]
+        { "view", "edit", "modify", "rename", "delete" };
+
+        String[] groups = new String[]
+        { "Admin", "TestGroup", "Foo" };
+        String[] groupActions = new String[]
+        { "view", "edit", null, null, "delete" };
+
+        // Calculate column widths
+        String colWidth;
+        if ( pageActions.length > 0 && roles.length > 0 )
+        {
+            colWidth = String.valueOf( 67f / ( pageActions.length * roles.length ) ) + "%";
+        }
+        else
+        {
+            colWidth = "67%";
+        }
+
+        StringBuffer s = new StringBuffer();
+
+        // Write the table header
+        s.append( "<table class=\"wikitable\" border=\"1\">\n" );
+        s.append( "  <colgroup span=\"1\" width=\"33%\"/>\n" );
+        s.append( "  <colgroup span=\"" + pageActions.length * roles.length + "\" width=\"" + colWidth
+                + "\" align=\"center\"/>\n" );
+        s.append( "  <tr>\n" );
+        s.append( "    <th rowspan=\"2\" valign=\"bottom\">Permission</th>\n" );
+        for( int i = 0; i < roles.length; i++ )
+        {
+            s.append( "    <th colspan=\"" + pageActions.length + "\" title=\"" + roles[i].getClass().getName() + "\">"
+                    + roles[i].getName() + "</th>\n" );
+        }
+        s.append( "  </tr>\n" );
+
+        // Print a column for each role
+        s.append( "  <tr>\n" );
+        for( int i = 0; i < roles.length; i++ )
+        {
+            for( int j = 0; j < pageActions.length; j++ )
+            {
+                String action = pageActions[j].substring( 0, 1 );
+                s.append( "    <th title=\"" + pageActions[j] + "\">" + action + "</th>\n" );
+            }
+        }
+        s.append( "  </tr>\n" );
+
+        // Write page permission tests first
+        for( int i = 0; i < pages.length; i++ )
+        {
+            String page = pages[i];
+            s.append( "  <tr>\n" );
+            s.append( "    <td>PagePermission \"" + wiki + ":" + page + "\"</td>\n" );
+            for( int j = 0; j < roles.length; j++ )
+            {
+                for( int k = 0; k < pageActions.length; k++ )
+                {
+                    Permission permission = PermissionFactory.getPagePermission( wiki + ":" + page, pageActions[k] );
+                    s.append( printPermissionTest( permission, roles[j], 1 ) );
+                }
+            }
+            s.append( "  </tr>\n" );
+        }
+
+        // Now do the group tests
+        for( int i = 0; i < groups.length; i++ )
+        {
+            String group = groups[i];
+            s.append( "  <tr>\n" );
+            s.append( "    <td>GroupPermission \"" + wiki + ":" + group + "\"</td>\n" );
+            for( int j = 0; j < roles.length; j++ )
+            {
+                for( int k = 0; k < groupActions.length; k++ )
+                {
+                    Permission permission = null;
+                    if ( groupActions[k] != null)
+                    {
+                        permission = new GroupPermission( wiki + ":" + group, groupActions[k] );
+                    }
+                    s.append( printPermissionTest( permission, roles[j], 1 ) );
+                }
+            }
+            s.append( "  </tr>\n" );
+        }
+
+
+        // Now check the wiki-wide permissions
+        String[] wikiPerms = new String[]
+        { "createGroups", "createPages", "login", "editPreferences", "editProfile" };
+        for( int i = 0; i < wikiPerms.length; i++ )
+        {
+            s.append( "  <tr>\n" );
+            s.append( "    <td>WikiPermission \"" + wiki + "\",\"" + wikiPerms[i] + "\"</td>\n" );
+            for( int j = 0; j < roles.length; j++ )
+            {
+                Permission permission = new WikiPermission( wiki, wikiPerms[i] );
+                s.append( printPermissionTest( permission, roles[j], pageActions.length ) );
+            }
+            s.append( "  </tr>\n" );
+        }
+
+        // Lastly, check for AllPermission
+        s.append( "  <tr>\n" );
+        s.append( "    <td>AllPermission \"" + wiki + "\"</td>\n" );
+        for( int j = 0; j < roles.length; j++ )
+        {
+            Permission permission = new AllPermission( wiki );
+            s.append( printPermissionTest( permission, roles[j], pageActions.length ) );
+        }
+        s.append( "  </tr>\n" );
+
+        // We're done!
+        s.append( "</table>" );
+        return s.toString();
+    }
+
+    /**
+     * Prints a &lt;td&gt; HTML element with the results of a permission test.
+     * @param perm the permission to format
+     * @param allowed whether the permission is allowed
+     */
+    private final String printPermissionTest( Permission permission, Principal principal, int cols )
+    {
+        StringBuffer s = new StringBuffer();
+        if ( permission == null )
+        {
+            s.append( "    <td colspan=\"" + cols + "\" align=\"center\" title=\"N/A\">" );
+            s.append( "&nbsp;</td>\n" );
+        }
+        else
+        {
+            boolean allowed = verifyStaticPermission( principal, permission );
+            s.append( "    <td colspan=\"" + cols + "\" align=\"center\" title=\"" );
+            s.append( allowed ? "ALLOW: " : "DENY: " );
+            s.append( permission.getClass().getName() );
+            s.append( " &quot;" );
+            s.append( permission.getName() );
+            s.append( "&quot;" );
+            if ( permission.getName() != null )
+            {
+                s.append( ",&quot;" );
+                s.append( permission.getActions() );
+                s.append( "&quot;" );
+            }
+            s.append( " " );
+            s.append( principal.getClass().getName() );
+            s.append( " &quot;" );
+            s.append( principal.getName() );
+            s.append( "&quot;" );
+            s.append( "\"" );
+            s.append( allowed ? BG_GREEN + ">" : BG_RED + ">" );
+            s.append( "&nbsp;</td>\n" );
+        }
+        return s.toString();
+    }
+
+    /**
+     * Formats and returns an HTML table containing the roles the web container
+     * is aware of, and whether each role maps to particular JSPs. This method
+     * throws an {@link IllegalStateException} if the authorizer is not of type
+     * {@link com.ecyrd.jspwiki.auth.authorize.WebContainerAuthorizer}
+     * @return the formatted HTML table containing the result of the tests
+     * @throws WikiException if tests fail for unexpected reasons
+     */
+    public final String containerRoleTable() throws WikiException
+    {
+
+        AuthorizationManager authorizationManager = m_engine.getAuthorizationManager();
+        Authorizer authorizer = authorizationManager.getAuthorizer();
+
+        // If authorizer not WebContainerAuthorizer, print error message
+        if ( !( authorizer instanceof WebContainerAuthorizer ) )
+        {
+            throw new IllegalStateException( "Authorizer should be WebContainerAuthorizer" );
+        }
+
+        // Now, print a table with JSP pages listed on the left, and
+        // an evaluation of each pages' constraints for each role
+        // we discovered
+        StringBuffer s = new StringBuffer();
+        Principal[] roles = authorizer.getRoles();
+        s.append( "<table class=\"wikitable\" border=\"1\">\n" );
+        s.append( "<thead>\n" );
+        s.append( "  <tr>\n" );
+        s.append( "    <th rowspan=\"2\">Action</th>\n" );
+        s.append( "    <th rowspan=\"2\">Page</th>\n" );
+        s.append( "    <th colspan=\"" + roles.length + 1 + "\">Roles</th>\n" );
+        s.append( "  </tr>\n" );
+        s.append( "  <tr>\n" );
+        s.append( "    <th>Anonymous</th>\n" );
+        for( int i = 0; i < roles.length; i++ )
+        {
+            s.append( "    <th>" + roles[i].getName() + "</th>\n" );
+        }
+        s.append( "</tr>\n" );
+        s.append( "</thead>\n" );
+        s.append( "<tbody>\n" );
+
+        try
+        {
+            WebContainerAuthorizer wca = (WebContainerAuthorizer) authorizer;
+            for( int i = 0; i < CONTAINER_ACTIONS.length; i++ )
+            {
+                String action = CONTAINER_ACTIONS[i];
+                String jsp = CONTAINER_JSPS[i];
+
+                // Print whether the page is constrained for each role
+                boolean allowsAnonymous = !wca.isConstrained( jsp, Role.ALL );
+                s.append( "  <tr>\n" );
+                s.append( "    <td>" + action + "</td>\n" );
+                s.append( "    <td>" + jsp + "</td>\n" );
+                s.append( "    <td title=\"" );
+                s.append( allowsAnonymous ? "ALLOW: " : "DENY: " );
+                s.append( jsp );
+                s.append( " Anonymous" );
+                s.append( "\"" );
+                s.append( allowsAnonymous ? BG_GREEN + ">" : BG_RED + ">" );
+                s.append( "&nbsp;</td>\n" );
+                for( int j = 0; j < roles.length; j++ )
+                {
+                    Role role = (Role) roles[j];
+                    boolean allowed = allowsAnonymous || wca.isConstrained( jsp, role );
+                    s.append( "    <td title=\"" );
+                    s.append( allowed ? "ALLOW: " : "DENY: " );
+                    s.append( jsp );
+                    s.append( " " );
+                    s.append( role.getClass().getName() );
+                    s.append( " &quot;" );
+                    s.append( role.getName() );
+                    s.append( "&quot;" );
+                    s.append( "\"" );
+                    s.append( allowed ? BG_GREEN + ">" : BG_RED + ">" );
+                    s.append( "&nbsp;</td>\n" );
+                }
+                s.append( "  </tr>\n" );
+            }
+        }
+        catch( JDOMException e )
+        {
+            // If we couldn't evaluate constraints it means
+            // there's some sort of IO mess or parsing issue
+            LOG.error( "Malformed XML in web.xml", e );
+            throw new InternalWikiException( e.getClass().getName() + ": " + e.getMessage() );
+        }
+
+        s.append( "</tbody>\n" );
+        s.append( "</table>\n" );
+        return s.toString();
+    }
+
+    /**
+     * Returns <code>true</code> if JAAS is configured correctly.
+     * @return the result of the configuration check
+     */
+    public final boolean isJaasConfigured()
+    {
+        return m_isJaasConfigured;
+    }
+
+    /**
+     * Returns <code>true</code> if the JAAS login configuration was already
+     * set when JSPWiki started up. We determine this value by consulting a
+     * protected member field of {@link AuthenticationManager}, which was set
+     * at in initialization by {@link PolicyLoader}.
+     * @return <code>true</code> if {@link PolicyLoader} successfully set the
+     *         policy, or <code>false</code> for any other reason.
+     */
+    public final boolean isJaasConfiguredAtStartup()
+    {
+        return m_engine.getAuthenticationManager().m_isJaasConfiguredAtStartup;
+    }
+
+    /**
+     * Returns <code>true</code> if JSPWiki can locate a named JAAS login
+     * configuration.
+     * @param config the name of the application (e.g.,
+     *            <code>JSPWiki-container</code>).
+     * @return <code>true</code> if found; <code>false</code> otherwise
+     */
+    protected final boolean isJaasConfigurationAvailable( String config )
+    {
+        try
+        {
+            m_session.addMessage( INFO_JAAS, "We found the '" + config + "' login configuration." );
+            new LoginContext( config );
+            return true;
+        }
+        catch( Exception e )
+        {
+            m_session.addMessage( ERROR_JAAS, "We could not find the '" + config + "' login configuration.</p>" );
+            return false;
+        }
+    }
+
+    /**
+     * Returns <code>true</code> if the Java security policy is configured
+     * correctly, and it verifies as valid.
+     * @return the result of the configuration check
+     */
+    public final boolean isSecurityPolicyConfigured()
+    {
+        return m_isSecurityPolicyConfigured;
+    }
+
+    /**
+     * If the active Authorizer is the WebContainerAuthorizer, returns the roles
+     * it knows about; otherwise, a zero-length array.
+     * @return the roles parsed from <code>web.xml</code>, or a zero-length array
+     * @throws WikiException if the web authorizer cannot obtain the list of roles
+     */
+    public final Principal[] webContainerRoles() throws WikiException
+    {
+        Authorizer authorizer = m_engine.getAuthorizationManager().getAuthorizer();
+        if ( authorizer instanceof WebContainerAuthorizer )
+        {
+            return ( (WebContainerAuthorizer) authorizer ).getRoles();
+        }
+        return new Principal[0];
+    }
+
+    /**
+     * Verifies that the roles given in the security policy are reflected by the
+     * container <code>web.xml</code> file.
+     * @throws WikiException if the web authorizer cannot verify the roles
+     */
+    protected final void verifyPolicyAndContainerRoles() throws WikiException
+    {
+        Authorizer authorizer = m_engine.getAuthorizationManager().getAuthorizer();
+        Principal[] containerRoles = authorizer.getRoles();
+        boolean missing = false;
+        for( int i = 0; i < m_policyPrincipals.length; i++ )
+        {
+            Principal principal = m_policyPrincipals[i];
+            if ( principal instanceof Role )
+            {
+                Role role = (Role) principal;
+                boolean isContainerRole = ArrayUtils.contains( containerRoles, role );
+                if ( !Role.isBuiltInRole( role ) && !isContainerRole )
+                {
+                    m_session.addMessage( ERROR_ROLES, "Role '" + role.getName() + "' is defined in security policy but not in web.xml." );
+                    missing = true;
+                }
+            }
+        }
+        if ( !missing )
+        {
+            m_session.addMessage( INFO_ROLES, "Every non-standard role defined in the security policy was also found in web.xml." );
+        }
+    }
+
+    /**
+     * Verifies that the group datbase was initialized properly, and that
+     * user add and delete operations work as they should.
+     */
+    protected final void verifyGroupDatabase()
+    {
+        GroupManager mgr = m_engine.getGroupManager();
+        GroupDatabase db = null;
+        try
+        {
+            db = m_engine.getGroupManager().getGroupDatabase();
+        }
+        catch ( WikiSecurityException e )
+        {
+            m_session.addMessage( ERROR_GROUPS, "Could not retrieve GroupManager: " + e.getMessage() );
+        }
+
+        // Check for obvious error conditions
+        if ( mgr == null || db == null )
+        {
+            if ( mgr == null )
+            {
+                m_session.addMessage( ERROR_GROUPS, "GroupManager is null; JSPWiki could not " +
+                        "initialize it. Check the error logs." );
+            }
+            if ( db == null )
+            {
+                m_session.addMessage( ERROR_GROUPS, "GroupDatabase is null; JSPWiki could not " +
+                        "initialize it. Check the error logs." );
+            }
+            return;
+        }
+
+        // Everything initialized OK...
+
+        // Tell user what class of database this is.
+        m_session.addMessage( INFO_GROUPS, "GroupDatabase is of type '" + db.getClass().getName() +
+                "'. It appears to be initialized properly." );
+
+        // Now, see how many groups we have.
+        int oldGroupCount = 0;
+        try
+        {
+            Group[] groups = db.groups();
+            oldGroupCount = groups.length;
+            m_session.addMessage( INFO_GROUPS, "The group database contains " + oldGroupCount + " groups." );
+        }
+        catch ( WikiSecurityException e )
+        {
+            m_session.addMessage( ERROR_GROUPS, "Could not obtain a list of current groups: " + e.getMessage() );
+            return;
+        }
+
+        // Try adding a bogus group with random name
+        String name = "TestGroup" + String.valueOf( System.currentTimeMillis() );
+        Group group = null;
+        try
+        {
+            // Create dummy test group
+            group = mgr.parseGroup( name, "", true );
+            Principal user = new WikiPrincipal( "TestUser" );
+            group.add( user );
+            db.save( group, new WikiPrincipal("SecurityVerifier") );
+
+            // Make sure the group saved successfully
+            if ( db.groups().length == oldGroupCount )
+            {
+                m_session.addMessage( ERROR_GROUPS, "Could not add a test group to the database." );
+                return;
+            }
+            m_session.addMessage( INFO_GROUPS, "The group database allows new groups to be created, as it should." );
+        }
+        catch ( WikiSecurityException e )
+        {
+            m_session.addMessage( ERROR_GROUPS, "Could not add a group to the database: " + e.getMessage() );
+            return;
+        }
+
+        // Now delete the group; should be back to old count
+        try
+        {
+            db.delete( group );
+            if ( db.groups().length != oldGroupCount )
+            {
+                m_session.addMessage( ERROR_GROUPS, "Could not delete a test group from the database." );
+                return;
+            }
+            m_session.addMessage( INFO_GROUPS, "The group database allows groups to be deleted, as it should." );
+        }
+        catch ( WikiSecurityException e )
+        {
+            m_session.addMessage( ERROR_GROUPS, "Could not delete a test group from the database: " + e.getMessage() );
+            return;
+        }
+
+        m_session.addMessage( INFO_GROUPS, "The group database configuration looks fine." );
+    }
+
+    /**
+     * Verfies the JAAS configuration. The configuration is valid if value of
+     * the system property <code>java.security.auth.login.config</code>
+     * resolves to an existing file, and we can find the JAAS login
+     * configurations for <code>JSPWiki-container</code> and
+     * <code>JSPWiki-custom</code>.
+     */
+    protected final void verifyJaas()
+    {
+        // See if JAAS is on
+        AuthorizationManager authMgr = m_engine.getAuthorizationManager();
+        if ( !authMgr.isJAASAuthorized() )
+        {
+            m_session.addMessage( ERROR_JAAS, "JSPWiki's JAAS-based authentication " +
+                    "and authorization system is turned off (your <code>jspwiki.properties</code> " +
+                    "contains the setting 'jspwiki.security = container'. This " +
+                    "setting disables authorization checks and is meant for testing " +
+                    "and troubleshooting only. The test results on this page will not " +
+                    "be reliable as a result. You should set this to 'jaas' " +
+                    "so that security works properly." );
+        }
+
+        // Validate the property is set correctly
+        m_jaasConfig = getFileFromProperty( "java.security.auth.login.config" );
+
+        // Look for the JSPWiki-container config
+        boolean foundJaasContainerConfig = isJaasConfigurationAvailable( "JSPWiki-container" );
+
+        // Look for the JSPWiki-custom config
+        boolean foundJaasCustomConfig = isJaasConfigurationAvailable( "JSPWiki-custom" );
+
+        m_isJaasConfigured = m_jaasConfig != null && foundJaasContainerConfig && foundJaasCustomConfig;
+    }
+
+    /**
+     * Looks up a file name based on a JRE system property and returns the associated
+     * File object if it exists. This method adds messages with the topic prefix 
+     * {@link #ERROR} and {@link #INFO} as appropriate, with the suffix matching the 
+     * supplied property.
+     * @param property the system property to look up
+     * @return the file object, or <code>null</code> if not found
+     */
+    protected final File getFileFromProperty( String property )
+    {
+        String propertyValue = null;
+        try
+        {
+            propertyValue = System.getProperty( property );
+            if ( propertyValue == null )
+            {
+                m_session.addMessage( "Error." + property, "The system property '" + property + "' is null." );
+                return null;
+            }
+
+            //
+            //  It's also possible to use "==" to mark a property.  We remove that
+            //  here so that we can actually find the property file, then.
+            //
+            if( propertyValue.startsWith("=") )
+            {
+                propertyValue = propertyValue.substring(1);
+            }
+
+            try
+            {
+                m_session.addMessage( "Info." + property, "The system property '" + property + "' is set to: "
+                        + propertyValue + "." );
+
+                // Prepend a file: prefix if not there already
+                if ( !propertyValue.startsWith( "file:" ) )
+                {
+                  propertyValue = "file:" + propertyValue;
+                }
+                URL url = new URL( propertyValue );
+                File file = new File( url.getPath() );
+                if ( file.exists() )
+                {
+                    m_session.addMessage( "Info." + property, "File '" + propertyValue + "' exists in the filesystem." );
+                    return file;
+                }
+            }
+            catch( MalformedURLException e )
+            {
+                // Swallow exception because we can't find it anyway
+            }
+            m_session.addMessage( "Error." + property, "File '" + propertyValue
+                    + "' doesn't seem to exist. This might be a problem." );
+            return null;
+        }
+        catch( SecurityException e )
+        {
+            m_session.addMessage( "Error." + property, "We could not read system property '" + property
+                    + "'. This is probably because you are running with a security manager." );
+            return null;
+        }
+    }
+
+    /**
+     * Verfies the Java security policy configuration. The configuration is
+     * valid if value of the local policy (at <code>WEB-INF/jspwiki.policy</code>
+     * resolves to an existing file, and the policy file contained therein
+     * represents a valid policy.
+     */
+    protected final void verifyPolicy()
+    {
+        // Look up the policy file and set the status text.
+        URL policyURL = AuthenticationManager.findConfigFile( m_engine, AuthorizationManager.DEFAULT_POLICY );
+        String path = policyURL.getPath();
+        if ( path.startsWith("file:") )
+        {
+            path = path.substring( 5 );
+        }
+        File policyFile = new File( path );
+
+        // Next, verify the policy
+        try
+        {
+            // Get the file
+            PolicyReader policy = new PolicyReader( policyFile );
+            m_session.addMessage( INFO_POLICY, "The security policy '" + policy.getFile() + "' exists." );
+
+            // See if there is a keystore that's valid
+            KeyStore ks = policy.getKeyStore();
+            if ( ks == null )
+            {
+                m_session.addMessage( ERROR_POLICY,
+                    "Policy file does not have a keystore... at least not one that we can locate." );
+            }
+            else
+            {
+                m_session.addMessage( INFO_POLICY,
+                    "The security policy specifies a keystore, and we were able to locate it in the filesystem." );
+            }
+
+            // Verify the file
+            policy.read();
+            List errors = policy.getMessages();
+            if ( errors.size() > 0 )
+            {
+                for( Iterator it = errors.iterator(); it.hasNext(); )
+                {
+                    Exception e = (Exception) it.next();
+                    m_session.addMessage( ERROR_POLICY, e.getMessage() );
+                }
+            }
+            else
+            {
+                m_session.addMessage( INFO_POLICY, "The security policy looks fine." );
+                m_isSecurityPolicyConfigured = true;
+            }
+
+            // Stash the unique principals mentioned in the file,
+            // plus our standard roles.
+            Set principals = new LinkedHashSet();
+            principals.add( Role.ALL );
+            principals.add( Role.ANONYMOUS );
+            principals.add( Role.ASSERTED );
+            principals.add( Role.AUTHENTICATED );
+            ProtectionDomain[] domains = policy.getProtectionDomains();
+            for ( int i = 0; i < domains.length; i++ )
+            {
+                Principal[] domainPrincipals = domains[i].getPrincipals();
+                for( int j = 0; j < domainPrincipals.length; j++ )
+                {
+                    principals.add( domainPrincipals[j] );
+                }
+            }
+            m_policyPrincipals = (Principal[]) principals.toArray( new Principal[principals.size()] );
+        }
+        catch( IOException e )
+        {
+            m_session.addMessage( ERROR_POLICY, e.getMessage() );
+        }
+    }
+
+    /**
+     * Verifies that a particular Principal possesses a Permission, as defined
+     * in the security policy file.
+     * @param principal the principal
+     * @param permission the permission
+     * @return the result, based on consultation with the active Java security
+     *         policy
+     */
+    protected final boolean verifyStaticPermission( Principal principal, final Permission permission )
+    {
+        Subject subject = new Subject();
+        subject.getPrincipals().add( principal );
+        boolean allowedByGlobalPolicy = ((Boolean)
+            Subject.doAsPrivileged( subject, new PrivilegedAction()
+            {
+                public Object run()
+                {
+                    try
+                    {
+                        AccessController.checkPermission( permission );
+                        return Boolean.TRUE;
+                    }
+                    catch ( AccessControlException e )
+                    {
+                        return Boolean.FALSE;
+                    }
+                }
+            }, null )).booleanValue();
+
+        if ( allowedByGlobalPolicy )
+        {
+            return true;
+        }
+
+        // Check local policy
+        Principal[] principals = new Principal[]{ principal };
+        return m_engine.getAuthorizationManager().allowedByLocalPolicy( principals, permission );
+    }
+
+    /**
+     * Verifies that the user datbase was initialized properly, and that
+     * user add and delete operations work as they should.
+     */
+    protected final void verifyUserDatabase()
+    {
+        UserDatabase db = m_engine.getUserManager().getUserDatabase();
+
+        // Check for obvious error conditions
+        if ( db == null )
+        {
+            m_session.addMessage( ERROR_DB, "UserDatabase is null; JSPWiki could not " +
+                    "initialize it. Check the error logs." );
+            return;
+        }
+
+        if ( db instanceof UserManager.DummyUserDatabase )
+        {
+            m_session.addMessage( ERROR_DB, "UserDatabase is DummyUserDatabase; JSPWiki " +
+                    "may not have been able to initialize the database you supplied in " +
+                    "jspwiki.properties, or you left the 'jspwiki.userdatabase' property " +
+                    "blank. Check the error logs." );
+        }
+
+        // Tell user what class of database this is.
+        m_session.addMessage( INFO_DB, "UserDatabase is of type '" + db.getClass().getName() +
+                "'. It appears to be initialized properly." );
+
+        // Now, see how many users we have.
+        int oldUserCount = 0;
+        try
+        {
+            Principal[] users = db.getWikiNames();
+            oldUserCount = users.length;
+            m_session.addMessage( INFO_DB, "The user database contains " + oldUserCount + " users." );
+        }
+        catch ( WikiSecurityException e )
+        {
+            m_session.addMessage( ERROR_DB, "Could not obtain a list of current users: " + e.getMessage() );
+            return;
+        }
+
+        // Try adding a bogus user with random name
+        String loginName = "TestUser" + String.valueOf( System.currentTimeMillis() );
+        try
+        {
+            UserProfile profile = new DefaultUserProfile();
+            profile.setEmail("testuser@testville.com");
+            profile.setLoginName( loginName );
+            profile.setFullname( "FullName"+loginName );
+            profile.setPassword("password");
+            db.save(profile);
+
+            // Make sure the profile saved successfully
+            if ( db.getWikiNames().length == oldUserCount )
+            {
+                m_session.addMessage( ERROR_DB, "Could not add a test user to the database." );
+                return;
+            }
+            m_session.addMessage( INFO_DB, "The user database allows new users to be created, as it should." );
+        }
+        catch ( WikiSecurityException e )
+        {
+            m_session.addMessage( ERROR_DB, "Could not add a test user to the database: " + e.getMessage() );
+            return;
+        }
+
+        // Now delete the profile; should be back to old count
+        try
+        {
+            db.deleteByLoginName( loginName );
+            if ( db.getWikiNames().length != oldUserCount )
+            {
+                m_session.addMessage( ERROR_DB, "Could not delete a test user from the database." );
+                return;
+            }
+            m_session.addMessage( INFO_DB, "The user database allows users to be deleted, as it should." );
+        }
+        catch ( WikiSecurityException e )
+        {
+            m_session.addMessage( ERROR_DB, "Could not delete a test user to the database: " + e.getMessage() );
+            return;
+        }
+
+        m_session.addMessage( INFO_DB, "The user database configuration looks fine." );
+    }
+
+    /**
+     * Returns the location of the JAAS configuration file if and only if the
+     * <code>java.security.auth.login.config</code> is set <em>and</em> the
+     * file it points to exists in the file system; returns <code>null</code>
+     * in all other cases.
+     * @return the location of the JAAS configuration file
+     */
+    public final File jaasConfiguration()
+    {
+        return m_jaasConfig;
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/SessionMonitor.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/SessionMonitor.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/SessionMonitor.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/SessionMonitor.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,286 @@
+/*
+  JSPWiki - a JSP-based WikiWiki clone.
+
+  Copyright (C) 2001-2006 JSPWiki Development Team
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU Lesser General Public License as published by
+  the Free Software Foundation; either version 2.1 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+*/
+package com.ecyrd.jspwiki.auth;
+
+import java.security.Principal;
+import java.util.*;
+
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionListener;
+
+import org.apache.log4j.Logger;
+
+import com.ecyrd.jspwiki.WikiEngine;
+import com.ecyrd.jspwiki.WikiSession;
+import com.ecyrd.jspwiki.event.WikiEventListener;
+import com.ecyrd.jspwiki.event.WikiEventManager;
+import com.ecyrd.jspwiki.event.WikiSecurityEvent;
+import com.ecyrd.jspwiki.rpc.json.JSONRPCManager;
+
+/**
+ *  <p>Manages WikiSession's for different WikiEngine's.</p>
+ *  <p>The WikiSession's are stored both in the remote user
+ *  HttpSession and in the SessionMonitor for the WikeEngine.
+ *  This class must be configured as a session listener in the
+ *  web.xml for the wiki web application.
+ *  </p>
+ */
+public class SessionMonitor implements HttpSessionListener
+{
+    private static Logger log = Logger.getLogger( SessionMonitor.class );
+
+    /** Map with WikiEngines as keys, and SessionMonitors as values. */
+    private static Map          c_monitors   = new HashMap();
+
+    /** Weak hashmap with HttpSessions as keys, and WikiSessions as values. */
+    private final Map                 m_sessions   = new WeakHashMap();
+
+    private       WikiEngine          m_engine;
+
+    private final PrincipalComparator m_comparator = new PrincipalComparator();
+
+    /**
+     * Returns the instance of the SessionMonitor for this wiki.
+     * Only one SessionMonitor exists per WikiEngine.
+     * @param engine the wiki engine
+     * @return the session monitor
+     */
+    public static final SessionMonitor getInstance( WikiEngine engine )
+    {
+        if( engine == null )
+        {
+            throw new IllegalArgumentException( "Engine cannot be null." );
+        }
+        SessionMonitor monitor;
+
+        synchronized( c_monitors )
+        {
+            monitor = (SessionMonitor) c_monitors.get(engine);
+            if( monitor == null )
+            {
+                monitor = new SessionMonitor(engine);
+
+                c_monitors.put( engine, monitor );
+            }
+        }
+        return monitor;
+    }
+
+    /**
+     * Construct the SessionListener
+     */
+    public SessionMonitor()
+    {
+    }
+
+    private SessionMonitor( WikiEngine engine )
+    {
+        m_engine = engine;
+    }
+
+    /**
+     *  Just looks for a WikiSession; does not create a new one.
+     * This method may return <code>null</code>, <em>and
+     * callers should check for this value</em>.
+     *
+     *  @param session the user's HTTP session
+     *  @return the WikiSession, if found
+     */
+    private WikiSession findSession( HttpSession session )
+    {
+        WikiSession wikiSession = null;
+        String sid = ( session == null ) ? "(null)" : session.getId();
+        WikiSession storedSession = (WikiSession)m_sessions.get( sid );
+
+        // If the weak reference returns a wiki session, return it
+        if( storedSession != null )
+        {
+            if( log.isDebugEnabled() )
+            {
+                log.debug( "Looking up WikiSession for session ID=" + sid + "... found it" );
+            }
+            wikiSession = storedSession;
+        }
+
+        return wikiSession;
+    }
+    /**
+     * <p>Looks up the wiki session associated with a user's Http session
+     * and adds it to the session cache. This method will return the
+     * "guest session" as constructed by {@link WikiSession#guestSession(WikiEngine)}
+     * if the HttpSession is not currently associated with a WikiSession.
+     * This method is guaranteed to return a non-<code>null</code> WikiSession.</p>
+     * <p>Internally, the session is stored in a HashMap; keys are
+     * the HttpSession objects, while the values are
+     * {@link java.lang.ref.WeakReference}-wrapped WikiSessions.</p>
+     * @param session the HTTP session
+     * @return the wiki session
+     */
+    public final WikiSession find( HttpSession session )
+    {
+        WikiSession wikiSession = findSession(session);
+        String sid = ( session == null ) ? "(null)" : session.getId();
+
+        // Otherwise, create a new guest session and stash it.
+        if( wikiSession == null )
+        {
+            if( log.isDebugEnabled() )
+            {
+                log.debug( "Looking up WikiSession for session ID=" + sid + "... not found. Creating guestSession()" );
+            }
+            wikiSession = WikiSession.guestSession( m_engine );
+            synchronized( m_sessions )
+            {
+                m_sessions.put( sid, wikiSession );
+            }
+        }
+
+        return wikiSession;
+    }
+
+    /**
+     * Removes the wiki session associated with the user's HttpSession
+     * from the session cache.
+     * @param session the user's HTTP session
+     */
+    public final void remove( HttpSession session )
+    {
+        if ( session == null )
+        {
+            throw new IllegalArgumentException( "Session cannot be null." );
+        }
+        synchronized ( m_sessions )
+        {
+            m_sessions.remove( session.getId() );
+        }
+    }
+
+    /**
+     * Returns the current number of active wiki sessions.
+     * @return the number of sessions
+     */
+    public final int sessions()
+    {
+        return userPrincipals().length;
+    }
+
+    /**
+     * <p>Returns the current wiki users as a sorted array of
+     * Principal objects. The principals are those returned by
+     * each WikiSession's {@link WikiSession#getUserPrincipal()}'s
+     * method.</p>
+     * <p>To obtain the list of current WikiSessions, we iterate
+     * through our session Map and obtain the list of values,
+     * which are WikiSessions wrapped in {@link java.lang.ref.WeakReference}
+     * objects. Those <code>WeakReference</code>s whose <code>get()</code>
+     * method returns non-<code>null</code> values are valid
+     * sessions.</p>
+     * @return the array of user principals
+     */
+    public final Principal[] userPrincipals()
+    {
+        Collection principals = new ArrayList();
+        for ( Iterator it = m_sessions.values().iterator(); it.hasNext(); )
+        {
+            WikiSession session = (WikiSession)it.next();
+
+            principals.add( session.getUserPrincipal() );
+        }
+        Principal[] p = (Principal[])principals.toArray( new Principal[principals.size()] );
+        Arrays.sort( p, m_comparator );
+        return p;
+    }
+
+    /**
+     * Registers a WikiEventListener with this instance.
+     * @param listener the event listener
+     * @since 2.4.75
+     */
+    public final synchronized void addWikiEventListener( WikiEventListener listener )
+    {
+        WikiEventManager.addWikiEventListener( this, listener );
+    }
+
+    /**
+     * Un-registers a WikiEventListener with this instance.
+     * @param listener the event listener
+     * @since 2.4.75
+     */
+    public final synchronized void removeWikiEventListener( WikiEventListener listener )
+    {
+        WikiEventManager.removeWikiEventListener( this, listener );
+    }
+
+    /**
+     * Fires a WikiSecurityEvent to all registered listeners.
+     * @param type  the event type
+     * @param principal the user principal associated with this session
+     * @param session the wiki session
+     * @since 2.4.75
+     */
+    protected final void fireEvent( int type, Principal principal, WikiSession session )
+    {
+        if( WikiEventManager.isListening(this) )
+        {
+            WikiEventManager.fireEvent(this,new WikiSecurityEvent(this,type,principal,session));
+        }
+    }
+
+    /**
+     * Fires when the web container creates a new HTTP session.
+     * 
+     * @param se the HTTP session event
+     */
+    public void sessionCreated( HttpSessionEvent se )
+    {
+        HttpSession session = se.getSession();
+        
+        JSONRPCManager.sessionCreated(session);
+    }
+
+    /**
+     * Removes the user's WikiSession from the internal session cache when the web
+     * container destoys an HTTP session.
+     * @param se the HTTP session event
+     */
+    public void sessionDestroyed( HttpSessionEvent se )
+    {
+        HttpSession session = se.getSession();
+        Iterator it = c_monitors.values().iterator();
+        while( it.hasNext() )
+        {
+            SessionMonitor monitor = (SessionMonitor)it.next();
+
+            WikiSession storedSession = monitor.findSession(session);
+
+            monitor.remove(session);
+
+            log.debug("Removed session "+session.getId()+".");
+
+            if( storedSession != null )
+            {
+                fireEvent( WikiSecurityEvent.SESSION_EXPIRED,
+                           storedSession.getLoginPrincipal(),
+                           storedSession );
+            }
+        }
+    }
+}

Added: incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/UserManager.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/UserManager.java?rev=627255&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/UserManager.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_STRIPES_BRANCH/src/com/ecyrd/jspwiki/auth/UserManager.java Tue Feb 12 21:53:55 2008
@@ -0,0 +1,836 @@
+/*
+  JSPWiki - a JSP-based WikiWiki clone.
+
+  Copyright (C) 2001-2005 Janne Jalkanen (Janne.Jalkanen@iki.fi)
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU Lesser General Public License as published by
+  the Free Software Foundation; either version 2.1 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+*/
+package com.ecyrd.jspwiki.auth;
+
+import java.security.Permission;
+import java.security.Principal;
+import java.text.MessageFormat;
+import java.util.*;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.AddressException;
+import javax.servlet.http.HttpServletRequest;
+
+import net.sourceforge.stripes.action.UrlBinding;
+
+import org.apache.log4j.Logger;
+
+import com.ecyrd.jspwiki.*;
+import com.ecyrd.jspwiki.action.LoginActionBean;
+import com.ecyrd.jspwiki.auth.permissions.AllPermission;
+import com.ecyrd.jspwiki.auth.permissions.WikiPermission;
+import com.ecyrd.jspwiki.auth.user.AbstractUserDatabase;
+import com.ecyrd.jspwiki.auth.user.DuplicateUserException;
+import com.ecyrd.jspwiki.auth.user.UserDatabase;
+import com.ecyrd.jspwiki.auth.user.UserProfile;
+import com.ecyrd.jspwiki.event.WikiEventListener;
+import com.ecyrd.jspwiki.event.WikiEventManager;
+import com.ecyrd.jspwiki.event.WikiSecurityEvent;
+import com.ecyrd.jspwiki.filters.PageFilter;
+import com.ecyrd.jspwiki.filters.SpamFilter;
+import com.ecyrd.jspwiki.i18n.InternationalizationManager;
+import com.ecyrd.jspwiki.rpc.RPCCallable;
+import com.ecyrd.jspwiki.rpc.json.JSONRPCManager;
+import com.ecyrd.jspwiki.ui.InputValidator;
+import com.ecyrd.jspwiki.util.ClassUtil;
+import com.ecyrd.jspwiki.util.MailUtil;
+import com.ecyrd.jspwiki.workflow.*;
+
+/**
+ * Provides a facade for obtaining user information.
+ * @author Janne Jalkanen
+ * @author Andrew Jaquith
+ * @since 2.3
+ */
+public final class UserManager
+{
+    private static final String USERDATABASE_PACKAGE = "com.ecyrd.jspwiki.auth.user";
+    private static final String SESSION_MESSAGES = "profile";
+    private static final String PARAM_EMAIL = "email";
+    private static final String PARAM_FULLNAME = "fullname";
+    private static final String PARAM_PASSWORD = "password";
+    private static final String PARAM_LOGINNAME = "loginname";
+    private static final String UNKNOWN_CLASS = "<unknown>";
+
+    private WikiEngine m_engine;
+
+    private static Logger log = Logger.getLogger(UserManager.class);
+
+    /** Message key for the "save profile" message. */
+    public  static final String SAVE_APPROVER               = "workflow.createUserProfile";
+    private static final String PROP_DATABASE               = "jspwiki.userdatabase";
+    protected static final String SAVE_TASK_MESSAGE_KEY     = "task.createUserProfile";
+    protected static final String SAVED_PROFILE             = "userProfile";
+    protected static final String SAVE_DECISION_MESSAGE_KEY = "decision.createUserProfile";
+    protected static final String FACT_SUBMITTER            = "fact.submitter";
+    protected static final String PREFS_LOGIN_NAME          = "prefs.loginname";
+    protected static final String PREFS_FULL_NAME           = "prefs.fullname";
+    protected static final String PREFS_EMAIL               = "prefs.email";
+
+    // private static final String  PROP_ACLMANAGER     = "jspwiki.aclManager";
+
+    /** Associateds wiki sessions with profiles */
+    private final Map<WikiSession,UserProfile> m_profiles = new WeakHashMap<WikiSession,UserProfile>(); 
+
+    /** The user database loads, manages and persists user identities */
+    private UserDatabase     m_database;
+
+    private boolean          m_useJAAS      = true;
+
+    /**
+     * Constructs a new UserManager instance.
+     */
+    public UserManager()
+    {
+    }
+
+    /**
+     * Initializes the engine for its nefarious purposes.
+     * @param engine the current wiki engine
+     * @param props the wiki engine initialization properties
+     */
+    public final void initialize( WikiEngine engine, Properties props )
+    {
+        m_engine = engine;
+
+        m_useJAAS = AuthenticationManager.SECURITY_JAAS.equals( props.getProperty(AuthenticationManager.PROP_SECURITY, AuthenticationManager.SECURITY_JAAS ) );
+
+        // Attach the PageManager as a listener
+        // TODO: it would be better if we did this in PageManager directly
+        addWikiEventListener( engine.getPageManager() );
+
+        JSONRPCManager.registerGlobalObject( "users", new JSONUserModule(), new AllPermission(null) );
+    }
+
+    /**
+     * Returns the UserDatabase employed by this WikiEngine. The UserDatabase is
+     * lazily initialized by this method, if it does not exist yet. If the
+     * initialization fails, this method will use the inner class
+     * DummyUserDatabase as a default (which is enough to get JSPWiki running).
+     * @return the dummy user database
+     * @since 2.3
+     */
+    public final UserDatabase getUserDatabase()
+    {
+        // FIXME: Must not throw RuntimeException, but something else.
+        if( m_database != null )
+        {
+            return m_database;
+        }
+
+        if( !m_useJAAS )
+        {
+            m_database = new DummyUserDatabase();
+            return m_database;
+        }
+
+        String dbClassName = UNKNOWN_CLASS;
+
+        try
+        {
+            dbClassName = WikiEngine.getRequiredProperty( m_engine.getWikiProperties(),
+                                                          PROP_DATABASE );
+
+            log.info("Attempting to load user database class "+dbClassName);
+            Class dbClass = ClassUtil.findClass( USERDATABASE_PACKAGE, dbClassName );
+            m_database = (UserDatabase) dbClass.newInstance();
+            m_database.initialize( m_engine, m_engine.getWikiProperties() );
+            log.info("UserDatabase initialized.");
+        }
+        catch( NoRequiredPropertyException e )
+        {
+            log.error( "You have not set the '"+PROP_DATABASE+"'. You need to do this if you want to enable user management by JSPWiki." );
+        }
+        catch( ClassNotFoundException e )
+        {
+            log.error( "UserDatabase class " + dbClassName + " cannot be found", e );
+        }
+        catch( InstantiationException e )
+        {
+            log.error( "UserDatabase class " + dbClassName + " cannot be created", e );
+        }
+        catch( IllegalAccessException e )
+        {
+            log.error( "You are not allowed to access this user database class", e );
+        }
+        finally
+        {
+            if( m_database == null )
+            {
+                log.info("I could not create a database object you specified (or didn't specify), so I am falling back to a default.");
+                m_database = new DummyUserDatabase();
+            }
+        }
+
+        return m_database;
+    }
+
+    /**
+     * <p>Retrieves the {@link com.ecyrd.jspwiki.auth.user.UserProfile}for the
+     * user in a wiki session. If the user is authenticated, the UserProfile
+     * returned will be the one stored in the user database; if one does not
+     * exist, a new one will be initialized and returned. If the user is
+     * anonymous or asserted, the UserProfile will <i>always</i> be newly
+     * initialized to prevent spoofing of identities. If a UserProfile needs to
+     * be initialized, its
+     * {@link com.ecyrd.jspwiki.auth.user.UserProfile#isNew()} method will
+     * return <code>true</code>, and its login name will will be set
+     * automatically if the user is authenticated. Note that this method does
+     * not modify the retrieved (or newly created) profile otherwise; other
+     * fields in the user profile may be <code>null</code>.</p>
+     * <p>If a new UserProfile was created, but its
+     * {@link com.ecyrd.jspwiki.auth.user.UserProfile#isNew()} method returns
+     * <code>false</code>, this method throws an {@link IllegalStateException}.
+     * This is meant as a quality check for UserDatabase providers;
+     * it should only be thrown if the implementation is faulty.</p>
+     * @param session the wiki session, which may not be <code>null</code>
+     * @return the user's profile, which will be newly initialized if the user
+     * is anonymous or asserted, or if the user cannot be found in the user
+     * database
+     */
+    public final UserProfile getUserProfile( WikiSession session )
+    {
+        // Look up cached user profile
+        UserProfile profile = m_profiles.get( session );
+        boolean newProfile = profile == null;
+        Principal user = null;
+
+        // If user is authenticated, figure out if this is an existing profile
+        if ( session.isAuthenticated() )
+        {
+            user = session.getUserPrincipal();
+            try
+            {
+                profile = getUserDatabase().find( user.getName() );
+                newProfile = false;
+            }
+            catch( NoSuchPrincipalException e )
+            {
+            }
+        }
+
+        if ( newProfile )
+        {
+            profile = getUserDatabase().newProfile();
+            if ( user != null )
+            {
+                profile.setLoginName( user.getName() );
+            }
+            if ( !profile.isNew() )
+            {
+                throw new IllegalStateException(
+                        "New profile should be marked 'new'. Check your UserProfile implementation." );
+            }
+        }
+
+        // Stash the profile for next time
+        m_profiles.put( session, profile );
+        return profile;
+    }
+
+    /**
+     * <p>
+     * Saves the {@link com.ecyrd.jspwiki.auth.user.UserProfile}for the user in
+     * a wiki session. This method verifies that a user profile to be saved
+     * doesn't collide with existing profiles; that is, the login name
+     * or full name is already used by another profile. If the profile
+     * collides, a <code>DuplicateUserException</code> is thrown. After saving
+     * the profile, the user database changes are committed, and the user's
+     * credential set is refreshed; if custom authentication is used, this means
+     * the user will be automatically be logged in.
+     * </p>
+     * <p>
+     * When the user's profile is saved succcessfully, this method fires a
+     * {@link WikiSecurityEvent#PROFILE_SAVE} event with the WikiSession as the
+     * source and the UserProfile as target. For existing profiles, if the
+     * user's full name changes, this method also fires a "name changed"
+     * event ({@link WikiSecurityEvent#PROFILE_NAME_CHANGED}) with the
+     * WikiSession as the source and an array containing the old and new
+     * UserProfiles, respectively. The <code>NAME_CHANGED</code> event allows
+     * the GroupManager and PageManager can change group memberships and
+     * ACLs if needed.
+     * </p>
+     * <p>
+     * Note that WikiSessions normally attach event listeners to the
+     * UserManager, so changes to the profile will automatically cause the
+     * correct Principals to be reloaded into the current WikiSession's Subject.
+     * </p>
+     * @param session the wiki session, which may not be <code>null</code>
+     * @param profile the user profile, which may not be <code>null</code>
+     * @throws DuplicateUserException if the proposed profile's login name or full name collides with another
+     * @throws WikiException if the save fails for some reason. If the current user does not have
+     * permission to save the profile, this will be a {@link com.ecyrd.jspwiki.auth.WikiSecurityException};
+     * if if the user profile must be approved before it can be saved, it will be a
+     * {@link com.ecyrd.jspwiki.workflow.DecisionRequiredException}. All other WikiException
+     * indicate a condition that is not normal is probably due to mis-configuration
+     */
+    public final void setUserProfile( WikiSession session, UserProfile profile ) throws DuplicateUserException, WikiException
+    {
+        // Verify user is allowed to save profile!
+        Permission p = new WikiPermission( m_engine.getApplicationName(), WikiPermission.EDIT_PROFILE_ACTION );
+        if ( !m_engine.getAuthorizationManager().checkPermission( session, p ) )
+        {
+            throw new WikiSecurityException( "You are not allowed to save wiki profiles." );
+        }
+
+        // Check if profile is new, and see if container allows creation
+        boolean newProfile = profile.isNew();
+
+        // Check if another user profile already has the fullname or loginname
+        UserProfile oldProfile = getUserProfile( session );
+        boolean nameChanged = ( oldProfile == null  || oldProfile.getFullname() == null )
+            ? false
+            : !( oldProfile.getFullname().equals( profile.getFullname() ) &&
+                 oldProfile.getLoginName().equals( profile.getLoginName() ) );
+        UserProfile otherProfile;
+        try
+        {
+            otherProfile = getUserDatabase().findByLoginName( profile.getLoginName() );
+            if ( otherProfile != null && !otherProfile.equals( oldProfile ) )
+            {
+                throw new DuplicateUserException( "The login name '" + profile.getLoginName() + "' is already taken." );
+            }
+        }
+        catch( NoSuchPrincipalException e )
+        {
+        }
+        try
+        {
+            otherProfile = getUserDatabase().findByFullName( profile.getFullname() );
+            if ( otherProfile != null && !otherProfile.equals( oldProfile ) )
+            {
+                throw new DuplicateUserException( "The full name '" + profile.getFullname() + "' is already taken." );
+            }
+        }
+        catch( NoSuchPrincipalException e )
+        {
+        }
+
+        // For new accounts, create approval workflow for user profile save.
+        if ( newProfile && oldProfile != null && oldProfile.isNew() )
+        {
+            WorkflowBuilder builder = WorkflowBuilder.getBuilder( m_engine );
+            Principal submitter = session.getUserPrincipal();
+            Task completionTask = new SaveUserProfileTask( m_engine );
+
+            // Add user profile attribute as Facts for the approver (if required)
+            boolean hasEmail = profile.getEmail() != null;
+            Fact[] facts = new Fact[ hasEmail ? 4 : 3];
+            facts[0] = new Fact( PREFS_FULL_NAME, profile.getFullname() );
+            facts[1] = new Fact( PREFS_LOGIN_NAME, profile.getLoginName() );
+            facts[2] = new Fact( FACT_SUBMITTER, submitter.getName() );
+            if ( hasEmail )
+            {
+                facts[3] = new Fact( PREFS_EMAIL, profile.getEmail() );
+            }
+            Workflow workflow = builder.buildApprovalWorkflow( submitter,
+                                                               SAVE_APPROVER,
+                                                               null,
+                                                               SAVE_DECISION_MESSAGE_KEY,
+                                                               facts,
+                                                               completionTask,
+                                                               null );
+
+            workflow.setAttribute( SAVED_PROFILE, profile );
+            m_engine.getWorkflowManager().start(workflow);
+
+            boolean approvalRequired = workflow.getCurrentStep() instanceof Decision;
+
+            // If the profile requires approval, redirect user to message page
+            if ( approvalRequired )
+            {
+                throw new DecisionRequiredException( "This profile must be approved before it becomes active" );
+            }
+
+            // If the profile doesn't need approval, then just log the user in
+
+            try
+            {
+                AuthenticationManager mgr = m_engine.getAuthenticationManager();
+                if ( newProfile && !mgr.isContainerAuthenticated() )
+                {
+                    mgr.login( session, profile.getLoginName(), profile.getPassword() );
+                }
+            }
+            catch ( WikiException e )
+            {
+                throw new WikiSecurityException( e.getMessage() );
+            }
+
+            // Alert all listeners that the profile changed...
+            // ...this will cause credentials to be reloaded in the wiki session
+            fireEvent( WikiSecurityEvent.PROFILE_SAVE, session, profile );
+        }
+
+        // For existing accounts, just save the profile
+        else
+        {
+            // If login name changed, rename it first
+            if ( nameChanged && oldProfile != null && !oldProfile.getLoginName().equals( profile.getLoginName() ) )
+            {
+                getUserDatabase().rename( oldProfile.getLoginName(), profile.getLoginName() );
+            }
+
+            // Now, save the profile (userdatabase will take care of timestamps for us)
+            getUserDatabase().save( profile );
+
+            if ( nameChanged )
+            {
+                // Fire an event if the login name or full name changed
+                UserProfile[] profiles = new UserProfile[] { oldProfile, profile };
+                fireEvent( WikiSecurityEvent.PROFILE_NAME_CHANGED, session, profiles );
+            }
+            else
+            {
+                // Fire an event that says we have new a new profile (new principals)
+                fireEvent( WikiSecurityEvent.PROFILE_SAVE, session, profile );
+            }
+        }
+    }
+
+    /**
+     * <p> Extracts user profile parameters from the HTTP request and populates
+     * a UserProfile with them. The UserProfile will either be a copy of the
+     * user's existing profile (if one can be found), or a new profile (if not).
+     * The rules for populating the profile as as follows: </p> <ul> <li>If the
+     * <code>email</code> or <code>password</code> parameter values differ
+     * from those in the existing profile, the passed parameters override the
+     * old values.</li> <li>For new profiles, the user-supplied
+     * <code>fullname</code parameter is always
+     * used; for existing profiles the existing value is used, and whatever
+     * value the user supplied is discarded. The wiki name is automatically
+     * computed by taking the full name and extracting all whitespace.</li>
+     * <li>In all cases, the
+     * created/last modified timestamps of the user's existing or new profile
+     * always override whatever values the user supplied.</li> <li>If
+     * container authentication is used, the login name property of the profile
+     * is set to the name of
+     * {@link com.ecyrd.jspwiki.WikiSession#getLoginPrincipal()}. Otherwise,
+     * the value of the <code>loginname</code> parameter is used.</li> </ul>
+     * @param context the current wiki context
+     * @return a new, populated user profile
+     */
+    public final UserProfile parseProfile( WikiContext context )
+    {
+        // Retrieve the user's profile (may have been previously cached)
+        UserProfile profile = getUserProfile( context.getWikiSession() );
+        HttpServletRequest request = context.getContext().getRequest();
+
+        // Extract values from request stream (cleanse whitespace as needed)
+        String loginName = request.getParameter( PARAM_LOGINNAME );
+        String password = request.getParameter( PARAM_PASSWORD );
+        String fullname = request.getParameter( PARAM_FULLNAME );
+        String email = request.getParameter( PARAM_EMAIL );
+        loginName = InputValidator.isBlank( loginName ) ? null : loginName;
+        password = InputValidator.isBlank( password ) ? null : password;
+        fullname = InputValidator.isBlank( fullname ) ? null : fullname;
+        email = InputValidator.isBlank( email ) ? null : email;
+
+        // A special case if we have container authentication
+        if ( m_engine.getAuthenticationManager().isContainerAuthenticated() )
+        {
+            // If authenticated, login name is always taken from container
+            if ( context.getWikiSession().isAuthenticated() )
+            {
+                loginName = context.getWikiSession().getLoginPrincipal().getName();
+            }
+        }
+
+        // Set the profile fields!
+        profile.setLoginName( loginName );
+        profile.setEmail( email );
+        profile.setFullname( fullname );
+        profile.setPassword( password );
+        return profile;
+    }
+
+    /**
+     * Validates a user profile, and appends any errors to the session errors
+     * list. If the profile is new, the password will be checked to make sure it
+     * isn't null. Otherwise, the password is checked for length and that it
+     * matches the value of the 'password2' HTTP parameter. Note that we have a
+     * special case when container-managed authentication is used and the user
+     * is not authenticated; this will always cause validation to fail. Any
+     * validation errors are added to the wiki session's messages collection
+     * (see {@link WikiSession#getMessages()}.
+     * @param context the current wiki context
+     * @param profile the supplied UserProfile
+     * @deprecated
+     */
+    public final void validateProfile( WikiContext context, UserProfile profile )
+    {
+        boolean isNew = profile.isNew();
+        WikiSession session = context.getWikiSession();
+        InputValidator validator = new InputValidator( SESSION_MESSAGES, session );
+        ResourceBundle rb = context.getBundle( InternationalizationManager.CORE_BUNDLE );
+
+        //
+        //  Query the SpamFilter first
+        //
+        
+        List ls = m_engine.getFilterManager().getFilterList();
+        for( Iterator i = ls.iterator(); i.hasNext(); )
+        {
+            PageFilter pf = (PageFilter)i.next();
+            
+            if( pf instanceof SpamFilter )
+            {
+                if( ((SpamFilter)pf).isValidUserProfile( context, profile ) == false )
+                {
+                    session.addMessage( SESSION_MESSAGES, "Invalid userprofile" );
+                    return;
+                }
+                break;
+            }
+        }
+        
+        // If container-managed auth and user not logged in, throw an error
+        // unless we're allowed to add profiles to the container
+        if ( m_engine.getAuthenticationManager().isContainerAuthenticated()
+             && !context.getWikiSession().isAuthenticated()
+             && !getUserDatabase().isSharedWithContainer() )
+        {
+            session.addMessage( SESSION_MESSAGES, rb.getString("security.error.createprofilebeforelogin") );
+        }
+
+        validator.validateNotNull( profile.getLoginName(), rb.getString("security.user.loginname") );
+        validator.validateNotNull( profile.getFullname(), rb.getString("security.user.fullname") );
+        validator.validate( profile.getEmail(), rb.getString("security.user.email"), InputValidator.EMAIL );
+
+        // If new profile, passwords must match and can't be null
+        if ( !m_engine.getAuthenticationManager().isContainerAuthenticated() )
+        {
+            String password = profile.getPassword();
+            if ( password == null )
+            {
+                if ( isNew )
+                {
+                    session.addMessage( SESSION_MESSAGES, rb.getString("security.error.blankpassword") );
+                }
+            }
+            else
+            {
+                HttpServletRequest request = context.getHttpRequest();
+                String password2 = ( request == null ) ? null : request.getParameter( "password2" );
+                if ( !password.equals( password2 ) )
+                {
+                    session.addMessage( SESSION_MESSAGES, rb.getString("security.error.passwordnomatch") );
+                }
+            }
+        }
+
+        UserProfile otherProfile;
+        String fullName = profile.getFullname();
+        String loginName = profile.getLoginName();
+
+        // It's illegal to use as a full name someone else's login name
+        try
+        {
+            otherProfile = getUserDatabase().find( fullName );
+            if ( otherProfile != null && !profile.equals( otherProfile ) && !fullName.equals( otherProfile.getFullname() ) )
+            {
+                Object[] args = { fullName };
+                session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString("security.error.illegalfullname"),
+                                                                            args ) );
+            }
+        }
+        catch ( NoSuchPrincipalException e)
+        { /* It's clean */ }
+
+        // It's illegal to use as a login name someone else's full name
+        try
+        {
+            otherProfile = getUserDatabase().find( loginName );
+            if ( otherProfile != null && !profile.equals( otherProfile ) && !loginName.equals( otherProfile.getLoginName() ) )
+            {
+                Object[] args = { loginName };
+                session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString("security.error.illegalloginname"),
+                                                                            args ) );
+            }
+        }
+        catch ( NoSuchPrincipalException e)
+        { /* It's clean */ }
+    }
+
+    public Principal[] listWikiNames()
+        throws WikiSecurityException
+    {
+        return getUserDatabase().getWikiNames();
+    }
+
+    /**
+     * This is a database that gets used if nothing else is available. It does
+     * nothing of note - it just mostly thorws NoSuchPrincipalExceptions if
+     * someone tries to log in.
+     * @author Janne Jalkanen
+     */
+    public static class DummyUserDatabase extends AbstractUserDatabase
+    {
+
+        /**
+         * No-op.
+         * @throws WikiSecurityException never...
+         */
+        public void commit() throws WikiSecurityException
+        {
+            // No operation
+        }
+
+        /**
+         * No-op.
+         * @param loginName the login name to delete
+         * @throws WikiSecurityException never...
+         */
+        public void deleteByLoginName( String loginName ) throws WikiSecurityException
+        {
+            // No operation
+        }
+
+        /**
+         * No-op; always throws <code>NoSuchPrincipalException</code>.
+         * @param index the name to search for
+         * @return the user profile
+         * @throws NoSuchPrincipalException never...
+         */
+        public UserProfile findByEmail(String index) throws NoSuchPrincipalException
+        {
+            throw new NoSuchPrincipalException("No user profiles available");
+        }
+
+        /**
+         * No-op; always throws <code>NoSuchPrincipalException</code>.
+         * @param index the name to search for
+         * @return the user profile
+         * @throws NoSuchPrincipalException never...
+         */
+        public UserProfile findByFullName(String index) throws NoSuchPrincipalException
+        {
+            throw new NoSuchPrincipalException("No user profiles available");
+        }
+
+        /**
+         * No-op; always throws <code>NoSuchPrincipalException</code>.
+         * @param index the name to search for
+         * @return the user profile
+         * @throws NoSuchPrincipalException never...
+         */
+        public UserProfile findByLoginName(String index) throws NoSuchPrincipalException
+        {
+            throw new NoSuchPrincipalException("No user profiles available");
+        }
+
+        /**
+         * No-op; always throws <code>NoSuchPrincipalException</code>.
+         * @param index the name to search for
+         * @return the user profile
+         * @throws NoSuchPrincipalException never...
+         */
+        public UserProfile findByWikiName(String index) throws NoSuchPrincipalException
+        {
+            throw new NoSuchPrincipalException("No user profiles available");
+        }
+
+        /**
+         * No-op.
+         * @return a zero-length array
+         * @throws WikiSecurityException never...
+         */
+        public Principal[] getWikiNames() throws WikiSecurityException
+        {
+            return new Principal[0];
+        }
+
+        /**
+         * No-op.
+         * @param engine the wiki engine
+         * @param props the properties used to initialize the wiki engine
+         * @throws NoRequiredPropertyException never...
+         */
+        public void initialize(WikiEngine engine, Properties props) throws NoRequiredPropertyException
+        {
+        }
+
+        /**
+         * No-op.
+         * @return <code>false</code>
+         */
+        public boolean isSharedWithContainer()
+        {
+            return false;
+        }
+
+        /**
+         * No-op; always throws <code>NoSuchPrincipalException</code>.
+         * @param loginName the login name
+         * @param newName the proposed new login name
+         * @throws DuplicateUserException never...
+         * @throws WikiSecurityException never...
+         */
+        public void rename( String loginName, String newName ) throws DuplicateUserException, WikiSecurityException
+        {
+            throw new NoSuchPrincipalException("No user profiles available");
+        }
+
+        /**
+         * No-op.
+         * @param profile the user profile
+         * @throws WikiSecurityException never...
+         */
+        public void save( UserProfile profile ) throws WikiSecurityException
+        {
+        }
+
+    }
+
+    // workflow task inner classes....................................................
+
+    /**
+     * Inner class that handles the actual profile save action. Instances
+     * of this class are assumed to have been added to an approval workflow via
+     * {@link com.ecyrd.jspwiki.workflow.WorkflowBuilder#buildApprovalWorkflow(Principal, String, Task, String, com.ecyrd.jspwiki.workflow.Fact[], Task, String)};
+     * they will not function correctly otherwise.
+     *
+     * @author Andrew Jaquith
+     */
+    public static class SaveUserProfileTask extends Task
+    {
+        private final UserDatabase m_db;
+        private final WikiEngine m_engine;
+
+        /**
+         * Constructs a new Task for saving a user profile.
+         * @param engine the wiki engine
+         */
+        public SaveUserProfileTask( WikiEngine engine )
+        {
+            super( SAVE_TASK_MESSAGE_KEY );
+            m_engine = engine;
+            m_db = engine.getUserManager().getUserDatabase();
+        }
+
+        /**
+         * Saves the user profile to the user database.
+         * @return {@link com.ecyrd.jspwiki.workflow.Outcome#STEP_COMPLETE} if the
+         * task completed successfully
+         * @throws WikiException if the save did not complete for some reason
+         */
+        public Outcome execute() throws WikiException
+        {
+            // Retrieve user profile
+            UserProfile profile = (UserProfile) getWorkflow().getAttribute( SAVED_PROFILE );
+
+            // Save the profile (userdatabase will take care of timestamps for us)
+            m_db.save( profile );
+
+            // Send e-mail if user supplied an e-mail address
+            if ( profile.getEmail() != null )
+            {
+                try
+                {
+                    String app = m_engine.getApplicationName();
+                    String to = profile.getEmail();
+                    String subject = "Welcome to " + app;
+                    String content = "Congratulations! Your new profile on "
+                        + app + " has been created. Your profile details are as follows: \n\n"
+                        + "Login name: " + profile.getLoginName() + "\n"
+                        + "Your name : " + profile.getFullname() + "\n"
+                        + "E-mail    : " + profile.getEmail() + "\n\n"
+                        + "If you forget your password, you can reset it at "
+                        + m_engine.getBaseURL() + LoginActionBean.class.getAnnotation(UrlBinding.class).value();
+                    MailUtil.sendMessage( m_engine, to, subject, content);
+                }
+                catch ( AddressException e)
+                {
+                }
+                catch ( MessagingException e )
+                {
+                    log.error( "Could not send registration confirmation e-mail. Is the e-mail server running?" );
+                }
+            }
+
+            return Outcome.STEP_COMPLETE;
+        }
+    }
+
+    // events processing .......................................................
+
+    /**
+     * Registers a WikiEventListener with this instance.
+     * This is a convenience method.
+     * @param listener the event listener
+     */
+    public final synchronized void addWikiEventListener( WikiEventListener listener )
+    {
+        WikiEventManager.addWikiEventListener( this, listener );
+    }
+
+    /**
+     * Un-registers a WikiEventListener with this instance.
+     * This is a convenience method.
+     * @param listener the event listener
+     */
+    public final synchronized void removeWikiEventListener( WikiEventListener listener )
+    {
+        WikiEventManager.removeWikiEventListener( this, listener );
+    }
+
+    /**
+     *  Fires a WikiSecurityEvent of the provided type, Principal and target Object
+     *  to all registered listeners.
+     *
+     * @see com.ecyrd.jspwiki.event.WikiSecurityEvent
+     * @param type       the event type to be fired
+     * @param session    the wiki session supporting the event
+     * @param profile    the user profile (or array of user profiles), which may be <code>null</code>
+     */
+    protected final void fireEvent( int type, WikiSession session, Object profile )
+    {
+        if ( WikiEventManager.isListening(this) )
+        {
+            WikiEventManager.fireEvent(this,new WikiSecurityEvent(session,type,profile));
+        }
+    }
+
+    /**
+     *  Implements the JSON API for usermanager.
+     *
+     *  @author Janne Jalkanen
+     */
+    public final class JSONUserModule implements RPCCallable
+    {
+        /**
+         *  Directly returns the UserProfile object attached to an uid.
+         *
+         *  @param uid The user id (e.g. WikiName)
+         *  @return A UserProfile object
+         *  @throws NoSuchPrincipalException If such a name does not exist.
+         */
+        public UserProfile getUserInfo( String uid )
+            throws NoSuchPrincipalException
+        {
+            log.info("request "+uid);
+            UserProfile prof = getUserDatabase().find( uid );
+
+            log.info("answer "+prof);
+
+            return prof;
+        }
+    }
+}
\ No newline at end of file