1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-24 04:04:24 +00:00

Merge branch 'main' into auth/pm-8111/browser-refresh-login-component

This commit is contained in:
Alec Rippberger
2024-10-07 14:31:11 -05:00
86 changed files with 1088 additions and 574 deletions

View File

@@ -1,8 +1,9 @@
import { DOCUMENT } from "@angular/common";
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { NavigationEnd, Router } from "@angular/router";
import * as jq from "jquery";
import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs";
import { Subject, filter, firstValueFrom, map, takeUntil, timeout, catchError, of } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
@@ -19,7 +20,9 @@ import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broa
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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 { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { SyncService } from "@bitwarden/common/platform/sync";
@@ -30,6 +33,8 @@ import { DialogService, ToastOptions, ToastService } from "@bitwarden/components
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { BiometricStateService } from "@bitwarden/key-management";
import { flagEnabled } from "../utils/flags";
import { PolicyListService } from "./admin-console/core/policy-list.service";
import {
DisableSendPolicy,
@@ -85,7 +90,28 @@ export class AppComponent implements OnDestroy, OnInit {
private stateEventRunnerService: StateEventRunnerService,
private organizationService: InternalOrganizationServiceAbstraction,
private accountService: AccountService,
) {}
private logService: LogService,
private sdkService: SdkService,
) {
if (flagEnabled("sdk")) {
// Warn if the SDK for some reason can't be initialized
this.sdkService.supported$
.pipe(
takeUntilDestroyed(),
catchError(() => {
return of(false);
}),
)
.subscribe((supported) => {
if (!supported) {
this.logService.debug("SDK is not supported");
this.sdkService.failedToInitialize().catch(this.logService.error);
} else {
this.logService.debug("SDK is supported");
}
});
}
}
ngOnInit() {
this.i18nService.locale$.pipe(takeUntil(this.destroy$)).subscribe((locale) => {

View File

@@ -58,6 +58,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service";
@@ -65,6 +66,7 @@ import { MemoryStorageService } from "@bitwarden/common/platform/services/memory
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
/* eslint-disable import/no-restricted-paths -- Implementation for memory storage */
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
@@ -80,6 +82,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { BiometricsService } from "@bitwarden/key-management";
import { flagEnabled } from "../../utils/flags";
import { PolicyListService } from "../admin-console/core/policy-list.service";
import {
WebSetPasswordJitService,
@@ -93,6 +96,7 @@ import { I18nService } from "../core/i18n.service";
import { WebBiometricsService } from "../key-management/web-biometric.service";
import { WebEnvironmentService } from "../platform/web-environment.service";
import { WebMigrationRunner } from "../platform/web-migration-runner";
import { WebSdkClientFactory } from "../platform/web-sdk-client-factory";
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
import { EventService } from "./event.service";
@@ -270,6 +274,11 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultCollectionAdminService,
deps: [ApiService, CryptoServiceAbstraction, EncryptService, CollectionService],
}),
safeProvider({
provide: SdkClientFactory,
useClass: flagEnabled("sdk") ? WebSdkClientFactory : NoopSdkClientFactory,
deps: [],
}),
];
@NgModule({

View File

@@ -0,0 +1,42 @@
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
import * as sdk from "@bitwarden/sdk-internal";
/**
* SDK client factory with a js fallback for when WASM is not supported.
*/
export class WebSdkClientFactory implements SdkClientFactory {
async createSdkClient(
...args: ConstructorParameters<typeof sdk.BitwardenClient>
): Promise<sdk.BitwardenClient> {
const module = await load();
(sdk as any).init(module);
return Promise.resolve(new sdk.BitwardenClient(...args));
}
}
// https://stackoverflow.com/a/47880734
const supported = (() => {
try {
if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") {
const module = new WebAssembly.Module(
Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00),
);
if (module instanceof WebAssembly.Module) {
return new WebAssembly.Instance(module) instanceof WebAssembly.Instance;
}
}
} catch (e) {
// ignore
}
return false;
})();
async function load() {
if (supported) {
return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm");
} else {
return await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js");
}
}

View File

@@ -65,7 +65,7 @@
>
{{ "cancel" | i18n }}
</button>
<div class="tw-ml-auto">
<div class="tw-ml-auto" *ngIf="showDelete">
<button
bitIconButton="bwi-trash"
type="button"
@@ -73,6 +73,7 @@
[appA11yTitle]="'delete' | i18n"
[bitAction]="delete"
[disabled]="!canDelete"
data-testid="delete-cipher-btn"
></button>
</div>
</ng-container>

View File

@@ -179,6 +179,15 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
return this.cipher?.edit ?? false;
}
protected get showDelete() {
// Don't show the delete button when cloning a cipher
if (this.params.mode == "form" && this.formConfig.mode === "clone") {
return false;
}
// Never show the delete button for new ciphers
return this.cipher != null;
}
protected get showCipherView() {
return this.cipher != undefined && (this.params.mode === "view" || this.loadingForm);
}
@@ -332,8 +341,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
};
cancel = async () => {
// We're in View mode, or we don't have a cipher, close the dialog.
if (this.params.mode === "view" || this.cipher == null) {
// We're in View mode, we don't have a cipher, or we were cloning, close the dialog.
if (this.params.mode === "view" || this.cipher == null || this.formConfig.mode === "clone") {
this.dialogRef.close(this._cipherModified ? VaultItemDialogResult.Saved : undefined);
return;
}

View File

@@ -0,0 +1,119 @@
import { TestBed } from "@angular/core/testing";
import { BehaviorSubject } from "rxjs";
import { CollectionAdminService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
import { AdminConsoleCipherFormConfigService } from "./admin-console-cipher-form-config.service";
describe("AdminConsoleCipherFormConfigService", () => {
let adminConsoleConfigService: AdminConsoleCipherFormConfigService;
const cipherId = "333-444-555" as CipherId;
const testOrg = { id: "333-44-55", name: "Test Org", canEditAllCiphers: false };
const organization$ = new BehaviorSubject<Organization>(testOrg as Organization);
const getCipherAdmin = jest.fn().mockResolvedValue(null);
const getCipher = jest.fn().mockResolvedValue(null);
beforeEach(async () => {
getCipherAdmin.mockClear();
getCipher.mockClear();
getCipher.mockResolvedValue({ id: cipherId, name: "Test Cipher - (non-admin)" });
getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" });
await TestBed.configureTestingModule({
providers: [
AdminConsoleCipherFormConfigService,
{ provide: OrganizationService, useValue: { get$: () => organization$ } },
{ provide: CipherService, useValue: { get: getCipher } },
{ provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([]) } },
{
provide: RoutedVaultFilterService,
useValue: { filter$: new BehaviorSubject({ organizationId: testOrg.id }) },
},
{ provide: ApiService, useValue: { getCipherAdmin } },
],
});
});
describe("buildConfig", () => {
it("sets individual attributes", async () => {
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
const { folders, hideIndividualVaultFields } = await adminConsoleConfigService.buildConfig(
"add",
cipherId,
);
expect(folders).toEqual([]);
expect(hideIndividualVaultFields).toBe(true);
});
it("sets mode based on passed mode", async () => {
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
const { mode } = await adminConsoleConfigService.buildConfig("edit", cipherId);
expect(mode).toBe("edit");
});
it("sets admin flag based on `canEditAllCiphers`", async () => {
// Disable edit all ciphers on org
testOrg.canEditAllCiphers = false;
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
let result = await adminConsoleConfigService.buildConfig("add", cipherId);
expect(result.admin).toBe(false);
// Enable edit all ciphers on org
testOrg.canEditAllCiphers = true;
result = await adminConsoleConfigService.buildConfig("add", cipherId);
expect(result.admin).toBe(true);
});
it("sets `allowPersonalOwnership` to false", async () => {
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
const result = await adminConsoleConfigService.buildConfig("clone", cipherId);
expect(result.allowPersonalOwnership).toBe(false);
});
describe("getCipher", () => {
it("retrieves the cipher from the cipher service", async () => {
testOrg.canEditAllCiphers = false;
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
const result = await adminConsoleConfigService.buildConfig("clone", cipherId);
expect(getCipher).toHaveBeenCalledWith(cipherId);
expect(result.originalCipher.name).toBe("Test Cipher - (non-admin)");
// Admin service not needed when cipher service can return the cipher
expect(getCipherAdmin).not.toHaveBeenCalled();
});
it("retrieves the cipher from the admin service", async () => {
getCipher.mockResolvedValueOnce(null);
getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" });
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
await adminConsoleConfigService.buildConfig("add", cipherId);
expect(getCipherAdmin).toHaveBeenCalledWith(cipherId);
expect(getCipher).toHaveBeenCalledWith(cipherId);
});
});
});
});

View File

@@ -0,0 +1,99 @@
import { inject, Injectable } from "@angular/core";
import { combineLatest, filter, firstValueFrom, map, switchMap } from "rxjs";
import { CollectionAdminService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import {
CipherFormConfig,
CipherFormConfigService,
CipherFormMode,
} from "../../../../../../../libs/vault/src/cipher-form/abstractions/cipher-form-config.service";
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
/** Admin Console implementation of the `CipherFormConfigService`. */
@Injectable()
export class AdminConsoleCipherFormConfigService implements CipherFormConfigService {
private organizationService: OrganizationService = inject(OrganizationService);
private cipherService: CipherService = inject(CipherService);
private routedVaultFilterService: RoutedVaultFilterService = inject(RoutedVaultFilterService);
private collectionAdminService: CollectionAdminService = inject(CollectionAdminService);
private apiService: ApiService = inject(ApiService);
private organizationId$ = this.routedVaultFilterService.filter$.pipe(
map((filter) => filter.organizationId),
filter((filter) => filter !== undefined),
);
private organization$ = this.organizationId$.pipe(
switchMap((organizationId) => this.organizationService.get$(organizationId)),
);
private editableCollections$ = this.organization$.pipe(
switchMap(async (org) => {
const collections = await this.collectionAdminService.getAll(org.id);
// Users that can edit all ciphers can implicitly add to / edit within any collection
if (org.canEditAllCiphers) {
return collections;
}
// The user is only allowed to add/edit items to assigned collections that are not readonly
return collections.filter((c) => c.assigned && !c.readOnly);
}),
);
async buildConfig(
mode: CipherFormMode,
cipherId?: CipherId,
cipherType?: CipherType,
): Promise<CipherFormConfig> {
const [organization, allCollections] = await firstValueFrom(
combineLatest([this.organization$, this.editableCollections$]),
);
const cipher = await this.getCipher(organization, cipherId);
const collections = allCollections.filter(
(c) => c.organizationId === organization.id && c.assigned && !c.readOnly,
);
return {
mode,
cipherType: cipher?.type ?? cipherType ?? CipherType.Login,
admin: organization.canEditAllCiphers ?? false,
allowPersonalOwnership: false,
originalCipher: cipher,
collections,
organizations: [organization], // only a single org is in context at a time
folders: [], // folders not applicable in the admin console
hideIndividualVaultFields: true,
};
}
private async getCipher(organization: Organization, id?: CipherId): Promise<Cipher | null> {
if (id == null) {
return Promise.resolve(null);
}
// Check to see if the user has direct access to the cipher
const cipherFromCipherService = await this.cipherService.get(id);
// If the organization doesn't allow admin/owners to edit all ciphers return the cipher
if (!organization.canEditAllCiphers && cipherFromCipherService != null) {
return cipherFromCipherService;
}
// Retrieve the cipher through the means of an admin
const cipherResponse = await this.apiService.getCipherAdmin(id);
cipherResponse.edit = true;
const cipherData = new CipherData(cipherResponse);
return new Cipher(cipherData);
}
}

View File

@@ -1,3 +1,4 @@
import { DialogRef } from "@angular/cdk/dialog";
import {
ChangeDetectorRef,
Component,
@@ -42,16 +43,17 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
import { SearchService } from "@bitwarden/common/abstractions/search.service";
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 { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
@@ -62,7 +64,12 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { DialogService, Icons, NoItemsModule, ToastService } from "@bitwarden/components";
import { CollectionAssignmentResult, PasswordRepromptService } from "@bitwarden/vault";
import {
CipherFormConfig,
CipherFormConfigService,
CollectionAssignmentResult,
PasswordRepromptService,
} from "@bitwarden/vault";
import { GroupService, GroupView } from "../../admin-console/organizations/core";
import { openEntityEventsDialog } from "../../admin-console/organizations/manage/entity-events.component";
@@ -75,6 +82,11 @@ import {
CollectionDialogTabType,
openCollectionDialog,
} from "../components/collection-dialog";
import {
VaultItemDialogComponent,
VaultItemDialogMode,
VaultItemDialogResult,
} from "../components/vault-item-dialog/vault-item-dialog.component";
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
import {
@@ -89,12 +101,6 @@ import {
All,
RoutedVaultFilterModel,
} from "../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
import {
openViewCipherDialog,
ViewCipherDialogCloseResult,
ViewCipherDialogResult,
ViewComponent,
} from "../individual-vault/view.component";
import { VaultHeaderComponent } from "../org-vault/vault-header/vault-header.component";
import { getNestedCollectionTree } from "../utils/collection-utils";
@@ -106,8 +112,8 @@ import {
} from "./bulk-collections-dialog";
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
import { openOrgVaultCollectionsDialog } from "./collections.component";
import { AdminConsoleCipherFormConfigService } from "./services/admin-console-cipher-form-config.service";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
const BroadcasterSubscriptionId = "OrgVaultComponent";
const SearchTextDebounceInterval = 200;
@@ -127,9 +133,12 @@ enum AddAccessStatusType {
VaultItemsModule,
SharedModule,
NoItemsModule,
ViewComponent,
],
providers: [RoutedVaultFilterService, RoutedVaultFilterBridgeService],
providers: [
RoutedVaultFilterService,
RoutedVaultFilterBridgeService,
{ provide: CipherFormConfigService, useClass: AdminConsoleCipherFormConfigService },
],
})
export class VaultComponent implements OnInit, OnDestroy {
protected Unassigned = Unassigned;
@@ -174,6 +183,8 @@ export class VaultComponent implements OnInit, OnDestroy {
private refresh$ = new BehaviorSubject<void>(null);
private destroy$ = new Subject<void>();
protected addAccessStatus$ = new BehaviorSubject<AddAccessStatusType>(0);
private extensionRefreshEnabled: boolean;
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
constructor(
private route: ActivatedRoute,
@@ -203,10 +214,15 @@ export class VaultComponent implements OnInit, OnDestroy {
private apiService: ApiService,
private collectionService: CollectionService,
private toastService: ToastService,
private accountService: AccountService,
private configService: ConfigService,
private cipherFormConfigService: CipherFormConfigService,
) {}
async ngOnInit() {
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
this.trashCleanupWarning = this.i18nService.t(
this.platformUtilsService.isSelfHost()
? "trashCleanupWarningSelfHosted"
@@ -466,22 +482,27 @@ export class VaultComponent implements OnInit, OnDestroy {
firstSetup$
.pipe(
switchMap(() => this.route.queryParams),
// Only process the queryParams if the dialog is not open (only when extension refresh is enabled)
filter(() => this.vaultItemDialogRef == undefined || !this.extensionRefreshEnabled),
withLatestFrom(allCipherMap$, allCollections$, organization$),
switchMap(async ([qParams, allCiphersMap, allCollections]) => {
switchMap(async ([qParams, allCiphersMap]) => {
const cipherId = getCipherIdFromParams(qParams);
if (!cipherId) {
return;
}
const cipher = allCiphersMap[cipherId];
const cipherCollections = allCollections.filter((c) =>
cipher.collectionIds.includes(c.id),
);
if (cipher) {
if (qParams.action === "view") {
await this.viewCipher(cipher, cipherCollections);
let action = qParams.action;
// Default to "view" if extension refresh is enabled
if (action == null && this.extensionRefreshEnabled) {
action = "view";
}
if (action === "view") {
await this.viewCipherById(cipher);
} else {
await this.editCipherId(cipherId);
await this.editCipherId(cipher, false);
}
} else {
this.toastService.showToast({
@@ -730,12 +751,16 @@ export class VaultComponent implements OnInit, OnDestroy {
}
async addCipher(cipherType?: CipherType) {
if (this.extensionRefreshEnabled) {
return this.addCipherV2(cipherType);
}
let collections: CollectionView[] = [];
// Admins limited to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
await this.editCipher(null, (comp) => {
await this.editCipher(null, false, (comp) => {
comp.type = cipherType || this.activeFilter.cipherType;
comp.collections = collections;
if (this.activeFilter.collectionId) {
@@ -744,20 +769,46 @@ export class VaultComponent implements OnInit, OnDestroy {
});
}
/** Opens the Add/Edit Dialog. Only to be used when the BrowserExtension feature flag is active */
async addCipherV2(cipherType?: CipherType) {
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
"add",
null,
cipherType,
);
const collectionId: CollectionId | undefined = this.activeFilter.collectionId as CollectionId;
cipherFormConfig.initialValues = {
organizationId: this.organization.id as OrganizationId,
collectionIds: collectionId ? [collectionId] : [],
};
await this.openVaultItemDialog("form", cipherFormConfig);
}
/**
* Edit the given cipher
* @param cipherView - The cipher to be edited
* @param cloneCipher - `true` when the cipher should be cloned.
* Used in place of the `additionalComponentParameters`, as
* the `editCipherIdV2` method has a differing implementation.
* @param defaultComponentParameters - A method that takes in an instance of
* the `AddEditComponent` to edit methods directly.
*/
async editCipher(
cipher: CipherView,
cloneCipher: boolean,
additionalComponentParameters?: (comp: AddEditComponent) => void,
) {
return this.editCipherId(cipher?.id, additionalComponentParameters);
return this.editCipherId(cipher, cloneCipher, additionalComponentParameters);
}
async editCipherId(
cipherId: string,
cipher: CipherView,
cloneCipher: boolean,
additionalComponentParameters?: (comp: AddEditComponent) => void,
) {
const cipher = await this.cipherService.get(cipherId);
// if cipher exists (cipher is null when new) and MP reprompt
// is on for this cipher, then show password reprompt
if (
cipher &&
cipher.reprompt !== 0 &&
@@ -768,10 +819,15 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
if (this.extensionRefreshEnabled) {
await this.editCipherIdV2(cipher, cloneCipher);
return;
}
const defaultComponentParameters = (comp: AddEditComponent) => {
comp.organization = this.organization;
comp.organizationId = this.organization.id;
comp.cipherId = cipherId;
comp.cipherId = cipher.id;
comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
@@ -807,46 +863,70 @@ export class VaultComponent implements OnInit, OnDestroy {
}
/**
* Takes a cipher and its assigned collections to opens dialog where it can be viewed.
* @param cipher - the cipher to view
* @param collections - the collections the cipher is assigned to
* Edit a cipher using the new AddEditCipherDialogV2 component.
* Only to be used behind the ExtensionRefresh feature flag.
*/
async viewCipher(cipher: CipherView, collections: CollectionView[] = []) {
private async editCipherIdV2(cipher: CipherView, cloneCipher: boolean) {
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
cloneCipher ? "clone" : "edit",
cipher.id as CipherId,
);
await this.openVaultItemDialog("form", cipherFormConfig, cipher);
}
/** Opens the view dialog for the given cipher unless password reprompt fails */
async viewCipherById(cipher: CipherView) {
if (!cipher) {
this.go({ cipherId: null, itemId: null });
return;
}
if (cipher.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
// didn't pass password prompt, so don't open the dialog
this.go({ cipherId: null, itemId: null });
if (
cipher &&
cipher.reprompt !== 0 &&
!(await this.passwordRepromptService.showPasswordPrompt())
) {
// Didn't pass password prompt, so don't open add / edit modal.
await this.go({ cipherId: null, itemId: null, action: null });
return;
}
const dialogRef = openViewCipherDialog(this.dialogService, {
data: {
cipher: cipher,
collections: collections,
disableEdit: !cipher.edit && !this.organization.canEditAllCiphers,
},
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
"edit",
cipher.id as CipherId,
cipher.type,
);
await this.openVaultItemDialog("view", cipherFormConfig, cipher);
}
/**
* Open the combined view / edit dialog for a cipher.
*/
async openVaultItemDialog(
mode: VaultItemDialogMode,
formConfig: CipherFormConfig,
cipher?: CipherView,
) {
const disableForm = cipher ? !cipher.edit && !this.organization.canEditAllCiphers : false;
// If the form is disabled, force the mode into `view`
const dialogMode = disableForm ? "view" : mode;
this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, {
mode: dialogMode,
formConfig,
disableForm,
});
// Wait for the dialog to close.
const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
// If the dialog was closed by clicking the edit button, navigate to open the edit dialog.
if (result?.action === ViewCipherDialogResult.Edited) {
this.go({ itemId: cipher.id, action: "edit" });
return;
}
const result = await lastValueFrom(this.vaultItemDialogRef.closed);
this.vaultItemDialogRef = undefined;
// If the dialog was closed by deleting the cipher, refresh the vault.
if (result?.action === ViewCipherDialogResult.Deleted) {
if (result === VaultItemDialogResult.Deleted || result === VaultItemDialogResult.Saved) {
this.refresh();
}
// Clear the query params when the view dialog closes
this.go({ cipherId: null, itemId: null, action: null });
// Clear the query params when the dialog closes
await this.go({ cipherId: null, itemId: null, action: null });
}
async cloneCipher(cipher: CipherView) {
@@ -867,7 +947,7 @@ export class VaultComponent implements OnInit, OnDestroy {
// Admins limited to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
await this.editCipher(cipher, (comp) => {
await this.editCipher(cipher, true, (comp) => {
comp.cloneMode = true;
comp.collections = collections;
comp.collectionIds = cipher.collectionIds;