Name

Externalize User Management

Status

Withdrawn from consideration

Target Release

Roller Weblogger 4.1

Original Authors

Dave Johnson

Proposal to make it possible to externalize user management so that Roller can pull user profile and role information from a separate user management system.

1.0 Abstract

For ease of installation and management, Roller is able to manage it's own users without relying on any external system other than its RDBMS. We definitely don't want to lose that ability, but as Roller moves into enterprise scenarios where Directory Servers rule and social networking scenarios where user profile information is key we need to make some changes. For Roller to be sucessful in large organizations and social networks, we need to make it easy to integrate Roller with existing user management systems. The way to do that is to externalize user management, or rather to make it externalizable.

This proposal outlines a plan to make it easy to hook Roller up to an external user management system for user information, user profiles and user roles. The general approach is to define a UserRepostory API, provide a default implementation for Roller, and change UserManager to use that API. Developer could then provide alternative implementations of that API to plug in their own user management systems. Also, to allow more authentication options, we should make it possible to configure Roller to use CMA instead of Acegi.

2.0 Requirements

Proposal satisfies these requirements.

  • Enable Roller to optionally read/write user profile information in an external system
  • Enable Roller to optionally read/write user role information in an external system
  • Increase the number of authentication/authorization options available in Roller by making it possible to configure Container Managed Authentication (CMA) and not only Acegi.
  • Define a User Management API and make it possible to plugin User Management API implementations without having to extend a UserManager implementation.

3.0 Issues

Issues raised and addressed during review process. TBD.

4.0 Background and Design

To understand this proposal you need to understand how Roller's existing user management system works. So here's an explanation of Roller's current system, the perceived problems and proposed solutions.

4.1 Roller manages it's own users and roles

So that it can stand-alone without an external user repository, Roller stores users and role information in it's own database tables. These tables, known as rolleruser and userrole are shown below (using MySQL DDL syntax).

create table rolleruser (
   id              varchar(48) not null primary key,
   username        varchar(255) not null,
   passphrase      varchar(255) not null,
   screenname      varchar(255) not null,
   fullname        varchar(255) not null,
   emailaddress    varchar(255) not null,
   activationcode	varchar(48),
   datecreated     timestamp not null,
   locale          varchar(20),  
   timezone        varchar(50),    
   isenabled       boolean not null
)
create table userrole (
   id               varchar(48) not null primary key,
   rolename         varchar(255) not null,
   username         varchar(255) not null,
   userid           varchar(48) not null
)

Currently, there are two distinct rolenames used in the userrole table:

  • editor: user able to login and see main menu
  • admin: user is able edit site-wide settings and any blog in system

Those tables are represented in Roller by the User and UserRole objects, which are POJOs stored by Roller's UserManager via Object Relational Mapping (ORM) technology (i.e. Apache OpenJPA).

4.1.1 The problem

For sites that have an external user repository or user permissions system, this is a problem. Some would like user information like email address, locale, timezone, fullname and etc. pulled from an external system. Some would like user's roles to be pulled from an external system. Some would like both.

2.1.2 Possible solutions

The overall solution is to modify Roller so that it's able to pull user profile data from an external system via a UserRepository API that we define. But there are at least a couple of different ways to do that. We could define a UserProfile class to augment the User class, add profile data as name/value pairs or simply add new fields to the User object. Let's examine each of those three possible solutions.

Profile solution 1: UserProfile class

Introduce UserProfile class to represent extended user profile data and move all extended profile information (i.e. everything except userid and username) to that UserProfile object.

We still need the User object because it's used in joins and queries. Profile data will still be available from the User object via a user.getProfile() method, but we'll have to take care of "joining" users to profiles either at fetch time or when a user calls user.getProfile().

Roller's UserManager would use a pluggable UserRepository to retrieve UserProfile objects like the one below. The Roller implementation of this would simply fetch profile data from a rol_userprofile table and other implementations can fetch the data from other systems.

UserRespository interface method for UserProfile solution

public UserProfile getUserProfile(String username)
public void saveUserProfile(UserProfile userProfile)

UserProfile bean properties

username
password
screenName
emailAddress
locale
timezone
biography
etc.

Profile solution 2: UserAttribute class

Introduce UserAttribute class to represent extended user profile data and move all extended profile information (i.e. everything except userid and username) into UserAttributes. This would enable us to add arbitrary name/value pairs to each user object.

We still need the User object because it's used in joins and queries. User profile data would be available from the User object via a user.getProfileAttribute(name) method, but we'll have to take care of "joining" users to profiles either at fetch time or when a user calls user.getProfile().

