From a5ea22d0ccd08f0080f959452d742a670ab79d60 Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:33:43 -0400 Subject: [PATCH] [AC-2843] Member access report api setup (#10434) * Initial setup and modifications for member access report api implementation * Adding the permissions logic for getting the permissions text * fixing the test cases * Some refactoring on async calls * Comments on the model * Resolving the mock issue * Added functionality to edit members on MemberAccessReportComponent (#10618) * Added functionality to edit members on MemberAccessReportComponent * Fixed test cases --------- Co-authored-by: aj-rosado <109146700+aj-rosado@users.noreply.github.com> --- .../member-access-report.component.html | 2 +- .../member-access-report.component.ts | 61 +++- .../model/member-access-report.model.ts | 31 +- .../response/member-access-report.response.ts | 57 ++++ .../member-access-report-api.service.ts | 18 +- .../member-access-report.abstraction.ts | 4 +- .../services/member-access-report.mock.ts | 302 ++++++++++++------ .../member-access-report.service.spec.ts | 68 ++-- .../services/member-access-report.service.ts | 144 ++++----- .../view/member-access-report.view.ts | 4 + 10 files changed, 455 insertions(+), 236 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/tools/reports/member-access-report/response/member-access-report.response.ts diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.html b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.html index 82ca34a89b..b50f197a11 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.html +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.html @@ -32,7 +32,7 @@
- diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts index a169d44701..c547c53d73 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts @@ -2,13 +2,22 @@ import { Component, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { debounceTime, firstValueFrom } from "rxjs"; +import { debounceTime, firstValueFrom, lastValueFrom } from "rxjs"; +import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { SearchModule, TableDataSource } from "@bitwarden/components"; +import { DialogService, SearchModule, TableDataSource } from "@bitwarden/components"; import { ExportHelper } from "@bitwarden/vault-export-core"; +import { CoreOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/core"; +import { + openUserAddEditDialog, + MemberDialogResult, + MemberDialogTab, +} from "@bitwarden/web-vault/app/admin-console/organizations/members/components/member-dialog"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { exportToCSV } from "@bitwarden/web-vault/app/tools/reports/report-utils"; @@ -22,12 +31,12 @@ import { MemberAccessReportView } from "./view/member-access-report.view"; @Component({ selector: "member-access-report", templateUrl: "member-access-report.component.html", - imports: [SharedModule, SearchModule, HeaderModule], + imports: [SharedModule, SearchModule, HeaderModule, CoreOrganizationModule], providers: [ safeProvider({ provide: MemberAccessReportServiceAbstraction, useClass: MemberAccessReportService, - deps: [MemberAccessReportApiService], + deps: [MemberAccessReportApiService, I18nService], }), ], standalone: true, @@ -36,11 +45,15 @@ export class MemberAccessReportComponent implements OnInit { protected dataSource = new TableDataSource(); protected searchControl = new FormControl("", { nonNullable: true }); protected organizationId: OrganizationId; + protected orgIsOnSecretsManagerStandalone: boolean; constructor( private route: ActivatedRoute, protected reportService: MemberAccessReportService, protected fileDownloadService: FileDownloadService, + protected dialogService: DialogService, + protected userNamePipe: UserNamePipe, + protected billingApiService: BillingApiServiceAbstraction, ) { // Connect the search input to the table dataSource filter input this.searchControl.valueChanges @@ -51,7 +64,20 @@ export class MemberAccessReportComponent implements OnInit { async ngOnInit() { const params = await firstValueFrom(this.route.params); this.organizationId = params.organizationId; - this.dataSource.data = this.reportService.generateMemberAccessReportView(); + + const billingMetadata = await this.billingApiService.getOrganizationBillingMetadata( + this.organizationId, + ); + + this.orgIsOnSecretsManagerStandalone = billingMetadata.isOnSecretsManagerStandalone; + + await this.load(); + } + + async load() { + this.dataSource.data = await this.reportService.generateMemberAccessReportView( + this.organizationId, + ); } exportReportAction = async (): Promise => { @@ -64,4 +90,29 @@ export class MemberAccessReportComponent implements OnInit { blobOptions: { type: "text/plain" }, }); }; + + edit = async (user: MemberAccessReportView | null): Promise => { + const dialog = openUserAddEditDialog(this.dialogService, { + data: { + name: this.userNamePipe.transform(user), + organizationId: this.organizationId, + organizationUserId: user != null ? user.userGuid : null, + allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [], + usesKeyConnector: user?.usesKeyConnector, + isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, + initialTab: MemberDialogTab.Role, + numConfirmedMembers: this.dataSource.data.length, + }, + }); + + const result = await lastValueFrom(dialog.closed); + switch (result) { + case MemberDialogResult.Deleted: + case MemberDialogResult.Saved: + case MemberDialogResult.Revoked: + case MemberDialogResult.Restored: + await this.load(); + return; + } + }; } diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/model/member-access-report.model.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/model/member-access-report.model.ts index 35f37eb45f..dae70f26fb 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/model/member-access-report.model.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/model/member-access-report.model.ts @@ -1,23 +1,16 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -export type MemberAccessCollectionModel = { - id: string; - name: EncString; +/** + * Details for the parents MemberAccessReport + */ +export type MemberAccessDetails = { + collectionId: string; + groupId: string; + groupName: string; + // Comes encrypted from the server + collectionName: EncString; itemCount: number; -}; - -export type MemberAccessGroupModel = { - id: string; - name: string; - itemCount: number; - collections: MemberAccessCollectionModel[]; -}; - -export type MemberAccessReportModel = { - userName: string; - email: string; - twoFactorEnabled: boolean; - accountRecoveryEnabled: boolean; - collections: MemberAccessCollectionModel[]; - groups: MemberAccessGroupModel[]; + readOnly: boolean; + hidePasswords: boolean; + manage: boolean; }; diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/response/member-access-report.response.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/response/member-access-report.response.ts new file mode 100644 index 0000000000..959b70b972 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/response/member-access-report.response.ts @@ -0,0 +1,57 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { Guid } from "@bitwarden/common/types/guid"; + +export class MemberAccessDetails extends BaseResponse { + collectionId: string; + groupId: string; + groupName: string; + collectionName: EncString; + itemCount: number; + readOnly: boolean; + hidePasswords: boolean; + manage: boolean; + + constructor(response: any) { + super(response); + this.groupId = this.getResponseProperty("GroupId"); + this.collectionId = this.getResponseProperty("CollectionId"); + this.groupName = this.getResponseProperty("GroupName"); + this.collectionName = new EncString(this.getResponseProperty("CollectionName")); + this.itemCount = this.getResponseProperty("ItemCount"); + this.readOnly = this.getResponseProperty("ReadOnly"); + this.hidePasswords = this.getResponseProperty("HidePasswords"); + this.manage = this.getResponseProperty("Manage"); + } +} + +export class MemberAccessResponse extends BaseResponse { + userName: string; + email: string; + twoFactorEnabled: boolean; + accountRecoveryEnabled: boolean; + collectionsCount: number; + groupsCount: number; + totalItemCount: number; + accessDetails: MemberAccessDetails[] = []; + userGuid: Guid; + usesKeyConnector: boolean; + + constructor(response: any) { + super(response); + this.userName = this.getResponseProperty("UserName"); + this.email = this.getResponseProperty("Email"); + this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled"); + this.accountRecoveryEnabled = this.getResponseProperty("AccountRecoveryEnabled"); + this.collectionsCount = this.getResponseProperty("CollectionsCount"); + this.groupsCount = this.getResponseProperty("GroupsCount"); + this.totalItemCount = this.getResponseProperty("TotalItemCount"); + this.userGuid = this.getResponseProperty("UserGuid"); + this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector"); + + const details = this.getResponseProperty("AccessDetails"); + if (details != null) { + this.accessDetails = details.map((o: any) => new MemberAccessDetails(o)); + } + } +} diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report-api.service.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report-api.service.ts index ad25308b61..ef3e7b90f0 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report-api.service.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report-api.service.ts @@ -1,12 +1,22 @@ import { Injectable } from "@angular/core"; -import { MemberAccessReportModel } from "../model/member-access-report.model"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { memberAccessReportsMock } from "./member-access-report.mock"; +import { MemberAccessResponse } from "../response/member-access-report.response"; @Injectable({ providedIn: "root" }) export class MemberAccessReportApiService { - getMemberAccessData(): MemberAccessReportModel[] { - return memberAccessReportsMock; + constructor(protected apiService: ApiService) {} + async getMemberAccessData(orgId: string): Promise { + const response = await this.apiService.send( + "GET", + "/reports/member-access/" + orgId, + null, + true, + true, + ); + const memberAccessResponses = response.map((o: any) => new MemberAccessResponse(o)); + + return memberAccessResponses; } } diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts index f26961e11d..5d17f8a017 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts @@ -4,7 +4,9 @@ import { MemberAccessExportItem } from "../view/member-access-export.view"; import { MemberAccessReportView } from "../view/member-access-report.view"; export abstract class MemberAccessReportServiceAbstraction { - generateMemberAccessReportView: () => MemberAccessReportView[]; + generateMemberAccessReportView: ( + organizationId: OrganizationId, + ) => Promise; generateUserReportExportItems: ( organizationId: OrganizationId, ) => Promise; diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts index 4a0ad310c3..9ace555dd2 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.mock.ts @@ -1,137 +1,231 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { MemberAccessReportModel } from "../model/member-access-report.model"; +import { + MemberAccessDetails, + MemberAccessResponse, +} from "../response/member-access-report.response"; -export const memberAccessReportsMock: MemberAccessReportModel[] = [ +export const memberAccessReportsMock: MemberAccessResponse[] = [ { userName: "Sarah Johnson", email: "sjohnson@email.com", twoFactorEnabled: true, accountRecoveryEnabled: true, - collections: [ + groupsCount: 2, + collectionsCount: 4, + totalItemCount: 20, + userGuid: "1234", + usesKeyConnector: false, + accessDetails: [ { - id: "c1", - name: new EncString( - "2.UiXa3L3Ol1G4QnfFfBjMQw==|sbVTj0EiEkhIrDiropn2Cg==|82P78YgmapW4TdN9jQJgMWKv2gGyK1AnGkr+W9/sq+A=", - ), + groupId: "", + collectionId: "c1", + collectionName: new EncString("Collection 1"), + groupName: "", itemCount: 10, - }, - { id: "c2", name: new EncString("Collection 2"), itemCount: 20 }, - { id: "c3", name: new EncString("Collection 3"), itemCount: 30 }, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + { + groupId: "", + collectionId: "c2", + collectionName: new EncString("Collection 2"), + groupName: "", + itemCount: 20, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + { + groupId: "", + collectionId: "c3", + collectionName: new EncString("Collection 3"), + groupName: "", + itemCount: 30, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + { + groupId: "g1", + collectionId: "c1", + collectionName: new EncString("Collection 1"), + groupName: "Group 1", + itemCount: 30, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + { + groupId: "g1", + collectionId: "c2", + collectionName: new EncString("Collection 2"), + groupName: "Group 1", + itemCount: 20, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, ], - groups: [ - { - id: "g1", - name: "Group 1", - itemCount: 3, - collections: [ - { - id: "c6", - name: new EncString( - "2.UiXa3L3Ol1G4QnfFfBjMQw==|sbVTj0EiEkhIrDiropn2Cg==|82P78YgmapW4TdN9jQJgMWKv2gGyK1AnGkr+W9/sq+A=", - ), - itemCount: 10, - }, - { id: "c2", name: new EncString("Collection 2"), itemCount: 20 }, - ], - }, - { - id: "g2", - name: "Group 2", - itemCount: 2, - collections: [ - { id: "c2", name: new EncString("Collection 2"), itemCount: 20 }, - { id: "c3", name: new EncString("Collection 3"), itemCount: 30 }, - ], - }, - { - id: "g3", - name: "Group 3", - itemCount: 2, - collections: [ - { - id: "c1", - name: new EncString( - "2.UiXa3L3Ol1G4QnfFfBjMQw==|sbVTj0EiEkhIrDiropn2Cg==|82P78YgmapW4TdN9jQJgMWKv2gGyK1AnGkr+W9/sq+A=", - ), - itemCount: 10, - }, - { id: "c3", name: new EncString("Collection 3"), itemCount: 30 }, - ], - }, - ], - }, + } as MemberAccessResponse, { userName: "James Lull", email: "jlull@email.com", twoFactorEnabled: false, accountRecoveryEnabled: false, - collections: [ - { id: "c4", name: new EncString("Collection 4"), itemCount: 5 }, - { id: "c5", name: new EncString("Collection 5"), itemCount: 15 }, - ], - groups: [ + groupsCount: 2, + collectionsCount: 4, + totalItemCount: 20, + userGuid: "1234", + usesKeyConnector: false, + accessDetails: [ { - id: "g4", - name: "Group 4", - itemCount: 2, - collections: [ - { id: "c4", name: new EncString("Collection 4"), itemCount: 5 }, - { id: "c5", name: new EncString("Collection 5"), itemCount: 15 }, - ], - }, + groupId: "g4", + collectionId: "c4", + groupName: "Group 4", + collectionName: new EncString("Collection 4"), + itemCount: 5, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, { - id: "g5", - name: "Group 5", - itemCount: 1, - collections: [{ id: "c5", name: new EncString("Collection 5"), itemCount: 15 }], - }, + groupId: "g4", + collectionId: "c5", + groupName: "Group 4", + collectionName: new EncString("Collection 5"), + itemCount: 15, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + { + groupId: "", + collectionId: "c4", + groupName: "", + collectionName: new EncString("Collection 4"), + itemCount: 5, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + { + groupId: "", + collectionId: "c5", + groupName: "", + collectionName: new EncString("Collection 5"), + itemCount: 15, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, ], - }, + } as MemberAccessResponse, { userName: "Beth Williams", email: "bwilliams@email.com", twoFactorEnabled: true, accountRecoveryEnabled: true, - collections: [{ id: "c6", name: new EncString("Collection 6"), itemCount: 25 }], - groups: [ + groupsCount: 2, + collectionsCount: 4, + totalItemCount: 20, + userGuid: "1234", + usesKeyConnector: false, + accessDetails: [ { - id: "g6", - name: "Group 6", - itemCount: 1, - collections: [{ id: "c4", name: new EncString("Collection 4"), itemCount: 35 }], - }, + groupId: "", + collectionId: "c6", + groupName: "", + collectionName: new EncString("Collection 6"), + itemCount: 25, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + { + groupId: "g6", + collectionId: "c4", + groupName: "Group 6", + collectionName: new EncString("Collection 4"), + itemCount: 35, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, ], - }, + } as MemberAccessResponse, { userName: "Ray Williams", email: "rwilliams@email.com", twoFactorEnabled: false, accountRecoveryEnabled: false, - collections: [ - { id: "c7", name: new EncString("Collection 7"), itemCount: 8 }, - { id: "c8", name: new EncString("Collection 8"), itemCount: 12 }, - { id: "c9", name: new EncString("Collection 9"), itemCount: 16 }, + groupsCount: 2, + collectionsCount: 4, + totalItemCount: 20, + userGuid: "1234", + usesKeyConnector: false, + accessDetails: [ + { + groupId: "", + collectionId: "c7", + groupName: "", + collectionName: new EncString("Collection 7"), + itemCount: 8, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + { + groupId: "", + collectionId: "c8", + groupName: "", + collectionName: new EncString("Collection 8"), + itemCount: 12, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + { + groupId: "", + collectionId: "c9", + groupName: "", + collectionName: new EncString("Collection 9"), + itemCount: 16, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + { + groupId: "g9", + collectionId: "c7", + groupName: "Group 9", + collectionName: new EncString("Collection 7"), + itemCount: 8, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + { + groupId: "g10", + collectionId: "c8", + groupName: "Group 10", + collectionName: new EncString("Collection 8"), + itemCount: 12, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, + { + groupId: "g11", + collectionId: "c9", + groupName: "Group 11", + collectionName: new EncString("Collection 9"), + itemCount: 16, + readOnly: false, + hidePasswords: false, + manage: false, + } as MemberAccessDetails, ], - groups: [ - { - id: "g9", - name: "Group 9", - itemCount: 1, - collections: [{ id: "c7", name: new EncString("Collection 7"), itemCount: 8 }], - }, - { - id: "g10", - name: "Group 10", - itemCount: 1, - collections: [{ id: "c8", name: new EncString("Collection 8"), itemCount: 12 }], - }, - { - id: "g11", - name: "Group 11", - itemCount: 1, - collections: [{ id: "c9", name: new EncString("Collection 9"), itemCount: 16 }], - }, - ], - }, + } as MemberAccessResponse, ]; diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts index 6a4e53ce23..20d33e314a 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts @@ -1,5 +1,6 @@ import { mock } from "jest-mock-extended"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrganizationId } from "@bitwarden/common/src/types/guid"; import { MemberAccessReportApiService } from "./member-access-report-api.service"; @@ -9,44 +10,56 @@ describe("ImportService", () => { const mockOrganizationId = "mockOrgId" as OrganizationId; const reportApiService = mock(); let memberAccessReportService: MemberAccessReportService; + const i18nService = mock(); beforeEach(() => { - reportApiService.getMemberAccessData.mockImplementation(() => memberAccessReportsMock); - memberAccessReportService = new MemberAccessReportService(reportApiService); + reportApiService.getMemberAccessData.mockImplementation(() => + Promise.resolve(memberAccessReportsMock), + ); + memberAccessReportService = new MemberAccessReportService(reportApiService, i18nService); }); describe("generateMemberAccessReportView", () => { - it("should generate member access report view", () => { - const result = memberAccessReportService.generateMemberAccessReportView(); + it("should generate member access report view", async () => { + const result = + await memberAccessReportService.generateMemberAccessReportView(mockOrganizationId); expect(result).toEqual([ { name: "Sarah Johnson", email: "sjohnson@email.com", collectionsCount: 4, - groupsCount: 3, - itemsCount: 70, + groupsCount: 2, + itemsCount: 20, + userGuid: expect.any(String), + usesKeyConnector: expect.any(Boolean), }, { name: "James Lull", email: "jlull@email.com", - collectionsCount: 2, + collectionsCount: 4, groupsCount: 2, itemsCount: 20, + userGuid: expect.any(String), + usesKeyConnector: expect.any(Boolean), }, { name: "Beth Williams", email: "bwilliams@email.com", - collectionsCount: 2, - groupsCount: 1, - itemsCount: 60, + collectionsCount: 4, + groupsCount: 2, + itemsCount: 20, + userGuid: expect.any(String), + usesKeyConnector: expect.any(Boolean), }, { name: "Ray Williams", email: "rwilliams@email.com", - collectionsCount: 3, - groupsCount: 3, - itemsCount: 36, + collectionsCount: 4, + groupsCount: 2, + itemsCount: 20, + userGuid: expect.any(String), + usesKeyConnector: expect.any(Boolean), }, ]); }); @@ -57,7 +70,24 @@ describe("ImportService", () => { const result = await memberAccessReportService.generateUserReportExportItems(mockOrganizationId); - expect(result).toEqual( + const filteredReportItems = result + .filter( + (item) => + (item.name === "Sarah Johnson" && + item.group === "Group 1" && + item.totalItems === "20") || + (item.name === "James Lull" && item.group === "Group 4" && item.totalItems === "5"), + ) + .map((item) => ({ + name: item.name, + email: item.email, + group: item.group, + totalItems: item.totalItems, + accountRecovery: item.accountRecovery, + twoStepLogin: item.twoStepLogin, + })); + + expect(filteredReportItems).toEqual( expect.arrayContaining([ expect.objectContaining({ email: "sjohnson@email.com", @@ -65,19 +95,15 @@ describe("ImportService", () => { twoStepLogin: "On", accountRecovery: "On", group: "Group 1", - collection: expect.any(String), - collectionPermission: "read only", - totalItems: "10", + totalItems: "20", }), expect.objectContaining({ email: "jlull@email.com", name: "James Lull", twoStepLogin: "Off", accountRecovery: "Off", - group: "(No group)", - collection: expect.any(String), - collectionPermission: "read only", - totalItems: "15", + group: "Group 4", + totalItems: "5", }), ]), ); diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts index 6f0cb92646..4ab7ffd6e4 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts @@ -1,16 +1,15 @@ import { Injectable } from "@angular/core"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { OrganizationId } from "@bitwarden/common/types/guid"; +import { CollectionAccessSelectionView } from "@bitwarden/web-vault/app/admin-console/organizations/core/views"; import { - collectProperty, - getUniqueItems, - sumValue, -} from "@bitwarden/web-vault/app/tools/reports/report-utils"; + getPermissionList, + convertToPermission, +} from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/access-selector"; -import { - MemberAccessCollectionModel, - MemberAccessGroupModel, -} from "../model/member-access-report.model"; +import { MemberAccessDetails } from "../response/member-access-report.response"; import { MemberAccessExportItem } from "../view/member-access-export.view"; import { MemberAccessReportView } from "../view/member-access-report.view"; @@ -18,7 +17,10 @@ import { MemberAccessReportApiService } from "./member-access-report-api.service @Injectable({ providedIn: "root" }) export class MemberAccessReportService { - constructor(private reportApiService: MemberAccessReportApiService) {} + constructor( + private reportApiService: MemberAccessReportApiService, + private i18nService: I18nService, + ) {} /** * Transforms user data into a MemberAccessReportView. * @@ -26,88 +28,68 @@ export class MemberAccessReportService { * @param {ReportCollection[]} collections - An array of collections, each with an ID and a total number of items. * @returns {MemberAccessReportView} The aggregated report view. */ - generateMemberAccessReportView(): MemberAccessReportView[] { - const memberAccessReportViewCollection: MemberAccessReportView[] = []; - const memberAccessData = this.reportApiService.getMemberAccessData(); - memberAccessData.forEach((userData) => { - const name = userData.userName; - const email = userData.email; - const groupCollections = collectProperty< - MemberAccessGroupModel, - "collections", - MemberAccessCollectionModel - >(userData.groups, "collections"); - - const uniqueCollections = getUniqueItems( - [...groupCollections, ...userData.collections], - (item: MemberAccessCollectionModel) => item.id, - ); - const collectionsCount = uniqueCollections.length; - const groupsCount = userData.groups.length; - const itemsCount = sumValue( - uniqueCollections, - (collection: MemberAccessCollectionModel) => collection.itemCount, - ); - - memberAccessReportViewCollection.push({ - name: name, - email: email, - collectionsCount: collectionsCount, - groupsCount: groupsCount, - itemsCount: itemsCount, - }); - }); - + async generateMemberAccessReportView( + organizationId: OrganizationId, + ): Promise { + const memberAccessData = await this.reportApiService.getMemberAccessData(organizationId); + const memberAccessReportViewCollection = memberAccessData.map((userData) => ({ + name: userData.userName, + email: userData.email, + collectionsCount: userData.collectionsCount, + groupsCount: userData.groupsCount, + itemsCount: userData.totalItemCount, + userGuid: userData.userGuid, + usesKeyConnector: userData.usesKeyConnector, + })); return memberAccessReportViewCollection; } async generateUserReportExportItems( organizationId: OrganizationId, ): Promise { - const memberAccessReports = this.reportApiService.getMemberAccessData(); - const userReportItemPromises = memberAccessReports.flatMap(async (memberAccessReport) => { - const partialMemberReportItem: Partial = { - email: memberAccessReport.email, - name: memberAccessReport.userName, - twoStepLogin: memberAccessReport.twoFactorEnabled ? "On" : "Off", - accountRecovery: memberAccessReport.accountRecoveryEnabled ? "On" : "Off", - }; - const groupCollectionPromises = memberAccessReport.groups.map(async (group) => { - const groupPartialReportItem = { ...partialMemberReportItem, group: group.name }; - return await this.buildReportItemFromCollection( - group.collections, - groupPartialReportItem, - organizationId, - ); + const memberAccessReports = await this.reportApiService.getMemberAccessData(organizationId); + const collectionNames = memberAccessReports.flatMap((item) => + item.accessDetails.map((dtl) => { + if (dtl.collectionName) { + return dtl.collectionName.encryptedString; + } + }), + ); + const collectionNameMap = new Map(collectionNames.map((col) => [col, ""])); + for await (const key of collectionNameMap.keys()) { + const decrypted = new EncString(key); + await decrypted.decrypt(organizationId); + collectionNameMap.set(key, decrypted.decryptedValue); + } + + const exportItems = memberAccessReports.flatMap((report) => { + const userDetails = report.accessDetails.map((detail) => { + return { + email: report.email, + name: report.userName, + twoStepLogin: report.twoFactorEnabled ? "On" : "Off", + accountRecovery: report.accountRecoveryEnabled ? "On" : "Off", + group: detail.groupName, + collection: collectionNameMap.get(detail.collectionName.encryptedString), + collectionPermission: this.getPermissionText(detail), + totalItems: detail.itemCount.toString(), + }; }); - const noGroupPartialReportItem = { ...partialMemberReportItem, group: "(No group)" }; - const noGroupCollectionPromises = await this.buildReportItemFromCollection( - memberAccessReport.collections, - noGroupPartialReportItem, - organizationId, - ); - - return Promise.all([...groupCollectionPromises, noGroupCollectionPromises]); + return userDetails; }); - - const nestedUserReportItems = (await Promise.all(userReportItemPromises)).flat(); - return nestedUserReportItems.flat(); + return exportItems.flat(); } - async buildReportItemFromCollection( - memberAccessCollections: MemberAccessCollectionModel[], - partialReportItem: Partial, - organizationId: string, - ): Promise { - const reportItemPromises = memberAccessCollections.map(async (collection) => { - return { - ...partialReportItem, - collection: await collection.name.decrypt(organizationId), - collectionPermission: "read only", //TODO update this value - totalItems: collection.itemCount.toString(), - }; + private getPermissionText(accessDetails: MemberAccessDetails): string { + const permissionList = getPermissionList(); + const collectionSelectionView = new CollectionAccessSelectionView({ + id: accessDetails.groupId ?? accessDetails.collectionId, + readOnly: accessDetails.readOnly, + hidePasswords: accessDetails.hidePasswords, + manage: accessDetails.manage, }); - - return Promise.all(reportItemPromises); + return this.i18nService.t( + permissionList.find((p) => p.perm === convertToPermission(collectionSelectionView))?.labelId, + ); } } diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/view/member-access-report.view.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/view/member-access-report.view.ts index eeb8cfee4f..5412babc0e 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/view/member-access-report.view.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/view/member-access-report.view.ts @@ -1,7 +1,11 @@ +import { Guid } from "@bitwarden/common/types/guid"; + export type MemberAccessReportView = { name: string; email: string; collectionsCount: number; groupsCount: number; itemsCount: number; + userGuid: Guid; + usesKeyConnector: boolean; };