Saturday, 31 May 2014

Writing custom permission evaluator in Spring

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:
  1. Data repositories
  2. Dependency injection
  3. Configuration components
  4. Expression-Based Access Control

Repositories


We have to create following repositories:
  1. PermissionRepository
  2. ProjectRepository
  3. RoleRepository
  4. UserRepository
  5. 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.