1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 06:23:38 +00:00

PM-20132 do not filter out members

This commit is contained in:
voommen-livefront
2025-07-14 15:00:57 -05:00
parent e382bd6156
commit 16506525e7
6 changed files with 100 additions and 32 deletions

View File

@@ -167,4 +167,19 @@ export enum DrawerType {
OrgAtRiskApps = 3,
}
export interface CipherHealthWithMemberDetails {
ciphers: CipherHealthReportDetail[];
members: MemberDetailsFlat[];
}
export interface CipherHealthReportUriDetailWithMemberDetails {
ciphers: CipherHealthReportUriDetail[];
members: MemberDetailsFlat[];
}
export interface HealthReportUriDetailWithMemberDetails {
healthReport: ApplicationHealthReportDetail[];
members: MemberDetailsFlat[];
}
export type PasswordHealthReportApplicationId = Opaque<string, "PasswordHealthReportApplicationId">;

View File

@@ -7,6 +7,8 @@ import {
AtRiskApplicationDetail,
AtRiskMemberDetail,
DrawerType,
HealthReportUriDetailWithMemberDetails,
MemberDetailsFlat,
} from "../models/password-health";
import { RiskInsightsReportService } from "./risk-insights-report.service";
@@ -27,6 +29,9 @@ export class RiskInsightsDataService {
private dataLastUpdatedSubject = new BehaviorSubject<Date | null>(null);
dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable();
private memberDetailsSubject = new BehaviorSubject<MemberDetailsFlat[]>([]);
memberDetails$ = this.memberDetailsSubject.asObservable();
openDrawer = false;
drawerInvokerId: string = "";
activeDrawerType: DrawerType = DrawerType.None;
@@ -56,8 +61,9 @@ export class RiskInsightsDataService {
}),
)
.subscribe({
next: (reports: ApplicationHealthReportDetail[]) => {
this.applicationsSubject.next(reports);
next: (reports: HealthReportUriDetailWithMemberDetails) => {
this.applicationsSubject.next(reports.healthReport);
this.memberDetailsSubject.next(reports.members);
this.errorSubject.next(null);
},
error: () => {

View File

@@ -43,9 +43,11 @@ describe("RiskInsightsReportService", () => {
it("should generate the raw data report correctly", async () => {
const result = await firstValueFrom(service.generateRawDataReport$("orgId"));
expect(result).toHaveLength(6);
expect(result.ciphers).toHaveLength(6);
let testCaseResults = result.filter((x) => x.id === "cbea34a8-bde4-46ad-9d19-b05001228ab1");
let testCaseResults = result.ciphers.filter(
(x) => x.id === "cbea34a8-bde4-46ad-9d19-b05001228ab1",
);
expect(testCaseResults).toHaveLength(1);
let testCase = testCaseResults[0];
expect(testCase).toBeTruthy();
@@ -55,7 +57,7 @@ describe("RiskInsightsReportService", () => {
expect(testCase.exposedPasswordDetail).toBeTruthy();
expect(testCase.reusedPasswordCount).toEqual(2);
testCaseResults = result.filter((x) => x.id === "cbea34a8-bde4-46ad-9d19-b05001227tt1");
testCaseResults = result.ciphers.filter((x) => x.id === "cbea34a8-bde4-46ad-9d19-b05001227tt1");
expect(testCaseResults).toHaveLength(1);
testCase = testCaseResults[0];
expect(testCase).toBeTruthy();
@@ -69,14 +71,16 @@ describe("RiskInsightsReportService", () => {
it("should generate the raw data + uri report correctly", async () => {
const result = await firstValueFrom(service.generateRawDataUriReport$("orgId"));
expect(result).toHaveLength(11);
expect(result.ciphers).toHaveLength(11);
// Two ciphers that have google.com as their uri. There should be 2 results
const googleResults = result.filter((x) => x.trimmedUri === "google.com");
const googleResults = result.ciphers.filter((x) => x.trimmedUri === "google.com");
expect(googleResults).toHaveLength(2);
// There is an invalid uri and it should not be trimmed
const invalidUriResults = result.filter((x) => x.trimmedUri === "this_is-not|a-valid-uri123@+");
const invalidUriResults = result.ciphers.filter(
(x) => x.trimmedUri === "this_is-not|a-valid-uri123@+",
);
expect(invalidUriResults).toHaveLength(1);
// Verify the details for one of the googles matches the password health info
@@ -92,13 +96,13 @@ describe("RiskInsightsReportService", () => {
it("should generate applications health report data correctly", async () => {
const result = await firstValueFrom(service.generateApplicationsReport$("orgId"));
expect(result).toHaveLength(8);
expect(result.healthReport).toHaveLength(8);
// Two ciphers have google.com associated with them. The first cipher
// has 2 members and the second has 4. However, the 2 members in the first
// cipher are also associated with the second. The total amount of members
// should be 4 not 6
const googleTestResults = result.filter((x) => x.applicationName === "google.com");
const googleTestResults = result.healthReport.filter((x) => x.applicationName === "google.com");
expect(googleTestResults).toHaveLength(1);
const googleTest = googleTestResults[0];
expect(googleTest.memberCount).toEqual(4);
@@ -111,7 +115,9 @@ describe("RiskInsightsReportService", () => {
expect(googleTest.atRiskPasswordCount).toEqual(2);
// There are 2 ciphers associated with 101domain.com
const domain101TestResults = result.filter((x) => x.applicationName === "101domain.com");
const domain101TestResults = result.healthReport.filter(
(x) => x.applicationName === "101domain.com",
);
expect(domain101TestResults).toHaveLength(1);
const domain101Test = domain101TestResults[0];
expect(domain101Test.passwordCount).toEqual(2);
@@ -132,7 +138,10 @@ describe("RiskInsightsReportService", () => {
it("should generate applications summary data correctly", async () => {
const reportResult = await firstValueFrom(service.generateApplicationsReport$("orgId"));
const reportSummary = service.generateApplicationsSummary(reportResult);
const reportSummary = service.generateApplicationsSummary(
reportResult.healthReport,
reportResult.members,
);
expect(reportSummary.totalMemberCount).toEqual(7);
expect(reportSummary.totalAtRiskMemberCount).toEqual(6);

View File

@@ -21,6 +21,9 @@ import {
WeakPasswordDetail,
WeakPasswordScore,
ApplicationHealthReportDetailWithCriticalFlagAndCipher,
CipherHealthWithMemberDetails,
CipherHealthReportUriDetailWithMemberDetails,
HealthReportUriDetailWithMemberDetails,
} from "../models/password-health";
import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service";
@@ -40,7 +43,7 @@ export class RiskInsightsReportService {
* @param organizationId
* @returns Cipher health report data with members and trimmed uris
*/
generateRawDataReport$(organizationId: string): Observable<CipherHealthReportDetail[]> {
generateRawDataReport$(organizationId: string): Observable<CipherHealthWithMemberDetails> {
const allCiphers$ = from(this.cipherService.getAllFromApiForOrganization(organizationId));
const memberCiphers$ = from(
this.memberCipherDetailsApiService.getMemberCipherDetails(organizationId),
@@ -55,7 +58,14 @@ export class RiskInsightsReportService {
);
return [allCiphers, details] as const;
}),
concatMap(([ciphers, flattenedDetails]) => this.getCipherDetails(ciphers, flattenedDetails)),
concatMap(async ([ciphers, flattenedDetails]) => {
const cipherDetails = await this.getCipherDetails(ciphers, flattenedDetails);
const memberDetails = this.getUniqueMembers(flattenedDetails);
return {
ciphers: cipherDetails,
members: memberDetails,
};
}),
first(),
);
@@ -68,10 +78,18 @@ export class RiskInsightsReportService {
* @param organizationId Id of the organization
* @returns Cipher health report data flattened to the uris
*/
generateRawDataUriReport$(organizationId: string): Observable<CipherHealthReportUriDetail[]> {
generateRawDataUriReport$(
organizationId: string,
): Observable<CipherHealthReportUriDetailWithMemberDetails> {
const cipherHealthDetails$ = this.generateRawDataReport$(organizationId);
const results$ = cipherHealthDetails$.pipe(
map((healthDetails) => this.getCipherUriDetails(healthDetails)),
map((healthDetails) => {
const cipherHealthUriDetail = this.getCipherUriDetails(healthDetails.ciphers);
return {
ciphers: cipherHealthUriDetail,
members: healthDetails.members,
};
}),
first(),
);
@@ -84,10 +102,18 @@ export class RiskInsightsReportService {
* @param organizationId Id of the organization
* @returns The all applications health report data
*/
generateApplicationsReport$(organizationId: string): Observable<ApplicationHealthReportDetail[]> {
generateApplicationsReport$(
organizationId: string,
): Observable<HealthReportUriDetailWithMemberDetails> {
const cipherHealthUriReport$ = this.generateRawDataUriReport$(organizationId);
const results$ = cipherHealthUriReport$.pipe(
map((uriDetails) => this.getApplicationHealthReport(uriDetails)),
map((uriDetails) => {
const appHealthReportDetail = this.getApplicationHealthReport(uriDetails.ciphers);
return {
healthReport: appHealthReportDetail,
members: uriDetails.members,
};
}),
first(),
);
@@ -150,9 +176,11 @@ export class RiskInsightsReportService {
*/
generateApplicationsSummary(
reports: ApplicationHealthReportDetail[],
memberDetails: MemberDetailsFlat[],
): ApplicationHealthReportSummary {
const totalMembers = reports.flatMap((x) => x.memberDetails);
const uniqueMembers = this.getUniqueMembers(totalMembers);
// const totalMembers = reports.flatMap((x) => x.memberDetails);
// const uniqueMembers = this.getUniqueMembers(totalMembers);
const uniqueMembers = this.getUniqueMembers(memberDetails);
const atRiskMembers = reports.flatMap((x) => x.atRiskMemberDetails);
const uniqueAtRiskMembers = this.getUniqueMembers(atRiskMembers);

View File

@@ -86,12 +86,13 @@ export class AllApplicationsComponent implements OnInit {
combineLatest([
this.dataService.applications$,
this.dataService.memberDetails$,
this.criticalAppsService.getAppsListForOrg(organizationId),
organization$,
])
.pipe(
takeUntilDestroyed(this.destroyRef),
map(([applications, criticalApps, organization]) => {
map(([applications, memberDetails, criticalApps, organization]) => {
if (applications && applications.length === 0 && criticalApps && criticalApps) {
const criticalUrls = criticalApps.map((ca) => ca.uri);
const data = applications?.map((app) => ({
@@ -101,9 +102,9 @@ export class AllApplicationsComponent implements OnInit {
return { data, organization };
}
return { data: applications, organization };
return { data: applications, organization, memberDetails };
}),
switchMap(async ({ data, organization }) => {
switchMap(async ({ data, organization, memberDetails }) => {
if (data && organization) {
const dataWithCiphers = await this.reportService.identifyCiphers(
data,
@@ -113,16 +114,20 @@ export class AllApplicationsComponent implements OnInit {
return {
data: dataWithCiphers,
organization,
memberDetails,
};
}
return { data: [], organization };
return { data: [], organization, memberDetails: [] };
}),
)
.subscribe(({ data, organization }) => {
.subscribe(({ data, organization, memberDetails }) => {
if (data) {
this.dataSource.data = data;
this.applicationSummary = this.reportService.generateApplicationsSummary(data);
this.applicationSummary = this.reportService.generateApplicationsSummary(
data,
memberDetails,
);
}
if (organization) {
this.organization = organization;

View File

@@ -75,33 +75,38 @@ export class CriticalApplicationsComponent implements OnInit {
combineLatest([
this.dataService.applications$,
this.dataService.memberDetails$,
this.criticalAppsService.getAppsListForOrg(this.organizationId),
])
.pipe(
takeUntilDestroyed(this.destroyRef),
map(([applications, criticalApps]) => {
map(([applications, memberDetails, criticalApps]) => {
const criticalUrls = criticalApps.map((ca) => ca.uri);
const data = applications?.map((app) => ({
...app,
isMarkedAsCritical: criticalUrls.includes(app.applicationName),
})) as ApplicationHealthReportDetailWithCriticalFlag[];
return data?.filter((app) => app.isMarkedAsCritical);
const appsMarkedAsCritical = data?.filter((app) => app.isMarkedAsCritical);
return { data: appsMarkedAsCritical, memberDetails };
}),
switchMap(async (data) => {
switchMap(async ({ data, memberDetails }) => {
if (data) {
const dataWithCiphers = await this.reportService.identifyCiphers(
data,
this.organizationId,
);
return dataWithCiphers;
return { applications: dataWithCiphers, memberDetails };
}
return null;
}),
)
.subscribe((applications) => {
.subscribe(({ applications, memberDetails }) => {
if (applications) {
this.dataSource.data = applications;
this.applicationSummary = this.reportService.generateApplicationsSummary(applications);
this.applicationSummary = this.reportService.generateApplicationsSummary(
applications,
memberDetails,
);
this.enableRequestPasswordChange = this.applicationSummary.totalAtRiskMemberCount > 0;
}
});