[PM-10426] Admin Console - Edit Modal (#11249)
* add `hideFolderSelection` for admin console ciphers * hide folder form field when configuration has `hideFolderSelection` set to true * add `addCipherV2` method in the admin console vault * add browser refresh logic for add/edit form * add admin console implementation of `AdminConsoleCipherFormConfigService` * only allow edit dialog in admin console * remove duplicate check * refactor comments * initial integration of combined dialog * integrate add cipher with admin console vault * account for special admin console collection permissions * add `edit` variable to AC ciphers when the user has permissions * Move comment to JSDoc * pass full cipher to view component * validate edit access when opening view form * partial-edit not applicable for admin console * refactor hideIndividualFields to be more generic and hide favorite button * pass entire cipher into edit logic to match view logic * add null check for cipher when attempting to view * remove logic for personal ownership, not needed in AC
This commit is contained in:
parent
7098a243ca
commit
a6db7e3086
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DialogRef } from "@angular/cdk/dialog";
|
||||||
import {
|
import {
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
|
@ -42,16 +43,17 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
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 { EventType } from "@bitwarden/common/enums";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.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 { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||||
import { DialogService, Icons, NoItemsModule, ToastService } from "@bitwarden/components";
|
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 { GroupService, GroupView } from "../../admin-console/organizations/core";
|
||||||
import { openEntityEventsDialog } from "../../admin-console/organizations/manage/entity-events.component";
|
import { openEntityEventsDialog } from "../../admin-console/organizations/manage/entity-events.component";
|
||||||
|
@ -75,6 +82,11 @@ import {
|
||||||
CollectionDialogTabType,
|
CollectionDialogTabType,
|
||||||
openCollectionDialog,
|
openCollectionDialog,
|
||||||
} from "../components/collection-dialog";
|
} 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 { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
||||||
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
||||||
import {
|
import {
|
||||||
|
@ -89,12 +101,6 @@ import {
|
||||||
All,
|
All,
|
||||||
RoutedVaultFilterModel,
|
RoutedVaultFilterModel,
|
||||||
} from "../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
} 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 { VaultHeaderComponent } from "../org-vault/vault-header/vault-header.component";
|
||||||
import { getNestedCollectionTree } from "../utils/collection-utils";
|
import { getNestedCollectionTree } from "../utils/collection-utils";
|
||||||
|
|
||||||
|
@ -106,8 +112,8 @@ import {
|
||||||
} from "./bulk-collections-dialog";
|
} from "./bulk-collections-dialog";
|
||||||
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
|
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
|
||||||
import { openOrgVaultCollectionsDialog } from "./collections.component";
|
import { openOrgVaultCollectionsDialog } from "./collections.component";
|
||||||
|
import { AdminConsoleCipherFormConfigService } from "./services/admin-console-cipher-form-config.service";
|
||||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "OrgVaultComponent";
|
const BroadcasterSubscriptionId = "OrgVaultComponent";
|
||||||
const SearchTextDebounceInterval = 200;
|
const SearchTextDebounceInterval = 200;
|
||||||
|
|
||||||
|
@ -127,9 +133,12 @@ enum AddAccessStatusType {
|
||||||
VaultItemsModule,
|
VaultItemsModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
NoItemsModule,
|
NoItemsModule,
|
||||||
ViewComponent,
|
|
||||||
],
|
],
|
||||||
providers: [RoutedVaultFilterService, RoutedVaultFilterBridgeService],
|
providers: [
|
||||||
|
RoutedVaultFilterService,
|
||||||
|
RoutedVaultFilterBridgeService,
|
||||||
|
{ provide: CipherFormConfigService, useClass: AdminConsoleCipherFormConfigService },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class VaultComponent implements OnInit, OnDestroy {
|
export class VaultComponent implements OnInit, OnDestroy {
|
||||||
protected Unassigned = Unassigned;
|
protected Unassigned = Unassigned;
|
||||||
|
@ -174,6 +183,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
private refresh$ = new BehaviorSubject<void>(null);
|
private refresh$ = new BehaviorSubject<void>(null);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
protected addAccessStatus$ = new BehaviorSubject<AddAccessStatusType>(0);
|
protected addAccessStatus$ = new BehaviorSubject<AddAccessStatusType>(0);
|
||||||
|
private extensionRefreshEnabled: boolean;
|
||||||
|
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
@ -203,10 +214,15 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private collectionService: CollectionService,
|
private collectionService: CollectionService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private accountService: AccountService,
|
private configService: ConfigService,
|
||||||
|
private cipherFormConfigService: CipherFormConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.ExtensionRefresh,
|
||||||
|
);
|
||||||
|
|
||||||
this.trashCleanupWarning = this.i18nService.t(
|
this.trashCleanupWarning = this.i18nService.t(
|
||||||
this.platformUtilsService.isSelfHost()
|
this.platformUtilsService.isSelfHost()
|
||||||
? "trashCleanupWarningSelfHosted"
|
? "trashCleanupWarningSelfHosted"
|
||||||
|
@ -466,22 +482,27 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
firstSetup$
|
firstSetup$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => this.route.queryParams),
|
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$),
|
withLatestFrom(allCipherMap$, allCollections$, organization$),
|
||||||
switchMap(async ([qParams, allCiphersMap, allCollections]) => {
|
switchMap(async ([qParams, allCiphersMap]) => {
|
||||||
const cipherId = getCipherIdFromParams(qParams);
|
const cipherId = getCipherIdFromParams(qParams);
|
||||||
if (!cipherId) {
|
if (!cipherId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cipher = allCiphersMap[cipherId];
|
const cipher = allCiphersMap[cipherId];
|
||||||
const cipherCollections = allCollections.filter((c) =>
|
|
||||||
cipher.collectionIds.includes(c.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (cipher) {
|
if (cipher) {
|
||||||
if (qParams.action === "view") {
|
let action = qParams.action;
|
||||||
await this.viewCipher(cipher, cipherCollections);
|
// Default to "view" if extension refresh is enabled
|
||||||
|
if (action == null && this.extensionRefreshEnabled) {
|
||||||
|
action = "view";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "view") {
|
||||||
|
await this.viewCipherById(cipher);
|
||||||
} else {
|
} else {
|
||||||
await this.editCipherId(cipherId);
|
await this.editCipherId(cipher, false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
|
@ -730,12 +751,16 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
async addCipher(cipherType?: CipherType) {
|
async addCipher(cipherType?: CipherType) {
|
||||||
|
if (this.extensionRefreshEnabled) {
|
||||||
|
return this.addCipherV2(cipherType);
|
||||||
|
}
|
||||||
|
|
||||||
let collections: CollectionView[] = [];
|
let collections: CollectionView[] = [];
|
||||||
|
|
||||||
// Admins limited to only adding items to collections they have access to.
|
// Admins limited to only adding items to collections they have access to.
|
||||||
collections = await firstValueFrom(this.editableCollections$);
|
collections = await firstValueFrom(this.editableCollections$);
|
||||||
|
|
||||||
await this.editCipher(null, (comp) => {
|
await this.editCipher(null, false, (comp) => {
|
||||||
comp.type = cipherType || this.activeFilter.cipherType;
|
comp.type = cipherType || this.activeFilter.cipherType;
|
||||||
comp.collections = collections;
|
comp.collections = collections;
|
||||||
if (this.activeFilter.collectionId) {
|
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(
|
async editCipher(
|
||||||
cipher: CipherView,
|
cipher: CipherView,
|
||||||
|
cloneCipher: boolean,
|
||||||
additionalComponentParameters?: (comp: AddEditComponent) => void,
|
additionalComponentParameters?: (comp: AddEditComponent) => void,
|
||||||
) {
|
) {
|
||||||
return this.editCipherId(cipher?.id, additionalComponentParameters);
|
return this.editCipherId(cipher, cloneCipher, additionalComponentParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
async editCipherId(
|
async editCipherId(
|
||||||
cipherId: string,
|
cipher: CipherView,
|
||||||
|
cloneCipher: boolean,
|
||||||
additionalComponentParameters?: (comp: AddEditComponent) => void,
|
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 (
|
if (
|
||||||
cipher &&
|
cipher &&
|
||||||
cipher.reprompt !== 0 &&
|
cipher.reprompt !== 0 &&
|
||||||
|
@ -768,10 +819,15 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.extensionRefreshEnabled) {
|
||||||
|
await this.editCipherIdV2(cipher, cloneCipher);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const defaultComponentParameters = (comp: AddEditComponent) => {
|
const defaultComponentParameters = (comp: AddEditComponent) => {
|
||||||
comp.organization = this.organization;
|
comp.organization = this.organization;
|
||||||
comp.organizationId = this.organization.id;
|
comp.organizationId = this.organization.id;
|
||||||
comp.cipherId = cipherId;
|
comp.cipherId = cipher.id;
|
||||||
comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||||
modal.close();
|
modal.close();
|
||||||
this.refresh();
|
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.
|
* Edit a cipher using the new AddEditCipherDialogV2 component.
|
||||||
* @param cipher - the cipher to view
|
* Only to be used behind the ExtensionRefresh feature flag.
|
||||||
* @param collections - the collections the cipher is assigned to
|
|
||||||
*/
|
*/
|
||||||
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) {
|
if (!cipher) {
|
||||||
this.go({ cipherId: null, itemId: null });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cipher.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
|
if (
|
||||||
// didn't pass password prompt, so don't open the dialog
|
cipher &&
|
||||||
this.go({ cipherId: null, itemId: null });
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialogRef = openViewCipherDialog(this.dialogService, {
|
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
|
||||||
data: {
|
"edit",
|
||||||
cipher: cipher,
|
cipher.id as CipherId,
|
||||||
collections: collections,
|
cipher.type,
|
||||||
disableEdit: !cipher.edit && !this.organization.canEditAllCiphers,
|
);
|
||||||
},
|
|
||||||
|
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 = await lastValueFrom(this.vaultItemDialogRef.closed);
|
||||||
const result: ViewCipherDialogCloseResult = await lastValueFrom(dialogRef.closed);
|
this.vaultItemDialogRef = undefined;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the dialog was closed by deleting the cipher, refresh the vault.
|
// 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();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the query params when the view dialog closes
|
// Clear the query params when the dialog closes
|
||||||
this.go({ cipherId: null, itemId: null, action: null });
|
await this.go({ cipherId: null, itemId: null, action: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
async cloneCipher(cipher: CipherView) {
|
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.
|
// Admins limited to only adding items to collections they have access to.
|
||||||
collections = await firstValueFrom(this.editableCollections$);
|
collections = await firstValueFrom(this.editableCollections$);
|
||||||
|
|
||||||
await this.editCipher(cipher, (comp) => {
|
await this.editCipher(cipher, true, (comp) => {
|
||||||
comp.cloneMode = true;
|
comp.cloneMode = true;
|
||||||
comp.collections = collections;
|
comp.collections = collections;
|
||||||
comp.collectionIds = cipher.collectionIds;
|
comp.collectionIds = cipher.collectionIds;
|
||||||
|
|
|
@ -79,6 +79,9 @@ type BaseCipherFormConfig = {
|
||||||
* List of organizations that the user can create ciphers for.
|
* List of organizations that the user can create ciphers for.
|
||||||
*/
|
*/
|
||||||
organizations?: Organization[];
|
organizations?: Organization[];
|
||||||
|
|
||||||
|
/** Hides the fields that are only applicable to individuals, useful in the Admin Console where folders aren't applicable */
|
||||||
|
hideIndividualVaultFields?: true;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<bit-section-header>
|
<bit-section-header>
|
||||||
<h2 bitTypography="h6">{{ "itemDetails" | i18n }}</h2>
|
<h2 bitTypography="h6">{{ "itemDetails" | i18n }}</h2>
|
||||||
<button
|
<button
|
||||||
|
*ngIf="!config.hideIndividualVaultFields"
|
||||||
slot="end"
|
slot="end"
|
||||||
type="button"
|
type="button"
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -18,7 +19,11 @@
|
||||||
<input bitInput formControlName="name" />
|
<input bitInput formControlName="name" />
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<div class="tw-grid tw-grid-cols-2 tw-gap-1">
|
<div class="tw-grid tw-grid-cols-2 tw-gap-1">
|
||||||
<bit-form-field *ngIf="showOwnership" [disableMargin]="!showCollectionsControl">
|
<bit-form-field
|
||||||
|
*ngIf="showOwnership"
|
||||||
|
[disableMargin]="!showCollectionsControl"
|
||||||
|
[class.tw-col-span-2]="config.hideIndividualVaultFields"
|
||||||
|
>
|
||||||
<bit-label>{{ "owner" | i18n }}</bit-label>
|
<bit-label>{{ "owner" | i18n }}</bit-label>
|
||||||
<bit-select formControlName="organizationId">
|
<bit-select formControlName="organizationId">
|
||||||
<bit-option
|
<bit-option
|
||||||
|
@ -36,6 +41,7 @@
|
||||||
<bit-form-field
|
<bit-form-field
|
||||||
[class.tw-col-span-2]="!showOwnership"
|
[class.tw-col-span-2]="!showOwnership"
|
||||||
[disableMargin]="!showCollectionsControl"
|
[disableMargin]="!showCollectionsControl"
|
||||||
|
*ngIf="!config.hideIndividualVaultFields"
|
||||||
>
|
>
|
||||||
<bit-label>{{ "folder" | i18n }}</bit-label>
|
<bit-label>{{ "folder" | i18n }}</bit-label>
|
||||||
<bit-select formControlName="folderId">
|
<bit-select formControlName="folderId">
|
||||||
|
|
Loading…
Reference in New Issue