From 47859e5a9bb73a4c5f7c4fcb2eb255c520fd7d1c Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 20 Apr 2026 21:14:24 +0200 Subject: [PATCH] feat(documents): retrofit WRITE_ALL guard on /incomplete-count + /incomplete/next Closes the CWE-285 gap Nora flagged on issue #296: both endpoints expose enrichment-queue information that only writers should see. Brings them in line with the new /incomplete list endpoint and every other write-path under DocumentController. Frontend callers (/enrich/[id]/+page.server.ts) already gate on WRITE_ALL at the route level, so no client-side change is needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../controller/DocumentController.java | 2 ++ .../controller/DocumentControllerTest.java | 21 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java index 359e8355..e5f0bb17 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -194,6 +194,7 @@ public class DocumentController { } @GetMapping("/incomplete-count") + @RequirePermission(Permission.WRITE_ALL) public Map getIncompleteCount() { return Map.of("count", documentService.getIncompleteCount()); } @@ -207,6 +208,7 @@ public class DocumentController { } @GetMapping("/incomplete/next") + @RequirePermission(Permission.WRITE_ALL) public ResponseEntity getNextIncomplete(@RequestParam UUID excludeId) { return documentService.findNextIncompleteDocument(excludeId) .map(ResponseEntity::ok) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java index f226f8f4..9976a83f 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java @@ -382,7 +382,7 @@ class DocumentControllerTest { } @Test - @WithMockUser + @WithMockUser(authorities = "WRITE_ALL") void getIncompleteCount_returns200_withCount() throws Exception { when(documentService.getIncompleteCount()).thenReturn(3L); @@ -391,6 +391,13 @@ class DocumentControllerTest { .andExpect(jsonPath("$.count").value(3)); } + @Test + @WithMockUser(authorities = "READ_ALL") + void getIncompleteCount_returns403_forReaderOnly() throws Exception { + mockMvc.perform(get("/api/documents/incomplete-count")) + .andExpect(status().isForbidden()); + } + // ─── GET /api/documents/incomplete ─────────────────────────────────────── @Test @@ -442,7 +449,7 @@ class DocumentControllerTest { } @Test - @WithMockUser + @WithMockUser(authorities = "WRITE_ALL") void getNextIncomplete_returns200_whenNextExists() throws Exception { UUID excludeId = UUID.randomUUID(); Document next = Document.builder() @@ -456,7 +463,15 @@ class DocumentControllerTest { } @Test - @WithMockUser + @WithMockUser(authorities = "READ_ALL") + void getNextIncomplete_returns403_forReaderOnly() throws Exception { + mockMvc.perform(get("/api/documents/incomplete/next") + .param("excludeId", UUID.randomUUID().toString())) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(authorities = "WRITE_ALL") void getNextIncomplete_returns204_whenNoneRemain() throws Exception { UUID excludeId = UUID.randomUUID(); when(documentService.findNextIncompleteDocument(excludeId)).thenReturn(Optional.empty());