This is here to lay down some details on an adjusted way of doing Proposal Externalize User And Permissions Management. The contents below basically start with section 5.0 of the existing doc and propose some slightly different ways of doing things with the main goal of being more abstract and easier to extend than the current proposal.
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 WebloggerPermission extends java.security.Permission. This is the permission class for all Roller Weblogger permissions and really only extends the base Permission class to include the ideas that a Permission has both a "type" and an "object" to which the set of actions apply. This class also implements the very important Permission method implies() which contains the logic sorts out if one permission implies another.
Key differences between this class and the version(s) suggested in the proposal:
package org.apache.roller.weblogger.pojos;
import java.security.Permission;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.apache.roller.weblogger.util.Utilities;
/**
* Base permission class for Weblogger.
*/
public final class WebloggerPermission extends java.security.Permission {
private final String type;
private final String object;
private final String actions;
private final Set<String> actionsAsSet;
private final Set<String> completeActionsAsSet;
public WebloggerPermission(String actions) {
this(null, null, actions);
}
public WebloggerPermission(String type, String object, String actions) {
super("WebloggerPermission");
if((type != null && object == null) || (type == null && object != null))
throw new IllegalArgumentException("'type' and 'object' must be either both null or both non-null.");
this.type = type;
this.object = object;
this.actions = actions;
this.actionsAsSet = Utilities.stringToStringSet(actions, ",");
this.completeActionsAsSet = buildFullActionsSet(actionsAsSet);
}
// --------------------------------------------------- Public Methods
public String getType() {
return type;
}
public String getObject() {
return object;
}
public String getActions() {
return actions;
}
public Set<String> getActionsAsSet() {
return actionsAsSet;
}
public boolean implies(Permission permission) {
// only compare WebloggerPermission objects
if (!(permission instanceof WebloggerPermission))
return false;
WebloggerPermission that = (WebloggerPermission) permission;
// only compare 2 permissions of the same type
if(that.getType() != null) {
// this type == null or the 2 types are not equal, invalid comparison
if(this.getType() == null || !(this.getType().equals(that.getType())))
return false;
} else if(this.getType() != null) {
// that has type == null but this has type != null, invalid comparison
return false;
}
// only compare 2 permissions of the same object
if(that.getObject() != null) {
// this object == null or the 2 objects are not equal, invalid comparison
if(this.getObject() == null || !that.getObject().equals(this.getObject()))
return false;
} else if(this.getObject() != null) {
// that has object == null but this has object != null, invalid comparison
return false;
}
// does "this" permission contain all actions of "that" permission?
// we consider a permission implied if our set of granted actions
// contains all of the actions of the permission we are checking
if(this.completeActionsAsSet.containsAll(that.completeActionsAsSet))
return true;
return false;
}
public boolean equals(Object arg0) {
throw new UnsupportedOperationException("Not supported yet.");
}
public int hashCode() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getName()).append(" : ");
for (String action : getActionsAsSet()) {
sb.append(" ").append(action).append(" ");
}
return sb.toString();
}
// --------------------------------------------------- Private Methods
// build an expanded Set of all actions implied by a Set of actions
private Set<String> buildFullActionsSet(Set<String> actions) {
Set<String> fullSet = new HashSet<String>();
for( String action : actions ) {
Set<String> impliedActions = getImpliedActions(action);
if(impliedActions.size() > 0) {
// if an action implies other actions, recurse
fullSet.addAll(buildFullActionsSet(impliedActions));
} else {
fullSet.add(action);
}
}
return fullSet;
}
// get implied actions for an action
private Set<String> getImpliedActions(String action) {
// TODO: lookup if action implies other actions
// this would likely be a function of the UserManager somehow
return Collections.EMPTY_SET;
}
}
|
Class UserPermission is the persisted version of Roller Weblogger user permission data. This class is present purely for the use by the built in JPAUserManagerImpl class so that user permission data can be persisted to a relational db. In the event that a UserManager implementation was managing users/security via an alternate system this class would not need to be used. We could potentially make this class confined to use by the JPAUserManagerImpl by moving it and setting it to package level privacy.
Key differences between this class and the version(s) suggested in the proposal:
java.security.Permission class or any of its subclasses such as WebloggerPermission. It's sole purpose is for persisting user permission information, not for actual security checks.
package org.apache.roller.weblogger.pojos;
import java.io.Serializable;
import java.util.Date;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.roller.util.UUIDGenerator;
import org.apache.roller.weblogger.util.Utilities;
/**
* Permissions for a specific user.
*/
public class UserPermission implements Serializable {
private static Log log = LogFactory.getLog(UserPermission.class);
private String id = UUIDGenerator.generateUUID();
private User user = null;
private String type = null;
private String object = null;
private String actions = null;
private Date dateCreated = new Date();
private boolean pending = false;
public UserPermission() {}
public UserPermission(User user, String type, String object, String actions) {
this.user = user;
this.type = type;
this.object = object;
this.actions = actions;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getObject() {
return object;
}
public void setObject(String object) {
this.object = object;
}
public void setActions(String actions) {
this.actions = actions;
}
public String getActions() {
return actions;
}
public Date getDateCreated() {
return dateCreated;
}
public void setDateCreated(Date dateCreated) {
this.dateCreated = dateCreated;
}
public boolean isPending() {
return pending;
}
public void setPending(boolean pending) {
this.pending = pending;
}
public void addActions(String newActions) {
Set<String> existing = Utilities.stringToStringSet(actions, ",");
Set<String> newActs = Utilities.stringToStringSet(newActions, ",");
existing.addAll(newActs);
setActions(Utilities.stringSetToString(existing, ","));
}
public void removeActions(String removeActions) {
Set<String> existing = Utilities.stringToStringSet(actions, ",");
Set<String> removeActs = Utilities.stringToStringSet(removeActions, ",");
existing.removeAll(removeActs);
setActions(Utilities.stringSetToString(existing, ","));
}
}
|
The need for roles is no longer necessary as a concept separate from permissions, but roles did provide one very crucial benefit that we need to retain. They provided the ability to dynamically group and imply permissions. i.e. they allowed a user with the role "editor" to imply having the permissions "login,weblog". We can accomplish the same thing by allowing any permission to imply additional permissions.
Like roles we'll want to that to be configurable and dynamic so that a permission can be modified to imply more and less other permissions as elements of the system change. Exactly how this would work is not definite, but similar to the original proposal a permission could be defined to imply other permissions, such as ...
permission.anonymous=comment permission.editor=login,comment,createWeblog permission.admin=login,comment,createWeblog,admin |
It's also important that this be expandable to any number of levels, so it would be okay do define ...
permission.level1=action0 permission.level2=level1,action1,action2 permission.level3=level2,action3 |
Thus, a user with permission "level3" is equivalent to having actions "action0,action1,action2,action3"
NOTE: we would probably also want to create a special action for "all" which can be used for admin purposes and implies all actions automatically.
Based on the behavior in Roller 4.0 we would have these permissions to start from ...
# Application Permissions admin=*all* editor=login,mainMenu,editProfile,createWeblog # Weblog Permissions admin=*all* author=entries,comments,categories,bookmarks,resources limited=editDraft |
The application would not need to actually check for individual actions such as "categories" or "editProfile" when the security model is first updated, it can continue to simply check for "admin" or "author", but eventually the true power of the security system comes in when we make use of more finely grained action controls. That way each action can be granted/revoked to individual users and combined in any way the owner of the application desires.
Here are the new methods to be added to UserManager, these implementations are an example of the version used by the built in JPA implementation.
Key differences between this class and the version(s) suggested in the proposal:
WebloggerPermission object as arguments and there is not need/desire to implement additional methods which work against specific subclasses, e.g. WeblogPermission. Rather than create alternate types of permissions via subclassing we are doing that via the use of the "type" attribute of the WebloggerPermission class.UserPermission object, which is really just used behind the scenes to store/load permission information to the db if that's the chosen data store. So ultimately these methods are persistent agnostic.
public boolean checkPermission(User user, WebloggerPermission requiredPerm)
throws WebloggerException {
// first see if the user has this permission defined
WebloggerPermission userPerm =
getPermission(user, requiredPerm.getType(), requiredPerm.getObject());
if(userPerm != null) {
// yes they do, now lets see if they have the required actions
return userPerm.implies(requiredPerm);
}
return false;
}
public void grantPermission(User user, WebloggerPermission perm)
throws WebloggerException {
// first, see if user already has a permission for the specified object
UserPermission userPerm = getUserPermission(user, perm.getObject(), perm.getType());
if (userPerm != null) {
// permission already exists, so just add actions
userPerm.addActions(perm.getActions());
this.strategy.store(userPerm);
} else {
// it's a new permission
UserPermission newPerm = new UserPermission(user, perm.getType(), perm.getObject(), perm.getActions());
this.strategy.store(newPerm);
}
}
public void revokePermission(User user, WebloggerPermission perm)
throws WebloggerException {
// first, see if user already has a permission for the specified object
UserPermission oldPerm = getUserPermission(user, perm.getObject(), perm.getType());
if(oldPerm == null) {
return;
}
// remove actions specified in perm agument
oldPerm.removeActions(perm.getActions());
if (oldPerm.getActions() == null || oldPerm.getActions().trim().length() == 0) {
// no actions left in permission so remove it
this.strategy.remove(oldPerm);
} else {
// otherwise save it
this.strategy.store(oldPerm);
}
}
public List<WebloggerPermission> getPermissions(User user, String type)
throws WebloggerException {
Query q = strategy.getNamedQuery("UserPermission.getByUser&Type");
q.setParameter(1, user);
q.setParameter(2, type);
List<UserPermission> results = q.getResultList();
List<WebloggerPermission> perms = new ArrayList<WebloggerPermission>();
for ( UserPermission uperm : results) {
perms.add(userPermissionToWebloggerPermission(uperm));
}
return perms;
}
public List<WebloggerPermission> getPermissions(String object, String type)
throws WebloggerException {
Query q = strategy.getNamedQuery("UserPermission.getByObject&Type");
q.setParameter(1, object);
q.setParameter(2, type);
List<UserPermission> results = q.getResultList();
List<WebloggerPermission> perms = new ArrayList<WebloggerPermission>();
for ( UserPermission uperm : results) {
perms.add(userPermissionToWebloggerPermission(uperm));
}
return perms;
}
public WebloggerPermission getPermission(User user, String type, String object)
throws WebloggerException {
UserPermission userPerm = getUserPermission(user, object, type);
// transform into WebloggerPermission
if(userPerm != null && !userPerm.isPending()) {
return userPermissionToWebloggerPermission(userPerm);
} else {
return null;
}
}
private UserPermission getUserPermission(User user, String object, String type)
throws WebloggerException {
Query q = strategy.getNamedQuery("UserPermission.getByUser&Object&Type");
q.setParameter(1, user);
q.setParameter(2, object);
q.setParameter(3, type);
try {
return (UserPermission) q.getSingleResult();
} catch (NoResultException ignored) {
return null;
}
}
private WebloggerPermission userPermissionToWebloggerPermission(UserPermission perm) {
return new WebloggerPermission(perm.getType(), perm.getObject(), perm.getActions());
}
|
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:
// remember that the arguments are ("type of permission", "id of object this applies to", "required action(s)")
WebloggerPermssion desiredPerm = new WebloggerPermission("weblog", weblog.getId(), "postEntry");
boolean allowed = userManager.checkPermssion(user, desiredPerm);
|