Name

Externalize User and Permissions Management

Status

Implemented in trunk for 4.1

Target Release

Roller Weblogger 4.1

Issue

ROL-1534

Original Authors

Dave Johnson

Proposal to make it possible to externalize user and permissions management so that Roller can be easily customized to pull user profile and permissions information from an external system.

1.0 Abstract

For ease of installation and management, Roller is able to manage it's own users and permissions 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 and permissions systems. The way to do that is to externalize user and permissions management, or rather to make it externalizable.

This proposal outlines a plan to make it easy to hook Roller up to an external user and permissions managements system for user information, user profiles and user permissions. The general approach is to enhance the existing UserManagement interface so that it fully encapsulates all user management functionality needed by Roller. Once we've done that, folks can plugin their own user and permissions management systems by simply plugging in a new UserManager. Also, to allow more authentication options, 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 and permission 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.

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).

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.1.1 The problem with managing Users and Roles via ORM

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.

4.1.2 Proposed solution

Ensure that user, role and permission information is always retrieved via UserManager methods and never by ORM relationship management (e.g. never by weblog.getUser()). That approach will make user/permissions management truly pluggable. Also, refactor the UserManager interface so that it is responsible only for user/perms management. The result of this refactoring will be this arrangement:

  • UserManager - for user and role/permissions management
  • WeblogManager - for Weblog and Page management
  • WeblogEntryManager - for WeblogEntry and Comment management

Now, let's move from user/role management to permissions.

4.2 Roller manages user-weblog permissions

In addition to roles, which are global across a Roller site, Roller also manages each user's permissions to access weblogs. There is a many-to-many relationship between users and weblogs and it's stored in a database table:

-- User permissions within a website
-- permission_mask: bitmask 000 limited, 001 author, 011 admin
-- pending: pending user acceptance of invitation to join website
create table roller_user_permissions (
   id              varchar(48) not null primary key,
   website_id      varchar(48) not null,
   user_id         varchar(48) not null,
   permission_mask integer not null, 
   pending         $db.BOOLEAN_SQL_TYPE_TRUE not null
);

There are three permission levels:

  • limited: can edit draft weblog entries only, can submit for review
  • author: can edit draft and publish weblog entries
  • admin: can author and can manage users, weblog settings, theme and etc.

Each User object provides access to the User's weblog permissions. When a user logs in, we use this to display the user's list of weblogs.

User

public List getPermissions()
public void setPermissions(List perms)

Each Weblog object provides access to the Weblog's permissions. When a weblog admin uses the manage members page, we use this information to display the list of weblog members and the permissions levels of each.

Weblog

public List getPermissions() 
public void setPermissions(List perms) 
public void removePermission(WeblogPermission perms)
public int getUserCount()
public int getAdminUserCount()

WeblogEntry

public boolean hasWritePermissions(User user)

4.2.1 Problem with managing permissions via ORM relationships

Permissions cannot be managed by external system because the User to Permissions to Weblog relationship is managed by the ORM, the information must be stored in Roller database tables and cannot be externalized and managed by another system.

4.2.2 Solution

Remove the Roller front-end's dependence on UserRole and WeblogPermissions completely and put in place a single consistent permissions system via the UserManager interface.

Now let's discussion authentication.

4.3 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.3.1 The problem with relying solely on Acegi

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.3.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.

5.0 Specific changes to Managers, POJOS, Actions and JSPs

Specific changes that will be made to implement the solutions described above.

5.1 Define new permissions classes

Define new permissions classes using java.security.Permission as the base class. A Permssion object defines a list of "actions" that are permitted.

Class RollerPermission extends java.security.Permission. This is simply a common base class for all Roller permissions, it provides no additional functionality over what is provided by Permssion.

Class GlobalPermission extends RollerPermission defines the global actions below. Each GlobalPermission object will have a list of those actions.

  • login: allowed to login and edit user profile
  • comment: allowed to comment on weblog entries
  • createWeblog: allowed to create weblogs
  • admin: allowed to administer the Roller installation

Class WeblogPermission extends RollerPermission defines the weblog specific actions below. Each WeblogPermission object will have a list of those actions.

  • editDraft: allowed to edit draft posts only
  • post: allowed to edit and post weblog entries
  • admin: allowed to administer the weblog

5.2 Define new permssions properties

GlobalPermissions are assiged to users via roles, so we need to define what global permission actions are allowed for each role. We do that in the roller.properties file so that users can easily override our default settings without editing withing the Roller WAR.

Here are the new properties:

role.names=anonymous,editor,admin
role.actions.anonymous=comment
role.actions.editor=login,comment,createWeblog
role.actions.admin=login,comment,createWeblog,admin

For example, if you wanted to prevent users from creating new weblogs you would put this in your roller-custom.properties file: role.actons.editor=login,comment

5.3 Add new UserManager methods

Here are the new methods to be added to UserManager:

// Provide a way for the Roller front-end to check any type of permission for a user.
public boolean checkPermission(RollerPermission perm, User user);

// A way for the Roller front-end to grant and revoke roles because roles imply global permissions
public void grantRole(String roleName, User user);
public void revokeRole(String roleName, User user);

// The Roller front-end also needs to be able to grant and revoke weblog permissions
public void grantWeblogPermission(WeblogPermission perm, User user);
public void revokeWeblogPermission(WeblogPermssion perm, User user);

// and to display the roles and permissions associated with each user:
public List<String> getRoles(User user);
public List<WeblogPermission> getWeblogPermssions(User user);
public List<WeblogPermission> getWeblogPermssions(Weblog weblog);

For example, if you want to check to see if a user has can post a weblog entry in a weblog, you would do this:

WeblogPermssion desiredPerm = new WeblogPermission(weblog, WeblogPermission.POST);
boolean allowed = userManager.checkPermssion(desiredPerm, user);

Roller will look up the WeblogPermission object for the specified user and weblog and will return true if the "post" action is in the permission's action list.

5.4 Implementation of new UserManagement permissions methods

Global permissions are implied by role, so we'll keep the userrole table and UserRole POJO.

Weblog permissions will be stored in a new table called roller_permisison table. This table should be fairly generic because someday we may want to permission other objects.

create table roller_permission (
   id              varchar(48) not null primary key,
   username        varchar(255) not null,
   actions         varchar(255) not null, -- comma separated list of actions permitted by permission
   objectid        varchar(48),           -- for now this will always store weblogid
   objectType      varchar(255),          -- for now this will always be 'Weblog'
   datecreated     timestamp not null
)

5.5 Removing dependence on ORM for user/role/perms management

Here are the specific changes to Roller POJOs and front-end code to remove dependence on ORM for user, role and permissions management.

User POJO: remove the set/getRoles() and set/getPermissions() methods

UserRole POJO: remove get/setUser methods

WeblogEntryTag POJO: replace set/getUser() with set/getUserName()

Weblog POJO

  • remove get/setPermission() methods
  • implement getUserCount() and getAdminCount() via calls to UserManager
  • remove set/getCreator() method

WeblogPermission POJO: remove it completely

WeblogEntry POJO: implement hasWritePermissions() by call to UserManager

Members Action: change it to use action strings rather than the permisisons mask.

5.6 Make Acegi and CMA optional

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.

6.0 Comments

Please comment on the dev mailing list.

  • No labels