1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-16 08:34:39 +00:00

add org-wide at-risk member dialog

This commit is contained in:
jaasen-livefront
2024-12-16 15:44:14 -08:00
parent 967a372821
commit 7009674d61
6 changed files with 105 additions and 2 deletions

View File

@@ -90,3 +90,12 @@ export type MemberDetailsFlat = {
email: string;
cipherId: string;
};
/**
* At risk member detail that contains the email
* and the count of at risk ciphers
*/
export type AtRiskMemberDetail = {
email: string;
atRiskPasswordCount: number;
};

View File

@@ -14,6 +14,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
ApplicationHealthReportDetail,
ApplicationHealthReportSummary,
AtRiskMemberDetail,
CipherHealthReportDetail,
CipherHealthReportUriDetail,
ExposedPasswordDetail,
@@ -92,6 +93,30 @@ export class RiskInsightsReportService {
return results$;
}
/**
* Generates a list of members with at-risk passwords along with the number of at-risk passwords.
*/
generateAtRiskMemberList(
cipherHealthReportDetails: ApplicationHealthReportDetail[],
): AtRiskMemberDetail[] {
const memberRiskMap = new Map<string, number>();
cipherHealthReportDetails.forEach((app) => {
app.atRiskMemberDetails.forEach((member) => {
if (memberRiskMap.has(member.email)) {
memberRiskMap.set(member.email, memberRiskMap.get(member.email) + 1);
} else {
memberRiskMap.set(member.email, 1);
}
});
});
return Array.from(memberRiskMap.entries()).map(([email, atRiskPasswordCount]) => ({
email,
atRiskPasswordCount,
}));
}
/**
* Gets the summary from the application health report. Returns total members and applications as well
* as the total at risk members and at risk applications

View File

@@ -0,0 +1,27 @@
<bit-dialog>
<ng-container bitDialogTitle>
<span bitDialogTitle>{{ "atRiskMembersWithCount" | i18n: atRiskMembers.length }} </span>
</ng-container>
<ng-container bitDialogContent>
<div class="tw-flex tw-flex-col tw-gap-2">
<span bitTypography="body2" class="tw-text-muted">{{
"atRiskMembersDescription" | i18n
}}</span>
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
<div bitTypography="body2" class="tw-font-bold">{{ "email" | i18n }}</div>
<div bitTypography="body2" class="tw-font-bold">{{ "atRiskPasswords" | i18n }}</div>
</div>
<ng-container *ngFor="let member of atRiskMembers">
<div class="tw-flex tw-justify-between tw-mt-2">
<div>{{ member.email }}</div>
<div>{{ member.atRiskPasswordCount }}</div>
</div>
</ng-container>
</div>
</ng-container>
<ng-container bitDialogFooter>
<button bitButton bitDialogClose buttonType="secondary" type="button">
{{ "ok" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@@ -0,0 +1,24 @@
import { DIALOG_DATA } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { Component, Inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AtRiskMemberDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components";
export const openOrgAtRiskMembersDialog = (
dialogService: DialogService,
dialogConfig: AtRiskMemberDetail[],
) =>
dialogService.open<boolean, AtRiskMemberDetail[]>(OrgAtRiskMembersDialogComponent, {
data: dialogConfig,
});
@Component({
standalone: true,
templateUrl: "./org-at-risk-members-dialog.component.html",
imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule],
})
export class OrgAtRiskMembersDialogComponent {
constructor(@Inject(DIALOG_DATA) protected atRiskMembers: AtRiskMemberDetail[]) {}
}

View File

@@ -5,6 +5,11 @@
{{ "reviewAtRiskPasswords" | i18n }}
&nbsp;<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
</div>
<div>
<button bitButton buttonType="primary" type="button" (click)="showAtRiskMembers()">
{{ "view" | i18n }}
</button>
</div>
<div
class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-4 tw-my-4 tw-flex tw-items-center"
>

View File

@@ -11,15 +11,19 @@ import {
RiskInsightsDataService,
RiskInsightsReportService,
} from "@bitwarden/bit-common/tools/reports/risk-insights";
import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
import {
ApplicationHealthReportDetail,
AtRiskMemberDetail,
} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components";
import { AsyncActionsModule, ButtonModule, DialogService, TabsModule } from "@bitwarden/components";
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
import { AllApplicationsComponent } from "./all-applications.component";
import { CriticalApplicationsComponent } from "./critical-applications.component";
import { NotifiedMembersTableComponent } from "./notified-members-table.component";
import { OrgAtRiskMembersDialogComponent } from "./org-at-risk-members-dialog.component";
export enum RiskInsightsTabType {
AllApps = 0,
@@ -60,12 +64,15 @@ export class RiskInsightsComponent implements OnInit {
isRefreshing$: Observable<boolean> = new Observable<boolean>();
dataLastUpdated$: Observable<Date | null> = new Observable<Date | null>();
refetching: boolean = false;
private atRiskMembers: AtRiskMemberDetail[] = [];
constructor(
private route: ActivatedRoute,
private router: Router,
private configService: ConfigService,
private dataService: RiskInsightsDataService,
private dialogService: DialogService,
private riskInsightsReportService: RiskInsightsReportService,
) {
this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps;
@@ -98,6 +105,8 @@ export class RiskInsightsComponent implements OnInit {
next: (applications: ApplicationHealthReportDetail[] | null) => {
if (applications) {
this.appsCount = applications.length;
this.atRiskMembers =
this.riskInsightsReportService.generateAtRiskMemberList(applications);
}
},
});
@@ -113,6 +122,10 @@ export class RiskInsightsComponent implements OnInit {
}
}
showAtRiskMembers = async () => {
this.dialogService.open(OrgAtRiskMembersDialogComponent, { data: this.atRiskMembers });
};
async onTabChange(newIndex: number): Promise<void> {
await this.router.navigate([], {
relativeTo: this.route,