-
Notifications
You must be signed in to change notification settings - Fork 4
SP-1173: add CUI marking for files the CLI writes to disk #405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
404b82a
SP-1173: mark CUI content when writing Studio listings to disk
dwoditsch f6cc020
SP-1173: throw instead of Promise.reject in BaseManager.findAll
dwoditsch f08c6ae
SP-1173: treat only 204 as "no CUI marking"
dwoditsch 843a094
SP-1173: treat 403 and 204 as no CUI marking
dwoditsch a317f63
SP-1173: trim the CuiService response types
dwoditsch 5745783
SP-1173: sonar
dwoditsch 66a75f8
SP-1173: comment and bugbot
dwoditsch File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { HttpClient } from "../http/http-client"; | ||
| import { FatalError, logger } from "./logger"; | ||
| import { Context } from "../command/cli-context"; | ||
|
|
||
| export interface CuiPdfCoverResponse { | ||
| resolvedCuiMarking?: { categories?: unknown[] }; | ||
| coverPage?: { pdfContent: string; encoding: string }; | ||
| } | ||
|
|
||
| export class CuiApi { | ||
| private static readonly CUI_PDF_COVER_SHEET_URL = "/api/team/cui-settings/cui-pdf-cover"; | ||
|
|
||
| private static readonly STATUS_OK = 200; | ||
| private static readonly STATUS_NO_CONTENT = 204; | ||
| private static readonly STATUS_FORBIDDEN = 403; | ||
|
|
||
| private readonly httpClient: () => HttpClient; | ||
|
|
||
| constructor(context: Context) { | ||
| this.httpClient = () => context.httpClient; | ||
| } | ||
|
|
||
| public async getCuiPdfCover(): Promise<CuiPdfCoverResponse | null> { | ||
| const { status, data } = await this.httpClient().getStatusAndData(CuiApi.CUI_PDF_COVER_SHEET_URL); | ||
|
|
||
| if (status === CuiApi.STATUS_FORBIDDEN) { | ||
| logger.debug("CUI marking does not apply, the feature flag is disabled"); | ||
| return null; | ||
| } | ||
|
|
||
| if (status === CuiApi.STATUS_NO_CONTENT) { | ||
| logger.debug("CUI marking does not apply, the team has CUI disabled"); | ||
| return null; | ||
| } | ||
|
|
||
| if (status === CuiApi.STATUS_OK && data) { | ||
| return data as CuiPdfCoverResponse; | ||
| } | ||
|
|
||
| throw new FatalError("Problem fetching cui pdf cover"); | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import * as path from "node:path"; | ||
| import AdmZip = require("adm-zip"); | ||
| import { Context } from "../command/cli-context"; | ||
| import { CuiApi, CuiPdfCoverResponse } from "./cui-api"; | ||
| import { fileService } from "./file-service"; | ||
| import { FileConstants } from "./file.constants"; | ||
| import { FatalError } from "./logger"; | ||
|
|
||
| export class CuiFileService { | ||
| public static readonly COVER_SHEET_FILE_NAME = "CUI_Cover_Sheet.pdf"; | ||
| public static readonly CLASSIFIED_PREFIX = "CUI - "; | ||
| public static readonly UNCLASSIFIED_PREFIX = "Unclassified - "; | ||
|
|
||
| private static readonly BASE64_ENCODING = "base64"; | ||
|
|
||
| private readonly cuiApi: CuiApi; | ||
|
|
||
| constructor(context: Context) { | ||
| this.cuiApi = new CuiApi(context); | ||
| } | ||
|
|
||
| public async writeToFileWithGivenName(data: string, filename: string): Promise<string> { | ||
| const cover = await this.cuiApi.getCuiPdfCover(); | ||
|
|
||
| if (cover === null) { | ||
| fileService.writeToFileWithGivenName(data, filename); | ||
| return filename; | ||
| } | ||
|
|
||
| if (!this.isClassified(cover)) { | ||
| const unclassifiedName = this.prefixFileName(filename, CuiFileService.UNCLASSIFIED_PREFIX); | ||
| fileService.writeToFileWithGivenName(data, unclassifiedName); | ||
| return unclassifiedName; | ||
| } | ||
|
|
||
| return this.writeClassifiedArchive(filename, data, cover); | ||
| } | ||
|
|
||
| private writeClassifiedArchive(filename: string, data: string, cover: CuiPdfCoverResponse): string { | ||
| const zip = new AdmZip(); | ||
| zip.addFile(path.basename(filename), Buffer.from(data, "utf-8"), "", FileConstants.DEFAULT_FILE_PERMISSIONS); | ||
| zip.addFile( | ||
| CuiFileService.COVER_SHEET_FILE_NAME, | ||
| this.decodeCoverPage(cover), | ||
| "", | ||
| FileConstants.DEFAULT_FILE_PERMISSIONS | ||
| ); | ||
|
|
||
| const archiveName = this.buildClassifiedArchiveName(filename); | ||
| fileService.writeBufferToFileWithGivenName(zip.toBuffer(), archiveName); | ||
|
|
||
| return archiveName; | ||
| } | ||
|
|
||
| private decodeCoverPage(cover: CuiPdfCoverResponse): Buffer { | ||
| const coverPage = cover.coverPage; | ||
| if (!coverPage?.pdfContent) { | ||
| throw new FatalError("CUI marking applies but the response contained no cover page."); | ||
| } | ||
| if (coverPage.encoding !== CuiFileService.BASE64_ENCODING) { | ||
| throw new FatalError(`Unsupported CUI cover page encoding: ${coverPage.encoding}`); | ||
| } | ||
|
|
||
| return Buffer.from(coverPage.pdfContent, CuiFileService.BASE64_ENCODING); | ||
| } | ||
|
|
||
| private isClassified(cover: CuiPdfCoverResponse): boolean { | ||
| return (cover.resolvedCuiMarking?.categories?.length ?? 0) > 0; | ||
| } | ||
|
|
||
| private buildClassifiedArchiveName(filename: string): string { | ||
| const baseName = path.basename(filename); | ||
| const nameWithoutExtension = baseName.slice(0, baseName.length - path.extname(baseName).length); | ||
|
|
||
| return path.join(path.dirname(filename), `${CuiFileService.CLASSIFIED_PREFIX}${nameWithoutExtension}.zip`); | ||
| } | ||
|
|
||
| private prefixFileName(filename: string, prefix: string): string { | ||
| return path.join(path.dirname(filename), `${prefix}${path.basename(filename)}`); | ||
| } | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| import { resolve } from "node:path"; | ||
| import { readFileSync } from "node:fs"; | ||
| import AdmZip = require("adm-zip"); | ||
| import { mockAxiosGet, mockAxiosGetWithStatus, mockedAxiosInstance } from "../../utls/http-requests-mock"; | ||
| import { SpaceCommandService } from "../../../src/commands/studio/command-service/space-command.service"; | ||
| import { PackageCommandService } from "../../../src/commands/studio/command-service/package-command.service"; | ||
| import { testContext } from "../../utls/test-context"; | ||
| import { loggingTestTransport } from "../../jest.setup"; | ||
| import { FileService } from "../../../src/core/utils/file-service"; | ||
| import { CuiFileService } from "../../../src/core/utils/cui-file-service"; | ||
|
|
||
| const SPACES_URL = "https://myTeam.celonis.cloud/package-manager/api/spaces"; | ||
| const PACKAGES_URL = "https://myTeam.celonis.cloud/package-manager/api/packages"; | ||
| const COVER_URL = "https://myTeam.celonis.cloud/api/team/cui-settings/cui-pdf-cover"; | ||
|
|
||
| const SPACES = [{ id: "space-1", name: "My Space" }]; | ||
| const PACKAGES = [{ key: "pkg-1", name: "My Package", rootNodeKey: "pkg-1" }]; | ||
| const LISTED_PACKAGES = [{ key: "pkg-1", name: "My Package" }]; | ||
| const PDF_BYTES = Buffer.from("%PDF-1.4 cover sheet"); | ||
|
|
||
| function classifiedCover(): object { | ||
| return { | ||
| resolvedCuiMarking: { categories: [{ code: "PRVCY", name: "Privacy" }] }, | ||
| coverPage: { pdfContent: PDF_BYTES.toString("base64"), encoding: "base64" }, | ||
| }; | ||
| } | ||
|
|
||
| function loggedFileName(): string { | ||
| return loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; | ||
| } | ||
|
|
||
| function readWrittenJson(filename: string): any { | ||
| return JSON.parse(readFileSync(resolve(process.cwd(), filename), "utf-8")); | ||
| } | ||
|
|
||
| function payloadFromArchive(filename: string): any { | ||
| const archive = new AdmZip(readFileSync(resolve(process.cwd(), filename))); | ||
| const entries = archive.getEntries().map(entry => entry.entryName); | ||
|
|
||
| expect(entries).toContain(CuiFileService.COVER_SHEET_FILE_NAME); | ||
| expect(archive.getEntry(CuiFileService.COVER_SHEET_FILE_NAME).getData().equals(PDF_BYTES)).toBe(true); | ||
|
|
||
| const payloadEntry = entries.find(entry => entry.endsWith(".json")); | ||
| return JSON.parse(archive.getEntry(payloadEntry).getData().toString()); | ||
| } | ||
|
|
||
| function coverWasRequested(): boolean { | ||
| return (mockedAxiosInstance.get as jest.Mock).mock.calls.some(call => call[0] === COVER_URL); | ||
| } | ||
|
|
||
| describe("CUI marking of Studio listings", () => { | ||
|
|
||
| describe("list spaces --json", () => { | ||
| const listSpaces = () => new SpaceCommandService(testContext).listSpaces(true); | ||
|
|
||
| beforeEach(() => { | ||
| mockAxiosGet(SPACES_URL, SPACES); | ||
| }); | ||
|
|
||
| it("Should wrap the listing and the cover sheet into a CUI archive when classified", async () => { | ||
| mockAxiosGetWithStatus(COVER_URL, 200, classifiedCover()); | ||
|
|
||
| await listSpaces(); | ||
|
|
||
| const filename = loggedFileName(); | ||
| expect(filename.startsWith(CuiFileService.CLASSIFIED_PREFIX)).toBe(true); | ||
| expect(filename.endsWith(".zip")).toBe(true); | ||
| expect(payloadFromArchive(filename)).toEqual(SPACES); | ||
| }); | ||
|
|
||
| it("Should keep the original filename when no marking applies", async () => { | ||
| mockAxiosGetWithStatus(COVER_URL, 204, ""); | ||
|
|
||
| await listSpaces(); | ||
|
|
||
| const filename = loggedFileName(); | ||
| expect(filename.startsWith(CuiFileService.UNCLASSIFIED_PREFIX)).toBe(false); | ||
| expect(filename.startsWith(CuiFileService.CLASSIFIED_PREFIX)).toBe(false); | ||
| expect(readWrittenJson(filename)).toEqual(SPACES); | ||
| }); | ||
|
|
||
| it("Should not probe CUI when the listing only goes to the console", async () => { | ||
| await new SpaceCommandService(testContext).listSpaces(false); | ||
|
|
||
| expect(loggingTestTransport.logMessages[0].message).toContain(`space-1 - Name: "My Space"`); | ||
| expect(coverWasRequested()).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("list packages --json", () => { | ||
| const listPackages = () => new PackageCommandService(testContext).listPackages(true, false, []); | ||
|
|
||
| beforeEach(() => { | ||
| mockAxiosGet(PACKAGES_URL, PACKAGES); | ||
| }); | ||
|
|
||
| it("Should wrap the listing and the cover sheet into a CUI archive when classified", async () => { | ||
| mockAxiosGetWithStatus(COVER_URL, 200, classifiedCover()); | ||
|
|
||
| await listPackages(); | ||
|
|
||
| const filename = loggedFileName(); | ||
| expect(filename.startsWith(CuiFileService.CLASSIFIED_PREFIX)).toBe(true); | ||
| expect(filename.endsWith(".zip")).toBe(true); | ||
| expect(payloadFromArchive(filename)).toEqual(LISTED_PACKAGES); | ||
| }); | ||
|
|
||
| it("Should keep the original filename when no marking applies", async () => { | ||
| mockAxiosGetWithStatus(COVER_URL, 204, ""); | ||
|
|
||
| await listPackages(); | ||
|
|
||
| const filename = loggedFileName(); | ||
| expect(filename.startsWith(CuiFileService.UNCLASSIFIED_PREFIX)).toBe(false); | ||
| expect(filename.startsWith(CuiFileService.CLASSIFIED_PREFIX)).toBe(false); | ||
| expect(readWrittenJson(filename)).toEqual(LISTED_PACKAGES); | ||
| }); | ||
|
|
||
| it("Should not probe CUI when the listing only goes to the console", async () => { | ||
| await new PackageCommandService(testContext).listPackages(false, false, []); | ||
|
|
||
| expect(loggingTestTransport.logMessages[0].message).toContain(`My Package - Key: "pkg-1"`); | ||
| expect(coverWasRequested()).toBe(false); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.