Apache Jackrabbit : AccessControl

Access Control / Authorization

Overview

JCR 2.0 specifies Access Control Management in section 16. The JCR API package is javax.jcr.security. It covers the authorization part, ie. what a certain user is allowed to do with the repository, but not UserManagement, which is provided by Jackrabbit as an implementation-specific feature.

Permissions / Privileges

These permissions, called "privileges", are defined by the JCR 2.0 specification. This is basically the set of read, create, modify and delete operations that can be done on nodes and properties via the JCR API. A repository can also define custom privileges.

Basic privileges:

  • jcr:read The privilege to retrieve a node and get its properties and their values.
  • jcr:modifyProperties The privilege to create, remove and modify the values of the properties of a node.
  • jcr:addChildNodes The privilege to create child nodes of a node.
  • jcr:removeNode The privilege to remove a node.
  • jcr:removeChildNodes The privilege to remove child nodes of a node.
  • jcr:write An aggregate privilege that contains: jcr:read, jcr:modifyProperties, jcr:addChildNodes, jcr:removeNode, jcr:removeChildNodes
  • jcr:all An aggregate privilege that contains all available permissions, including jcr:read, jcr:write and the advanced permssions.

For advanced privileges, see section 16.2.3 of the JCR 2.0 specification.

Resource-based ACLs

The JCR 2.0 specification is modeled with resource-based access control lists in mind. That means that a resource = node is associated with a list of allow/deny entries for certain principals (users or groups), which naturally maps to store them along the JCR node. A core concept of resource-based ACLs is that they inherit the ACLs from the parent node, thus for each node, all the ACLs of its ancestor come into play as well.

Resource-based ACLs are the default access control mechanism in Jackrabbit 2.x.

Advantages:

  • fully supported by the JCR API / specification
  • very widely available ACL model (eg. file systems)
  • simple resource inheritance
  • default mechanism in Jackrabbit, no configuration needed

Disadvantages:

  • cannot assign ACLs to non-existent nodes
  • cumbersome when many users need un-groupable ACLs on a few resources (e.g. "subscriptions"), lots of ACL entries per resource
  • permissions are stored right inside the content (can be cumbersome for backups, etc.)

How Resource-based ACLs are stored

Resource-based ACLs are stored per resource/node in a special child node rep:policy. This one will have a list of rep:GrantACE child nodes (usually named allow, allow0,...) for grant access control entries and rep:DenyACE child nodes (usually named deny, deny0,...) for deny access control entries.

Each ACE node has a rep:principalName STRING property pointing to the user or group this ACE belongs to, and a rep:privileges NAME multi-value property, containing all the privileges of this ACE.

Note that you can read/browse these nodes using the JCR API, but cannot modify them. This must always happen through the JCR access control API.

API for setting resource-based ACLs

Note that there is a utility AccessControlUtils in jackrabbit-jcr-commons that simplifies the JCR API calls for ACLs a bit.

This is an example granting all rights to everyone, using the plain JCR API:

AccessControlManager aMgr = session.getAccessControlManager();

// create a privilege set with jcr:all
Privilege[] privileges = new Privilege[] { aMgr.privilegeFromName(Privilege.JCR_ALL) };
AccessControlList acl;
try {
    // get first applicable policy (for nodes w/o a policy)
    acl = aMgr.getApplicablePolicies(path).nextAccessControlPolicy();
} catch (NoSuchElementException e) {
    // else node already has a policy, get that one
    acl = aMgr.getPolicies(path)[0];
}
// remove all existing entries
for (AccessControlEntry e : acl.getAccessControlEntries()) {
    acl.removeAccessControlEntry(e);
}
// add a new one for the special "everyone" principal
acl.addAccessControlEntry(EveryonePrincipal.getInstance(), privileges);

// the policy must be re-set
aMgr.setPolicy(path, acl);

// and the session must be saved for the changes to be applied
session.save();

(for links and more code, see below)

Difference between getPolicies() and getApplicablePolicies()

AccessControlManager.getApplicablePolicies(path) returns an iterator over all applicable policies that you can potentially define for that node. This depends on what the implementation provides. Jackrabbit only supports one policy, the AccessControlList. If this is already applied to that node (in case the node is not new) you get an empty iterator.

A more correct way (than the code sample above) would check if any of the policies returned by the iterator is an AccessControlList and use that one. If you cannot find one for a new node, then that policy is not supported by that specific JCR implementation.

The AccessControlManager.getPolicies(path) only returns the policies that are already applied (i.e. persisted and active) on that node. In case of a new node, this should be empty.

