mirror of
https://github.com/bitwarden/browser
synced 2026-02-13 15:03:26 +00:00
PM-14927 Refactor critical app service and render counts
This commit is contained in:
@@ -5,19 +5,13 @@ import { ActivatedRoute } from "@angular/router";
|
||||
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 { CriticalAppsApiService } 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";
|
||||
@@ -28,7 +22,6 @@ 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";
|
||||
@@ -53,7 +46,6 @@ export class AllApplicationsComponent implements OnInit {
|
||||
noItemsIcon = Icons.Security;
|
||||
protected markingAsCritical = false;
|
||||
isCritialAppsFeatureEnabled = false;
|
||||
private flaggedCriticalApps: PasswordHealthReportApplicationsResponse[] = [];
|
||||
|
||||
// MOCK DATA
|
||||
protected mockData = applicationTableMockData;
|
||||
@@ -74,17 +66,7 @@ export class AllApplicationsComponent implements OnInit {
|
||||
}),
|
||||
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);
|
||||
await this.criticalAppsService.getCriticalApps(organizationId);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
@@ -103,9 +85,7 @@ export class AllApplicationsComponent implements OnInit {
|
||||
protected toastService: ToastService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected configService: ConfigService,
|
||||
protected criticalAppsService: CriticalAppsService,
|
||||
private keyService: KeyService,
|
||||
private encryptService: EncryptService,
|
||||
protected criticalAppsService: CriticalAppsApiService,
|
||||
) {
|
||||
this.dataSource.data = applicationTableMockData;
|
||||
this.searchControl.valueChanges
|
||||
@@ -124,38 +104,10 @@ export class AllApplicationsComponent implements OnInit {
|
||||
|
||||
markAppsAsCritical = async () => {
|
||||
this.markingAsCritical = true;
|
||||
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);
|
||||
});
|
||||
|
||||
.setCriticalApps(this.organization.id, Array.from(this.selectedUrls))
|
||||
.then(() => {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
|
||||
@@ -4,6 +4,8 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CriticalAppsApiService } from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
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";
|
||||
@@ -11,6 +13,7 @@ import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/compone
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
|
||||
import { AllApplicationsComponent } from "./all-applications.component";
|
||||
import { applicationTableMockData } from "./application-table.mock";
|
||||
import { CriticalApplicationsComponent } from "./critical-applications.component";
|
||||
import { NotifiedMembersTableComponent } from "./notified-members-table.component";
|
||||
import { PasswordHealthMembersURIComponent } from "./password-health-members-uri.component";
|
||||
@@ -46,7 +49,7 @@ export class RiskInsightsComponent implements OnInit {
|
||||
dataLastUpdated = new Date();
|
||||
isCritialAppsFeatureEnabled = false;
|
||||
|
||||
apps: any[] = [];
|
||||
apps: any[] = applicationTableMockData;
|
||||
criticalApps: any[] = [];
|
||||
notifiedMembers: any[] = [];
|
||||
|
||||
@@ -78,9 +81,14 @@ export class RiskInsightsComponent implements OnInit {
|
||||
protected route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
private criticalAppsApiService: CriticalAppsApiService,
|
||||
) {
|
||||
route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
|
||||
this.tabIndex = !isNaN(tabIndex) ? tabIndex : RiskInsightsTabType.AllApps;
|
||||
});
|
||||
|
||||
this.criticalAppsApiService.criticalApps$.pipe(takeUntilDestroyed()).subscribe((apps) => {
|
||||
this.criticalApps = apps;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { Guid } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
CriticalAppsApiService,
|
||||
PasswordHealthReportApplicationsRequest,
|
||||
PasswordHealthReportApplicationsResponse,
|
||||
} from "./critical-apps-api.service";
|
||||
|
||||
describe("CriticalAppsApiService", () => {
|
||||
let service: CriticalAppsApiService;
|
||||
const apiService = mock<ApiService>();
|
||||
const keyService = mock<KeyService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CriticalAppsApiService,
|
||||
{ provide: ApiService, useValue: apiService },
|
||||
{ provide: KeyService, useValue: keyService },
|
||||
{ provide: EncryptService, useValue: encryptService },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(CriticalAppsApiService);
|
||||
|
||||
// reset mocks
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should be created", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should set critical apps", async () => {
|
||||
// arrange
|
||||
const criticalApps = ["https://example.com", "https://example.org"];
|
||||
|
||||
const request = [
|
||||
{ organizationId: "org1", url: "encryptedUrlName" },
|
||||
{ organizationId: "org1", url: "encryptedUrlName" },
|
||||
] as PasswordHealthReportApplicationsRequest[];
|
||||
|
||||
const response = [
|
||||
{ id: "id1", organizationId: "org1", uri: "https://example.com" },
|
||||
{ id: "id2", organizationId: "org1", uri: "https://example.org" },
|
||||
] as PasswordHealthReportApplicationsResponse[];
|
||||
|
||||
encryptService.encrypt.mockResolvedValue(new EncString("encryptedUrlName"));
|
||||
apiService.send.mockResolvedValue(response);
|
||||
|
||||
// act
|
||||
await service.setCriticalApps("org1", criticalApps);
|
||||
|
||||
// expectations
|
||||
expect(keyService.getOrgKey).toHaveBeenCalledWith("org1");
|
||||
expect(encryptService.encrypt).toHaveBeenCalledTimes(2);
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
"/reports/password-health-report-applications/",
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("should exclude records that already exist", async () => {
|
||||
// arrange
|
||||
// one record already exists
|
||||
service.criticalApps = [
|
||||
{ id: randomUUID() as Guid, organizationId: "org1" as Guid, uri: "https://example.com" },
|
||||
];
|
||||
|
||||
// two records are selected - one already in the database
|
||||
const selectedUrls = ["https://example.com", "https://example.org"];
|
||||
|
||||
// expect only one record to be sent to the server
|
||||
const request = [
|
||||
{ organizationId: "org1", url: "encryptedUrlName" },
|
||||
] as PasswordHealthReportApplicationsRequest[];
|
||||
|
||||
// mocked response
|
||||
const response = [
|
||||
{ id: "id1", organizationId: "org1", uri: "test" },
|
||||
] as PasswordHealthReportApplicationsResponse[];
|
||||
|
||||
encryptService.encrypt.mockResolvedValue(new EncString("encryptedUrlName"));
|
||||
apiService.send.mockResolvedValue(response);
|
||||
|
||||
// act
|
||||
await service.setCriticalApps("org1", selectedUrls);
|
||||
|
||||
// expectations
|
||||
expect(keyService.getOrgKey).toHaveBeenCalledWith("org1");
|
||||
expect(encryptService.encrypt).toHaveBeenCalledTimes(1);
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
"/reports/password-health-report-applications/",
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("should get critical apps", async () => {
|
||||
const orgId = "org1";
|
||||
const response = [
|
||||
{ id: "id1", organizationId: "org1", uri: "https://example.com" },
|
||||
{ id: "id2", organizationId: "org1", uri: "https://example.org" },
|
||||
] as PasswordHealthReportApplicationsResponse[];
|
||||
|
||||
encryptService.decryptToUtf8.mockResolvedValue("https://example.com");
|
||||
apiService.send.mockResolvedValue(response);
|
||||
await service.getCriticalApps(orgId);
|
||||
|
||||
expect(keyService.getOrgKey).toHaveBeenCalledWith(orgId);
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledTimes(2);
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"GET",
|
||||
`/reports/password-health-report-applications/${orgId}`,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { Guid } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class CriticalAppsApiService {
|
||||
private criticalAppsList = new BehaviorSubject<PasswordHealthReportApplicationsResponse[]>([]);
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private keyService: KeyService,
|
||||
private encryptService: EncryptService,
|
||||
) {}
|
||||
|
||||
get criticalApps$(): Observable<PasswordHealthReportApplicationsResponse[]> {
|
||||
return this.criticalAppsList.asObservable();
|
||||
}
|
||||
|
||||
set criticalApps(value: PasswordHealthReportApplicationsResponse[]) {
|
||||
this.criticalAppsList.next(value);
|
||||
}
|
||||
|
||||
async setCriticalApps(orgId: string, selectedUrls: string[]) {
|
||||
const key = await this.keyService.getOrgKey(orgId);
|
||||
|
||||
// only save records that are not already in the database
|
||||
const newEntries = Array.from(selectedUrls).filter((url) => {
|
||||
return !this.criticalAppsList.value.some((r) => r.uri === url);
|
||||
});
|
||||
|
||||
const criticalAppsPromises = newEntries.map(async (url) => {
|
||||
const encryptedUrlName = await this.encryptService.encrypt(url, key);
|
||||
return {
|
||||
organizationId: orgId,
|
||||
url: encryptedUrlName.encryptedString.toString(),
|
||||
} as PasswordHealthReportApplicationsRequest;
|
||||
});
|
||||
|
||||
const criticalAppsRequests = await Promise.all(criticalAppsPromises);
|
||||
|
||||
await this.apiService
|
||||
.send(
|
||||
"POST",
|
||||
"/reports/password-health-report-applications/",
|
||||
criticalAppsRequests,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.then((result: PasswordHealthReportApplicationsResponse[]) => {
|
||||
result.forEach(async (r) => {
|
||||
const decryptedUrl = await this.encryptService.decryptToUtf8(new EncString(r.uri), key);
|
||||
if (!this.criticalAppsList.value.some((f) => f.uri === decryptedUrl)) {
|
||||
this.criticalAppsList.value.push({
|
||||
id: r.id,
|
||||
organizationId: r.organizationId,
|
||||
uri: decryptedUrl,
|
||||
} as PasswordHealthReportApplicationsResponse);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getCriticalApps(orgId: string): Promise<PasswordHealthReportApplicationsResponse[]> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
`/reports/password-health-report-applications/${orgId}`,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
this.criticalAppsList.next([]);
|
||||
const key = await this.keyService.getOrgKey(orgId);
|
||||
|
||||
await Promise.all(
|
||||
response.map(async (r: { id: any; organizationId: any; uri: any }) => {
|
||||
const decryptedUrl = await this.encryptService.decryptToUtf8(new EncString(r.uri), key);
|
||||
this.criticalAppsList.value.push({
|
||||
id: r.id,
|
||||
organizationId: r.organizationId,
|
||||
uri: decryptedUrl,
|
||||
} as PasswordHealthReportApplicationsResponse);
|
||||
}),
|
||||
);
|
||||
|
||||
return this.criticalAppsList.value;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PasswordHealthReportApplicationsRequest {
|
||||
organizationId: Guid;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface PasswordHealthReportApplicationsResponse {
|
||||
id: Guid;
|
||||
organizationId: Guid;
|
||||
uri: string;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
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<ApiService>();
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
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<PasswordHealthReportApplicationsResponse[]> {
|
||||
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<PasswordHealthReportApplicationsResponse[]> {
|
||||
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;
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from "./member-cipher-details-api.service";
|
||||
export * from "./password-health.service";
|
||||
export * from "./critical-apps.service";
|
||||
export * from "./critical-apps-api.service";
|
||||
|
||||
Reference in New Issue
Block a user