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