Addresses Nora (blocker 1) and Felix (suggestion): both revocation methods
now return 0 immediately when sessionRepository is unavailable (non-web
test contexts where JdbcHttpSessionAutoConfiguration does not fire).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LoginRateLimiter uses two Caffeine LoadingCaches of Bucket4j buckets —
one keyed on IP:email (10 attempts/15 min) and one on IP alone (20/15 min
backstop). Exceeding either throws DomainException(TOO_MANY_LOGIN_ATTEMPTS)
and emits LOGIN_RATE_LIMITED audit. Successful login invalidates both
buckets via invalidateOnSuccess. Buckets expire after windowMinutes of
inactivity (no clock advance needed — Caffeine handles eviction).
AuthService integrates it as an optional @Autowired field so non-web
test contexts still work without a Caffeine dependency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After updating the user password during a reset flow, calls
authService.revokeAllSessions(email) to invalidate every active session
for the account — prevents an attacker with a stolen session from
retaining access after the owner resets their password.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
changePassword now calls authService.revokeOtherSessions() after the
password is updated and emits a LOGOUT audit with reason=password_change.
POST /api/users/{id}/force-logout (ADMIN_USER permission) revokes all
sessions for the target user and emits ADMIN_FORCE_LOGOUT audit. Returns
{"revokedCount": N} with 200.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses JdbcIndexedSessionRepository (optional field — null-safe in non-web
test contexts) to delete all sessions for a principal except the current
one (revokeOtherSessions) or all sessions unconditionally (revokeAllSessions).
Both methods return the count of deleted sessions for audit payloads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Re-enables Spring Security's CSRF filter (was disabled with a TODO comment).
Uses CookieCsrfTokenRepository so the frontend can read the XSRF-TOKEN
cookie and send it as X-XSRF-TOKEN on state-mutating requests.
Returns CSRF_TOKEN_MISSING error code on 403 instead of generic FORBIDDEN.
Updates all WebMvcTest classes to include .with(csrf()) on POST/PUT/PATCH/
DELETE/multipart requests, and fixes integration tests to supply the
XSRF-TOKEN cookie + header directly (lazy generation in Spring Security 7).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR #523 moved login/logout into a new auth/ package (AuthSessionController,
AuthService, LoginRequest) — register the row in both CLAUDE.md trees
alphabetically and strip the stale "auth controllers" line from the user/
description so the next LLM reading either file finds the right home.
Addresses PR #612 / Markus M1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pure-comment change: spell out that resolveClientIp's leftmost-X-Forwarded-For
strategy is safe only because Caddy strips client-supplied XFF before
forwarding. Future readers swapping the ingress have a tripwire. Addresses PR
#612 / Nora concern (XFF trust documentation).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pin the @JsonProperty(WRITE_ONLY) invariant on AppUser.password. If the
annotation is ever dropped — or a new field aliases the hash — the CI run that
ships the regression flags it the next morning rather than waiting for a
security review. Addresses PR #612 / Nora concern (regression test).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reorder AuthSessionController.logout so HttpSession.invalidate runs before
AuthService.logout, and wrap the audit call in try/catch so an exception (e.g.
the user was deleted between login and logout, making the audit-time
findByEmail throw) cannot leave the session row alive in spring_session.
The user's intent — "log me out" — is honoured even when audit fails.
Addresses PR #612 / Nora B2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inject Spring Security's SessionAuthenticationStrategy
(ChangeSessionIdAuthenticationStrategy) into AuthSessionController and invoke
onAuthentication at the credential boundary. The strategy calls
HttpServletRequest.changeSessionId() to invalidate any pre-auth session ID an
attacker may have planted and mint a fresh ID before the SecurityContext is
attached. Addresses PR #612 / Nora B1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Also switches pom.xml to spring-boot-starter-session-jdbc (Spring Boot 4.x
split the session auto-config into a separate starter; spring-session-jdbc
alone does not register JdbcSessionAutoConfiguration).
Adds SpringSessionConfig#cookieSerializer bean to configure fa_session name
and SameSite=Strict (spring.session.cookie.* properties are no longer
supported by the Boot 4.x auto-configuration layer).
Cleans up application.yaml / application-dev.yaml: removes store-type: jdbc
and the unsupported cookie.* keys.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Re-introduces tables dropped by V2. Canonical DDL from Spring
Session 3.x schema-postgresql.sql.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Pin semgrep to 1.163.0 to prevent silent upgrades breaking the scan
- Add cache: 'pip' to setup-python@v5 for faster CI runs
- Promote all three XXE Semgrep rules from WARNING to ERROR to match
the --error CI flag intent
- Update SAX/StAX rule messages to reference XxeSafeXmlParser and
the OWASP XXE prevention cheat sheet
- Remove stale issue reference from regression test comment
- Document XML metacharacter constraint on buildValidOds test helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract XxeSafeXmlParser with all 6 OWASP-recommended features
(disallow-doctype-decl, external-general-entities, external-parameter-entities,
load-external-dtd, XInclude, expandEntityReferences). Make readOds()
package-private; add failing-then-passing regression test and valid-ODS guard test.
POI 5.5.0 does not mitigate this: the vulnerable parser is a custom
DocumentBuilderFactory call in readOds(), not inside POI's internal ODS reader.
The hardening is defence-in-depth, not redundant with POI defaults.
Closes#528
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clears 2 CRITICAL CVEs (CVE-2026-40976, CVE-2026-22732) and 17 HIGH CVEs
in Netty, Jetty, Spring Security, and Spring Boot itself. Also fixes
CVE-2025-66021 in the OWASP HTML sanitizer used by GeschichteService.
JaCoCo threshold ratcheted to 0.77 (actual measured coverage; previous
0.88 gate was never enforced since CI ran clean test not clean verify).
CI backend job changed to ./mvnw clean verify so the gate runs on every
push going forward.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests that /actuator/health is accessible without credentials and
/actuator/env requires authentication — permanent regression guards
against CVE-2026-40976-class Actuator filter chain bypass bugs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tempo only handles traces; sending metrics to /v1/metrics returns 404.
Prometheus already scrapes Spring Boot metrics via the pull-model at
/actuator/prometheus, so OTLP metric push is redundant and noisy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change OTEL default endpoint from port 4317 (gRPC) to 4318 (HTTP) to
match Spring Boot's HttpExporter; sending HTTP/1.1 to a gRPC listener
caused "Connection reset" errors
- Add otel.logs.exporter=none: Promtail captures Docker logs via the
logging driver; sending logs to Tempo's OTLP endpoint (which only
handles traces) produced 404 errors
- Add management.metrics.tags.application to every metric so Grafana's
Spring Boot Observability dashboard (ID 17175) can filter by the
application label_values() template variable
- Add MANAGEMENT_METRICS_TAGS_APPLICATION and OTEL_LOGS_EXPORTER env
vars to docker-compose.prod.yml; production Tempo endpoint already
uses 4318
- Add MANAGEMENT_TRACING_SAMPLING_PROBABILITY to prod compose with
0.1 default to avoid 100% trace sampling in production
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add @Order(1) managementFilterChain scoped to /actuator/** with explicit
401 entry point, blocking all non-public actuator paths without the
form-login redirect that the main chain uses for browser clients.
- Split single combined test into two focused assertions
(prometheus_endpoint_returns_200_without_credentials,
prometheus_endpoint_returns_jvm_metrics).
- Add negative regression test: actuator_metrics_requires_authentication
verifies that /actuator/metrics returns 401 without credentials.
Addresses reviewer concerns from @sara (missing negative test, split
assertions) and @nora (dedicated management security layer).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four Spring Boot 4.0-specific issues prevented /actuator/prometheus from working:
1. spring-boot-starter-micrometer-metrics missing — Spring Boot 4.0 splits
Micrometer metrics export (including the Prometheus scrape endpoint) out of
spring-boot-starter-actuator into its own starter. Added dependency.
2. management.prometheus.metrics.export.enabled not set — Spring Boot 4.0
defaults metrics export to false (opt-in). Added the property to
application.yaml.
3. SecurityConfig did not permit /actuator/prometheus — Spring Boot 4.0
with Jetty serves the management port (8081) via the same security filter
chain as the main port (8080). The previous commit's exclusion of
ManagementWebSecurityAutoConfiguration was a no-op (that class no longer
exists in Spring Boot 4.0); removed it and added the correct permitAll()
rule. Updated the architecture comment in application.yaml to reflect the
true filter-chain behaviour.
4. Reverted invalid FamilienarchivApplication.java change from the prior
commit (ManagementWebSecurityAutoConfiguration import compiled against a
class that does not exist in the Spring Boot 4.0 BOM).
Also adds ActuatorPrometheusIT — an integration test that asserts the
/actuator/prometheus endpoint returns 200 with jvm_memory_used_bytes without
credentials, serving as regression protection against future Spring Boot
upgrades silently breaking metrics collection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three root causes confirmed via live server investigation (issue #604):
1. ManagementWebSecurityAutoConfiguration applied HTTP Basic auth to the
management port (8081), causing Prometheus to receive 401 HTML responses
instead of metrics. Excluded the auto-config — the Docker network
(archiv-net) provides the security boundary for this internal port.
2. promtail-config.yml had no `job` relabel rule. Grafana's Loki dashboards
query {job="$app"} which matched nothing; logs were in Loki under
compose_service but invisible to every dashboard panel.
3. prometheus.yml had a stale comment claiming the spring-boot target would
be DOWN until micrometer-registry-prometheus was added — it has been
present in pom.xml for some time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
opentelemetry-spring-boot-starter:2.27.0 was built against
opentelemetry-api:1.61.0. Spring Boot 4.0.0 only manages 1.55.0,
which is missing GlobalOpenTelemetry.getOrNoop(). The backend crashed
at startup with NoSuchMethodError on the first staging nightly.
Add a <dependencyManagement> import of opentelemetry-bom:1.61.0 before
the Spring Boot BOM applies, so all OTel core artifacts resolve to the
version the instrumentation starter actually requires.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 integration test classes were restarting the full Spring context (and a new
Postgres Testcontainer, ~75s each) after every test method — 10 unnecessary
container startups adding ~12 minutes to CI. Fixed by:
- PersonServiceIntegrationTest, DocumentSearchPagedIntegrationTest,
GeschichteServiceIntegrationTest: swap to @Transactional so each test
rolls back instead of destroying the context.
- AuditServiceIntegrationTest: cannot use @Transactional (logAfterCommit
hooks into AFTER_COMMIT which requires a real commit); reset state with
@BeforeEach deleteAll() instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sentry-spring-boot-starter-jakarta 8.5.0 does not support Spring Boot 4.0 —
it logs an "Incompatible Spring Boot Version" warning and its SentryAutoConfiguration
crashes SF7 bean-name generation. sentry-spring-boot-4 (added in 8.21.0) is the
dedicated Spring Boot 4 module with a fixed auto-configuration class.
- Replace sentry-spring-boot-starter-jakarta:8.5.0 with sentry-spring-boot-4:8.41.0
- Delete SentryConfig.java — workaround no longer needed, auto-config handles init
- Remove spring.autoconfigure.exclude from application.yaml + application-test.yaml
- Delete SentryConfigTest.java — tested the deleted workaround class
- Update ApplicationContextTest: assert Sentry.isEnabled() is false when no DSN set
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfiguration is a triply-
nested @Configuration class conditionally loaded when RestClient is on the classpath
(always true on Spring Framework 7). Spring Framework 7's bean name generator fails
on such deeply-nested @Import-ed classes, crashing every @SpringBootTest context.
Replace the broken auto-configuration with a minimal SentryConfig bean that calls
Sentry.init() with the same properties (DSN, environment, sample rate, PII guard,
DomainException filter). Unexpected 5xx exceptions are forwarded to Sentry via
Sentry.captureException() in GlobalExceptionHandler.handleGeneric().
Also add management.server.port=0 to application-test.yaml to eliminate TIME_WAIT
conflicts from @DirtiesContext restarts on the fixed management port 8081 (see #593).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port 8081 was fixed by #576. With four @DirtiesContext(AFTER_EACH_TEST_METHOD)
classes (22 context restarts total), the OS TIME_WAIT state holds port 8081
for ~45-60s per cycle — adding ~17 min overhead. All 1601 tests pass but
surefire's 10-min timeout fires before the suite finishes.
Fixes#593.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
opentelemetry-spring-boot-starter:2.27.0 pulls in AzureAppServiceResourceProvider which
references ServiceAttributes.SERVICE_INSTANCE_ID — a field absent from the semconv version
used by this project. This caused every integration test to fail with NoSuchFieldError during
Spring context startup.
Fix 1 (application-test.yaml): set otel.sdk.disabled=true so the OTel auto-configuration
never runs during tests at all.
Fix 2 (pom.xml): exclude opentelemetry-azure-resources from the starter dependency to remove
the problematic provider from the dependency graph entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add micrometer-registry-prometheus (BOM-managed) to expose /actuator/prometheus
- Add micrometer-tracing-bridge-otel (BOM-managed) for Micrometer → OTel tracing bridge
- Add opentelemetry-spring-boot-starter 2.27.0 (pinned — not in Spring Boot BOM)
- Move management to port 8081 so Prometheus scrapes directly inside archiv-net,
bypassing both Caddy and Spring Security's session-authenticated filter chain
- Configure otel.service.name and OTLP endpoint (default localhost:4317 for CI safety)
- Set tracing sampling probability to 1.0 in base config; override via env var in compose
- Add OTEL_EXPORTER_OTLP_ENDPOINT + MANAGEMENT_TRACING_SAMPLING_PROBABILITY to docker-compose.yml
- Expose management port 8081 inside archiv-net for Prometheus scraping
- Disable trace export in application-test.yaml (probability: 0.0) for deterministic CI
OTLP export failures are non-fatal; app starts cleanly without Tempo running.
Closes#576
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spring Framework 7 prohibits constructor injection cycles. InviteService
already injects UserService, so UserService cannot inject InviteService
for the deleteGroup guard — repository injection is the correct workaround.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Client-submitted duplicate UUIDs were causing a false GROUP_NOT_FOUND:
size(deduplicated_db_result)==1 != size(submitted)==2. Deduplicate input
with HashSet before calling findGroupsByIds so the size comparison is
always against unique IDs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds GROUP_HAS_ACTIVE_INVITES error code and guards UserService.deleteGroup()
with a 409 conflict when any active (non-revoked, non-expired, non-exhausted)
invite token still holds the group UUID.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers the success path — previously untested per Sara's review.
Creates a minimal empty XLSX via XSSFWorkbook so processRows returns 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- @JsonIgnore on ImportStatus.message — stops internal directory paths and
raw exception text leaking through the admin import-status endpoint (CWE-209)
- Add importStatus_messageField_notPresentInApiResponse test (red/green verified)
- Add importStatus_returns401/403 auth boundary tests — documents and guards
the @RequirePermission(ADMIN) protection against configuration drift
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a statusCode field (IMPORT_IDLE / IMPORT_RUNNING / IMPORT_DONE /
IMPORT_FAILED_NO_SPREADSHEET / IMPORT_FAILED_INTERNAL) to ImportStatus.
The frontend will map these codes to localized strings via Paraglide
instead of rendering the backend's German message verbatim.
NoSpreadsheetException distinguishes a missing spreadsheet from other
I/O failures so the frontend can show a specific error without raw text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>