Name |
Externalize User and Permissions Management |
Status |
Implemented in trunk for 4.1 |
Target Release |
Roller Weblogger 4.1 |
Issue |
|
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.
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.
Proposal satisfies these requirements.
Issues raised and addressed during review process. TBD.
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.
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:
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:
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.
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:
Now, let's move from user/role management to 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:
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) |
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.
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.
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.
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.
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.
Specific changes that will be made to implement the solutions described above.
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.
Class WeblogPermission extends RollerPermission defines the weblog specific actions below. Each WeblogPermission object will have a list of those actions.
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
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.
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 ) |
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
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.
To make CMA optional:
RollerContext into an abstract base class so that we can have a AcegiPlease comment on the dev mailing list.