1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +00:00

[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
This commit is contained in:
SmithThe4th
2025-06-23 12:04:56 -04:00
committed by GitHub
parent 2e8c0de719
commit e291e2df0a
24 changed files with 444 additions and 113 deletions

View File

@@ -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 { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.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 { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service";
import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks"; import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks";
@@ -411,6 +412,7 @@ export default class MainBackground {
inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
taskService: TaskService; taskService: TaskService;
cipherEncryptionService: CipherEncryptionService; cipherEncryptionService: CipherEncryptionService;
restrictedItemTypesService: RestrictedItemTypesService;
ipcContentScriptManagerService: IpcContentScriptManagerService; ipcContentScriptManagerService: IpcContentScriptManagerService;
ipcService: IpcService; ipcService: IpcService;
@@ -1043,6 +1045,13 @@ export default class MainBackground {
this.sdkService, this.sdkService,
); );
this.restrictedItemTypesService = new RestrictedItemTypesService(
this.configService,
this.accountService,
this.organizationService,
this.policyService,
);
this.individualVaultExportService = new IndividualVaultExportService( this.individualVaultExportService = new IndividualVaultExportService(
this.folderService, this.folderService,
this.cipherService, this.cipherService,
@@ -1053,6 +1062,7 @@ export default class MainBackground {
this.kdfConfigService, this.kdfConfigService,
this.accountService, this.accountService,
this.apiService, this.apiService,
this.restrictedItemTypesService,
); );
this.organizationVaultExportService = new OrganizationVaultExportService( this.organizationVaultExportService = new OrganizationVaultExportService(
@@ -1065,6 +1075,7 @@ export default class MainBackground {
this.collectionService, this.collectionService,
this.kdfConfigService, this.kdfConfigService,
this.accountService, this.accountService,
this.restrictedItemTypesService,
); );
this.exportService = new VaultExportService( this.exportService = new VaultExportService(

View File

@@ -76,6 +76,7 @@ describe("VaultPopupListFiltersService", () => {
const restrictedItemTypesService = { const restrictedItemTypesService = {
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]), restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
isCipherRestricted: jest.fn().mockReturnValue(false),
}; };
beforeEach(() => { beforeEach(() => {
@@ -729,6 +730,7 @@ function createSeededVaultPopupListFiltersService(
const accountServiceMock = mockAccountServiceWith("userId" as UserId); const accountServiceMock = mockAccountServiceWith("userId" as UserId);
const restrictedItemTypesServiceMock = { const restrictedItemTypesServiceMock = {
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]), restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
isCipherRestricted: jest.fn().mockReturnValue(false),
} as any; } as any;
const formBuilderInstance = new FormBuilder(); const formBuilderInstance = new FormBuilder();

View File

@@ -39,10 +39,7 @@ import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
isCipherViewRestricted,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
import { ChipSelectOption } from "@bitwarden/components"; import { ChipSelectOption } from "@bitwarden/components";
@@ -230,7 +227,7 @@ export class VaultPopupListFiltersService {
} }
// Check if cipher type is restricted (with organization exemptions) // Check if cipher type is restricted (with organization exemptions)
if (isCipherViewRestricted(cipher, restrictions)) { if (this.restrictedItemTypesService.isCipherRestricted(cipher, restrictions)) {
return false; return false;
} }

View File

@@ -24,6 +24,7 @@ import { Response } from "../models/response";
import { CliUtils } from "../utils"; import { CliUtils } from "../utils";
import { CipherResponse } from "../vault/models/cipher.response"; import { CipherResponse } from "../vault/models/cipher.response";
import { FolderResponse } from "../vault/models/folder.response"; import { FolderResponse } from "../vault/models/folder.response";
import { CliRestrictedItemTypesService } from "../vault/services/cli-restricted-item-types.service";
export class EditCommand { export class EditCommand {
constructor( constructor(
@@ -34,6 +35,7 @@ export class EditCommand {
private apiService: ApiService, private apiService: ApiService,
private folderApiService: FolderApiServiceAbstraction, private folderApiService: FolderApiServiceAbstraction,
private accountService: AccountService, private accountService: AccountService,
private cliRestrictedItemTypesService: CliRestrictedItemTypesService,
) {} ) {}
async run( async run(
@@ -95,6 +97,13 @@ export class EditCommand {
return Response.badRequest("You may not edit a deleted item. Use the restore command first."); return Response.badRequest("You may not edit a deleted item. Use the restore command first.");
} }
cipherView = CipherExport.toView(req, cipherView); 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); const encCipher = await this.cipherService.encrypt(cipherView, activeUserId);
try { try {
const updatedCipher = await this.cipherService.updateWithServer(encCipher); const updatedCipher = await this.cipherService.updateWithServer(encCipher);

View File

@@ -27,7 +27,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response"
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; 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 { CliUtils } from "../utils";
import { CipherResponse } from "../vault/models/cipher.response"; import { CipherResponse } from "../vault/models/cipher.response";
import { FolderResponse } from "../vault/models/folder.response"; import { FolderResponse } from "../vault/models/folder.response";
import { CliRestrictedItemTypesService } from "../vault/services/cli-restricted-item-types.service";
import { DownloadCommand } from "./download.command"; import { DownloadCommand } from "./download.command";
@@ -66,6 +67,7 @@ export class GetCommand extends DownloadCommand {
private eventCollectionService: EventCollectionService, private eventCollectionService: EventCollectionService,
private accountProfileService: BillingAccountProfileStateService, private accountProfileService: BillingAccountProfileStateService,
private accountService: AccountService, private accountService: AccountService,
private cliRestrictedItemTypesService: CliRestrictedItemTypesService,
) { ) {
super(encryptService, apiService); super(encryptService, apiService);
} }
@@ -110,16 +112,16 @@ export class GetCommand extends DownloadCommand {
} }
} }
private async getCipherView(id: string): Promise<CipherView | CipherView[]> { private async getCipherView(id: string, userId: UserId): Promise<CipherView | CipherView[]> {
let decCipher: CipherView = null; let decCipher: CipherView = null;
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (Utils.isGuid(id)) { if (Utils.isGuid(id)) {
const cipher = await this.cipherService.get(id, activeUserId); const cipher = await this.cipherService.get(id, userId);
if (cipher != null) { if (cipher != null) {
decCipher = await this.cipherService.decrypt(cipher, activeUserId); decCipher = await this.cipherService.decrypt(cipher, userId);
} }
} else if (id.trim() !== "") { } else if (id.trim() !== "") {
let ciphers = await this.cipherService.getAllDecrypted(activeUserId); let ciphers = await this.cipherService.getAllDecrypted(userId);
ciphers = this.searchService.searchCiphersBasic(ciphers, id); ciphers = this.searchService.searchCiphersBasic(ciphers, id);
if (ciphers.length > 1) { if (ciphers.length > 1) {
return ciphers; return ciphers;
@@ -133,20 +135,45 @@ export class GetCommand extends DownloadCommand {
} }
private async getCipher(id: string, filter?: (c: CipherView) => boolean) { 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) { if (decCipher == null) {
return Response.notFound(); return Response.notFound();
} }
if (Array.isArray(decCipher)) { 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) { if (filter != null) {
decCipher = decCipher.filter(filter); decCipher = decCipher.filter(filter);
}
if (decCipher.length === 0) {
return Response.notFound();
}
if (decCipher.length === 1) { if (decCipher.length === 1) {
decCipher = decCipher[0]; decCipher = decCipher[0];
} } else {
}
if (Array.isArray(decCipher)) {
return Response.multipleResults(decCipher.map((c) => c.id)); 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( await this.eventCollectionService.collect(
@@ -317,7 +344,8 @@ export class GetCommand extends DownloadCommand {
return cipherResponse; 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 ( if (
cipher == null || cipher == null ||
Array.isArray(cipher) || Array.isArray(cipher) ||
@@ -345,7 +373,6 @@ export class GetCommand extends DownloadCommand {
return Response.multipleResults(attachments.map((a) => a.id)); return Response.multipleResults(attachments.map((a) => a.id));
} }
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const canAccessPremium = await firstValueFrom( const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$(activeUserId), this.accountProfileService.hasPremiumFromAnySource$(activeUserId),
); );

View File

@@ -29,6 +29,7 @@ import { ListResponse } from "../models/response/list.response";
import { CliUtils } from "../utils"; import { CliUtils } from "../utils";
import { CipherResponse } from "../vault/models/cipher.response"; import { CipherResponse } from "../vault/models/cipher.response";
import { FolderResponse } from "../vault/models/folder.response"; import { FolderResponse } from "../vault/models/folder.response";
import { CliRestrictedItemTypesService } from "../vault/services/cli-restricted-item-types.service";
export class ListCommand { export class ListCommand {
constructor( constructor(
@@ -41,6 +42,7 @@ export class ListCommand {
private apiService: ApiService, private apiService: ApiService,
private eventCollectionService: EventCollectionService, private eventCollectionService: EventCollectionService,
private accountService: AccountService, private accountService: AccountService,
private cliRestrictedItemTypesService: CliRestrictedItemTypesService,
) {} ) {}
async run(object: string, cmdOptions: Record<string, any>): Promise<Response> { async run(object: string, cmdOptions: Record<string, any>): Promise<Response> {
@@ -134,6 +136,8 @@ export class ListCommand {
ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash); ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash);
} }
ciphers = await this.cliRestrictedItemTypesService.filterRestrictedCiphers(ciphers);
await this.eventCollectionService.collectMany(EventType.Cipher_ClientViewed, ciphers, true); await this.eventCollectionService.collectMany(EventType.Cipher_ClientViewed, ciphers, true);
const res = new ListResponse(ciphers.map((o) => new CipherResponse(o))); const res = new ListResponse(ciphers.map((o) => new CipherResponse(o)));

View File

@@ -66,6 +66,7 @@ export class OssServeConfigurator {
this.serviceContainer.eventCollectionService, this.serviceContainer.eventCollectionService,
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService,
); );
this.listCommand = new ListCommand( this.listCommand = new ListCommand(
this.serviceContainer.cipherService, this.serviceContainer.cipherService,
@@ -77,6 +78,7 @@ export class OssServeConfigurator {
this.serviceContainer.apiService, this.serviceContainer.apiService,
this.serviceContainer.eventCollectionService, this.serviceContainer.eventCollectionService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService,
); );
this.createCommand = new CreateCommand( this.createCommand = new CreateCommand(
this.serviceContainer.cipherService, this.serviceContainer.cipherService,
@@ -88,6 +90,7 @@ export class OssServeConfigurator {
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.organizationService, this.serviceContainer.organizationService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService,
); );
this.editCommand = new EditCommand( this.editCommand = new EditCommand(
this.serviceContainer.cipherService, this.serviceContainer.cipherService,
@@ -97,6 +100,7 @@ export class OssServeConfigurator {
this.serviceContainer.apiService, this.serviceContainer.apiService,
this.serviceContainer.folderApiService, this.serviceContainer.folderApiService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService,
); );
this.generateCommand = new GenerateCommand( this.generateCommand = new GenerateCommand(
this.serviceContainer.passwordGenerationService, this.serviceContainer.passwordGenerationService,
@@ -117,6 +121,7 @@ export class OssServeConfigurator {
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.cipherAuthorizationService, this.serviceContainer.cipherAuthorizationService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService,
); );
this.confirmCommand = new ConfirmCommand( this.confirmCommand = new ConfirmCommand(
this.serviceContainer.apiService, this.serviceContainer.apiService,

View File

@@ -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 { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.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 { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { import {
legacyPasswordGenerationServiceFactory, legacyPasswordGenerationServiceFactory,
@@ -187,6 +188,7 @@ import { I18nService } from "../platform/services/i18n.service";
import { LowdbStorageService } from "../platform/services/lowdb-storage.service"; import { LowdbStorageService } from "../platform/services/lowdb-storage.service";
import { NodeApiService } from "../platform/services/node-api.service"; import { NodeApiService } from "../platform/services/node-api.service";
import { NodeEnvSecureStorageService } from "../platform/services/node-env-secure-storage.service"; import { NodeEnvSecureStorageService } from "../platform/services/node-env-secure-storage.service";
import { CliRestrictedItemTypesService } from "../vault/services/cli-restricted-item-types.service";
// Polyfills // Polyfills
global.DOMParser = new jsdom.JSDOM().window.DOMParser; global.DOMParser = new jsdom.JSDOM().window.DOMParser;
@@ -287,6 +289,8 @@ export class ServiceContainer {
masterPasswordApiService: MasterPasswordApiServiceAbstraction; masterPasswordApiService: MasterPasswordApiServiceAbstraction;
bulkEncryptService: FallbackBulkEncryptService; bulkEncryptService: FallbackBulkEncryptService;
cipherEncryptionService: CipherEncryptionService; cipherEncryptionService: CipherEncryptionService;
restrictedItemTypesService: RestrictedItemTypesService;
cliRestrictedItemTypesService: CliRestrictedItemTypesService;
constructor() { constructor() {
let p = null; let p = null;
@@ -811,6 +815,7 @@ export class ServiceContainer {
this.kdfConfigService, this.kdfConfigService,
this.accountService, this.accountService,
this.apiService, this.apiService,
this.restrictedItemTypesService,
); );
this.organizationExportService = new OrganizationVaultExportService( this.organizationExportService = new OrganizationVaultExportService(
@@ -823,6 +828,7 @@ export class ServiceContainer {
this.collectionService, this.collectionService,
this.kdfConfigService, this.kdfConfigService,
this.accountService, this.accountService,
this.restrictedItemTypesService,
); );
this.exportService = new VaultExportService( this.exportService = new VaultExportService(
@@ -864,6 +870,17 @@ export class ServiceContainer {
); );
this.masterPasswordApiService = new MasterPasswordApiService(this.apiService, this.logService); 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() { async logout() {

View File

@@ -153,6 +153,7 @@ export class SendProgram extends BaseProgram {
this.serviceContainer.eventCollectionService, this.serviceContainer.eventCollectionService,
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService,
); );
const response = await cmd.run("template", object, null); const response = await cmd.run("template", object, null);
this.processResponse(response); this.processResponse(response);

View File

@@ -114,6 +114,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.apiService, this.serviceContainer.apiService,
this.serviceContainer.eventCollectionService, this.serviceContainer.eventCollectionService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService,
); );
const response = await command.run(object, cmd); const response = await command.run(object, cmd);
@@ -188,6 +189,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.eventCollectionService, this.serviceContainer.eventCollectionService,
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService,
); );
const response = await command.run(object, id, cmd); const response = await command.run(object, id, cmd);
this.processResponse(response); this.processResponse(response);
@@ -233,6 +235,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.organizationService, this.serviceContainer.organizationService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService,
); );
const response = await command.run(object, encodedJson, cmd); const response = await command.run(object, encodedJson, cmd);
this.processResponse(response); this.processResponse(response);
@@ -280,6 +283,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.apiService, this.serviceContainer.apiService,
this.serviceContainer.folderApiService, this.serviceContainer.folderApiService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService,
); );
const response = await command.run(object, id, encodedJson, cmd); const response = await command.run(object, id, encodedJson, cmd);
this.processResponse(response); this.processResponse(response);
@@ -323,6 +327,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.cipherAuthorizationService, this.serviceContainer.cipherAuthorizationService,
this.serviceContainer.accountService, this.serviceContainer.accountService,
this.serviceContainer.cliRestrictedItemTypesService,
); );
const response = await command.run(object, id, cmd); const response = await command.run(object, id, cmd);
this.processResponse(response); this.processResponse(response);

View File

@@ -29,6 +29,7 @@ import { CliUtils } from "../utils";
import { CipherResponse } from "./models/cipher.response"; import { CipherResponse } from "./models/cipher.response";
import { FolderResponse } from "./models/folder.response"; import { FolderResponse } from "./models/folder.response";
import { CliRestrictedItemTypesService } from "./services/cli-restricted-item-types.service";
export class CreateCommand { export class CreateCommand {
constructor( constructor(
@@ -41,6 +42,7 @@ export class CreateCommand {
private accountProfileService: BillingAccountProfileStateService, private accountProfileService: BillingAccountProfileStateService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private accountService: AccountService, private accountService: AccountService,
private cliRestrictedItemTypesService: CliRestrictedItemTypesService,
) {} ) {}
async run( async run(
@@ -90,6 +92,15 @@ export class CreateCommand {
private async createCipher(req: CipherExport) { private async createCipher(req: CipherExport) {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); 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 cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId);
try { try {
const newCipher = await this.cipherService.createWithServer(cipher); const newCipher = await this.cipherService.createWithServer(cipher);

View File

@@ -13,6 +13,8 @@ import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cip
import { Response } from "../models/response"; import { Response } from "../models/response";
import { CliUtils } from "../utils"; import { CliUtils } from "../utils";
import { CliRestrictedItemTypesService } from "./services/cli-restricted-item-types.service";
export class DeleteCommand { export class DeleteCommand {
constructor( constructor(
private cipherService: CipherService, private cipherService: CipherService,
@@ -22,6 +24,7 @@ export class DeleteCommand {
private accountProfileService: BillingAccountProfileStateService, private accountProfileService: BillingAccountProfileStateService,
private cipherAuthorizationService: CipherAuthorizationService, private cipherAuthorizationService: CipherAuthorizationService,
private accountService: AccountService, private accountService: AccountService,
private cliRestrictedItemTypesService: CliRestrictedItemTypesService,
) {} ) {}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> { async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
@@ -60,6 +63,12 @@ export class DeleteCommand {
return Response.error("You do not have permission to delete this item."); 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 { try {
if (options.permanent) { if (options.permanent) {
await this.cipherService.deleteWithServer(id, activeUserId); await this.cipherService.deleteWithServer(id, activeUserId);

View File

@@ -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<RestrictedCipherType[]>;
let restrictedItemTypesService: Partial<RestrictedItemTypesService>;
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<RestrictedCipherType[]>([]);
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);
});
});
});

View File

@@ -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<RestrictedCipherType[]> {
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<CipherView[]> {
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<boolean> {
return firstValueFrom(this.restrictedItemTypesService.isCipherRestricted$(cipher));
}
}

View File

@@ -3,7 +3,6 @@
import { Unassigned } from "@bitwarden/admin-console/common"; import { Unassigned } from "@bitwarden/admin-console/common";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; 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 { createFilterFunction } from "./filter-function";
import { All } from "./routed-vault-filter.model"; import { All } from "./routed-vault-filter.model";
@@ -215,46 +214,6 @@ describe("createFilter", () => {
expect(result).toBe(true); 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<CipherView> = {}) { function createCipher(options: Partial<CipherView> = {}) {

View File

@@ -1,19 +1,12 @@
import { Unassigned } from "@bitwarden/admin-console/common"; import { Unassigned } from "@bitwarden/admin-console/common";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; 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"; import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
export type FilterFunction = (cipher: CipherView) => boolean; export type FilterFunction = (cipher: CipherView) => boolean;
export function createFilterFunction( export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunction {
filter: RoutedVaultFilterModel,
restrictedTypes?: RestrictedCipherType[],
): FilterFunction {
return (cipher) => { return (cipher) => {
if (filter.type === "favorites" && !cipher.favorite) { if (filter.type === "favorites" && !cipher.favorite) {
return false; return false;
@@ -86,10 +79,7 @@ export function createFilterFunction(
) { ) {
return false; return false;
} }
// Restricted types
if (restrictedTypes && isCipherViewRestricted(cipher, restrictedTypes)) {
return false;
}
return true; return true;
}; };
} }

View File

@@ -354,17 +354,26 @@ export class VaultComponent implements OnInit, OnDestroy {
this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search)); 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)), this.cipherService.cipherViews$(activeUserId).pipe(filter((c) => c !== null)),
filter$,
this.currentSearchText$,
this.restrictedItemTypesService.restricted$, this.restrictedItemTypesService.restricted$,
]).pipe( ]).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), filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
concatMap(async ([ciphers, filter, searchText, restrictedTypes]) => { concatMap(async ([ciphers, filter, searchText]) => {
const failedCiphers = const failedCiphers =
(await firstValueFrom(this.cipherService.failedToDecryptCiphers$(activeUserId))) ?? []; (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 // Append any failed to decrypt ciphers to the top of the cipher list
const allCiphers = [...failedCiphers, ...ciphers]; const allCiphers = [...failedCiphers, ...ciphers];

View File

@@ -891,6 +891,7 @@ const safeProviders: SafeProvider[] = [
KdfConfigService, KdfConfigService,
AccountServiceAbstraction, AccountServiceAbstraction,
ApiServiceAbstraction, ApiServiceAbstraction,
RestrictedItemTypesService,
], ],
}), }),
safeProvider({ safeProvider({
@@ -906,6 +907,7 @@ const safeProviders: SafeProvider[] = [
CollectionService, CollectionService,
KdfConfigService, KdfConfigService,
AccountServiceAbstraction, AccountServiceAbstraction,
RestrictedItemTypesService,
], ],
}), }),
safeProvider({ safeProvider({

View File

@@ -22,10 +22,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
isCipherViewRestricted,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
@Directive() @Directive()
@@ -174,7 +171,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
allCiphers = [..._failedCiphers, ...allCiphers]; allCiphers = [..._failedCiphers, ...allCiphers];
const restrictedTypeFilter = (cipher: CipherView) => const restrictedTypeFilter = (cipher: CipherView) =>
!isCipherViewRestricted(cipher, restricted); !this.restrictedItemTypesService.isCipherRestricted(cipher, restricted);
return this.searchService.searchCiphers( return this.searchService.searchCiphers(
userId, userId,

View File

@@ -11,11 +11,15 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { Cipher } from "../models/domain/cipher";
export type RestrictedCipherType = { export type RestrictedCipherType = {
cipherType: CipherType; cipherType: CipherType;
allowViewOrgIds: string[]; allowViewOrgIds: string[];
}; };
type CipherLike = Cipher | CipherView;
export class RestrictedItemTypesService { export class RestrictedItemTypesService {
/** /**
* Emits an array of RestrictedCipherType objects: * Emits an array of RestrictedCipherType objects:
@@ -76,26 +80,47 @@ export class RestrictedItemTypesService {
private organizationService: OrganizationService, private organizationService: OrganizationService,
private policyService: PolicyService, private policyService: PolicyService,
) {} ) {}
/**
* 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;
} }
/** /**
* Filter that returns whether a cipher is restricted from being viewed by the user * Convenience method that combines getting restrictions and checking a cipher.
* Criteria: *
* - the cipher's type is restricted by at least one org * @param cipher - The cipher to check
* UNLESS * @returns Observable<boolean> indicating if the cipher is restricted
* - 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( isCipherRestricted$(cipher: CipherLike): Observable<boolean> {
cipher: CipherView, return this.restricted$.pipe(
restrictedTypes: RestrictedCipherType[], map((restrictedTypes) => this.isCipherRestricted(cipher, restrictedTypes)),
) {
return restrictedTypes.some(
(restrictedType) =>
restrictedType.cipherType === cipher.type &&
(cipher.organizationId
? !restrictedType.allowViewOrgIds.includes(cipher.organizationId)
: restrictedType.allowViewOrgIds.length === 0),
); );
} }
}

View File

@@ -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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import {
RestrictedCipherType,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { import {
DEFAULT_KDF_CONFIG, DEFAULT_KDF_CONFIG,
PBKDF2KdfConfig, PBKDF2KdfConfig,
@@ -170,6 +174,8 @@ describe("VaultExportService", () => {
let kdfConfigService: MockProxy<KdfConfigService>; let kdfConfigService: MockProxy<KdfConfigService>;
let accountService: MockProxy<AccountService>; let accountService: MockProxy<AccountService>;
let apiService: MockProxy<ApiService>; let apiService: MockProxy<ApiService>;
let restrictedSubject: BehaviorSubject<RestrictedCipherType[]>;
let restrictedItemTypesService: Partial<RestrictedItemTypesService>;
let fetchMock: jest.Mock; let fetchMock: jest.Mock;
const userId = "" as UserId; const userId = "" as UserId;
@@ -186,6 +192,12 @@ describe("VaultExportService", () => {
apiService = mock<ApiService>(); apiService = mock<ApiService>();
keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any));
restrictedSubject = new BehaviorSubject<RestrictedCipherType[]>([]);
restrictedItemTypesService = {
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
isCipherRestricted: jest.fn().mockReturnValue(false),
isCipherRestricted$: jest.fn().mockReturnValue(of(false)),
};
const accountInfo: AccountInfo = { const accountInfo: AccountInfo = {
email: "", email: "",
@@ -223,6 +235,7 @@ describe("VaultExportService", () => {
kdfConfigService, kdfConfigService,
accountService, accountService,
apiService, apiService,
restrictedItemTypesService as RestrictedItemTypesService,
); );
}); });
@@ -262,6 +275,46 @@ describe("VaultExportService", () => {
expectEqualCiphers(UserCipherDomains.slice(0, 2), exportedData.data); 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", () => { describe("zip export", () => {
it("contains data.json", async () => { it("contains data.json", async () => {
cipherService.getAllDecrypted.mockResolvedValue([]); cipherService.getAllDecrypted.mockResolvedValue([]);

View File

@@ -20,6 +20,7 @@ import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { Folder } from "@bitwarden/common/vault/models/domain/folder"; import { Folder } from "@bitwarden/common/vault/models/domain/folder";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.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 { KdfConfigService, KeyService } from "@bitwarden/key-management";
import { import {
@@ -50,6 +51,7 @@ export class IndividualVaultExportService
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
private accountService: AccountService, private accountService: AccountService,
private apiService: ApiService, private apiService: ApiService,
private restrictedItemTypesService: RestrictedItemTypesService,
) { ) {
super(pinService, encryptService, cryptoFunctionService, kdfConfigService); super(pinService, encryptService, cryptoFunctionService, kdfConfigService);
} }
@@ -169,9 +171,15 @@ export class IndividualVaultExportService
}), }),
); );
const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$);
promises.push( promises.push(
this.cipherService.getAllDecrypted(activeUserId).then((ciphers) => { 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( promises.push(
this.cipherService.getAll(activeUserId).then((c) => { 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),
);
}), }),
); );

View File

@@ -24,6 +24,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; 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 { KdfConfigService, KeyService } from "@bitwarden/key-management";
import { import {
@@ -52,6 +53,7 @@ export class OrganizationVaultExportService
private collectionService: CollectionService, private collectionService: CollectionService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
private accountService: AccountService, private accountService: AccountService,
private restrictedItemTypesService: RestrictedItemTypesService,
) { ) {
super(pinService, encryptService, cryptoFunctionService, kdfConfigService); super(pinService, encryptService, cryptoFunctionService, kdfConfigService);
} }
@@ -133,6 +135,8 @@ export class OrganizationVaultExportService
const decCiphers: CipherView[] = []; const decCiphers: CipherView[] = [];
const promises = []; const promises = [];
const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$);
promises.push( promises.push(
this.apiService.getOrganizationExport(organizationId).then((exportData) => { this.apiService.getOrganizationExport(organizationId).then((exportData) => {
const exportPromises: any = []; const exportPromises: any = [];
@@ -156,7 +160,11 @@ export class OrganizationVaultExportService
const cipher = new Cipher(new CipherData(c)); const cipher = new Cipher(new CipherData(c));
exportPromises.push( exportPromises.push(
this.cipherService.decrypt(cipher, activeUserId).then((decCipher) => { this.cipherService.decrypt(cipher, activeUserId).then((decCipher) => {
if (
!this.restrictedItemTypesService.isCipherRestricted(decCipher, restrictions)
) {
decCiphers.push(decCipher); decCiphers.push(decCipher);
}
}), }),
); );
}); });
@@ -176,7 +184,7 @@ export class OrganizationVaultExportService
private async getOrganizationEncryptedExport(organizationId: string): Promise<string> { private async getOrganizationEncryptedExport(organizationId: string): Promise<string> {
const collections: Collection[] = []; const collections: Collection[] = [];
const ciphers: Cipher[] = []; let ciphers: Cipher[] = [];
const promises = []; const promises = [];
promises.push( promises.push(
@@ -190,15 +198,17 @@ export class OrganizationVaultExportService
}), }),
); );
const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$);
promises.push( promises.push(
this.apiService.getCiphersOrganization(organizationId).then((c) => { this.apiService.getCiphersOrganization(organizationId).then((c) => {
if (c != null && c.data != null && c.data.length > 0) { if (c != null && c.data != null && c.data.length > 0) {
c.data ciphers = c.data
.filter((item) => item.deletedDate === null) .filter((item) => item.deletedDate === null)
.forEach((item) => { .map((item) => new Cipher(new CipherData(item)))
const cipher = new Cipher(new CipherData(item)); .filter(
ciphers.push(cipher); (cipher) => !this.restrictedItemTypesService.isCipherRestricted(cipher, restrictions),
}); );
} }
}), }),
); );
@@ -231,11 +241,14 @@ export class OrganizationVaultExportService
); );
await Promise.all(promises); await Promise.all(promises);
const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$);
decCiphers = allDecCiphers.filter( decCiphers = allDecCiphers.filter(
(f) => (f) =>
f.deletedDate == null && f.deletedDate == null &&
f.organizationId == organizationId && 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") { if (format === "csv") {
@@ -267,11 +280,14 @@ export class OrganizationVaultExportService
await Promise.all(promises); await Promise.all(promises);
const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$);
encCiphers = allCiphers.filter( encCiphers = allCiphers.filter(
(f) => (f) =>
f.deletedDate == null && f.deletedDate == null &&
f.organizationId == organizationId && 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); return this.BuildEncryptedExport(organizationId, encCollections, encCiphers);

View File

@@ -19,6 +19,10 @@ import { Login } from "@bitwarden/common/vault/models/domain/login";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import {
RestrictedCipherType,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { import {
DEFAULT_KDF_CONFIG, DEFAULT_KDF_CONFIG,
PBKDF2KdfConfig, PBKDF2KdfConfig,
@@ -159,6 +163,7 @@ describe("VaultExportService", () => {
let accountService: MockProxy<AccountService>; let accountService: MockProxy<AccountService>;
let kdfConfigService: MockProxy<KdfConfigService>; let kdfConfigService: MockProxy<KdfConfigService>;
let apiService: MockProxy<ApiService>; let apiService: MockProxy<ApiService>;
let restrictedItemTypesService: Partial<RestrictedItemTypesService>;
beforeEach(() => { beforeEach(() => {
cryptoFunctionService = mock<CryptoFunctionService>(); cryptoFunctionService = mock<CryptoFunctionService>();
@@ -186,6 +191,12 @@ describe("VaultExportService", () => {
const activeAccount = { id: userId, ...accountInfo }; const activeAccount = { id: userId, ...accountInfo };
accountService.activeAccount$ = new BehaviorSubject(activeAccount); accountService.activeAccount$ = new BehaviorSubject(activeAccount);
restrictedItemTypesService = {
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
isCipherRestricted: jest.fn().mockReturnValue(false),
isCipherRestricted$: jest.fn().mockReturnValue(of(false)),
};
exportService = new IndividualVaultExportService( exportService = new IndividualVaultExportService(
folderService, folderService,
cipherService, cipherService,
@@ -196,6 +207,7 @@ describe("VaultExportService", () => {
kdfConfigService, kdfConfigService,
accountService, accountService,
apiService, apiService,
restrictedItemTypesService as RestrictedItemTypesService,
); );
}); });