1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-28 06:03:40 +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

@@ -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);

View File

@@ -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<string, any>): Promise<Response> {
@@ -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);

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));
}
}