1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-25 12:15:18 +01:00

[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>
This commit is contained in:
Tom 2024-09-04 14:33:43 -04:00 committed by GitHub
parent 0419f91df6
commit a5ea22d0cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 455 additions and 236 deletions

View File

@ -32,7 +32,7 @@
<div class="tw-flex tw-items-center"> <div class="tw-flex tw-items-center">
<bit-avatar size="small" [text]="r.name" class="tw-mr-3"></bit-avatar> <bit-avatar size="small" [text]="r.name" class="tw-mr-3"></bit-avatar>
<div class="tw-flex tw-flex-col"> <div class="tw-flex tw-flex-col">
<button type="button" bitLink> <button type="button" bitLink (click)="edit(r)">
{{ r.name }} {{ r.name }}
</button> </button>

View File

@ -2,13 +2,22 @@ import { Component, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms"; import { FormControl } from "@angular/forms";
import { ActivatedRoute } from "@angular/router"; 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 { 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 { 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 { 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 { 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 { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { exportToCSV } from "@bitwarden/web-vault/app/tools/reports/report-utils"; import { exportToCSV } from "@bitwarden/web-vault/app/tools/reports/report-utils";
@ -22,12 +31,12 @@ import { MemberAccessReportView } from "./view/member-access-report.view";
@Component({ @Component({
selector: "member-access-report", selector: "member-access-report",
templateUrl: "member-access-report.component.html", templateUrl: "member-access-report.component.html",
imports: [SharedModule, SearchModule, HeaderModule], imports: [SharedModule, SearchModule, HeaderModule, CoreOrganizationModule],
providers: [ providers: [
safeProvider({ safeProvider({
provide: MemberAccessReportServiceAbstraction, provide: MemberAccessReportServiceAbstraction,
useClass: MemberAccessReportService, useClass: MemberAccessReportService,
deps: [MemberAccessReportApiService], deps: [MemberAccessReportApiService, I18nService],
}), }),
], ],
standalone: true, standalone: true,
@ -36,11 +45,15 @@ export class MemberAccessReportComponent implements OnInit {
protected dataSource = new TableDataSource<MemberAccessReportView>(); protected dataSource = new TableDataSource<MemberAccessReportView>();
protected searchControl = new FormControl("", { nonNullable: true }); protected searchControl = new FormControl("", { nonNullable: true });
protected organizationId: OrganizationId; protected organizationId: OrganizationId;
protected orgIsOnSecretsManagerStandalone: boolean;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
protected reportService: MemberAccessReportService, protected reportService: MemberAccessReportService,
protected fileDownloadService: FileDownloadService, protected fileDownloadService: FileDownloadService,
protected dialogService: DialogService,
protected userNamePipe: UserNamePipe,
protected billingApiService: BillingApiServiceAbstraction,
) { ) {
// Connect the search input to the table dataSource filter input // Connect the search input to the table dataSource filter input
this.searchControl.valueChanges this.searchControl.valueChanges
@ -51,7 +64,20 @@ export class MemberAccessReportComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
const params = await firstValueFrom(this.route.params); const params = await firstValueFrom(this.route.params);
this.organizationId = params.organizationId; 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<void> => { exportReportAction = async (): Promise<void> => {
@ -64,4 +90,29 @@ export class MemberAccessReportComponent implements OnInit {
blobOptions: { type: "text/plain" }, blobOptions: { type: "text/plain" },
}); });
}; };
edit = async (user: MemberAccessReportView | null): Promise<void> => {
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;
}
};
} }

View File

@ -1,23 +1,16 @@
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
export type MemberAccessCollectionModel = { /**
id: string; * Details for the parents MemberAccessReport
name: EncString; */
export type MemberAccessDetails = {
collectionId: string;
groupId: string;
groupName: string;
// Comes encrypted from the server
collectionName: EncString;
itemCount: number; itemCount: number;
}; readOnly: boolean;
hidePasswords: boolean;
export type MemberAccessGroupModel = { manage: boolean;
id: string;
name: string;
itemCount: number;
collections: MemberAccessCollectionModel[];
};
export type MemberAccessReportModel = {
userName: string;
email: string;
twoFactorEnabled: boolean;
accountRecoveryEnabled: boolean;
collections: MemberAccessCollectionModel[];
groups: MemberAccessGroupModel[];
}; };

View File

@ -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));
}
}
}

View File

@ -1,12 +1,22 @@
import { Injectable } from "@angular/core"; 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" }) @Injectable({ providedIn: "root" })
export class MemberAccessReportApiService { export class MemberAccessReportApiService {
getMemberAccessData(): MemberAccessReportModel[] { constructor(protected apiService: ApiService) {}
return memberAccessReportsMock; async getMemberAccessData(orgId: string): Promise<MemberAccessResponse[]> {
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;
} }
} }

View File

@ -4,7 +4,9 @@ import { MemberAccessExportItem } from "../view/member-access-export.view";
import { MemberAccessReportView } from "../view/member-access-report.view"; import { MemberAccessReportView } from "../view/member-access-report.view";
export abstract class MemberAccessReportServiceAbstraction { export abstract class MemberAccessReportServiceAbstraction {
generateMemberAccessReportView: () => MemberAccessReportView[]; generateMemberAccessReportView: (
organizationId: OrganizationId,
) => Promise<MemberAccessReportView[]>;
generateUserReportExportItems: ( generateUserReportExportItems: (
organizationId: OrganizationId, organizationId: OrganizationId,
) => Promise<MemberAccessExportItem[]>; ) => Promise<MemberAccessExportItem[]>;

View File

@ -1,137 +1,231 @@
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; 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", userName: "Sarah Johnson",
email: "sjohnson@email.com", email: "sjohnson@email.com",
twoFactorEnabled: true, twoFactorEnabled: true,
accountRecoveryEnabled: true, accountRecoveryEnabled: true,
collections: [ groupsCount: 2,
collectionsCount: 4,
totalItemCount: 20,
userGuid: "1234",
usesKeyConnector: false,
accessDetails: [
{ {
id: "c1", groupId: "",
name: new EncString( collectionId: "c1",
"2.UiXa3L3Ol1G4QnfFfBjMQw==|sbVTj0EiEkhIrDiropn2Cg==|82P78YgmapW4TdN9jQJgMWKv2gGyK1AnGkr+W9/sq+A=", collectionName: new EncString("Collection 1"),
), groupName: "",
itemCount: 10, itemCount: 10,
}, readOnly: false,
{ id: "c2", name: new EncString("Collection 2"), itemCount: 20 }, hidePasswords: false,
{ id: "c3", name: new EncString("Collection 3"), itemCount: 30 }, manage: false,
], } as MemberAccessDetails,
groups: [
{ {
id: "g1", groupId: "",
name: "Group 1", collectionId: "c2",
itemCount: 3, collectionName: new EncString("Collection 2"),
collections: [ groupName: "",
itemCount: 20,
readOnly: false,
hidePasswords: false,
manage: false,
} as MemberAccessDetails,
{ {
id: "c6", groupId: "",
name: new EncString( collectionId: "c3",
"2.UiXa3L3Ol1G4QnfFfBjMQw==|sbVTj0EiEkhIrDiropn2Cg==|82P78YgmapW4TdN9jQJgMWKv2gGyK1AnGkr+W9/sq+A=", collectionName: new EncString("Collection 3"),
), groupName: "",
itemCount: 10, itemCount: 30,
}, readOnly: false,
{ id: "c2", name: new EncString("Collection 2"), itemCount: 20 }, hidePasswords: false,
], manage: false,
}, } as MemberAccessDetails,
{ {
id: "g2", groupId: "g1",
name: "Group 2", collectionId: "c1",
itemCount: 2, collectionName: new EncString("Collection 1"),
collections: [ groupName: "Group 1",
{ id: "c2", name: new EncString("Collection 2"), itemCount: 20 }, itemCount: 30,
{ id: "c3", name: new EncString("Collection 3"), itemCount: 30 }, readOnly: false,
], hidePasswords: false,
}, manage: false,
} as MemberAccessDetails,
{ {
id: "g3", groupId: "g1",
name: "Group 3", collectionId: "c2",
itemCount: 2, collectionName: new EncString("Collection 2"),
collections: [ groupName: "Group 1",
{ itemCount: 20,
id: "c1", readOnly: false,
name: new EncString( hidePasswords: false,
"2.UiXa3L3Ol1G4QnfFfBjMQw==|sbVTj0EiEkhIrDiropn2Cg==|82P78YgmapW4TdN9jQJgMWKv2gGyK1AnGkr+W9/sq+A=", manage: false,
), } as MemberAccessDetails,
itemCount: 10,
},
{ id: "c3", name: new EncString("Collection 3"), itemCount: 30 },
], ],
}, } as MemberAccessResponse,
],
},
{ {
userName: "James Lull", userName: "James Lull",
email: "jlull@email.com", email: "jlull@email.com",
twoFactorEnabled: false, twoFactorEnabled: false,
accountRecoveryEnabled: false, accountRecoveryEnabled: false,
collections: [ groupsCount: 2,
{ id: "c4", name: new EncString("Collection 4"), itemCount: 5 }, collectionsCount: 4,
{ id: "c5", name: new EncString("Collection 5"), itemCount: 15 }, totalItemCount: 20,
], userGuid: "1234",
groups: [ usesKeyConnector: false,
accessDetails: [
{ {
id: "g4", groupId: "g4",
name: "Group 4", collectionId: "c4",
itemCount: 2, groupName: "Group 4",
collections: [ collectionName: new EncString("Collection 4"),
{ id: "c4", name: new EncString("Collection 4"), itemCount: 5 }, itemCount: 5,
{ id: "c5", name: new EncString("Collection 5"), itemCount: 15 }, readOnly: false,
], hidePasswords: false,
}, manage: false,
} as MemberAccessDetails,
{ {
id: "g5", groupId: "g4",
name: "Group 5", collectionId: "c5",
itemCount: 1, groupName: "Group 4",
collections: [{ id: "c5", name: new EncString("Collection 5"), itemCount: 15 }], 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", userName: "Beth Williams",
email: "bwilliams@email.com", email: "bwilliams@email.com",
twoFactorEnabled: true, twoFactorEnabled: true,
accountRecoveryEnabled: true, accountRecoveryEnabled: true,
collections: [{ id: "c6", name: new EncString("Collection 6"), itemCount: 25 }], groupsCount: 2,
groups: [ collectionsCount: 4,
totalItemCount: 20,
userGuid: "1234",
usesKeyConnector: false,
accessDetails: [
{ {
id: "g6", groupId: "",
name: "Group 6", collectionId: "c6",
itemCount: 1, groupName: "",
collections: [{ id: "c4", name: new EncString("Collection 4"), itemCount: 35 }], 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", userName: "Ray Williams",
email: "rwilliams@email.com", email: "rwilliams@email.com",
twoFactorEnabled: false, twoFactorEnabled: false,
accountRecoveryEnabled: false, accountRecoveryEnabled: false,
collections: [ groupsCount: 2,
{ id: "c7", name: new EncString("Collection 7"), itemCount: 8 }, collectionsCount: 4,
{ id: "c8", name: new EncString("Collection 8"), itemCount: 12 }, totalItemCount: 20,
{ id: "c9", name: new EncString("Collection 9"), itemCount: 16 }, 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: [ } as MemberAccessResponse,
{
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 }],
},
],
},
]; ];

View File

@ -1,5 +1,6 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { OrganizationId } from "@bitwarden/common/src/types/guid"; import { OrganizationId } from "@bitwarden/common/src/types/guid";
import { MemberAccessReportApiService } from "./member-access-report-api.service"; import { MemberAccessReportApiService } from "./member-access-report-api.service";
@ -9,44 +10,56 @@ describe("ImportService", () => {
const mockOrganizationId = "mockOrgId" as OrganizationId; const mockOrganizationId = "mockOrgId" as OrganizationId;
const reportApiService = mock<MemberAccessReportApiService>(); const reportApiService = mock<MemberAccessReportApiService>();
let memberAccessReportService: MemberAccessReportService; let memberAccessReportService: MemberAccessReportService;
const i18nService = mock<I18nService>();
beforeEach(() => { beforeEach(() => {
reportApiService.getMemberAccessData.mockImplementation(() => memberAccessReportsMock); reportApiService.getMemberAccessData.mockImplementation(() =>
memberAccessReportService = new MemberAccessReportService(reportApiService); Promise.resolve(memberAccessReportsMock),
);
memberAccessReportService = new MemberAccessReportService(reportApiService, i18nService);
}); });
describe("generateMemberAccessReportView", () => { describe("generateMemberAccessReportView", () => {
it("should generate member access report view", () => { it("should generate member access report view", async () => {
const result = memberAccessReportService.generateMemberAccessReportView(); const result =
await memberAccessReportService.generateMemberAccessReportView(mockOrganizationId);
expect(result).toEqual([ expect(result).toEqual([
{ {
name: "Sarah Johnson", name: "Sarah Johnson",
email: "sjohnson@email.com", email: "sjohnson@email.com",
collectionsCount: 4, collectionsCount: 4,
groupsCount: 3, groupsCount: 2,
itemsCount: 70, itemsCount: 20,
userGuid: expect.any(String),
usesKeyConnector: expect.any(Boolean),
}, },
{ {
name: "James Lull", name: "James Lull",
email: "jlull@email.com", email: "jlull@email.com",
collectionsCount: 2, collectionsCount: 4,
groupsCount: 2, groupsCount: 2,
itemsCount: 20, itemsCount: 20,
userGuid: expect.any(String),
usesKeyConnector: expect.any(Boolean),
}, },
{ {
name: "Beth Williams", name: "Beth Williams",
email: "bwilliams@email.com", email: "bwilliams@email.com",
collectionsCount: 2, collectionsCount: 4,
groupsCount: 1, groupsCount: 2,
itemsCount: 60, itemsCount: 20,
userGuid: expect.any(String),
usesKeyConnector: expect.any(Boolean),
}, },
{ {
name: "Ray Williams", name: "Ray Williams",
email: "rwilliams@email.com", email: "rwilliams@email.com",
collectionsCount: 3, collectionsCount: 4,
groupsCount: 3, groupsCount: 2,
itemsCount: 36, itemsCount: 20,
userGuid: expect.any(String),
usesKeyConnector: expect.any(Boolean),
}, },
]); ]);
}); });
@ -57,7 +70,24 @@ describe("ImportService", () => {
const result = const result =
await memberAccessReportService.generateUserReportExportItems(mockOrganizationId); 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.arrayContaining([
expect.objectContaining({ expect.objectContaining({
email: "sjohnson@email.com", email: "sjohnson@email.com",
@ -65,19 +95,15 @@ describe("ImportService", () => {
twoStepLogin: "On", twoStepLogin: "On",
accountRecovery: "On", accountRecovery: "On",
group: "Group 1", group: "Group 1",
collection: expect.any(String), totalItems: "20",
collectionPermission: "read only",
totalItems: "10",
}), }),
expect.objectContaining({ expect.objectContaining({
email: "jlull@email.com", email: "jlull@email.com",
name: "James Lull", name: "James Lull",
twoStepLogin: "Off", twoStepLogin: "Off",
accountRecovery: "Off", accountRecovery: "Off",
group: "(No group)", group: "Group 4",
collection: expect.any(String), totalItems: "5",
collectionPermission: "read only",
totalItems: "15",
}), }),
]), ]),
); );

View File

@ -1,16 +1,15 @@
import { Injectable } from "@angular/core"; 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 { OrganizationId } from "@bitwarden/common/types/guid";
import { CollectionAccessSelectionView } from "@bitwarden/web-vault/app/admin-console/organizations/core/views";
import { import {
collectProperty, getPermissionList,
getUniqueItems, convertToPermission,
sumValue, } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/access-selector";
} from "@bitwarden/web-vault/app/tools/reports/report-utils";
import { import { MemberAccessDetails } from "../response/member-access-report.response";
MemberAccessCollectionModel,
MemberAccessGroupModel,
} from "../model/member-access-report.model";
import { MemberAccessExportItem } from "../view/member-access-export.view"; import { MemberAccessExportItem } from "../view/member-access-export.view";
import { MemberAccessReportView } from "../view/member-access-report.view"; import { MemberAccessReportView } from "../view/member-access-report.view";
@ -18,7 +17,10 @@ import { MemberAccessReportApiService } from "./member-access-report-api.service
@Injectable({ providedIn: "root" }) @Injectable({ providedIn: "root" })
export class MemberAccessReportService { export class MemberAccessReportService {
constructor(private reportApiService: MemberAccessReportApiService) {} constructor(
private reportApiService: MemberAccessReportApiService,
private i18nService: I18nService,
) {}
/** /**
* Transforms user data into a MemberAccessReportView. * 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. * @param {ReportCollection[]} collections - An array of collections, each with an ID and a total number of items.
* @returns {MemberAccessReportView} The aggregated report view. * @returns {MemberAccessReportView} The aggregated report view.
*/ */
generateMemberAccessReportView(): MemberAccessReportView[] { async generateMemberAccessReportView(
const memberAccessReportViewCollection: MemberAccessReportView[] = []; organizationId: OrganizationId,
const memberAccessData = this.reportApiService.getMemberAccessData(); ): Promise<MemberAccessReportView[]> {
memberAccessData.forEach((userData) => { const memberAccessData = await this.reportApiService.getMemberAccessData(organizationId);
const name = userData.userName; const memberAccessReportViewCollection = memberAccessData.map((userData) => ({
const email = userData.email; name: userData.userName,
const groupCollections = collectProperty< email: userData.email,
MemberAccessGroupModel, collectionsCount: userData.collectionsCount,
"collections", groupsCount: userData.groupsCount,
MemberAccessCollectionModel itemsCount: userData.totalItemCount,
>(userData.groups, "collections"); userGuid: userData.userGuid,
usesKeyConnector: userData.usesKeyConnector,
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,
});
});
return memberAccessReportViewCollection; return memberAccessReportViewCollection;
} }
async generateUserReportExportItems( async generateUserReportExportItems(
organizationId: OrganizationId, organizationId: OrganizationId,
): Promise<MemberAccessExportItem[]> { ): Promise<MemberAccessExportItem[]> {
const memberAccessReports = this.reportApiService.getMemberAccessData(); const memberAccessReports = await this.reportApiService.getMemberAccessData(organizationId);
const userReportItemPromises = memberAccessReports.flatMap(async (memberAccessReport) => { const collectionNames = memberAccessReports.flatMap((item) =>
const partialMemberReportItem: Partial<MemberAccessExportItem> = { item.accessDetails.map((dtl) => {
email: memberAccessReport.email, if (dtl.collectionName) {
name: memberAccessReport.userName, return dtl.collectionName.encryptedString;
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 collectionNameMap = new Map(collectionNames.map((col) => [col, ""]));
const noGroupPartialReportItem = { ...partialMemberReportItem, group: "(No group)" }; for await (const key of collectionNameMap.keys()) {
const noGroupCollectionPromises = await this.buildReportItemFromCollection( const decrypted = new EncString(key);
memberAccessReport.collections, await decrypted.decrypt(organizationId);
noGroupPartialReportItem, collectionNameMap.set(key, decrypted.decryptedValue);
organizationId,
);
return Promise.all([...groupCollectionPromises, noGroupCollectionPromises]);
});
const nestedUserReportItems = (await Promise.all(userReportItemPromises)).flat();
return nestedUserReportItems.flat();
} }
async buildReportItemFromCollection( const exportItems = memberAccessReports.flatMap((report) => {
memberAccessCollections: MemberAccessCollectionModel[], const userDetails = report.accessDetails.map((detail) => {
partialReportItem: Partial<MemberAccessExportItem>,
organizationId: string,
): Promise<MemberAccessExportItem[]> {
const reportItemPromises = memberAccessCollections.map(async (collection) => {
return { return {
...partialReportItem, email: report.email,
collection: await collection.name.decrypt(organizationId), name: report.userName,
collectionPermission: "read only", //TODO update this value twoStepLogin: report.twoFactorEnabled ? "On" : "Off",
totalItems: collection.itemCount.toString(), accountRecovery: report.accountRecoveryEnabled ? "On" : "Off",
group: detail.groupName,
collection: collectionNameMap.get(detail.collectionName.encryptedString),
collectionPermission: this.getPermissionText(detail),
totalItems: detail.itemCount.toString(),
}; };
}); });
return userDetails;
});
return exportItems.flat();
}
return Promise.all(reportItemPromises); 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 this.i18nService.t(
permissionList.find((p) => p.perm === convertToPermission(collectionSelectionView))?.labelId,
);
} }
} }

View File

@ -1,7 +1,11 @@
import { Guid } from "@bitwarden/common/types/guid";
export type MemberAccessReportView = { export type MemberAccessReportView = {
name: string; name: string;
email: string; email: string;
collectionsCount: number; collectionsCount: number;
groupsCount: number; groupsCount: number;
itemsCount: number; itemsCount: number;
userGuid: Guid;
usesKeyConnector: boolean;
}; };