Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export type OrganizationReportApplication = {
};

/**
* All applications report detail. Application is the cipher
* Report details for an application
* uri. Has the at risk, password, and member information
*/
export type ApplicationHealthReportDetail = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe("CriticalAppsService", () => {
const orgKey$ = new BehaviorSubject(OrgRecords);
keyService.orgKeys$.mockReturnValue(orgKey$);

service.setOrganizationId(SomeOrganization, SomeUser);
service.loadOrganizationContext(SomeOrganization, SomeUser);

// act
await service.setCriticalApps(SomeOrganization, criticalApps);
Expand Down Expand Up @@ -112,7 +112,7 @@ describe("CriticalAppsService", () => {
const orgKey$ = new BehaviorSubject(OrgRecords);
keyService.orgKeys$.mockReturnValue(orgKey$);

service.setOrganizationId(SomeOrganization, SomeUser);
service.loadOrganizationContext(SomeOrganization, SomeUser);

// act
await service.setCriticalApps(SomeOrganization, selectedUrls);
Expand All @@ -136,7 +136,7 @@ describe("CriticalAppsService", () => {
const orgKey$ = new BehaviorSubject(OrgRecords);
keyService.orgKeys$.mockReturnValue(orgKey$);

service.setOrganizationId(SomeOrganization, SomeUser);
service.loadOrganizationContext(SomeOrganization, SomeUser);

expect(keyService.orgKeys$).toHaveBeenCalledWith(SomeUser);
expect(encryptService.decryptString).toHaveBeenCalledTimes(2);
Expand All @@ -154,7 +154,7 @@ describe("CriticalAppsService", () => {

const orgKey$ = new BehaviorSubject(OrgRecords);
keyService.orgKeys$.mockReturnValue(orgKey$);
service.setOrganizationId(SomeOrganization, SomeUser);
service.loadOrganizationContext(SomeOrganization, SomeUser);
service.setAppsInListForOrg(response);
service.getAppsListForOrg(orgId as OrganizationId).subscribe((res) => {
expect(res).toHaveLength(2);
Expand All @@ -173,7 +173,7 @@ describe("CriticalAppsService", () => {
const orgKey$ = new BehaviorSubject(OrgRecords);
keyService.orgKeys$.mockReturnValue(orgKey$);

service.setOrganizationId(SomeOrganization, SomeUser);
service.loadOrganizationContext(SomeOrganization, SomeUser);

service.setAppsInListForOrg(initialList);

Expand Down Expand Up @@ -204,7 +204,7 @@ describe("CriticalAppsService", () => {
const orgKey$ = new BehaviorSubject(OrgRecords);
keyService.orgKeys$.mockReturnValue(orgKey$);

service.setOrganizationId(SomeOrganization, SomeUser);
service.loadOrganizationContext(SomeOrganization, SomeUser);

service.setAppsInListForOrg(initialList);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import {
map,
Observable,
of,
Subject,
switchMap,
takeUntil,
zip,
} from "rxjs";

Expand All @@ -30,43 +28,69 @@ import { CriticalAppsApiService } from "./critical-apps-api.service";
* Encrypts and saves data for a given organization
*/
export class CriticalAppsService {
private orgId = new BehaviorSubject<OrganizationId | null>(null);
// -------------------------- Context state --------------------------
// The organization ID of the organization the user is currently viewing
private organizationId = new BehaviorSubject<OrganizationId | null>(null);
private orgKey$ = new Observable<OrgKey>();
private criticalAppsList = new BehaviorSubject<PasswordHealthReportApplicationsResponse[]>([]);
private teardown = new Subject<void>();

private fetchOrg$ = this.orgId
.pipe(
switchMap((orgId) => this.retrieveCriticalApps(orgId)),
takeUntil(this.teardown),
)
.subscribe((apps) => this.criticalAppsList.next(apps));
// -------------------------- Data ------------------------------------
private criticalAppsListSubject$ = new BehaviorSubject<
PasswordHealthReportApplicationsResponse[]
>([]);
criticalAppsList$ = this.criticalAppsListSubject$.asObservable();

constructor(
private keyService: KeyService,
private encryptService: EncryptService,
private criticalAppsApiService: CriticalAppsApiService,
) {}

// Set context for the service for a specific organization
loadOrganizationContext(orgId: OrganizationId, userId: UserId) {
// Fetch the organization key for the user
this.orgKey$ = this.keyService.orgKeys$(userId).pipe(
filter((OrgKeys) => !!OrgKeys),
map((organizationKeysById) => organizationKeysById[orgId as OrganizationId]),
);

// Store organization id for service context
this.organizationId.next(orgId);

// Setup the critical apps fetching for the organization
if (orgId) {
this.retrieveCriticalApps(orgId).subscribe({
next: (result) => {
this.criticalAppsListSubject$.next(result);
},
error: (error: unknown) => {
throw error;
},
});
}
}

// Get a list of critical apps for a given organization
getAppsListForOrg(orgId: OrganizationId): Observable<PasswordHealthReportApplicationsResponse[]> {
if (orgId != this.orgId.value) {
throw new Error("Organization ID mismatch");
// [FIXME] Get organization id from context for all functions in this file
if (orgId != this.organizationId.value) {
throw new Error(
`Organization ID mismatch: expected ${this.organizationId.value}, got ${orgId}`,
);
}

return this.criticalAppsList
return this.criticalAppsListSubject$
.asObservable()
.pipe(map((apps) => apps.filter((app) => app.organizationId === orgId)));
}

// Reset the critical apps list
setAppsInListForOrg(apps: PasswordHealthReportApplicationsResponse[]) {
this.criticalAppsList.next(apps);
this.criticalAppsListSubject$.next(apps);
}

// Save the selected critical apps for a given organization
async setCriticalApps(orgId: OrganizationId, selectedUrls: string[]) {
if (orgId != this.orgId.value) {
if (orgId != this.organizationId.value) {
throw new Error("Organization ID mismatch");
}

Expand All @@ -79,7 +103,7 @@ export class CriticalAppsService {
// only save records that are not already in the database
const newEntries = await this.filterNewEntries(orgId as OrganizationId, selectedUrls);
const criticalAppsRequests = await this.encryptNewEntries(
this.orgId.value as OrganizationId,
this.organizationId.value as OrganizationId,
orgKey,
newEntries,
);
Expand All @@ -89,7 +113,7 @@ export class CriticalAppsService {
);

// add the new entries to the criticalAppsList
const updatedList = [...this.criticalAppsList.value];
const updatedList = [...this.criticalAppsListSubject$.value];
for (const responseItem of dbResponse) {
const decryptedUrl = await this.encryptService.decryptString(
new EncString(responseItem.uri),
Expand All @@ -103,26 +127,17 @@ export class CriticalAppsService {
} as PasswordHealthReportApplicationsResponse);
}
}
this.criticalAppsList.next(updatedList);
}

// Get the critical apps for a given organization
setOrganizationId(orgId: OrganizationId, userId: UserId) {
this.orgKey$ = this.keyService.orgKeys$(userId).pipe(
filter((OrgKeys) => !!OrgKeys),
map((organizationKeysById) => organizationKeysById[orgId as OrganizationId]),
);
this.orgId.next(orgId);
this.criticalAppsListSubject$.next(updatedList);
}

// Drop a critical app for a given organization
// Only one app may be dropped at a time
async dropCriticalApp(orgId: OrganizationId, selectedUrl: string) {
if (orgId != this.orgId.value) {
if (orgId != this.organizationId.value) {
throw new Error("Organization ID mismatch");
}

const app = this.criticalAppsList.value.find(
const app = this.criticalAppsListSubject$.value.find(
(f) => f.organizationId === orgId && f.uri === selectedUrl,
);

Expand All @@ -135,7 +150,9 @@ export class CriticalAppsService {
passwordHealthReportApplicationIds: [app.id],
});

this.criticalAppsList.next(this.criticalAppsList.value.filter((f) => f.uri !== selectedUrl));
this.criticalAppsListSubject$.next(
this.criticalAppsListSubject$.value.filter((f) => f.uri !== selectedUrl),
);
}

private retrieveCriticalApps(
Expand Down Expand Up @@ -170,7 +187,7 @@ export class CriticalAppsService {
}

private async filterNewEntries(orgId: OrganizationId, selectedUrls: string[]): Promise<string[]> {
return await firstValueFrom(this.criticalAppsList).then((criticalApps) => {
return await firstValueFrom(this.criticalAppsListSubject$).then((criticalApps) => {
const criticalAppsUri = criticalApps
.filter((f) => f.organizationId === orgId)
.map((f) => f.uri);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,49 @@
import { BehaviorSubject } from "rxjs";
import { finalize } from "rxjs/operators";
import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs";
import { finalize, switchMap, withLatestFrom } from "rxjs/operators";

import { OrganizationId } from "@bitwarden/common/types/guid";
import {
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";

import {
AppAtRiskMembersDialogParams,
AtRiskApplicationDetail,
AtRiskMemberDetail,
DrawerType,
ApplicationHealthReportDetail,
ApplicationHealthReportDetailEnriched,
} from "../models/report-models";

import { CriticalAppsService } from "./critical-apps.service";
import { RiskInsightsReportService } from "./risk-insights-report.service";
export class RiskInsightsDataService {
// -------------------------- Context state --------------------------
// Current user viewing risk insights
private userIdSubject = new BehaviorSubject<UserId | null>(null);
userId$ = this.userIdSubject.asObservable();

// Organization the user is currently viewing
private organizationDetailsSubject = new BehaviorSubject<{
organizationId: OrganizationId;
organizationName: string;
} | null>(null);
organizationDetails$ = this.organizationDetailsSubject.asObservable();

// -------------------------- Data ------------------------------------
private applicationsSubject = new BehaviorSubject<ApplicationHealthReportDetail[] | null>(null);

applications$ = this.applicationsSubject.asObservable();

private dataLastUpdatedSubject = new BehaviorSubject<Date | null>(null);
dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable();

criticalApps$ = this.criticalAppsService.criticalAppsList$;

// --------------------------- UI State ------------------------------------

private isLoadingSubject = new BehaviorSubject<boolean>(false);
isLoading$ = this.isLoadingSubject.asObservable();

Expand All @@ -26,17 +53,58 @@ export class RiskInsightsDataService {
private errorSubject = new BehaviorSubject<string | null>(null);
error$ = this.errorSubject.asObservable();

private dataLastUpdatedSubject = new BehaviorSubject<Date | null>(null);
dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable();

openDrawer = false;
drawerInvokerId: string = "";
activeDrawerType: DrawerType = DrawerType.None;
atRiskMemberDetails: AtRiskMemberDetail[] = [];
appAtRiskMembers: AppAtRiskMembersDialogParams | null = null;
atRiskAppDetails: AtRiskApplicationDetail[] | null = null;

constructor(private reportService: RiskInsightsReportService) {}
constructor(
private accountService: AccountService,
private criticalAppsService: CriticalAppsService,
private organizationService: OrganizationService,
private reportService: RiskInsightsReportService,
) {}

// [FIXME] PM-25612 - Call Initialization in RiskInsightsComponent instead of child components
async initializeForOrganization(organizationId: OrganizationId) {
// Fetch current user
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (userId) {
this.userIdSubject.next(userId);
}

// [FIXME] getOrganizationById is now deprecated - update when we can
// Fetch organization details
const org = await firstValueFrom(
this.organizationService.organizations$(userId).pipe(getOrganizationById(organizationId)),
);
if (org) {
this.organizationDetailsSubject.next({
organizationId: organizationId,
organizationName: org.name,
});
}

// Load critical applications for organization
await this.criticalAppsService.loadOrganizationContext(organizationId, userId);

// TODO: PM-25613
// // Load existing report

// this.fetchLastReport(organizationId, userId);

// // Setup new report generation
// this._runApplicationsReport().subscribe({
// next: (result) => {
// this.isRunningReportSubject.next(false);
// },
// error: () => {
// this.errorSubject.next("Failed to save report");
// },
// });
}

/**
* Fetches the applications report and updates the applicationsSubject.
Expand Down Expand Up @@ -72,6 +140,44 @@ export class RiskInsightsDataService {
this.fetchApplicationsReport(organizationId, true);
}

// ------------------------------- Enrichment methods -------------------------------
/**
* Takes the basic application health report details and enriches them to include
* critical app status and associated ciphers.
*
* @param applications The list of application health report details to enrich
* @returns The enriched application health report details with critical app status and ciphers
*/
enrichReportData$(
applications: ApplicationHealthReportDetail[],
): Observable<ApplicationHealthReportDetailEnriched[]> {
return of(applications).pipe(
withLatestFrom(this.organizationDetails$, this.criticalApps$),
switchMap(async ([apps, orgDetails, criticalApps]) => {
if (!orgDetails) {
return [];
}

// Get ciphers for application
const cipherMap = await this.reportService.getApplicationCipherMap(
apps,
orgDetails.organizationId,
);

// Find critical apps
const criticalApplicationNames = new Set(criticalApps.map((ca) => ca.uri));

// Return enriched application data
return apps.map((app) => ({
...app,
ciphers: cipherMap.get(app.applicationName) || [],
isMarkedAsCritical: criticalApplicationNames.has(app.applicationName),
})) as ApplicationHealthReportDetailEnriched[];
}),
);
}

// ------------------------------- Drawer management methods -------------------------------
isActiveDrawerType = (drawerType: DrawerType): boolean => {
return this.activeDrawerType === drawerType;
};
Expand Down
Loading
Loading