mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-12047] Remove usage of ActiveUserState from cipher.service (#12814)
* Cipher service web changes * Updated browser client to pass user id to cipher service observable changes * Cli changes * desktop changes * Fixed test * Libs changes * Fixed merge conflicts * Fixed merge conflicts * removed duplicate reference fixed conflict * Fixed test * Fixed test * Fixed test * Fixed desturcturing issue on failed to decrypt ciphers cipher service * Updated abstraction to use method syntax * Fixed conflicts * Fixed test on add edit v2 Passed active userId to delete function * Used getUserId utility function * made vault changes * made suggestion changes * made suggestion changes * made suggestion changes * Replace getUserId function calls with pipe operator syntax for better consistency * fixed merge conflicts * revert mistake made of usinf account activity during merge conflict fix * fixed conflicts * fixed tests
This commit is contained in:
@@ -7,9 +7,11 @@ import { CollectionService, CollectionView } from "@bitwarden/admin-console/comm
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
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";
|
||||
@@ -45,11 +47,9 @@ export class CollectionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.cipherDomain = await this.loadCipher();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.cipherDomain = await this.loadCipher(activeUserId);
|
||||
this.collectionIds = this.loadCipherCollections();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
||||
);
|
||||
@@ -95,7 +95,8 @@ export class CollectionsComponent implements OnInit {
|
||||
}
|
||||
this.cipherDomain.collectionIds = selectedCollectionIds;
|
||||
try {
|
||||
this.formPromise = this.saveCollections();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.formPromise = this.saveCollections(activeUserId);
|
||||
await this.formPromise;
|
||||
this.onSavedCollections.emit();
|
||||
this.toastService.showToast({
|
||||
@@ -114,8 +115,8 @@ export class CollectionsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
protected loadCipher() {
|
||||
return this.cipherService.get(this.cipherId);
|
||||
protected loadCipher(userId: UserId) {
|
||||
return this.cipherService.get(this.cipherId, userId);
|
||||
}
|
||||
|
||||
protected loadCipherCollections() {
|
||||
@@ -129,7 +130,7 @@ export class CollectionsComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
protected saveCollections() {
|
||||
return this.cipherService.saveCollectionsWithServer(this.cipherDomain);
|
||||
protected saveCollections(userId: UserId) {
|
||||
return this.cipherService.saveCollectionsWithServer(this.cipherDomain, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -73,10 +74,8 @@ export class ShareComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
||||
this.cipher = await cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
|
||||
);
|
||||
@@ -104,10 +103,8 @@ export class ShareComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
|
||||
const cipherView = await cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
@@ -101,8 +102,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
private personalOwnershipPolicyAppliesToActiveUser: boolean;
|
||||
private previousCipherId: string;
|
||||
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
|
||||
get fido2CredentialCreationDateValue(): string {
|
||||
const dateCreated = this.i18nService.t("dateCreated");
|
||||
const creationDate = this.datePipe.transform(
|
||||
@@ -263,12 +262,13 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
this.title = this.i18nService.t("addItem");
|
||||
}
|
||||
|
||||
const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo(activeUserId);
|
||||
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
if (this.cipher == null) {
|
||||
if (this.editMode) {
|
||||
const cipher = await this.loadCipher();
|
||||
const cipher = await this.loadCipher(activeUserId);
|
||||
this.cipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
@@ -420,9 +420,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
this.cipher.id = null;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipher = await this.encryptCipher(activeUserId);
|
||||
try {
|
||||
this.formPromise = this.saveCipher(cipher);
|
||||
@@ -516,7 +514,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
try {
|
||||
this.deletePromise = this.deleteCipher();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.deletePromise = this.deleteCipher(activeUserId);
|
||||
await this.deletePromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -542,7 +541,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
try {
|
||||
this.restorePromise = this.restoreCipher();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.restorePromise = this.restoreCipher(activeUserId);
|
||||
await this.restorePromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -725,8 +725,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
return allCollections.filter((c) => !c.readOnly);
|
||||
}
|
||||
|
||||
protected loadCipher() {
|
||||
return this.cipherService.get(this.cipherId);
|
||||
protected loadCipher(userId: UserId) {
|
||||
return this.cipherService.get(this.cipherId, userId);
|
||||
}
|
||||
|
||||
protected encryptCipher(userId: UserId) {
|
||||
@@ -746,14 +746,14 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
: this.cipherService.updateWithServer(cipher, orgAdmin);
|
||||
}
|
||||
|
||||
protected deleteCipher() {
|
||||
protected deleteCipher(userId: UserId) {
|
||||
return this.cipher.isDeleted
|
||||
? this.cipherService.deleteWithServer(this.cipher.id, this.asAdmin)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id, this.asAdmin);
|
||||
? this.cipherService.deleteWithServer(this.cipher.id, userId, this.asAdmin)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id, userId, this.asAdmin);
|
||||
}
|
||||
|
||||
protected restoreCipher() {
|
||||
return this.cipherService.restoreWithServer(this.cipher.id, this.asAdmin);
|
||||
protected restoreCipher(userId: UserId) {
|
||||
return this.cipherService.restoreWithServer(this.cipher.id, userId, this.asAdmin);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -773,8 +773,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
return this.ownershipOptions[0].value;
|
||||
}
|
||||
|
||||
async loadAddEditCipherInfo(): Promise<boolean> {
|
||||
const addEditCipherInfo: any = await firstValueFrom(this.cipherService.addEditCipherInfo$);
|
||||
async loadAddEditCipherInfo(userId: UserId): Promise<boolean> {
|
||||
const addEditCipherInfo: any = await firstValueFrom(
|
||||
this.cipherService.addEditCipherInfo$(userId),
|
||||
);
|
||||
const loadedSavedInfo = addEditCipherInfo != null;
|
||||
|
||||
if (loadedSavedInfo) {
|
||||
@@ -787,7 +789,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
await this.cipherService.setAddEditCipherInfo(null);
|
||||
await this.cipherService.setAddEditCipherInfo(null, userId);
|
||||
|
||||
return loadedSavedInfo;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
@@ -84,9 +85,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.formPromise = this.saveCipherAttachment(files[0], activeUserId);
|
||||
this.cipherDomain = await this.formPromise;
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
@@ -125,12 +124,11 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id);
|
||||
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id, activeUserId);
|
||||
const updatedCipher = await this.deletePromises[attachment.id];
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const cipher = new Cipher(updatedCipher);
|
||||
this.cipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
@@ -228,10 +226,8 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
this.cipherDomain = await this.loadCipher();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.cipherDomain = await this.loadCipher(activeUserId);
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
|
||||
);
|
||||
@@ -287,7 +283,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
: await this.keyService.getOrgKey(this.cipher.organizationId);
|
||||
const decBuf = await this.encryptService.decryptToBytes(encBuf, key);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
|
||||
this.cipherDomain,
|
||||
@@ -301,7 +297,10 @@ export class AttachmentsComponent implements OnInit {
|
||||
);
|
||||
|
||||
// 3. Delete old
|
||||
this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id);
|
||||
this.deletePromises[attachment.id] = this.deleteCipherAttachment(
|
||||
attachment.id,
|
||||
activeUserId,
|
||||
);
|
||||
await this.deletePromises[attachment.id];
|
||||
const foundAttachment = this.cipher.attachments.filter((a2) => a2.id === attachment.id);
|
||||
if (foundAttachment.length > 0) {
|
||||
@@ -335,16 +334,16 @@ export class AttachmentsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
protected loadCipher() {
|
||||
return this.cipherService.get(this.cipherId);
|
||||
protected loadCipher(userId: UserId) {
|
||||
return this.cipherService.get(this.cipherId, userId);
|
||||
}
|
||||
|
||||
protected saveCipherAttachment(file: File, userId: UserId) {
|
||||
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, userId);
|
||||
}
|
||||
|
||||
protected deleteCipherAttachment(attachmentId: string) {
|
||||
return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId);
|
||||
protected deleteCipherAttachment(attachmentId: string, userId: UserId) {
|
||||
return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId, userId);
|
||||
}
|
||||
|
||||
protected async reupload(attachment: AttachmentView) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -39,10 +40,8 @@ export class PasswordHistoryComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
const cipher = await this.cipherService.get(this.cipherId);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const cipher = await this.cipherService.get(this.cipherId, activeUserId);
|
||||
const decCipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs";
|
||||
import { BehaviorSubject, Subject, firstValueFrom, from, map, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
@@ -41,11 +44,20 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
protected searchService: SearchService,
|
||||
protected cipherService: CipherService,
|
||||
protected accountService: AccountService,
|
||||
) {
|
||||
this.cipherService.cipherViews$.pipe(takeUntilDestroyed()).subscribe((ciphers) => {
|
||||
void this.doSearch(ciphers);
|
||||
this.loaded = true;
|
||||
});
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.cipherService.cipherViews$(userId).pipe(map((ciphers) => ({ userId, ciphers }))),
|
||||
),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe(({ userId, ciphers }) => {
|
||||
void this.doSearch(ciphers, userId);
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -122,10 +134,16 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;
|
||||
|
||||
protected async doSearch(indexedCiphers?: CipherView[]) {
|
||||
indexedCiphers = indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$));
|
||||
protected async doSearch(indexedCiphers?: CipherView[], userId?: UserId) {
|
||||
// Get userId from activeAccount if not provided from parent stream
|
||||
if (!userId) {
|
||||
userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
}
|
||||
|
||||
const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$);
|
||||
indexedCiphers =
|
||||
indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$(userId)));
|
||||
|
||||
const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$(userId));
|
||||
if (failedCiphers != null && failedCiphers.length > 0) {
|
||||
indexedCiphers = [...failedCiphers, ...indexedCiphers];
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -29,7 +30,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
import { CollectionId, 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 { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
@@ -79,7 +80,6 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
private previousCipherId: string;
|
||||
private passwordReprompted = false;
|
||||
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
private destroyed$ = new Subject<void>();
|
||||
|
||||
get fido2CredentialCreationDateValue(): string {
|
||||
@@ -144,9 +144,10 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
async load() {
|
||||
this.cleanUp();
|
||||
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
// Grab individual cipher from `cipherViews$` for the most up-to-date information
|
||||
this.cipherService.cipherViews$
|
||||
this.cipherService
|
||||
.cipherViews$(activeUserId)
|
||||
.pipe(
|
||||
map((ciphers) => ciphers?.find((c) => c.id === this.cipherId)),
|
||||
filter((cipher) => !!cipher),
|
||||
@@ -250,7 +251,8 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.deleteCipher();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.deleteCipher(activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
@@ -272,7 +274,8 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.restoreCipher();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.restoreCipher(activeUserId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
@@ -380,7 +383,8 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
if (cipherId) {
|
||||
await this.cipherService.updateLastLaunchedDate(cipherId);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.cipherService.updateLastLaunchedDate(cipherId, activeUserId);
|
||||
}
|
||||
|
||||
this.platformUtilsService.launchUri(uri.launchUri);
|
||||
@@ -498,14 +502,14 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
a.downloading = false;
|
||||
}
|
||||
|
||||
protected deleteCipher() {
|
||||
protected deleteCipher(userId: UserId) {
|
||||
return this.cipher.isDeleted
|
||||
? this.cipherService.deleteWithServer(this.cipher.id)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id);
|
||||
? this.cipherService.deleteWithServer(this.cipher.id, userId)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id, userId);
|
||||
}
|
||||
|
||||
protected restoreCipher() {
|
||||
return this.cipherService.restoreWithServer(this.cipher.id);
|
||||
protected restoreCipher(userId: UserId) {
|
||||
return this.cipherService.restoreWithServer(this.cipher.id, userId);
|
||||
}
|
||||
|
||||
protected async promptPassword() {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { 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 { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
@@ -30,8 +31,6 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
private readonly collapsedGroupings$: Observable<Set<string>> =
|
||||
this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c)));
|
||||
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
|
||||
constructor(
|
||||
protected organizationService: OrganizationService,
|
||||
protected folderService: FolderService,
|
||||
@@ -63,7 +62,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
}
|
||||
|
||||
buildNestedFolders(organizationId?: string): Observable<DynamicTreeNode<FolderView>> {
|
||||
const transformation = async (storedFolders: FolderView[]) => {
|
||||
const transformation = async (storedFolders: FolderView[], userId: UserId) => {
|
||||
let folders: FolderView[];
|
||||
|
||||
// If no org or "My Vault" is selected, show all folders
|
||||
@@ -71,7 +70,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
folders = storedFolders;
|
||||
} else {
|
||||
// Otherwise, show only folders that have ciphers from the selected org and the "no folder" folder
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
const ciphers = await this.cipherService.getAllDecrypted(userId);
|
||||
const orgCiphers = ciphers.filter((c) => c.organizationId == organizationId);
|
||||
folders = storedFolders.filter(
|
||||
(f) => orgCiphers.some((oc) => oc.folderId == f.id) || f.id == null,
|
||||
@@ -85,9 +84,13 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
});
|
||||
};
|
||||
|
||||
return this.activeUserId$.pipe(
|
||||
switchMap((userId) => this.folderService.folderViews$(userId)),
|
||||
mergeMap((folders) => from(transformation(folders))),
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.folderService
|
||||
.folderViews$(userId)
|
||||
.pipe(mergeMap((folders) => from(transformation(folders, userId)))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,7 +134,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
}
|
||||
|
||||
async getFolderNested(id: string): Promise<TreeNode<FolderView>> {
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const folders = await this.getAllFoldersNested(
|
||||
await firstValueFrom(this.folderService.folderViews$(activeUserId)),
|
||||
);
|
||||
|
||||
@@ -151,11 +151,15 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
|
||||
await this.syncService.syncUpsertCipher(
|
||||
notification.payload as SyncCipherNotification,
|
||||
notification.type === NotificationType.SyncCipherUpdate,
|
||||
payloadUserId,
|
||||
);
|
||||
break;
|
||||
case NotificationType.SyncCipherDelete:
|
||||
case NotificationType.SyncLoginDelete:
|
||||
await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification);
|
||||
await this.syncService.syncDeleteCipher(
|
||||
notification.payload as SyncCipherNotification,
|
||||
payloadUserId,
|
||||
);
|
||||
break;
|
||||
case NotificationType.SyncFolderCreate:
|
||||
case NotificationType.SyncFolderUpdate:
|
||||
|
||||
@@ -3,7 +3,8 @@ import { TextEncoder } from "util";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { Account, AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { mockAccountServiceWith } from "../../../../spec";
|
||||
import { Account } from "../../../auth/abstractions/account.service";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
||||
@@ -46,7 +47,6 @@ describe("FidoAuthenticatorService", () => {
|
||||
let userInterface!: MockProxy<Fido2UserInterfaceService<ParentWindowReference>>;
|
||||
let userInterfaceSession!: MockProxy<Fido2UserInterfaceSession>;
|
||||
let syncService!: MockProxy<SyncService>;
|
||||
let accountService!: MockProxy<AccountService>;
|
||||
let authenticator!: Fido2AuthenticatorService<ParentWindowReference>;
|
||||
let windowReference!: ParentWindowReference;
|
||||
|
||||
@@ -58,7 +58,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
syncService = mock<SyncService>({
|
||||
activeUserLastSync$: () => of(new Date()),
|
||||
});
|
||||
accountService = mock<AccountService>();
|
||||
const accountService = mockAccountServiceWith("testId" as UserId);
|
||||
authenticator = new Fido2AuthenticatorService(
|
||||
cipherService,
|
||||
userInterface,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { getUserId } from "../../../auth/services/account.service";
|
||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
||||
import { CipherRepromptType } from "../../../vault/enums/cipher-reprompt-type";
|
||||
@@ -145,10 +146,10 @@ export class Fido2AuthenticatorService<ParentWindowReference>
|
||||
try {
|
||||
keyPair = await createKeyPair();
|
||||
pubKeyDer = await crypto.subtle.exportKey("spki", keyPair.publicKey);
|
||||
const encrypted = await this.cipherService.get(cipherId);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
const encrypted = await this.cipherService.get(cipherId, activeUserId);
|
||||
|
||||
cipher = await encrypted.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(encrypted, activeUserId),
|
||||
@@ -309,7 +310,7 @@ export class Fido2AuthenticatorService<ParentWindowReference>
|
||||
|
||||
if (selectedFido2Credential.counter > 0) {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
const encrypted = await this.cipherService.encrypt(selectedCipher, activeUserId);
|
||||
await this.cipherService.updateWithServer(encrypted);
|
||||
@@ -400,7 +401,8 @@ export class Fido2AuthenticatorService<ParentWindowReference>
|
||||
return [];
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const ciphers = await this.cipherService.getAllDecrypted(activeUserId);
|
||||
return ciphers
|
||||
.filter(
|
||||
(cipher) =>
|
||||
@@ -421,7 +423,8 @@ export class Fido2AuthenticatorService<ParentWindowReference>
|
||||
return [];
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const ciphers = await this.cipherService.getAllDecrypted(activeUserId);
|
||||
return ciphers.filter(
|
||||
(cipher) =>
|
||||
!cipher.isDeleted &&
|
||||
@@ -438,7 +441,8 @@ export class Fido2AuthenticatorService<ParentWindowReference>
|
||||
}
|
||||
|
||||
private async findCredentialsByRp(rpId: string): Promise<CipherView[]> {
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const ciphers = await this.cipherService.getAllDecrypted(activeUserId);
|
||||
return ciphers.filter(
|
||||
(cipher) =>
|
||||
!cipher.isDeleted &&
|
||||
|
||||
@@ -129,12 +129,18 @@ export abstract class CoreSyncService implements SyncService {
|
||||
return this.syncCompleted(false);
|
||||
}
|
||||
|
||||
async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise<boolean> {
|
||||
async syncUpsertCipher(
|
||||
notification: SyncCipherNotification,
|
||||
isEdit: boolean,
|
||||
userId: UserId,
|
||||
): Promise<boolean> {
|
||||
this.syncStarted();
|
||||
if (await this.stateService.getIsAuthenticated()) {
|
||||
|
||||
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
|
||||
if (authStatus >= AuthenticationStatus.Locked) {
|
||||
try {
|
||||
let shouldUpdate = true;
|
||||
const localCipher = await this.cipherService.get(notification.id);
|
||||
const localCipher = await this.cipherService.get(notification.id, userId);
|
||||
if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) {
|
||||
shouldUpdate = false;
|
||||
}
|
||||
@@ -182,7 +188,7 @@ export abstract class CoreSyncService implements SyncService {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e != null && e.statusCode === 404 && isEdit) {
|
||||
await this.cipherService.delete(notification.id);
|
||||
await this.cipherService.delete(notification.id, userId);
|
||||
this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id });
|
||||
return this.syncCompleted(true);
|
||||
}
|
||||
@@ -191,10 +197,12 @@ export abstract class CoreSyncService implements SyncService {
|
||||
return this.syncCompleted(false);
|
||||
}
|
||||
|
||||
async syncDeleteCipher(notification: SyncCipherNotification): Promise<boolean> {
|
||||
async syncDeleteCipher(notification: SyncCipherNotification, userId: UserId): Promise<boolean> {
|
||||
this.syncStarted();
|
||||
if (await this.stateService.getIsAuthenticated()) {
|
||||
await this.cipherService.delete(notification.id);
|
||||
|
||||
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
|
||||
if (authStatus >= AuthenticationStatus.Locked) {
|
||||
await this.cipherService.delete(notification.id, userId);
|
||||
this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id });
|
||||
return this.syncCompleted(true);
|
||||
}
|
||||
|
||||
@@ -62,8 +62,9 @@ export abstract class SyncService {
|
||||
abstract syncUpsertCipher(
|
||||
notification: SyncCipherNotification,
|
||||
isEdit: boolean,
|
||||
userId: UserId,
|
||||
): Promise<boolean>;
|
||||
abstract syncDeleteCipher(notification: SyncFolderNotification): Promise<boolean>;
|
||||
abstract syncDeleteCipher(notification: SyncFolderNotification, userId: UserId): Promise<boolean>;
|
||||
abstract syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise<boolean>;
|
||||
abstract syncDeleteSend(notification: SyncSendNotification): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service";
|
||||
import { EventUploadService } from "../../abstractions/event/event-upload.service";
|
||||
@@ -46,7 +47,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION);
|
||||
|
||||
if (!(await this.shouldUpdate(null, eventType, ciphers))) {
|
||||
if (!(await this.shouldUpdate(userId, null, eventType, ciphers))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,7 +92,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION);
|
||||
|
||||
if (!(await this.shouldUpdate(organizationId, eventType, undefined, cipherId))) {
|
||||
if (!(await this.shouldUpdate(userId, organizationId, eventType, undefined, cipherId))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -113,18 +114,18 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
|
||||
}
|
||||
|
||||
/** Verifies if the event collection should be updated for the provided information
|
||||
* @param userId the active user's id
|
||||
* @param cipherId the cipher for the event
|
||||
* @param organizationId the organization for the event
|
||||
*/
|
||||
private async shouldUpdate(
|
||||
userId: UserId,
|
||||
organizationId: string = null,
|
||||
eventType: EventType = null,
|
||||
ciphers: CipherView[] = [],
|
||||
cipherId?: string,
|
||||
): Promise<boolean> {
|
||||
const cipher$ = from(this.cipherService.get(cipherId));
|
||||
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
const cipher$ = from(this.cipherService.get(cipherId, userId));
|
||||
|
||||
const orgIds$ = this.organizationService
|
||||
.organizations$(userId)
|
||||
|
||||
@@ -19,57 +19,70 @@ import { FieldView } from "../models/view/field.view";
|
||||
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
|
||||
|
||||
export abstract class CipherService implements UserKeyRotationDataProvider<CipherWithIdRequest> {
|
||||
cipherViews$: Observable<CipherView[]>;
|
||||
ciphers$: Observable<Record<CipherId, CipherData>>;
|
||||
localData$: Observable<Record<CipherId, LocalData>>;
|
||||
abstract cipherViews$(userId: UserId): Observable<CipherView[]>;
|
||||
abstract ciphers$(userId: UserId): Observable<Record<CipherId, CipherData>>;
|
||||
abstract localData$(userId: UserId): Observable<Record<CipherId, LocalData>>;
|
||||
/**
|
||||
* An observable monitoring the add/edit cipher info saved to memory.
|
||||
*/
|
||||
addEditCipherInfo$: Observable<AddEditCipherInfo>;
|
||||
abstract addEditCipherInfo$(userId: UserId): Observable<AddEditCipherInfo>;
|
||||
/**
|
||||
* Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed.
|
||||
*
|
||||
* An empty array indicates that all ciphers were successfully decrypted.
|
||||
*/
|
||||
failedToDecryptCiphers$: Observable<CipherView[]>;
|
||||
clearCache: (userId?: string) => Promise<void>;
|
||||
encrypt: (
|
||||
abstract failedToDecryptCiphers$(userId: UserId): Observable<CipherView[]>;
|
||||
abstract clearCache(userId: UserId): Promise<void>;
|
||||
abstract encrypt(
|
||||
model: CipherView,
|
||||
userId: UserId,
|
||||
keyForEncryption?: SymmetricCryptoKey,
|
||||
keyForCipherKeyDecryption?: SymmetricCryptoKey,
|
||||
originalCipher?: Cipher,
|
||||
) => Promise<Cipher>;
|
||||
encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise<Field[]>;
|
||||
encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise<Field>;
|
||||
get: (id: string) => Promise<Cipher>;
|
||||
getAll: () => Promise<Cipher[]>;
|
||||
getAllDecrypted: () => Promise<CipherView[]>;
|
||||
getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise<CipherView[]>;
|
||||
getAllDecryptedForUrl: (
|
||||
): Promise<Cipher>;
|
||||
abstract encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise<Field[]>;
|
||||
abstract encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise<Field>;
|
||||
abstract get(id: string, userId: UserId): Promise<Cipher>;
|
||||
abstract getAll(userId: UserId): Promise<Cipher[]>;
|
||||
abstract getAllDecrypted(userId: UserId): Promise<CipherView[]>;
|
||||
abstract getAllDecryptedForGrouping(
|
||||
groupingId: string,
|
||||
userId: UserId,
|
||||
folder?: boolean,
|
||||
): Promise<CipherView[]>;
|
||||
abstract getAllDecryptedForUrl(
|
||||
url: string,
|
||||
userId: UserId,
|
||||
includeOtherTypes?: CipherType[],
|
||||
defaultMatch?: UriMatchStrategySetting,
|
||||
) => Promise<CipherView[]>;
|
||||
filterCiphersForUrl: (
|
||||
): Promise<CipherView[]>;
|
||||
abstract filterCiphersForUrl(
|
||||
ciphers: CipherView[],
|
||||
url: string,
|
||||
includeOtherTypes?: CipherType[],
|
||||
defaultMatch?: UriMatchStrategySetting,
|
||||
) => Promise<CipherView[]>;
|
||||
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
|
||||
): Promise<CipherView[]>;
|
||||
abstract getAllFromApiForOrganization(organizationId: string): Promise<CipherView[]>;
|
||||
/**
|
||||
* Gets ciphers belonging to the specified organization that the user has explicit collection level access to.
|
||||
* Ciphers that are not assigned to any collections are only included for users with admin access.
|
||||
*/
|
||||
getManyFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
|
||||
getLastUsedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>;
|
||||
getLastLaunchedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>;
|
||||
getNextCipherForUrl: (url: string) => Promise<CipherView>;
|
||||
updateLastUsedIndexForUrl: (url: string) => void;
|
||||
updateLastUsedDate: (id: string) => Promise<void>;
|
||||
updateLastLaunchedDate: (id: string) => Promise<void>;
|
||||
saveNeverDomain: (domain: string) => Promise<void>;
|
||||
abstract getManyFromApiForOrganization(organizationId: string): Promise<CipherView[]>;
|
||||
abstract getLastUsedForUrl(
|
||||
url: string,
|
||||
userId: UserId,
|
||||
autofillOnPageLoad: boolean,
|
||||
): Promise<CipherView>;
|
||||
abstract getLastLaunchedForUrl(
|
||||
url: string,
|
||||
userId: UserId,
|
||||
autofillOnPageLoad: boolean,
|
||||
): Promise<CipherView>;
|
||||
abstract getNextCipherForUrl(url: string, userId: UserId): Promise<CipherView>;
|
||||
abstract updateLastUsedIndexForUrl(url: string): void;
|
||||
abstract updateLastUsedDate(id: string, userId: UserId): Promise<void>;
|
||||
abstract updateLastLaunchedDate(id: string, userId: UserId): Promise<void>;
|
||||
abstract saveNeverDomain(domain: string): Promise<void>;
|
||||
/**
|
||||
* Create a cipher with the server
|
||||
*
|
||||
@@ -78,7 +91,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
*
|
||||
* @returns A promise that resolves to the created cipher
|
||||
*/
|
||||
createWithServer: (cipher: Cipher, orgAdmin?: boolean) => Promise<Cipher>;
|
||||
abstract createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise<Cipher>;
|
||||
/**
|
||||
* Update a cipher with the server
|
||||
* @param cipher The cipher to update
|
||||
@@ -87,88 +100,105 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
*
|
||||
* @returns A promise that resolves to the updated cipher
|
||||
*/
|
||||
updateWithServer: (cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean) => Promise<Cipher>;
|
||||
shareWithServer: (
|
||||
abstract updateWithServer(
|
||||
cipher: Cipher,
|
||||
orgAdmin?: boolean,
|
||||
isNotClone?: boolean,
|
||||
): Promise<Cipher>;
|
||||
abstract shareWithServer(
|
||||
cipher: CipherView,
|
||||
organizationId: string,
|
||||
collectionIds: string[],
|
||||
userId: UserId,
|
||||
) => Promise<Cipher>;
|
||||
shareManyWithServer: (
|
||||
): Promise<Cipher>;
|
||||
abstract shareManyWithServer(
|
||||
ciphers: CipherView[],
|
||||
organizationId: string,
|
||||
collectionIds: string[],
|
||||
userId: UserId,
|
||||
) => Promise<any>;
|
||||
saveAttachmentWithServer: (
|
||||
): Promise<any>;
|
||||
abstract saveAttachmentWithServer(
|
||||
cipher: Cipher,
|
||||
unencryptedFile: any,
|
||||
userId: UserId,
|
||||
admin?: boolean,
|
||||
) => Promise<Cipher>;
|
||||
saveAttachmentRawWithServer: (
|
||||
): Promise<Cipher>;
|
||||
abstract saveAttachmentRawWithServer(
|
||||
cipher: Cipher,
|
||||
filename: string,
|
||||
data: ArrayBuffer,
|
||||
userId: UserId,
|
||||
admin?: boolean,
|
||||
) => Promise<Cipher>;
|
||||
): Promise<Cipher>;
|
||||
/**
|
||||
* Save the collections for a cipher with the server
|
||||
*
|
||||
* @param cipher The cipher to save collections for
|
||||
* @param userId The user ID
|
||||
*
|
||||
* @returns A promise that resolves when the collections have been saved
|
||||
*/
|
||||
saveCollectionsWithServer: (cipher: Cipher) => Promise<Cipher>;
|
||||
abstract saveCollectionsWithServer(cipher: Cipher, userId: UserId): Promise<Cipher>;
|
||||
|
||||
/**
|
||||
* Save the collections for a cipher with the server as an admin.
|
||||
* Used for Unassigned ciphers or when the user only has admin access to the cipher (not assigned normally).
|
||||
* @param cipher
|
||||
*/
|
||||
saveCollectionsWithServerAdmin: (cipher: Cipher) => Promise<Cipher>;
|
||||
abstract saveCollectionsWithServerAdmin(cipher: Cipher): Promise<Cipher>;
|
||||
/**
|
||||
* Bulk update collections for many ciphers with the server
|
||||
* @param orgId
|
||||
* @param userId
|
||||
* @param cipherIds
|
||||
* @param collectionIds
|
||||
* @param removeCollections - If true, the collections will be removed from the ciphers, otherwise they will be added
|
||||
*/
|
||||
bulkUpdateCollectionsWithServer: (
|
||||
abstract bulkUpdateCollectionsWithServer(
|
||||
orgId: OrganizationId,
|
||||
userId: UserId,
|
||||
cipherIds: CipherId[],
|
||||
collectionIds: CollectionId[],
|
||||
removeCollections: boolean,
|
||||
) => Promise<void>;
|
||||
): Promise<void>;
|
||||
/**
|
||||
* Update the local store of CipherData with the provided data. Values are upserted into the existing store.
|
||||
*
|
||||
* @param cipher The cipher data to upsert. Can be a single CipherData object or an array of CipherData objects.
|
||||
* @returns A promise that resolves to a record of updated cipher store, keyed by their cipher ID. Returns all ciphers, not just those updated
|
||||
*/
|
||||
upsert: (cipher: CipherData | CipherData[]) => Promise<Record<CipherId, CipherData>>;
|
||||
replace: (ciphers: { [id: string]: CipherData }, userId: UserId) => Promise<any>;
|
||||
clear: (userId?: string) => Promise<void>;
|
||||
moveManyWithServer: (ids: string[], folderId: string) => Promise<any>;
|
||||
delete: (id: string | string[]) => Promise<any>;
|
||||
deleteWithServer: (id: string, asAdmin?: boolean) => Promise<any>;
|
||||
deleteManyWithServer: (ids: string[], asAdmin?: boolean) => Promise<any>;
|
||||
deleteAttachment: (id: string, revisionDate: string, attachmentId: string) => Promise<CipherData>;
|
||||
deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise<CipherData>;
|
||||
sortCiphersByLastUsed: (a: CipherView, b: CipherView) => number;
|
||||
sortCiphersByLastUsedThenName: (a: CipherView, b: CipherView) => number;
|
||||
getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number;
|
||||
softDelete: (id: string | string[]) => Promise<any>;
|
||||
softDeleteWithServer: (id: string, asAdmin?: boolean) => Promise<any>;
|
||||
softDeleteManyWithServer: (ids: string[], asAdmin?: boolean) => Promise<any>;
|
||||
restore: (
|
||||
abstract upsert(cipher: CipherData | CipherData[]): Promise<Record<CipherId, CipherData>>;
|
||||
abstract replace(ciphers: { [id: string]: CipherData }, userId: UserId): Promise<any>;
|
||||
abstract clear(userId?: string): Promise<void>;
|
||||
abstract moveManyWithServer(ids: string[], folderId: string, userId: UserId): Promise<any>;
|
||||
abstract delete(id: string | string[], userId: UserId): Promise<any>;
|
||||
abstract deleteWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<any>;
|
||||
abstract deleteManyWithServer(ids: string[], userId: UserId, asAdmin?: boolean): Promise<any>;
|
||||
abstract deleteAttachment(
|
||||
id: string,
|
||||
revisionDate: string,
|
||||
attachmentId: string,
|
||||
userId: UserId,
|
||||
): Promise<CipherData>;
|
||||
abstract deleteAttachmentWithServer(
|
||||
id: string,
|
||||
attachmentId: string,
|
||||
userId: UserId,
|
||||
): Promise<CipherData>;
|
||||
abstract sortCiphersByLastUsed(a: CipherView, b: CipherView): number;
|
||||
abstract sortCiphersByLastUsedThenName(a: CipherView, b: CipherView): number;
|
||||
abstract getLocaleSortingFunction(): (a: CipherView, b: CipherView) => number;
|
||||
abstract softDelete(id: string | string[], userId: UserId): Promise<any>;
|
||||
abstract softDeleteWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<any>;
|
||||
abstract softDeleteManyWithServer(ids: string[], userId: UserId, asAdmin?: boolean): Promise<any>;
|
||||
abstract restore(
|
||||
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
|
||||
) => Promise<any>;
|
||||
restoreWithServer: (id: string, asAdmin?: boolean) => Promise<any>;
|
||||
restoreManyWithServer: (ids: string[], orgId?: string) => Promise<void>;
|
||||
getKeyForCipherKeyDecryption: (cipher: Cipher, userId: UserId) => Promise<any>;
|
||||
setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise<void>;
|
||||
userId: UserId,
|
||||
): Promise<any>;
|
||||
abstract restoreWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<any>;
|
||||
abstract restoreManyWithServer(ids: string[], orgId?: string): Promise<void>;
|
||||
abstract getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise<any>;
|
||||
abstract setAddEditCipherInfo(value: AddEditCipherInfo, userId: UserId): Promise<void>;
|
||||
/**
|
||||
* Returns user ciphers re-encrypted with the new user key.
|
||||
* @param originalUserKey the original user key
|
||||
@@ -177,11 +207,11 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
* @throws Error if new user key is null
|
||||
* @returns a list of user ciphers that have been re-encrypted with the new user key
|
||||
*/
|
||||
getRotatedData: (
|
||||
abstract getRotatedData(
|
||||
originalUserKey: UserKey,
|
||||
newUserKey: UserKey,
|
||||
userId: UserId,
|
||||
) => Promise<CipherWithIdRequest[]>;
|
||||
getNextCardCipher: () => Promise<CipherView>;
|
||||
getNextIdentityCipher: () => Promise<CipherView>;
|
||||
): Promise<CipherWithIdRequest[]>;
|
||||
abstract getNextCardCipher(userId: UserId): Promise<CipherView>;
|
||||
abstract getNextIdentityCipher(userId: UserId): Promise<CipherView>;
|
||||
}
|
||||
|
||||
@@ -382,8 +382,16 @@ describe("Cipher Service", () => {
|
||||
Cipher1: cipher1,
|
||||
Cipher2: cipher2,
|
||||
});
|
||||
cipherService.cipherViews$ = decryptedCiphers.pipe(map((ciphers) => Object.values(ciphers)));
|
||||
cipherService.failedToDecryptCiphers$ = failedCiphers = new BehaviorSubject<CipherView[]>([]);
|
||||
jest
|
||||
.spyOn(cipherService, "cipherViews$")
|
||||
.mockImplementation((userId: UserId) =>
|
||||
decryptedCiphers.pipe(map((ciphers) => Object.values(ciphers))),
|
||||
);
|
||||
|
||||
failedCiphers = new BehaviorSubject<CipherView[]>([]);
|
||||
jest
|
||||
.spyOn(cipherService, "failedToDecryptCiphers$")
|
||||
.mockImplementation((userId: UserId) => failedCiphers);
|
||||
|
||||
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
|
||||
encryptedKey = new EncString("Re-encrypted Cipher Key");
|
||||
|
||||
@@ -37,7 +37,7 @@ import Domain from "../../platform/models/domain/domain-base";
|
||||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ActiveUserState, StateProvider } from "../../platform/state";
|
||||
import { StateProvider } from "../../platform/state";
|
||||
import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid";
|
||||
import { OrgKey, UserKey } from "../../types/key";
|
||||
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
||||
@@ -97,33 +97,6 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
*/
|
||||
private forceCipherViews$: Subject<CipherView[]> = new Subject<CipherView[]>();
|
||||
|
||||
localData$: Observable<Record<CipherId, LocalData>>;
|
||||
ciphers$: Observable<Record<CipherId, CipherData>>;
|
||||
|
||||
/**
|
||||
* Observable that emits an array of decrypted ciphers for the active user.
|
||||
* This observable will not emit until the encrypted ciphers have either been loaded from state or after sync.
|
||||
*
|
||||
* A `null` value indicates that the latest encrypted ciphers have not been decrypted yet and that
|
||||
* decryption is in progress. The latest decrypted ciphers will be emitted once decryption is complete.
|
||||
*
|
||||
*/
|
||||
cipherViews$: Observable<CipherView[] | null>;
|
||||
addEditCipherInfo$: Observable<AddEditCipherInfo>;
|
||||
|
||||
/**
|
||||
* Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed.
|
||||
*
|
||||
* An empty array indicates that all ciphers were successfully decrypted.
|
||||
*/
|
||||
failedToDecryptCiphers$: Observable<CipherView[]>;
|
||||
|
||||
private localDataState: ActiveUserState<Record<CipherId, LocalData>>;
|
||||
private encryptedCiphersState: ActiveUserState<Record<CipherId, CipherData>>;
|
||||
private decryptedCiphersState: ActiveUserState<Record<CipherId, CipherView>>;
|
||||
private failedToDecryptCiphersState: ActiveUserState<CipherView[]>;
|
||||
private addEditCipherInfoState: ActiveUserState<AddEditCipherInfo>;
|
||||
|
||||
constructor(
|
||||
private keyService: KeyService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
@@ -138,30 +111,49 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
private configService: ConfigService,
|
||||
private stateProvider: StateProvider,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY);
|
||||
this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS);
|
||||
this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS);
|
||||
this.failedToDecryptCiphersState = this.stateProvider.getActive(FAILED_DECRYPTED_CIPHERS);
|
||||
this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY);
|
||||
) {}
|
||||
|
||||
this.localData$ = this.localDataState.state$.pipe(map((data) => data ?? {}));
|
||||
this.ciphers$ = this.encryptedCiphersState.state$.pipe(map((ciphers) => ciphers ?? {}));
|
||||
localData$(userId: UserId): Observable<Record<CipherId, LocalData>> {
|
||||
return this.localDataState(userId).state$.pipe(map((data) => data ?? {}));
|
||||
}
|
||||
|
||||
// Decrypted ciphers depend on both ciphers and local data and need to be updated when either changes
|
||||
this.cipherViews$ = combineLatest([this.encryptedCiphersState.state$, this.localData$]).pipe(
|
||||
/**
|
||||
* Observable that emits an object of encrypted ciphers for the active user.
|
||||
*/
|
||||
ciphers$(userId: UserId): Observable<Record<CipherId, CipherData>> {
|
||||
return this.encryptedCiphersState(userId).state$.pipe(map((ciphers) => ciphers ?? {}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable that emits an array of decrypted ciphers for the active user.
|
||||
* This observable will not emit until the encrypted ciphers have either been loaded from state or after sync.
|
||||
*
|
||||
* A `null` value indicates that the latest encrypted ciphers have not been decrypted yet and that
|
||||
* decryption is in progress. The latest decrypted ciphers will be emitted once decryption is complete.
|
||||
*/
|
||||
cipherViews$(userId: UserId): Observable<CipherView[] | null> {
|
||||
return combineLatest([this.encryptedCiphersState(userId).state$, this.localData$(userId)]).pipe(
|
||||
filter(([ciphers]) => ciphers != null), // Skip if ciphers haven't been loaded yor synced yet
|
||||
switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted())),
|
||||
switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted(userId))),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
}
|
||||
|
||||
this.failedToDecryptCiphers$ = this.failedToDecryptCiphersState.state$.pipe(
|
||||
addEditCipherInfo$(userId: UserId): Observable<AddEditCipherInfo> {
|
||||
return this.addEditCipherInfoState(userId).state$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed.
|
||||
*
|
||||
* An empty array indicates that all ciphers were successfully decrypted.
|
||||
*/
|
||||
failedToDecryptCiphers$(userId: UserId): Observable<CipherView[]> {
|
||||
return this.failedToDecryptCiphersState(userId).state$.pipe(
|
||||
filter((ciphers) => ciphers != null),
|
||||
switchMap((ciphers) => merge(this.forceCipherViews$, of(ciphers))),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
this.addEditCipherInfo$ = this.addEditCipherInfoState.state$;
|
||||
}
|
||||
|
||||
async setDecryptedCipherCache(value: CipherView[], userId: UserId) {
|
||||
@@ -212,7 +204,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
): Promise<Cipher> {
|
||||
if (model.id != null) {
|
||||
if (originalCipher == null) {
|
||||
originalCipher = await this.get(model.id);
|
||||
originalCipher = await this.get(model.id, userId);
|
||||
}
|
||||
if (originalCipher != null) {
|
||||
await this.updateModelfromExistingCipher(model, originalCipher, userId);
|
||||
@@ -366,22 +358,22 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return ph;
|
||||
}
|
||||
|
||||
async get(id: string): Promise<Cipher> {
|
||||
const ciphers = await firstValueFrom(this.ciphers$);
|
||||
async get(id: string, userId: UserId): Promise<Cipher> {
|
||||
const ciphers = await firstValueFrom(this.ciphers$(userId));
|
||||
// eslint-disable-next-line
|
||||
if (ciphers == null || !ciphers.hasOwnProperty(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const localData = await firstValueFrom(this.localData$);
|
||||
const localData = await firstValueFrom(this.localData$(userId));
|
||||
const cipherId = id as CipherId;
|
||||
|
||||
return new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null);
|
||||
}
|
||||
|
||||
async getAll(): Promise<Cipher[]> {
|
||||
const localData = await firstValueFrom(this.localData$);
|
||||
const ciphers = await firstValueFrom(this.ciphers$);
|
||||
async getAll(userId: UserId): Promise<Cipher[]> {
|
||||
const localData = await firstValueFrom(this.localData$(userId));
|
||||
const ciphers = await firstValueFrom(this.ciphers$(userId));
|
||||
const response: Cipher[] = [];
|
||||
for (const id in ciphers) {
|
||||
// eslint-disable-next-line
|
||||
@@ -399,33 +391,27 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
* @deprecated Use `cipherViews$` observable instead
|
||||
*/
|
||||
@sequentialize(() => "getAllDecrypted")
|
||||
async getAllDecrypted(): Promise<CipherView[]> {
|
||||
const decCiphers = await this.getDecryptedCiphers();
|
||||
async getAllDecrypted(userId: UserId): Promise<CipherView[]> {
|
||||
const decCiphers = await this.getDecryptedCiphers(userId);
|
||||
if (decCiphers != null && decCiphers.length !== 0) {
|
||||
await this.reindexCiphers();
|
||||
return await this.getDecryptedCiphers();
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
|
||||
if (activeUserId == null) {
|
||||
return [];
|
||||
await this.reindexCiphers(userId);
|
||||
return await this.getDecryptedCiphers(userId);
|
||||
}
|
||||
|
||||
const [newDecCiphers, failedCiphers] = await this.decryptCiphers(
|
||||
await this.getAll(),
|
||||
activeUserId,
|
||||
await this.getAll(userId),
|
||||
userId,
|
||||
);
|
||||
|
||||
await this.setDecryptedCipherCache(newDecCiphers, activeUserId);
|
||||
await this.setFailedDecryptedCiphers(failedCiphers, activeUserId);
|
||||
await this.setDecryptedCipherCache(newDecCiphers, userId);
|
||||
await this.setFailedDecryptedCiphers(failedCiphers, userId);
|
||||
|
||||
return newDecCiphers;
|
||||
}
|
||||
|
||||
private async getDecryptedCiphers() {
|
||||
private async getDecryptedCiphers(userId: UserId) {
|
||||
return Object.values(
|
||||
await firstValueFrom(this.decryptedCiphersState.state$.pipe(map((c) => c ?? {}))),
|
||||
await firstValueFrom(this.decryptedCiphersState(userId).state$.pipe(map((c) => c ?? {}))),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -491,18 +477,21 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
private async reindexCiphers() {
|
||||
const userId = await this.stateService.getUserId();
|
||||
private async reindexCiphers(userId: UserId) {
|
||||
const reindexRequired =
|
||||
this.searchService != null &&
|
||||
((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId;
|
||||
if (reindexRequired) {
|
||||
await this.searchService.indexCiphers(await this.getDecryptedCiphers(), userId);
|
||||
await this.searchService.indexCiphers(await this.getDecryptedCiphers(userId), userId);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllDecryptedForGrouping(groupingId: string, folder = true): Promise<CipherView[]> {
|
||||
const ciphers = await this.getAllDecrypted();
|
||||
async getAllDecryptedForGrouping(
|
||||
groupingId: string,
|
||||
userId: UserId,
|
||||
folder = true,
|
||||
): Promise<CipherView[]> {
|
||||
const ciphers = await this.getAllDecrypted(userId);
|
||||
|
||||
return ciphers.filter((cipher) => {
|
||||
if (cipher.isDeleted) {
|
||||
@@ -524,10 +513,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
|
||||
async getAllDecryptedForUrl(
|
||||
url: string,
|
||||
userId: UserId,
|
||||
includeOtherTypes?: CipherType[],
|
||||
defaultMatch: UriMatchStrategySetting = null,
|
||||
): Promise<CipherView[]> {
|
||||
const ciphers = await this.getAllDecrypted();
|
||||
const ciphers = await this.getAllDecrypted(userId);
|
||||
return await this.filterCiphersForUrl(ciphers, url, includeOtherTypes, defaultMatch);
|
||||
}
|
||||
|
||||
@@ -569,8 +559,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
});
|
||||
}
|
||||
|
||||
private async getAllDecryptedCiphersOfType(type: CipherType[]): Promise<CipherView[]> {
|
||||
const ciphers = await this.getAllDecrypted();
|
||||
private async getAllDecryptedCiphersOfType(
|
||||
type: CipherType[],
|
||||
userId: UserId,
|
||||
): Promise<CipherView[]> {
|
||||
const ciphers = await this.getAllDecrypted(userId);
|
||||
return ciphers
|
||||
.filter((cipher) => cipher.deletedDate == null && type.includes(cipher.type))
|
||||
.sort((a, b) => this.sortCiphersByLastUsedThenName(a, b));
|
||||
@@ -613,23 +606,31 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return decCiphers;
|
||||
}
|
||||
|
||||
async getLastUsedForUrl(url: string, autofillOnPageLoad = false): Promise<CipherView> {
|
||||
return this.getCipherForUrl(url, true, false, autofillOnPageLoad);
|
||||
async getLastUsedForUrl(
|
||||
url: string,
|
||||
userId: UserId,
|
||||
autofillOnPageLoad = false,
|
||||
): Promise<CipherView> {
|
||||
return this.getCipherForUrl(url, userId, true, false, autofillOnPageLoad);
|
||||
}
|
||||
|
||||
async getLastLaunchedForUrl(url: string, autofillOnPageLoad = false): Promise<CipherView> {
|
||||
return this.getCipherForUrl(url, false, true, autofillOnPageLoad);
|
||||
async getLastLaunchedForUrl(
|
||||
url: string,
|
||||
userId: UserId,
|
||||
autofillOnPageLoad = false,
|
||||
): Promise<CipherView> {
|
||||
return this.getCipherForUrl(url, userId, false, true, autofillOnPageLoad);
|
||||
}
|
||||
|
||||
async getNextCipherForUrl(url: string): Promise<CipherView> {
|
||||
return this.getCipherForUrl(url, false, false, false);
|
||||
async getNextCipherForUrl(url: string, userId: UserId): Promise<CipherView> {
|
||||
return this.getCipherForUrl(url, userId, false, false, false);
|
||||
}
|
||||
|
||||
async getNextCardCipher(): Promise<CipherView> {
|
||||
async getNextCardCipher(userId: UserId): Promise<CipherView> {
|
||||
const cacheKey = "cardCiphers";
|
||||
|
||||
if (!this.sortedCiphersCache.isCached(cacheKey)) {
|
||||
const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Card]);
|
||||
const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Card], userId);
|
||||
if (!ciphers?.length) {
|
||||
return null;
|
||||
}
|
||||
@@ -640,11 +641,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return this.sortedCiphersCache.getNext(cacheKey);
|
||||
}
|
||||
|
||||
async getNextIdentityCipher(): Promise<CipherView> {
|
||||
async getNextIdentityCipher(userId: UserId): Promise<CipherView> {
|
||||
const cacheKey = "identityCiphers";
|
||||
|
||||
if (!this.sortedCiphersCache.isCached(cacheKey)) {
|
||||
const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Identity]);
|
||||
const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Identity], userId);
|
||||
if (!ciphers?.length) {
|
||||
return null;
|
||||
}
|
||||
@@ -659,9 +660,8 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
this.sortedCiphersCache.updateLastUsedIndex(url);
|
||||
}
|
||||
|
||||
async updateLastUsedDate(id: string): Promise<void> {
|
||||
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
let ciphersLocalData = await firstValueFrom(this.localData$);
|
||||
async updateLastUsedDate(id: string, userId: UserId): Promise<void> {
|
||||
let ciphersLocalData = await firstValueFrom(this.localData$(userId));
|
||||
|
||||
if (!ciphersLocalData) {
|
||||
ciphersLocalData = {};
|
||||
@@ -676,9 +676,9 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
};
|
||||
}
|
||||
|
||||
await this.localDataState.update(() => ciphersLocalData);
|
||||
await this.localDataState(userId).update(() => ciphersLocalData);
|
||||
|
||||
const decryptedCipherCache = await this.getDecryptedCiphers();
|
||||
const decryptedCipherCache = await this.getDecryptedCiphers(userId);
|
||||
if (!decryptedCipherCache) {
|
||||
return;
|
||||
}
|
||||
@@ -693,9 +693,8 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.setDecryptedCiphers(decryptedCipherCache, userId);
|
||||
}
|
||||
|
||||
async updateLastLaunchedDate(id: string): Promise<void> {
|
||||
const userId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
let ciphersLocalData = await firstValueFrom(this.localData$);
|
||||
async updateLastLaunchedDate(id: string, userId: UserId): Promise<void> {
|
||||
let ciphersLocalData = await firstValueFrom(this.localData$(userId));
|
||||
|
||||
if (!ciphersLocalData) {
|
||||
ciphersLocalData = {};
|
||||
@@ -707,9 +706,9 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
lastUsedDate: currentTime,
|
||||
};
|
||||
|
||||
await this.localDataState.update(() => ciphersLocalData);
|
||||
await this.localDataState(userId).update(() => ciphersLocalData);
|
||||
|
||||
const decryptedCipherCache = await this.getDecryptedCiphers();
|
||||
const decryptedCipherCache = await this.getDecryptedCiphers(userId);
|
||||
if (!decryptedCipherCache) {
|
||||
return;
|
||||
}
|
||||
@@ -914,13 +913,13 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return new Cipher(cData);
|
||||
}
|
||||
|
||||
async saveCollectionsWithServer(cipher: Cipher): Promise<Cipher> {
|
||||
async saveCollectionsWithServer(cipher: Cipher, userId: UserId): Promise<Cipher> {
|
||||
const request = new CipherCollectionsRequest(cipher.collectionIds);
|
||||
const response = await this.apiService.putCipherCollections(cipher.id, request);
|
||||
// The response will now check for an unavailable value. This value determines whether
|
||||
// the user still has Can Manage access to the item after updating.
|
||||
if (response.unavailable) {
|
||||
await this.delete(cipher.id);
|
||||
await this.delete(cipher.id, userId);
|
||||
return;
|
||||
}
|
||||
const data = new CipherData(response.cipher);
|
||||
@@ -944,6 +943,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
*/
|
||||
async bulkUpdateCollectionsWithServer(
|
||||
orgId: OrganizationId,
|
||||
userId: UserId,
|
||||
cipherIds: CipherId[],
|
||||
collectionIds: CollectionId[],
|
||||
removeCollections: boolean = false,
|
||||
@@ -958,7 +958,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false);
|
||||
|
||||
// Update the local state
|
||||
const ciphers = await firstValueFrom(this.ciphers$);
|
||||
const ciphers = await firstValueFrom(this.ciphers$(userId));
|
||||
|
||||
for (const id of cipherIds) {
|
||||
const cipher = ciphers[id];
|
||||
@@ -975,7 +975,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.encryptedCiphersState.update(() => ciphers);
|
||||
await this.encryptedCiphersState(userId).update(() => ciphers);
|
||||
}
|
||||
|
||||
async upsert(cipher: CipherData | CipherData[]): Promise<Record<CipherId, CipherData>> {
|
||||
@@ -1016,10 +1016,10 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.clearCache(userId);
|
||||
}
|
||||
|
||||
async moveManyWithServer(ids: string[], folderId: string): Promise<any> {
|
||||
async moveManyWithServer(ids: string[], folderId: string, userId: UserId): Promise<any> {
|
||||
await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId));
|
||||
|
||||
let ciphers = await firstValueFrom(this.ciphers$);
|
||||
let ciphers = await firstValueFrom(this.ciphers$(userId));
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
@@ -1032,11 +1032,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
});
|
||||
|
||||
await this.clearCache();
|
||||
await this.encryptedCiphersState.update(() => ciphers);
|
||||
await this.encryptedCiphersState(userId).update(() => ciphers);
|
||||
}
|
||||
|
||||
async delete(id: string | string[]): Promise<any> {
|
||||
const ciphers = await firstValueFrom(this.ciphers$);
|
||||
async delete(id: string | string[], userId: UserId): Promise<any> {
|
||||
const ciphers = await firstValueFrom(this.ciphers$(userId));
|
||||
if (ciphers == null) {
|
||||
return;
|
||||
}
|
||||
@@ -1054,35 +1054,36 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.encryptedCiphersState.update(() => ciphers);
|
||||
await this.encryptedCiphersState(userId).update(() => ciphers);
|
||||
}
|
||||
|
||||
async deleteWithServer(id: string, asAdmin = false): Promise<any> {
|
||||
async deleteWithServer(id: string, userId: UserId, asAdmin = false): Promise<any> {
|
||||
if (asAdmin) {
|
||||
await this.apiService.deleteCipherAdmin(id);
|
||||
} else {
|
||||
await this.apiService.deleteCipher(id);
|
||||
}
|
||||
|
||||
await this.delete(id);
|
||||
await this.delete(id, userId);
|
||||
}
|
||||
|
||||
async deleteManyWithServer(ids: string[], asAdmin = false): Promise<any> {
|
||||
async deleteManyWithServer(ids: string[], userId: UserId, asAdmin = false): Promise<any> {
|
||||
const request = new CipherBulkDeleteRequest(ids);
|
||||
if (asAdmin) {
|
||||
await this.apiService.deleteManyCiphersAdmin(request);
|
||||
} else {
|
||||
await this.apiService.deleteManyCiphers(request);
|
||||
}
|
||||
await this.delete(ids);
|
||||
await this.delete(ids, userId);
|
||||
}
|
||||
|
||||
async deleteAttachment(
|
||||
id: string,
|
||||
revisionDate: string,
|
||||
attachmentId: string,
|
||||
userId: UserId,
|
||||
): Promise<CipherData> {
|
||||
let ciphers = await firstValueFrom(this.ciphers$);
|
||||
let ciphers = await firstValueFrom(this.ciphers$(userId));
|
||||
const cipherId = id as CipherId;
|
||||
// eslint-disable-next-line
|
||||
if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[cipherId].attachments == null) {
|
||||
@@ -1100,7 +1101,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
ciphers[cipherId].revisionDate = revisionDate;
|
||||
|
||||
await this.clearCache();
|
||||
await this.encryptedCiphersState.update(() => {
|
||||
await this.encryptedCiphersState(userId).update(() => {
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
@@ -1110,7 +1111,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return ciphers[cipherId];
|
||||
}
|
||||
|
||||
async deleteAttachmentWithServer(id: string, attachmentId: string): Promise<CipherData> {
|
||||
async deleteAttachmentWithServer(
|
||||
id: string,
|
||||
attachmentId: string,
|
||||
userId: UserId,
|
||||
): Promise<CipherData> {
|
||||
let cipherResponse = null;
|
||||
try {
|
||||
cipherResponse = await this.apiService.deleteCipherAttachment(id, attachmentId);
|
||||
@@ -1119,7 +1124,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
const cipherData = CipherData.fromJSON(cipherResponse?.cipher);
|
||||
|
||||
return await this.deleteAttachment(id, cipherData.revisionDate, attachmentId);
|
||||
return await this.deleteAttachment(id, cipherData.revisionDate, attachmentId, userId);
|
||||
}
|
||||
|
||||
sortCiphersByLastUsed(a: CipherView, b: CipherView): number {
|
||||
@@ -1192,8 +1197,8 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
};
|
||||
}
|
||||
|
||||
async softDelete(id: string | string[]): Promise<any> {
|
||||
let ciphers = await firstValueFrom(this.ciphers$);
|
||||
async softDelete(id: string | string[], userId: UserId): Promise<any> {
|
||||
let ciphers = await firstValueFrom(this.ciphers$(userId));
|
||||
if (ciphers == null) {
|
||||
return;
|
||||
}
|
||||
@@ -1212,7 +1217,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.encryptedCiphersState.update(() => {
|
||||
await this.encryptedCiphersState(userId).update(() => {
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
@@ -1220,17 +1225,17 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
});
|
||||
}
|
||||
|
||||
async softDeleteWithServer(id: string, asAdmin = false): Promise<any> {
|
||||
async softDeleteWithServer(id: string, userId: UserId, asAdmin = false): Promise<any> {
|
||||
if (asAdmin) {
|
||||
await this.apiService.putDeleteCipherAdmin(id);
|
||||
} else {
|
||||
await this.apiService.putDeleteCipher(id);
|
||||
}
|
||||
|
||||
await this.softDelete(id);
|
||||
await this.softDelete(id, userId);
|
||||
}
|
||||
|
||||
async softDeleteManyWithServer(ids: string[], asAdmin = false): Promise<any> {
|
||||
async softDeleteManyWithServer(ids: string[], userId: UserId, asAdmin = false): Promise<any> {
|
||||
const request = new CipherBulkDeleteRequest(ids);
|
||||
if (asAdmin) {
|
||||
await this.apiService.putDeleteManyCiphersAdmin(request);
|
||||
@@ -1238,13 +1243,14 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.apiService.putDeleteManyCiphers(request);
|
||||
}
|
||||
|
||||
await this.softDelete(ids);
|
||||
await this.softDelete(ids, userId);
|
||||
}
|
||||
|
||||
async restore(
|
||||
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
|
||||
userId: UserId,
|
||||
) {
|
||||
let ciphers = await firstValueFrom(this.ciphers$);
|
||||
let ciphers = await firstValueFrom(this.ciphers$(userId));
|
||||
if (ciphers == null) {
|
||||
return;
|
||||
}
|
||||
@@ -1265,7 +1271,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
|
||||
await this.clearCache();
|
||||
await this.encryptedCiphersState.update(() => {
|
||||
await this.encryptedCiphersState(userId).update(() => {
|
||||
if (ciphers == null) {
|
||||
ciphers = {};
|
||||
}
|
||||
@@ -1273,7 +1279,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
});
|
||||
}
|
||||
|
||||
async restoreWithServer(id: string, asAdmin = false): Promise<any> {
|
||||
async restoreWithServer(id: string, userId: UserId, asAdmin = false): Promise<any> {
|
||||
let response;
|
||||
if (asAdmin) {
|
||||
response = await this.apiService.putRestoreCipherAdmin(id);
|
||||
@@ -1281,14 +1287,14 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
response = await this.apiService.putRestoreCipher(id);
|
||||
}
|
||||
|
||||
await this.restore({ id: id, revisionDate: response.revisionDate });
|
||||
await this.restore({ id: id, revisionDate: response.revisionDate }, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* No longer using an asAdmin Param. Org Vault bulkRestore will assess if an item is unassigned or editable
|
||||
* The Org Vault will pass those ids an array as well as the orgId when calling bulkRestore
|
||||
*/
|
||||
async restoreManyWithServer(ids: string[], orgId: string = null): Promise<void> {
|
||||
async restoreManyWithServer(ids: string[], userId: UserId, orgId: string = null): Promise<void> {
|
||||
let response;
|
||||
|
||||
if (orgId) {
|
||||
@@ -1303,7 +1309,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
for (const cipher of response.data) {
|
||||
restores.push({ id: cipher.id, revisionDate: cipher.revisionDate });
|
||||
}
|
||||
await this.restore(restores);
|
||||
await this.restore(restores, userId);
|
||||
}
|
||||
|
||||
async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise<UserKey | OrgKey> {
|
||||
@@ -1313,8 +1319,8 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async setAddEditCipherInfo(value: AddEditCipherInfo) {
|
||||
await this.addEditCipherInfoState.update(() => value, {
|
||||
async setAddEditCipherInfo(value: AddEditCipherInfo, userId: UserId) {
|
||||
await this.addEditCipherInfoState(userId).update(() => value, {
|
||||
shouldUpdate: (current) => !(current == null && value == null),
|
||||
});
|
||||
}
|
||||
@@ -1333,8 +1339,8 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
|
||||
let encryptedCiphers: CipherWithIdRequest[] = [];
|
||||
|
||||
const ciphers = await firstValueFrom(this.cipherViews$);
|
||||
const failedCiphers = await firstValueFrom(this.failedToDecryptCiphers$);
|
||||
const ciphers = await firstValueFrom(this.cipherViews$(userId));
|
||||
const failedCiphers = await firstValueFrom(this.failedToDecryptCiphers$(userId));
|
||||
if (!ciphers) {
|
||||
return encryptedCiphers;
|
||||
}
|
||||
@@ -1357,6 +1363,41 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
return encryptedCiphers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a SingleUserState
|
||||
*/
|
||||
private localDataState(userId: UserId) {
|
||||
return this.stateProvider.getUser(userId, LOCAL_DATA_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a SingleUserState for the encrypted ciphers
|
||||
*/
|
||||
private encryptedCiphersState(userId: UserId) {
|
||||
return this.stateProvider.getUser(userId, ENCRYPTED_CIPHERS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a SingleUserState for the decrypted ciphers
|
||||
*/
|
||||
private decryptedCiphersState(userId: UserId) {
|
||||
return this.stateProvider.getUser(userId, DECRYPTED_CIPHERS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a SingleUserState for the add/edit cipher info
|
||||
*/
|
||||
private addEditCipherInfoState(userId: UserId) {
|
||||
return this.stateProvider.getUser(userId, ADD_EDIT_CIPHER_INFO_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a SingleUserState for the failed to decrypt ciphers
|
||||
*/
|
||||
private failedToDecryptCiphersState(userId: UserId) {
|
||||
return this.stateProvider.getUser(userId, FAILED_DECRYPTED_CIPHERS);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
// In the case of a cipher that is being shared with an organization, we want to decrypt the
|
||||
@@ -1660,6 +1701,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
|
||||
private async getCipherForUrl(
|
||||
url: string,
|
||||
userId: UserId,
|
||||
lastUsed: boolean,
|
||||
lastLaunched: boolean,
|
||||
autofillOnPageLoad: boolean,
|
||||
@@ -1667,7 +1709,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
const cacheKey = autofillOnPageLoad ? "autofillOnPageLoad-" + url : url;
|
||||
|
||||
if (!this.sortedCiphersCache.isCached(cacheKey)) {
|
||||
let ciphers = await this.getAllDecryptedForUrl(url);
|
||||
let ciphers = await this.getAllDecryptedForUrl(url, userId);
|
||||
if (!ciphers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
||||
});
|
||||
|
||||
// Items in a deleted folder are re-assigned to "No Folder"
|
||||
const ciphers = await this.cipherService.getAll();
|
||||
const ciphers = await this.cipherService.getAll(userId);
|
||||
if (ciphers != null) {
|
||||
const updates: Cipher[] = [];
|
||||
for (const cId in ciphers) {
|
||||
|
||||
@@ -103,7 +103,7 @@ export class DefaultUserAsymmetricKeysRegenerationService
|
||||
}
|
||||
|
||||
// The private isn't decryptable, check to see if we can decrypt something with the userKey.
|
||||
const userKeyCanDecrypt = await this.userKeyCanDecrypt(userKey);
|
||||
const userKeyCanDecrypt = await this.userKeyCanDecrypt(userKey, userId);
|
||||
if (userKeyCanDecrypt) {
|
||||
this.logService.info(
|
||||
"[UserAsymmetricKeyRegeneration] User Asymmetric Key decryption failure detected, attempting regeneration.",
|
||||
@@ -155,8 +155,8 @@ export class DefaultUserAsymmetricKeysRegenerationService
|
||||
);
|
||||
}
|
||||
|
||||
private async userKeyCanDecrypt(userKey: UserKey): Promise<boolean> {
|
||||
const ciphers = await this.cipherService.getAll();
|
||||
private async userKeyCanDecrypt(userKey: UserKey, userId: UserId): Promise<boolean> {
|
||||
const ciphers = await this.cipherService.getAll(userId);
|
||||
const cipher = ciphers.find((cipher) => cipher.organizationId == null);
|
||||
|
||||
if (cipher != null) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import * as papa from "papaparse";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
@@ -32,8 +33,6 @@ export class IndividualVaultExportService
|
||||
extends BaseVaultExportService
|
||||
implements IndividualVaultExportServiceAbstraction
|
||||
{
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
|
||||
constructor(
|
||||
private folderService: FolderService,
|
||||
private cipherService: CipherService,
|
||||
@@ -63,7 +62,7 @@ export class IndividualVaultExportService
|
||||
let decFolders: FolderView[] = [];
|
||||
let decCiphers: CipherView[] = [];
|
||||
const promises = [];
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
promises.push(
|
||||
firstValueFrom(this.folderService.folderViews$(activeUserId)).then((folders) => {
|
||||
@@ -72,7 +71,7 @@ export class IndividualVaultExportService
|
||||
);
|
||||
|
||||
promises.push(
|
||||
this.cipherService.getAllDecrypted().then((ciphers) => {
|
||||
this.cipherService.getAllDecrypted(activeUserId).then((ciphers) => {
|
||||
decCiphers = ciphers.filter((f) => f.deletedDate == null);
|
||||
}),
|
||||
);
|
||||
@@ -90,7 +89,7 @@ export class IndividualVaultExportService
|
||||
let folders: Folder[] = [];
|
||||
let ciphers: Cipher[] = [];
|
||||
const promises = [];
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
promises.push(
|
||||
firstValueFrom(this.folderService.folders$(activeUserId)).then((f) => {
|
||||
@@ -99,7 +98,7 @@ export class IndividualVaultExportService
|
||||
);
|
||||
|
||||
promises.push(
|
||||
this.cipherService.getAll().then((c) => {
|
||||
this.cipherService.getAll(activeUserId).then((c) => {
|
||||
ciphers = c.filter((f) => f.deletedDate == null);
|
||||
}),
|
||||
);
|
||||
@@ -107,7 +106,7 @@ export class IndividualVaultExportService
|
||||
await Promise.all(promises);
|
||||
|
||||
const userKey = await this.keyService.getUserKeyWithLegacySupport(
|
||||
await firstValueFrom(this.activeUserId$),
|
||||
await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
|
||||
);
|
||||
const encKeyValidation = await this.encryptService.encrypt(Utils.newGuid(), userKey);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import * as papa from "papaparse";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionService,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
@@ -94,9 +95,7 @@ export class OrganizationVaultExportService
|
||||
const decCollections: CollectionView[] = [];
|
||||
const decCiphers: CipherView[] = [];
|
||||
const promises = [];
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
promises.push(
|
||||
this.apiService.getOrganizationExport(organizationId).then((exportData) => {
|
||||
@@ -184,6 +183,7 @@ export class OrganizationVaultExportService
|
||||
let allDecCiphers: CipherView[] = [];
|
||||
let decCollections: CollectionView[] = [];
|
||||
const promises = [];
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
promises.push(
|
||||
this.collectionService.getAllDecrypted().then(async (collections) => {
|
||||
@@ -192,7 +192,7 @@ export class OrganizationVaultExportService
|
||||
);
|
||||
|
||||
promises.push(
|
||||
this.cipherService.getAllDecrypted().then((ciphers) => {
|
||||
this.cipherService.getAllDecrypted(activeUserId).then((ciphers) => {
|
||||
allDecCiphers = ciphers;
|
||||
}),
|
||||
);
|
||||
@@ -216,6 +216,7 @@ export class OrganizationVaultExportService
|
||||
let allCiphers: Cipher[] = [];
|
||||
let encCollections: Collection[] = [];
|
||||
const promises = [];
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
promises.push(
|
||||
this.collectionService.getAll().then((collections) => {
|
||||
@@ -224,7 +225,7 @@ export class OrganizationVaultExportService
|
||||
);
|
||||
|
||||
promises.push(
|
||||
this.cipherService.getAll().then((ciphers) => {
|
||||
this.cipherService.getAll(activeUserId).then((ciphers) => {
|
||||
allCiphers = ciphers;
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -110,7 +110,7 @@ describe("CipherAttachmentsComponent", () => {
|
||||
it("fetches cipherView using `cipherId`", async () => {
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(cipherServiceGet).toHaveBeenCalledWith("5555-444-3333");
|
||||
expect(cipherServiceGet).toHaveBeenCalledWith("5555-444-3333", mockUserId);
|
||||
expect(component.cipher).toEqual(cipherView);
|
||||
});
|
||||
|
||||
|
||||
@@ -21,10 +21,11 @@ import {
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -118,10 +119,8 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.cipherDomain = await this.cipherService.get(this.cipherId);
|
||||
this.activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.cipherDomain = await this.cipherService.get(this.cipherId, this.activeUserId);
|
||||
this.cipher = await this.cipherDomain.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId),
|
||||
);
|
||||
|
||||
@@ -2,12 +2,16 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { mockAccountServiceWith } from "../../../../../../common/spec";
|
||||
|
||||
import { DeleteAttachmentComponent } from "./delete-attachment.component";
|
||||
|
||||
describe("DeleteAttachmentComponent", () => {
|
||||
@@ -42,6 +46,7 @@ describe("DeleteAttachmentComponent", () => {
|
||||
},
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{ provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) },
|
||||
],
|
||||
})
|
||||
.overrideProvider(DialogService, {
|
||||
@@ -90,7 +95,11 @@ describe("DeleteAttachmentComponent", () => {
|
||||
});
|
||||
|
||||
// Called with cipher id and attachment id
|
||||
expect(deleteAttachmentWithServer).toHaveBeenCalledWith("5555-444-3333", "222-3333-4444");
|
||||
expect(deleteAttachmentWithServer).toHaveBeenCalledWith(
|
||||
"5555-444-3333",
|
||||
"222-3333-4444",
|
||||
"UserId",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows toast message on successful deletion", async () => {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -36,6 +39,7 @@ export class DeleteAttachmentComponent {
|
||||
private cipherService: CipherService,
|
||||
private logService: LogService,
|
||||
private dialogService: DialogService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
delete = async () => {
|
||||
@@ -50,7 +54,19 @@ export class DeleteAttachmentComponent {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.cipherService.deleteAttachmentWithServer(this.cipherId, this.attachment.id);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
|
||||
if (activeUserId == null) {
|
||||
throw new Error("An active user is expected while deleting an attachment.");
|
||||
}
|
||||
|
||||
await this.cipherService.deleteAttachmentWithServer(
|
||||
this.cipherId,
|
||||
this.attachment.id,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/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";
|
||||
@@ -34,14 +35,12 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService {
|
||||
private collectionService: CollectionService = inject(CollectionService);
|
||||
private accountService = inject(AccountService);
|
||||
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
|
||||
async buildConfig(
|
||||
mode: CipherFormMode,
|
||||
cipherId?: CipherId,
|
||||
cipherType?: CipherType,
|
||||
): Promise<CipherFormConfig> {
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
const [organizations, collections, allowPersonalOwnership, folders, cipher] =
|
||||
await firstValueFrom(
|
||||
@@ -62,7 +61,7 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService {
|
||||
),
|
||||
),
|
||||
),
|
||||
this.getCipher(cipherId),
|
||||
this.getCipher(activeUserId, cipherId),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -94,10 +93,10 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService {
|
||||
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
|
||||
.pipe(map((p) => !p));
|
||||
|
||||
private getCipher(id?: CipherId): Promise<Cipher | null> {
|
||||
private getCipher(userId: UserId, id?: CipherId): Promise<Cipher | null> {
|
||||
if (id == null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.cipherService.get(id);
|
||||
return this.cipherService.get(id, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
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";
|
||||
@@ -23,9 +24,7 @@ export class DefaultCipherFormService implements CipherFormService {
|
||||
private apiService: ApiService = inject(ApiService);
|
||||
|
||||
async decryptCipher(cipher: Cipher): Promise<CipherView> {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
return await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
@@ -33,9 +32,7 @@ export class DefaultCipherFormService implements CipherFormService {
|
||||
|
||||
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 activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const encryptedCipher = await this.cipherService.encrypt(
|
||||
cipher,
|
||||
activeUserId,
|
||||
@@ -90,7 +87,10 @@ export class DefaultCipherFormService implements CipherFormService {
|
||||
// When using an admin config or the cipher was unassigned, update collections as an admin
|
||||
savedCipher = await this.cipherService.saveCollectionsWithServerAdmin(encryptedCipher);
|
||||
} else {
|
||||
savedCipher = await this.cipherService.saveCollectionsWithServer(encryptedCipher);
|
||||
savedCipher = await this.cipherService.saveCollectionsWithServer(
|
||||
encryptedCipher,
|
||||
activeUserId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
@@ -38,10 +41,12 @@ export class AutofillOptionsViewComponent {
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private cipherService: CipherService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async openWebsite(selectedUri: string) {
|
||||
await this.cipherService.updateLastLaunchedDate(this.cipherId);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.cipherService.updateLastLaunchedDate(this.cipherId, activeUserId);
|
||||
this.platformUtilsService.launchUri(selectedUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,6 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
private get selectedOrgId(): OrganizationId {
|
||||
return this.formGroup.getRawValue().selectedOrg || this.params.organizationId;
|
||||
}
|
||||
private activeUserId: UserId;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
@@ -193,10 +192,6 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
const onlyPersonalItems = this.params.ciphers.every((c) => c.organizationId == null);
|
||||
|
||||
if (this.selectedOrgId === MY_VAULT_ID || onlyPersonalItems) {
|
||||
@@ -253,12 +248,15 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
.filter((i) => i.organizationId)
|
||||
.map((i) => i.id as CipherId);
|
||||
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
// Move personal items to the organization
|
||||
if (this.personalItemsCount > 0) {
|
||||
await this.moveToOrganization(
|
||||
this.selectedOrgId,
|
||||
this.params.ciphers.filter((c) => c.organizationId == null),
|
||||
this.formGroup.controls.collections.value.map((i) => i.id as CollectionId),
|
||||
activeUserId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,8 +265,8 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
|
||||
// Update assigned collections for single org cipher or bulk update collections for multiple org ciphers
|
||||
await (isSingleOrgCipher
|
||||
? this.updateAssignedCollections(this.editableItems[0])
|
||||
: this.bulkUpdateCollections(cipherIds));
|
||||
? this.updateAssignedCollections(this.editableItems[0], activeUserId)
|
||||
: this.bulkUpdateCollections(cipherIds, activeUserId));
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -447,12 +445,13 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
organizationId: OrganizationId,
|
||||
shareableCiphers: CipherView[],
|
||||
selectedCollectionIds: CollectionId[],
|
||||
userId: UserId,
|
||||
) {
|
||||
await this.cipherService.shareManyWithServer(
|
||||
shareableCiphers,
|
||||
organizationId,
|
||||
selectedCollectionIds,
|
||||
this.activeUserId,
|
||||
userId,
|
||||
);
|
||||
|
||||
this.toastService.showToast({
|
||||
@@ -465,10 +464,11 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
});
|
||||
}
|
||||
|
||||
private async bulkUpdateCollections(cipherIds: CipherId[]) {
|
||||
private async bulkUpdateCollections(cipherIds: CipherId[], userId: UserId) {
|
||||
if (this.formGroup.controls.collections.value.length > 0) {
|
||||
await this.cipherService.bulkUpdateCollectionsWithServer(
|
||||
this.selectedOrgId,
|
||||
userId,
|
||||
cipherIds,
|
||||
this.formGroup.controls.collections.value.map((i) => i.id as CollectionId),
|
||||
false,
|
||||
@@ -483,6 +483,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
) {
|
||||
await this.cipherService.bulkUpdateCollectionsWithServer(
|
||||
this.selectedOrgId,
|
||||
userId,
|
||||
cipherIds,
|
||||
[this.params.activeCollection.id as CollectionId],
|
||||
true,
|
||||
@@ -490,14 +491,14 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
|
||||
}
|
||||
}
|
||||
|
||||
private async updateAssignedCollections(cipherView: CipherView) {
|
||||
private async updateAssignedCollections(cipherView: CipherView, userId: UserId) {
|
||||
const { collections } = this.formGroup.getRawValue();
|
||||
cipherView.collectionIds = collections.map((i) => i.id as CollectionId);
|
||||
const cipher = await this.cipherService.encrypt(cipherView, this.activeUserId);
|
||||
const cipher = await this.cipherService.encrypt(cipherView, userId);
|
||||
if (this.params.isSingleCipherAdmin) {
|
||||
await this.cipherService.saveCollectionsWithServerAdmin(cipher);
|
||||
} else {
|
||||
await this.cipherService.saveCollectionsWithServer(cipher);
|
||||
await this.cipherService.saveCollectionsWithServer(cipher, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user