Let's imagine I have a SpringBoot
app with the following requirements.
- It has a
User
class (entity) - Each user has zero or more
Workspaces
(one-to-many entity relationship) - Each
Workspace
has zero or moreWorkItems
(one-to-many entity relationship)
There is a CRUD REST API controller to manage all of the entities, i.e. we have
UserController
-> CRUD operations forUser
entityWorkspaceController
-> CRUD operations forWorkspace
entityWorkItemContoller
-> CRUD operations forWorkItem
entity
Now there are requirements that ...
- A
User
can only create/edit/delete his ownWorkspace
entities - A
User
can only create/edit/deleteWorkItem
entities in his ownWorkspaces
Also, assume that the User
entity is integrated with SpringSecurity
and we know the current user in the contoller and service.
Then the question is ...
What is the most elegant/clean/maintainable way to implement user permission checks ? How do we write the code, which in the Service
classes will check that the user is authorized to perform operations on a given resource.
The way I do it now is that there's a class like this, which checks the permissions in every Service
call.
class PermissionManager {
void checkUserAllowedToUseWorkspace(User u, Workspace w);
void checkUserAlloweToUseWorkitem(User u, WorkItem)
}
As you can see ... as the number of scoped resources will grow ... this class will get super bloated and hard to maintain.
Is anyone aware of a better way to do this scoped resource access in a clean and maintainable way ?
CodePudding user response:
The most clean and maintainable solution would be to leverage Spring Security AOP for the task.
You can use the @PreAuthorize
annotation, paired with your PermissionManager
service to allow or reject access at the Controller
level harnessing the power of Spring Expression Language.
Having defined a Service
stereotype that checks the user access to a particular resource (the workspace in your example):
@Service
public class PermissionManagerImpl implements PermissionManager {
@Autowired
private UserRepository userRepository;
/**
* @param authentication the current authenticated user following your authentication scheme
* @param workspaceId the workspace (or other resource) identifier
*/
@Override
public boolean checkUserAllowedToUseWorkspace(Authentication authentication, Long workspaceId) {
return authentication != null
/* check that the `authentication` has access to the argument workspace: e.g. userRepository.findWorkspaceByUserNameAndWorkspaceId(authentication.getName(), workspaceId) != null */;
}
}
You can define an expression based control policy on your Controller
method as follows:
@RestController
public class WorkspaceController {
// the DIed `permissionManagerImpl` service will be called prior to your endpoint invocation with the current `authentication` and workspace `id` injected
@PreAuthorize("@permissionManagerImpl.checkUserAllowedToUseWorkspace(authentication, #id)")
@RequestMapping("/workspaces/{id}")
public List<Workspace> getWorkspaces(@PathVariable Long id) {
// retrieve the user workspaces once authorized
}
}
This will lead a readable and reusable solution acting on the top level resource, the Controller
.
You can learn more on Expression-based Access Control in the official Spring docs.
CodePudding user response:
Some database queries for Workspace Entity :
// DELETE
@Transactional
void deleteByIdAndUserId(String workspaceId, String userId);
userRepository.deleteByIdAndUserId(workspaceId, userId);
// UPDATE
Optional<Workspace> workspace = workspace.findByIdAndUserId(String workspaceId, String userId);
Workspace workspace = workspace.orElseThrow(() -> new RuntimeException("You don't have an workspace under user "))
workspace.setName("MyWorkspace2");
//CREATE
User user = userRepository.findById(SecurityContextHolder...getPriciple().getId())
.orElseThrow(() -> new RuntimeException("User can not be found by given id"));
workspaceRepository.save(WorkspaceBuilder.builder().user(user).name("firstWorkspace").build());