1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-27 01:53:23 +00:00

[PM-18855] Add edit Cipher permission check to Cipher Authorization Service and use in Vault dialog (#18375)

Centralize edit permission checks in CipherAuthorizationService instead of using the disableForm parameter passed to VaultItemDialogComponent. This refactoring improves consistency with how delete and restore permissions are handled, establishes a single source of truth for authorization logic, and simplifies caller components.

This change also fixes the bug in ticket, which allows Users to properly edit Ciphers inside of the various Admin Console report types.
This commit is contained in:
Brad
2026-02-06 10:18:20 -08:00
committed by GitHub
parent 6a01e7436c
commit b6ff3a110e
10 changed files with 218 additions and 114 deletions

View File

@@ -205,6 +205,70 @@ describe("CipherAuthorizationService", () => {
});
});
describe("canEditCipher$", () => {
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.canEditCipher$(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.canEditCipher$(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.canEditCipher$(cipher, true).subscribe((result) => {
expect(result).toBe(false);
done();
});
});
it("should return true if cipher.edit is true and is not an admin action", (done) => {
const cipher = createMockCipher("org1", [], true) as CipherView;
const organization = createMockOrganization();
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
cipherAuthorizationService.canEditCipher$(cipher, false).subscribe((result) => {
expect(result).toBe(true);
expect(mockCollectionService.decryptedCollections$).not.toHaveBeenCalled();
done();
});
});
it("should return false if cipher.edit is false and is not an admin action", (done) => {
const cipher = createMockCipher("org1", [], false) as CipherView;
const organization = createMockOrganization();
mockOrganizationService.organizations$.mockReturnValue(of([organization] as Organization[]));
cipherAuthorizationService.canEditCipher$(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;

View File

@@ -53,6 +53,19 @@ export abstract class CipherAuthorizationService {
cipher: CipherLike,
isAdminConsoleAction?: boolean,
) => Observable<boolean>;
/**
* Determines if the user can edit the specified cipher.
*
* @param {CipherLike} cipher - The cipher object to evaluate for edit 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 edit the cipher.
*/
abstract canEditCipher$: (
cipher: CipherLike,
isAdminConsoleAction?: boolean,
) => Observable<boolean>;
}
/**
@@ -118,6 +131,29 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer
);
}
/**
*
* {@link CipherAuthorizationService.canEditCipher$}
*/
canEditCipher$(cipher: CipherLike, isAdminConsoleAction?: boolean): Observable<boolean> {
return this.organization$(cipher).pipe(
map((organization) => {
if (isAdminConsoleAction) {
// If the user is an admin, they can edit an unassigned cipher
if (!cipher.collectionIds || cipher.collectionIds.length === 0) {
return organization?.canEditUnassignedCiphers === true;
}
if (organization?.canEditAllCiphers) {
return true;
}
}
return !!cipher.edit;
}),
);
}
/**
* {@link CipherAuthorizationService.canCloneCipher$}
*/