Identity & token CLI #2

Closed
opened 2026-05-09 23:23:59 +02:00 by arne · 1 comment
Owner

What to build

Operator CLI for managing identities in the multi-tenant daemon. Atomic manifest rewrites (tmpfile + fsync + rename), Ed25519 keypair generation, per-identity DB initialization with migrations applied. --print-token flag on identity add is the bootstrap escape hatch — skips the invite flow and prints an mst_… directly. For everyone else, invites are created in #4.

Existing token * subcommands take a new required --slug flag to scope to one identity's auth_tokens table.

Mutating commands print a "restart the daemon to load" hint rather than auto-firing the restart (Q8 of design grill).

Acceptance criteria

  • posta-server identity add --slug=X --url=Y --name=Z [--print-token] generates keys, creates DB, runs migrations, atomically rewrites manifest
  • posta-server identity list reads manifest + verifies per-identity directory layout
  • posta-server identity remove --slug=X drops manifest entry, leaves data on disk (soft delete)
  • posta-server identity purge --slug=X --yes removes manifest entry AND deletes /var/lib/posta/<slug>/; supports --dry-run
  • posta-server token create|list|revoke --slug=X slug-scoped variants of existing commands
  • All mutating commands print restart-required hint; no auto-restart
  • Slug validation: [a-z0-9-]+ only; reject anything else at write time
  • Tests cover happy paths + manifest collision (slug already taken), missing slug, malformed URL

Blocked by

## What to build Operator CLI for managing identities in the multi-tenant daemon. Atomic manifest rewrites (tmpfile + fsync + rename), Ed25519 keypair generation, per-identity DB initialization with migrations applied. `--print-token` flag on `identity add` is the bootstrap escape hatch — skips the invite flow and prints an `mst_…` directly. For everyone else, invites are created in #4. Existing `token *` subcommands take a new required `--slug` flag to scope to one identity's `auth_tokens` table. Mutating commands print a "restart the daemon to load" hint rather than auto-firing the restart (Q8 of design grill). ## Acceptance criteria - [ ] `posta-server identity add --slug=X --url=Y --name=Z [--print-token]` generates keys, creates DB, runs migrations, atomically rewrites manifest - [ ] `posta-server identity list` reads manifest + verifies per-identity directory layout - [ ] `posta-server identity remove --slug=X` drops manifest entry, leaves data on disk (soft delete) - [ ] `posta-server identity purge --slug=X --yes` removes manifest entry AND deletes `/var/lib/posta/<slug>/`; supports `--dry-run` - [ ] `posta-server token create|list|revoke --slug=X` slug-scoped variants of existing commands - [ ] All mutating commands print restart-required hint; no auto-restart - [ ] Slug validation: `[a-z0-9-]+` only; reject anything else at write time - [ ] Tests cover happy paths + manifest collision (slug already taken), missing slug, malformed URL ## Blocked by - #1
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Add operator CLI for managing multi-tenant identities and rework the existing token * subcommands to be slug-scoped.

Blocked by: #1 (the multi-tenant daemon skeleton must land first; this issue mutates the same manifest the daemon reads).

Current behavior:
There is no identity subcommand. The token create|list|revoke subcommands take --config and --db flags and operate on a single SQLite database — implicitly the only identity. There is no concept of multiple identities, no manifest mutation flow, no keypair generation outside of one-shot bootstrap, and no slug-based addressing.

Desired behavior:
A new identity subcommand tree manages the lifecycle of identities in the manifest written by #1. Each mutation rewrites the manifest atomically. The token subcommands become slug-scoped and operate against the per-identity database derived from the manifest. No mutating command restarts the daemon; instead each prints a clear "restart the daemon to load this change" hint. A bootstrap escape hatch lets the first identity be created without an invite (invites land in #4).

Manifest mutation semantics:
Every command that writes the manifest must do so atomically: write to a temp file in the same directory, fsync the temp file, then rename over the destination. A crash mid-write must never leave the manifest truncated, partially-written, or missing.

Slug validation:
Slugs match ^[a-z0-9-]+$. Reject anything else at write time with a clear error. Slug collisions (a slug already present in the manifest) are a write-time error too.

