Skip to content

Commit 5e799f3

Browse files
feat: enforce app-admin role gating and update docs
Restrict app administration to an optional allowlist of Nextcloud admins with middleware-based enforcement and clear 403 handling. Document the new admin setup flow and add focused Docker security test commands for role-gating verification. Made-with: Cursor
1 parent 8076168 commit 5e799f3

21 files changed

Lines changed: 1655 additions & 80 deletions

CHANGELOG.de.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
## [Unreleased]
22

3+
### Hinzugefügt
4+
5+
- **Monatsabschluss: Karenz und Auto-Finalisierung**: Admin-Einstellung `month_closure_grace_days_after_eom` (0–90, Standard 0). Nach Monatsende haben Mitarbeitende so viele Kalendertage zur manuellen Finalisierung; ist der Monat danach noch offen, finalisiert ein täglicher Hintergrundauftrag automatisch (gleicher Snapshot wie manuell). Ausstehende Zeiteintragsfreigaben und offene Abwesenheits-Workflows blockieren die Auto-Finalisierung. Wiederöffnen bleibt Administrator:innen vorbehalten.
6+
- **App-Admin-Whitelist**: Neue Admin-Einstellung `app_admin_user_ids`, um die Administration von ArbeitszeitCheck auf eine ausgewählte Teilmenge der Nextcloud-Admins zu begrenzen. Leere Auswahl bleibt rückwärtskompatibel (alle Nextcloud-Admins dürfen die App verwalten).
7+
- **Docker-Testziel für Security-Role-Gating**: Verdrahtung von `scripts/test-security-role-gating-docker.sh` über `make test-security-role-gating-docker` und `composer test:security-role-gating:docker` für schnelle Autorisierungs-Regressionstests im Container-Setup.
8+
9+
### Geändert
10+
11+
- **Monatsabschluss UX/API**: Klarere Karten-UI, sichtbares Erfolgs-/Fehlerfeedback (WCAG), serverseitiges `canFinalize` mit lokalisierten Sperrgründen; manuelle Finalisierung lehnt zukünftige Kalendermonate ab; Abwesenheits-Workflow (`pending`, `substitute_pending`, `substitute_declined`) zusätzlich zu ausstehenden Zeiteintragskorrekturen; API 401 bei fehlender Anmeldung wo passend; Admin: eigener Abschnitt „Monatsabschluss“; Karenzfeld bleibt editierbar mit Hinweis, dass der Wert gespeichert wird und bei aktivierter Funktion gilt; Wiederöffnen mit durchsuchbarer Mitarbeitenden-Auswahl und klarerer Rollenbeschreibung; Validierungsfehler mit höherem Kontrast über Themes hinweg. Auto-Finalize protokolliert Einzelfehler.
12+
- **Release-/Signatur-Workflow für Integritätsprüfung gehärtet**: `make release-signed` signiert jetzt den entpackten Release-Archivinhalt (nicht den lokalen Entwicklungs-Checkout), prüft verbotene Entwicklungs-Pfade und packt das signierte Archiv für Deployment/App-Store neu.
13+
- **Admin-Autorisierung zentral erzwungen**: Zugriffe auf `AdminController`-Routen werden jetzt per Middleware auf App-Admin-Rechte geprüft; nicht berechtigte angemeldete Nutzer erhalten eine konsistente 403-Seite.
14+
15+
### Dokumentation
16+
17+
- **Deployment-Hinweise ergänzt**: Die Release-Dokumentation fordert nun explizit das Deployment aus dem signierten Tarball und beschreibt das typische Fehlerbild (`.git/*` / `node_modules/*`) bei versehentlicher Signierung eines Dev-Trees.
18+
- **Deployment-Helferskript**: `release/deploy-from-release.sh` hinzugefügt für Deployment aus signierten Release-Archiven mit Sicherheitsprüfungen (verbotene Pfade, erforderliche `signature.json`, optionales Disable/Enable und `occ integrity:check-app`).
19+
- **Admin-Betrieb**: Nutzer-/Entwicklerdokumentation ergänzt um Einrichtung der App-Admin-Whitelist, Rückfallverhalten bei leerer Auswahl und Verifikation des Role-Gatings im Docker-Testlauf.
20+
21+
## 1.1.12 – 2026-04-09
22+
23+
### Hinzugefügt
24+
25+
- **Revisionssichere Monatsfinalisierung (optional)**: Admin-Schalter `month_closure_enabled` (Standard aus). Mitarbeitende können einen vollen Kalendermonat finalisieren; die App speichert kanonischen JSON-Snapshot, SHA-256-Hashkette, Anhänge-Revisionen, Audit-Ereignisse und ein schlankes PDF. Finalisierte Monate sind über normale App-APIs nicht mehr änderbar; Administrator:innen können einen Monat mit Pflichtbegründung wieder öffnen (Audit). Monatsberichte für finalisierte Monate lesen den gespeicherten Snapshot. Datenbank: `at_month_closure`, `at_month_closure_revision` (Migration `Version1014Date20260409120000`).
26+
27+
### Dokumentation
28+
29+
- Nutzerhandbücher (DE/EN), Entwicklerdokumentation und Compliance-Hinweise zu Monatsabschluss, Aufbewahrung und Grenzen (Nachweis in der App, keine QES) ergänzt.
30+
331
## 1.1.11 – 2026-04-09
432

