From a73fddefe3fc88d42d628b7364e03453eda501c6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 31 May 2026 12:03:39 +0200 Subject: [PATCH] test(security): lock READ_ALL -> 403 on transcription/annotation writes (#697) Read-only users will soon be able to open the transcription read view, so the write endpoints become the real authorization boundary. Explicitly assert a READ_ALL-only principal is forbidden from create/update/reorder/ review block writes and annotation create/patch (the prior tests only used a no-authority principal). Co-Authored-By: Claude Opus 4.8 --- .../annotation/AnnotationControllerTest.java | 18 ++++++++++ .../TranscriptionBlockControllerTest.java | 35 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/annotation/AnnotationControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/annotation/AnnotationControllerTest.java index 2157f6c7..5305d277 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/annotation/AnnotationControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/annotation/AnnotationControllerTest.java @@ -83,6 +83,15 @@ class AnnotationControllerTest { .andExpect(status().isForbidden()); } + @Test + @WithMockUser(authorities = "READ_ALL") + void createAnnotation_returns403_whenUserHasOnlyReadAllPermission() throws Exception { + mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations").with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(ANNOTATION_JSON)) + .andExpect(status().isForbidden()); + } + @Test @WithMockUser(authorities = "WRITE_ALL") void createAnnotation_returns201_whenHasWriteAllPermission() throws Exception { @@ -190,6 +199,15 @@ class AnnotationControllerTest { .andExpect(status().isForbidden()); } + @Test + @WithMockUser(authorities = "READ_ALL") + void patchAnnotation_returns403_whenUserHasOnlyReadAllPermission() throws Exception { + mockMvc.perform(patch("/api/documents/" + UUID.randomUUID() + "/annotations/" + UUID.randomUUID()).with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(PATCH_JSON)) + .andExpect(status().isForbidden()); + } + @Test @WithMockUser(authorities = "WRITE_ALL") void patchAnnotation_returns200_withWriteAllPermission() throws Exception { diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/transcription/TranscriptionBlockControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/transcription/TranscriptionBlockControllerTest.java index 5cb0769c..1df57ffe 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/transcription/TranscriptionBlockControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/transcription/TranscriptionBlockControllerTest.java @@ -159,6 +159,15 @@ class TranscriptionBlockControllerTest { .andExpect(status().isForbidden()); } + @Test + @WithMockUser(authorities = "READ_ALL") + void createBlock_returns403_whenUserHasOnlyReadAllPermission() throws Exception { + mockMvc.perform(post(URL_BASE).with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(CREATE_JSON)) + .andExpect(status().isForbidden()); + } + @Test @WithMockUser(authorities = "WRITE_ALL") void createBlock_returns201_withSavedBlock_whenAuthorised() throws Exception { @@ -233,6 +242,15 @@ class TranscriptionBlockControllerTest { .andExpect(status().isForbidden()); } + @Test + @WithMockUser(authorities = "READ_ALL") + void updateBlock_returns403_whenUserHasOnlyReadAllPermission() throws Exception { + mockMvc.perform(put(URL_BLOCK).with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(UPDATE_JSON)) + .andExpect(status().isForbidden()); + } + @Test @WithMockUser(authorities = "WRITE_ALL") void updateBlock_returns200_withUpdatedBlock_whenAuthorised() throws Exception { @@ -363,6 +381,15 @@ class TranscriptionBlockControllerTest { .andExpect(status().isForbidden()); } + @Test + @WithMockUser(authorities = "READ_ALL") + void reorderBlocks_returns403_whenUserHasOnlyReadAllPermission() throws Exception { + mockMvc.perform(put(URL_REORDER).with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(REORDER_JSON)) + .andExpect(status().isForbidden()); + } + @Test @WithMockUser(authorities = "WRITE_ALL") void reorderBlocks_returns200_withReorderedBlocks_whenAuthorised() throws Exception { @@ -440,6 +467,14 @@ class TranscriptionBlockControllerTest { .andExpect(jsonPath("$.reviewed").value(true)); } + @Test + @WithMockUser(authorities = "READ_ALL") + void reviewBlock_returns403_whenUserHasOnlyReadAllPermission() throws Exception { + mockMvc.perform(put("/api/documents/{documentId}/transcription-blocks/{blockId}/review", + DOC_ID, BLOCK_ID).with(csrf())) + .andExpect(status().isForbidden()); + } + // ─── PUT .../review-all ─────────────────────────────────────────────────── private static final String URL_REVIEW_ALL = URL_BASE + "/review-all";