Skip to content

Commit

Permalink
[PM-10426] Admin Console - Edit Modal (#11249)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nick-livefront authored Oct 7, 2024
1 parent 7098a24 commit a6db7e3
Show file tree
Hide file tree
Showing 5 changed files with 361 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit a6db7e3

Please sign in to comment.