1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-31 15:43:28 +00:00
Files
browser/libs/common/src/vault/services/cipher-authorization.service.spec.ts
Brandon Treston d0d1359ff4 [PM-12048] Wire up vNextCollectionService (#14871)
* remove derived state, add cache in service. Fix ts strict errors

* cleanup

* promote vNextCollectionService

* wip

* replace callers in web WIP

* refactor tests for web

* update callers to use vNextCollectionServcie methods in CLI

* WIP make decryptMany public again, fix callers, imports

* wip cli

* wip desktop

* update callers in browser, fix tests

* remove in service cache

* cleanup

* fix test

* clean up

* address cr feedback

* remove duplicate userId

* clean up

* remove unused import

* fix vault-settings-import-nudge.service

* fix caching issue

* clean up

* refactor decryption, cleanup, update callers

* clean up

* Use in-memory statedefinition

* Ac/pm 12048 v next collection service pairing (#15239)

* Draft from pairing with Gibson

* Add todos

* Add comment

* wip

* refactor upsert

---------

Co-authored-by: Brandon <btreston@bitwarden.com>

* clean up

* fix state definitions

* fix linter error

* cleanup

* add test, fix shareReplay

* fix item-more-options component

* fix desktop build

* refactor state to account for null as an initial value, remove caching

* add proper cache, add unit test, update callers

* clean up

* fix routing when deleting collections

* cleanup

* use combineLatest

* fix ts-strict errors, fix error handling

* refactor Collection and CollectionView properties for ts-strict

* Revert "refactor Collection and CollectionView properties for ts-strict"

This reverts commit a5c63aab76.

---------

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
2025-07-23 19:05:15 -04:00

284 lines
11 KiB
TypeScript

import { mock } from "jest-mock-extended";
import { Observable, firstValueFrom, of } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec";
import { CipherPermissionsApi } from "../models/api/cipher-permissions.api";
import { CipherView } from "../models/view/cipher.view";
import {
CipherAuthorizationService,
DefaultCipherAuthorizationService,
} from "./cipher-authorization.service";
describe("CipherAuthorizationService", () => {
let cipherAuthorizationService: CipherAuthorizationService;
const mockCollectionService = mock<CollectionService>();
const mockOrganizationService = mock<OrganizationService>();
const mockUserId = Utils.newGuid() as UserId;
let mockAccountService: FakeAccountService;
// Mock factories
const createMockCipher = (
organizationId: string | null,
collectionIds: string[],
edit: boolean = true,
permissions: CipherPermissionsApi = new CipherPermissionsApi(),
) => ({
organizationId,
collectionIds,
edit,
permissions,
});
const createMockCollection = (id: string, manage: boolean) => ({
id,
manage,
});
const createMockOrganization = ({
allowAdminAccessToAllCollectionItems = false,
canEditAllCiphers = false,
canEditUnassignedCiphers = false,
isAdmin = false,
editAnyCollection = false,
} = {}) => ({
id: "org1",
allowAdminAccessToAllCollectionItems,
canEditAllCiphers,
canEditUnassignedCiphers,
isAdmin,
permissions: {
editAnyCollection,
},
});
beforeEach(() => {
jest.clearAllMocks();
mockAccountService = mockAccountServiceWith(mockUserId);
cipherAuthorizationService = new DefaultCipherAuthorizationService(
mockCollectionService,
mockOrganizationService,
mockAccountService,
);
});
describe("canRestoreCipher$", () => {
it("should return true if isAdminConsoleAction and cipher is unassigned", (done) => {
const cipher = createMockCipher("org1", []) as CipherView;
const organization = createMockOrganization({ canEditUnassignedCiphers: true });
mockOrganizationService.organizations$.mockReturnValue(
of([organization]) as Observable<Organization[]>,
);
cipherAuthorizationService.canRestoreCipher$(cipher, true).subscribe((result) => {
expect(result).toBe(true);
done();
});
});
it("should return true if isAdminConsoleAction and user can edit all ciphers in the org", (done) => {
const cipher = createMockCipher("org1", ["col1"]) as CipherView;
const organization = createMockOrganization({ canEditAllCiphers: true });
mockOrganizationService.organizations$.mockReturnValue(
of([organization]) as Observable<Organization[]>,
);
cipherAuthorizationService.canRestoreCipher$(cipher, true).subscribe((result) => {
expect(result).toBe(true);
expect(mockOrganizationService.organizations$).toHaveBeenCalledWith(mockUserId);
done();
});
});
it("should return false if isAdminConsoleAction is true but user does not have permission to edit unassigned ciphers", (done) => {
const cipher = createMockCipher("org1", []) as CipherView;
const organization = createMockOrganization({ canEditUnassignedCiphers: false });
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
cipherAuthorizationService.canRestoreCipher$(cipher, true).subscribe((result) => {
expect(result).toBe(false);
done();
});
});
it("should return false if cipher.permission.restore is false and is not an admin action", (done) => {
const cipher = createMockCipher("org1", [], true, {
restore: false,
} as CipherPermissionsApi) as CipherView;
const organization = createMockOrganization();
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
cipherAuthorizationService.canRestoreCipher$(cipher, false).subscribe((result) => {
expect(result).toBe(false);
expect(mockCollectionService.decryptedCollections$).not.toHaveBeenCalled();
done();
});
});
it("should return true if cipher.permission.restore is true and is not an admin action", (done) => {
const cipher = createMockCipher("org1", [], true, {
restore: true,
} as CipherPermissionsApi) as CipherView;
const organization = createMockOrganization();
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
cipherAuthorizationService.canRestoreCipher$(cipher, false).subscribe((result) => {
expect(result).toBe(true);
expect(mockCollectionService.decryptedCollections$).not.toHaveBeenCalled();
done();
});
});
});
describe("canDeleteCipher$", () => {
it("should return true if isAdminConsoleAction is true and cipher is unassigned", (done) => {
const cipher = createMockCipher("org1", []) as CipherView;
const organization = createMockOrganization({ canEditUnassignedCiphers: true });
mockOrganizationService.organizations$.mockReturnValue(
of([organization]) as Observable<Organization[]>,
);
cipherAuthorizationService.canDeleteCipher$(cipher, true).subscribe((result) => {
expect(result).toBe(true);
done();
});
});
it("should return true if isAdminConsoleAction is true and user can edit all ciphers in the org", (done) => {
const cipher = createMockCipher("org1", ["col1"]) as CipherView;
const organization = createMockOrganization({ canEditAllCiphers: true });
mockOrganizationService.organizations$.mockReturnValue(
of([organization]) as Observable<Organization[]>,
);
cipherAuthorizationService.canDeleteCipher$(cipher, true).subscribe((result) => {
expect(result).toBe(true);
expect(mockOrganizationService.organizations$).toHaveBeenCalledWith(mockUserId);
done();
});
});
it("should return false if isAdminConsoleAction is true but user does not have permission to edit unassigned ciphers", (done) => {
const cipher = createMockCipher("org1", []) as CipherView;
const organization = createMockOrganization({ canEditUnassignedCiphers: false });
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
cipherAuthorizationService.canDeleteCipher$(cipher, true).subscribe((result) => {
expect(result).toBe(false);
done();
});
});
it("should return true when cipher.permissions.delete is true", (done) => {
const cipher = createMockCipher("org1", [], true, {
delete: true,
} as CipherPermissionsApi) as CipherView;
const organization = createMockOrganization();
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
cipherAuthorizationService.canDeleteCipher$(cipher, false).subscribe((result) => {
expect(result).toBe(true);
done();
});
});
it("should return false when cipher.permissions.delete is false", (done) => {
const cipher = createMockCipher("org1", []) as CipherView;
const organization = createMockOrganization();
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
cipherAuthorizationService.canDeleteCipher$(cipher, false).subscribe((result) => {
expect(result).toBe(false);
expect(mockCollectionService.decryptedCollections$).not.toHaveBeenCalled();
done();
});
});
});
describe("canCloneCipher$", () => {
it("should return true if cipher has no organizationId", async () => {
const cipher = createMockCipher(null, []) as CipherView;
const result = await firstValueFrom(cipherAuthorizationService.canCloneCipher$(cipher));
expect(result).toBe(true);
});
describe("isAdminConsoleAction is true", () => {
it("should return true for admin users", async () => {
const cipher = createMockCipher("org1", []) as CipherView;
const organization = createMockOrganization({ isAdmin: true });
mockOrganizationService.organizations$.mockReturnValue(
of([organization] as Organization[]),
);
const result = await firstValueFrom(
cipherAuthorizationService.canCloneCipher$(cipher, true),
);
expect(result).toBe(true);
});
it("should return true for custom user with canEditAnyCollection", async () => {
const cipher = createMockCipher("org1", []) as CipherView;
const organization = createMockOrganization({ editAnyCollection: true });
mockOrganizationService.organizations$.mockReturnValue(
of([organization] as Organization[]),
);
const result = await firstValueFrom(
cipherAuthorizationService.canCloneCipher$(cipher, true),
);
expect(result).toBe(true);
});
});
describe("isAdminConsoleAction is false", () => {
it("should return true if at least one cipher collection has manage permission", async () => {
const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView;
const organization = createMockOrganization();
mockOrganizationService.organizations$.mockReturnValue(
of([organization] as Organization[]),
);
const allCollections = [
createMockCollection("col1", true),
createMockCollection("col2", false),
];
mockCollectionService.decryptedCollections$.mockReturnValue(
of(allCollections as CollectionView[]),
);
const result = await firstValueFrom(cipherAuthorizationService.canCloneCipher$(cipher));
expect(result).toBe(true);
});
it("should return false if no collection has manage permission", async () => {
const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView;
const organization = createMockOrganization();
mockOrganizationService.organizations$.mockReturnValue(
of([organization] as Organization[]),
);
const allCollections = [
createMockCollection("col1", false),
createMockCollection("col2", false),
];
mockCollectionService.decryptedCollections$.mockReturnValue(
of(allCollections as CollectionView[]),
);
const result = await firstValueFrom(cipherAuthorizationService.canCloneCipher$(cipher));
expect(result).toBe(false);
});
});
});
});