diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts index 2527b875b6f..dbafc851776 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts @@ -194,7 +194,7 @@ export interface SaveRiskInsightsReportResponse { export interface GetRiskInsightsReportResponse { id: string; organizationId: OrganizationId; - reportDate: string; + date: string; reportData: string; totalMembers: number; totalAtRiskMembers: number; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts index 2e5b6ca5cda..8961392f7a3 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts @@ -39,11 +39,11 @@ describe("RiskInsightsApiService", () => { apiService.send.mockReturnValue(Promise.resolve(response)); - service.saveRiskInsightsReport(orgId, request).subscribe((result) => { + service.saveRiskInsightsReport(request).subscribe((result) => { expect(result).toEqual(response); expect(apiService.send).toHaveBeenCalledWith( - "PUT", - `/reports/risk-insights-report/${orgId.toString()}`, + "POST", + `/reports/organization-reports`, request.data, true, true, diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts index 85f33f43eb0..f9dd503df76 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts @@ -13,12 +13,11 @@ export class RiskInsightsApiService { constructor(private apiService: ApiService) {} saveRiskInsightsReport( - orgId: OrganizationId, request: SaveRiskInsightsReportRequest, ): Observable { const dbResponse = this.apiService.send( - "PUT", - `/reports/risk-insights-report/${orgId.toString()}`, + "POST", + `/reports/organization-reports`, request.data, true, true, @@ -29,7 +28,7 @@ export class RiskInsightsApiService { getRiskInsightsReport(orgId: OrganizationId): Observable { const dbResponse = this.apiService - .send("GET", `/reports/risk-insights-report/${orgId.toString()}`, null, true, true) + .send("GET", `/reports/organization-reports/latest/${orgId.toString()}`, null, true, true) .catch((error: any): any => { if (error.statusCode === 404) { return null; // Handle 404 by returning null or an appropriate default value diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts index 1e5e28f27c1..1448c54ed8f 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-data.service.ts @@ -1,7 +1,10 @@ +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { BehaviorSubject } from "rxjs"; -import { finalize, switchMap } from "rxjs/operators"; +import { switchMap } from "rxjs/operators"; import { OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { AppAtRiskMembersDialogParams, @@ -35,6 +38,9 @@ export class RiskInsightsDataService { private dataLastUpdatedSubject = new BehaviorSubject(null); dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable(); + private cipherViewsForOrganizationSubject = new BehaviorSubject([]); + cipherViewsForOrganization$ = this.cipherViewsForOrganizationSubject.asObservable(); + openDrawer = false; drawerInvokerId: string = ""; activeDrawerType: DrawerType = DrawerType.None; @@ -45,6 +51,7 @@ export class RiskInsightsDataService { constructor( private reportService: RiskInsightsReportService, private riskInsightsApiService: RiskInsightsApiService, + private cipherService: CipherService, ) {} /** @@ -52,19 +59,9 @@ export class RiskInsightsDataService { * @param organizationId The ID of the organization. */ fetchApplicationsReport(organizationId: string, isRefresh?: boolean): void { - if (isRefresh) { - this.isRefreshingSubject.next(true); - } else { - this.isLoadingSubject.next(true); - } this.reportService .generateApplicationsReport$(organizationId) - .pipe( - finalize(() => { - this.isLoadingSubject.next(false); - this.isRefreshingSubject.next(false); - }), - ) + .pipe(takeUntilDestroyed()) .subscribe({ next: (reports: ApplicationHealthReportDetail[]) => { this.applicationsSubject.next(reports); @@ -83,13 +80,13 @@ export class RiskInsightsDataService { .getRiskInsightsReport(organizationId as OrganizationId) .pipe( switchMap(async (reportFromArchive) => { - if (!reportFromArchive || !reportFromArchive?.reportDate) { + if (!reportFromArchive || !reportFromArchive?.date) { this.fetchApplicationsReport(organizationId); return { report: [], summary: null, - fromDb: false, + fromArchive: false, lastUpdated: new Date(), }; } else { @@ -101,20 +98,23 @@ export class RiskInsightsDataService { return { report, summary, - fromDb: true, - lastUpdated: new Date(reportFromArchive.reportDate), + fromArchive: true, + lastUpdated: new Date(reportFromArchive.date), }; } }), ) .subscribe({ - next: ({ report, summary, fromDb, lastUpdated }) => { - if (fromDb) { + next: ({ report, summary, fromArchive, lastUpdated }) => { + // in this block, only set the applicationsSubject and appsSummarySubject if the report is from archive + // the fetchApplicationsReport will set them if the report is not from archive + if (fromArchive) { this.applicationsSubject.next(report); this.errorSubject.next(null); this.appsSummarySubject.next(summary); } - this.isReportFromArchiveSubject.next(fromDb); + + this.isReportFromArchiveSubject.next(fromArchive); this.dataLastUpdatedSubject.next(lastUpdated); }, error: (error: unknown) => { @@ -124,11 +124,31 @@ export class RiskInsightsDataService { }); } + async fetchCipherViewsForOrganization( + organizationId: OrganizationId, + isRefresh: boolean = false, + ): Promise { + if (isRefresh) { + this.cipherViewsForOrganizationSubject.next([]); + } + + if (this.cipherViewsForOrganizationSubject.value) { + return; + } + + const cipherViews = await this.cipherService.getAllFromApiForOrganization(organizationId); + this.cipherViewsForOrganizationSubject.next(cipherViews); + } + isLoadingData(started: boolean): void { this.isLoadingSubject.next(started); this.isRefreshingSubject.next(started); } + setReportFromArchiveStatus(isFromArchive: boolean): void { + this.isReportFromArchiveSubject.next(isFromArchive); + } + refreshApplicationsReport(organizationId: string): void { this.fetchApplicationsReport(organizationId, true); } diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts index 38711f48fb0..abe3e733be2 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts @@ -256,10 +256,8 @@ export class RiskInsightsReportService { async identifyCiphers( data: ApplicationHealthReportDetail[], - organizationId: string, + cipherViews: CipherView[], ): Promise { - const cipherViews = await this.cipherService.getAllFromApiForOrganization(organizationId); - const dataWithCiphers = data.map( (app, index) => ({ diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts index 4333178a0df..80cd4e7ca7f 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts @@ -42,7 +42,7 @@ import { RiskInsightsComponent } from "./risk-insights.component"; }, { provide: RiskInsightsDataService, - deps: [RiskInsightsReportService, RiskInsightsApiService], + deps: [RiskInsightsReportService, RiskInsightsApiService, CipherService], }, safeProvider({ provide: CriticalAppsService, diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts index a7663b35580..c01893e1dd9 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts @@ -4,13 +4,13 @@ import { FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { BehaviorSubject, + combineLatest, debounceTime, firstValueFrom, map, Observable, of, switchMap, - zip, } from "rxjs"; import { @@ -85,7 +85,6 @@ export class AllApplicationsComponent implements OnInit { destroyRef = inject(DestroyRef); isLoading$: Observable = of(false); - isCriticalAppsFeatureEnabled = false; private atRiskInsightsReport = new BehaviorSubject<{ data: ApplicationHealthReportDetailWithCriticalFlag[]; @@ -107,10 +106,6 @@ export class AllApplicationsComponent implements OnInit { this.dataService.isLoadingData(true); - this.isCriticalAppsFeatureEnabled = await this.configService.getFeatureFlag( - FeatureFlag.CriticalApps, - ); - const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? ""; const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); @@ -119,25 +114,70 @@ export class AllApplicationsComponent implements OnInit { .organizations$(userId) .pipe(getOrganizationById(organizationId)); + await this.dataService.fetchCipherViewsForOrganization(organizationId as OrganizationId); this.dataService.fetchApplicationsReportFromCache(organizationId as OrganizationId); - zip([ + combineLatest([ this.dataService.applications$, this.dataService.appsSummary$, this.dataService.isReportFromArchive$, organization$, this.criticalAppsService.getAppsListForOrg(organizationId as OrganizationId), + this.dataService.cipherViewsForOrganization$, ]) .pipe( - map(([report, summary, isReportFromArchive, organization, criticalApps]) => { - const criticalUrls = criticalApps?.map((ca) => ca.uri); - const data = report?.map((app) => ({ - ...app, - isMarkedAsCritical: criticalUrls.includes(app.applicationName), - })) as ApplicationHealthReportDetailWithCriticalFlag[]; + map( + ([ + report, + summary, + isReportFromArchive, + organization, + criticalApps, + cipherViewsForOrg, + ]) => { + const criticalUrls = criticalApps?.map((ca) => ca.uri); + const data = report?.map((app) => ({ + ...app, + isMarkedAsCritical: criticalUrls.includes(app.applicationName), + })) as ApplicationHealthReportDetailWithCriticalFlag[]; - return { report: data, summary, criticalApps, isReportFromArchive, organization }; - }), + return { + report: data, + summary, + criticalApps, + isReportFromArchive, + organization, + cipherViewsForOrg, + }; + }, + ), + switchMap( + async ({ + report, + summary, + isReportFromArchive, + organization, + criticalApps, + cipherViewsForOrg, + }) => { + if (report && organization) { + const dataWithCiphers = await this.reportService.identifyCiphers( + report, + cipherViewsForOrg, + ); + + return { + report: dataWithCiphers, + summary, + isReportFromArchive, + organization, + criticalApps, + }; + } + + return { report: [], summary, isReportFromArchive, organization, criticalApps: [] }; + }, + ), takeUntilDestroyed(this.destroyRef), ) .subscribe(({ report, summary, criticalApps, isReportFromArchive, organization }) => { @@ -150,7 +190,7 @@ export class AllApplicationsComponent implements OnInit { this.organization = organization; } - if (!isReportFromArchive && report && organization && summary && criticalApps) { + if (!isReportFromArchive && report && organization && summary) { this.atRiskInsightsReport.next({ data: report, organization: organization, @@ -200,14 +240,14 @@ export class AllApplicationsComponent implements OnInit { const request = { data: reportData }; try { const response = await firstValueFrom( - this.riskInsightsApiService.saveRiskInsightsReport( - this.organization.id as OrganizationId, - request, - ), + this.riskInsightsApiService.saveRiskInsightsReport(request), ); return response; } catch { /* continue as usual */ + } finally { + // now that we have saved the data, we are in sync with the archive + this.dataService.setReportFromArchiveStatus(true); } return null; diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts index 765d979bbe6..cff8b577570 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts @@ -76,23 +76,22 @@ export class CriticalApplicationsComponent implements OnInit { combineLatest([ this.dataService.applications$, this.criticalAppsService.getAppsListForOrg(this.organizationId), + this.dataService.cipherViewsForOrganization$, ]) .pipe( takeUntilDestroyed(this.destroyRef), - map(([applications, criticalApps]) => { + map(([applications, criticalApps, cipherViewsForOrg]) => { 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 dataWithCriticalAppsFlag = data?.filter((app) => app.isMarkedAsCritical); + return { data: dataWithCriticalAppsFlag, cipherViews: cipherViewsForOrg }; }), - switchMap(async (data) => { + switchMap(async ({ data, cipherViews }) => { if (data) { - const dataWithCiphers = await this.reportService.identifyCiphers( - data, - this.organizationId, - ); + const dataWithCiphers = await this.reportService.identifyCiphers(data, cipherViews); return dataWithCiphers; } return null; diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index 375a82ef593..ca7a88098cb 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -14,7 +14,6 @@ import { DrawerType, PasswordHealthReportApplicationsResponse, } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { @@ -84,7 +83,6 @@ export class RiskInsightsComponent implements OnInit { constructor( private route: ActivatedRoute, private router: Router, - private configService: ConfigService, protected dataService: RiskInsightsDataService, private criticalAppsService: CriticalAppsService, ) {