Fork me on GitHub

Managing Access by Principal

General

Oak 1.16.0 introduces a new, optional authorization model in the oak-authorization-principalbased module intended to be used in combination with the default implementation present with Apache Jackrabbit Oak. In contrast to the default authorization it provides native support for access control management based upon principals.

The model leverages the fact that JSR 283 allows to redefine to scope of a given policy beyond the access controlled node it is bound to. Quoting section 16.3 Access Control Policies of JSR 283:

Note that the scope of the effect of an access control policy may not be identical to the node to which that policy is bound.

The model is by default disabled and it requires manual configuration steps in order to add it to the Oak security setup. The configuration steps include defining which principals are supported and how to map them to an access controlled node in the repository that will hold the policy (see section API Extensions).

Jackrabbit API

Jackrabbit API 2.18 defines an extension of the AccessControlList and JackrabbitAccessControlList interfaces bound to a given Principal

  • PrincipalAccessControlList

The entries contained in this type of ACL are expected to be of type

  • PrincipalAccessControlList.Entry

which in addition to the methods inherited from AccessControlEntry and JackrabbitAccessControlEntry defined the absolute path where they will ultimately take effect. See Javadoc for PrincipalAccessControlList and Entry in Jackrabbit API 2.18 for additional details.

API Extensions

The module comes with the following extension in the org.apache.jackrabbit.oak.spi.security.authorization.principalbased package space:

FilterProvider and Filter

In order to be operational the principal-based authorization needs to have a FilterProvider configured. The corresponding Filter defines if the model is able to evaluate permissions for a given set of principals. For any unsupported set of principals permission evaluation will be skipped altogether. Similarly, access control policies can only be obtained and modified for supported principals.

Apart from validating principals the Filter interface is also responsible for mapping each supported principal to a location in the repository where the access control setup for that principal is being stored.

See section Implementation Details for a description of the provider implementation present with the module. Section Pluggability describes how to deploy a custom implementation.

Implementation Details

Access Control Management

The access control management follows the requirements defined by JSR 283 and the extensions defined by Jackrabbit API (see also section Access Control Management).

Access Control Policies

The principal-based authorization model returns two types of policies:

  • PrincipalPolicyImpl: a mutable policy implementating PrincipalAccessControlList, which is returned upon JackrabbitAccessControlManager.getApplicablePolicies(Principal) and JackrabbitAccessControlManager.getPolicies(Principal).
  • ImmutableACL: whenever effective policies are returned upon calling AccessControlManager.getEffectivePolicies(String) and JackrabbitAccessControlManager.getEffectivePolicies(Set<Principal>).
Management by Principal

In order to manage access control the Jackrabbit API extensions need to be used (see JackrabbitAccessControlManager).

  • JackrabbitAccessControlManager.getApplicablePolicies(Principal): if the configured Filter handles the specified principal this method will return a single empty modifiable policy of type PrincipalAccessControlList if no policy has been set for the target principal before.
  • JackrabbitAccessControlManager.getPolicies(Principal): once an applicable policy has been set, this method will return a single modifiable policy of type PrincipalAccessControlList.

The following characteristics apply when modifying the PrincipalAccessControlList defined by this implementation:

  • all entries will grant access (i.e. no deny)
  • the effective path parameter must be an absolute JCR path or null for repository level privileges.
  • the effective path may point to a non-existing node.
  • the entry may define one or many supported privileges (see AccessControlManager.getSupportedPrivileges(String absPath) and Supported Privileges below)
  • additional restrictions may optionally be specified according to JackrabbitAccessControlList.getRestrictionNames (see Supported Restrictions below)
  • entries will be added to the list in the order they are created
  • while equal entries will not be added to the list, no additional effort is made to avoid or cleanup redundant entries.
  • entries can be reordered within the list (JackrabbitAccessControlList.orderBefore) but this doesn’t impact the net effect (no denies).