Per-identity layout (convention, not configured):
For an identity with slug = "X", the daemon and CLI both expect:

  • Keys at /var/lib/posta/X/keys.json
  • Database at /var/lib/posta/X/inbox.db

The CLI creates these paths on identity add and only deletes them on identity purge.

Key interfaces:

  • posta-server identity add --slug=X --url=Y --name=Z [--print-token] [--manifest=path]
    • Generates a fresh Ed25519 keypair and writes keys.json.
    • Creates the per-identity SQLite DB and runs the existing migrations against it.
    • Atomically rewrites the manifest to include the new entry.
    • With --print-token: also mints and prints an mst_… bearer token (bootstrap escape hatch — skips the invite flow, suitable for the first identity on a fresh install).
  • posta-server identity list [--manifest=path]
    • Reads the manifest and verifies that each entry's per-identity directory layout exists. Reports any drift (manifest entry without dir, or vice versa) without modifying anything.
  • posta-server identity remove --slug=X [--manifest=path]
    • Drops the manifest entry only. Leaves /var/lib/posta/X/ on disk (soft delete).
  • posta-server identity purge --slug=X --yes [--dry-run] [--manifest=path]
    • Drops the manifest entry and deletes /var/lib/posta/X/ recursively. Requires --yes to proceed. With --dry-run, prints what would be removed and exits without touching anything.
  • posta-server token create|list|revoke --slug=X [--manifest=path] [other existing flags]
    • Slug-scoped variants of the existing commands. The DB is derived from --slug via the manifest. The legacy --db and --config flags are removed.
  • All mutating commands print a "restart the daemon to load this change" hint on success. None of them auto-restart or signal the daemon.
  • Manifest path defaults to /etc/posta/identities.toml everywhere; --manifest overrides.

Acceptance criteria:

  • identity add generates Ed25519 keys, creates the DB, runs migrations, and atomically rewrites the manifest in a single command.
  • identity add --print-token additionally mints and prints a bearer token in the same invocation.
  • identity list reads the manifest and surfaces any per-identity directory drift without mutating state.
  • identity remove drops the manifest entry and leaves the data directory untouched.
  • identity purge --yes drops the manifest entry and deletes the data directory; without --yes it refuses; with --dry-run it prints intent without acting.
  • token create|list|revoke --slug=X scope to the named identity's auth_tokens table; running them without --slug fails with a clear error.
  • The --db and --config flags are gone from token *; --manifest is available with default /etc/posta/identities.toml.
  • Every mutation prints a "restart the daemon" hint; nothing auto-restarts.
  • Slug validation ^[a-z0-9-]+$ is enforced at write time; invalid slugs produce a clear error.
  • Manifest writes are atomic (tmpfile + fsync + rename); a crash mid-write cannot corrupt the manifest.
  • Tests cover: happy paths for each command; slug collision; missing slug; malformed URL; purge --dry-run; manifest atomicity (truncate after rename should still leave a valid manifest).

