Encryption at rest: operator surface (P3A) - #7501
Open
ConnorYoh wants to merge 13 commits into
Open
Conversation
Contributor
🌐 TOML Translation Verification Summary🔄 Reference Branch:
|
Puts the P2 admin API behind a UI. Storyboard state: components and Storybook states are real, wired to MSW fixtures rather than a live backend, so the look and the copy can be reviewed before the API is plumbed in. Covers the P3A user stories: whether encryption is on and how much is covered, the master-key fingerprint for verifying a backup, the scope key table with the kill switch, the encrypt-existing migration including its FAILED state, rotation status, the encrypted badge, and the Pro-tier notice that audit events need Enterprise. The revoke dialog states the two consequences that surprised us during the P2 review: revoking does not stop the scope uploading, and on a cluster other nodes take up to 60 seconds. Copy keyed by backend enum values (key status, write state, migration state, refusal reason) is assembled at runtime, so those families are registered in the translation audit's ignore list. Not yet wired: key provenance and cluster-enabled both need small backend additions, and the badge is not yet mounted in the documents list.
Reviewing the storyboard shots surfaced problems the code did not. The never-used state rendered five cards of machinery that does not exist yet: a master key with no fingerprint, a rotation card reading "Unknown", and an empty key table complete with column headers. It now collapses to coverage plus the backlog, which is the whole story at that point. The plaintext backlog printed 1840 rather than 1,840: the plural key interpolated the raw count. Count still drives plural selection, a formatted value is what renders. Coverage had a bar and no number, in the default blue at every value. It now shows the percentage and turns green only at full coverage, matching how the sibling storage meter uses colour. Also: the unavailable states (storage disabled, registry unreadable) were a bare centred message with no heading, orphaned in the middle of the Storage tab, so they keep the section header and sit in a card; the fingerprint sat half a card away from its own label; the two split cards had ragged heights; banners butted against the content below them; a stopped migration drew a healthy blue progress bar; the in-memory progress caveat showed even with no run to lose; and Start sat disabled next to a backlog the copy said could be encrypted now, with no reason given.
…lesheet The panel borrowed three portal-infra__ text classes, and one of them (portal-infra__muted) had already been deleted by #7497's trim, so the key table was one merge away from losing its styling silently. Those three now live in the portal-enc__ namespace. The stories keep importing Infrastructure.css, because SectionHeader has no stylesheet of its own and the real view supplies it: without the import the stories render at a different scale to the app, which makes the captured screenshots misleading rather than merely unstyled.
CI caught two things. Infrastructure.test.tsx pins which tabs are inert, and storage is no longer one of them. The disabled-deep-link case moves to models, and storage gains its own click and deep-link cases. EncryptionPanel is mocked there like the other tabs, since that test is about tab wiring, not panel internals. The a11y scan flagged duplicate banner landmarks: SectionHeader renders a <header>, which is a banner unless a sectioning element scopes it, and the panel placed several of them in plain divs and cards. The old StorageTab wrapped every one in a <section>, which is why it never tripped this. Each header is now scoped the same way. The revoke story's play function asserted toBeVisible on the dialog copy; the a11y harness does not compute layout, so it failed there while rendering correctly in a browser. It asserts presence instead.
Coverage counts now refresh when a migration reaches a terminal state; the card previously kept its pre-run numbers until a reload, and Start stayed enabled against a backlog that no longer existed. Coverage colour keys off plaintextFiles rather than the exact fraction, so 4,127 of 4,128 no longer renders "100% encrypted" in amber beside a bar that is not green. The rounded percentage is computed once and shared. Coverage copy now states what the feature covers. Only FileStorageService and WorkflowSessionService create StoredFile rows, and nothing under policy/network writes through StorageProvider, so files a pipeline reads in place (including SFTP, FTP and SMB sources) are not encrypted at rest and were being counted as if they did not exist. The rotation backlog is now counted in the database and returned by /status. Filtering the key array was correct only while that array is the whole table, and this PR proposes paginating it; under-reporting 0 pending rows is exactly the signal an operator uses to decide the outgoing master key is safe to delete. /status also reports the storage provider, and the panel names it and warns that object-store downloads stream through the server while anything is encrypted, which is otherwise an invisible consequence of switching this on. Copying the fingerprint now confirms, and falls back to selecting the text when navigator.clipboard is absent, as it is on the plain-http origins many self-hosted installs are reached on. Also: the in-memory-progress caveat is shown at IDLE when a backlog remains, since that is the state a run lost to a restart leaves behind; the 403 reason match no longer depends on one exact English sentence; EncryptedFileBadge drops featureInUse, which no caller could supply and which a plaintext file already implies; and the revoke dialog passes the scope label straight to the modal instead of through a key whose entire value was an interpolation.
CI's proprietary typecheck reported no exported member Table or TableColumn on @app/ui, while Button, Card, Modal and StatusBadge from the same barrel resolved fine, and the vite build in the same run transformed every module without complaint. It does not reproduce locally on any of the nine typecheck variants. The three portal components that already pull Table from the barrel (DocumentExtractions, ReviewQueueTable, PolicyCatalogueTable) all do it as one combined import; this file was alone in splitting the value import and two type imports across three statements from the same module. Matching the form that demonstrably passes CI.
Root cause of the CI typecheck failure: #7312 renamed core/ui/Table to DataTable with a different API, after this branch was cut. CI builds the merge with main, so it saw the rename while the branch tip did not, which is why only Table and TableColumn were missing from @app/ui while every other name in the same import resolved, and why no local typecheck could reproduce it until the rebase. Rebased onto main and moved to the column vocabulary DataTable requires: entity for scope plus key version, badge for status, muted for the last change, and actions for revoke/restore, which also gets the destructive tone the raw button never had. The previous commit's guess at the import form was wrong and is superseded.
ConnorYoh
force-pushed
the
claude/encryption-ui-p3a
branch
from
August 17, 2026 17:31
cd57ef3 to
22a928c
Compare
The /status key list is not paged: it is findAll(). Both comments claimed otherwise, which would mislead anyone deciding whether the count is redundant.
theme-lint reads #bac in #backing-up-the-master-key as a hex colour. Building the fragment from an anchor argument avoids the false positive without an exemption comment claiming a URL is a colour.
Revoke opened a dialog explaining itself while rotation and migration fired straight off the button, which had the friction backwards: revoke is reversible, those two are not, and migration cannot be stopped once started. Both now confirm. The migration dialog says there is no stop control, since that is the part an operator cannot discover until it is too late. Also unstacks two Javadoc blocks in the status response: the blob-backend comment had been orphaned above masterKeySource, leaving provider undocumented.
# Conflicts: # frontend/editor/src/core/i18n/translationAudit.ts # frontend/editor/src/portal/views/Infrastructure.test.tsx # frontend/editor/src/portal/views/Infrastructure.tsx
Contributor
🚀 V2 Auto-Deployment Complete!🔗 Direct Test URL (non-SSL) http://54.175.155.236:7501 🧩 Admin portal included - try it at http://54.175.155.236:7501/portal. 📚 Storybook: http://54.175.155.236:27501/ 2 stories changedStories
This deployment will be automatically cleaned up when the PR is closed. 🔄 Auto-deployed for approved V2 contributors. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Encryption at rest: operator surface (P3A)
Encryption at rest has shipped twice and has never been visible in the product. P1 (#7155) built the
crypto, P2 (#7173) built the admin API. Today an operator enables it by editing YAML, revokes a key
with curl, and has no screen anywhere telling them the feature exists or whether their files are
actually encrypted.
This PR puts the P2 API behind a UI. No crypto changes, no schema changes.
Scope
In: the Storage tab re-enabled with encryption as its content, key actions, migration control,
rotation status, the encrypted badge on documents, and honest licence messaging.
Note on placement: #7497 removed the Infrastructure tabs backed by mock-only
/v1/infrastructure/*data, including the old Storage tab, leavingstorageas a disabledplaceholder. This PR re-enables it, because it now reads the real
/api/v1/admin/storage-encryptionsurface, which is the criterion #7497 used. The mock usage,quota, provider and retention UI that used to live there is not restored.
Out: per-source encryption (P3B, needs a schema change), KMS and BYOK backends (P4), FIPS builds
(P4).
User stories
Knowing where you stand
US-1. See whether encryption is actually on
As an administrator, I want one screen that tells me whether stored files are being encrypted, so
that I can answer "are we encrypted at rest?" without reading config files or logs.
writeEnabled: new uploads are encrypted, or they are notencryptedFilesandplaintextFilescounts, so partial coverage is obviouswriteEnabled: false, active: true(decrypt-only, the state after turning theflag off) from
writeEnabled: false, active: false(feature never used)startup
database. The panel shows that as an explanatory state, not an error toast
unavailable and suggests checking the database, rather than showing zero keys
US-2. Verify my key backup matches the running system
As an administrator, I want to compare the live master key against my backup without exposing key
material, so that I can prove my disaster recovery actually works before I need it.
masterKeyFingerprint(SHA-256 prefix, 16 hex characters) andmasterKeyVersionso an operator can check their archived key is the one in use
machinery has not been materialised
US-3. Be told when my licence gives me no audit trail
As a compliance reviewer on a Pro licence, I want the product to tell me that encryption events are
not being recorded, so that I do not report an audit trail to my auditor that does not exist.
events are not recorded, this requires Enterprise
devGuide/STORAGE_ENCRYPTION_AT_REST.md, so the log,the docs and the UI agree
Note: encryption is gated at Pro but
AuditServiceonly records on Enterprise. Today this isvisible only in a startup log line. If we would rather move audit down to Pro, that is a licence
decision and this story changes to "remove the notice".
US-4. Be told my master key was generated for me
As an administrator of a single-node install, I want to know that the system created an encryption
key on my behalf, so that I do not discover an unbacked-up secret after losing it.
file-encryption.keyfile rather than explicitconfig, the panel says so and states the consequence: lose this file and encrypted files
cannot be recovered
/statusaddition to report key provenance (config, environment, or generatedfile). Included in this PR
Acting on it
US-5. Revoke a scope's stored content
As an administrator responding to an incident, I want to revoke access to one team's stored files,
so that their content cannot be read while I investigate.
POST /keys/{keyId}/disablestatusChangedByandstatusChangedAtNote: the "still uploads" and "60 seconds" points are not padding. Both are real behaviour that
surprised us during review, and an admin who believes revoke means "frozen instantly" will be wrong.
US-6. Restore access, and understand what came back
As an administrator, I want to undo a revocation and see exactly what state the key returned to, so
that I am not misled into thinking a key is wrapping new files when it is not.
POST /keys/{keyId}/enableRETIRED if one was minted while it was revoked
and a newer key is wrapping new uploads
US-7. Encrypt the files I already had
As an administrator who has just enabled encryption, I want to encrypt the existing plaintext
backlog and watch it happen, so that "encrypted at rest" is true of my whole estate rather than only
new uploads.
POST /migrate, disabled whenplaintextFilesis zeroGET /migrate/status: state, total, processed, skipped, failed, started timewrite flag is turned off mid-run, and the message says so and what to do
copy alone
failure
appearing to lose the run
UI without a stop button is not defensible
US-8. Finish a key rotation without sealing my files
As an administrator rotating the master key, I want to see how many key rows are still on the old
key, so that I do not remove the outgoing key while rows still depend on it.
masterKeyVersionand the number of key rows below itPOST /master/rotateand reports therewrappedcountoperation, and this button only performs the re-wrap step
unreadable
Everyone else
US-9. See which of my files are encrypted — moved to #7550
The badge component was built here but never wired to a surface, and no documents endpoint returns
encryptionKeyId, so it could not render in the product. It moved to its own draft PR rather thanshipping as unreachable code behind a screenshot.
US-10. Understand why a file will not open
As a user whose team's key has been revoked, I want a clear message rather than a generic error, so
that I raise the right request with the right person.
generic failure toast
Definition of done
portal/api/, following the conventions insources.tsen-UScatalogue, which is the source of truth<button>elements, per the lint rule/statuspaginates its key list, since this UI is the consumer that makes an unbounded listmatter
Not covered in this PR
The five unticked boxes above, all tracked elsewhere. Everything else was checked against the code,
not assumed.
Split into follow-up PRs:
encryptionKeyIdbefore it can render anywherepause knobs, and pagination on the
/statuskey listAlso tracked: #7549, to publish the operator docs. The two runbook links in this PR point at
devGuide/STORAGE_ENCRYPTION_AT_REST.mduntil those pages exist; both are constants instorageEncryption.ts, so swapping them is a one-file change.Backend changes riding along
Two fields added to
/status. Nothing else in the backend changed:masterKeySource(config, environment, generated)FileEncryptionMasterKey.resolveKeyand was simply never returnedprovider(local, database, s3)pendingRotationRowsTwo items from the original plan are still not done and are called out below: key-list pagination,
and the migration cancel endpoint and knobs.
Screenshots
Replace each placeholder with the matching image.
Extras captured while iterating, not required in the body: registry unavailable

(


13), generated master key (14), dark migration (
15).
Test plan
What is actually covered:
encryption off, active, Pro notice, revoke dialog, revoked row, restore-as-RETIRED, migration
running, migration FAILED, rotation pending, storage disabled (403), registry unreadable (503),
and generated master key
Infrastructure.test.tsxcovers the tab wiring: Storage is enabled, reachable by click and by?tab=storage, and disabled tabs stay inertFileEncryptionMasterKeySourceTestcovers key provenance, including that the wire names match thevalues the UI switches on
Tooltiphas a play-function story asserting it opens on keyboard focus and wiresaria-describedbyNot covered, and worth a reviewer's attention:
enable-returns-RETIRED path are exercised by stories, which render them but assert almost nothing.
This is the biggest remaining gap in the PR
revoked key. Every state in this PR was driven by fixtures
Notes for reviewers
The revoke confirmation copy is deliberately blunt about two things that are easy to get wrong:
revoking does not stop the team storing new files, and on a cluster it takes up to a minute. Both
were found during the P2 review. If the copy reads as over-explaining, that is the intent.