From c42a7b2ef5fe4cdc9d8ba3a6813a19cae14e325d Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Tue, 4 Nov 2025 15:51:17 -0500 Subject: [PATCH] [PM-27506] CLI allows creating SSH key items with null fields (#17063) * Added checks to on the sshkey view to prevent null fields * Give default values to the template * Give default values to the template * change function signature to match ts-strct styles * Added unit tests for the ssh key to view and replaced deafults to empty strings --- apps/cli/src/vault/create.command.ts | 22 ++++++------ .../src/models/export/cipher.export.spec.ts | 34 +++++++++++++++++++ .../src/models/export/ssh-key.export.ts | 17 +++++++++- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index 03a205e9c4..5602c59394 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -92,18 +92,18 @@ export class CreateCommand { } private async createCipher(req: CipherExport) { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - - const cipherView = CipherExport.toView(req); - const isCipherTypeRestricted = - await this.cliRestrictedItemTypesService.isCipherRestricted(cipherView); - - if (isCipherTypeRestricted) { - return Response.error("Creating this item type is restricted by organizational policy."); - } - - const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId); try { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + const cipherView = CipherExport.toView(req); + const isCipherTypeRestricted = + await this.cliRestrictedItemTypesService.isCipherRestricted(cipherView); + + if (isCipherTypeRestricted) { + return Response.error("Creating this item type is restricted by organizational policy."); + } + + const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId); const newCipher = await this.cipherService.createWithServer(cipher); const decCipher = await this.cipherService.decrypt(newCipher, activeUserId); const res = new CipherResponse(decCipher); diff --git a/libs/common/src/models/export/cipher.export.spec.ts b/libs/common/src/models/export/cipher.export.spec.ts index 42c01ccef1..53541aa675 100644 --- a/libs/common/src/models/export/cipher.export.spec.ts +++ b/libs/common/src/models/export/cipher.export.spec.ts @@ -3,6 +3,8 @@ import { SecureNoteExport } from "@bitwarden/common/models/export/secure-note.ex import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { SshKeyExport } from "./ssh-key.export"; + describe("Cipher Export", () => { describe("toView", () => { it.each([[null], [undefined]])( @@ -41,4 +43,36 @@ describe("Cipher Export", () => { expect(resultView.deletedDate).toEqual(request.deletedDate); }); }); + + describe("SshKeyExport.toView", () => { + const validSshKey = { + privateKey: "PRIVATE_KEY", + publicKey: "PUBLIC_KEY", + keyFingerprint: "FINGERPRINT", + }; + + it.each([null, undefined, "", " "])("should throw when privateKey is %p", (value) => { + const sshKey = { ...validSshKey, privateKey: value } as any; + expect(() => SshKeyExport.toView(sshKey)).toThrow("SSH key private key is required."); + }); + + it.each([null, undefined, "", " "])("should throw when publicKey is %p", (value) => { + const sshKey = { ...validSshKey, publicKey: value } as any; + expect(() => SshKeyExport.toView(sshKey)).toThrow("SSH key public key is required."); + }); + + it.each([null, undefined, "", " "])("should throw when keyFingerprint is %p", (value) => { + const sshKey = { ...validSshKey, keyFingerprint: value } as any; + expect(() => SshKeyExport.toView(sshKey)).toThrow("SSH key fingerprint is required."); + }); + + it("should succeed with valid inputs", () => { + const sshKey = { ...validSshKey }; + const result = SshKeyExport.toView(sshKey); + expect(result).toBeDefined(); + expect(result?.privateKey).toBe(validSshKey.privateKey); + expect(result?.publicKey).toBe(validSshKey.publicKey); + expect(result?.keyFingerprint).toBe(validSshKey.keyFingerprint); + }); + }); }); diff --git a/libs/common/src/models/export/ssh-key.export.ts b/libs/common/src/models/export/ssh-key.export.ts index 9bae57ee62..fe528ac6ba 100644 --- a/libs/common/src/models/export/ssh-key.export.ts +++ b/libs/common/src/models/export/ssh-key.export.ts @@ -16,7 +16,22 @@ export class SshKeyExport { return req; } - static toView(req: SshKeyExport, view = new SshKeyView()) { + static toView(req?: SshKeyExport, view = new SshKeyView()): SshKeyView | undefined { + if (req == null) { + return undefined; + } + + // Validate required fields + if (!req.privateKey || req.privateKey.trim() === "") { + throw new Error("SSH key private key is required."); + } + if (!req.publicKey || req.publicKey.trim() === "") { + throw new Error("SSH key public key is required."); + } + if (!req.keyFingerprint || req.keyFingerprint.trim() === "") { + throw new Error("SSH key fingerprint is required."); + } + view.privateKey = req.privateKey; view.publicKey = req.publicKey; view.keyFingerprint = req.keyFingerprint;