Simple Attribute-Based Access Control with Spring Security

Datetime:2016-08-22 23:13:13          Topic:          Share

Introduction

Have you ever worked on software where the access rules are based not only on user's role but also on the specific entity that role was granted on (i.e. Scoped Roles), something like "Project Manager can add users to HIS PROJECT ONLY", "Store Agent can access Store Information for HIS STORE ONLY",  or "Document Owner can modify HIS DOCUMENTS"?

Or, where the access rules are based on context, where the access happens, like time, user-network, or channel (like web-site, mobile-app, some-internal-system, etc.). For example "This resource can be accessed only DURING OFFICE HOURS or ONLY FROM OFFICE NETWORK"?

All of that, and still the logic of these access rules needs to configurable and flexible to be modified with minimum (or no) software coding or new deployment.

Then, you will probably find Attribute-Based Access Control very useful.

What is Attribute-Based Access Control (ABAC)

Any access request will have four elements (subject, resource, action, and environment), where:

  • Subject is the entity (mostly a user) that requests access
  • Resource is the entity to be accessed (e.g. file, database record, Store Information, ...)
  • Action is the operation to be carried on the resource (e.g. read, write, delete, ...)
  • Environment is any information regarding the context of the access that might be used in making the access decision (e.g. time, network, ...).

ABAC is where each of the elements above is represented by a set of attributes. Each of these attributes has a key and one (or many) value(s). For example, subject could have id , name and roles attributes.

The ABAC access rules are based on a relationship of elements' attributes (like subject.id equals resource.ownerId ) or/and element's attribute has/contains specific value (like subject.name equals "Smith", or subject.roles contains "ADMIN" ).

ABAC allows you to define Access Rules with far more fine granularity of access control, relative to other models like Role-Based (RBAC) or Access-Control-List (ACL), without losing any of the capabilities  found in other models (e.g. defining rules based only the user-role as in RBAC).

For more details on ABAC, check NIST Guide to Attribute Based Access Control (ABAC) Definition and Considerations .

For a concise comparison of ABAC and other Access Control models check (quite old, but still informative) NIST: A Survey Of Access Control Models (Draft) .

Spring Security Framework and SpEL

The Spring Security Framework enables the developers to inject their Access-Control logic in centralized component and to be enforced (using  Expression-Based Access Control ) in various execution points of the application, like before/after REST API calls and before/after method calls providing all necessary context (like method parameters or return object) for Access-Control logic to work.

On the other hand, SpEL (Spring Expression Language) is an Expression Language similar to Java EL used in JSP and JSF, it used by default in Spring Security when Expression-Based access control is enabled.

In this article, we will use SpEL as language to define the Access Rules.

Figure below describes the sequence flow for each method call protected by access control:

Inside the AccessDecisionManager the below sequence takes place:

And inside the AfterInvocationManager the below sequence take place:

As shown in theses diagrams, the access decisions are (eventually) delegated to a component called PermissionEvaluator (colored in blue above), and this is where the ABAC logic will be.

Note: If the diagrams appear to be too small, just drag them into a new tab (or download them).

Key Components

