Skip to content

Commit 21d3bd5

Browse files
feat(arbeitszeitcheck): 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 3e43eab commit 21d3bd5

21 files changed

Lines changed: 643 additions & 9 deletions

CHANGELOG.de.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@
33
### Hinzugefügt
44

55
- **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.
68

79
### Geändert
810

911
- **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.
1012
- **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.
1114

1215
### Dokumentation
1316

1417
- **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.
1518
- **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.
1620

1721
## 1.1.12 – 2026-04-09
1822

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- **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.
1315

1416
### Changed
1517

1618
- **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.
1719
- **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.
1821

1922
### Documentation
2023

2124
- **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.
2225
- **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.
2327

2428
## 1.1.12 - 2026-04-09
2529

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ archive_name = $(app_name)-$(version).tar.gz
88
archive_path = $(release_dir)/$(archive_name)
99
occ = ../../occ
1010

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

1313
release:
1414
@echo "Building $(app_name) v$(version)..."
@@ -80,3 +80,6 @@ sign-release: verify-release
8080

8181
release-signed: release sign-release verify-signature-manifest
8282
@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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"scripts": {
2323
"test": "phpunit",
2424
"test:docker": "bash ../../docker/run-app-phpunit.sh arbeitszeitcheck",
25+
"test:security-role-gating:docker": "bash scripts/test-security-role-gating-docker.sh",
2526
"test:unit": "phpunit --testsuite unit",
2627
"test:integration": "phpunit --testsuite integration",
2728
"test:coverage": "phpunit --coverage-html tests/coverage",

docs/Developer-Documentation.en.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Developer Documentation – ArbeitszeitCheck
22

33
**Version:** 1.1.12
4-
**Last Updated:** 2026-04-12
4+
**Last Updated:** 2026-04-13
55

66
This guide is for developers who want to contribute to ArbeitszeitCheck or integrate with it.
77

@@ -699,6 +699,13 @@ docker compose exec -T nextcloud-app bash -lc "cd /var/www/html/custom_apps/arbe
699699
docker compose exec -T nextcloud-app bash -lc "cd /var/www/html/custom_apps/arbeitszeitcheck && composer test:integration"
700700
```
701701

702+
Run focused security role-gating checks in Docker:
703+
```bash
704+
make test-security-role-gating-docker
705+
# or
706+
composer test:security-role-gating:docker
707+
```
708+
702709
Run JS unit tests inside the Nextcloud container:
703710
```bash
704711
docker compose exec -T nextcloud bash -lc "cd /var/www/html/custom_apps/arbeitszeitcheck && npm ci && npm test"
@@ -835,6 +842,14 @@ public function getEntry(int $id): JSONResponse
835842
}
836843
```
837844

845+
### App-admin authorization model
846+
847+
- The app distinguishes between **Nextcloud platform admins** and optional **ArbeitszeitCheck app admins**.
848+
- Config key: `app_admin_user_ids` (`Constants::CONFIG_APP_ADMIN_USER_IDS`) stores a JSON array of allowed user IDs.
849+
- Empty list is intentionally backward compatible: all Nextcloud admins are app admins.
850+
- `AppAdminMiddleware` is registered in `Application::register()` and gates `AdminController` methods centrally.
851+
- Unauthorized access to admin pages throws `NotAppAdminException` and resolves to a 403 response.
852+
838853
### SQL Injection Prevention
839854

840855
Always use parameterized queries:

docs/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ This folder contains the documentation included with ArbeitszeitCheck for admini
66

77
| Document | Description |
88
|----------|-------------|
9-
| [User-Manual.en.md](User-Manual.en.md) | Short end-user guide (calendar vs Nextcloud Calendar, absences, roles, revision-safe month closure including optional grace period and auto-finalization) |
10-
| [User-Manual.de.md](User-Manual.de.md) | Kurzanleitung für Endnutzer (Deutsch), inkl. Monatsnachweis mit Karenz und Auto-Finalisierung |
9+
| [User-Manual.en.md](User-Manual.en.md) | Short end-user/admin guide (calendar vs Nextcloud Calendar, absences, roles, app-admin allowlist usage, revision-safe month closure including optional grace period and auto-finalization) |
10+
| [User-Manual.de.md](User-Manual.de.md) | Kurzanleitung für Endnutzer/Admins (Deutsch), inkl. App-Admin-Whitelist sowie Monatsnachweis mit Karenz und Auto-Finalisierung |
1111
| [GDPR-Compliance-Guide.en.md](GDPR-Compliance-Guide.en.md) | How to operate ArbeitszeitCheck in a GDPR-compliant way (legal basis, data minimization, employee rights, retention) |
1212
| [Compliance-Implementation.en.md](Compliance-Implementation.en.md) | Technical implementation of ArbZG compliance checks (breaks, rest periods, real-time vs batch) |
1313
| [Compliance-Implementation.de.md](Compliance-Implementation.de.md) | Same content in German |

docs/User-Manual.de.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,19 @@ Alle Daten verbleiben in Ihrer **Nextcloud-Instanz**.
3535
- **Vertretung** (falls genutzt): Eindeckung für Abwesenheiten bestätigen oder ablehnen.
3636
- **Führungskraft / Team**: Abwesenheiten genehmigen, Teamübersichten je nach Rechten.
3737
- **Administratorin / Administrator**: Globale Einstellungen, Nutzer, Feiertage, Compliance-Optionen, Exporte.
38+
- **App-Administrator:in (optionale Einschränkung)**: Ihre Organisation kann die ArbeitszeitCheck-Administration auf ausgewählte Nextcloud-Admins begrenzen. Ist das gesetzt, bleiben andere Nextcloud-Admins zwar Plattform-Admins, erhalten in den ArbeitszeitCheck-Adminseiten aber „Zugriff verweigert“.
3839

3940
Die genauen Rechte hängen von Nextcloud-Gruppen und der App-Konfiguration ab.
4041

42+
### Für Administrator:innen: App-Administrator:innen konfigurieren
43+
44+
1. Öffnen Sie **ArbeitszeitCheck → Admin-Einstellungen**.
45+
2. Wählen Sie im Bereich **App-Administratoren (ArbeitszeitCheck)** die Nextcloud-Admin-Konten aus, die diese App verwalten dürfen.
46+
3. Speichern Sie die Einstellungen.
47+
4. Lassen Sie die Liste leer, wenn das rückwärtskompatible Standardverhalten gelten soll (**alle** Nextcloud-Admins dürfen ArbeitszeitCheck verwalten).
48+
49+
Im Auswahldialog erscheinen nur Konten aus der Nextcloud-`admin`-Gruppe.
50+
4151
---
4252

4353
## 4. Alltägliche Aufgaben

docs/User-Manual.en.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,19 @@ All data stays in your **Nextcloud** instance.
3535
- **Substitute** (if used): Approve or reject coverage for an absence.
3636
- **Manager / team lead**: Approve absences for team members, see team views where permitted.
3737
- **Administrator**: Global settings, users, holidays, compliance options, exports.
38+
- **App administrator (optional restriction)**: Your organization can limit ArbeitszeitCheck administration to selected Nextcloud admins only. If this is configured, other Nextcloud admins may still be platform admins but will see access denied for ArbeitszeitCheck admin pages.
3839

3940
Exact permissions depend on your Nextcloud groups and app configuration.
4041

42+
### For administrators: how to configure app administrators
43+
44+
1. Open **ArbeitszeitCheck → Admin settings**.
45+
2. In **App administrators (ArbeitszeitCheck)**, search/select the Nextcloud admin accounts that should manage this app.
46+
3. Save settings.
47+
4. Leave the list empty if you want the backward-compatible default (**all** Nextcloud admins can administer ArbeitszeitCheck).
48+
49+
Only users in Nextcloud's `admin` group are eligible in this picker.
50+
4151
---
4252

4353
## 4. Everyday tasks

js/admin-settings.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,63 @@
4040
});
4141

4242
initMonthReopenUserPicker();
43+
initAppAdminsPicker();
4344
initAccessGroupsPicker();
4445
}
4546

47+
function initAppAdminsPicker() {
48+
const search = Utils.$('#appAdminUsersSearch');
49+
const list = Utils.$('#appAdminUsersList');
50+
const empty = Utils.$('#appAdminUsersEmpty');
51+
const countEl = Utils.$('#appAdminUsersCount');
52+
const l10n = window.ArbeitszeitCheck && window.ArbeitszeitCheck.l10n ? window.ArbeitszeitCheck.l10n : {};
53+
if (!search || !list) {
54+
return;
55+
}
56+
57+
const items = Array.prototype.slice.call(list.querySelectorAll('.access-groups-item'));
58+
const checkboxes = Array.prototype.slice.call(list.querySelectorAll('input[name="appAdminUserIds[]"]'));
59+
60+
function updateCount() {
61+
if (!countEl) {
62+
return;
63+
}
64+
const selectedCount = checkboxes.filter(function(box) { return box.checked; }).length;
65+
if (selectedCount === 0) {
66+
countEl.textContent = l10n.appAdminsAllAdmins || 'No app admins selected (all Nextcloud admins are allowed).';
67+
return;
68+
}
69+
const template = l10n.appAdminsSelected || '%s app admin(s) selected';
70+
countEl.textContent = template.indexOf('%s') !== -1
71+
? template.replace('%s', String(selectedCount))
72+
: String(selectedCount) + ' ' + template;
73+
}
74+
75+
function applyFilter() {
76+
const q = String(search.value || '').trim().toLowerCase();
77+
let visible = 0;
78+
items.forEach(function(item) {
79+
const haystack = String(item.getAttribute('data-app-admin-search') || '');
80+
const show = q === '' || haystack.indexOf(q) !== -1;
81+
item.hidden = !show;
82+
if (show) {
83+
visible++;
84+
}
85+
});
86+
if (empty) {
87+
empty.hidden = visible !== 0;
88+
}
89+
}
90+
91+
Utils.on(search, 'input', applyFilter);
92+
checkboxes.forEach(function(box) {
93+
Utils.on(box, 'change', updateCount);
94+
});
95+
96+
updateCount();
97+
applyFilter();
98+
}
99+
46100
function initAccessGroupsPicker() {
47101
const search = Utils.$('#accessAllowedGroupsSearch');
48102
const list = Utils.$('#accessAllowedGroupsList');
@@ -136,6 +190,11 @@
136190
? []
137191
: (Array.isArray(accessGroupsRaw) ? accessGroupsRaw : [accessGroupsRaw]);
138192
delete formData['accessAllowedGroups[]'];
193+
const appAdminRaw = formData['appAdminUserIds[]'];
194+
formData.appAdminUserIds = appAdminRaw === undefined
195+
? []
196+
: (Array.isArray(appAdminRaw) ? appAdminRaw : [appAdminRaw]);
197+
delete formData['appAdminUserIds[]'];
139198

140199
// Convert numbers (use defaults on invalid/empty)
141200
const num = (v, def) => { const n = parseFloat(v); return (Number.isFinite(n) ? n : def); };

lib/AppInfo/Application.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use OCA\ArbeitszeitCheck\Listener\LoadSidebarScripts;
1818
use OCA\ArbeitszeitCheck\Listener\CSPListener;
1919
use OCA\ArbeitszeitCheck\Listener\UserDeletedListener;
20+
use OCA\ArbeitszeitCheck\Middleware\AppAdminMiddleware;
2021
use OCA\ArbeitszeitCheck\Notification\Notifier;
2122
use OCA\ArbeitszeitCheck\Service\TimeTrackingService;
2223
use OCA\ArbeitszeitCheck\Service\AbsenceService;
@@ -64,6 +65,7 @@ public function register(IRegistrationContext $context): void {
6465

6566
// Register notification provider
6667
$context->registerNotifierService(Notifier::class);
68+
$context->registerMiddleware(AppAdminMiddleware::class);
6769

6870
// Register event listeners
6971
$context->registerEventListener(LoadSidebar::class, LoadSidebarScripts::class);
@@ -373,6 +375,7 @@ public function register(IRegistrationContext $context): void {
373375
return new PermissionService(
374376
$c->query(\OCP\IGroupManager::class),
375377
$c->query(\OCP\App\IAppManager::class),
378+
$c->query(\OCP\IConfig::class),
376379
$c->query(\OCP\IUserManager::class),
377380
$c->query(TeamResolverService::class),
378381
$c->query(\Psr\Log\LoggerInterface::class)

0 commit comments

Comments
 (0)