mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
PM-14470 save/get encrypt/decrypt urls
This commit is contained in:
@@ -58,7 +58,7 @@
|
||||
buttonType="secondary"
|
||||
bitButton
|
||||
*ngIf="isCritialAppsFeatureEnabled"
|
||||
[disabled]="!selectedIds.size"
|
||||
[disabled]="!selectedUrls.size"
|
||||
[loading]="markingAsCritical"
|
||||
(click)="markAppsAsCritical()"
|
||||
>
|
||||
@@ -83,8 +83,8 @@
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
[checked]="selectedIds.has(r.id)"
|
||||
(change)="onCheckboxChange(r.id, $event)"
|
||||
[checked]="selectedUrls.has(r.name)"
|
||||
(change)="onCheckboxChange(r.name, $event)"
|
||||
/>
|
||||
</td>
|
||||
<td bitCell>
|
||||
|
||||
@@ -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<any>();
|
||||
protected selectedIds: Set<number> = new Set<number>();
|
||||
protected selectedUrls: Set<string> = new Set<string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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<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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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<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,2 +1,3 @@
|
||||
export * from "./member-cipher-details-api.service";
|
||||
export * from "./password-health.service";
|
||||
export * from "./critical-apps.service";
|
||||
|
||||
Reference in New Issue
Block a user