mirror of
https://github.com/bitwarden/browser
synced 2025-12-20 10:13:31 +00:00
[PM-7162] Cipher Form - Item Details (#9758)
* [PM-7162] Fix weird angular error regarding disabled component bit-select * [PM-7162] Introduce CipherFormConfigService and related types * [PM-7162] Introduce CipherFormService * [PM-7162] Introduce the Item Details section component and the CipherFormContainer interface * [PM-7162] Introduce the CipherForm component * [PM-7162] Add strongly typed QueryParams to the add-edit-v2.component * [PM-7162] Export CipherForm from Vault Lib * [PM-7162] Use the CipherForm in Browser AddEditV2 * [PM-7162] Introduce CipherForm storybook * [PM-7162] Remove VaultPopupListFilterService dependency from NewItemDropDownV2 component * [PM-7162] Add support for content projection of attachment button * [PM-7162] Fix typo * [PM-7162] Cipher form service cleanup * [PM-7162] Move readonly collection notice to bit-hint * [PM-7162] Refactor CipherFormConfig type to enforce required properties with Typescript * [PM-7162] Fix storybook after config changes * [PM-7162] Use new add-edit component for clone route
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { combineLatest, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
|
||||
import {
|
||||
CipherFormConfig,
|
||||
CipherFormConfigService,
|
||||
CipherFormMode,
|
||||
} from "../abstractions/cipher-form-config.service";
|
||||
|
||||
/**
|
||||
* Default implementation of the `CipherFormConfigService`. This service should suffice for most use cases, however
|
||||
* the admin console may need to provide a custom implementation to support admin/custom users who have access to
|
||||
* collections that are not part of their normal sync data.
|
||||
*/
|
||||
@Injectable()
|
||||
export class DefaultCipherFormConfigService implements CipherFormConfigService {
|
||||
private policyService: PolicyService = inject(PolicyService);
|
||||
private organizationService: OrganizationService = inject(OrganizationService);
|
||||
private cipherService: CipherService = inject(CipherService);
|
||||
private folderService: FolderService = inject(FolderService);
|
||||
private collectionService: CollectionService = inject(CollectionService);
|
||||
|
||||
async buildConfig(
|
||||
mode: CipherFormMode,
|
||||
cipherId?: CipherId,
|
||||
cipherType?: CipherType,
|
||||
): Promise<CipherFormConfig> {
|
||||
const [organizations, collections, allowPersonalOwnership, folders, cipher] =
|
||||
await firstValueFrom(
|
||||
combineLatest([
|
||||
this.organizations$,
|
||||
this.collectionService.decryptedCollections$,
|
||||
this.allowPersonalOwnership$,
|
||||
this.folderService.folderViews$,
|
||||
this.getCipher(cipherId),
|
||||
]),
|
||||
);
|
||||
|
||||
return {
|
||||
mode,
|
||||
cipherType,
|
||||
admin: false,
|
||||
allowPersonalOwnership,
|
||||
originalCipher: cipher,
|
||||
collections,
|
||||
organizations,
|
||||
folders,
|
||||
};
|
||||
}
|
||||
|
||||
private organizations$ = this.organizationService.organizations$.pipe(
|
||||
map((orgs) =>
|
||||
orgs.filter(
|
||||
(o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
private allowPersonalOwnership$ = this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
|
||||
.pipe(map((p) => !p));
|
||||
|
||||
private getCipher(id?: CipherId): Promise<Cipher | null> {
|
||||
if (id == null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.cipherService.get(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { CipherFormConfig } from "../abstractions/cipher-form-config.service";
|
||||
import { CipherFormService } from "../abstractions/cipher-form.service";
|
||||
|
||||
function isSetEqual(a: Set<string>, b: Set<string>) {
|
||||
return a.size === b.size && [...a].every((value) => b.has(value));
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DefaultCipherFormService implements CipherFormService {
|
||||
private cipherService: CipherService = inject(CipherService);
|
||||
|
||||
async decryptCipher(cipher: Cipher): Promise<CipherView> {
|
||||
return await cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher));
|
||||
}
|
||||
|
||||
async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise<CipherView> {
|
||||
// Passing the original cipher is important here as it is responsible for appending to password history
|
||||
const encryptedCipher = await this.cipherService.encrypt(
|
||||
cipher,
|
||||
null,
|
||||
null,
|
||||
config.originalCipher ?? null,
|
||||
);
|
||||
|
||||
let savedCipher: Cipher;
|
||||
|
||||
// Creating a new cipher
|
||||
if (cipher.id == null) {
|
||||
savedCipher = await this.cipherService.createWithServer(encryptedCipher, config.admin);
|
||||
return await savedCipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(savedCipher),
|
||||
);
|
||||
}
|
||||
|
||||
if (config.originalCipher == null) {
|
||||
throw new Error("Original cipher is required for updating an existing cipher");
|
||||
}
|
||||
|
||||
// Updating an existing cipher
|
||||
|
||||
const originalCollectionIds = new Set(config.originalCipher.collectionIds ?? []);
|
||||
const newCollectionIds = new Set(cipher.collectionIds ?? []);
|
||||
|
||||
// If the collectionIds are the same, update the cipher normally
|
||||
if (isSetEqual(originalCollectionIds, newCollectionIds)) {
|
||||
savedCipher = await this.cipherService.updateWithServer(encryptedCipher, config.admin);
|
||||
} else {
|
||||
// Updating a cipher with collection changes is not supported with a single request currently
|
||||
// First update the cipher with the original collectionIds
|
||||
encryptedCipher.collectionIds = config.originalCipher.collectionIds;
|
||||
await this.cipherService.updateWithServer(encryptedCipher, config.admin);
|
||||
|
||||
// Then save the new collection changes separately
|
||||
encryptedCipher.collectionIds = cipher.collectionIds;
|
||||
savedCipher = await this.cipherService.saveCollectionsWithServer(encryptedCipher);
|
||||
}
|
||||
|
||||
// Its possible the cipher was made no longer available due to collection assignment changes
|
||||
// e.g. The cipher was moved to a collection that the user no longer has access to
|
||||
if (savedCipher == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await savedCipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(savedCipher),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user