JACLP: ACL Permission library for Spring Security introduces static
ACL-based role permission system with a touch of ABAC (Attribute-based
access control) over resources. It is integrated within Spring Security and its
expression based permission control which might be used from Authorize
-like
annotations over endpoints or generally methods in services. Built with
Java 17.
Installation of the library is possible through maven dependencies, it is hosted on Maven Central. Be sure to fill in the latest version:
<dependency>
<groupId>cz.polankam.security.acl</groupId>
<artifactId>jaclp</artifactId>
<version>!!VERSION!!</version>
</dependency>
With jaclp
library you can define roles with ACL permissions or ABAC
authorization. Role-based ACL defines if action is allowed on resource or not.
ABAC in this implementation is created on top of ACL and adds condition to the
authorization. Condition is resource-specific action which has to be checked
against particular resource object obtained from resource repository. Examples
of simple and complex definition of ACL and ABAC permissions follows.
Define role-based ACL permissions:
Role userRole = RoleBuilder.create("user")
.addAllowedRule("group", "viewAll")
.build();
Define simple ABAC permissions on resource:
Role userRole = RoleBuilder.create("user")
.addAllowedRule("group",
(UserDetails user, GroupEntity group) -> group.isPublic(),
"viewDetail")
.build();
Define permissions with wildcards:
There is one defined wildcard, the asterisk, it can be used as a resource or as
an action. If asterisk is used all resources or actions used in hasPermission
calls are matched against specified permission.
Role superadminRole = RoleBuilder.create("superadmin")
.addAllowedRule("*", "*")
.build();
Define complex ABAC permissions on resource:
Role userRole = RoleBuilder.create("user")
.addAllowedRule("group")
.addAction("viewStats")
.condition(ConditionsFactory.and(
(UserDetails user, GroupEntity group) -> group.isPublic(),
ConditionsFactory.or(
GroupConditions::isVisibleFromNow,
GroupConditions::isSuperGlobal
)
))
.endRule()
.build();
After you defined roles used within your application, the next this is to use
them to actually protect some endpoints or internal APIs. After successful
integration of jaclp
library to Spring
application, permissions are used whenever Spring Security permission expression
hasPermission
is called. Therefore we can use permissions in Authorize
annotations, these annotations should be preferably placed on public endpoints
of your application.
Sample GET endpoints using permission evaluation:
@GetMapping("groups")
@PreAuthorize("hasPermission('group', 'viewAll')")
public List<GroupDTO> getCurrentUser() {
return this.groupService.findAllGroups();
}
@GetMapping("groups/{id}")
@PreAuthorize("hasPermission(#id, 'group', 'viewDetail')")
public GroupDetailDTO getGroupDetail(@PathVariable long id) {
return this.groupService.getGroupDetail(id);
}
There is example project which demonstrates usage and integration of JACLP into the Spring Boot, Spring Data JPA and Spring Security stack. This example is located in separated repository jaclp-demo.
There are two steps which needs to be done after installing jaclp
dependency.
Former is ideally import pre-defined permissions configuration, latter defining
PermissionService
. Configuration is used for creating permission expression
evaluator and integrate it in your project. Permission service on the other hand
should implement IPermissionService
interface and define all user roles and
their permissions within your project.
package app.config;
import cz.polankam.security.acl.AclPermissionEvaluator;
import cz.polankam.security.acl.AuthorizatorService;
import cz.polankam.security.acl.IPermissionsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
/**
* Enable and set method security, most importantly define custom behavior for
* <code>hasPermission</code> authorization methods within authorize
* annotations.
*/
@Configuration
@Import(JaclpSpringConfiguration.class)
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
private final AclPermissionEvaluator permissionEvaluator;
/**
* Note: @Lazy annotation is very important here, it protects evaluator and
* potential autowired classes from not being able to be processed by
* BeanPostProcessor, which handles for example Spring AOP.
*/
@Autowired
public MethodSecurityConfig(@Lazy AclPermissionEvaluator permissionEvaluator) {
this.permissionEvaluator = permissionEvaluator;
}
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
// set custom permission evaluator for hasPermission expressions
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(permissionEvaluator);
return handler;
}
}
Following implementation is only example and it should be different for every
project. The important thing is to implement getRole()
and getResource()
methods to comply with IPermissionService
interface. Get role method should
return Role
object which contains defined permission rules for the given role
identification. Get resource method is used for ABAC authorization and should
return resource repository for given resource identification. If project does
not use ABAC authorization getResource()
can return empty list.
package app.security.acl;
import app.repositories.FileRepository;
import app.repositories.GroupRepository;
import app.security.acl.conditions.GroupConditions;
import cz.polankam.security.acl.IPermissionsService;
import cz.polankam.security.acl.IResourceRepository;
import cz.polankam.security.acl.Role;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class PermissionsService implements IPermissionsService {
private final Map<String, Role> roles = new HashMap<>();
private final Map<String, IResourceRepository> resources = new HashMap<>();
/**
* Default constructor which initialize all user roles used within
* application and assign permission rules to them.
* @param groupRepository
* @param fileRepository
*/
@Autowired
public PermissionsService(
GroupRepository groupRepository,
FileRepository fileRepository
) {
Role user = new Role(Roles.USER);
Role admin = new Role(Roles.ADMINISTRATOR, user);
user.addPermissionRules(
true,
"group",
new String[] {"view"},
GroupConditions::isMember
).addPermissionRules(
true,
"group",
new String[] {"update"},
GroupConditions::isManager
);
admin.addPermissionRules(
true,
"group",
"create"
);
roles.put(user.getName(), user);
roles.put(admin.getName(), admin);
// repositories which will be used to find resources by identification
resources.put("group", groupRepository);
resources.put("file", fileRepository);
}
public boolean roleExists(String role) {
return roles.containsKey(role);
}
public Role getRole(String roleString) {
Role role = roles.get(roleString);
if (role == null) {
throw new RuntimeException("Role '" + roleString + "' not found");
}
return role;
}
public IResourceRepository getResource(String resource) {
IResourceRepository repository = resources.get(resource);
if (repository == null) {
throw new RuntimeException("Resource '" + resource + "' not found");
}
return repository;
}
}