From e291e2df0ac7eeaff7e13f1ecc79250aa988cb5c Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Mon, 23 Jun 2025 12:04:56 -0400 Subject: [PATCH] [PM-21451] [Vault] [CLI] Changes to Enforce "Remove card item type policy" (#15187) * Created new service to get restricted types for the CLI * Created service for cli to get restricted types * Utilized restriction service in commands * Renamed function * Refactored service and made it simpler to check when a cipher type is restricted or not * Moved service to common so it can be utilized on the cli * Refactored service to use restricted type service * Removed userId passing from commands * Exclude restrict types from export * Added missing dependency * Added missing dependency * Added missing dependency * Added service utils commit from desktop PR * refactored to use reusable function * updated reference * updated reference * Fixed merge conflicts * Refactired services to use isCipherRestricted * Refactored restricted item types service * Updated services to use the reafctored item types service --- .../browser/src/background/main.background.ts | 11 ++ .../vault-popup-list-filters.service.spec.ts | 2 + .../vault-popup-list-filters.service.ts | 7 +- apps/cli/src/commands/edit.command.ts | 9 ++ apps/cli/src/commands/get.command.ts | 53 +++++++-- apps/cli/src/commands/list.command.ts | 4 + apps/cli/src/oss-serve-configurator.ts | 5 + .../service-container/service-container.ts | 17 +++ apps/cli/src/tools/send/send.program.ts | 1 + apps/cli/src/vault.program.ts | 5 + apps/cli/src/vault/create.command.ts | 11 ++ apps/cli/src/vault/delete.command.ts | 9 ++ .../cli-restricted-item-types.service.spec.ts | 111 ++++++++++++++++++ .../cli-restricted-item-types.service.ts | 45 +++++++ .../shared/models/filter-function.spec.ts | 41 ------- .../shared/models/filter-function.ts | 14 +-- .../vault/individual-vault/vault.component.ts | 19 ++- .../src/services/jslib-services.module.ts | 2 + .../vault/components/vault-items.component.ts | 7 +- .../services/restricted-item-types.service.ts | 67 +++++++---- .../individual-vault-export.service.spec.ts | 53 +++++++++ .../individual-vault-export.service.ts | 18 ++- .../src/services/org-vault-export.service.ts | 34 ++++-- .../src/services/vault-export.service.spec.ts | 12 ++ 24 files changed, 444 insertions(+), 113 deletions(-) create mode 100644 apps/cli/src/vault/services/cli-restricted-item-types.service.spec.ts create mode 100644 apps/cli/src/vault/services/cli-restricted-item-types.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index f24d769b912..4ba869768f5 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -204,6 +204,7 @@ import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service"; import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks"; @@ -411,6 +412,7 @@ export default class MainBackground { inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; taskService: TaskService; cipherEncryptionService: CipherEncryptionService; + restrictedItemTypesService: RestrictedItemTypesService; ipcContentScriptManagerService: IpcContentScriptManagerService; ipcService: IpcService; @@ -1043,6 +1045,13 @@ export default class MainBackground { this.sdkService, ); + this.restrictedItemTypesService = new RestrictedItemTypesService( + this.configService, + this.accountService, + this.organizationService, + this.policyService, + ); + this.individualVaultExportService = new IndividualVaultExportService( this.folderService, this.cipherService, @@ -1053,6 +1062,7 @@ export default class MainBackground { this.kdfConfigService, this.accountService, this.apiService, + this.restrictedItemTypesService, ); this.organizationVaultExportService = new OrganizationVaultExportService( @@ -1065,6 +1075,7 @@ export default class MainBackground { this.collectionService, this.kdfConfigService, this.accountService, + this.restrictedItemTypesService, ); this.exportService = new VaultExportService( diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 8b2786fab77..cb29532c93c 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -76,6 +76,7 @@ describe("VaultPopupListFiltersService", () => { const restrictedItemTypesService = { restricted$: new BehaviorSubject([]), + isCipherRestricted: jest.fn().mockReturnValue(false), }; beforeEach(() => { @@ -729,6 +730,7 @@ function createSeededVaultPopupListFiltersService( const accountServiceMock = mockAccountServiceWith("userId" as UserId); const restrictedItemTypesServiceMock = { restricted$: new BehaviorSubject([]), + isCipherRestricted: jest.fn().mockReturnValue(false), } as any; const formBuilderInstance = new FormBuilder(); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 9f7363afd7e..ef843939035 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -39,10 +39,7 @@ import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; -import { - isCipherViewRestricted, - RestrictedItemTypesService, -} from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; import { ChipSelectOption } from "@bitwarden/components"; @@ -230,7 +227,7 @@ export class VaultPopupListFiltersService { } // Check if cipher type is restricted (with organization exemptions) - if (isCipherViewRestricted(cipher, restrictions)) { + if (this.restrictedItemTypesService.isCipherRestricted(cipher, restrictions)) { return false; } diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 677139d5451..ebf877011b7 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -24,6 +24,7 @@ import { Response } from "../models/response"; import { CliUtils } from "../utils"; import { CipherResponse } from "../vault/models/cipher.response"; import { FolderResponse } from "../vault/models/folder.response"; +import { CliRestrictedItemTypesService } from "../vault/services/cli-restricted-item-types.service"; export class EditCommand { constructor( @@ -34,6 +35,7 @@ export class EditCommand { private apiService: ApiService, private folderApiService: FolderApiServiceAbstraction, private accountService: AccountService, + private cliRestrictedItemTypesService: CliRestrictedItemTypesService, ) {} async run( @@ -95,6 +97,13 @@ export class EditCommand { return Response.badRequest("You may not edit a deleted item. Use the restore command first."); } cipherView = CipherExport.toView(req, cipherView); + + const isCipherRestricted = + await this.cliRestrictedItemTypesService.isCipherRestricted(cipherView); + if (isCipherRestricted) { + return Response.error("Editing this item type is restricted by organizational policy."); + } + const encCipher = await this.cipherService.encrypt(cipherView, activeUserId); try { const updatedCipher = await this.cipherService.updateWithServer(encCipher); diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 8554f8e2ae1..28a5680da77 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -27,7 +27,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response" import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; -import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; @@ -48,6 +48,7 @@ import { SendResponse } from "../tools/send/models/send.response"; import { CliUtils } from "../utils"; import { CipherResponse } from "../vault/models/cipher.response"; import { FolderResponse } from "../vault/models/folder.response"; +import { CliRestrictedItemTypesService } from "../vault/services/cli-restricted-item-types.service"; import { DownloadCommand } from "./download.command"; @@ -66,6 +67,7 @@ export class GetCommand extends DownloadCommand { private eventCollectionService: EventCollectionService, private accountProfileService: BillingAccountProfileStateService, private accountService: AccountService, + private cliRestrictedItemTypesService: CliRestrictedItemTypesService, ) { super(encryptService, apiService); } @@ -110,16 +112,16 @@ export class GetCommand extends DownloadCommand { } } - private async getCipherView(id: string): Promise { + private async getCipherView(id: string, userId: UserId): Promise { let decCipher: CipherView = null; - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + if (Utils.isGuid(id)) { - const cipher = await this.cipherService.get(id, activeUserId); + const cipher = await this.cipherService.get(id, userId); if (cipher != null) { - decCipher = await this.cipherService.decrypt(cipher, activeUserId); + decCipher = await this.cipherService.decrypt(cipher, userId); } } else if (id.trim() !== "") { - let ciphers = await this.cipherService.getAllDecrypted(activeUserId); + let ciphers = await this.cipherService.getAllDecrypted(userId); ciphers = this.searchService.searchCiphersBasic(ciphers, id); if (ciphers.length > 1) { return ciphers; @@ -133,20 +135,45 @@ export class GetCommand extends DownloadCommand { } private async getCipher(id: string, filter?: (c: CipherView) => boolean) { - let decCipher = await this.getCipherView(id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + let decCipher = await this.getCipherView(id, activeUserId); if (decCipher == null) { return Response.notFound(); } + if (Array.isArray(decCipher)) { + // Apply restricted ciphers filter + decCipher = await this.cliRestrictedItemTypesService.filterRestrictedCiphers(decCipher); + + if (decCipher.length === 0) { + return Response.error("Access to this item type is restricted by organizational policy."); + } + if (filter != null) { decCipher = decCipher.filter(filter); - if (decCipher.length === 1) { - decCipher = decCipher[0]; - } } - if (Array.isArray(decCipher)) { + + if (decCipher.length === 0) { + return Response.notFound(); + } + + if (decCipher.length === 1) { + decCipher = decCipher[0]; + } else { return Response.multipleResults(decCipher.map((c) => c.id)); } + } else { + const isCipherRestricted = + await this.cliRestrictedItemTypesService.isCipherRestricted(decCipher); + if (isCipherRestricted) { + return Response.error("Access to this item type is restricted by organizational policy."); + } + + // Apply filter if provided to single cipher + if (filter != null && !filter(decCipher)) { + return Response.notFound(); + } } await this.eventCollectionService.collect( @@ -317,7 +344,8 @@ export class GetCommand extends DownloadCommand { return cipherResponse; } - const cipher = await this.getCipherView(itemId); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const cipher = await this.getCipherView(itemId, activeUserId); if ( cipher == null || Array.isArray(cipher) || @@ -345,7 +373,6 @@ export class GetCommand extends DownloadCommand { return Response.multipleResults(attachments.map((a) => a.id)); } - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const canAccessPremium = await firstValueFrom( this.accountProfileService.hasPremiumFromAnySource$(activeUserId), ); diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 018e742baad..49ec7689b20 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -29,6 +29,7 @@ import { ListResponse } from "../models/response/list.response"; import { CliUtils } from "../utils"; import { CipherResponse } from "../vault/models/cipher.response"; import { FolderResponse } from "../vault/models/folder.response"; +import { CliRestrictedItemTypesService } from "../vault/services/cli-restricted-item-types.service"; export class ListCommand { constructor( @@ -41,6 +42,7 @@ export class ListCommand { private apiService: ApiService, private eventCollectionService: EventCollectionService, private accountService: AccountService, + private cliRestrictedItemTypesService: CliRestrictedItemTypesService, ) {} async run(object: string, cmdOptions: Record): Promise { @@ -134,6 +136,8 @@ export class ListCommand { ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash); } + ciphers = await this.cliRestrictedItemTypesService.filterRestrictedCiphers(ciphers); + await this.eventCollectionService.collectMany(EventType.Cipher_ClientViewed, ciphers, true); const res = new ListResponse(ciphers.map((o) => new CipherResponse(o))); diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index 1b11b467388..875b8cc7507 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -66,6 +66,7 @@ export class OssServeConfigurator { this.serviceContainer.eventCollectionService, this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.accountService, + this.serviceContainer.cliRestrictedItemTypesService, ); this.listCommand = new ListCommand( this.serviceContainer.cipherService, @@ -77,6 +78,7 @@ export class OssServeConfigurator { this.serviceContainer.apiService, this.serviceContainer.eventCollectionService, this.serviceContainer.accountService, + this.serviceContainer.cliRestrictedItemTypesService, ); this.createCommand = new CreateCommand( this.serviceContainer.cipherService, @@ -88,6 +90,7 @@ export class OssServeConfigurator { this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.organizationService, this.serviceContainer.accountService, + this.serviceContainer.cliRestrictedItemTypesService, ); this.editCommand = new EditCommand( this.serviceContainer.cipherService, @@ -97,6 +100,7 @@ export class OssServeConfigurator { this.serviceContainer.apiService, this.serviceContainer.folderApiService, this.serviceContainer.accountService, + this.serviceContainer.cliRestrictedItemTypesService, ); this.generateCommand = new GenerateCommand( this.serviceContainer.passwordGenerationService, @@ -117,6 +121,7 @@ export class OssServeConfigurator { this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.cipherAuthorizationService, this.serviceContainer.accountService, + this.serviceContainer.cliRestrictedItemTypesService, ); this.confirmCommand = new ConfirmCommand( this.serviceContainer.apiService, diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 05437e3e3d3..d2cc729e481 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -150,6 +150,7 @@ import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { legacyPasswordGenerationServiceFactory, @@ -187,6 +188,7 @@ import { I18nService } from "../platform/services/i18n.service"; import { LowdbStorageService } from "../platform/services/lowdb-storage.service"; import { NodeApiService } from "../platform/services/node-api.service"; import { NodeEnvSecureStorageService } from "../platform/services/node-env-secure-storage.service"; +import { CliRestrictedItemTypesService } from "../vault/services/cli-restricted-item-types.service"; // Polyfills global.DOMParser = new jsdom.JSDOM().window.DOMParser; @@ -287,6 +289,8 @@ export class ServiceContainer { masterPasswordApiService: MasterPasswordApiServiceAbstraction; bulkEncryptService: FallbackBulkEncryptService; cipherEncryptionService: CipherEncryptionService; + restrictedItemTypesService: RestrictedItemTypesService; + cliRestrictedItemTypesService: CliRestrictedItemTypesService; constructor() { let p = null; @@ -811,6 +815,7 @@ export class ServiceContainer { this.kdfConfigService, this.accountService, this.apiService, + this.restrictedItemTypesService, ); this.organizationExportService = new OrganizationVaultExportService( @@ -823,6 +828,7 @@ export class ServiceContainer { this.collectionService, this.kdfConfigService, this.accountService, + this.restrictedItemTypesService, ); this.exportService = new VaultExportService( @@ -864,6 +870,17 @@ export class ServiceContainer { ); this.masterPasswordApiService = new MasterPasswordApiService(this.apiService, this.logService); + + this.restrictedItemTypesService = new RestrictedItemTypesService( + this.configService, + this.accountService, + this.organizationService, + this.policyService, + ); + + this.cliRestrictedItemTypesService = new CliRestrictedItemTypesService( + this.restrictedItemTypesService, + ); } async logout() { diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index 052faa33867..6af714cb786 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -153,6 +153,7 @@ export class SendProgram extends BaseProgram { this.serviceContainer.eventCollectionService, this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.accountService, + this.serviceContainer.cliRestrictedItemTypesService, ); const response = await cmd.run("template", object, null); this.processResponse(response); diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 4393075810d..d5615d0bb1c 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -114,6 +114,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.apiService, this.serviceContainer.eventCollectionService, this.serviceContainer.accountService, + this.serviceContainer.cliRestrictedItemTypesService, ); const response = await command.run(object, cmd); @@ -188,6 +189,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.eventCollectionService, this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.accountService, + this.serviceContainer.cliRestrictedItemTypesService, ); const response = await command.run(object, id, cmd); this.processResponse(response); @@ -233,6 +235,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.organizationService, this.serviceContainer.accountService, + this.serviceContainer.cliRestrictedItemTypesService, ); const response = await command.run(object, encodedJson, cmd); this.processResponse(response); @@ -280,6 +283,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.apiService, this.serviceContainer.folderApiService, this.serviceContainer.accountService, + this.serviceContainer.cliRestrictedItemTypesService, ); const response = await command.run(object, id, encodedJson, cmd); this.processResponse(response); @@ -323,6 +327,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.cipherAuthorizationService, this.serviceContainer.accountService, + this.serviceContainer.cliRestrictedItemTypesService, ); const response = await command.run(object, id, cmd); this.processResponse(response); diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index b1536e23748..39a0b8d464d 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -29,6 +29,7 @@ import { CliUtils } from "../utils"; import { CipherResponse } from "./models/cipher.response"; import { FolderResponse } from "./models/folder.response"; +import { CliRestrictedItemTypesService } from "./services/cli-restricted-item-types.service"; export class CreateCommand { constructor( @@ -41,6 +42,7 @@ export class CreateCommand { private accountProfileService: BillingAccountProfileStateService, private organizationService: OrganizationService, private accountService: AccountService, + private cliRestrictedItemTypesService: CliRestrictedItemTypesService, ) {} async run( @@ -90,6 +92,15 @@ 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 newCipher = await this.cipherService.createWithServer(cipher); diff --git a/apps/cli/src/vault/delete.command.ts b/apps/cli/src/vault/delete.command.ts index d1b0b093cf8..8df1f8f316e 100644 --- a/apps/cli/src/vault/delete.command.ts +++ b/apps/cli/src/vault/delete.command.ts @@ -13,6 +13,8 @@ import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cip import { Response } from "../models/response"; import { CliUtils } from "../utils"; +import { CliRestrictedItemTypesService } from "./services/cli-restricted-item-types.service"; + export class DeleteCommand { constructor( private cipherService: CipherService, @@ -22,6 +24,7 @@ export class DeleteCommand { private accountProfileService: BillingAccountProfileStateService, private cipherAuthorizationService: CipherAuthorizationService, private accountService: AccountService, + private cliRestrictedItemTypesService: CliRestrictedItemTypesService, ) {} async run(object: string, id: string, cmdOptions: Record): Promise { @@ -60,6 +63,12 @@ export class DeleteCommand { return Response.error("You do not have permission to delete this item."); } + const isCipherTypeRestricted = + await this.cliRestrictedItemTypesService.isCipherRestricted(cipher); + if (isCipherTypeRestricted) { + return Response.error("Deleting this item type is restricted by organizational policy."); + } + try { if (options.permanent) { await this.cipherService.deleteWithServer(id, activeUserId); diff --git a/apps/cli/src/vault/services/cli-restricted-item-types.service.spec.ts b/apps/cli/src/vault/services/cli-restricted-item-types.service.spec.ts new file mode 100644 index 00000000000..8e3f40c5d3f --- /dev/null +++ b/apps/cli/src/vault/services/cli-restricted-item-types.service.spec.ts @@ -0,0 +1,111 @@ +import { BehaviorSubject, of } from "rxjs"; + +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + RestrictedItemTypesService, + RestrictedCipherType, +} from "@bitwarden/common/vault/services/restricted-item-types.service"; + +import { CliRestrictedItemTypesService } from "./cli-restricted-item-types.service"; + +describe("CliRestrictedItemTypesService", () => { + let service: CliRestrictedItemTypesService; + let restrictedSubject: BehaviorSubject; + let restrictedItemTypesService: Partial; + + const cardCipher: CipherView = { + id: "cipher1", + type: CipherType.Card, + organizationId: "org1", + } as CipherView; + + const loginCipher: CipherView = { + id: "cipher2", + type: CipherType.Login, + organizationId: "org1", + } as CipherView; + + const identityCipher: CipherView = { + id: "cipher3", + type: CipherType.Identity, + organizationId: "org2", + } as CipherView; + + beforeEach(() => { + restrictedSubject = new BehaviorSubject([]); + + restrictedItemTypesService = { + restricted$: restrictedSubject, + isCipherRestricted: jest.fn(), + isCipherRestricted$: jest.fn(), + }; + + service = new CliRestrictedItemTypesService( + restrictedItemTypesService as RestrictedItemTypesService, + ); + }); + + describe("filterRestrictedCiphers", () => { + it("filters out restricted cipher types from array", async () => { + restrictedSubject.next([{ cipherType: CipherType.Card, allowViewOrgIds: [] }]); + + (restrictedItemTypesService.isCipherRestricted as jest.Mock) + .mockReturnValueOnce(true) + .mockReturnValueOnce(false) + .mockReturnValueOnce(false); + const ciphers = [cardCipher, loginCipher, identityCipher]; + + const result = await service.filterRestrictedCiphers(ciphers); + + expect(result).toEqual([loginCipher, identityCipher]); + }); + + it("returns all ciphers when no restrictions exist", async () => { + restrictedSubject.next([]); + + (restrictedItemTypesService.isCipherRestricted as jest.Mock).mockReturnValue(false); + + const ciphers = [cardCipher, loginCipher, identityCipher]; + const result = await service.filterRestrictedCiphers(ciphers); + + expect(result).toEqual(ciphers); + }); + + it("handles empty cipher array", async () => { + const result = await service.filterRestrictedCiphers([]); + + expect(result).toEqual([]); + }); + }); + + describe("isCipherRestricted", () => { + it("returns true for restricted cipher type with no organization exemptions", async () => { + (restrictedItemTypesService.isCipherRestricted$ as jest.Mock).mockReturnValue(of(true)); + + const result = await service.isCipherRestricted(cardCipher); + expect(result).toBe(true); + }); + + it("returns false for non-restricted cipher type", async () => { + (restrictedItemTypesService.isCipherRestricted$ as jest.Mock).mockReturnValue(of(false)); + + const result = await service.isCipherRestricted(loginCipher); + expect(result).toBe(false); + }); + + it("returns false when no restrictions exist", async () => { + (restrictedItemTypesService.isCipherRestricted$ as jest.Mock).mockReturnValue(of(false)); + + const result = await service.isCipherRestricted(cardCipher); + expect(result).toBe(false); + }); + + it("returns false for organization cipher when organization is in allowViewOrgIds", async () => { + (restrictedItemTypesService.isCipherRestricted$ as jest.Mock).mockReturnValue(of(false)); + + const result = await service.isCipherRestricted(cardCipher); + expect(result).toBe(false); + }); + }); +}); diff --git a/apps/cli/src/vault/services/cli-restricted-item-types.service.ts b/apps/cli/src/vault/services/cli-restricted-item-types.service.ts new file mode 100644 index 00000000000..4219330bc67 --- /dev/null +++ b/apps/cli/src/vault/services/cli-restricted-item-types.service.ts @@ -0,0 +1,45 @@ +import { firstValueFrom } from "rxjs"; + +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + RestrictedCipherType, + RestrictedItemTypesService, +} from "@bitwarden/common/vault/services/restricted-item-types.service"; + +export class CliRestrictedItemTypesService { + constructor(private restrictedItemTypesService: RestrictedItemTypesService) {} + + /** + * Gets all restricted cipher types for the current user. + * + * @returns Promise resolving to array of restricted cipher types with allowed organization IDs + */ + async getRestrictedTypes(): Promise { + return firstValueFrom(this.restrictedItemTypesService.restricted$); + } + + /** + * Filters out restricted cipher types from an array of ciphers. + * + * @param ciphers - Array of ciphers to filter + * @returns Promise resolving to filtered array with restricted ciphers removed + */ + async filterRestrictedCiphers(ciphers: CipherView[]): Promise { + const restrictions = await this.getRestrictedTypes(); + + return ciphers.filter( + (cipher) => !this.restrictedItemTypesService.isCipherRestricted(cipher, restrictions), + ); + } + + /** + * Checks if a specific cipher type is restricted for the user. + * + * @param cipherType - The cipher type to check + * @returns Promise resolving to true if the cipher type is restricted, false otherwise + */ + async isCipherRestricted(cipher: Cipher | CipherView): Promise { + return firstValueFrom(this.restrictedItemTypesService.isCipherRestricted$(cipher)); + } +} diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts index 2ec2b2c40a9..3082d7cb809 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts @@ -3,7 +3,6 @@ import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { RestrictedCipherType } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { createFilterFunction } from "./filter-function"; import { All } from "./routed-vault-filter.model"; @@ -215,46 +214,6 @@ describe("createFilter", () => { expect(result).toBe(true); }); }); - - describe("given restricted types", () => { - const restrictedTypes: RestrictedCipherType[] = [ - { cipherType: CipherType.Login, allowViewOrgIds: [] }, - ]; - - it("should filter out a cipher whose type is fully restricted", () => { - const cipher = createCipher({ type: CipherType.Login }); - const filterFunction = createFilterFunction({}, restrictedTypes); - - expect(filterFunction(cipher)).toBe(false); - }); - - it("should allow a cipher when the cipher's organization allows it", () => { - const cipher = createCipher({ type: CipherType.Login, organizationId: "org1" }); - const restricted: RestrictedCipherType[] = [ - { cipherType: CipherType.Login, allowViewOrgIds: ["org1"] }, - ]; - const filterFunction2 = createFilterFunction({}, restricted); - - expect(filterFunction2(cipher)).toBe(true); - }); - - it("should filter out a personal vault cipher when the owning orgs does not allow it", () => { - const cipher = createCipher({ type: CipherType.Card, organizationId: "org1" }); - const restricted2: RestrictedCipherType[] = [ - { cipherType: CipherType.Card, allowViewOrgIds: [] }, - ]; - const filterFunction3 = createFilterFunction({}, restricted2); - - expect(filterFunction3(cipher)).toBe(false); - }); - - it("should not filter a cipher if there are no restricted types", () => { - const cipher = createCipher({ type: CipherType.Login }); - const filterFunction = createFilterFunction({}, []); - - expect(filterFunction(cipher)).toBe(true); - }); - }); }); function createCipher(options: Partial = {}) { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts index 93071aecae3..a39918df4a7 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts @@ -1,19 +1,12 @@ import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { - isCipherViewRestricted, - RestrictedCipherType, -} from "@bitwarden/common/vault/services/restricted-item-types.service"; import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model"; export type FilterFunction = (cipher: CipherView) => boolean; -export function createFilterFunction( - filter: RoutedVaultFilterModel, - restrictedTypes?: RestrictedCipherType[], -): FilterFunction { +export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunction { return (cipher) => { if (filter.type === "favorites" && !cipher.favorite) { return false; @@ -86,10 +79,7 @@ export function createFilterFunction( ) { return false; } - // Restricted types - if (restrictedTypes && isCipherViewRestricted(cipher, restrictedTypes)) { - return false; - } + return true; }; } diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 3d59a186705..29ad0ead621 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -354,17 +354,26 @@ export class VaultComponent implements OnInit, OnDestroy { this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search)); - const ciphers$ = combineLatest([ + /** + * This observable filters the ciphers based on the active user ID and the restricted item types. + */ + const allowedCiphers$ = combineLatest([ this.cipherService.cipherViews$(activeUserId).pipe(filter((c) => c !== null)), - filter$, - this.currentSearchText$, this.restrictedItemTypesService.restricted$, ]).pipe( + map(([ciphers, restrictedTypes]) => + ciphers.filter( + (cipher) => !this.restrictedItemTypesService.isCipherRestricted(cipher, restrictedTypes), + ), + ), + ); + + const ciphers$ = combineLatest([allowedCiphers$, filter$, this.currentSearchText$]).pipe( filter(([ciphers, filter]) => ciphers != undefined && filter != undefined), - concatMap(async ([ciphers, filter, searchText, restrictedTypes]) => { + concatMap(async ([ciphers, filter, searchText]) => { const failedCiphers = (await firstValueFrom(this.cipherService.failedToDecryptCiphers$(activeUserId))) ?? []; - const filterFunction = createFilterFunction(filter, restrictedTypes); + const filterFunction = createFilterFunction(filter); // Append any failed to decrypt ciphers to the top of the cipher list const allCiphers = [...failedCiphers, ...ciphers]; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index c1c4844a61d..559761bd1bf 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -891,6 +891,7 @@ const safeProviders: SafeProvider[] = [ KdfConfigService, AccountServiceAbstraction, ApiServiceAbstraction, + RestrictedItemTypesService, ], }), safeProvider({ @@ -906,6 +907,7 @@ const safeProviders: SafeProvider[] = [ CollectionService, KdfConfigService, AccountServiceAbstraction, + RestrictedItemTypesService, ], }), safeProvider({ diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 0679d141bbd..424243fe118 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -22,10 +22,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { - isCipherViewRestricted, - RestrictedItemTypesService, -} from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; @Directive() @@ -174,7 +171,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy { allCiphers = [..._failedCiphers, ...allCiphers]; const restrictedTypeFilter = (cipher: CipherView) => - !isCipherViewRestricted(cipher, restricted); + !this.restrictedItemTypesService.isCipherRestricted(cipher, restricted); return this.searchService.searchCiphers( userId, diff --git a/libs/common/src/vault/services/restricted-item-types.service.ts b/libs/common/src/vault/services/restricted-item-types.service.ts index 63c9577bc09..7ec70831b22 100644 --- a/libs/common/src/vault/services/restricted-item-types.service.ts +++ b/libs/common/src/vault/services/restricted-item-types.service.ts @@ -11,11 +11,15 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { Cipher } from "../models/domain/cipher"; + export type RestrictedCipherType = { cipherType: CipherType; allowViewOrgIds: string[]; }; +type CipherLike = Cipher | CipherView; + export class RestrictedItemTypesService { /** * Emits an array of RestrictedCipherType objects: @@ -76,26 +80,47 @@ export class RestrictedItemTypesService { private organizationService: OrganizationService, private policyService: PolicyService, ) {} -} -/** - * Filter that returns whether a cipher is restricted from being viewed by the user - * Criteria: - * - the cipher's type is restricted by at least one org - * UNLESS - * - the cipher belongs to an organization and that organization does not restrict that type - * OR - * - the cipher belongs to the user's personal vault and at least one other organization does not restrict that type - */ -export function isCipherViewRestricted( - cipher: CipherView, - restrictedTypes: RestrictedCipherType[], -) { - return restrictedTypes.some( - (restrictedType) => - restrictedType.cipherType === cipher.type && - (cipher.organizationId - ? !restrictedType.allowViewOrgIds.includes(cipher.organizationId) - : restrictedType.allowViewOrgIds.length === 0), - ); + /** + * Determines if a cipher is restricted from being viewed by the user. + * + * @param cipher - The cipher to check + * @param restrictedTypes - Array of restricted cipher types (from restricted$ observable) + * @returns true if the cipher is restricted, false otherwise + * + * Restriction logic: + * - If cipher type is not restricted by any org → allowed + * - If cipher belongs to an org that allows this type → allowed + * - If cipher is personal vault and any org allows this type → allowed + * - Otherwise → restricted + */ + isCipherRestricted(cipher: CipherLike, restrictedTypes: RestrictedCipherType[]): boolean { + const restriction = restrictedTypes.find((r) => r.cipherType === cipher.type); + + // If cipher type is not restricted by any organization, allow it + if (!restriction) { + return false; + } + + // If cipher belongs to an organization + if (cipher.organizationId) { + // Check if this organization allows viewing this cipher type + return !restriction.allowViewOrgIds.includes(cipher.organizationId); + } + + // For personal vault ciphers: restricted only if NO organizations allow this type + return restriction.allowViewOrgIds.length === 0; + } + + /** + * Convenience method that combines getting restrictions and checking a cipher. + * + * @param cipher - The cipher to check + * @returns Observable indicating if the cipher is restricted + */ + isCipherRestricted$(cipher: CipherLike): Observable { + return this.restricted$.pipe( + map((restrictedTypes) => this.isCipherRestricted(cipher, restrictedTypes)), + ); + } } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index 6ed4caa3f8d..d539c85c94c 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -25,6 +25,10 @@ import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.v import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { + RestrictedCipherType, + RestrictedItemTypesService, +} from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DEFAULT_KDF_CONFIG, PBKDF2KdfConfig, @@ -170,6 +174,8 @@ describe("VaultExportService", () => { let kdfConfigService: MockProxy; let accountService: MockProxy; let apiService: MockProxy; + let restrictedSubject: BehaviorSubject; + let restrictedItemTypesService: Partial; let fetchMock: jest.Mock; const userId = "" as UserId; @@ -186,6 +192,12 @@ describe("VaultExportService", () => { apiService = mock(); keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); + restrictedSubject = new BehaviorSubject([]); + restrictedItemTypesService = { + restricted$: new BehaviorSubject([]), + isCipherRestricted: jest.fn().mockReturnValue(false), + isCipherRestricted$: jest.fn().mockReturnValue(of(false)), + }; const accountInfo: AccountInfo = { email: "", @@ -223,6 +235,7 @@ describe("VaultExportService", () => { kdfConfigService, accountService, apiService, + restrictedItemTypesService as RestrictedItemTypesService, ); }); @@ -262,6 +275,46 @@ describe("VaultExportService", () => { expectEqualCiphers(UserCipherDomains.slice(0, 2), exportedData.data); }); + it("does not unencrypted export restricted user items", async () => { + restrictedSubject.next([{ cipherType: CipherType.Card, allowViewOrgIds: [] }]); + const cardCipher = generateCipherView(false); + cardCipher.type = CipherType.Card; + + (restrictedItemTypesService.isCipherRestricted as jest.Mock) + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) // cardCipher - restricted + .mockReturnValueOnce(false); + + const testCiphers = [UserCipherViews[0], cardCipher, UserCipherViews[1]]; + cipherService.getAllDecrypted.mockResolvedValue(testCiphers); + + const actual = await exportService.getExport("json"); + expect(typeof actual.data).toBe("string"); + const exportedData = actual as ExportedVaultAsString; + + expectEqualCiphers([UserCipherViews[0], UserCipherViews[1]], exportedData.data); + }); + + it("does not encrypted export restricted user items", async () => { + restrictedSubject.next([{ cipherType: CipherType.Card, allowViewOrgIds: [] }]); + const cardCipher = generateCipherDomain(false); + cardCipher.type = CipherType.Card; + + (restrictedItemTypesService.isCipherRestricted as jest.Mock) + .mockReturnValueOnce(false) + .mockReturnValueOnce(true) // cardCipher - restricted + .mockReturnValueOnce(false); + + const testCiphers = [UserCipherDomains[0], cardCipher, UserCipherDomains[1]]; + cipherService.getAll.mockResolvedValue(testCiphers); + + const actual = await exportService.getExport("encrypted_json"); + expect(typeof actual.data).toBe("string"); + const exportedData = actual as ExportedVaultAsString; + + expectEqualCiphers([UserCipherDomains[0], UserCipherDomains[1]], exportedData.data); + }); + describe("zip export", () => { it("contains data.json", async () => { cipherService.getAllDecrypted.mockResolvedValue([]); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 537585aac7e..214b2d832a4 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -20,6 +20,7 @@ import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Folder } from "@bitwarden/common/vault/models/domain/folder"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { @@ -50,6 +51,7 @@ export class IndividualVaultExportService kdfConfigService: KdfConfigService, private accountService: AccountService, private apiService: ApiService, + private restrictedItemTypesService: RestrictedItemTypesService, ) { super(pinService, encryptService, cryptoFunctionService, kdfConfigService); } @@ -169,9 +171,15 @@ export class IndividualVaultExportService }), ); + const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$); + promises.push( this.cipherService.getAllDecrypted(activeUserId).then((ciphers) => { - decCiphers = ciphers.filter((f) => f.deletedDate == null); + decCiphers = ciphers.filter( + (f) => + f.deletedDate == null && + !this.restrictedItemTypesService.isCipherRestricted(f, restrictions), + ); }), ); @@ -203,9 +211,15 @@ export class IndividualVaultExportService }), ); + const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$); + promises.push( this.cipherService.getAll(activeUserId).then((c) => { - ciphers = c.filter((f) => f.deletedDate == null); + ciphers = c.filter( + (f) => + f.deletedDate == null && + !this.restrictedItemTypesService.isCipherRestricted(f, restrictions), + ); }), ); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index 4f30f299062..61fbcd261f4 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -24,6 +24,7 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { KdfConfigService, KeyService } from "@bitwarden/key-management"; import { @@ -52,6 +53,7 @@ export class OrganizationVaultExportService private collectionService: CollectionService, kdfConfigService: KdfConfigService, private accountService: AccountService, + private restrictedItemTypesService: RestrictedItemTypesService, ) { super(pinService, encryptService, cryptoFunctionService, kdfConfigService); } @@ -133,6 +135,8 @@ export class OrganizationVaultExportService const decCiphers: CipherView[] = []; const promises = []; + const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$); + promises.push( this.apiService.getOrganizationExport(organizationId).then((exportData) => { const exportPromises: any = []; @@ -156,7 +160,11 @@ export class OrganizationVaultExportService const cipher = new Cipher(new CipherData(c)); exportPromises.push( this.cipherService.decrypt(cipher, activeUserId).then((decCipher) => { - decCiphers.push(decCipher); + if ( + !this.restrictedItemTypesService.isCipherRestricted(decCipher, restrictions) + ) { + decCiphers.push(decCipher); + } }), ); }); @@ -176,7 +184,7 @@ export class OrganizationVaultExportService private async getOrganizationEncryptedExport(organizationId: string): Promise { const collections: Collection[] = []; - const ciphers: Cipher[] = []; + let ciphers: Cipher[] = []; const promises = []; promises.push( @@ -190,15 +198,17 @@ export class OrganizationVaultExportService }), ); + const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$); + promises.push( this.apiService.getCiphersOrganization(organizationId).then((c) => { if (c != null && c.data != null && c.data.length > 0) { - c.data + ciphers = c.data .filter((item) => item.deletedDate === null) - .forEach((item) => { - const cipher = new Cipher(new CipherData(item)); - ciphers.push(cipher); - }); + .map((item) => new Cipher(new CipherData(item))) + .filter( + (cipher) => !this.restrictedItemTypesService.isCipherRestricted(cipher, restrictions), + ); } }), ); @@ -231,11 +241,14 @@ export class OrganizationVaultExportService ); await Promise.all(promises); + const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$); + decCiphers = allDecCiphers.filter( (f) => f.deletedDate == null && f.organizationId == organizationId && - decCollections.some((dC) => f.collectionIds.some((cId) => dC.id === cId)), + decCollections.some((dC) => f.collectionIds.some((cId) => dC.id === cId)) && + !this.restrictedItemTypesService.isCipherRestricted(f, restrictions), ); if (format === "csv") { @@ -267,11 +280,14 @@ export class OrganizationVaultExportService await Promise.all(promises); + const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$); + encCiphers = allCiphers.filter( (f) => f.deletedDate == null && f.organizationId == organizationId && - encCollections.some((eC) => f.collectionIds.some((cId) => eC.id === cId)), + encCollections.some((eC) => f.collectionIds.some((cId) => eC.id === cId)) && + !this.restrictedItemTypesService.isCipherRestricted(f, restrictions), ); return this.BuildEncryptedExport(organizationId, encCollections, encCiphers); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts index 4e0dbfcc330..931cb6af740 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts @@ -19,6 +19,10 @@ import { Login } from "@bitwarden/common/vault/models/domain/login"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { + RestrictedCipherType, + RestrictedItemTypesService, +} from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DEFAULT_KDF_CONFIG, PBKDF2KdfConfig, @@ -159,6 +163,7 @@ describe("VaultExportService", () => { let accountService: MockProxy; let kdfConfigService: MockProxy; let apiService: MockProxy; + let restrictedItemTypesService: Partial; beforeEach(() => { cryptoFunctionService = mock(); @@ -186,6 +191,12 @@ describe("VaultExportService", () => { const activeAccount = { id: userId, ...accountInfo }; accountService.activeAccount$ = new BehaviorSubject(activeAccount); + restrictedItemTypesService = { + restricted$: new BehaviorSubject([]), + isCipherRestricted: jest.fn().mockReturnValue(false), + isCipherRestricted$: jest.fn().mockReturnValue(of(false)), + }; + exportService = new IndividualVaultExportService( folderService, cipherService, @@ -196,6 +207,7 @@ describe("VaultExportService", () => { kdfConfigService, accountService, apiService, + restrictedItemTypesService as RestrictedItemTypesService, ); });