HIBERNATE JBoss.org
 |  Register  | 
     
News 
About 
   Feature List 
   Road Map 
Documentation 
   Related Projects 
   External Documentation 
Download 
Forum & Mailinglists 
Support & Training 
JIRA Issue Tracking
Wiki Community Area


Hibernate Public Training Courses


Get Hibernate in Action eBook!


JavaWorld 2003 Finalist


Jolt Award 2004 Winner
      
Documentation > Community Area > Security: Declarative permissions using JAAS and Interceptors

Security: Declarative permissions using JAAS and Interceptors

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);
   }
}
      

coWiki