SP-1173: add CUI marking for files the CLI writes to disk - #405
Draft
Dennis Woditsch (dwoditsch) wants to merge 5 commits into
Draft
SP-1173: add CUI marking for files the CLI writes to disk#405Dennis Woditsch (dwoditsch) wants to merge 5 commits into
Dennis Woditsch (dwoditsch) wants to merge 5 commits into
Conversation
Route the JSON output of `list spaces` and `list packages` through a new CuiFileService, which asks the team's CUI settings how the content is classified and names the output accordingly: a `CUI - <name>.zip` holding the listing plus the decoded cover sheet when categories apply, an `Unclassified - <name>` rename when none do, and the original filename when no marking applies at all. CuiService owns the API contract and derives enablement from the response status, so the CLI translates the backend's answer rather than deciding entitlement itself. HttpClient.getStatusAndData exposes the status code because get() throws on 4xx and would otherwise hide the 204/403 signal. BaseManager.findAll now awaits onFindAll so an async listing callback completes before the command resolves. Includes-AI-Code: true Co-authored-by: Cursor <cursoragent@cursor.com>
Once findAll moved to async/await, return Promise.reject() was equivalent to throwing undefined: Sonar flags it, and the command handler logged "undefined" instead of the real cause. Rethrow the original error. Includes-AI-Code: true Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the 403 branch from CuiService. Any status other than 200 or 204 now raises a FatalError, so no file is written when the marking cannot be resolved. Also flatten the nested template literal in the error path, which Sonar flags. Includes-AI-Code: true Co-authored-by: Cursor <cursoragent@cursor.com>
403 means the feature flag is disabled, 204 means the team has CUI disabled. Both keep the original filename, as separate branches so the debug log says which one applied. Any other non-200 status still fails closed. Includes-AI-Code: true Co-authored-by: Cursor <cursoragent@cursor.com>
CuiPdfCoverResponse is the only type consumed outside the module, so the three helper interfaces are inlined into it. Categories are counted, never read, so their element type carries no weight. Includes-AI-Code: true Co-authored-by: Cursor <cursoragent@cursor.com>
|
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.




Description
Every user-facing file content-cli writes to disk has to carry the team's CUI (Controlled Unclassified Information) marking. This PR adds the mechanism and wires up the first two commands; the remaining writers follow in later PRs against the same service.
Architecture. Two classes, layered so each has one job, and neither is specific to the commands wired up here:
CuiServiceowns the API contract. It callsGET /api/team/cui-settings/cui-pdf-coverand turns the response into a decision. Enablement is derived from the status code, so the CLI translates what the backend says rather than deciding entitlement itself.CuiFileServiceturns that decision into a filename and a write, delegating all disk I/O to the existing statelessfileService. One place knows about CUI, one place touches the filesystem.Call sites stay thin. A writer builds its payload, hands it to
CuiFileService, and logs whichever filename comes back, so adding the next command is a one-line swap fromfileServicetoCuiFileService.States. The status code decides whether marking applies at all, and the body decides how it is marked:
flowchart TD write["a command writes a file"] --> cover["GET /api/team/cui-settings/cui-pdf-cover"] cover --> status{"response"} status -->|"403, feature flag disabled"| plain["original name<br/>no marking applies"] status -->|"204, CUI disabled for the team"| plain status -->|"200, no categories"| unclassified["'Unclassified - name'<br/>rename only"] status -->|"200, with categories"| classified["'CUI - name.zip'<br/>payload + CUI_Cover_Sheet.pdf"] status -->|"anything else"| fatal["FatalError<br/>no file written"]categories: unclassified, so rename only, prefixedUnclassified -. No archive and no cover sheet, since there is nothing to cover.categories: classified, so the payload is wrapped intoCUI - <name>.zipalongsideCUI_Cover_Sheet.pdf, decoded from the base64coverPagein the response.FatalError, and no file is written. A 500 or an unexpected code means the marking could not be resolved, which is not the same as knowing that no marking applies.Commands that only print to the console never probe the endpoint, because nothing reaches disk.
Scope: how the write is triggered
Grouped by what makes the command produce a file, since that determines how marking has to be applied.
--jsonlist spaces,list packages<uuid>.json--jsonlist assets,list assignments,list data-pools,config list/diff/validate,config metadata export,config package list/validate/import,config versions get/create,config variables list,config nodes *,config branch *,t2tc package list/diff,deployment *,asset-registry *<uuid>.json, some named, e.g.config_validate_report_<uuid>.json-o, --outputToJsonFileanalyze action-flows,import action-flows,export data-pool,import data-poolsaction-flows_metadata_<uuid>.json,batch_import_report_<uuid>.json--zipconfig package export,config branch export<packageKey>.zip, single packaget2tc package export(and deprecatedconfig export),export action-flows,pull package(deprecated)export_<uuid>.zip(batch archive, manifest plus one nested zip per package),action-flows_export_<uuid>.zip,package_<key>.zippull asset,pull skill,pull bookmarks,pull view-bookmarks,pull data-pool.yml/.json--unzip, output is a directoryconfig package export,config branch export,t2tc package export --unzipTwo things worth settling before the follow-ups:
For the rows that already produce a zip, marking should add
CUI_Cover_Sheet.pdfinto the existing archive and rename it, not nest a zip inside a zip.CuiFileServicecurrently takes a serialized string and builds the archive itself, so it needs a second entry point that merges into an archive it is handed.For the row that writes a directory tree, there is no single file to rename. Options are to drop the cover sheet into the directory, prefix the directory name, or force a zip when marking applies.
Supporting changes.
HttpClient.getStatusAndDataexposes the status code, because the existingget()throws on 4xx and would hide the 403 and 204 signals that carry the meaning here.BaseManager.findAllnow awaitsonFindAllso an async listing callback finishes before the command resolves; that method also moved from anew Promise(...)wrapper to plain async/await.Rollout. The cover endpoint is allowlisted in policy-guard for staging only, via celonis/policy-guard#127, which changed
staging/policies/service-policies/team.rego. Until the matching production rule lands, the gateway answers 403 there and every writer takes the unmarked path, so this ships inert in production rather than breaking anything.Relevant links
Checklist