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) <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-20 21:14:24 +02:00
parent 758c708766
commit 47859e5a9b
2 changed files with 20 additions and 3 deletions

View File

@@ -194,6 +194,7 @@ public class DocumentController {
}
@GetMapping("/incomplete-count")
@RequirePermission(Permission.WRITE_ALL)
public Map<String, Long> getIncompleteCount() {
return Map.of("count", documentService.getIncompleteCount());
}
@@ -207,6 +208,7 @@ public class DocumentController {
}
@GetMapping("/incomplete/next")
@RequirePermission(Permission.WRITE_ALL)
public ResponseEntity<Document> getNextIncomplete(@RequestParam UUID excludeId) {
return documentService.findNextIncompleteDocument(excludeId)
.map(ResponseEntity::ok)

View File

@@ -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());