From 5ebe1f1a5ab19b0a1cae7e964dbc39d3376d7e6e Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 28 Apr 2026 21:02:29 +0200 Subject: [PATCH] feat(person): require READ_ALL permission on GET /api/persons and /api/persons/{id} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defense in depth: until now both list and single-person reads only required authentication, while the write endpoints (POST/PUT/DELETE) were already gated with @RequirePermission. The hover-card and typeahead introduced in issue #362 expose person details (life dates, notes, family relationships) to anyone who can authenticate — adding READ_ALL aligns the GETs with the write endpoints and matches the access tier already enforced for documents and transcription blocks. Two new controller-slice tests assert 403 when an authenticated user lacks READ_ALL; existing 200-path tests now stipulate `authorities = "READ_ALL"` explicitly. Refs #362 Co-Authored-By: Claude Opus 4.7 --- .../controller/PersonController.java | 2 ++ .../controller/PersonControllerTest.java | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/PersonController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/PersonController.java index 329a1969..33be9f1b 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/PersonController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/PersonController.java @@ -34,11 +34,13 @@ public class PersonController { private final DocumentService documentService; @GetMapping + @RequirePermission(Permission.READ_ALL) public ResponseEntity> getPersons(@RequestParam(required = false) String q) { return ResponseEntity.ok(personService.findAll(q)); } @GetMapping("/{id}") + @RequirePermission(Permission.READ_ALL) public Person getPerson(@PathVariable UUID id) { return personService.getById(id); } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/PersonControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/PersonControllerTest.java index e31e2ad0..9de8a3a1 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/PersonControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/PersonControllerTest.java @@ -57,6 +57,13 @@ class PersonControllerTest { @Test @WithMockUser + void getPersons_returns403_whenMissingReadAllPermission() throws Exception { + mockMvc.perform(get("/api/persons")) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(authorities = "READ_ALL") void getPersons_returns200_withEmptyList() throws Exception { when(personService.findAll(null)).thenReturn(Collections.emptyList()); mockMvc.perform(get("/api/persons")) @@ -64,7 +71,7 @@ class PersonControllerTest { } @Test - @WithMockUser + @WithMockUser(authorities = "READ_ALL") void getPersons_delegatesQueryParam_toService() throws Exception { PersonSummaryDTO dto = mockPersonSummary("Hans", "Müller"); when(personService.findAll("Hans")).thenReturn(List.of(dto)); @@ -100,6 +107,13 @@ class PersonControllerTest { @Test @WithMockUser + void getPerson_returns403_whenMissingReadAllPermission() throws Exception { + mockMvc.perform(get("/api/persons/{id}", UUID.randomUUID())) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(authorities = "READ_ALL") void getPerson_returns200_whenFound() throws Exception { UUID id = UUID.randomUUID(); Person person = Person.builder().id(id).firstName("Anna").lastName("Schmidt").build();