Version 2

    Disclaimer: this code compiles but has not yet been tested because I wish to use a hibernate-based Policy instead of the standard text-based one.  Use with caution.

    In Security: JAAS LoginModule I mentioned that JAAS supports declarative permissions. This document provides details on how JAAS Permissions can be used to support create/read/update/delete declarative permissions via the Hibernate Interceptor .

     

    Before we jump into the code, it's important to understand the basic types of permission that JAAS supports. The first is role-based, using Principals or credentials.

       ...
       Principal editor = new RolePrincipal("editor");
       if (subject.getPrincipals().contains(editor)) {
          // do something
       }
       ...
    

     

    The second is a simple owner/owned relationship.

       ...
       Principal owner = new OwnerPrincipal(object.getOwner());
       if (subject.getPrincipals().contains(owner)) {
          // do something
       }
       ...
    

     

    Guarded objects are another option.  A Guard is one object that controls access to a second object.

       ...
       Guard guard = new HibernateGuard(guardObject);
       try {
         guard.checkGuard(protectedObject);
          // do something...
       } catch (SecurityException e) {
          log.error("woof woof woof!");
       }
    

     

    The final option (for this list) is declarative permissions

       ...
       SecurityManager sm = new SecurityManager();
       Permission requiredPermission = new HibernatePermission(...);
       try {
          sm.checkPermission(requiredPermission);
          // do something...
       } catch (SecurityException e) {
          log.error("sorry, I can't do that, Dave.");
       }
    

     

    where we have explicitly stated permissions elsewhere.  E.g., with a database-based policy our table may look something like:

     id | permission                | action | classname | principal | oid
    ----+---------------------------+--------+-----------+-----------+-----
      1 | HibernateClassPermission  | *      | *         | bob       |
      2 | HibernateObjectPermission | load   | User      | alice     | 47
    

     

    The best way to understand the differences is to think about how a user could manage their own profile under the different model. E.g., under a owner/owned model the user would "own" their own profile. Under a declarative permission model a new Permission would be added granting each user load and update rights to their own profile, but not creation or deletion rights. Under a guarded model you would have a proxy class that associates users with their profiles.


    We begin by declaring several useful Permission classes. First is a abstract base class that provides pattern matching for persistent class names - this allows us to specify a single class, an entire package, or everything.

    import gnu.regexp.RE;
    import gnu.regexp.REException;
    
    /**
     * className is the name of the Java class mapped into
     * the database.  The class name may be a fully qualified class name,
     * a class name ending terminated with a single "*" to indicate all
     * classes in the specified package, or an unadorned "*" to indicate 
     * all classes.
     *
     * Implementation note: we translate the className parameter
     * into a regular expression and use that RE in implies.
     */
    public abstract class ExPermission extends Permission implements Serializable {
    
       /** className */
       protected String className;
    
       /** className RE */
       protected transient RE classNameRE;
    
       /**
        * Convert a class name patten into a regular expression.
        */
       protected RE REize(String className) {
          StringBuffer sb;
          if (className.equals("*")) {
             sb = new StringBuffer(".*");
          }
          else {
             sb = new StringBuffer("^");
             int len = className.length();
             boolean first = true;
             boolean bad = false;
             for (int i = 0; i < len && !bad; i++) {
                char ch = className.charAt(i);
                if (first) {
                   if (ch == '*') {
                      if (className.substring(i).equals("*")) {
                         sb.append("\\.[[:alpha:]][[:alnum:]_$]*");
                         i = len;
                      }
                      else {
                         bad = true;
                      }
                      break;
                   }
                   else if (ch == '.') {
                      // doubled ".", e.g., "com..foo"
                      bad = true;
                   }
                   else if (Character.isJavaIdentifierStart(ch)) {
                      sb.append(ch);
                      first = false;
                   }
                   else {
                      // anything else
                      bad = true;
                   }
                }
                else {
                   if (ch == '.') {
                      sb.append("\\.");
                   }
                   else if (Character.isJavaIdentifierPart(ch)) {
                      sb.append(ch);
                      first = false;
                   }
                   else {
                      throw new IllegalArgumentException(
                         "'className' must be java class or package name");
                   }
                }
             }
             sb.append("$");
    
             if (bad) {
                throw new IllegalArgumentException(
                   "'className' must be java class or package name");
             }
          }
    
          try {
             return new RE(sb.toString());
          } catch (REException e) {
             throw new RuntimeException(e.getMessage(), e);
          }
       }
    
       /**
        * constructor
        */
       public ExPermission(String name, String className) {
          super(name);
    
          // convert className to regular expression, verifying it
          // is properly formed as we do so.
          this.className = className;
          this.classNameRE = REize(className);
       }
    }
    

     

    and a derived CRUDPermission that understands the four basic operations we're concerned about with persistent objects.

    import gnu.regexp.RE;
    import gnu.regexp.REException;
    
    /**
     * This class represents access to an O/R mapped object in a database.
     *
     * className is the name of the Java class mapped into
     * the database.  The class name may be a fully qualified class name,
     * a class name ending terminated with a single "*" to indicate all
     * classes in the specified package, or an unadorned "*" to indicate 
     * all classes.
     *
     * The actions to be granted are passed to the constructor in a
     * string containing a list of one or more comma-separated case-insensitive
     * keywords.  The possible keywords are "load", "create", "modify" and 
     * "delete", or a single "*" to indicate all actions.
     * The permitted actions indicated by each keyword follows:
     *
     * load     Load an object from the database. (onLoad)*
     * create   Create an object in the database. (onSave)*
     * modify   Modify an object in the database. (onUpdate)*
     * delete   Delete an object from the database. (onDelete)*
     */
    public class CRUDPermission extends ExPermission implements Serializable {
    
       /** Principal who has these permissions *
       private Principal principal;
    
       /** permission to load object */
       private boolean canLoad = false;
    
       /** permission to create object */
       private boolean canCreate = false;
    
       /** permission to modify object */
       private boolean canModify = false;
    
       /** permission to delete object */
       private boolean canDelete = false;
    
       /**
        * Create an instance of row permissions.
        */
       public CRUDPermission(
          String name, String className, String actions, Principal principal) {
    
          super(name, className);
    
          if (actions == null || actions.length() < 1) {
             throw new NullPointerException("no actions specified");
          }
    
          StringTokenizer st = new StringTokenizer(actions.toLowerCase(), ",");
          while (st.hasMoreElements()) {
             String s = st.nextToken().trim();
             if (s.equals("*")) {
                canLoad = true;
                canCreate = true;
                canModify = true;
                canDelete = true;
             } else if (s.equals("load")) {
                canLoad = true;
             } else if (s.equals("create")) {
                canCreate = true;
             } else if (s.equals("modify")) {
                canModify = true;
             } else if (s.equals("delete")) {
                canDelete = true;
             } else {
                throw new IllegalArgumentException(
                   "unrecognized action: '" + s + "'");
             }
    
             this.principal = principal;
          }
       }
    
       /**
        * Return the actions as a string.
        *
        * @returns the actions of this permission.
        */
       public String getActions() { 
          StringBuffer sb = new StringBuffer();
          boolean first = true;
    
          if (canLoad) {
             sb.append("load");
             first = false;
          }
          if (canCreate) {
             if (!first) sb.append(",");
             sb.append("create");
             first = false;
          }
          if (canModify) {
             if (!first) sb.append(",");
             sb.append("modify");
             first = false;
          }
          if (canDelete) {
             if (!first) sb.append(",");
             sb.append("delete");
             first = false;
          }
          return sb.toString();
       }
    
       /**
        * Checks if this CRUDPermission object implies the specified
        * permission.
        *
        * Specifically, this method returns true if:
        *
        * - p is an instanceof HibernateObjectPermission,
        * - p's actions are a proper subset of this object's actions, and
        * - p's className is implied by this object's className.
        *
        * @params p the permission to check against.
        * @returns true if the specified permission is
        * implied by this object, false otherwise.
        */
       public boolean implies(Permission p) {
          // first stanza
          if (!(p instanceof CRUDPermission)) {
             return false;
          }
    
          // second stanza
          CRUDPermission rp = (CRUDPermission) p;
          if (!principal.equals(rp.principal)) {
             return false;
          }
          if (!canLoad && rp.canLoad) {
             return false;
          }
          if (!canCreate && rp.canCreate) {
             return false;
          }
          if (!canModify && rp.canModify) {
             return false;
          }
          if (!canDelete && rp.canDelete) {
             return false;
          }
    
          // third stanza
          if (!classNameRE.isMatch(rp.className)) {
             return false;
          }
          return true;
       }
    
       /**
        * Checks to CRUDPermission objects for equality.
        *
        * @param obj the object we are testing for equality with this object.
        * @return true if obj is a 
        * CRUDPermission and has the same class name and
        * actions as this object.
        */
       public boolean equals(Object obj) { 
          if (obj == null) {
             return false;
          }
          if (!(obj instanceof CRUDPermission)) {
             return false;
          }
    
          CRUDPermission p = (CRUDPermission) obj;
          boolean results = true;
          results = results && (className.equals(p.className));
          results = results && (principal.equals(p.principal));
          results = results && (canLoad == p.canLoad);
          results = results && (canCreate == p.canCreate);
          results = results && (canModify == p.canModify);
          results = results && (canDelete == p.canDelete);
          return results;
       }
    
       /**
        * Returns the hash code value for this object.
        *
        * @returns a hash code value for this object.
        */
       public int hashCode() { 
          int code = className.hashCode() << 4;
          if (canLoad)   { code |= (1 << 0); }
          if (canCreate) { code |= (1 << 1); }
          if (canModify) { code |= (1 << 2); }
          if (canDelete) { code |= (1 << 3); }
    
          return code;
       }
    }
    

     

    We can now easily define two useful Permission classes.  The first controls access to any persistent object of a particular class:

    final public class HibernateClassPermission
       extends CRUDPermission implements Serializable {
    
       public HibernateClassPermission(
          String className, String actions, Principal principal) {
    
          super("HibernateClassPermission", className, actions, principal);
       }
    
       public boolean implies(Permission p) {
          // first stanza
          if (!(p instanceof HibernateClassPermission)) {
             return false;
          }
    
          if (!super.implies(p)) {
             return false;
          }
    
          return true;
       }
    
       public boolean equals(Object obj) { 
          if (obj == null) {
             return false;
          }
          if (!(obj instanceof HibernateClassPermission)) {
             return false;
          }
    
          return super.equals(obj);
       }
    }
    

     

    and the second controls access to a specific instance:

    final public class HibernateObjectPermission
       extends CRUDPermission implements Serializable {
    
       private Serializable id = null;
    
       public HibernateObjectPermission(
          String className, String actions, Principal principal, Serializable id) {
    
          super("HibernateObjectPermission", className, actions, principal);
    
          this.id = id;
       }
    
       public boolean implies(Permission p) {
          // first stanza
          if (!(p instanceof HibernateObjectPermission)) {
             return false;
          }
    
          if (!super.implies(p)) {
             return false;
          }
    
          HibernateObjectPermission rp = (HibernateObjectPermission) p;
          return id.equals(rp.id);
       }
    
       public boolean equals(Object obj) { 
          if (obj == null) {
             return false;
          }
    
          if (!(obj instanceof HibernateObjectPermission)) {
             return false;
          }
    
          HibernateObjectPermission p = (HibernateObjectPermission) obj;
          return super.equals(obj) && id.equals(p.id);
       }
    }
    

     

    Omitted discussion that we must define our Permission in a Policy and set it as the default policy used by the AccessController, or how we want to use a Hibernate-based Policy.

    At this point we have everything in place to implement JAAS-based declarative permissions via a Hibernate Interceptor.

    /**
     * Hibernate interceptor that implements CRUD access control
     * via JAAS.
     *
     * N.B., This is not a typical Interceptor 
     * implementation, don't use this as a model.
     */
    public class AccessInterceptor extends ChainedInterceptor
       implements Interceptor, Serializable { 
    
       private Principal principal = null;
    
       /**
        * Default constructor.
        */
       public AccessInterceptor() {
          super();
          Subject subject = Subject.getSubject(AccessController.getContext());
          Iterator i = subject.getPrincipals(HibernatePrincipal).iterator();
          if (i.hasNext()) {
             principal = (Principal) i.next();
          }
       }
    
       /**
        * Common routine that performs table- and row-level
        * access checks for specified action and object.
        *
        * @param clazz a mapped class
        * @param action the action we seek to perform.
        * @param id id of object to be created, updated or deleted
        * from the database.
        * @throws CallbackException if the action is not permitted.
        */
       private void check(Class clazz, String action, Serializable id)
          throws CallbackException {
    
          String className = clazz.getClass().getName();
          SecurityManager sm = System.getSecurityManager();
          if (sm != null) {
             try {
                sm.checkPermission(
                   new HibernateClassPermission(className, action));
                if (id != null) {
                   sm.checkPermission(
                      new HibernateObjectPermission(className, action, id));
                }
             }
             catch (SecurityException e) {
                throw new CallbackException(e.getMessage(), e);
             }
          }
       }
    
       /**
        * Method called just before an object is initialized. 
        * This method is called by "load" actions and queries,
        * but there does not seem to be a way to distinguish
        * between these cases.
        *
        * @param entity uninitialized instance of the class to be loaded
        * @param id the identifier of the new instance.
        * @param state array of property values.
        * @param propertyNames array of property names.
        * @param types array of property types.
        * @return true if the state was
        * modified in any way.
        * @throws CallbackException if a problem occured.
        */
       public boolean onLoad (
             Object entity,
             Serializable id,
             Object[] state,
             String[] propertyNames,
             Type[] types)
          throws CallbackException{
    
          check(entity.getClass(), "load", id);
          return super.onLoad(entity, id, state, propertyNames, types);
       }
    
       /**
        * Method called when an object is detected to be dirty, during
        * a flush.  This method is called by "modify" actions.
        *
        * @param entity object to be updated in the database.
        * @param id the identifier of the instance.
        * @param currentState array of property values.
        * @param previousState cached array of property values.
        * @param propertyNames array of property names.
        * @param types array of property types.
        * @return true if the currentState was
        * modified in any way.
        * @throws CallbackException if a problem occured.
        */
       public boolean onFlushDirty (
             Object entity,
             Serializable id,
             Object[] currentState,
             Object[] previousState,
             String[] propertyNames,
             Type[] types)
          throws CallbackException {
    
          check(entity.getClass(), "modify", id);
          return super.onFlushDirty(entity, id, currentState, previousState, propertyNames, types);
       }
    
       /**
        * Method called before an object is saved.  This method is
        * called by "create" actions.
        *
        * @param entity object to be saved to the database.
        * @param id the identifier of the instance.
        * @param state array of property values.
        * @param propertyNames array of property names.
        * @param types array of property types.
        * @return true if the user modified the state
        * in any way.
        * @throws CallbackException if a problem occured.
        */
       public boolean onSave (
             Object entity,
             Serializable id,
             Object[] state,
             String[] propertyNames,
             Type[] types)
          throws CallbackException {
    
          check(entity.getClass(), "create", id);
          return super.onSave(entity, id, state, propertyNames, types);
       }
    
       /**
        * Method called before an object is delete.  This method is
        * called by "delete" actions.
        *
        * @param entity object to be deleted from the database.
        * @param id the identifier of the instance.
        * @param state array of property values.
        * @param propertyNames array of property names.
        * @param types array of property types.
        * @throws CallbackException if a problem occured.
        */
       public void onDelete (
             Object entity,
             Serializable id,
             Object[] state,
             String[] propertyNames,
             Type[] types)
          throws CallbackException {
    
          check(entity.getClass(), "delete", id);
          super.onDelete(entity, id, state, propertyNames, types);
       }
    }