Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-10426] Admin Console - Edit Modal #11249

Merged
merged 25 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1ab007f
add `hideFolderSelection` for admin console ciphers
nick-livefront Sep 23, 2024
2442dcc
hide folder form field when configuration has `hideFolderSelection` sโ€ฆ
nick-livefront Sep 23, 2024
33dcddc
add `addCipherV2` method in the admin console vault
nick-livefront Sep 23, 2024
0af3543
add browser refresh logic for add/edit form
nick-livefront Sep 24, 2024
80d5b33
add admin console implementation of `AdminConsoleCipherFormConfigServโ€ฆ
nick-livefront Sep 24, 2024
bf83b4f
Merge branch 'main' of https://github.com/bitwarden/clients into vaulโ€ฆ
nick-livefront Sep 25, 2024
57cd1ad
Merge branch 'main' of https://github.com/bitwarden/clients into vaulโ€ฆ
nick-livefront Sep 25, 2024
67e4305
only allow edit dialog in admin console
nick-livefront Sep 25, 2024
867bff9
remove duplicate check
nick-livefront Sep 25, 2024
970427e
refactor comments
nick-livefront Sep 25, 2024
12b36c5
Merge branch 'main' of https://github.com/bitwarden/clients into vaulโ€ฆ
nick-livefront Sep 30, 2024
4c75c07
Merge branch 'main' of https://github.com/bitwarden/clients into vaulโ€ฆ
nick-livefront Oct 3, 2024
50cf19e
initial integration of combined dialog
nick-livefront Oct 3, 2024
17f4fe9
integrate add cipher with admin console vault
nick-livefront Oct 3, 2024
100621a
Merge branch 'main' of https://github.com/bitwarden/clients into vaulโ€ฆ
nick-livefront Oct 4, 2024
e4b3f28
account for special admin console collection permissions
nick-livefront Oct 4, 2024
09d7fe2
add `edit` variable to AC ciphers when the user has permissions
nick-livefront Oct 4, 2024
1edc24e
Move comment to JSDoc
nick-livefront Oct 4, 2024
ca3a369
pass full cipher to view component
nick-livefront Oct 4, 2024
85a53f4
validate edit access when opening view form
nick-livefront Oct 4, 2024
03b8a29
partial-edit not applicable for admin console
nick-livefront Oct 4, 2024
787c962
refactor hideIndividualFields to be more generic and hide favorite buโ€ฆ
nick-livefront Oct 4, 2024
87ff20a
pass entire cipher into edit logic to match view logic
nick-livefront Oct 4, 2024
caa3b46
add null check for cipher when attempting to view
nick-livefront Oct 4, 2024
f04fef5
remove logic for personal ownership, not needed in AC
nick-livefront Oct 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.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 policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(true);
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: PolicyService,
useValue: { policyAppliesToActiveUser$: () => policyAppliesToActiveUser$ },
},
{ 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`", async () => {
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);

policyAppliesToActiveUser$.next(true);

let result = await adminConsoleConfigService.buildConfig("clone", cipherId);

expect(result.allowPersonalOwnership).toBe(false);

policyAppliesToActiveUser$.next(false);

result = await adminConsoleConfigService.buildConfig("clone", cipherId);

expect(result.allowPersonalOwnership).toBe(true);
});

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,106 @@
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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
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 policyService: PolicyService = inject(PolicyService);
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 allowPersonalOwnership$ = this.policyService
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
.pipe(map((p) => !p));

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, allowPersonalOwnership, allCollections] = await firstValueFrom(
combineLatest([this.organization$, this.allowPersonalOwnership$, 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,
shane-melton marked this conversation as resolved.
Show resolved Hide resolved
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 warning on line 88 in apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts#L88

Added line #L88 was not covered by tests
}

// 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
Loading