How to build?
JDK 8+ is required.
mvn clean install
How to use?
To start working with the Easy-ABAC Framework you need to add the easy-abac-{version}.jar to the classpath/module-path or add it as a maven dependency, like this:
<dependency>
<groupId>com.exadel.security</groupId>
<artifactId>easy-abac</artifactId>
<version>${abac-version}</version>
</dependency>
Framework also requires spring-context dependency for not spring-based projects:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
Import framework configuration using @Import
annotation:
@SpringBootApplication
@Import(AbacConfiguration.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
or using @ImportResource
annotation:
@SpringBootApplication
@ImportResource("classpath*:abac-config.xml")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Core Attributes
Action
interface - to define possible actions with entity@Access
annotation - to define custom annotation to restrict access to entity@Id
annotation - to define entity identifier parameter in methodEntityAccessValidator
interface - to define access validation rules for entity(s)@ProtectedResource
and@PublicResource
annotations - to turn on / turn of easy-abac validation respectively
Example
Let's consider simple example: you have resource (entity) Project
, CRUD operations with them and would like to restrict access to the resource.
1. Define available actions for your resource. For example:
public enum ProjectAction implements com.exadel.easyabac.model.core.Action {
VIEW,
UPDATE,
CREATE,
DELETE
}
Actions are used to differentiate access rights to the resource, each authenticated user may have different set of available actions. Further will be described have to attach these actions to user.
2.: Create your entity's entityId annotation :
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface ProjectId {
}
The @ProjectId
annotation will help us define entity identifier parameter in controller or service method:
public ResponseEntity get(@ProjectId @PathVariable("projectId") Long projectId) {...}
3.: Define the annotation which protects your REST resource.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Access(id = ProjectId.class)
public @interface ProjectAccess {
/* required actions, see Step 1 */
ProjectAction[] value();
/* Choose your validator */
Class<? extends EntityAccessValidator> validator() default ProjectValidator.class;
}
The @ProjectAccess
annotation will be used to protect REST methods. Here you should define type of actions created in Step 1 as well as validator.
4.: Implement the EntityAccessValidator interface for this resource, where the authorization logic is implemented:
public class ProjectValidator implements com.exadel.easyabac.model.validation.EntityAccessValidator<ProjectAction> {
@Override
public void validate(ExecutionContext<ProjectAction> context) {
//your validation logic here
}
}
The validator might be defined as simple class as well as Spring component. If your validator throws any exception, the access to the resource is denied. The ExecutionContext have the following attributes you can use for validation and logging:
- Entity identifier (Project identifier in our case)
- Set of actions which are required to access the resource.
- The action class type.
- The
org.aspectj.lang.JoinPoint
which contains more details about protected method.
5.: Protect your REST endpoints using your annotations.
@RequestMapping("/{projectId}")
@ProtectedResource
@ProjectAccess(ProjectAction.VIEW)
public class ProjectController {
@RequestMapping("/get")
public Project get(@ProjectId @PathVariable("projectId") Long projectId) {
// your code here
}
@ProjectAccess(ProjectAction.UPDATE)
@RequestMapping("/update")
public Project update(@ProjectId @PathVariable("projectId") Long projectId) {
//your code here
}
@ProjectAccess(ProjectAction.DELETE)
@RequestMapping("/delete")
public Project delete(@PathVariable("projectId") Long projectId) {
//your code here
}
@PublicResource // turns off EASY-ABAC validation
public Project close() {
//your code here
}
}
@ProtectedResource
is a class-level annotation to turn on easy-abac validation.
All public instance methods will be protected unless @ProtectedResource
is provided on method, to turn off easy-abac validation.
@ProjectAccess(ProjectAction.VIEW)
construction says that only users which have
ProjectAction.VIEW
action will have access right to the Project.
@ProjectAccess
can restricted access as globally when used on class level as locally for particular method.
When used both on class and method levels then set of actions are added up together.
For example, ProjectController.update
requires two actions - ProjectAction.VIEW
& ProjectAction.UPDATE
.
Compile time checks
The easy-abac provides user-friendly compile time checks:
- This will raise compile-time error as
@ProjectId
annotation is missing:
@ProjectAccess(ProjectAction.DELETE)
@RequestMapping("/delete")
public Project delete(@PathVariable("projectId") Long projectId) {
//your code here
}
- This will raise compile-time error as
@ProjectAccess
annotation is missing while@ProjectId
provided:
@RequestMapping("/delete")
public Project delete(@ProjectId @PathVariable("projectId") Long projectId) {
//your code here
}
- This will raise compile-time error as
@ProjectAccess
annotation is missing while resource is marked with@ProtectedResource
globally:
@ProtectedResource
public class ProjectController {
@RequestMapping("/delete")
public Project delete(@ProjectId @PathVariable("projectId") Long projectId) {
//your code here
}
}
- This will raise compile-time error as
value()
is missing while required:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Access(id = ProjectId.class)
public @interface ProjectAccess {
/* required actions, see Step 1 */
ProjectAction[] value();
/* Choose your validator */
Class<? extends EntityAccessValidator> validator() default ProjectValidator.class;
}
- This will raise compile-time error as
validator()
is missing while required:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Access(id = ProjectId.class)
public @interface ProjectAccess {
/* required actions, see Step 1 */
ProjectAction[] value();
}
Custom validator implementation
Let's consider example for validator implementation.
@Component
public class GeneralEntityAccessValidator implements EntityAccessValidator<Action> {
private static final String ERROR_TEMPLATE = "Access to entity[id=%s] denied.";
@Autowired
private ActionProvider actionProvider;
@Autowired
private DemoAuthorization authorization;
@Override
public void validate(ExecutionContext<Action> context) {
Long entityId = context.getEntityId();
Set<Action> availableActions = actionProvider.getAvailableActions(entityId, context.getActionType());
Set<Action> requiredActions = context.getRequiredActions();
Set<Action> missingActions = SetUtils.difference(requiredActions, availableActions);
if (CollectionUtils.isEmpty(missingActions)) {
return;
}
AccessResponse response = new AccessResponse(
authorization.getLoggedUserRole(),
entityId,
missingActions,
context.getJoinPoint().getSignature().toString()
);
throw new AccessException(String.format(ERROR_TEMPLATE, entityId), response);
}
}
ActionProvider
is provider of actions is available for current logged in user.
Here we calculate difference between actions available for user and required actions.
In case when user missing some required actions - AccessException
is thrown.
Further you're free to handle this exception. For example using ExceptionHandler
.
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
private static final String ACCESS_DENIED_PAGE = "403.html";
@ExceptionHandler(AccessException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public String handleAccessDeniedException(AccessException exception, Model model) {
model.addAttribute(exception.getResponse());
return ACCESS_DENIED_PAGE;
}
}
Why actions instead of permissions?
We consider the following concepts:
- Permissions - static user access rights, which cannot be changed depending on any other facts.
- Actions - dynamic user access rights, which can be changed depending on any other facts.
Example:
User has ProjectAction.UPDATE
, but we need to restrict it depending on some project attributes.
For example user should be unable to update projects with status 'closed'.
So project action ProjectAction.UPDATE
is available only for not 'closed' projects.
This can be simply implemented as an action provider,
which takes user static actions and then filtering them using some dynamic attributes.
This also works for static actions.
You can see the framework in action in easy-abac-demo
Project structure: