mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
add admin console implementation of AdminConsoleCipherFormConfigService
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
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 { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
|
||||
import { CollectionAdminService } from "../../core/collection-admin.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();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AdminConsoleCipherFormConfigService,
|
||||
{
|
||||
provide: PolicyService,
|
||||
useValue: { policyAppliesToActiveUser$: () => policyAppliesToActiveUser$ },
|
||||
},
|
||||
{ provide: OrganizationService, useValue: { get$: () => organization$ } },
|
||||
{ provide: CipherService, useValue: { get: getCipher } },
|
||||
{ provide: CollectionService, useValue: { getAllDecrypted: () => Promise.resolve([]) } },
|
||||
{ provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([]) } },
|
||||
{
|
||||
provide: RoutedVaultFilterService,
|
||||
useValue: { filter$: new BehaviorSubject({ organizationId: testOrg.id }) },
|
||||
},
|
||||
{ provide: ApiService, useValue: { getCipherAdmin } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildConfig", () => {
|
||||
it("sets folder attributes", async () => {
|
||||
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
|
||||
|
||||
const { folders, hideFolderSelection } = await adminConsoleConfigService.buildConfig(
|
||||
"add",
|
||||
cipherId,
|
||||
);
|
||||
|
||||
expect(folders).toEqual([]);
|
||||
expect(hideFolderSelection).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 () => {
|
||||
getCipher.mockResolvedValueOnce({ id: cipherId, name: "Test Cipher - (non-admin)" });
|
||||
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 () => {
|
||||
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,121 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { combineLatest, defer, filter, firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
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 { CollectionService } from "@bitwarden/common/vault/abstractions/collection.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 { CollectionAdminService } from "../../core/collection-admin.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 collectionService: CollectionService = inject(CollectionService);
|
||||
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 allCollectionsWithoutUnassigned$ = combineLatest([
|
||||
this.organizationId$.pipe(switchMap((orgId) => this.collectionAdminService.getAll(orgId))),
|
||||
defer(() => this.collectionService.getAllDecrypted()),
|
||||
]).pipe(
|
||||
map(([adminCollections, syncCollections]) => {
|
||||
const syncCollectionDict = Object.fromEntries(syncCollections.map((c) => [c.id, c]));
|
||||
|
||||
return adminCollections.map((collection) => {
|
||||
const currentId: any = collection.id;
|
||||
|
||||
const match = syncCollectionDict[currentId];
|
||||
|
||||
if (match) {
|
||||
collection.manage = match.manage;
|
||||
collection.readOnly = match.readOnly;
|
||||
collection.hidePasswords = match.hidePasswords;
|
||||
}
|
||||
return collection;
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
async buildConfig(
|
||||
mode: CipherFormMode,
|
||||
cipherId?: CipherId,
|
||||
cipherType?: CipherType,
|
||||
): Promise<CipherFormConfig> {
|
||||
const [organization, allowPersonalOwnership, allCollections] = await firstValueFrom(
|
||||
combineLatest([
|
||||
this.organization$,
|
||||
this.allowPersonalOwnership$,
|
||||
this.allCollectionsWithoutUnassigned$,
|
||||
]),
|
||||
);
|
||||
|
||||
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,
|
||||
originalCipher: cipher,
|
||||
collections,
|
||||
organizations: [organization], // only a single org is in context at a time
|
||||
folders: [], // folders not applicable in the admin console
|
||||
hideFolderSelection: 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);
|
||||
const cipherData = new CipherData(cipherResponse);
|
||||
|
||||
return new Cipher(cipherData);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user