Since PrincipalAccessControlList extends JackrabbitAccessControlList, new entries can also be added using variants of the addEntry method. Please note the following principles:

  • the specified Principal must be equal to the principal for which the policy was obtained
  • only allow entries are supported (see above)
  • the entry may define one or many supported privileges (see AccessControlManager.getSupportedPrivileges(String absPath) and Supported Privileges below)
  • the new entry must come with a single value rep:nodePath restriction specifying the absolute JCR path where this policy will take effect. To indicate that the entry takes effect at the repository level an empty string value is used.
  • additional restrictions may optionally be specified according to JackrabbitAccessControlList.getRestrictionNames (see Supported Restrictions below)

The path of the policies (JackrabbitAccessControlPolicy.getPath) is defined by the configured Filter implementation and will be used to persist the modified policy (AccessControlManager.setPolicy(String, AccessControlPolicy)) or remove it (AccessControlManager.removePolicy(String, AccessControlPolicy)).

Both operations require the editing session to have jcr:modifyAccessControl privilege granted at the access controlled node that will hold the policy. Since the access control entries contained in the policy will take effect at the tree defined by Entry.getEffectivePath(), the editing session in addition needs permission to modify access control content at the path defined with each individual entry. This contrasts the default implementation where a given entry only takes effect at the tree defined by the access controlled node.

Management by Path

Editing access control by path is not supported with the principal-based access control manager. Consequently, AccessControlManager.getApplicablePolicies(String) and AccessControlManager.getPolicies(String) will return an empty iterator/array.

Note however that AccessControlManager.getEffectivePolicies(String) will make a best-effort approach searching for entries that take effect at a given absolute path: the query will look for nodes of type rep:PrincipalEntry that have a property rep:effectivePath matching the target path or any of its ancestors. Restrictions limiting the effect of a given entry are not taken into account. See also JSR 283 section 16.3.5 Scope of a Policy in JSR 283.

Supported Privileges

All privileges registered with the repository are supported by this authorization model.

Supported Restrictions

The principal-based authorization model doesn’t come with a dedicated RestrictionProvider. Instead it is built to handle any restriction defined by the Oak authorization setup.

Readable Paths

If the principal-based authorization is used together with the default implementation, it will respect the readable-paths configuration. For trees located at or below the readable paths AccessControlManager.getEffectivePolicies(String absPath) will include a NamedAccessControlPolicy. Note, that in accordance to the default authorization model, this effective policy is not currently not included when looking up effective policies by principal.

Permission Evaluation

If a given set of principals is supported by the configured FilterProvider/Filter implementation, the principal-based authorization model will contribute an implementation of AggregatedPermissionProvider to the composite. Whether or not access will be granted depends on the aggregated providers and their ranking, the composition type and whether or not an AggregationFilter is defined for the setup (see also section Combining Multiple Authorization Models for details).

If the set of principals is not supported an EmptyPermissionProvider will be returned and the model will be ignored altogether. It follows that in this case permission evaluation delegated to other authorization modules configured in the composite.

Reading and Caching

Once permission evalution is triggered the principal-based model will directly read the effective permissions from the persisted access control content. There exists no separate location for permissions like the permission store present with the default implementation.

All entries defined for a given set of principal are eagerly loaded from the access control content and kept in memory for each instance of ContentSession. This applies to all supported principals irrespective of the size of the individual policies or the size of the principal set.

Note, that the intended use-case for this authorization model expects small sets of system user principals each with a limited set of permissions, which result in comparably small ACLs. See OAK-8227 for benchmark series that measure read operations with increasing number of entries and principals.

Permission Inheritance

In contrast to the default permission evalution the principal-based setup makes no distinction between user and group principals nor does’t make any assumptions about the order of principals computed and placed in the Subject upon login. The inheritance model only takes the item hierarchy into account. In other words the evaluation process will start at the target item and search up the item hierarchy for a matching entry. An entry is considered matching if it is defined for any of the principals in the given set, applies to the target item and grants the specified permissions.

Allowed if Granted

As soon as an entry matches the target item and grants the requested permission the evaluation will stop. As this model only supports allowing entries there exists no particular requirement to maintain and handle the order of entries for different principals that take effect at a given target.

However, in order to minimize excessive read on the NodeStore it is recommended to avoid fully redundant entries such as e.g.

  • entry: granting privileges, redundant: same privileges with additional restrictions
  • entry: granting privileges, redundant: subset of these privileges
Readable Paths

