[AC-2509] add member access component (#9411)

* Added new report card and FeatureFlag for MemberAccessReport

* Add new "isEnterpriseOrgGuard"

* Add member access icon

* Show upgrade organization dialog for enterprise on member access report click

* verify member access featureflag on enterprise org guard

* add comment with TODO information for follow up task

* Initial member access report component

* Improved readability, removed path to wrong component and refactored buildReports to use the productType

* finished MemberAccessReport layout and added temporary service to provide mock data

* Moved member-access-report files to bitwarden_license/
Removed unnecessary files

* Added new tools path on bitwarden_license to the CODEOWNERS file

* added member access description to the messages.json

* layout changes to member access report

* Created new reports-routing under bitwarden_license
Moved member-access-report files to corresponding subfolder

* Added search logic

* Removed routing from member-access-report BL component on OSS.
Added member-access-report navigation to organizations-routing on BL

* removed unnecessary ng-container

* Added OrganizationPermissionsGuard and canAccessReports validation to member-access-report navigation

* replaced deprecated search code with searchControl

* Address PR feedback

* removed unnecessary canAccessReports method
This commit is contained in:
aj-rosado 2024-07-09 18:36:18 +01:00 committed by GitHub
parent dd40faf72e
commit 0e2c486a38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 151 additions and 0 deletions

1
.github/CODEOWNERS vendored
View File

@ -29,6 +29,7 @@ libs/common/src/models/export @bitwarden/team-tools-dev
libs/common/src/tools @bitwarden/team-tools-dev
libs/importer @bitwarden/team-tools-dev
libs/tools @bitwarden/team-tools-dev
bitwarden_license/bit-web/src/app/tools @bitwarden/team-tools-dev
## Localization/Crowdin (Tools team)
apps/browser/src/_locales @bitwarden/team-tools-dev

View File

@ -8445,6 +8445,9 @@
"memberAccessReportDesc": {
"message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations."
},
"memberAccessReportPageDesc": {
"message": "Audit organization member access across groups, collections, and collection items. The CSV export provides a detailed breakdown per member, including information on collection permissions and account configurations."
},
"higherKDFIterations": {
"message": "Higher KDF iterations can help protect your master password from being brute forced by an attacker."
},

View File

@ -3,6 +3,7 @@ import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/auth/guards";
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { IsEnterpriseOrgGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/is-enterprise-org.guard";
import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard";
import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-console/organizations/layouts/organization-layout.component";
@ -58,6 +59,23 @@ const routes: Routes = [
},
],
},
{
path: "reporting/reports",
canActivate: [AuthGuard, organizationPermissionsGuard((org) => org.canAccessReports)],
children: [
{
path: "member-access-report",
loadComponent: () =>
import(
"../../tools/reports/member-access-report/member-access-report.component"
).then((mod) => mod.MemberAccessReportComponent),
data: {
titleId: "memberAccessReport",
},
canActivate: [IsEnterpriseOrgGuard],
},
],
},
],
},
];

View File

@ -0,0 +1,50 @@
<app-header>
<bit-search
[formControl]="searchControl"
[placeholder]="'searchMembers' | i18n"
class="tw-grow"
></bit-search>
<button type="button" bitButton buttonType="primary">
<span>{{ "export" | i18n }}</span>
<i class="bwi bwi-fw bwi-sign-in" aria-hidden="true"></i>
</button>
</app-header>
<div class="tw-max-w-4xl">
<p bitTypography="body1">
{{ "memberAccessReportPageDesc" | i18n }}
</p>
</div>
<bit-table [dataSource]="dataSource" class="tw-mt-2">
<ng-container header>
<tr>
<th bitCell bitSortable="name" default>Members</th>
<th bitCell bitSortable="groups">Groups</th>
<th bitCell bitSortable="collections">Collections</th>
<th bitCell bitSortable="items">Items</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td bitCell>
<div class="tw-flex tw-items-center">
<bit-avatar size="small" [text]="r.name" class="tw-mr-3"></bit-avatar>
<div class="tw-flex tw-flex-col">
<button type="button" bitLink>
{{ r.name }}
</button>
<div class="tw-text-sm tw-mt-1 tw-text-muted">
{{ r.email }}
</div>
</div>
</div>
</td>
<td bitCell class="tw-text-muted tw-w-[278px] tw-p-4">{{ r.groups }}</td>
<td bitCell class="tw-text-muted tw-w-[278px] tw-p-4">{{ r.collections }}</td>
<td bitCell class="tw-text-muted tw-w-[278px] tw-p-4">{{ r.items }}</td>
</tr>
</ng-template>
</bit-table>

View File

@ -0,0 +1,33 @@
import { Component, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms";
import { debounceTime } from "rxjs";
import { SearchModule, TableDataSource } from "@bitwarden/components";
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { MemberAccessReportService } from "./member-access-report.service";
import { MemberAccessReportView } from "./view/member-access-report.view";
@Component({
selector: "member-access-report",
templateUrl: "member-access-report.component.html",
imports: [SharedModule, SearchModule, HeaderModule],
standalone: true,
})
export class MemberAccessReportComponent implements OnInit {
protected dataSource = new TableDataSource<MemberAccessReportView>();
protected searchControl = new FormControl("", { nonNullable: true });
constructor(protected reportService: MemberAccessReportService) {
// Connect the search input to the table dataSource filter input
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
}
ngOnInit() {
this.dataSource.data = this.reportService.getMemberAccessMockData();
}
}

View File

@ -0,0 +1,39 @@
import { Injectable } from "@angular/core";
import { MemberAccessReportView } from "./view/member-access-report.view";
@Injectable({ providedIn: "root" })
export class MemberAccessReportService {
//Temporary method to provide mock data for test purposes only
getMemberAccessMockData(): MemberAccessReportView[] {
const memberAccess = new MemberAccessReportView();
memberAccess.email = "sjohnson@email.com";
memberAccess.name = "Sarah Johnson";
memberAccess.groups = 3;
memberAccess.collections = 12;
memberAccess.items = 3;
const memberAccess2 = new MemberAccessReportView();
memberAccess2.email = "jlull@email.com";
memberAccess2.name = "James Lull";
memberAccess2.groups = 2;
memberAccess2.collections = 24;
memberAccess2.items = 2;
const memberAccess3 = new MemberAccessReportView();
memberAccess3.email = "bwilliams@email.com";
memberAccess3.name = "Beth Williams";
memberAccess3.groups = 6;
memberAccess3.collections = 12;
memberAccess3.items = 1;
const memberAccess4 = new MemberAccessReportView();
memberAccess4.email = "rwilliams@email.com";
memberAccess4.name = "Ray Williams";
memberAccess4.groups = 5;
memberAccess4.collections = 21;
memberAccess4.items = 2;
return [memberAccess, memberAccess2, memberAccess3, memberAccess4];
}
}

View File

@ -0,0 +1,7 @@
export class MemberAccessReportView {
name: string;
email: string;
collections: number;
groups: number;
items: number;
}