javaspringspring-boot

Repetitive ownership checks for nested resources in Spring Boot with JWT authentication


I'm building a Spring Boot backend API with JWT authentication. The domain model is:

Authentication is working, but authorization/ownership checks are becoming repetitive and error-prone when I go down into nested endpoints. Examples:

Right now I end up manually passing the User everywhere and doing checks like findAllByOwnerAndId(user, projectId) for example. This repetition is spreading across controllers and services. Is there a cleaner, centralized pattern to handle ownership/authorization for nested resources so I don't have to litter every handler with the same logic?

ProjectController:

@RestController
@RequestMapping("/projects")
public class ProjectController {
    private final ProjectService projectService;

    public ProjectController(ProjectService projectService) {
        this.projectService = projectService;
    }

    @GetMapping
    public ResponseEntity<Iterable<ProjectDTO>> getAll(@AuthenticationPrincipal UserPrincipal principal) {
        return ResponseEntity.ok(projectService.getAllByUser(principal.getUser()));
    }

    @DeleteMapping("/{projectId}")
    public ResponseEntity<?> delete(@PathVariable Integer projectId, @AuthenticationPrincipal UserPrincipal principal) {
        projectService.delete(projectId, principal.getUser());
        return ResponseEntity.ok().build();
    }
}

ExamController:

@RestController
@RequestMapping("/projects/{projectId}/exams")
public class ExamController {
    private final ExamService examService;
    public ExamController(ExamService examService) {
        this.examService = examService;
    }

    @GetMapping
    public ResponseEntity<Iterable<Exam>> getAll(
        @PathVariable Integer projectId,
        @AuthenticationPrincipal UserPrincipal principal
    ) {
        return ResponseEntity.ok(examService.getAllByProject(projectId, principal.getUser()));
    }

    @DeleteMapping("/{examId}")
    public ResponseEntity<?> delete(@PathVariable Integer examId) {
        examService.delete(examId);
        return ResponseEntity.ok().build();
    }
}

ExamService (ownership check inside): This ownership check feels very awkward.

public Iterable<Exam> getAllByProject(Integer projectId, User user) {
    Optional<Project> project = projectRepository.findAllByOwnerAndId(user, projectId);
    if (project.isEmpty()) {
        throw new ProjectNotFoundException(projectId);
    }
    return project.get().getExams();
}

Is there a better architectural or Spring idiomatic way to centralize or eliminate these repetitive ownership checks for nested resources (project → exam → question)?


Solution

  • Create @CheckOwnership annotation to mark methods needing verification. try AOP to intercept calls and validate resource ownership automatically.
    Centralize checks in an AuthorizationService and inject this service wherever needed

    validateProjectOwnership(projectId, user)  
    validateExamOwnership(examId, user)
    

    Also prevalidate ownership using @ModelAttribute methods in a @ControllerAdvice so controllers can safely use the prevalidated resources:

    @RestController
    @RequestMapping("/projects/{projectId}/exams")
    public class ExamController {
        private final ExamService examService;
        
        @GetMapping
        public ResponseEntity<Iterable<Exam>> getAll(@ModelAttribute("project") Project project) {
            return ResponseEntity.ok(examService.getAllByProject(project));
        }
        
        @DeleteMapping("/{examId}")
        public ResponseEntity<?> delete(@ModelAttribute("exam") Exam exam) {
            examService.delete(exam);
            return ResponseEntity.ok().build();
        }
    }