mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
PM-15090 - unmark-critical-app (#13015)
This commit is contained in:
@@ -176,6 +176,12 @@
|
|||||||
"totalApplications": {
|
"totalApplications": {
|
||||||
"message": "Total applications"
|
"message": "Total applications"
|
||||||
},
|
},
|
||||||
|
"unmarkAsCriticalApp": {
|
||||||
|
"message": "Unmark as critical app"
|
||||||
|
},
|
||||||
|
"criticalApplicationSuccessfullyUnmarked": {
|
||||||
|
"message": "Critical application successfully unmarked"
|
||||||
|
},
|
||||||
"whatTypeOfItem": {
|
"whatTypeOfItem": {
|
||||||
"message": "What type of item is this?"
|
"message": "What type of item is this?"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
|
||||||
|
import { Opaque } from "type-fest";
|
||||||
|
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { BadgeVariant } from "@bitwarden/components";
|
import { BadgeVariant } from "@bitwarden/components";
|
||||||
|
|
||||||
@@ -113,3 +116,31 @@ export type AtRiskApplicationDetail = {
|
|||||||
applicationName: string;
|
applicationName: string;
|
||||||
atRiskPasswordCount: number;
|
atRiskPasswordCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to drop a password health report application
|
||||||
|
* Model is expected by the API endpoint
|
||||||
|
*/
|
||||||
|
export interface PasswordHealthReportApplicationDropRequest {
|
||||||
|
organizationId: OrganizationId;
|
||||||
|
passwordHealthReportApplicationIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from the API after marking an app as critical
|
||||||
|
*/
|
||||||
|
export interface PasswordHealthReportApplicationsResponse {
|
||||||
|
id: PasswordHealthReportApplicationId;
|
||||||
|
organizationId: OrganizationId;
|
||||||
|
uri: string;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Request to save a password health report application
|
||||||
|
* Model is expected by the API endpoint
|
||||||
|
*/
|
||||||
|
export interface PasswordHealthReportApplicationsRequest {
|
||||||
|
organizationId: OrganizationId;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PasswordHealthReportApplicationId = Opaque<string, "PasswordHealthReportApplicationId">;
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { mock } from "jest-mock-extended";
|
|||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { CriticalAppsApiService } from "./critical-apps-api.service";
|
|
||||||
import {
|
import {
|
||||||
|
PasswordHealthReportApplicationDropRequest,
|
||||||
PasswordHealthReportApplicationId,
|
PasswordHealthReportApplicationId,
|
||||||
PasswordHealthReportApplicationsRequest,
|
PasswordHealthReportApplicationsRequest,
|
||||||
PasswordHealthReportApplicationsResponse,
|
PasswordHealthReportApplicationsResponse,
|
||||||
} from "./critical-apps.service";
|
} from "../models/password-health";
|
||||||
|
|
||||||
|
import { CriticalAppsApiService } from "./critical-apps-api.service";
|
||||||
|
|
||||||
describe("CriticalAppsApiService", () => {
|
describe("CriticalAppsApiService", () => {
|
||||||
let service: CriticalAppsApiService;
|
let service: CriticalAppsApiService;
|
||||||
@@ -76,4 +78,24 @@ describe("CriticalAppsApiService", () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should call apiService.send with correct parameters for DropCriticalApp", (done) => {
|
||||||
|
const request: PasswordHealthReportApplicationDropRequest = {
|
||||||
|
organizationId: "org1" as OrganizationId,
|
||||||
|
passwordHealthReportApplicationIds: ["123"],
|
||||||
|
};
|
||||||
|
|
||||||
|
apiService.send.mockReturnValue(Promise.resolve());
|
||||||
|
|
||||||
|
service.dropCriticalApp(request).subscribe(() => {
|
||||||
|
expect(apiService.send).toHaveBeenCalledWith(
|
||||||
|
"DELETE",
|
||||||
|
"/reports/password-health-report-application/",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|||||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
PasswordHealthReportApplicationDropRequest,
|
||||||
PasswordHealthReportApplicationsRequest,
|
PasswordHealthReportApplicationsRequest,
|
||||||
PasswordHealthReportApplicationsResponse,
|
PasswordHealthReportApplicationsResponse,
|
||||||
} from "./critical-apps.service";
|
} from "../models/password-health";
|
||||||
|
|
||||||
export class CriticalAppsApiService {
|
export class CriticalAppsApiService {
|
||||||
constructor(private apiService: ApiService) {}
|
constructor(private apiService: ApiService) {}
|
||||||
@@ -36,4 +37,16 @@ export class CriticalAppsApiService {
|
|||||||
|
|
||||||
return from(dbResponse as Promise<PasswordHealthReportApplicationsResponse[]>);
|
return from(dbResponse as Promise<PasswordHealthReportApplicationsResponse[]>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dropCriticalApp(request: PasswordHealthReportApplicationDropRequest): Observable<void> {
|
||||||
|
const dbResponse = this.apiService.send(
|
||||||
|
"DELETE",
|
||||||
|
"/reports/password-health-report-application/",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return from(dbResponse as Promise<void>);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ import { OrganizationId } from "@bitwarden/common/types/guid";
|
|||||||
import { OrgKey } from "@bitwarden/common/types/key";
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
import { CriticalAppsApiService } from "./critical-apps-api.service";
|
|
||||||
import {
|
import {
|
||||||
CriticalAppsService,
|
|
||||||
PasswordHealthReportApplicationId,
|
PasswordHealthReportApplicationId,
|
||||||
PasswordHealthReportApplicationsRequest,
|
PasswordHealthReportApplicationsRequest,
|
||||||
PasswordHealthReportApplicationsResponse,
|
PasswordHealthReportApplicationsResponse,
|
||||||
} from "./critical-apps.service";
|
} from "../models/password-health";
|
||||||
|
|
||||||
|
import { CriticalAppsApiService } from "./critical-apps-api.service";
|
||||||
|
import { CriticalAppsService } from "./critical-apps.service";
|
||||||
|
|
||||||
describe("CriticalAppsService", () => {
|
describe("CriticalAppsService", () => {
|
||||||
let service: CriticalAppsService;
|
let service: CriticalAppsService;
|
||||||
@@ -139,4 +140,54 @@ describe("CriticalAppsService", () => {
|
|||||||
expect(res).toHaveLength(2);
|
expect(res).toHaveLength(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should drop a critical app", async () => {
|
||||||
|
// arrange
|
||||||
|
const orgId = "org1" as OrganizationId;
|
||||||
|
const selectedUrl = "https://example.com";
|
||||||
|
|
||||||
|
const initialList = [
|
||||||
|
{ id: "id1", organizationId: "org1", uri: "https://example.com" },
|
||||||
|
{ id: "id2", organizationId: "org1", uri: "https://example.org" },
|
||||||
|
] as PasswordHealthReportApplicationsResponse[];
|
||||||
|
|
||||||
|
service.setAppsInListForOrg(initialList);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await service.dropCriticalApp(orgId, selectedUrl);
|
||||||
|
|
||||||
|
// expectations
|
||||||
|
expect(criticalAppsApiService.dropCriticalApp).toHaveBeenCalledWith({
|
||||||
|
organizationId: orgId,
|
||||||
|
passwordHealthReportApplicationIds: ["id1"],
|
||||||
|
});
|
||||||
|
expect(service.getAppsListForOrg(orgId)).toBeTruthy();
|
||||||
|
service.getAppsListForOrg(orgId).subscribe((res) => {
|
||||||
|
expect(res).toHaveLength(1);
|
||||||
|
expect(res[0].uri).toBe("https://example.org");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not drop a critical app if it does not exist", async () => {
|
||||||
|
// arrange
|
||||||
|
const orgId = "org1" as OrganizationId;
|
||||||
|
const selectedUrl = "https://nonexistent.com";
|
||||||
|
|
||||||
|
const initialList = [
|
||||||
|
{ id: "id1", organizationId: "org1", uri: "https://example.com" },
|
||||||
|
{ id: "id2", organizationId: "org1", uri: "https://example.org" },
|
||||||
|
] as PasswordHealthReportApplicationsResponse[];
|
||||||
|
|
||||||
|
service.setAppsInListForOrg(initialList);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await service.dropCriticalApp(orgId, selectedUrl);
|
||||||
|
|
||||||
|
// expectations
|
||||||
|
expect(criticalAppsApiService.dropCriticalApp).not.toHaveBeenCalled();
|
||||||
|
expect(service.getAppsListForOrg(orgId)).toBeTruthy();
|
||||||
|
service.getAppsListForOrg(orgId).subscribe((res) => {
|
||||||
|
expect(res).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
takeUntil,
|
takeUntil,
|
||||||
zip,
|
zip,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import { Opaque } from "type-fest";
|
|
||||||
|
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
@@ -20,6 +19,11 @@ import { OrganizationId } from "@bitwarden/common/types/guid";
|
|||||||
import { OrgKey } from "@bitwarden/common/types/key";
|
import { OrgKey } from "@bitwarden/common/types/key";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import {
|
||||||
|
PasswordHealthReportApplicationsRequest,
|
||||||
|
PasswordHealthReportApplicationsResponse,
|
||||||
|
} from "../models/password-health";
|
||||||
|
|
||||||
import { CriticalAppsApiService } from "./critical-apps-api.service";
|
import { CriticalAppsApiService } from "./critical-apps-api.service";
|
||||||
|
|
||||||
/* Retrieves and decrypts critical apps for a given organization
|
/* Retrieves and decrypts critical apps for a given organization
|
||||||
@@ -94,6 +98,25 @@ export class CriticalAppsService {
|
|||||||
this.orgId.next(orgId);
|
this.orgId.next(orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop a critical app for a given organization
|
||||||
|
// Only one app may be dropped at a time
|
||||||
|
async dropCriticalApp(orgId: OrganizationId, selectedUrl: string) {
|
||||||
|
const app = this.criticalAppsList.value.find(
|
||||||
|
(f) => f.organizationId === orgId && f.uri === selectedUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.criticalAppsApiService.dropCriticalApp({
|
||||||
|
organizationId: app.organizationId,
|
||||||
|
passwordHealthReportApplicationIds: [app.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.criticalAppsList.next(this.criticalAppsList.value.filter((f) => f.uri !== selectedUrl));
|
||||||
|
}
|
||||||
|
|
||||||
private retrieveCriticalApps(
|
private retrieveCriticalApps(
|
||||||
orgId: OrganizationId | null,
|
orgId: OrganizationId | null,
|
||||||
): Observable<PasswordHealthReportApplicationsResponse[]> {
|
): Observable<PasswordHealthReportApplicationsResponse[]> {
|
||||||
@@ -144,16 +167,3 @@ export class CriticalAppsService {
|
|||||||
return await Promise.all(criticalAppsPromises);
|
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">;
|
|
||||||
|
|||||||
@@ -95,6 +95,21 @@
|
|||||||
<td bitCell data-testid="total-membership">
|
<td bitCell data-testid="total-membership">
|
||||||
{{ r.memberCount }}
|
{{ r.memberCount }}
|
||||||
</td>
|
</td>
|
||||||
|
<td bitCell>
|
||||||
|
<button
|
||||||
|
[bitMenuTriggerFor]="rowMenu"
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-ellipsis-v"
|
||||||
|
size="small"
|
||||||
|
appA11yTitle="{{ 'options' | i18n }}"
|
||||||
|
></button>
|
||||||
|
|
||||||
|
<bit-menu #rowMenu>
|
||||||
|
<button type="button" bitMenuItem (click)="unmarkAsCriticalApp(r.applicationName)">
|
||||||
|
<i aria-hidden="true" class="bwi bwi-star-f"></i> {{ "unmarkAsCriticalApp" | i18n }}
|
||||||
|
</button>
|
||||||
|
</bit-menu>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</bit-table>
|
</bit-table>
|
||||||
|
|||||||
@@ -15,12 +15,15 @@ import {
|
|||||||
ApplicationHealthReportDetailWithCriticalFlag,
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import {
|
import {
|
||||||
DialogService,
|
DialogService,
|
||||||
Icons,
|
Icons,
|
||||||
NoItemsModule,
|
NoItemsModule,
|
||||||
SearchModule,
|
SearchModule,
|
||||||
TableDataSource,
|
TableDataSource,
|
||||||
|
ToastService,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { CardComponent } from "@bitwarden/tools-card";
|
import { CardComponent } from "@bitwarden/tools-card";
|
||||||
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
||||||
@@ -37,6 +40,7 @@ import { RiskInsightsTabType } from "./risk-insights.component";
|
|||||||
selector: "tools-critical-applications",
|
selector: "tools-critical-applications",
|
||||||
templateUrl: "./critical-applications.component.html",
|
templateUrl: "./critical-applications.component.html",
|
||||||
imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule],
|
imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule],
|
||||||
|
providers: [],
|
||||||
})
|
})
|
||||||
export class CriticalApplicationsComponent implements OnInit {
|
export class CriticalApplicationsComponent implements OnInit {
|
||||||
protected dataSource = new TableDataSource<ApplicationHealthReportDetailWithCriticalFlag>();
|
protected dataSource = new TableDataSource<ApplicationHealthReportDetailWithCriticalFlag>();
|
||||||
@@ -80,13 +84,38 @@ export class CriticalApplicationsComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
unmarkAsCriticalApp = async (hostname: string) => {
|
||||||
|
try {
|
||||||
|
await this.criticalAppsService.dropCriticalApp(
|
||||||
|
this.organizationId as OrganizationId,
|
||||||
|
hostname,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
this.toastService.showToast({
|
||||||
|
message: this.i18nService.t("unexpectedError"),
|
||||||
|
variant: "error",
|
||||||
|
title: this.i18nService.t("error"),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toastService.showToast({
|
||||||
|
message: this.i18nService.t("criticalApplicationSuccessfullyUnmarked"),
|
||||||
|
variant: "success",
|
||||||
|
title: this.i18nService.t("success"),
|
||||||
|
});
|
||||||
|
this.dataSource.data = this.dataSource.data.filter((app) => app.applicationName !== hostname);
|
||||||
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected activatedRoute: ActivatedRoute,
|
protected activatedRoute: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
protected toastService: ToastService,
|
||||||
protected dataService: RiskInsightsDataService,
|
protected dataService: RiskInsightsDataService,
|
||||||
protected criticalAppsService: CriticalAppsService,
|
protected criticalAppsService: CriticalAppsService,
|
||||||
protected reportService: RiskInsightsReportService,
|
protected reportService: RiskInsightsReportService,
|
||||||
protected dialogService: DialogService,
|
protected dialogService: DialogService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
) {
|
) {
|
||||||
this.searchControl.valueChanges
|
this.searchControl.valueChanges
|
||||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { Component, DestroyRef, OnInit, inject } from "@angular/core";
|
import { Component, DestroyRef, OnInit, inject } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { Observable, EMPTY } from "rxjs";
|
import { EMPTY, Observable } 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 {
|
import {
|
||||||
RiskInsightsDataService,
|
|
||||||
CriticalAppsService,
|
CriticalAppsService,
|
||||||
PasswordHealthReportApplicationsResponse,
|
RiskInsightsDataService,
|
||||||
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||||
import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
import {
|
||||||
|
ApplicationHealthReportDetail,
|
||||||
|
PasswordHealthReportApplicationsResponse,
|
||||||
|
} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health";
|
||||||
// eslint-disable-next-line no-restricted-imports -- used for dependency injection
|
// 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";
|
||||||
|
|||||||
Reference in New Issue
Block a user