The approach presented in this article based on the following ideas:

  • Use boolean Spring EL Expressions to define access rules (e.g.  subject.project.id == resource.project.id ) which will be stored in central repository (e.g. memory, database, LDAP, file)
  • Define a centralized component that load the rules, wrap the elements of access context and evaluate the rules' expressions to decide whether access is granted or denied.
  • Use the Spring annotations -   @PostAuthorize/@PreAuthorize("hasPermission (...))  - (along with other artifacts mentioned later) to enforce the access rules.

The following are the key components of this approach:

PermissionEvaluator

This is the entry point for ABAC logic to be executed. As mentioned before, all access decisions made by Spring Security framework (Expression-Based Access Control) are delegated to this component. I.e. All annotations @PreAuthorize("hasPermision(...)") ,    @PostAuthorize("hasPermision(...)") all are delegated to the this component.

The work presented here creates a custom implementation of this component that just delegates the access decision to the PolicyEnforcement component.

public class AbacPermissionEvaluator implements PermissionEvaluator {
    @Autowired
    PolicyEnforcement policy;

    @Override
    public boolean hasPermission(Authentication authentication , Object targetDomainObject, Object permission) {
        //Getting subject
        Object user = authentication.getPrincipal();

        //Getting environment
        Map<String, Object> environment = new HashMap<>();
        environment.put("time", new Date());

        return policy.check(user, targetDomainObject, permission, environment);
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}

ContextAwarePolicyEnforcement

This an optional component that is similar to PermissionEvaluator but can be called at any point in your code, given that the SecurityContext is available and filled with current, authenticated user information.

This component is used when the data needed to make the access decision is not available to @PreAuthorize and   @PostAuthorize   annotations.

For example, when updating an entity:

  •   @PreAuthorize   will have access only to the method's parameters, which are the updated entity's information, while the access decision needs the information of the existing entity.
  •   @PostAuthorize  will be called after the update is done, which is too late for access decision to be taken.
public class ContextAwarePolicyEnforcement {
    @Autowired
    protected PolicyEnforcement policy;

    public void checkPermission(Object resource, String permission) {
        //Getting the subject
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        //Getting the environment
        Map<String, Object> environment = new HashMap<>();
        environment.put("time", new Date());

        if(!policy.check(auth.getPrincipal(), resource, permission, environment))
        throw new AccessDeniedException("Access is denied");
    }
}

PolicyEnforcement

This is where the actual access decision is taken. It works as follows:

  1. Load all PolicyRule s using the PolicyDefinition
  2. Filter the PolicyRule s leaving only the applicable rules (i.e. rules where the target expression evaluates to true ) in the current access context.
  3. Evaluates all applicable PolicyRule s (i.e. evaluating the condition  expression) in the current access context. If any returned true then access is granted, otherwise, access denied.

The reason that we did not implement the above logic inside the PermissionEvaluator is for decoupling the ABAC logic from the Spring Security and allowing the decision making to be invoked outside the Spring Security Framework (e.g. by calling the PolicyEnforecment directly inside any method's logic).

public class BasicPolicyEnforcement implements PolicyEnforcement {
    @Autowired
    private PolicyDefinition policyDefinition;

    @Override
    public boolean check(Object subject, Object resource, Object action, Object environment) {
        //Get all policy rules
        List<PolicyRule> allRules = policyDefinition.getAllPolicyRules();

        //Wrap the context
        SecurityAccessContext cxt = new SecurityAccessContext(subject, resource, action, environment);

        //Filter the rules according to context.
        List<PolicyRule> matchedRules = filterRules(allRules, cxt);

        //finally, check if any of the rules are satisfied, otherwise return false.
        return checkRules(matchedRules, cxt);
    }

    private List<PolicyRule> filterRules(List<PolicyRule> allRules, SecurityAccessContext cxt) {
        List<PolicyRule> matchedRules = new ArrayList<>();
        for(PolicyRule rule : allRules) {
            try {
                if(rule.getTarget().getValue(cxt, Boolean.class)) {
                    matchedRules.add(rule);
                }
            } catch(EvaluationException ex) {
                logger.error("An error occurred while evaluating PolicyRule.", ex);
            }
        }
        return matchedRules;
    }

    private boolean checkRules(List<PolicyRule> matchedRules, SecurityAccessContext cxt) {
        for(PolicyRule rule : matchedRules) {
            try {
                if(rule.getCondition().getValue(cxt, Boolean.class)) {
                    return true;
                }
            } catch(EvaluationException ex) {
                logger.error("An error occurred while evaluating PolicyRule.", ex);
            }
        }
        return false;
    }
}

PolicyDefinition

This interface represents the PolicyRule repository. It has one method getAllPolicyRules that loads all available policy rules.

This interface hides the details of how-policy-rules-are-stored from the policy clients. This component could be implemented for (but not limited to) in-memory policy, JSON-file policy, or database policy. The details for each repository format is up to the implementer of this component.

PolicyRule

This is the atomic element of access policy, this is where the ABAC logic is defined to be evaluated when needed.

PolicyRule has the following main properties:

  • target: A SpEL boolean expression where this rule is applicable (i.e. if the expression evaluates to true, then this rule is applicable)
  • condition: A SpEL boolean expression where this rule is satisfied (i.e. if the expression evaluates to true, then access is granted)

Both expressions have access to the four elements of access-request, namely subject, resource, action, and environment.

For sake of simplicity, the Rules have positive effect only (i.e. granting access if satisfied) but they can easily be extended to have negative effect also (i.e. denying access if satisfied)

SecurityAccessContext

This is a wrapper class for all the access elements. It has fields for each of Subject, Resource, Action, and Environment.

For each access decision to be taken, an instance of this class is created by PolicyEnforcement and filled with corresponding access elements.

The instances of this class serve as Root object for evaluating the PolicyRule s expressions.

Sample Application

I have made a sample application to illustrate the details of the ABAC using Spring Security.

Overview

The application is very simple issue tracking system, where there are set of projects and each project has Project Manager (PM) and set of Testers and set of Developers

It is REST APIs using Spring MVC (no GUI), and source code can be found here .

For simplicity, all data are stored in memory.

Business overview

Users

There are 4 types of users: Admins, Project Managers, Testers, and Developers

For simplicity, users can have one role only.

Projects

Issues are grouped in projects, each project is created and managed through the application.

Each Project can have one Project Manager and many Testers or/and Developers.

For simplicity, users can be assigned to one project only.

Issues and Status

Issues can be created and managed through the application.

There two types of issues: Tasks and Bugs

Each issue has a status. Issues Status can be any: NEW, ASSIGNED, COMPLETED

Issues can be assigned to Users, and authorized users can change the Issues status.

Access Rules

Below are sample access rules and their translation to ABAC SpEL expression:

  • Admin can do all

    • Target:  subject.role.name() == 'ADMIN'
    • Condition: true
  • PM can add new issues to his project only.
    • Target:  subject.role.name() == 'PM' && action == 'ISSUES_CREATE'
    • Condition:  subject.project.id == resource.project.id
  • Tester can add bugs (and only bugs) to his project

    • Target:  subject.role.name() == 'TESTER' && action == 'ISSUES_CREATE'
    • Condition:  subject.project.id == resource.project.id && resource.type.name() == 'BUG'
  • Users can complete issues assigned to them.

    • Target:  action == 'ISSUES_STATUS_CLOSE'
    • Condition:  subject.project.id == resource.project.id && subject.name == resource.assignedTo

All access rules are stored at this JSON file .

Challenges and Enhancement

The work presented here, despite its flexibility, does not cover all concerns that will be faced when using it in real-world applications, below are some of them:

Performance issues

Loading, filtering, and checking policy rules for each access decision could have considerable impact in the application performance. This could be reduced by the following:

  1. Caching access decisions (i.e. the results of   PolicyEnoforecment#check(..) )
  2. Caching PolicyRules (i.e. the results of   PolicyDefinition#getAllPolicyRules() )
  3. Using compiled SpEL, for more detail check here .

More Elaborate Policy

The access rules presented here are simple (one-level) rule which can be enriched by the following:

  1. Introducing PolicySet s that are groups of policy rules and other nested PolicySet s, each PolicySet will have its own condition and target expressions, something like XACML standard .
  2. PolicyRules could have a negative effect.

Policy Rules Validation

The policy rules defined in article are taken as valid, but in production environments, these rules need to be validated before storing them in the policy repository. For example, expressions should be limited to access only the access context elements (subject, resource, action, and environment)

Policy Repository

In the sample application, I have used in-memory, and static-JSON-file to store the PolicyRules, but in practice, more complex repositories should be used, like Database, or LDAP.

The entry for this work is by presenting new implementation of PolicyDefinition.

Policy Editor

We did not cover how the rules are defined. In real-world applications, there should be   a tool (probably with GUI) to define those rules allowing Admins and maybe SMEs to define and manage these rules with little (or no) programming experience required.