Lets say we have to implement permission engine in our application.
I will describe requirements during presentation of domain classes.
Model domain
Firstly. We have bunch of users.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | @Entity
public class User {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String login;
@Column(nullable = false)
private String password;
@ManyToOne(targetEntity = UserRole.class, optional = false)
@JoinColumn(name = "user_role")
private UserRole userRole;
}
|
Each user has some standard attributes like login and password which are not important to us. What is important is user role. In most simplified solution user role can be either admin or "standard role".
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | @Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Role {
@Id
private String roleName;
@ManyToMany
@JoinTable(
name = "role_permissions"
)
private Set<Permission> permissions;
}
@Entity
public class UserRole extends Role {
}
|
Each role comes with set of permissions. In example admin can block users, create new users, add news to home page etc.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | @Entity
public class Permission {
@Id
private String permissionName;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Permission that = (Permission) o;
if (!permissionName.equals(that.permissionName)) return false;
return true;
}
@Override
public int hashCode() {
return 31 * permissionName.hashCode();
}
}
|
These classes describes what user can and cannot do in our application. But this is not everything we have to check.
Users can be assigned to projects.
1
2
3
4
5
6
7
8
9
10
11
12 | @Entity
public class Project {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = true)
private String description;
}
|
If user is assigned to project he plays some role in this project. User can be project manager, developer, client etc.
1
2
3 | @Entity
public class ProjectRole extends Role {
}
|
To model this connection there is created additional entity.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | @Entity
public class UserProjectRole {
@Id
@GeneratedValue
private long id;
@ManyToOne(optional = false)
private User user;
@ManyToOne(optional = false)
private Project project;
@ManyToOne(optional = false)
private ProjectRole projectRole;
}
|
Working with this domain model we have to provide easy to use permission checker which handles two types of permissions: user permissions and project permissions.
Spring environment
Choosing Spring as our framework give us great possibilities. We are going to use following features:
- Data repositories
- Dependency injection
- Configuration components
- Expression-Based Access Control
Repositories
We have to create following repositories:
- PermissionRepository
- ProjectRepository
- RoleRepository
- UserRepository
- UserProjectRepository
All are similar to:
1
2
3
4
5 | @Repository
public interface UserProjectRoleRepository extends CrudRepository<UserProjectRole, Long> {
UserProjectRole findByUserAndProject(User user, Project project);
List<UserProjectRole> findByUser(User user);
}
|
We also have to create
UserService which will provide method to get currently logged user.
Basic permission checker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | @Component
public class PermissionChecker {
private final UserService userService;
private final UserProjectRoleRepository userProjectRoleRepository;
@Autowired
public PermissionChecker(UserService userService, UserProjectRoleRepository userProjectRoleRepository) {
this.userService = userService;
this.userProjectRoleRepository = userProjectRoleRepository;
}
public boolean hasProjectPermission(Project project, Permission permission) {
User user = userService.getLoggedUser();
UserProjectRole userProjectRole = userProjectRoleRepository.findByUserAndProject(user, project);
if(userProjectRole != null) {
return hasPermission(userProjectRole.getProjectRole().getPermissions(), permission);
} else {
return false;
}
}
public boolean hasPermission(Permission permission) {
User user = userService.getLoggedUser();
return hasPermission(user.getUserRole().getPermissions(), permission);
}
private boolean hasPermission(Set<Permission> permissions, Permission permission) {
return permissions
.stream()
.filter(setPermission -> setPermission.equals(permission))
.findFirst().isPresent();
}
}
|
This component has two public methods (except constructor). One for testing project permissions and one for testing user permissions.
In both methods we have to obtain permissions set and check if it contains our permission.
Annotation-based permission checking
We want to check our permissions in following way:
1
2
3 | @PreAuthorize("hasPermission(#id, 'Project', 'project:delete') || hasPermission(#id, 'Project', 'project:everything')")
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
public void delete(@PathVariable("id") Long id) {}
|
Using
PermissionChecker everywhere is redundant, time-consuming, error-prone and (most important) looks ugly.
How to achieve our goal?
Custom permission evaluator
Permission evaluator is class which handles invocations of hasPermission methods written inside
@PreAuthorize annotation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 | public class CustomPermissionEvaluator implements PermissionEvaluator {
public static final String PROJECT_PERMISSION_TYPE = "Project";
private final ProjectRepository projectRepository;
private final PermissionChecker permissionChecker;
@Autowired
public CustomPermissionEvaluator(ProjectRepository projectRepository, PermissionChecker permissionChecker) {
this.projectRepository = projectRepository;
this.permissionChecker = permissionChecker;
}
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permissionText) {
checkNotNull(permissionText);
Permission permission = new Permission(permissionText.toString());
if(targetDomainObject != null) {
checkArgument(targetDomainObject instanceof Project, "Permissions are allowed only for Project objects");
return permissionChecker.hasProjectPermission((Project) targetDomainObject, permission);
}
return permissionChecker.hasPermission(permission);
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permissionText) {
checkArgument(targetType.equals(PROJECT_PERMISSION_TYPE), "Only project and user specific permission check is allowed");
checkNotNull(permissionText);
Project project = projectRepository.findOne(Long.parseLong(targetId.toString()));
checkNotNull(project);
Permission permission = new Permission();
permission.setPermissionName(permissionText.toString());
return permissionChecker.hasProjectPermission(project, permission);
}
}
|
Each
PermissionEvaluator has to implement two
hasPermission methods. It is required because we can invoke
hasPermission (in
@PreAuthorize) in many ways:
1
2
3 | @PreAuthorize("hasPermission(#id, 'Project', 'project:delete')")
@PreAuthorize("hasPermission(#project, 'project:delete')")
@PreAuthorize("hasPermission(null, 'user:delete')")
|
Methods calls have different argument number.
First one check if user has permission to delete project and we are passing only project id.
Second one check if user has permission to delete project when we have project object.
Third one check if user has permission to delete user form database (we are basing here on user, not project, role).
Enabling our permission evaluator
After writing our own permission evaluator we have to enable it in our application. To do this we have to use
@Configuration annotation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | @Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private ProjectRepository projectRepository;
@Autowired
private PermissionChecker permissionChecker;
public MethodSecurityConfig() {
}
@Autowired
public MethodSecurityConfig(ProjectRepository projectRepository, PermissionChecker permissionChecker) {
this.projectRepository = projectRepository;
this.permissionChecker = permissionChecker;
}
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
CustomPermissionEvaluator permissionEvaluator = new CustomPermissionEvaluator(projectRepository, permissionChecker);
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
}
|
@EnableGlobalMethodSecurity(prePostEnabled = true) enables @PreAuthorize
annotation.