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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user