Home > OS >  How to handle the multitenancy problem in Spring Boot applications?
How to handle the multitenancy problem in Spring Boot applications?

Time:11-09

Let's imagine I have a SpringBoot app with the following requirements.

  1. It has a User class (entity)
  2. Each user has zero or more Workspaces (one-to-many entity relationship)
  3. Each Workspace has zero or more WorkItems (one-to-many entity relationship)

There is a CRUD REST API controller to manage all of the entities, i.e. we have

  1. UserController -> CRUD operations for User entity
  2. WorkspaceController -> CRUD operations for Workspace entity
  3. WorkItemContoller -> CRUD operations for WorkItem entity

Now there are requirements that ...

  1. A User can only create/edit/delete his own Workspace entities
  2. A User can only create/edit/delete WorkItem entities in his own Workspaces

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());
  • Related