Security: Declarative permissions using JAAS and InterceptorsDisclaimer: 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);
}
}
|