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.

5.0 Specific changes to Managers, POJOS, Actions and JSPs

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

5.05 Permissions behavioral rules

  • we only support comparing 2 permissions of type WebloggerPermission, and WebloggerPermission cannot be subclassed
  • a permission with type = null indicates a global (application wide) permission
  • permissions that are global (type = null) cannot specify an object (global perms do not pertain to objects)
  • permissions of a specific type must also specify an object (typed perms must correspond to an object)
  • permissions must be of the same type to be compared (we do not compare global and 'weblog' permissions)
  • permissions of a specific type must also specify the same object to be compared ('weblog' perms for different weblogs are never comparable)
  • a permission action can only imply other permissions of the same type ('weblog' perms can only imply other 'weblog' perms)
  • the 'all' action is a special action that implies all possible actions within the specified permission type/object combo
  • a global permission with the 'all' action implies all permissions in the system (the global admin role)
  • a permission can imply any number of other permissions

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 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:

  • All instances of this class are immutable, which is supposed to be part of the contract when using any java.security.Permission object. From the javadoc "Permission objects are similar to String objects in that they are immutable once they have been created. Subclasses should not provide methods that can change the state of a permission once it has been created."
  • This class is not meant to be subclassed and thus "could" potentially be written as a final class. The implementation is purposely generic and does not require a subclass per "type" such as WeblogPermission. This is beneficial because the security system can then be expanded dynamically without requiring new code and Permission classes.
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:

  • This class does not subclass the 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.
  • This class does not require any subclassing to support alternative types of permissions, it is generic enough to support any "type" of permission.
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, ","));
    }
    
}

5.2 Define new permissions properties

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.

5.3 Add new UserManager methods

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:

  • These methods only accept the single 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.
  • The implementations do not user or allow the use of the 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);
  • No labels