mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 01:33:33 +00:00
PM-15070 Star critical apps (#12109)
Ability to star a record when flagged as critical. This is still behind a feature flag
This commit is contained in:
@@ -30,6 +30,10 @@ export type ApplicationHealthReportDetail = {
|
|||||||
atRiskMemberDetails: MemberDetailsFlat[];
|
atRiskMemberDetails: MemberDetailsFlat[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApplicationHealthReportDetailWithCriticalFlag = ApplicationHealthReportDetail & {
|
||||||
|
isMarkedAsCritical: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Breaks the cipher health info out by uri and passes
|
* Breaks the cipher health info out by uri and passes
|
||||||
* along the password health and member info
|
* along the password health and member info
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { CriticalAppsApiService } from "./critical-apps-api.service";
|
||||||
|
import {
|
||||||
|
PasswordHealthReportApplicationId,
|
||||||
|
PasswordHealthReportApplicationsRequest,
|
||||||
|
PasswordHealthReportApplicationsResponse,
|
||||||
|
} from "./critical-apps.service";
|
||||||
|
|
||||||
|
describe("CriticalAppsApiService", () => {
|
||||||
|
let service: CriticalAppsApiService;
|
||||||
|
const apiService = mock<ApiService>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new CriticalAppsApiService(apiService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be created", () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call apiService.send with correct parameters for SaveCriticalApps", (done) => {
|
||||||
|
const requests: PasswordHealthReportApplicationsRequest[] = [
|
||||||
|
{ organizationId: "org1" as OrganizationId, url: "test one" },
|
||||||
|
{ organizationId: "org1" as OrganizationId, url: "test two" },
|
||||||
|
];
|
||||||
|
const response: PasswordHealthReportApplicationsResponse[] = [
|
||||||
|
{
|
||||||
|
id: "1" as PasswordHealthReportApplicationId,
|
||||||
|
organizationId: "org1" as OrganizationId,
|
||||||
|
uri: "test one",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2" as PasswordHealthReportApplicationId,
|
||||||
|
organizationId: "org1" as OrganizationId,
|
||||||
|
uri: "test two",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
apiService.send.mockReturnValue(Promise.resolve(response));
|
||||||
|
|
||||||
|
service.saveCriticalApps(requests).subscribe((result) => {
|
||||||
|
expect(result).toEqual(response);
|
||||||
|
expect(apiService.send).toHaveBeenCalledWith(
|
||||||
|
"POST",
|
||||||
|
"/reports/password-health-report-applications/",
|
||||||
|
requests,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call apiService.send with correct parameters for GetCriticalApps", (done) => {
|
||||||
|
const orgId: OrganizationId = "org1" as OrganizationId;
|
||||||
|
const response: PasswordHealthReportApplicationsResponse[] = [
|
||||||
|
{ id: "1" as PasswordHealthReportApplicationId, organizationId: orgId, uri: "test one" },
|
||||||
|
{ id: "2" as PasswordHealthReportApplicationId, organizationId: orgId, uri: "test two" },
|
||||||
|
];
|
||||||
|
|
||||||
|
apiService.send.mockReturnValue(Promise.resolve(response));
|
||||||
|
|
||||||
|
service.getCriticalApps(orgId).subscribe((result) => {
|
||||||
|
expect(result).toEqual(response);
|
||||||
|
expect(apiService.send).toHaveBeenCalledWith(
|
||||||
|
"GET",
|
||||||
|
`/reports/password-health-report-applications/${orgId.toString()}`,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { from, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import {
|
||||||
|
PasswordHealthReportApplicationsRequest,
|
||||||
|
PasswordHealthReportApplicationsResponse,
|
||||||
|
} from "./critical-apps.service";
|
||||||
|
|
||||||
|
export class CriticalAppsApiService {
|
||||||
|
constructor(private apiService: ApiService) {}
|
||||||
|
|
||||||
|
saveCriticalApps(
|
||||||
|
requests: PasswordHealthReportApplicationsRequest[],
|
||||||
|
): Observable<PasswordHealthReportApplicationsResponse[]> {
|
||||||
|
const dbResponse = this.apiService.send(
|
||||||
|
"POST",
|
||||||
|
"/reports/password-health-report-applications/",
|
||||||
|
requests,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return from(dbResponse as Promise<PasswordHealthReportApplicationsResponse[]>);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCriticalApps(orgId: OrganizationId): Observable<PasswordHealthReportApplicationsResponse[]> {
|
||||||
|
const dbResponse = this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
`/reports/password-health-report-applications/${orgId.toString()}`,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return from(dbResponse as Promise<PasswordHealthReportApplicationsResponse[]>);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
|
import { fakeAsync, flush } from "@angular/core/testing";
|
||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { CriticalAppsApiService } from "./critical-apps-api.service";
|
||||||
|
import {
|
||||||
|
CriticalAppsService,
|
||||||
|
PasswordHealthReportApplicationId,
|
||||||
|
PasswordHealthReportApplicationsRequest,
|
||||||
|
PasswordHealthReportApplicationsResponse,
|
||||||
|
} from "./critical-apps.service";
|
||||||
|
|
||||||
|
describe("CriticalAppsService", () => {
|
||||||
|
let service: CriticalAppsService;
|
||||||
|
const keyService = mock<KeyService>();
|
||||||
|
const encryptService = mock<EncryptService>();
|
||||||
|
const criticalAppsApiService = mock<CriticalAppsApiService>({
|
||||||
|
saveCriticalApps: jest.fn(),
|
||||||
|
getCriticalApps: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new CriticalAppsService(keyService, encryptService, 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"));
|
||||||
|
criticalAppsApiService.saveCriticalApps.mockReturnValue(of(response));
|
||||||
|
|
||||||
|
// act
|
||||||
|
await service.setCriticalApps("org1", criticalApps);
|
||||||
|
|
||||||
|
// expectations
|
||||||
|
expect(keyService.getOrgKey).toHaveBeenCalledWith("org1");
|
||||||
|
expect(encryptService.encrypt).toHaveBeenCalledTimes(2);
|
||||||
|
expect(criticalAppsApiService.saveCriticalApps).toHaveBeenCalledWith(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should exclude records that already exist", async () => {
|
||||||
|
// arrange
|
||||||
|
// one record already exists
|
||||||
|
service.setAppsInListForOrg([
|
||||||
|
{
|
||||||
|
id: randomUUID() as PasswordHealthReportApplicationId,
|
||||||
|
organizationId: "org1" as OrganizationId,
|
||||||
|
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"));
|
||||||
|
criticalAppsApiService.saveCriticalApps.mockReturnValue(of(response));
|
||||||
|
|
||||||
|
// act
|
||||||
|
await service.setCriticalApps("org1", selectedUrls);
|
||||||
|
|
||||||
|
// expectations
|
||||||
|
expect(keyService.getOrgKey).toHaveBeenCalledWith("org1");
|
||||||
|
expect(encryptService.encrypt).toHaveBeenCalledTimes(1);
|
||||||
|
expect(criticalAppsApiService.saveCriticalApps).toHaveBeenCalledWith(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get critical apps", fakeAsync(() => {
|
||||||
|
const orgId = "org1" as OrganizationId;
|
||||||
|
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");
|
||||||
|
criticalAppsApiService.getCriticalApps.mockReturnValue(of(response));
|
||||||
|
|
||||||
|
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||||
|
const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey;
|
||||||
|
keyService.getOrgKey.mockResolvedValue(mockOrgKey);
|
||||||
|
|
||||||
|
service.setOrganizationId(orgId as OrganizationId);
|
||||||
|
flush();
|
||||||
|
|
||||||
|
expect(keyService.getOrgKey).toHaveBeenCalledWith(orgId.toString());
|
||||||
|
expect(encryptService.decryptToUtf8).toHaveBeenCalledTimes(2);
|
||||||
|
expect(criticalAppsApiService.getCriticalApps).toHaveBeenCalledWith(orgId);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it("should get by org id", () => {
|
||||||
|
const orgId = "org1" as OrganizationId;
|
||||||
|
const response = [
|
||||||
|
{ id: "id1", organizationId: "org1", uri: "https://example.com" },
|
||||||
|
{ id: "id2", organizationId: "org1", uri: "https://example.org" },
|
||||||
|
{ id: "id3", organizationId: "org2", uri: "https://example.org" },
|
||||||
|
{ id: "id4", organizationId: "org2", uri: "https://example.org" },
|
||||||
|
] as PasswordHealthReportApplicationsResponse[];
|
||||||
|
|
||||||
|
service.setAppsInListForOrg(response);
|
||||||
|
|
||||||
|
service.getAppsListForOrg(orgId as OrganizationId).subscribe((res) => {
|
||||||
|
expect(res).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
first,
|
||||||
|
firstValueFrom,
|
||||||
|
forkJoin,
|
||||||
|
from,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
Subject,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
zip,
|
||||||
|
} from "rxjs";
|
||||||
|
import { Opaque } from "type-fest";
|
||||||
|
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { CriticalAppsApiService } from "./critical-apps-api.service";
|
||||||
|
|
||||||
|
/* Retrieves and decrypts critical apps for a given organization
|
||||||
|
* Encrypts and saves data for a given organization
|
||||||
|
*/
|
||||||
|
export class CriticalAppsService {
|
||||||
|
private orgId = new BehaviorSubject<OrganizationId | null>(null);
|
||||||
|
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));
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private keyService: KeyService,
|
||||||
|
private encryptService: EncryptService,
|
||||||
|
private criticalAppsApiService: CriticalAppsApiService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// Get a list of critical apps for a given organization
|
||||||
|
getAppsListForOrg(orgId: string): Observable<PasswordHealthReportApplicationsResponse[]> {
|
||||||
|
return this.criticalAppsList
|
||||||
|
.asObservable()
|
||||||
|
.pipe(map((apps) => apps.filter((app) => app.organizationId === orgId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the critical apps list
|
||||||
|
setAppsInListForOrg(apps: PasswordHealthReportApplicationsResponse[]) {
|
||||||
|
this.criticalAppsList.next(apps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the selected critical apps for a given organization
|
||||||
|
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 = await this.filterNewEntries(orgId as OrganizationId, selectedUrls);
|
||||||
|
const criticalAppsRequests = await this.encryptNewEntries(
|
||||||
|
orgId as OrganizationId,
|
||||||
|
key,
|
||||||
|
newEntries,
|
||||||
|
);
|
||||||
|
|
||||||
|
const dbResponse = await firstValueFrom(
|
||||||
|
this.criticalAppsApiService.saveCriticalApps(criticalAppsRequests),
|
||||||
|
);
|
||||||
|
|
||||||
|
// add the new entries to the criticalAppsList
|
||||||
|
const updatedList = [...this.criticalAppsList.value];
|
||||||
|
for (const responseItem of dbResponse) {
|
||||||
|
const decryptedUrl = await this.encryptService.decryptToUtf8(
|
||||||
|
new EncString(responseItem.uri),
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
if (!updatedList.some((f) => f.uri === decryptedUrl)) {
|
||||||
|
updatedList.push({
|
||||||
|
id: responseItem.id,
|
||||||
|
organizationId: responseItem.organizationId,
|
||||||
|
uri: decryptedUrl,
|
||||||
|
} as PasswordHealthReportApplicationsResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.criticalAppsList.next(updatedList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the critical apps for a given organization
|
||||||
|
setOrganizationId(orgId: OrganizationId) {
|
||||||
|
this.orgId.next(orgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private retrieveCriticalApps(
|
||||||
|
orgId: OrganizationId | null,
|
||||||
|
): Observable<PasswordHealthReportApplicationsResponse[]> {
|
||||||
|
if (orgId === null) {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result$ = zip(
|
||||||
|
this.criticalAppsApiService.getCriticalApps(orgId),
|
||||||
|
from(this.keyService.getOrgKey(orgId)),
|
||||||
|
).pipe(
|
||||||
|
switchMap(([response, key]) => {
|
||||||
|
const results = response.map(async (r: PasswordHealthReportApplicationsResponse) => {
|
||||||
|
const encrypted = new EncString(r.uri);
|
||||||
|
const uri = await this.encryptService.decryptToUtf8(encrypted, key);
|
||||||
|
return { id: r.id, organizationId: r.organizationId, uri: uri };
|
||||||
|
});
|
||||||
|
return forkJoin(results);
|
||||||
|
}),
|
||||||
|
first(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return result$ as Observable<PasswordHealthReportApplicationsResponse[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async filterNewEntries(orgId: OrganizationId, selectedUrls: string[]): Promise<string[]> {
|
||||||
|
return await firstValueFrom(this.criticalAppsList).then((criticalApps) => {
|
||||||
|
const criticalAppsUri = criticalApps
|
||||||
|
.filter((f) => f.organizationId === orgId)
|
||||||
|
.map((f) => f.uri);
|
||||||
|
return selectedUrls.filter((url) => !criticalAppsUri.includes(url));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async encryptNewEntries(
|
||||||
|
orgId: OrganizationId,
|
||||||
|
key: OrgKey,
|
||||||
|
newEntries: string[],
|
||||||
|
): Promise<PasswordHealthReportApplicationsRequest[]> {
|
||||||
|
const criticalAppsPromises = newEntries.map(async (url) => {
|
||||||
|
const encryptedUrlName = await this.encryptService.encrypt(url, key);
|
||||||
|
return {
|
||||||
|
organizationId: orgId,
|
||||||
|
url: encryptedUrlName?.encryptedString?.toString() ?? "",
|
||||||
|
} as PasswordHealthReportApplicationsRequest;
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Promise.all(criticalAppsPromises);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasswordHealthReportApplicationsRequest {
|
||||||
|
organizationId: OrganizationId;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasswordHealthReportApplicationsResponse {
|
||||||
|
id: PasswordHealthReportApplicationId;
|
||||||
|
organizationId: OrganizationId;
|
||||||
|
uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PasswordHealthReportApplicationId = Opaque<string, "PasswordHealthReportApplicationId">;
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export * from "./member-cipher-details-api.service";
|
export * from "./member-cipher-details-api.service";
|
||||||
export * from "./password-health.service";
|
export * from "./password-health.service";
|
||||||
|
export * from "./critical-apps.service";
|
||||||
|
export * from "./critical-apps-api.service";
|
||||||
export * from "./risk-insights-report.service";
|
export * from "./risk-insights-report.service";
|
||||||
export * from "./risk-insights-data.service";
|
export * from "./risk-insights-data.service";
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||||
|
import { CriticalAppsService } from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||||
import {
|
import {
|
||||||
|
CriticalAppsApiService,
|
||||||
MemberCipherDetailsApiService,
|
MemberCipherDetailsApiService,
|
||||||
RiskInsightsDataService,
|
RiskInsightsDataService,
|
||||||
RiskInsightsReportService,
|
RiskInsightsReportService,
|
||||||
} from "@bitwarden/bit-common/tools/reports/risk-insights/services";
|
} from "@bitwarden/bit-common/tools/reports/risk-insights/services";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
|
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
|
||||||
import { RiskInsightsComponent } from "./risk-insights.component";
|
import { RiskInsightsComponent } from "./risk-insights.component";
|
||||||
@@ -33,6 +38,16 @@ import { RiskInsightsComponent } from "./risk-insights.component";
|
|||||||
provide: RiskInsightsDataService,
|
provide: RiskInsightsDataService,
|
||||||
deps: [RiskInsightsReportService],
|
deps: [RiskInsightsReportService],
|
||||||
},
|
},
|
||||||
|
safeProvider({
|
||||||
|
provide: CriticalAppsService,
|
||||||
|
useClass: CriticalAppsService,
|
||||||
|
deps: [KeyService, EncryptService, CriticalAppsApiService],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: CriticalAppsApiService,
|
||||||
|
useClass: CriticalAppsApiService,
|
||||||
|
deps: [ApiService],
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AccessIntelligenceModule {}
|
export class AccessIntelligenceModule {}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
buttonType="secondary"
|
buttonType="secondary"
|
||||||
bitButton
|
bitButton
|
||||||
*ngIf="isCriticalAppsFeatureEnabled"
|
*ngIf="isCriticalAppsFeatureEnabled"
|
||||||
[disabled]="!selectedIds.size"
|
[disabled]="!selectedUrls.size"
|
||||||
[loading]="markingAsCritical"
|
[loading]="markingAsCritical"
|
||||||
(click)="markAppsAsCritical()"
|
(click)="markAppsAsCritical()"
|
||||||
>
|
>
|
||||||
@@ -80,9 +80,11 @@
|
|||||||
<input
|
<input
|
||||||
bitCheckbox
|
bitCheckbox
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
[checked]="selectedIds.has(r.id)"
|
*ngIf="!r.isMarkedAsCritical"
|
||||||
(change)="onCheckboxChange(r.id, $event)"
|
[checked]="selectedUrls.has(r.applicationName)"
|
||||||
|
(change)="onCheckboxChange(r.applicationName, $event)"
|
||||||
/>
|
/>
|
||||||
|
<i class="bwi bwi-star-f" *ngIf="r.isMarkedAsCritical"></i>
|
||||||
</td>
|
</td>
|
||||||
<td class="tw-cursor-pointer" (click)="showAppAtRiskMembers(r.applicationName)" bitCell>
|
<td class="tw-cursor-pointer" (click)="showAppAtRiskMembers(r.applicationName)" bitCell>
|
||||||
<span>{{ r.applicationName }}</span>
|
<span>{{ r.applicationName }}</span>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { Component, DestroyRef, OnDestroy, OnInit, inject } from "@angular/core";
|
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormControl } from "@angular/forms";
|
import { FormControl } from "@angular/forms";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { debounceTime, map, Observable, of, Subscription } from "rxjs";
|
import { combineLatest, debounceTime, map, Observable, of, skipWhile } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CriticalAppsService,
|
||||||
RiskInsightsDataService,
|
RiskInsightsDataService,
|
||||||
RiskInsightsReportService,
|
RiskInsightsReportService,
|
||||||
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||||
import {
|
import {
|
||||||
ApplicationHealthReportDetail,
|
ApplicationHealthReportDetail,
|
||||||
|
ApplicationHealthReportDetailWithCriticalFlag,
|
||||||
ApplicationHealthReportSummary,
|
ApplicationHealthReportSummary,
|
||||||
} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
@@ -50,16 +52,15 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"
|
|||||||
SharedModule,
|
SharedModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AllApplicationsComponent implements OnInit, OnDestroy {
|
export class AllApplicationsComponent implements OnInit {
|
||||||
protected dataSource = new TableDataSource<ApplicationHealthReportDetail>();
|
protected dataSource = new TableDataSource<ApplicationHealthReportDetailWithCriticalFlag>();
|
||||||
protected selectedIds: Set<number> = new Set<number>();
|
protected selectedUrls: Set<string> = new Set<string>();
|
||||||
protected searchControl = new FormControl("", { nonNullable: true });
|
protected searchControl = new FormControl("", { nonNullable: true });
|
||||||
protected loading = true;
|
protected loading = true;
|
||||||
protected organization = {} as Organization;
|
protected organization = {} as Organization;
|
||||||
noItemsIcon = Icons.Security;
|
noItemsIcon = Icons.Security;
|
||||||
protected markingAsCritical = false;
|
protected markingAsCritical = false;
|
||||||
protected applicationSummary = {} as ApplicationHealthReportSummary;
|
protected applicationSummary = {} as ApplicationHealthReportSummary;
|
||||||
private subscription = new Subscription();
|
|
||||||
|
|
||||||
destroyRef = inject(DestroyRef);
|
destroyRef = inject(DestroyRef);
|
||||||
isLoading$: Observable<boolean> = of(false);
|
isLoading$: Observable<boolean> = of(false);
|
||||||
@@ -70,28 +71,33 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
|
|||||||
FeatureFlag.CriticalApps,
|
FeatureFlag.CriticalApps,
|
||||||
);
|
);
|
||||||
|
|
||||||
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId");
|
const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? "";
|
||||||
|
combineLatest([
|
||||||
|
this.dataService.applications$,
|
||||||
|
this.criticalAppsService.getAppsListForOrg(organizationId),
|
||||||
|
this.organizationService.get$(organizationId),
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
skipWhile(([_, __, organization]) => !organization),
|
||||||
|
map(([applications, criticalApps, organization]) => {
|
||||||
|
const criticalUrls = criticalApps.map((ca) => ca.uri);
|
||||||
|
const data = applications?.map((app) => ({
|
||||||
|
...app,
|
||||||
|
isMarkedAsCritical: criticalUrls.includes(app.applicationName),
|
||||||
|
})) as ApplicationHealthReportDetailWithCriticalFlag[];
|
||||||
|
return { data, organization };
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe(({ data, organization }) => {
|
||||||
|
this.dataSource.data = data ?? [];
|
||||||
|
this.applicationSummary = this.reportService.generateApplicationsSummary(data ?? []);
|
||||||
|
if (organization) {
|
||||||
|
this.organization = organization;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (organizationId) {
|
this.isLoading$ = this.dataService.isLoading$;
|
||||||
this.organization = await this.organizationService.get(organizationId);
|
|
||||||
this.subscription = this.dataService.applications$
|
|
||||||
.pipe(
|
|
||||||
map((applications) => {
|
|
||||||
if (applications) {
|
|
||||||
this.dataSource.data = applications;
|
|
||||||
this.applicationSummary =
|
|
||||||
this.reportService.generateApplicationsSummary(applications);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
takeUntilDestroyed(this.destroyRef),
|
|
||||||
)
|
|
||||||
.subscribe();
|
|
||||||
this.isLoading$ = this.dataService.isLoading$;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.subscription?.unsubscribe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -103,6 +109,7 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
|
|||||||
protected dataService: RiskInsightsDataService,
|
protected dataService: RiskInsightsDataService,
|
||||||
protected organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
protected reportService: RiskInsightsReportService,
|
protected reportService: RiskInsightsReportService,
|
||||||
|
protected criticalAppsService: CriticalAppsService,
|
||||||
protected dialogService: DialogService,
|
protected dialogService: DialogService,
|
||||||
) {
|
) {
|
||||||
this.searchControl.valueChanges
|
this.searchControl.valueChanges
|
||||||
@@ -119,21 +126,28 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
isMarkedAsCriticalItem(applicationName: string) {
|
||||||
|
return this.selectedUrls.has(applicationName);
|
||||||
|
}
|
||||||
|
|
||||||
markAppsAsCritical = async () => {
|
markAppsAsCritical = async () => {
|
||||||
// TODO: Send to API once implemented
|
|
||||||
this.markingAsCritical = true;
|
this.markingAsCritical = true;
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
try {
|
||||||
this.selectedIds.clear();
|
await this.criticalAppsService.setCriticalApps(
|
||||||
this.toastService.showToast({
|
this.organization.id,
|
||||||
variant: "success",
|
Array.from(this.selectedUrls),
|
||||||
title: "",
|
);
|
||||||
message: this.i18nService.t("appsMarkedAsCritical"),
|
|
||||||
});
|
this.toastService.showToast({
|
||||||
resolve(true);
|
variant: "success",
|
||||||
this.markingAsCritical = false;
|
title: "",
|
||||||
}, 1000);
|
message: this.i18nService.t("appsMarkedAsCritical"),
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
this.selectedUrls.clear();
|
||||||
|
this.markingAsCritical = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
trackByFunction(_: number, item: ApplicationHealthReportDetail) {
|
trackByFunction(_: number, item: ApplicationHealthReportDetail) {
|
||||||
@@ -161,12 +175,14 @@ export class AllApplicationsComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onCheckboxChange(id: number, event: Event) {
|
onCheckboxChange(applicationName: string, event: Event) {
|
||||||
const isChecked = (event.target as HTMLInputElement).checked;
|
const isChecked = (event.target as HTMLInputElement).checked;
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
this.selectedIds.add(id);
|
this.selectedUrls.add(applicationName);
|
||||||
} else {
|
} else {
|
||||||
this.selectedIds.delete(id);
|
this.selectedUrls.delete(applicationName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSelectedUrls = () => Array.from(this.selectedUrls);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const applicationTableMockData = [
|
|||||||
totalPasswords: 10,
|
totalPasswords: 10,
|
||||||
atRiskMembers: 2,
|
atRiskMembers: 2,
|
||||||
totalMembers: 5,
|
totalMembers: 5,
|
||||||
|
isMarkedAsCritical: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -14,6 +15,7 @@ export const applicationTableMockData = [
|
|||||||
totalPasswords: 8,
|
totalPasswords: 8,
|
||||||
atRiskMembers: 1,
|
atRiskMembers: 1,
|
||||||
totalMembers: 3,
|
totalMembers: 3,
|
||||||
|
isMarkedAsCritical: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -22,6 +24,7 @@ export const applicationTableMockData = [
|
|||||||
totalPasswords: 6,
|
totalPasswords: 6,
|
||||||
atRiskMembers: 0,
|
atRiskMembers: 0,
|
||||||
totalMembers: 2,
|
totalMembers: 2,
|
||||||
|
isMarkedAsCritical: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
@@ -30,6 +33,7 @@ export const applicationTableMockData = [
|
|||||||
totalPasswords: 4,
|
totalPasswords: 4,
|
||||||
atRiskMembers: 0,
|
atRiskMembers: 0,
|
||||||
totalMembers: 1,
|
totalMembers: 1,
|
||||||
|
isMarkedAsCritical: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
@@ -38,6 +42,7 @@ export const applicationTableMockData = [
|
|||||||
totalPasswords: 2,
|
totalPasswords: 2,
|
||||||
atRiskMembers: 0,
|
atRiskMembers: 0,
|
||||||
totalMembers: 0,
|
totalMembers: 0,
|
||||||
|
isMarkedAsCritical: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
@@ -46,5 +51,6 @@ export const applicationTableMockData = [
|
|||||||
totalPasswords: 1,
|
totalPasswords: 1,
|
||||||
atRiskMembers: 0,
|
atRiskMembers: 0,
|
||||||
totalMembers: 0,
|
totalMembers: 0,
|
||||||
|
isMarkedAsCritical: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<bit-tab *ngIf="isCriticalAppsFeatureEnabled">
|
<bit-tab *ngIf="isCriticalAppsFeatureEnabled">
|
||||||
<ng-template bitTabLabel>
|
<ng-template bitTabLabel>
|
||||||
<i class="bwi bwi-star"></i>
|
<i class="bwi bwi-star"></i>
|
||||||
{{ "criticalApplicationsWithCount" | i18n: criticalAppsCount }}
|
{{ "criticalApplicationsWithCount" | i18n: (criticalApps$ | async)?.length ?? 0 }}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<tools-critical-applications></tools-critical-applications>
|
<tools-critical-applications></tools-critical-applications>
|
||||||
</bit-tab>
|
</bit-tab>
|
||||||
|
|||||||
@@ -6,11 +6,17 @@ import { Observable, EMPTY } from "rxjs";
|
|||||||
import { map, switchMap } from "rxjs/operators";
|
import { map, switchMap } from "rxjs/operators";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { RiskInsightsDataService } from "@bitwarden/bit-common/tools/reports/risk-insights";
|
import {
|
||||||
|
RiskInsightsDataService,
|
||||||
|
CriticalAppsService,
|
||||||
|
PasswordHealthReportApplicationsResponse,
|
||||||
|
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||||
import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||||
|
// eslint-disable-next-line no-restricted-imports -- used for dependency injection
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components";
|
import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components";
|
||||||
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
||||||
|
|
||||||
@@ -51,6 +57,7 @@ export class RiskInsightsComponent implements OnInit {
|
|||||||
dataLastUpdated: Date = new Date();
|
dataLastUpdated: Date = new Date();
|
||||||
|
|
||||||
isCriticalAppsFeatureEnabled: boolean = false;
|
isCriticalAppsFeatureEnabled: boolean = false;
|
||||||
|
criticalApps$: Observable<PasswordHealthReportApplicationsResponse[]> = new Observable();
|
||||||
showDebugTabs: boolean = false;
|
showDebugTabs: boolean = false;
|
||||||
|
|
||||||
appsCount: number = 0;
|
appsCount: number = 0;
|
||||||
@@ -69,10 +76,13 @@ export class RiskInsightsComponent implements OnInit {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private dataService: RiskInsightsDataService,
|
private dataService: RiskInsightsDataService,
|
||||||
|
private criticalAppsService: CriticalAppsService,
|
||||||
) {
|
) {
|
||||||
this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
|
this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
|
||||||
this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps;
|
this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps;
|
||||||
});
|
});
|
||||||
|
const orgId = this.route.snapshot.paramMap.get("organizationId") ?? "";
|
||||||
|
this.criticalApps$ = this.criticalAppsService.getAppsListForOrg(orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -104,6 +114,7 @@ export class RiskInsightsComponent implements OnInit {
|
|||||||
if (applications) {
|
if (applications) {
|
||||||
this.appsCount = applications.length;
|
this.appsCount = applications.length;
|
||||||
}
|
}
|
||||||
|
this.criticalAppsService.setOrganizationId(this.organizationId as OrganizationId);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user