Skip to main content
← Back to list
01Issue
FeatureShippedSwamp CLI
Assigneesstack72

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:

  1. Read `authConfig.admins` from the parsed `ServeAuthConfig` (#682)
  2. 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)
  3. 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`
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 3 MOREPR_MERGED+ 1 MORENOTIFICATION_SKIPPED

Shipped

6/18/2026, 10:51:23 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack726/18/2026, 9:36:27 PM
stack72 linked parent of #6636/18/2026, 9:39:15 PM
stack72 removed parent of #6636/19/2026, 3:05:28 PM
stack72 linked parent of #6626/19/2026, 3:05:38 PM

Sign in to post a ripple.