docs(secrets): interactive CLI console design (approved)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# Design: `secret` interactive console (ZB.MOM.WW.Secrets CLI, 0.3.0)
|
||||
|
||||
**Date:** 2026-07-19 · **Status:** Approved · **Execution:** git worktree `worktree-secrets-interactive-cli` (another agent is active on `main`)
|
||||
|
||||
## 1. Purpose & posture
|
||||
|
||||
Extend the existing headless `secret` CLI (`src/ZB.MOM.WW.Secrets.Cli`) with a menu-driven
|
||||
Spectre.Console TUI for two operator jobs:
|
||||
|
||||
1. **Deployment setup** — seed a deployment's secrets before first startup (apps fail closed
|
||||
when a `${secret:...}` reference is missing).
|
||||
2. **Lockout recovery** — diagnose and fix wrong/lost-KEK and missing-secret states.
|
||||
|
||||
No authentication: the tool only runs locally with direct file/DB access — the same trust
|
||||
model as the existing headless verbs. All existing verbs stay byte-identical for scripts.
|
||||
|
||||
## 2. Entry point
|
||||
|
||||
`secret` with **no args in a real terminal** (`!Console.IsInputRedirected &&
|
||||
!Console.IsOutputRedirected`) launches the TUI. Redirected I/O keeps today's
|
||||
usage-and-exit-2 behavior, so nothing scriptable changes. All other `Program.cs` paths are
|
||||
untouched.
|
||||
|
||||
## 3. Target selection & sessions
|
||||
|
||||
First screen picks a **store target**:
|
||||
|
||||
- **Recent targets** — persisted to `~/.zb-secrets/recent-targets.json` (names and paths
|
||||
only; NEVER key material).
|
||||
- **Browse** to an app's `appsettings.json`.
|
||||
- **Manual entry** — db path + KEK source.
|
||||
|
||||
`TargetConfigReader` loads the app's real config the way the app would
|
||||
(`appsettings.json` + optional `appsettings.<Environment>.json` overlay + environment
|
||||
variables; section `Secrets` by default, overridable) and binds `SecretsOptions`
|
||||
(+ `SqlServerSecretsOptions` when present) — operating on exactly what the app will read at
|
||||
startup, so a fix cannot land in the wrong store.
|
||||
|
||||
`SecretsSessionFactory` constructs the stack directly — `SecretsSqliteConnectionFactory` /
|
||||
`SqlServerSecretStore`, `MasterKeyProviderFactory`, `AesGcmEnvelopeCipher`, the matching
|
||||
migrator — no per-target DI host. If the configured KEK env var isn't set in the CLI's
|
||||
shell (common: it exists only in the service's environment), the session opens **degraded**:
|
||||
metadata ops work; decrypt/seal ops prompt for the key via masked input or file path, held
|
||||
in memory only.
|
||||
|
||||
Dependency changes to `ZB.MOM.WW.Secrets.Cli`: add `Spectre.Console` and a project
|
||||
reference to `ZB.MOM.WW.Secrets.Replicator.SqlServer` (to open SQL-hub stores such as
|
||||
ScadaBridge central).
|
||||
|
||||
## 4. Menu actions
|
||||
|
||||
| Action | Behavior |
|
||||
|---|---|
|
||||
| List / set-rotate / get / delete | Thin TUI wrappers over the same core seams `SecretCommands` uses. Masked value entry; tables for listings; **get** reveals once after confirm, never cached. |
|
||||
| **Reference audit + seeding** | Scan the target app's fully-composed config for `${secret:NAME}` tokens (same regex as `SecretReferenceExpander`); report each OK / missing / tombstoned / undecryptable; walk through seeding every non-OK one with masked prompts. This is the deployment-setup flow. |
|
||||
| **KEK doctor** | Verify the session KEK against the store: per-row KekId match + actual DEK-unwrap probe; report row classes (OK / wrong-KEK / corrupt). Guided remedies: `rewrap-all` (old key from env/file/masked paste, reusing `KekRotationService`) or re-set affected secrets when the old KEK is truly lost. This is the lockout flow. |
|
||||
| **Bundle export / import** | Export rows as ciphertext-only JSON (name, metadata, wrapped DEK, kekId, ciphertext — safe at rest, useless without the KEK). Import into another store; conflicts resolved by core `SecretLastWriterWins` with per-row override prompt. Cross-KEK import offers inline rewrap given the source KEK. |
|
||||
| Switch target / quit | Rebuild session for a new target; quit exits 0. |
|
||||
|
||||
## 5. Structure
|
||||
|
||||
New `Interactive/` layer inside `ZB.MOM.WW.Secrets.Cli`:
|
||||
|
||||
- `InteractiveShell` — menu loop; the ONLY class that touches `IAnsiConsole`.
|
||||
- UI-free services: `TargetConfigReader`, `SecretsSessionFactory`, `ReferenceAuditor`,
|
||||
`KekDoctor`, `SecretBundleCodec` + import/export service, `RecentTargetsStore`.
|
||||
|
||||
Services never touch the console; the shell never touches the store directly. Secret values
|
||||
are never written to recents, bundles (plaintext), logs, or screen except the explicit
|
||||
reveal action.
|
||||
|
||||
## 6. Error handling
|
||||
|
||||
Every action catches `SecretDecryptionException` / `ArgumentException` / IO / SQL failures
|
||||
and renders a red panel with a remedy hint (usually "run KEK doctor"), returning to the
|
||||
menu — the shell never crashes out mid-recovery. Ctrl-C at a prompt cancels back to the
|
||||
menu; at the menu it exits 0.
|
||||
|
||||
## 7. Testing
|
||||
|
||||
- Unit tests for all services in `tests/ZB.MOM.WW.Secrets.Tests/Cli/` (temp SQLite stores,
|
||||
fake KEK providers — same style as existing tests).
|
||||
- Shell flows scripted via `Spectre.Console.Testing`'s `TestConsole`: menu navigation,
|
||||
masked entry, degraded-KEK path, audit→seed walk-through, doctor→rewrap.
|
||||
- Pin test: non-TTY no-args still prints usage / exit 2.
|
||||
- Suite stays 0-warning (`TreatWarningsAsErrors` posture of the repo).
|
||||
|
||||
## 8. Versioning & execution
|
||||
|
||||
- `Directory.Build.props` version bumps `0.2.3` → `0.3.0` (new feature; CLI is unpacked but
|
||||
the 5 packages ship together — publish decision deferred to rollout).
|
||||
- Implemented on worktree branch `worktree-secrets-interactive-cli`; merge to `main` when
|
||||
the full suite is green.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Separate `ZB.MOM.WW.Secrets.Cli.Interactive` library** — cleaner layering if another
|
||||
front-end ever hosts the flows; rejected as YAGNI for a one-exe operator tool.
|
||||
- **Prompts inside `SecretCommands`** — least code but couples UI into the tested headless
|
||||
layer and breaks its TextWriter-only contract; rejected.
|
||||
- **REPL shell / wizards-only UX** — rejected in favor of discoverable menus (operators
|
||||
under lockout stress shouldn't need to remember verbs).
|
||||
- **Own profiles file as source of truth** — rejected: can drift from what the app actually
|
||||
reads; parsing the app's own config is the honest source.
|
||||
Reference in New Issue
Block a user