533
### Hinzugefügt

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **Month closure grace period and auto-finalization**: Admin setting `month_closure_grace_days_after_eom` (0–90, default 0). After end-of-month, employees have that many calendar days to finalize manually; if the month is still open afterward, a daily background job finalizes it automatically (same snapshot as manual finalize). Pending time entry approvals and open absence workflow states block auto-finalization. Reopening remains admin-only.
13+
- **App-admin allowlist**: New admin setting `app_admin_user_ids` to restrict ArbeitszeitCheck administration to a selected subset of Nextcloud admins. Empty selection keeps backward-compatible behavior (all Nextcloud admins can administer the app).
14+
- **Security role-gating Docker test target**: Added `scripts/test-security-role-gating-docker.sh` wiring via `make test-security-role-gating-docker` and `composer test:security-role-gating:docker` for fast authorization regression checks in containerized setups.
15+
16+
### Changed
17+
18+
- **Month closure UX and API**: Employee UI uses a clearer card layout, visible feedback for success/errors (WCAG-friendly), server-driven `canFinalize` with localized block reasons (feature off, future month, pending approvals). Manual finalize rejects future calendar months. Absence workflow (`pending`, `substitute_pending`, `substitute_declined`) is enforced alongside pending time entry corrections. Unauthorized API access returns 401 where appropriate. Admin settings: dedicated “Month closure” section; grace-days field stays editable with copy explaining it is saved even when closure is off; reopen uses searchable employee picker and clearer administrator vs. employee wording. Form validation error callouts use higher-contrast text and tinted surfaces across themes. Auto-finalize job logs per-user failures for operations.
19+
- **Release/signing workflow hardened for integrity checks**: `make release-signed` now signs the extracted release archive payload (not the local development checkout), validates forbidden development paths are excluded, and repacks the signed archive for deployment/App Store upload.
20+
- **Admin authorization enforcement**: Access to `AdminController` routes now uses middleware-level app-admin checks with a dedicated exception and a consistent 403 response page for authenticated users without app-admin rights.
21+
22+
### Documentation
23+
24+
- **Deployment guidance**: Release docs now explicitly require production deployment from the signed tarball only and document the common integrity-failure pattern (`.git/*` / `node_modules/*` lists) caused by signing a dev tree.
25+
- **Deployment helper script**: Added `release/deploy-from-release.sh` to deploy from signed release archives with safety checks (forbidden path scan, required `signature.json`, optional app disable/enable and `occ integrity:check-app`).
26+
- **Admin operations**: User/developer docs now describe how to configure app-admin allowlisting, what the default fallback is, and how to verify authorization gating in Docker-based test runs.
27+
28+
## 1.1.12 - 2026-04-09
29+
30+
### Added
31+
32+
- **Revision-safe month finalization (optional)**: Admin toggle `month_closure_enabled` (default off). Employees can finalize a full calendar month; the app stores a canonical JSON snapshot, SHA-256 hash chain, append-only revision rows, audit events, and a minimal PDF download. Finalized months are read-only through normal app APIs; administrators may reopen a month with a mandatory reason (audit). Monthly reports for a finalized month use the stored snapshot. Database: `at_month_closure`, `at_month_closure_revision` (migration `Version1014Date20260409120000`).
33+
34+
### Documentation
35+
36+
- User manuals (EN/DE), developer documentation, and compliance notes updated for month closure, retention context, and limits (in-app tamper evidence, not QES).
37+
1038
## 1.1.11 - 2026-04-09
1139

