1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00
Files
browser/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts
Nik Gilmore 4d2106a7c0 [PM-26096] Bugfix: Decryption errors for folders prevent vault access (#16796)
* Handle scenario where folders fail to decrypt

* Add comment explaining the folderViews$ filter check
2025-10-09 10:46:20 -07:00

113 lines
4.6 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject, Injectable } from "@angular/core";
import { combineLatest, filter, firstValueFrom, map, switchMap } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { CollectionService } from "@bitwarden/admin-console/common";
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.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);
private accountService = inject(AccountService);
async buildConfig(
mode: CipherFormMode,
cipherId?: CipherId,
cipherType?: CipherType,
): Promise<CipherFormConfig> {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const [organizations, collections, organizationDataOwnershipDisabled, folders, cipher] =
await firstValueFrom(
combineLatest([
this.organizations$(activeUserId),
this.collectionService.encryptedCollections$(activeUserId).pipe(
map((collections) => collections ?? []),
switchMap((c) =>
this.collectionService.decryptedCollections$(activeUserId).pipe(
filter((d) => d.length === c.length), // Ensure all collections have been decrypted
),
),
),
this.organizationDataOwnershipDisabled$,
this.folderService.folders$(activeUserId).pipe(
switchMap((f) =>
this.folderService
.folderViews$(activeUserId)
// Ensure the folders have decrypted. f.length === 0 indicates we don't have any
// folders to wait for, and d.length > 0 indicates that `folderViews` has emitted the
// array, which includes the "No Folder" default folder.
.pipe(filter((d) => d.length > 0 || f.length === 0)),
),
),
this.getCipher(activeUserId, cipherId),
]),
);
return {
mode,
cipherType: cipher?.type ?? cipherType ?? CipherType.Login,
admin: false,
organizationDataOwnershipDisabled,
originalCipher: cipher,
collections,
organizations,
folders,
};
}
organizations$(userId: UserId) {
return this.organizationService
.organizations$(userId)
.pipe(
map((orgs) =>
orgs.filter(
(o) => o.isMember && o.enabled && o.status === OrganizationUserStatusType.Confirmed,
),
),
);
}
private organizationDataOwnershipDisabled$ = this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) =>
this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId),
),
map((p) => !p),
);
private getCipher(userId: UserId, id?: CipherId): Promise<Cipher | null> {
if (id == null) {
return Promise.resolve(null);
}
return this.cipherService.get(id, userId);
}
}