Simple Attribute-Based Access Control With Spring Security
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? You will probably find Attribute-Based Access Control very useful — this article will tell you how.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
Have you ever worked on software where the access rules are based not only on the 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 be 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 out 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) and/or element's attribute has/contains specific values (like subject.name equals "Smith", or subject.roles contains "ADMIN").
ABAC allows you to define Access Rules with a far finer 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
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 the language to define the Access Rules.
The 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 takes 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 is 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 a central repository (e.g. memory, database, LDAP, file).
- Define a centralized component that loads 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(...)")
are delegated to the 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 an 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:
- Load all PolicyRules using the PolicyDefinition.
- Filter the PolicyRules leaving only the applicable rules (i.e. rules where the target expression evaluates to true) in the current access context.
- Evaluates all applicable PolicyRules (i.e., evaluating the condition expression) in the current access context. If any returned true then access is granted; otherwise, access is 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 the sake of simplicity, the Rules have a positive effect only (i.e., granting access if satisfied) but they can easily be extended to have negative effects 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 the Root object for evaluating the PolicyRules expressions.
Sample Application
I have made a sample application to illustrate the details of the ABAC using Spring Security.
Overview
The application is a very simple issue tracking system, where there are set of projects and each project has a Project Manager (PM) and a set of Testers and a set of Developers.
It is made up of REST APIs using Spring MVC (no GUI), and the source code can be found here.
For simplicity, all data is stored in memory.
Business Overview
Users
There are 4 types of users: Admins, Project Managers, Testers, and Developers.
For simplicity, users can only have one role.
Projects
Issues are grouped into projects, each project is created and managed through the application.
Each Project can have one Project Manager and many Testers and/or 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 an 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 in this JSON file.
Challenges and Enhancement
The work presented here, despite its flexibility, does not cover all the 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 a considerable impact on the application's performance. This could be reduced by the following:
- Caching access decisions (i.e. the results of
PolicyEnoforecment#check(..)
). - Caching PolicyRules (i.e. the results of
PolicyDefinition#getAllPolicyRules()
). - Using compiled SpEL, for more detail check here.
More Elaborate Policy
The access rules presented here are simple (one-level) rules which can be enriched by the following:
- Introducing PolicySets that are groups of policy rules and other nested PolicySets. Each PolicySet will have its own condition and target expressions, something like XACML standard.
- PolicyRules could have a negative effect.
Policy Rules Validation
The policy rules defined in the 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 an in-memory, and static-JSON-file to store the PolicyRules, but in practice, more complex repositories should be used, like Database, or LDAP.
To enter this work into your JSON file use the new implementation, PolicyDefinition.
Policy Editor
We did not cover how the rules are defined. In real-world applications, there should be a tool (probably with a GUI) to define those rules allowing Admins and maybe SMEs to define and manage these rules with little (or no) programming experience required.
Related Work
- Attribute Based Access Control for APIs in Spring Security is very similar to this article, but it is quite elaborate and no source code was provided.
Opinions expressed by DZone contributors are their own.
Comments