UI design spec — Unified Secret Provider (Angular)
- Status: Design (binding for the FE sprint, SPRINT_20260622_006).
- Date: 2026-06-22
- Grounded in: the web-ui audit. Reuses existing components/routes under
src/Web/StellaOps.Web/src/app; no fabricated paths.
All new UI gates on the existing scope helpers in src/app/core/auth/scopes.ts (CRYPTO_READ, CRYPTO_ADMIN, CRYPTO_PROFILE_ADMIN, CRYPTO_KEK_READ, CRYPTO_KEK_ROTATE). Never hardcode mock data; drive all state from backend clients (seed via DB/backend).
1. Master-key setup step (first-run wizard)
- Where:
src/app/features/setup-wizard/— addmaster-key-stepundercomponents/, register insetup-wizard.routes.ts’sSetupWizardComponentflow, ordered immediately before the existing Crypto step. - Backend: new Platform endpoints
POST /api/v1/admin/setup/crypto/master-key/{generate,import}andPOST /api/v1/admin/setup/crypto/master-key/probe(gatescrypto:kek:rotate). - Form (new shared component
SecretMaterialInputComponent):- Key format toggle: Symmetric (default) | Asymmetric (RSA/EC).
- Symmetric: Generate button (calls
/generate, shows fingerprint + one-time copy) OR Import (maskedtype=passwordpaste). - Asymmetric: Generate keypair (shows fingerprint + one-time PEM download for backup) OR Import PEM/PKCS#8 (textarea).
- Backend source select (
Crypto:Kek:Source):env | file | vault | hsm | asymmetric. - Provider backend select (
Crypto:SecretProvider:Backend):builtin | vault | openbao.
- Gate: step cannot complete until
/probe(round-trip seal/open) returns OK. Show a RED banner with operator-actionable guidance on failure (do not advance). - Backup warning: mandatory acknowledgement that master-key loss == ciphertext loss; link to
docs/runbooks/secret-provider-master-key-rotation.md.
2. Crypto / secret settings page (extend crypto-control)
- Where: extend
src/app/features/console-admin/crypto-control/and its routes inconsole-admin.routes.ts(existing/crypto-control,/crypto-control/:kekId,/crypto-control/:kekId/rotate,/crypto-control/validate). - Additions to
crypto-control-list.component.ts:- Surface the active provider backend (
builtin/vault/openbao) and the KEK source kind + fingerprint perkek_id, sourced from the newISecretProvider.DescribeAsyncprojection inCryptoControlService. - “Secrets audit” sub-view: all active credential references across backends
- last-rotated date (read-only,
crypto:read).
- last-rotated date (read-only,
- Surface the active provider backend (
- Rotate wizard (
crypto-rotate-wizard.component.ts): extend Step 2 so whennewSource === 'asymmetric'(or hsm) the operator enters a key reference / imports PEM viaSecretMaterialInputComponent— references only, never raw bytes on the wire (preserve the existing “server never echoes KEK material” constraint). - Scope banner: add reusable
ScopeGatingBannerComponentto replace ad-hoc per-component scope checks (used here + registry + setup).
3. Registry provider-selection UI
- Where:
src/app/features/integration-hub/(parentsetup/integrations) andsrc/app/features/integrations/integration-wizard.component.ts(auth step). - Additions: the auth step’s
CredentialStorageModeselector gains an explicit secret backend choice (builtin / vault / openbao) that maps toISecretProvider. Whenvault/openbao, reuse the existingSecretAuthorityTarget/SecretRefpicker (Vault path or pasted AuthRef). Whenbuiltin, the secret is sealed inline via the provider. - Add a timeout/error boundary on Vault/OpenBao AuthRef discovery (the audit flagged an indefinite spinner when the backend is slow/offline).
4. Deployment provider / setup UI
- Tenant default: existing
src/app/features/console-admin/tenants/tenant-deployment-secret-mode.component.ts(pull vs push) is unchanged; document that the secret backend now flows throughISecretProvider. - New deployment secret-backend selector: add a shared
DeploymentSecretBackendSelectorComponentconsumed by the release-create flow (release orchestrator feature) to pickbuiltin | vault | openbaoper deployment, defaulting to the tenant/installation default. Gatesorch:operate. - Setup: the wizard’s master-key step (§1) also persists the deployment provider backend so push-mode bundles seal under the unified provider.
5. Shared components introduced (reused across §1–§4)
| Component | Purpose | Home |
|---|---|---|
SecretMaterialInputComponent | Toggle generate/import, symmetric/asymmetric, masked paste | src/app/shared/secret-provider/ |
ScopeGatingBannerComponent | “You need scope X” actionable banner | src/app/shared/secret-provider/ |
SecretBackendSelectorComponent | builtin/vault/openbao picker (registry + deployment reuse) | src/app/shared/secret-provider/ |
6. DI / services
- Extend
CryptoControlService(crypto-control.service.ts) withdescribeProvider()andgetSecretsAudit()— proxying the new Platform endpoints via the existing gateway prefix pattern. No new global facade is required for v1; keep service-level clients.
8. SecretReferenceInputComponent — URL references everywhere (ADR-032)
- Status: Design (binding for SPRINT_20260622_011).
- ADR: ADR-032; grammar in secret-reference-url-scheme.md.
- Home:
src/Web/StellaOps.Web/src/app/shared/secret-provider/— a NEWsecret-reference-input.component.ts(+.html,.spec.ts) that EVOLVES from the existing shared building blocks (reusesSecretProviderService,ScopeGatingBannerComponent; sits alongsideSecretMaterialInputComponent).
8.1 What it is
The everyday secret-reference control. It edits a canonical URL (the non-secret pointer) and provides:
- URL field — free-text canonical reference (
<scheme>://[id/]path[#version]), parsed + validated client-side (mirrorSecretReferencerules); shows the resolved scheme/provider/path breakdown as a hint. - Select… affordance — a two-step picker: (a) choose provider from the
/secret-providerslist (kind + id + reachable badge), then (b) choose path. - Debounced autocomplete (~250 ms) — as the operator types a path prefix, calls
GET /secret-providers/{id}/paths?prefix=&pageToken=and lists path names only (never values),crypto:read-gated, paged (“load more”). - Reference-existing / Seal-new toggle:
- Reference existing (default) — pick a path that already holds a secret; the form value is just the URL.
- Seal new here — type a new path under the chosen provider + supply the secret value once (reuse the masked input from
SecretMaterialInput); on save the host calls the backend seal and stores the returned URL.
- Scope gating — wrap in
ScopeGatingBannerComponent; autocomplete + Select requirecrypto:read; seal-new requires the surface’s existing write scope.
8.2 Distinction from SecretMaterialInputComponent
SecretMaterialInput stays for offline master-key material (raw key paste / PEM / generate at setup). SecretReferenceInput is for everyday secret references (URL + provider/path picker + autocomplete). A surface migrating from a plain password input adopts SecretReferenceInput; it does NOT use SecretMaterialInput.
8.3 Service additions (secret-provider.service.ts)
listProviders(): Observable<SecretProviderSummary[]>; // GET /secret-providers
listPaths(providerId, prefix, pageToken?): Observable<PathPage>; // GET /secret-providers/{id}/paths
parseReference(url): ParsedSecretReference; // client-side mirror of SecretReference
PathPage = { paths: string[]; nextPageToken: string | null }. Never carries secret values.
8.4 Per-surface migration table (every Tier 1/2/3 surface)
| Tier | Surface (path under src/Web/StellaOps.Web/src/app/) | Field(s) to migrate | Target | Notes |
|---|---|---|---|---|
| 1 | features/integrations/integration-wizard.component.{ts,html} | basic password, token, OAuth2 clientSecret, secret-authority bundle entry values | SecretReferenceInput (picker + autocomplete + seal-new) | registry-credential-backend selector already migrated; these auth fields are STILL plain password inputs |
| 1 | features/setup-wizard/ master-key-step | master-key material | KEEP SecretMaterialInput (already WF3) | offline material, not a reference — no change |
| 1 | features/notify/notify-panel.* | channel secretRef | SecretReferenceInput | upgrade existing secretRef text field to URL picker/autocomplete |
| 1 | features/admin-notifications/components/channel-management.component.ts | channel secretRef | SecretReferenceInput | already a secretRef text field — add picker/autocomplete |
| 2 | features/console-admin/connector-credentials/connector-credentials-list.component.ts | api_key/PAT, basic password, oauth2 clientSecret, cert PEM + passphrase | SecretReferenceInput (reference-existing + seal-new) | plain inputs today; cert PEM uses seal-new (large value) |
| 2 | features/sbom-sources/components/source-wizard/source-wizard.component.ts | basic/token/oauth2 secrets + authref field | SecretReferenceInput | authref:// parses as vault alias — existing values keep working |
| 2 | features/vault-bundles/vault-bundle-author-page.component.ts | bundle entry secret values | SecretReferenceInput | values may seal-new into a chosen provider |
| 3 | features/release-orchestrator/.../create-release.component.ts | deployment backend selector | already migrated | verify it adopts URL form; no plain-input fields remain |
| 3 | features/console-admin/crypto-control/crypto-rotate-wizard.component.ts | KEK material | KEEP SecretMaterialInput | references-only constraint already satisfied; no change |
| 3 | features/console-admin/tenants/tenant-deployment-secret-mode.component.ts | deployment secret mode | SecretReferenceInput where it stores a reference; mode toggle unchanged |
Migration discipline: each surface keeps its existing form-control name + emitted value shape as a URL string; carry the surface’s .spec.ts (TESTING_PRACTICES Rule A) and add a spec asserting the emitted value is a canonical URL and that autocomplete never surfaces secret values.