1240
### Added

Makefile

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ app_name = arbeitszeitcheck
44
build_dir = build
55
release_dir = $(build_dir)/release
66
version = $(shell grep '^\s*<version>' appinfo/info.xml | sed 's/.*<version>\([0-9.]*\)<\/version>.*/\1/' | head -1)
7+
archive_name = $(app_name)-$(version).tar.gz
8+
archive_path = $(release_dir)/$(archive_name)
9+
occ = ../../occ
710

8-
.PHONY: release clean
11+
.PHONY: release verify-release verify-signature-manifest sign-release release-signed clean test-security-role-gating-docker
912

1013
release:
1114
@echo "Building $(app_name) v$(version)..."
@@ -14,10 +17,38 @@ release:
1417
mkdir -p "$$staging/$(app_name)" && \
1518
rsync -a --exclude='.git' --exclude='$(build_dir)' --exclude='.github' \
1619
--exclude='node_modules' --exclude='tests' --exclude='.phpunit.result.cache' \
20+
--exclude='scripts' --exclude='release/*.tar.gz' --exclude='release/*.asc' \
21+
--exclude='appinfo/signature.json' \
1722
./ "$$staging/$(app_name)/" && \
18-
tar -czf $(release_dir)/$(app_name).tar.gz -C "$$staging" $(app_name) && \
23+
tar -czf $(archive_path) -C "$$staging" $(app_name) && \
1924
rm -rf "$$staging"
20-
@echo "Created $(release_dir)/$(app_name).tar.gz"
25+
@echo "Created $(archive_path)"
26+
27+
verify-release:
28+
@test -f $(archive_path) || (echo "Error: Run 'make release' first"; exit 1)
29+
@if tar -tzf $(archive_path) | grep -Eq '/(\.git/|node_modules/|build/|tests/|scripts/)'; then \
30+
echo "Error: release archive contains forbidden development paths"; \
31+
tar -tzf $(archive_path) | grep -E '/(\.git/|node_modules/|build/|tests/|scripts/)' || true; \
32+
exit 1; \
33+
fi
34+
@echo "Release archive layout looks clean."
35+
36+
verify-signature-manifest:
37+
@test -f $(archive_path) || (echo "Error: Run 'make release-signed' first"; exit 1)
38+
@tmpdir=$$(mktemp -d) && \
39+
trap 'rm -rf "$$tmpdir"' EXIT && \
40+
tar -xzf $(archive_path) -C "$$tmpdir" "$(app_name)/appinfo/signature.json" && \
41+
sig="$$tmpdir/$(app_name)/appinfo/signature.json" && \
42+
if ! test -f "$$sig"; then \
43+
echo "Error: signature.json missing from signed archive"; \
44+
exit 1; \
45+
fi && \
46+
if grep -Eq '"([^"]*/)?(\.git|node_modules|build|tests|scripts)\\/' "$$sig"; then \
47+
echo "Error: signature.json references forbidden development paths"; \
48+
grep -E '"([^"]*/)?(\.git|node_modules|build|tests|scripts)\\/' "$$sig" || true; \
49+
exit 1; \
50+
fi
51+
@echo "Signature manifest sanity check passed."
2152