Since OAK-8671 principal-based authorization respects the readable paths configuration option present with the default authorization model. For any tree located at or below these configured paths read-access is always granted.

Administrative Access

The principal-based authorization doesn’t enforce any special handling for administrative principals. When implementing a custom FilterProvider this should be part of the considerations. An implementation may e.g. choose not to support administrative principals and thus delegate the permission evalution to the default implementation.

Permission Evaluation with Multiplexed Stores

This authorization model can be used in combinition with non-default mounts with one notable limitation: None of the non-default mounts may be located below the configured filter root (see FilterProvider.getFilterRoot()) in order to make sure that all policies managemed and evaluated by this model are residing the same mount.

FilterProvider Implementation

The model comes with an implementation of the FilterProvider and Filter interfaces which (if enabled) will limit the scope of the principal-based authorization according to the following rules:

  • the set of principals must not be empty and must only contain SystemUserPrincipals
  • each SystemUserPrincipal must be associated with a location in the repository (i.e. must be ItemBasedPrincipal when obtained through principal management API).
  • all principals must additionally be located below the path configured with FilterProviderImpl (see below

So, if this implementation is enabled the principal-based authorization will only take effect for SystemUserPrincipals that are created below the configured path. As soon as a given Subject or set of principals contains principals that doesn’t match the filter definition (e.g. group principals, principals not located in the repository or located elsewhere in the repository), principal-based authorization will be skipped. This applies both to permission evaluation and to access control management.

AggregationFilter Implementation

In addition principal-based authorization provides a implementation of the AggregationFilter interface that stops the aggregation of PermissionProviders and effective policies as soon as the PrincipalBasedPermissionProvider takes effect (i.e. the mandatory FilterProvider will handle a given set of principals). The AggregationFilter can be enabled by setting the corresponding flag with the module configuration.

Representation in the Repository

The access control lists defined by this module are represented by nodes named rep:principalPolicy with primary node type rep:PrincipalPolicy. The declaring mixin type of this policy node is rep:PrincipalBasedMixin (according to rep:AccessControllable). The policy node has a single mandatory, protected property containing the name of principal this policy is bound to and a list of entries of type rep:PrincipalEntry. Apart from mandatory privileges and optional restrictions each entry defines the target path (rep:effectivePath), where it will take effect upon successful commit.

/**
 * @since oak 1.14
 */
[rep:PrincipalBasedMixin]
  mixin
  + rep:principalPolicy (rep:PrincipalPolicy) protected IGNORE

/**
 * @since oak 1.14
 */
[rep:PrincipalPolicy] > rep:Policy
  orderable
  - rep:principalName (STRING) protected mandatory IGNORE
  + * (rep:PrincipalEntry) = rep:PrincipalEntry protected IGNORE

/**
 * @since oak 1.14
 */
[rep:PrincipalEntry]
  - rep:effectivePath (PATH) protected mandatory
  - rep:privileges (NAME) multiple protected mandatory multiple
  + rep:restrictions (rep:Restrictions) = rep:Restrictions protected

Note: While the definition of the rep:principalName property doesn’t mandate any particular value, it is the mandatory FilterProvider implementation that will ultimately define, for which types of principals this type of policy can be created and where these principals are to be located in the repository.

Validation

The validity of the content structure is asserted by a dedicated Validator on creation and modification. The following commit failures of type AccessControl may occur:

Code Message
0030 Attempt create policy node with different name than rep:principalPolicy
0031 Attempt to change primary type from/to rep:PrincipalPolicy
0032 Reserved node name ‘rep:principalPolicy’ must only be used for nodes of type ‘rep:PrincipalPolicy’
0033 Parent node not of mixin type ‘rep:PrincipalBasedMixin’
0034 Reserved node name ‘rep:restrictions’ must only be used for nodes of type ‘rep:Restrictions’
0035 Invalid restriction
0002 Expected access control entry parent (isolated restriction)
0036 Isolated entry of principal policy
0037 Empty rep:privileges property
0038 Abstract privilege
0039 Invalid privilege

Note, that the validator performs additional checks regarding ability to modify access control content that will take effect at the location indicated by the rep:effectivePath property. In case the editing session doesn’t have sufficient permissions at the target location the commit will fail with an error of type Access:

Code Message
0003 Access denied: If editing session is not granted modify access content at effective target path

Configuration

Configuration Parameters

The org.apache.jackrabbit.oak.spi.security.authorization.principalbased.impl.PrincipalBasedAuthorizationConfiguration defines the following configuration parameters:

Parameter Type Default Description
PARAM_ENABLE_AGGREGATION_FILTER boolean false Flag to enable the aggregration filter.
PARAM_RANKING int 500 Ranking within the composite authorization setup.

The principal-based authorization in addition requires a FilterProvider to be configured along side with it in order to be operational (mandatory reference in an OSGi setup). This could either be the example implementation present with the module or a custom implementation.

FilterProvider Implementation

The FilterProvider implementation present with the module limits the effect to system users principals located below the configured subtree. The absolute path of this subtree is a mandatory configuration option with the Apache Jackrabbit Oak Filter for Principal Based Authorization (ConfigurationPolicy.REQUIRE):

Parameter Type Default Description
Path String - Required path underneath which all filtered system user principals must be located in the repository.

Note: It is equally possible to plug a custom FilterProvider implementation matching specific needs (see below).

Pluggability

The following section describes how to deploy this authorization model into an Oak repository and how to customize the FilterProvider extension point.

Deploy PrincipalBasedAuthorizationConfiguration

OSGi Setup

The following steps are required in order to deploy the CUG authorization model in an OSGi-base Oak repository:

  1. Deploy the oak-authorization-principalbased bundle
  2. Configure and activate the built-in FilterProvider or deploy a custom implementation (see below).
  3. Make sure you have the default or a custom MountInfoProvider service running
  4. Optionally configure the PrincipalBasedAuthorizationConfiguration (“Apache Jackrabbit Oak Principal Based AuthorizationConfiguration”)
  5. Find the SecurityProviderRegistration (“Apache Jackrabbit Oak SecurityProvider”) configuration and enter org.apache.jackrabbit.oak.spi.security.authorization.principalbased.impl.PrincipalBasedAuthorizationConfiguration as additional value to the requiredServicePids property.

The third step will enforce the recreation of the SecurityProvider and hence trigger the RepositoryInitializer provided by the principal-based authorization module, that will make sure the required node type definitions are installed.

Non-OSGi Setup

The following example shows a simplified setup that contains the PrincipalBasedAuthorizationConfiguration as additional authorization model (second position in the aggregation). See also unit tests for an alternative approach.

 // setup PrincipalBasedAuthorizationConfiguration
 FilterProvider filterProvider = TODO: define the filter provider;
 MountInfoProvider mip = Mounts.defaultMountInfoProvider();
                 
 PrincipalBasedAuthorizationConfiguration ac = new PrincipalBasedAuthorizationConfiguration();
 ac.bindFilterProvider(filterProvider);
 ac.bindMountInfoProvider(mip);
 // optionally set configuration parameters: ranking, enable aggregationfilter
 
 // bind it to the security provider
 ConfigurationParameters securityConfig = ConfigurationParameters.EMPTY; // TODO define security config options
 SecurityProvider securityProvider = SecurityProviderBuilder.newBuilder().with(securityConfig)
                      .withRootProvider(rootProvider)
                      .withTreeProvider(treeProvider)
                      .build();
 SecurityProviderHelper.updateConfig(securityProvider, ac, AuthorizationConfiguration.class);
                               
 // create the Oak repository (alternatively: create the JCR repository)
 Oak oak = new Oak()
         .with(new InitialContent())
         // TODO: add all required editors
         .with(securityProvider);
         withEditors(oak);
 ContentRepository contentRepository = oak.createContentRepository();

Customize FilterProvider

The following steps are required in order to customize the FilterProvider implementation in a OSGi-based repository setup. Ultimately the implementation needs to be referenced in the org.apache.jackrabbit.oak.spi.security.authorization.principalbased.impl.PrincipalBasedAuthorizationConfiguration.

  1. implement FilterProvider and Filter interface according to you needs,
  2. make your FilterProvider implementation an OSGi service
  3. deploy the bundle containing your implementation in the OSGi container and activate the service.