mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
Refactor canClone method to use CipherAuthorizationService (#16849)
This commit is contained in:
@@ -162,7 +162,7 @@
|
||||
[showPremiumFeatures]="showPremiumFeatures"
|
||||
[useEvents]="useEvents"
|
||||
[viewingOrgVault]="viewingOrgVault"
|
||||
[cloneable]="canClone(item)"
|
||||
[cloneable]="canClone$(item) | async"
|
||||
[organizations]="allOrganizations"
|
||||
[collections]="allCollections"
|
||||
[checked]="selection.isSelected(item)"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { toSignal, takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
@@ -111,8 +111,6 @@ export class VaultItemsComponent<C extends CipherViewLike> {
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() enforceOrgDataOwnershipPolicy: boolean;
|
||||
|
||||
private readonly restrictedPolicies = toSignal(this.restrictedItemTypesService.restricted$);
|
||||
|
||||
private _ciphers?: C[] = [];
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@@ -390,37 +388,22 @@ export class VaultItemsComponent<C extends CipherViewLike> {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: PM-13944 Refactor to use cipherAuthorizationService.canClone$ instead
|
||||
protected canClone(vaultItem: VaultItem<C>) {
|
||||
// This will check for restrictions from org policies before allowing cloning.
|
||||
const isItemRestricted = this.restrictedPolicies().some(
|
||||
(rt) => rt.cipherType === CipherViewLikeUtils.getType(vaultItem.cipher),
|
||||
protected canClone$(vaultItem: VaultItem<C>): Observable<boolean> {
|
||||
return this.restrictedItemTypesService.restricted$.pipe(
|
||||
switchMap((restrictedTypes) => {
|
||||
// This will check for restrictions from org policies before allowing cloning.
|
||||
const isItemRestricted = restrictedTypes.some(
|
||||
(rt) => rt.cipherType === CipherViewLikeUtils.getType(vaultItem.cipher),
|
||||
);
|
||||
if (isItemRestricted) {
|
||||
return of(false);
|
||||
}
|
||||
return this.cipherAuthorizationService.canCloneCipher$(
|
||||
vaultItem.cipher,
|
||||
this.showAdminActions,
|
||||
);
|
||||
}),
|
||||
);
|
||||
if (isItemRestricted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (vaultItem.cipher.organizationId == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const org = this.allOrganizations.find((o) => o.id === vaultItem.cipher.organizationId);
|
||||
|
||||
// Admins and custom users can always clone in the Org Vault
|
||||
if (this.viewingOrgVault && (org.isAdmin || org.permissions.editAnyCollection)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the cipher belongs to a collection with canManage permission
|
||||
const orgCollections = this.allCollections.filter((c) => c.organizationId === org.id);
|
||||
|
||||
for (const collection of orgCollections) {
|
||||
if (vaultItem.cipher.collectionIds.includes(collection.id as any) && collection.manage) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected canEditCipher(cipher: C) {
|
||||
|
||||
@@ -12,6 +12,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { CipherLike } from "../types/cipher-like";
|
||||
|
||||
import { RestrictedItemTypesService, RestrictedCipherType } from "./restricted-item-types.service";
|
||||
|
||||
describe("RestrictedItemTypesService", () => {
|
||||
@@ -130,4 +132,170 @@ describe("RestrictedItemTypesService", () => {
|
||||
{ cipherType: CipherType.Identity, allowViewOrgIds: ["org1"] },
|
||||
]);
|
||||
});
|
||||
|
||||
describe("isCipherRestricted", () => {
|
||||
it("returns false when cipher type is not in restricted types", () => {
|
||||
const cipher: CipherLike = {
|
||||
type: CipherType.Login,
|
||||
organizationId: "Pete the Cat",
|
||||
} as CipherLike;
|
||||
const restrictedTypes: RestrictedCipherType[] = [
|
||||
{ cipherType: CipherType.Card, allowViewOrgIds: [] },
|
||||
];
|
||||
|
||||
const result = service.isCipherRestricted(cipher, restrictedTypes);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when restricted types array is empty", () => {
|
||||
const cipher: CipherLike = { type: CipherType.Card, organizationId: "org1" } as CipherLike;
|
||||
const restrictedTypes: RestrictedCipherType[] = [];
|
||||
|
||||
const result = service.isCipherRestricted(cipher, restrictedTypes);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when cipher type does not match any restricted types", () => {
|
||||
const cipher: CipherLike = {
|
||||
type: CipherType.SecureNote,
|
||||
organizationId: "org1",
|
||||
} as CipherLike;
|
||||
const restrictedTypes: RestrictedCipherType[] = [
|
||||
{ cipherType: CipherType.Card, allowViewOrgIds: [] },
|
||||
{ cipherType: CipherType.Login, allowViewOrgIds: [] },
|
||||
{ cipherType: CipherType.Identity, allowViewOrgIds: [] },
|
||||
];
|
||||
|
||||
const result = service.isCipherRestricted(cipher, restrictedTypes);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for personal cipher when type is restricted", () => {
|
||||
const cipher: CipherLike = { type: CipherType.Card, organizationId: null } as CipherLike;
|
||||
const restrictedTypes: RestrictedCipherType[] = [
|
||||
{ cipherType: CipherType.Card, allowViewOrgIds: ["org1"] },
|
||||
];
|
||||
|
||||
const result = service.isCipherRestricted(cipher, restrictedTypes);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for personal cipher with undefined organizationId when type is restricted", () => {
|
||||
const cipher: CipherLike = {
|
||||
type: CipherType.Login,
|
||||
organizationId: undefined,
|
||||
} as CipherLike;
|
||||
const restrictedTypes: RestrictedCipherType[] = [
|
||||
{ cipherType: CipherType.Login, allowViewOrgIds: ["org1", "org2"] },
|
||||
];
|
||||
|
||||
const result = service.isCipherRestricted(cipher, restrictedTypes);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for personal cipher regardless of allowViewOrgIds content", () => {
|
||||
const cipher: CipherLike = { type: CipherType.Identity, organizationId: null } as CipherLike;
|
||||
const restrictedTypes: RestrictedCipherType[] = [
|
||||
{ cipherType: CipherType.Identity, allowViewOrgIds: [] },
|
||||
];
|
||||
|
||||
const result = service.isCipherRestricted(cipher, restrictedTypes);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when organization is in allowViewOrgIds", () => {
|
||||
const cipher: CipherLike = { type: CipherType.Card, organizationId: "org1" } as CipherLike;
|
||||
const restrictedTypes: RestrictedCipherType[] = [
|
||||
{ cipherType: CipherType.Card, allowViewOrgIds: ["org1"] },
|
||||
];
|
||||
|
||||
const result = service.isCipherRestricted(cipher, restrictedTypes);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when organization is among multiple allowViewOrgIds", () => {
|
||||
const cipher: CipherLike = { type: CipherType.Login, organizationId: "org2" } as CipherLike;
|
||||
const restrictedTypes: RestrictedCipherType[] = [
|
||||
{ cipherType: CipherType.Login, allowViewOrgIds: ["org1", "org2", "org3"] },
|
||||
];
|
||||
|
||||
const result = service.isCipherRestricted(cipher, restrictedTypes);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when type is restricted globally but cipher org allows it", () => {
|
||||
const cipher: CipherLike = { type: CipherType.Card, organizationId: "org2" } as CipherLike;
|
||||
const restrictedTypes: RestrictedCipherType[] = [
|
||||
{ cipherType: CipherType.Card, allowViewOrgIds: ["org2"] },
|
||||
];
|
||||
|
||||
const result = service.isCipherRestricted(cipher, restrictedTypes);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when organization is not in allowViewOrgIds", () => {
|
||||
const cipher: CipherLike = { type: CipherType.Card, organizationId: "org3" } as CipherLike;
|
||||
const restrictedTypes: RestrictedCipherType[] = [
|
||||
{ cipherType: CipherType.Card, allowViewOrgIds: ["org1", "org2"] },
|
||||
];
|
||||
|
||||
const result = service.isCipherRestricted(cipher, restrictedTypes);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when allowViewOrgIds is empty for org cipher", () => {
|
||||
const cipher: CipherLike = { type: CipherType.Login, organizationId: "org1" } as CipherLike;
|
||||
const restrictedTypes: RestrictedCipherType[] = [
|
||||
{ cipherType: CipherType.Login, allowViewOrgIds: [] },
|
||||
];
|
||||
|
||||
const result = service.isCipherRestricted(cipher, restrictedTypes);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when cipher org differs from all allowViewOrgIds", () => {
|
||||
const cipher: CipherLike = {
|
||||
type: CipherType.Identity,
|
||||
organizationId: "org5",
|
||||
} as CipherLike;
|
||||
const restrictedTypes: RestrictedCipherType[] = [
|
||||
{ cipherType: CipherType.Identity, allowViewOrgIds: ["org1", "org2", "org3", "org4"] },
|
||||
];
|
||||
|
||||
const result = service.isCipherRestricted(cipher, restrictedTypes);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCipherRestricted$", () => {
|
||||
it("returns true when cipher is restricted by policy", async () => {
|
||||
policyService.policiesByType$.mockReturnValue(of([policyOrg1]));
|
||||
const cipher: CipherLike = { type: CipherType.Card, organizationId: null } as CipherLike;
|
||||
|
||||
const result = await firstValueFrom(service.isCipherRestricted$(cipher));
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when cipher is not restricted", async () => {
|
||||
policyService.policiesByType$.mockReturnValue(of([policyOrg1]));
|
||||
const cipher: CipherLike = { type: CipherType.Login, organizationId: "org2" } as CipherLike;
|
||||
|
||||
const result = await firstValueFrom(service.isCipherRestricted$(cipher));
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user