Relationships
↑ child of #662#683 serve-auth: mandatory admin materialization from config at startup
Opened by stack72 · 6/18/2026· Shipped 6/18/2026
Parent
Sub-issue of #662 (serve authentication & authorization). Layer 4, item 4.
Dependencies
- ✅ #667 — Grant model
- ✅ #672 — AccessDecisionService
- 🔄 #682 — Auth config schema (PR open, must merge first)
Summary
When `swamp serve` starts in an enforcing mode (`token` or `oauth`), the `--admins` config lists principal IDs that must have admin authority. This issue materializes those principals as real grants at startup — `subject: user:`, `effect: allow`, `actions: [admin]`, `resource: access:*`, `source: config`.
These are not invisible side-channel privileges. They are real, listable grants visible in `swamp access grant list`, distinguished by `source: config`. They are the fail-safe root of "who may change the rules."
What to build
Materialization logic
At `swamp serve` startup, after the auth config is parsed and model registries are loaded:
- Read `authConfig.admins` from the parsed `ServeAuthConfig` (#682)
- For each admin principal, ensure a grant exists:
- `subject: user:`
- `effect: allow`
- `actions: [admin]`
- `resource: access:*`
- `source: config`
- `createdBy: user:system` (system-generated, not user-initiated)
- For any existing `source: config` grants whose subject is NOT in the current admins list, revoke them (admin removed from config)
Reconciliation semantics
The config file is the source of truth for config-sourced admins. On every boot:
- Added to config → grant created
- Still in config → no-op (grant already exists)
- Removed from config → grant revoked
This means an admin locked out of the running system is recoverable by editing config and restarting. And an admin removed from config loses authority on the next boot.
Identity matching
Match config admins to stored `source: config` grants by subject. The identity tuple is simpler than the grants file reconciler would have been — it's always `(user:, allow, [admin], access:*, config)`.
Runtime admin grants are separate
An admin may grant `admin` to others at runtime via `swamp access grant create`. Those grants have `source: method` and are untouched by this reconciler. The config-sourced set cannot be revoked to zero through the API — losing the last admin requires editing config.
Where it runs
In `src/cli/commands/serve.ts`, after:
- Auth config parsing (from #682)
- Model registry loading
- PolicySnapshotLoader initial load
Before the server starts accepting connections.
When `authConfig.mode` is `none`, skip materialization entirely (no admins needed, no auth enforced).
Implementation approach
This should NOT go through `modelMethodRun`. Follow the same approach discussed for the grants file reconciler — call the Grant model methods directly. The materialization creates/revokes a small number of grants (the admins list) at startup.
Use a domain service in `src/domain/access/` (e.g., `admin_materializer.ts`) that takes the admins list and the data query/write infrastructure, and handles the reconciliation. The serve command calls it. Testable in isolation.
Scope
- Admin materializer domain service in `src/domain/access/`
- Wire into `swamp serve` startup
- Tests: materialization creates grants, idempotent on restart, removed admin is revoked, `mode: none` skips materialization, runtime grants untouched
Out of scope
- `mode: token` authentication — layer 4, item 2
- OAuth — layer 4, item 3
- Enforcement — layer 5
References
- Auth config: `src/domain/access/serve_auth_config.ts` (#682)
- Grant model: `src/domain/models/access/grant_model.ts`
- Serve startup: `src/cli/commands/serve.ts`
- Grant query pattern: `queryGrants()` in `src/cli/commands/access_grant.ts`
- PolicySnapshotLoader: `src/domain/access/policy_snapshot_loader.ts`
Shipped
Click a lifecycle step above to view its details.