From 769984608bf11dd6995b707c825dfaffd535c8ec Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 22 May 2026 17:21:01 +0200 Subject: [PATCH] test(observability): expand grafana_reader coverage with write-deny + PII negatives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original 4 tests asserted SELECT existed on the three granted tables and was absent on app_users. That left two gaps a future migration could slip through silently: - INSERT/UPDATE/DELETE on the granted tables — if someone GRANTed write access on, say, documents to grafana_reader, the SELECT positives stay green and the boundary is breached invisibly. - Other PII / sensitive tables — the single app_users negative checks one table; a wildcard "GRANT SELECT ON ALL TABLES IN SCHEMA public" would still leave it green by accident if app_users wasn't the only sensitive table. Switch to a hasPrivilege(table, privilege) helper, add three write-deny tests (INSERT/UPDATE/DELETE on each granted table), and replace the single app_users negative with a parameterized sweep over app_users, user_groups, persons, notifications, document_comments, document_annotations, geschichten. New sensitive tables get added to that list as they appear. Co-Authored-By: Claude Opus 4.7 --- .../GrafanaReaderRoleIntegrationTest.java | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/config/GrafanaReaderRoleIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/config/GrafanaReaderRoleIntegrationTest.java index f930b096..d7c3ccec 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/config/GrafanaReaderRoleIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/config/GrafanaReaderRoleIntegrationTest.java @@ -1,6 +1,8 @@ package org.raddatz.familienarchiv.config; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.raddatz.familienarchiv.PostgresContainerConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; @@ -10,6 +12,9 @@ import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThat; +// GRAFANA_DB_PASSWORD is supplied via the global test default in +// src/test/resources/application.properties — FlywayConfig fails closed +// when it is unset, so all tests that load the migration path need it. @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @Import({PostgresContainerConfig.class, FlywayConfig.class}) @@ -17,31 +22,68 @@ class GrafanaReaderRoleIntegrationTest { @Autowired JdbcTemplate jdbc; + // --- positive grants (SELECT on the three explicitly granted tables) --- + @Test void grafana_reader_has_select_on_audit_log() { - assertThat(hasSelect("audit_log")).isTrue(); + assertThat(hasPrivilege("audit_log", "SELECT")).isTrue(); } @Test void grafana_reader_has_select_on_documents() { - assertThat(hasSelect("documents")).isTrue(); + assertThat(hasPrivilege("documents", "SELECT")).isTrue(); } @Test void grafana_reader_has_select_on_transcription_blocks() { - assertThat(hasSelect("transcription_blocks")).isTrue(); + assertThat(hasPrivilege("transcription_blocks", "SELECT")).isTrue(); + } + + // --- write-deny on the granted tables: SELECT-only means SELECT-only. + // A future migration that GRANTs INSERT/UPDATE/DELETE on any of these + // would fail these tests, even though the original positive grants still + // pass. Locks the boundary in both directions. + + @Test + void grafana_reader_has_no_INSERT_on_documents() { + assertThat(hasPrivilege("documents", "INSERT")).isFalse(); } @Test - void grafana_reader_has_no_select_on_app_users() { - assertThat(hasSelect("app_users")).isFalse(); + void grafana_reader_has_no_UPDATE_on_audit_log() { + assertThat(hasPrivilege("audit_log", "UPDATE")).isFalse(); } - private boolean hasSelect(String table) { + @Test + void grafana_reader_has_no_DELETE_on_transcription_blocks() { + assertThat(hasPrivilege("transcription_blocks", "DELETE")).isFalse(); + } + + // --- negative grants: PII / sensitive tables MUST NOT be readable. + // The parameterized form catches the "someone widened the grant to + // ALL TABLES IN SCHEMA public" footgun — three specific positive grants + // would still pass while this sweep turns red. + + @ParameterizedTest + @ValueSource(strings = { + "app_users", + "user_groups", + "persons", + "notifications", + "document_comments", + "document_annotations", + "geschichten" + }) + void grafana_reader_has_no_SELECT_on_protected_table(String table) { + assertThat(hasPrivilege(table, "SELECT")).isFalse(); + } + + private boolean hasPrivilege(String table, String privilege) { Boolean result = jdbc.queryForObject( - "SELECT has_table_privilege('grafana_reader', ?, 'SELECT')", + "SELECT has_table_privilege('grafana_reader', ?, ?)", Boolean.class, - table); + table, + privilege); return Boolean.TRUE.equals(result); } }