Commit 728bd30
authored
LEGLINK-620: Add-Secret-Key-to-Vendor (#1788)
* LEGLINK-620: record the vendor signing-key UI design
Captures the decisions behind associating a Key Vault secret with a vendor,
so the reasoning survives outside a chat log: why the association is
vendor-scoped rather than facility-scoped (Veradigm needs a different key
generation algorithm than Epic and Cerner), and why the stored value is the
Key Vault secret id rather than a JWKS kid, which LEGLINK-63's title
conflates.
Also records what this ticket deliberately cannot do. The update endpoint
that persists a secret id is owned by neither LEGLINK-620 nor LEGLINK-743,
so the UI is built against a single isolated service method. LEGLINK-63's
audit-trail acceptance criterion is produced from backend managers onto
Kafka and no UI change can satisfy it.
Claude-Session: https://claude.ai/code/session_01STFKxmDaJenKVny6WUoWKo
* LEGLINK-620: associate a Key Vault secret with a vendor in the UI
Link signs the JWT that Data Acquisition presents to Epic and Cerner during
client-credentials auth. LEGLINK-14 moved the PEM into Key Vault; this makes
the association explicit and vendor-scoped, because Veradigm needs a
different key generation algorithm and so cannot share a key.
Add an edit path to Vendor Management. The dashboard grows a Secret ID
column that reads "Not set" when empty and a per-row edit action; the form
grows a JWT / Authentication panel holding the Key Vault Secret ID, expanded
when a vendor already has one so existing configuration is visible without
hunting. An emptied box travels as undefined rather than "", so clearing the
association reads as absent rather than set-to-empty.
No update endpoint exists yet. The Vendor model is moving out of
Normalization into Tenant under LEGLINK-743, whose acceptance criteria cover
list, add and delete but not update, so this operation is owned by neither
ticket. VendorService.updateVendor is the single place that knows the route:
when the contract lands, that method is the only edit required. A
config-flagged dual path was considered and rejected as permanent complexity
bought against a decision expected within days.
Two defects in files this already touches: createVendor was typed as
IVendorConfigModel while callers pass a name string, which interpolated
"[object Object]" into the URL for anything else; and getVendors never
cleared its loading flag on success.
Deferred: the mocked Playwright spec the design calls for. That harness
arrives with PR #1773, which is not yet merged into dev, so there is nowhere
on this branch for the spec to live. LEGLINK-63's audit-trail criterion is
also outstanding -- audit events are produced from backend managers onto
Kafka, so it belongs with the update endpoint rather than here.
16 unit specs pass; the app builds clean.
Claude-Session: https://claude.ai/code/session_01STFKxmDaJenKVny6WUoWKo
* LEGLINK-620: report a failed vendor save once, and clear a key explicitly
Two defects found reviewing 4b894e1 against the design.
ErrorHandlingService raises its own toastr before rethrowing, so once the
save paths began emitting failure to the dialog -- which shows a snackbar and
stays open so the admin's input survives -- one failed save reported itself
twice, toastr bottom-full-width and snackbar top-right. Saves now route
through handleSaveError, which suppresses the toastr and leaves the dialog as
the single surface. List and delete keep theirs, having no dialog to carry
the news. The rethrown error still carries the sanitized message either way.
Clearing a key sent secretId as undefined, which JSON.stringify drops, so the
field never reached the wire. An absent field reads as "leave unchanged" to
any endpoint with partial-update semantics, which would have made clearing an
association succeed visibly and do nothing. It now travels as an explicit
null, and the design's open items record that the backend must honour null as
"remove the association" when the contract is settled.
Adds vendor.service.spec.ts, the service having had no direct coverage: the
update route and body, a cleared key surviving serialization, name escaping
in the create route, and the toastr suppressed for saves but kept for list
and delete. Both new behaviours fail against the previous code -- args[1] was
absent rather than false, and secretId was undefined rather than null.
21 unit specs pass; the app builds clean.
Claude-Session: https://claude.ai/code/session_01CX9BzMrPhTzGSakXYDaAVa
* LEGLINK-620: cover the vendor create failure branch
A failed createVendor had no test. Assert it emits a single failure with
the error message and does not fall through into the update path.
Claude-Session: https://claude.ai/code/session_01TDPYGwkQDCXConLTuyU4is
* LEGLINK-620: gate vendor editing until an update endpoint exists
VendorController exposes list, add and delete only -- no PUT -- so the edit
dialog added on this branch would save into a 404. Put the edit button and
onEdit behind a vendorEditEnabled config flag, shipped off, following the
existing AppConfig boolean pattern. Flip it once the update contract,
including clearing secretId with null, is confirmed.
Claude-Session: https://claude.ai/code/session_01TDPYGwkQDCXConLTuyU4is
* LEGLINK-566: design for validating a vendor secret id against Key Vault
Validation lives on Admin.BFF, which already holds an ISecretManager and is
independent of the Vendor model's move to Tenant. Adds ISecretInspector and
PemSigningKeyValidator to Shared; the UI warns inline on blur and on save
without ever blocking the save.
Claude-Session: https://claude.ai/code/session_01TDPYGwkQDCXConLTuyU4is
* LEGLINK-566: implementation plan for vendor secret id validation
Eight tasks, TDD throughout: characterize EpicAuth's PKCS#8 behavior, then
PemSigningKeyValidator and ISecretInspector in Shared, the Admin.BFF endpoint,
the Angular service call, and the form's blur/save warnings.
Claude-Session: https://claude.ai/code/session_01TDPYGwkQDCXConLTuyU4is
* LEGLINK-620: store a vendor's Key Vault signing key secret id
Vendor moved into Tenant under LEGLINK-743 carrying only Id and Name, so there
was nowhere to record the Key Vault secret holding a vendor's PEM signing key.
LEGLINK-63 scopes that key to the vendor rather than the facility, because the
key generation algorithm differs by EHR.
Stored as a JSON column rather than a plain one so later vendor-level auth
settings need no migration, following the AuthenticationConfiguration precedent
in DataAcquisitionDbContext. Only the signing key lives here: TokenUrl, Audience
and ClientId are per-EHR-instance and stay on the facility's authentication
configuration, where EpicAuth already reads them.
The value converter carries an explicit ValueComparer. Without one EF snapshots
the property by reference, so mutating a field on the existing instance is never
detected and SaveChanges writes nothing -- a test covers that specifically.
Update treats a missing authentication object as "leave unchanged", matching how
Name already behaves, so a caller that omits it cannot wipe a configured key.
Clearing a key means sending the object with a null inside it.
Vendor versions expose the parent vendor's settings as a read-only projection,
so consumers holding only a vendor version id -- as Data Acquisition will -- can
resolve the key in one call while writes stay on the vendor.
EpicAuth is untouched: it still derives {facilityId}-pem, so nothing changes at
runtime until the fallback rule between vendor and facility keys is settled.
Claude-Session: https://claude.ai/code/session_01QoUHyt1ALkruCbuYxHSXiB
* LEGLINK-620: point the vendor screens at the Tenant API
LEGLINK-743 deleted Normalization's VendorController when it moved the model to
Tenant, but left this service calling the old routes. Every vendor operation in
Admin.UI has been hitting a controller that no longer exists.
The API nests the signing key under authentication; the vendor screens work with
a flat secretId. Translating at the gateway keeps that difference out of the
components, so a second vendor-level setting only touches this file.
Create now carries the secret id. The add form has always shown the field, but
the create branch sent the name alone, so anything typed there was silently
discarded behind a success message. Both write paths build the same payload
before branching, which is what stops them drifting apart again.
authentication is always sent, including when the key is being cleared: the
Tenant manager reads an absent object as "leave unchanged", so omitting it would
make a clear no-op. Null inside the object is what removes the association.
The vendorEditEnabled flag is gone with it. It existed only to keep the edit
button hidden while no update endpoint existed, and PUT /api/vendor/{id} now
does, so the gate has nothing left to protect.
Claude-Session: https://claude.ai/code/session_01QoUHyt1ALkruCbuYxHSXiB
* LEGLINK-620: drop the superpowers design and plan docs
The vendor signing-key design and the secret-id validation design and plan were
working notes for this change, not reference material the repository needs to
carry. They stay recoverable through history.
Claude-Session: https://claude.ai/code/session_01QoUHyt1ALkruCbuYxHSXiB
* LEGLINK-620: Add Secret Key to Vendor screen
* LEGLINK-620: revert local environment files off the branch
The previous commit swept in five files that belong to a local development
setup rather than to this change. Both appsettings.Development.json files in
particular replaced the committed SQLEXPRESS defaults with a machine-specific
SQL Server instance and a plaintext sa password, which would have become the
checked-in default for everyone.
Restores all five to their dev contents so the branch carries only the vendor
signing key work. The AGENTS.md documentation, the check_health.sh compose fix
and the Admin.UI Dockerfile npm layer-caching fix are worth landing, but each
on its own terms rather than inside this ticket.
Claude-Session: https://claude.ai/code/session_01QoUHyt1ALkruCbuYxHSXiB1 parent 10cb5f5 commit 728bd30
29 files changed
Lines changed: 1273 additions & 325 deletions
File tree
- DotNet
- ServiceTests
- IntegrationTests/Tenant
- UnitTests/Tenant
- Tenant
- Business
- Managers
- Queries
- Controllers
- Data
- Entities
- Repository
- Migrations
- Web/Admin.UI/src
- app
- components/vendor
- vendor-config-form
- vendor-dashboard
- interfaces
- tenant
- vendor
- services
- gateway/vendor
- assets
- docs/superpowers/specs
Lines changed: 30 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
36 | 66 | | |
37 | 67 | | |
38 | 68 | | |
| |||
Lines changed: 91 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
75 | 76 | | |
76 | 77 | | |
77 | 78 | | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
78 | 169 | | |
79 | 170 | | |
80 | 171 | | |
| |||
Lines changed: 78 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
Lines changed: 47 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
Lines changed: 3 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
12 | 15 | | |
13 | 16 | | |
Lines changed: 3 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
12 | 15 | | |
13 | 16 | | |
Lines changed: 13 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
16 | 20 | | |
17 | 21 | | |
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
26 | 30 | | |
27 | 31 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
80 | | - | |
| 80 | + | |
| 81 | + | |
81 | 82 | | |
82 | 83 | | |
83 | 84 | | |
| |||
91 | 92 | | |
92 | 93 | | |
93 | 94 | | |
94 | | - | |
| 95 | + | |
| 96 | + | |
95 | 97 | | |
96 | 98 | | |
97 | 99 | | |
| |||
187 | 189 | | |
188 | 190 | | |
189 | 191 | | |
| 192 | + | |
| 193 | + | |
190 | 194 | | |
191 | 195 | | |
192 | 196 | | |
193 | 197 | | |
194 | 198 | | |
195 | | - | |
| 199 | + | |
| 200 | + | |
196 | 201 | | |
197 | 202 | | |
198 | 203 | | |
| |||
0 commit comments