Out of scope:

  • Invite flow / redemption — covered by #4. The only non-invite onboarding is identity add --print-token.
  • Daemon auto-restart, signal handling, or any IPC with a running daemon. Operator restarts manually.
  • Per-identity ACL / rate-limit / config — those remain global daemon flags as decided in #1.
  • Manifest schema changes beyond the slug/url shape established by #1.
  • Migrating existing single-tenant deployments — that's #3.
  • Web UI for identity management — out of scope for this milestone entirely.
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Add operator CLI for managing multi-tenant identities and rework the existing `token *` subcommands to be slug-scoped. **Blocked by:** #1 (the multi-tenant daemon skeleton must land first; this issue mutates the same manifest the daemon reads). **Current behavior:** There is no `identity` subcommand. The `token create|list|revoke` subcommands take `--config` and `--db` flags and operate on a single SQLite database — implicitly the only identity. There is no concept of multiple identities, no manifest mutation flow, no keypair generation outside of one-shot bootstrap, and no slug-based addressing. **Desired behavior:** A new `identity` subcommand tree manages the lifecycle of identities in the manifest written by #1. Each mutation rewrites the manifest atomically. The `token` subcommands become slug-scoped and operate against the per-identity database derived from the manifest. No mutating command restarts the daemon; instead each prints a clear "restart the daemon to load this change" hint. A bootstrap escape hatch lets the first identity be created without an invite (invites land in #4). **Manifest mutation semantics:** Every command that writes the manifest must do so atomically: write to a temp file in the same directory, `fsync` the temp file, then `rename` over the destination. A crash mid-write must never leave the manifest truncated, partially-written, or missing. **Slug validation:** Slugs match `^[a-z0-9-]+$`. Reject anything else at write time with a clear error. Slug collisions (a slug already present in the manifest) are a write-time error too. **Per-identity layout (convention, not configured):** For an identity with `slug = "X"`, the daemon and CLI both expect: - Keys at `/var/lib/posta/X/keys.json` - Database at `/var/lib/posta/X/inbox.db` The CLI creates these paths on `identity add` and only deletes them on `identity purge`. **Key interfaces:** - `posta-server identity add --slug=X --url=Y --name=Z [--print-token] [--manifest=path]` - Generates a fresh Ed25519 keypair and writes `keys.json`. - Creates the per-identity SQLite DB and runs the existing migrations against it. - Atomically rewrites the manifest to include the new entry. - With `--print-token`: also mints and prints an `mst_…` bearer token (bootstrap escape hatch — skips the invite flow, suitable for the first identity on a fresh install). - `posta-server identity list [--manifest=path]` - Reads the manifest and verifies that each entry's per-identity directory layout exists. Reports any drift (manifest entry without dir, or vice versa) without modifying anything. - `posta-server identity remove --slug=X [--manifest=path]` - Drops the manifest entry only. Leaves `/var/lib/posta/X/` on disk (soft delete). - `posta-server identity purge --slug=X --yes [--dry-run] [--manifest=path]` - Drops the manifest entry **and** deletes `/var/lib/posta/X/` recursively. Requires `--yes` to proceed. With `--dry-run`, prints what would be removed and exits without touching anything. - `posta-server token create|list|revoke --slug=X [--manifest=path] [other existing flags]` - Slug-scoped variants of the existing commands. The DB is derived from `--slug` via the manifest. The legacy `--db` and `--config` flags are removed. - All mutating commands print a "restart the daemon to load this change" hint on success. None of them auto-restart or signal the daemon. - Manifest path defaults to `/etc/posta/identities.toml` everywhere; `--manifest` overrides. **Acceptance criteria:** - [ ] `identity add` generates Ed25519 keys, creates the DB, runs migrations, and atomically rewrites the manifest in a single command. - [ ] `identity add --print-token` additionally mints and prints a bearer token in the same invocation. - [ ] `identity list` reads the manifest and surfaces any per-identity directory drift without mutating state. - [ ] `identity remove` drops the manifest entry and leaves the data directory untouched. - [ ] `identity purge --yes` drops the manifest entry **and** deletes the data directory; without `--yes` it refuses; with `--dry-run` it prints intent without acting. - [ ] `token create|list|revoke --slug=X` scope to the named identity's `auth_tokens` table; running them without `--slug` fails with a clear error. - [ ] The `--db` and `--config` flags are gone from `token *`; `--manifest` is available with default `/etc/posta/identities.toml`. - [ ] Every mutation prints a "restart the daemon" hint; nothing auto-restarts. - [ ] Slug validation `^[a-z0-9-]+$` is enforced at write time; invalid slugs produce a clear error. - [ ] Manifest writes are atomic (tmpfile + fsync + rename); a crash mid-write cannot corrupt the manifest. - [ ] Tests cover: happy paths for each command; slug collision; missing slug; malformed URL; `purge --dry-run`; manifest atomicity (truncate after rename should still leave a valid manifest). **Out of scope:** - Invite flow / redemption — covered by #4. The only non-invite onboarding is `identity add --print-token`. - Daemon auto-restart, signal handling, or any IPC with a running daemon. Operator restarts manually. - Per-identity ACL / rate-limit / config — those remain global daemon flags as decided in #1. - Manifest schema changes beyond the `slug`/`url` shape established by #1. - Migrating existing single-tenant deployments — that's #3. - Web UI for identity management — out of scope for this milestone entirely.
arne closed this issue 2026-05-10 01:19:49 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
posta/server#2
No description provided.