2253
clean:
2354
rm -rf $(build_dir)
@@ -26,17 +57,29 @@ clean:
2657
# Paste the output into the App Store upload form's "Signature" field
2758
sign-tarball:
2859
@test -f ~/.nextcloud/certificates/$(app_name).key || (echo "Error: Missing ~/.nextcloud/certificates/$(app_name).key"; exit 1)
29-
@test -f $(release_dir)/$(app_name).tar.gz || (echo "Error: Run 'make release' first"; exit 1)
30-
@openssl dgst -sha512 -sign $$HOME/.nextcloud/certificates/$(app_name).key $(release_dir)/$(app_name).tar.gz 2>/dev/null | base64 | tr -d '\n'; echo
60+
@test -f $(archive_path) || (echo "Error: Run 'make release' first"; exit 1)
61+
@openssl dgst -sha512 -sign $$HOME/.nextcloud/certificates/$(app_name).key $(archive_path) 2>/dev/null | base64 | tr -d '\n'; echo
3162

32-
# Sign the app for Nextcloud App Store (requires certificate)
63+
# Sign the release archive payload with Nextcloud app signature
64+
# This signs the extracted archive tree (not your local dev checkout), then repacks it.
3365
# Generate cert: openssl req -nodes -newkey rsa:4096 -keyout ~/.nextcloud/certificates/arbeitszeitcheck.key -out ~/.nextcloud/certificates/arbeitszeitcheck.csr -subj "/CN=arbeitszeitcheck"
3466
# Store signed cert as ~/.nextcloud/certificates/arbeitszeitcheck.crt
35-
sign:
36-
@test -f ~/.nextcloud/certificates/$(app_name).key || (echo "Error: Run 'make release' first, then obtain certificate from https://github.com/nextcloud/app-certificate-requests"; exit 1)
67+
sign-release: verify-release
68+
@test -f ~/.nextcloud/certificates/$(app_name).key || (echo "Error: Missing ~/.nextcloud/certificates/$(app_name).key (see https://github.com/nextcloud/app-certificate-requests)"; exit 1)
3769
@test -f ~/.nextcloud/certificates/$(app_name).crt || (echo "Error: Store signed certificate at ~/.nextcloud/certificates/$(app_name).crt"; exit 1)
38-
php ../../occ integrity:sign-app \
39-
--privateKey=$$HOME/.nextcloud/certificates/$(app_name).key \
40-
--certificate=$$HOME/.nextcloud/certificates/$(app_name).crt \
41-
--path=$$(pwd)
42-
@echo "App signed. Commit appinfo/signature.json before release."
70+
@test -f $(occ) || (echo "Error: occ not found at $(occ). Override with 'make sign-release occ=/path/to/occ'"; exit 1)
71+
@staging=$$(mktemp -d) && \
72+
trap 'rm -rf "$$staging"' EXIT && \
73+
tar -xzf $(archive_path) -C "$$staging" && \
74+
php $(occ) integrity:sign-app \
75+
--privateKey=$$HOME/.nextcloud/certificates/$(app_name).key \
76+
--certificate=$$HOME/.nextcloud/certificates/$(app_name).crt \
77+
--path="$$staging/$(app_name)" && \
78+
tar -czf $(archive_path) -C "$$staging" $(app_name)
79+
@echo "Signed archive updated at $(archive_path)"
80+
81+
release-signed: release sign-release verify-signature-manifest
82+
@echo "Release build + Nextcloud signature complete."
83+
84+
test-security-role-gating-docker:
85+
@bash scripts/test-security-role-gating-docker.sh

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
},
2222
"scripts": {
2323
"test": "phpunit",
24+
"test:docker": "bash ../../docker/run-app-phpunit.sh arbeitszeitcheck",
25+
"test:security-role-gating:docker": "bash scripts/test-security-role-gating-docker.sh",
2426
"test:unit": "phpunit --testsuite unit",
2527
"test:integration": "phpunit --testsuite integration",
2628
"test:coverage": "phpunit --coverage-html tests/coverage",

0 commit comments

Comments
 (0)