1
0
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:
voommen-livefront
2024-11-14 14:04:06 -06:00
parent 4963b28b82
commit c0b8f78db6
6 changed files with 207 additions and 15 deletions

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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,
},
];

View File

@@ -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,
);
});
});

View File

@@ -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;
}

View File

@@ -1,2 +1,3 @@
export * from "./member-cipher-details-api.service";
export * from "./password-health.service";
export * from "./critical-apps.service";