From c0b8f78db66579ce3d316cd608161d0189b601be Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Thu, 14 Nov 2024 14:04:06 -0600 Subject: [PATCH] PM-14470 save/get encrypt/decrypt urls --- .../all-applications.component.html | 6 +- .../all-applications.component.ts | 81 ++++++++++++++++--- .../risk-insights/application-table.mock.ts | 6 ++ .../services/critical-apps.service.spec.ts | 68 ++++++++++++++++ .../services/critical-apps.service.ts | 60 ++++++++++++++ .../reports/risk-insights/services/index.ts | 1 + 6 files changed, 207 insertions(+), 15 deletions(-) create mode 100644 bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts create mode 100644 bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts diff --git a/apps/web/src/app/tools/risk-insights/all-applications.component.html b/apps/web/src/app/tools/risk-insights/all-applications.component.html index 4ed31adea78..48eb7e2bac6 100644 --- a/apps/web/src/app/tools/risk-insights/all-applications.component.html +++ b/apps/web/src/app/tools/risk-insights/all-applications.component.html @@ -58,7 +58,7 @@ buttonType="secondary" bitButton *ngIf="isCritialAppsFeatureEnabled" - [disabled]="!selectedIds.size" + [disabled]="!selectedUrls.size" [loading]="markingAsCritical" (click)="markAppsAsCritical()" > @@ -83,8 +83,8 @@ diff --git a/apps/web/src/app/tools/risk-insights/all-applications.component.ts b/apps/web/src/app/tools/risk-insights/all-applications.component.ts index 5d76403f46b..cebf479e834 100644 --- a/apps/web/src/app/tools/risk-insights/all-applications.component.ts +++ b/apps/web/src/app/tools/risk-insights/all-applications.component.ts @@ -2,14 +2,22 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { debounceTime, firstValueFrom, map } from "rxjs"; +import { debounceTime, firstValueFrom, map, switchMap } from "rxjs"; +// eslint-disable-next-line no-restricted-imports +import { + CriticalAppsService, + PasswordHealthReportApplicationsRequest, + PasswordHealthReportApplicationsResponse, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -20,6 +28,7 @@ import { TableDataSource, ToastService, } from "@bitwarden/components"; +import { KeyService } from "@bitwarden/key-management"; import { CardComponent } from "@bitwarden/tools-card"; import { HeaderModule } from "../../layouts/header/header.module"; @@ -36,7 +45,7 @@ import { applicationTableMockData } from "./application-table.mock"; }) export class AllApplicationsComponent implements OnInit { protected dataSource = new TableDataSource(); - protected selectedIds: Set = new Set(); + protected selectedUrls: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); private destroyRef = inject(DestroyRef); protected loading = false; @@ -44,6 +53,7 @@ export class AllApplicationsComponent implements OnInit { noItemsIcon = Icons.Security; protected markingAsCritical = false; isCritialAppsFeatureEnabled = false; + private flaggedCriticalApps: PasswordHealthReportApplicationsResponse[] = []; // MOCK DATA protected mockData = applicationTableMockData; @@ -59,8 +69,23 @@ export class AllApplicationsComponent implements OnInit { map(async (params) => { const organizationId = params.get("organizationId"); this.organization = await firstValueFrom(this.organizationService.get$(organizationId)); + return params; // TODO: use organizationId to fetch data }), + switchMap(async (params) => { + const organizationId = (await params).get("organizationId"); + const result = await this.criticalAppsService.getCriticalApps(organizationId); + const key = await this.keyService.getOrgKey(this.organization.id); + const flaggedCriticalAppsPromise = result.map(async (r) => { + const decryptedUrl = await this.encryptService.decryptToUtf8(new EncString(r.uri), key); + return { + id: r.id, + organizationId: r.organizationId, + uri: decryptedUrl, + } as PasswordHealthReportApplicationsResponse; + }); + this.flaggedCriticalApps = await Promise.all(flaggedCriticalAppsPromise); + }), ) .subscribe(); @@ -78,6 +103,9 @@ export class AllApplicationsComponent implements OnInit { protected toastService: ToastService, protected organizationService: OrganizationService, protected configService: ConfigService, + protected criticalAppsService: CriticalAppsService, + private keyService: KeyService, + private encryptService: EncryptService, ) { this.dataSource.data = applicationTableMockData; this.searchControl.valueChanges @@ -95,32 +123,61 @@ export class AllApplicationsComponent implements OnInit { }; markAppsAsCritical = async () => { - // TODO: Send to API once implemented this.markingAsCritical = true; - return new Promise((resolve) => { - setTimeout(() => { - this.selectedIds.clear(); + const key = await this.keyService.getOrgKey(this.organization.id); + + // only save records that are not already in the database + const newEntries = Array.from(this.selectedUrls).filter((url) => { + return !this.flaggedCriticalApps.some((r) => r.uri === url); + }); + + const criticalAppsPromises = newEntries.map(async (url) => { + const encryptedUrlName = await this.encryptService.encrypt(url, key); + return { + organizationId: this.organization.id, + url: encryptedUrlName.encryptedString.toString(), + } as PasswordHealthReportApplicationsRequest; + }); + + const criticalApps = await Promise.all(criticalAppsPromises); + + await this.criticalAppsService + .setCriticalApps(criticalApps) + .then((result) => { + // append to flaggedCriticalApps + result + .filter((r) => !this.flaggedCriticalApps.some((f) => f.uri === r.uri)) + .forEach(async (r) => { + const decryptedUrl = await this.encryptService.decryptToUtf8(new EncString(r.uri), key); + this.flaggedCriticalApps.push({ + id: r.id, + organizationId: r.organizationId, + uri: decryptedUrl, + } as PasswordHealthReportApplicationsResponse); + }); + this.toastService.showToast({ variant: "success", title: null, message: this.i18nService.t("appsMarkedAsCritical"), }); - resolve(true); + }) + .finally(() => { + this.selectedUrls.clear(); this.markingAsCritical = false; - }, 1000); - }); + }); }; trackByFunction(_: number, item: CipherView) { return item.id; } - onCheckboxChange(id: number, event: Event) { + onCheckboxChange(urlName: string, event: Event) { const isChecked = (event.target as HTMLInputElement).checked; if (isChecked) { - this.selectedIds.add(id); + this.selectedUrls.add(urlName); } else { - this.selectedIds.delete(id); + this.selectedUrls.delete(urlName); } } } diff --git a/apps/web/src/app/tools/risk-insights/application-table.mock.ts b/apps/web/src/app/tools/risk-insights/application-table.mock.ts index 4df363ab2c7..4dffa60b562 100644 --- a/apps/web/src/app/tools/risk-insights/application-table.mock.ts +++ b/apps/web/src/app/tools/risk-insights/application-table.mock.ts @@ -6,6 +6,7 @@ export const applicationTableMockData = [ totalPasswords: 10, atRiskMembers: 2, totalMembers: 5, + isMarkedAsCritical: false, }, { id: 2, @@ -14,6 +15,7 @@ export const applicationTableMockData = [ totalPasswords: 8, atRiskMembers: 1, totalMembers: 3, + isMarkedAsCritical: false, }, { id: 3, @@ -22,6 +24,7 @@ export const applicationTableMockData = [ totalPasswords: 6, atRiskMembers: 0, totalMembers: 2, + isMarkedAsCritical: false, }, { id: 4, @@ -30,6 +33,7 @@ export const applicationTableMockData = [ totalPasswords: 4, atRiskMembers: 0, totalMembers: 1, + isMarkedAsCritical: false, }, { id: 5, @@ -38,6 +42,7 @@ export const applicationTableMockData = [ totalPasswords: 2, atRiskMembers: 0, totalMembers: 0, + isMarkedAsCritical: false, }, { id: 6, @@ -46,5 +51,6 @@ export const applicationTableMockData = [ totalPasswords: 1, atRiskMembers: 0, totalMembers: 0, + isMarkedAsCritical: false, }, ]; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts new file mode 100644 index 00000000000..2e9a9c33e7f --- /dev/null +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.spec.ts @@ -0,0 +1,68 @@ +import { TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; + +import { + CriticalAppsService, + PasswordHealthReportApplicationsRequest, + PasswordHealthReportApplicationsResponse, +} from "./critical-apps.service"; + +describe("CriticaAppsService", () => { + let service: CriticalAppsService; + const apiService = mock(); + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [CriticalAppsService, { provide: ApiService, useValue: apiService }], + }); + service = TestBed.inject(CriticalAppsService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + it("should set critical apps", async () => { + const criticalApps = [ + { organizationId: "org1", url: "https://example.com" }, + { organizationId: "org2", url: "https://example.org" }, + ] as PasswordHealthReportApplicationsRequest[]; + + const response = [ + { id: "id1", organizationId: "org1", uri: "https://example.com" }, + { id: "id2", organizationId: "org2", uri: "https://example.org" }, + ] as PasswordHealthReportApplicationsResponse[]; + + apiService.send.mockResolvedValue(response); + await service.setCriticalApps(criticalApps); + + expect(apiService.send).toHaveBeenCalledWith( + "POST", + "/reports/password-health-report-applications/", + criticalApps, + true, + true, + ); + }); + + it("should get critical apps", async () => { + const orgId = "org1"; + const response = [ + { id: "id1", organizationId: "org1", uri: "https://example.com" }, + { id: "id2", organizationId: "org2", uri: "https://example.org" }, + ] as PasswordHealthReportApplicationsResponse[]; + + apiService.send.mockResolvedValue(response); + await service.getCriticalApps(orgId); + + expect(apiService.send).toHaveBeenCalledWith( + "GET", + `/reports/password-health-report-applications/${orgId}`, + null, + true, + true, + ); + }); +}); diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts new file mode 100644 index 00000000000..f1fb8dd3a09 --- /dev/null +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/critical-apps.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { Guid } from "@bitwarden/common/types/guid"; + +@Injectable({ + providedIn: "root", +}) +export class CriticalAppsService { + constructor(private apiService: ApiService) {} + + async setCriticalApps( + criticalApps: PasswordHealthReportApplicationsRequest[], + ): Promise { + const response = await this.apiService.send( + "POST", + "/reports/password-health-report-applications/", + criticalApps, + true, + true, + ); + + return response.map((r: { id: any; organizationId: any; uri: any }) => { + return { + id: r.id, + organizationId: r.organizationId, + uri: r.uri, + }; + }); + } + + async getCriticalApps(orgId: string): Promise { + const response = await this.apiService.send( + "GET", + `/reports/password-health-report-applications/${orgId}`, + null, + true, + true, + ); + + return response.map((r: { id: any; organizationId: any; uri: any }) => { + return { + id: r.id, + organizationId: r.organizationId, + uri: r.uri, + }; + }); + } +} + +export interface PasswordHealthReportApplicationsRequest { + organizationId: Guid; + url: string; +} + +export interface PasswordHealthReportApplicationsResponse { + id: Guid; + organizationId: Guid; + uri: string; +} diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts index c7bace84e5b..aa34a8b6f43 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts @@ -1,2 +1,3 @@ export * from "./member-cipher-details-api.service"; export * from "./password-health.service"; +export * from "./critical-apps.service";