|
@@ -87,7 +87,7 @@ per-cell audit trail.
|
|
|
│ ├── htmx.min.js # htmx.org
|
|
│ ├── htmx.min.js # htmx.org
|
|
|
│ └── sortable.min.js # SortableJS
|
|
│ └── sortable.min.js # SortableJS
|
|
|
├── src/
|
|
├── src/
|
|
|
-│ ├── Auth/ LocalAdmin, OidcClient, SessionGuard
|
|
|
|
|
|
|
+│ ├── Auth/ BootstrapAdmin, LocalAdmin, OidcClient, SessionGuard
|
|
|
│ ├── Controllers/ AuthController, WorkerController, SprintController,
|
|
│ ├── Controllers/ AuthController, WorkerController, SprintController,
|
|
|
│ │ TaskController, AuditController, UserController,
|
|
│ │ TaskController, AuditController, UserController,
|
|
|
│ │ SettingsController, ImportController (Phase 20)
|
|
│ │ SettingsController, ImportController (Phase 20)
|
|
@@ -280,6 +280,13 @@ DB_PATH=/var/www/data/app.sqlite
|
|
|
SESSION_PATH=/var/www/data/sessions
|
|
SESSION_PATH=/var/www/data/sessions
|
|
|
APP_ENV=production
|
|
APP_ENV=production
|
|
|
|
|
|
|
|
|
|
+# Optional explicit OIDC bootstrap (R01-N03). When the `users` table has no
|
|
|
|
|
+# admin and the signing user matches one of these (case-insensitive,
|
|
|
|
|
+# timing-safe), they are promoted on sign-in. Either or both may be set;
|
|
|
|
|
+# leave both blank to disable OIDC auto-promotion entirely.
|
|
|
|
|
+BOOTSTRAP_ADMIN_OID=
|
|
|
|
|
+BOOTSTRAP_ADMIN_EMAIL=
|
|
|
|
|
+
|
|
|
# Optional local admin fallback (disables when blank).
|
|
# Optional local admin fallback (disables when blank).
|
|
|
# Password is verified with PHP's password_verify() against the bcrypt hash
|
|
# Password is verified with PHP's password_verify() against the bcrypt hash
|
|
|
# stored in LOCAL_ADMIN_PASSWORD_HASH; the plaintext password never lands on
|
|
# stored in LOCAL_ADMIN_PASSWORD_HASH; the plaintext password never lands on
|
|
@@ -291,9 +298,21 @@ LOCAL_ADMIN_PASSWORD_HASH=
|
|
|
LOCAL_ADMIN_NAME=Local Admin
|
|
LOCAL_ADMIN_NAME=Local Admin
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-First-login bootstrap: when the `users` table is empty at the moment of
|
|
|
|
|
-successful login (either OIDC or local), that user is promoted to `is_admin=1`
|
|
|
|
|
-with a `BOOTSTRAP_ADMIN` audit row.
|
|
|
|
|
|
|
+First-admin bootstrap (R01-N03 hardening):
|
|
|
|
|
+
|
|
|
|
|
+- **OIDC path.** Auto-promotion requires `users.countAdmins() === 0` AND the
|
|
|
|
|
+ signing user's `oid`/`email` to match `BOOTSTRAP_ADMIN_OID` /
|
|
|
|
|
+ `BOOTSTRAP_ADMIN_EMAIL`. With both env vars blank, OIDC never promotes —
|
|
|
|
|
+ the operator must seed the first admin via the local-admin fallback or by
|
|
|
|
|
+ flipping `users.is_admin = 1` directly. The promotion emits a
|
|
|
|
|
+ `BOOTSTRAP_ADMIN` audit row tagged `via=oidc`.
|
|
|
|
|
+- **Local-admin path.** `LOCAL_ADMIN_EMAIL` + `LOCAL_ADMIN_PASSWORD_HASH` is
|
|
|
|
|
+ itself an explicit env-bootstrap, so the local user is always promoted on
|
|
|
|
|
+ sign-in (`forceAdmin=true`). When the DB was empty before this sign-in, a
|
|
|
|
|
+ `BOOTSTRAP_ADMIN` audit row tagged `via=local` is also recorded.
|
|
|
|
|
+
|
|
|
|
|
+The pre-R01-N03 behaviour (first user to sign in via any path becomes admin)
|
|
|
|
|
+is gone — see `src/Auth/BootstrapAdmin.php`.
|
|
|
|
|
|
|
|
## 9. Build phases — status
|
|
## 9. Build phases — status
|
|
|
|
|
|
|
@@ -1183,6 +1202,36 @@ with a `BOOTSTRAP_ADMIN` audit row.
|
|
|
time is injected so no test sleeps. Tests: 176 / 484 (was 163 /
|
|
time is injected so no test sleeps. Tests: 176 / 484 (was 163 /
|
|
|
417). Sixth fix from `doc/REVIEW_01.md`.
|
|
417). Sixth fix from `doc/REVIEW_01.md`.
|
|
|
|
|
|
|
|
|
|
+- [x] **R01-N03 — Explicit env-bootstrap for the first OIDC admin**
|
|
|
|
|
+ (`f565c86`). The OIDC sign-in path used to auto-promote the very
|
|
|
|
|
+ first user to land on `/auth/callback` (`users.count() === 0`).
|
|
|
|
|
+ On a public-facing first deploy that was a land-grab — any
|
|
|
|
|
+ tenant member with a valid Entra account could win the race
|
|
|
|
|
+ against the intended operator. Two new env vars,
|
|
|
|
|
+ `BOOTSTRAP_ADMIN_OID` and `BOOTSTRAP_ADMIN_EMAIL`, now name
|
|
|
|
|
+ the bootstrap principal up front; OIDC auto-promotion requires
|
|
|
|
|
+ `countAdmins() === 0` AND a match (case-insensitive, trimmed,
|
|
|
|
|
+ `hash_equals`). With both env vars blank, OIDC NEVER promotes
|
|
|
|
|
+ anyone — operators must seed the first admin via the local-
|
|
|
|
|
+ admin fallback (itself an explicit env-bootstrap) or by
|
|
|
|
|
+ flipping `is_admin` in `app.sqlite`. The local-admin path is
|
|
|
|
|
+ unchanged: `forceAdmin: true` continues to keep the configured
|
|
|
|
|
+ local user admin on every sign-in, and the `BOOTSTRAP_ADMIN`
|
|
|
|
|
+ audit row still fires on the first local sign-in into an empty
|
|
|
|
|
+ `users` table — its `after` payload now carries
|
|
|
|
|
+ `via=local`/`via=oidc` so `/audit` distinguishes the two
|
|
|
|
|
+ channels. New `src/Auth/BootstrapAdmin.php` owns
|
|
|
|
|
+ `isConfigured()` + `matches(oid, email)`; blank incoming fields
|
|
|
|
|
+ never opportunistically match an absent env value. New
|
|
|
|
|
+ `tests/Auth/BootstrapAdminTest.php` (8 cases) pins the matcher;
|
|
|
|
|
+ `UserRepositoryTest` comments lost their stale "count === 0"
|
|
|
|
|
+ reference but the repo's promoteToAdmin / forceAdmin contract
|
|
|
|
|
+ is mechanically unchanged. README + admin-manual rewritten:
|
|
|
|
|
+ admin-manual gains a new §3.5 "Nominating the first
|
|
|
|
|
+ administrator (OIDC)" and the old §3.5 (Local admin) shifts to
|
|
|
|
|
+ §3.6. Tests: 184 / 502 (was 176 / 484). Seventh fix from
|
|
|
|
|
+ `doc/REVIEW_01.md`.
|
|
|
|
|
+
|
|
|
- [x] **New sprint form: drop weeks input + task list row hover**
|
|
- [x] **New sprint form: drop weeks input + task list row hover**
|
|
|
(`3728106`). The `/sprints/new` form no longer collects an
|
|
(`3728106`). The `/sprints/new` form no longer collects an
|
|
|
`n_weeks` value — the week count is derived from `start_date` /
|
|
`n_weeks` value — the week count is derived from `start_date` /
|
|
@@ -1252,7 +1301,7 @@ for f in $(git ls-files '*.php'); do php -l "$f" | tail -1 | sed "s|^|$f: |"; do
|
|
|
Run the test suite:
|
|
Run the test suite:
|
|
|
```bash
|
|
```bash
|
|
|
vendor/bin/phpunit
|
|
vendor/bin/phpunit
|
|
|
-# → OK (176 tests, 484 assertions)
|
|
|
|
|
|
|
+# → OK (184 tests, 502 assertions)
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
The Phase 20 parser tests need `ext-dom`, `ext-zip`, `ext-xmlreader`,
|
|
The Phase 20 parser tests need `ext-dom`, `ext-zip`, `ext-xmlreader`,
|
|
@@ -1299,6 +1348,8 @@ before acting — nothing here is load-bearing once it grows stale.
|
|
|
## 13. Git history (as of this writing)
|
|
## 13. Git history (as of this writing)
|
|
|
|
|
|
|
|
```
|
|
```
|
|
|
|
|
+f565c86 Fix R01-N03: explicit env-bootstrap for the first OIDC admin
|
|
|
|
|
+2b8f167 Docs: mark R01-N06 fixed, refresh SPEC §3 / §4 / §7 / §9 / §11 / §13
|
|
|
e295432 Fix R01-N06: throttle local-admin login by (ip, email)
|
|
e295432 Fix R01-N06: throttle local-admin login by (ip, email)
|
|
|
851f8cf Docs: mark R01-N11 fixed, refresh SPEC §9 / §13
|
|
851f8cf Docs: mark R01-N11 fixed, refresh SPEC §9 / §13
|
|
|
4ae1817 Fix R01-N11: whitelist column in AuditRepository::distinctColumn
|
|
4ae1817 Fix R01-N11: whitelist column in AuditRepository::distinctColumn
|