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

[PM-13892] Browser Refresh - Organization item clone permission fix (#11660)

* [PM-13892] Introduce canClone$ method on CipherAuthorizationService

* [PM-13892] Use new canClone$ method for the 3dot menu in browser extension

* [PM-13892] Add todo for vault-items.component.ts
This commit is contained in:
Shane Melton
2024-10-24 14:12:04 -07:00
committed by GitHub
parent 81d7f319f6
commit a0fe4f4ca6
5 changed files with 120 additions and 4 deletions

View File

@@ -1,5 +1,5 @@
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { firstValueFrom, of } from "rxjs";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -39,10 +39,16 @@ describe("CipherAuthorizationService", () => {
allowAdminAccessToAllCollectionItems = false,
canEditAllCiphers = false,
canEditUnassignedCiphers = false,
isAdmin = false,
editAnyCollection = false,
} = {}) => ({
allowAdminAccessToAllCollectionItems,
canEditAllCiphers,
canEditUnassignedCiphers,
isAdmin,
permissions: {
editAnyCollection,
},
});
beforeEach(() => {
@@ -197,4 +203,73 @@ describe("CipherAuthorizationService", () => {
});
});
});
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.get$.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.get$.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.get$.mockReturnValue(of(organization as Organization));
const allCollections = [
createMockCollection("col1", true),
createMockCollection("col2", false),
];
mockCollectionService.decryptedCollectionViews$.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.get$.mockReturnValue(of(organization as Organization));
const allCollections = [
createMockCollection("col1", false),
createMockCollection("col2", false),
];
mockCollectionService.decryptedCollectionViews$.mockReturnValue(
of(allCollections as CollectionView[]),
);
const result = await firstValueFrom(cipherAuthorizationService.canCloneCipher$(cipher));
expect(result).toBe(false);
});
});
});
});

View File

@@ -1,4 +1,4 @@
import { map, Observable, of, switchMap } from "rxjs";
import { map, Observable, of, shareReplay, switchMap } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -30,6 +30,16 @@ export abstract class CipherAuthorizationService {
allowedCollections?: CollectionId[],
isAdminConsoleAction?: boolean,
) => Observable<boolean>;
/**
* Determines if the user can clone the specified cipher.
*
* @param {CipherLike} cipher - The cipher object to evaluate for cloning permissions.
* @param {boolean} isAdminConsoleAction - Optional. A flag indicating if the action is being performed from the admin console.
*
* @returns {Observable<boolean>} - An observable that emits a boolean value indicating if the user can clone the cipher.
*/
canCloneCipher$: (cipher: CipherLike, isAdminConsoleAction?: boolean) => Observable<boolean>;
}
/**
@@ -83,4 +93,30 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
}),
);
}
/**
* {@link CipherAuthorizationService.canCloneCipher$}
*/
canCloneCipher$(cipher: CipherLike, isAdminConsoleAction?: boolean): Observable<boolean> {
if (cipher.organizationId == null) {
return of(true);
}
return this.organizationService.get$(cipher.organizationId).pipe(
switchMap((organization) => {
// Admins and custom users can always clone when in the Admin Console
if (
isAdminConsoleAction &&
(organization.isAdmin || organization.permissions?.editAnyCollection)
) {
return of(true);
}
return this.collectionService
.decryptedCollectionViews$(cipher.collectionIds as CollectionId[])
.pipe(map((allCollections) => allCollections.some((collection) => collection.manage)));
}),
shareReplay({ bufferSize: 1, refCount: false }),
);
}
}