diff --git a/docs/plans/2026-06-02-auth-audit-normalization-phase1.md b/docs/plans/2026-06-02-auth-audit-normalization-phase1.md index 5430bf9..85378dc 100644 --- a/docs/plans/2026-06-02-auth-audit-normalization-phase1.md +++ b/docs/plans/2026-06-02-auth-audit-normalization-phase1.md @@ -142,6 +142,35 @@ a guard/contract test. → Bearer, per-method-approval → scopes/constraints, **all inbound keys re-issued**). Largest/highest-risk single item in the program; warrants its own focused pass (likely decomposed). +## ScadaBridge ApiKeys re-architecture — spec (FULL ADOPT, 2026-06-02) + +Decision: **full adopt** the library SQLite store + scopes model. Single consistent contract all layers build to: + +- **Token format**: `Authorization: Bearer sbk__` (prefix `sbk`). Replaces the raw `X-API-Key` header. +- **Scope model = method name.** A key's `Scopes` set = the API-method names it may call. `ApiMethod.ApprovedApiKeyIds` + (CSV of key int IDs) is **retired**; per-method approval moves to the key's scopes. Auth check at the endpoint: + `identity.Scopes.Contains(methodName)`. +- **Storage**: inbound keys move to the library's SQLite store (new `ScadaBridge:InboundApi:ApiKeyStore` sqlite path + + pepper via `ApiKeyOptions.PepperSecretName`, `RunMigrationsOnStartup`). The SQL Server `ApiKey` entity is retired; + `ApiMethod` is KEPT minus `ApprovedApiKeyIds` (EF migration drops the column). `InboundApiRepository` loses its ApiKey + methods + `GetApprovedKeysForMethodAsync`. +- **Auth path** (`InboundAPI`): endpoint reads Bearer, calls library `IApiKeyVerifier.VerifyAsync`, then the scope check. + PRESERVE the security invariants: 401 (missing/invalid/disabled), **403 identical message for both "method not found" + and "not in scope"** (enumeration-safety, InboundAPI-011), constant-time compare (library does it), active-node 503 + + body-cap 413 filters unchanged, audit actor = key DisplayName. Delete `ApiKeyValidator` hashing + `ApiKeyHasher`. +- **Management** (`ManagementActor` + CLI `security api-key` + Commons messages): drive the library `IApiKeyAdminStore` + + `ApiKeySecretGenerator`. `create` returns `sbk__` once (plaintext-once preserved); methods a key may call + = its scopes, set on create/update (e.g. `--methods a,b` or grant/revoke-method commands). `list` returns id/name/enabled + (no secret), `update --enabled`, `delete`/revoke. Audit preserved. +- **CentralUI**: `ApiKeys.razor` (list/create/toggle/delete via admin store; show token once), `ApiKeyForm.razor` (edit the + key's method-scopes), `ApiMethodForm.razor` (method-side "approved keys" now reads/writes key scopes across keys). +- **Breaking change**: all inbound keys re-issued (new format); clients switch `X-API-Key` → `Authorization: Bearer`. + Needs a runbook + CHANGELOG. Re-pin ScadaBridge Auth packages to **0.1.2**. + +Sub-tasks (sequential where files overlap): **(A)** storage retire + EF migration + library wiring/options; +**(B)** auth-path rewrite (Bearer + verifier + scope check); **(C)** management (ManagementActor + CLI + messages); +**(D)** CentralUI pages; **(E)** runbook/CHANGELOG + integration test sweep. A→(B,C)→D→E. + ## Resolved decisions (2026-06-02) - **Decision A — ScadaBridge inbound API keys depth → (a) FULL ADOPT.** Re-architect inbound-API auth to the