Roller's UserManager would use a pluggable UserRepository to retrieve UserProfile objects like the one below. The Roller implementation of this would simply fetch profile data from a rol_userprofile table and other implementations can fetch the data from other systems.

UserRespository interface methods for UserAttribute solution

/** Get set of profile attributes names associated with a user */
public Set<String> getUserProfileAttributeNames(String username);

/** Get set of profile attributes associated with a user */
public Set<UserAttribute> getUserProfile(String username)

/** Set specified profile attribute for user */
public void saveUserProfile(String username, Set<UserAttribute>)

/** Save specified profile attribute for user */
public void saveUserProfileAttribute(String uname, String name, String value)

UserAttribute bean properties

name
value

Profile solution 3: add to User class

Instead of adding a new profile class or attributes, just add the extended profile data fields directly to the User object.

UserRespository interface method for UserProfile solution

public User getUser(String username)
public void saveUser(User user)

We need to decide which approach to take, or come up with new ones. Now, let's move from user profiles to user authentication.

4.2 Roller authentication is managed via Acegi

Roller uses a framework called Acegi to handle authentication and authorization. Instead of relying on the authentication and authorization features built into the container on which Roller runs, Roller relies on Acegi.

When Acegi is authenticating a user it pulls username, password and role information from the RollerUserDetailsService, which in turn fetches that information from Roller's User and UserRole POJOs via UserManager.

Acegi is implemented as a Servlet Filter, which intercepts each request and decides if the user is authenticated, needs to login first, etc. Acegi takes care of routing the user to the login page and back to the original page that the user requested. Acegi wraps the ServletRequest so that it can return the
right value when request.getUserPrincipal() is called by the application.

4.2.1 The problem

Using Acegi makes Roller installation painless in standalone situations, but some Acegi skills are required to reconfigure Roller to authenticate against an LDAP system. Unfortunately, the only SSO system that Acegi supports out of the box is Yale CAS. Plus, some folks would like to disable Acegi to take advantage of the powerful auth & auth services that are built into containers now, e.g. SSO support that's built into Glassfish, Websphere, JBoss, etc.

4.2.2 The Solution

Make it possible to turn off Acegi by modifying web.xml and to extend the Roller application class RollerContext to init without Acegi. Moving forward, we should not introduce further dependencies on Acegi in Roller.

Here's some notes on what would have to happen to make CMA optional.

  • Make RollerContext into an abstract base class so that we can have a Acegi
    version and a non-Acegi version that share the same base logic.
  • Add a 'security.containerManaged' property which defaults to off.
  • Add logic to the login page to ensure that login credentials are posted to the correct URI for either CMA or Acegi.
  • Add a section to the installation guide that explains how to modify web.xml to remove the Acegi filter and add the right XML incantation for CMA.
  • There's more (still investigating...)

4.4 Roller manages all other authorization involving user roles

Roller also does it's own authorization checks on user Roles. For each new user session, Roller creates a RollerSession object. RollerSession calls request.getUserPrincipal().getName() to get the user name, fetches corresponing User object from UserManager and holds on to that User object.

The RollerSession provides access to the session's authenticated user:

public User getAuthenticatedUser()

The User object provides read/write access to user's roles:

public boolean hasRole(String roleName)
public void grantRole(String roleName)
public Set getRoles()

As of Roller 4.0, Roller calls hasRole() for one reason, to ensure that only those with the admin role can:

  • View set the Admin options in the tabbed menu
  • Modify another user's profile
  • Edit any weblog
  • Set pinned field of a weblog entry
  • Use RAP web services

4.4.1 The Problem

Because Roller uses the ORM system to load a User's Roles, the roles must come from the database. And because Roles are part of the User, some sort of join must happen to load each User object with Roles.

4.4.2 The Solution

Instead of relying on ORM supported role methods in the user object, Roller front-end code should call the Roller UserManager:

UserManager interface methods

public boolean isUserInRole(String username)
public Set<String> getUserRoles(String username)
public void revokeRole(String userid, String rolename)
public void grantRole(String userid, String rolename)

And instead of calling role-related methods on the user object, Roller code should use either request.isUserInRole() or the isUserInRole() method provided by the UserManager.

To allow us to plugin alternate user management systems Roller's default UserManager implementation will call a User Repository API interface to store and retrieve permissions:

User Respository API, part 2/3

UserRespository interface methods

public boolean isUserInRole(String username)
public Set<String> getUserRoles(String username)
public void revokeRole(String userid, String rolename)
public void grantRole(String userid, String rolename)

Roller will include a User Repository API that stores data in the Roller database. Other implementations can be plugged in via DI.

4.5 Specific changes to Managers, POJOS, Actions and JSPs

TBD

6.0 Comments

Please comment on the dev mailing list.

  • No labels