Principal-based ACLs

A different approach for specifying and storing ACLs is to assign certain principals (users or groups) a list of nodes that they are allowed or denied to work on. The nodes will be referenced by paths, and might even include wildcards.

To work with principal-based ACLs, a Jackrabbit-propietary extension to the ACL management API is required.

Advantages:

  • permissions can be assigned to non-existent nodes
  • permissions are stored separately from the content (good for content replication, backup etc.)
  • good for having many users with un-groupable ACLs (e.g. "subscriptions"); resources don't get filled up with ACL entries

Disadvantages:

  • additional Jackrabbit API has to be used for setting ACLs
  • modeling resource inheritance requires more ACLs than resource-based

How Principal-based ACLs are stored

An access control list (rep:ACL) is stored for each user and group (this is transparent, currently it's mirroring the users's home path at /rep:accesscontrol/<principal-path>/rep:policy/). This consists of entries (rep:ACE), which are either allow (rep:GrantACE) or deny (rep:DenyACE) entries.

The rep:ACE nodetype (used by both resource- and principal-based ACLs) defines the following two properties for principal-based usage. These exact same names need to be used as restrictions when using the Jackrabbit API (JackrabbitAccessControlList.addEntry()):

  • rep:nodePath defines the (base) path of a subtree the ACL applies to (mandatory, PATH property)
  • rep:glob defines a glob pattern to restrict the subtree, both child nodes & properties (only a path matching is done) (optional, STRING property)

For the glob pattern, see http://jackrabbit.apache.org/api/2.2/org/apache/jackrabbit/core/security/authorization/GlobPattern.html (NodePath is the rep:nodePath and restriction is the rep:glob).

API for setting principal-based ACLs

This uses the Jackrabbit (security) API extending the base JCR (security) API. Please note that this is example code and might need to be adapted (smile)

// usual entry point into the Jackrabbit API
JackrabbitSession js = (JackrabbitSession) session;

// get user/principal for whom to read/set ACLs

// Note: the ACL security API works using Java Principals as high-level abstraction and does not
// assume the users are actually stored in the JCR with the Jackrabbit UserManagement; an example
// are external users provided by a custom LoginModule via LDAP
PrincipalManager pMgr = js.getPrincipalManager();
Principal principal = pMgr.getPrincipal(session.getUserID());

// alternatively: get the current user as Authorizable from the user management
// (there is a one-to-one mapping between Authorizables and Principals)
User user = ((User) js.getUserManager().getAuthorizable(session.getUserID()));
Principal principal = user.getPrincipal();

// get the Jackrabbit access control manager
JackrabbitAccessControlManager acMgr = (JackrabbitAccessControlManager) session.getAccessControlManager();

JackrabbitAccessControlPolicy[] ps = acMgr.getPolicies(principal); // or getApplicablePolicies()
JackrabbitAccessControlList list = (JackrabbitAccessControlList) ps[0];

// list entries
JackrabbitAccessControlEntry[] entries = (JackrabbitAccessControlEntry[]) list.getAccessControlEntries();
JackrabbitAccessControlEntry entry = entries[0];

// remove entry
list.removeAccessControlEntry(entry);

// add entry
Privilege[] privileges = new Privileges[] { acMgr.privilegeFromName(Privilege.JCR_READ) };
Map<String, Value> restrictions = new HashMap<String, Value>();
ValueFactory vf = session.getValueFactory();
restrictions.put("rep:nodePath", vf.createValue("/some/path", PropertyType.PATH));
restrictions.put("rep:glob", vf.createValue("*"));
list.addEntry(principal, privileges, true /* allow or deny */, restrictions);

// reorder entries
list.orderBefore(entry, entry2);

// finally set policy again & save
acMgr.setPolicy(list.getPath(), list);
session.save();

Classes used are:

Repository Configuration

To use a different ACL mechanism, a different AccessControlProvider must be configured. This is a per-workspace config, and must be set in the workspace.xml of each workspace, or the <Workspace/> element of the repository.xml for to-be-created workspaces.

    <WorkspaceSecurity>
        <AccessControlProvider class="org.apache.jackrabbit.core.security.authorization.combined.CombinedProvider" />
    </WorkspaceSecurity>

The following are built-in jackrabbit-core:

  • Resource-based (default): org.apache.jackrabbit.core.security.authorization.acl.ACLProvider
  • Principal-based: org.apache.jackrabbit.core.security.authorization.principalbased.ACLProvider
  • Combined, resource+principal-based: org.apache.jackrabbit.core.security.authorization.combined.CombinedProvider

TODO