plan(phase1): ScadaBridge re-arch discovered architecture (CentralUI direct-repo + TransportExport) + C1-C5 decomposition + transport=exclude-keys

This commit is contained in:
Joseph Doherty
2026-06-02 03:22:19 -04:00
parent a4f9968917
commit e69e9c635b
2 changed files with 46 additions and 2 deletions
@@ -189,11 +189,48 @@ keeps the build green at each step.
secret (proven by test asserting `SecretHash.SequenceEqual` + unchanged `last_used_utc`). This is what lets C/D edit a key's
method-scopes and toggle enabled WITHOUT re-issuing the token. **ScadaBridge must re-pin Auth packages 0.1.2 → 0.1.3.**
- **C (management), D (CentralUI), E (retire SQL Server ApiKey + ApiMethod.ApprovedApiKeyIds migration + runbook/CHANGELOG)
— IN PROGRESS (C next).** Mapping for C: `CreateApiKeyCommand``CreateKeyAsync` (keyId = `Guid.NewGuid().ToString("N")`,
— IN PROGRESS.** Mapping: `CreateApiKeyCommand``CreateKeyAsync` (keyId = `Guid.NewGuid().ToString("N")`,
DisplayName = name, scopes = `--methods`); `ListApiKeysCommand``ListKeysAsync` (enabled = `RevokedUtc is null`);
`UpdateApiKeyCommand(IsEnabled)``SetEnabledAsync`; new set-scopes path → `SetScopesAsync`; `DeleteApiKeyCommand`
revoke-then-`DeleteKeyAsync`. All management message keys switch `int ApiKeyId``string KeyId`.
### Discovered architecture (CentralUI Explore, 2026-06-02) — expands C/D/E
Two facts the original AE spec missed:
1. **CentralUI bypasses the ManagementActor.** `Components/Pages/Admin/ApiKeys.razor`, `ApiKeyForm.razor`, and
`Components/Pages/Design/ApiMethodForm.razor` call `IInboundApiRepository` (SQL Server EF) **directly** — they do NOT
send the `CreateApiKeyCommand`/etc. management messages. So there are **two** management entry points to rewire
(CLI→ManagementActor uses the messages; CentralUI→repository uses the entities). Decoupling: introduce one app-side
**`IInboundApiKeyAdmin` seam** over the library `ApiKeyAdminCommands`, and route BOTH CLI and CentralUI through it
(DRY + single audit path). The message-contract change (int→string) touches only CLI+ManagementActor; the
entity/repository change (`ApiKey.Id`, `ApiMethod.ApprovedApiKeyIds`) touches CentralUI + TransportExport.
2. **TransportExport couples API keys + methods into config export/import** (`Components/Pages/Design/TransportExport.razor`
+ `.razor.cs`, `HashSet<int>` selections, `ExportSelection`). With keys now in the library SQLite store (per-env pepper,
secret-once), a key can't be exported/re-imported usefully. **Decision (user, 2026-06-02): EXCLUDE inbound API keys from
transport — export API methods only; keys are re-created + method-scopes re-granted per environment.**
CentralUI blast radius (string keyId + scopes replace int Id + ApprovedApiKeyIds CSV): `Admin/ApiKeys.razor`,
`Admin/ApiKeyForm.razor`, `Design/ApiMethodForm.razor` (approved-keys ↔ key-scopes), `Design/TransportExport.razor(.cs)`,
`Design/ExternalSystems.razor` (uses method `int` id — methods STAY int in SQL Server, so unaffected for keys),
`Dashboard.razor` (key count), test `Admin/ApiKeyFormAuditDrillinTests.cs`.
### C/D/E decomposition — 5 reviewed green sub-commits (user: "coordinated multi-commit now", 2026-06-02)
- **C1** — re-pin ScadaBridge Auth 0.1.2→0.1.3; add app-side `IInboundApiKeyAdmin` seam (string-keyId model:
Create(name,methods)→(keyId,token) / List / SetEnabled / SetMethods / Delete[=revoke+delete] / GetMethodsForKey /
GetKeysForMethod) over the library facade; register `ApiKeyAdminCommands` + the seam in Host **and** CentralUI DI; seam
unit tests. **Purely additive — build green.**
- **C2** — Commons `Messages/Management/SecurityCommands.cs` contracts int→string keyId + add `Methods` + new
`SetApiKeyMethodsCommand`; rewire ManagementActor handlers + CLI `security api-key` onto the seam; update ManagementActor
tests. (CentralUI unaffected — it doesn't use these messages.)
- **C3** — CentralUI `ApiKeys.razor`/`ApiKeyForm.razor`/`ApiMethodForm.razor` (+ Dashboard count) off `IInboundApiRepository`-
for-keys onto the seam; string keyId; method-scope editing replaces `ApprovedApiKeyIds`; update bUnit test. (Methods stay
in SQL Server; just stop using the `ApprovedApiKeyIds` column — dropped in C5.)
- **C4** — TransportExport: remove API-key selection/export (methods-only); drop key `HashSet<int>` + `ExportSelection` keys;
tests.
- **C5 (=E)** — retire SQL Server `ApiKey` entity + DbContext reg + `IInboundApiRepository` key methods +
`GetApprovedKeysForMethodAsync`; drop `ApiMethod.ApprovedApiKeyIds`; EF migration (drop ApiKeys table + column); delete
residual `ApiKeyValidator`/`ApiKeyHasher`; runbook + CHANGELOG (breaking: re-issue keys, `X-API-Key``Authorization: Bearer`);
full build+test sweep.
## Resolved decisions (2026-06-02)
- **Decision A — ScadaBridge inbound API keys depth → (a) FULL ADOPT.** Re-architect inbound-API auth to the
@@ -30,7 +30,14 @@
{"id": 29, "subject": "Task 2.5: ScadaBridge rename→IAuditRedactor + AuditOutcome (#3) [high-risk]", "status": "pending", "blockedBy": [25]},
{"id": 30, "subject": "Task 3.1: Introduce IAuditActorAccessor seam", "status": "pending", "blockedBy": [9]},
{"id": 31, "subject": "Task 3.2-3.4: Wire emit sites to Auth principal (#4)", "status": "pending", "blockedBy": [30]}
{"id": 31, "subject": "Task 3.2-3.4: Wire emit sites to Auth principal (#4)", "status": "pending", "blockedBy": [30]},
{"id": 32, "subject": "Task 1.3-L: Extend Auth.ApiKeys admin store (SetScopes/SetEnabled) -> lib 0.1.3 (PUBLISHED)", "status": "completed", "blockedBy": []},
{"id": 33, "subject": "Task 1.3-C1: ScadaBridge re-pin 0.1.3 + IInboundApiKeyAdmin seam (additive)", "status": "pending", "blockedBy": [32]},
{"id": 34, "subject": "Task 1.3-C2: ManagementActor + CLI + Commons messages onto seam", "status": "pending", "blockedBy": [33]},
{"id": 35, "subject": "Task 1.3-C3: CentralUI pages onto seam (string keyId + scopes)", "status": "pending", "blockedBy": [33]},
{"id": 36, "subject": "Task 1.3-C4: TransportExport exclude API keys (methods-only)", "status": "pending", "blockedBy": [33, 35]},
{"id": 37, "subject": "Task 1.3-C5 (=E): retire SQL Server ApiKey entity + EF migration + runbook", "status": "pending", "blockedBy": [34, 35, 36]}
],
"lastUpdated": "2026-06-02"
}