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:
parent
0419f91df6
commit
a5ea22d0cc
@ -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>
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -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[];
|
|
||||||
};
|
};
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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[]>;
|
||||||
|
@ -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 }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
@ -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",
|
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user