mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
PM-26508 when password change is requested, the all-activity table count will change (#16753)
This commit is contained in:
@@ -35,6 +35,9 @@ export class AllActivitiesService {
|
||||
passwordChangeProgressMetricHasProgressBar$ =
|
||||
this.passwordChangeProgressMetricHasProgressBarSubject$.asObservable();
|
||||
|
||||
private taskCreatedCountSubject$ = new BehaviorSubject<number>(0);
|
||||
taskCreatedCount$ = this.taskCreatedCountSubject$.asObservable();
|
||||
|
||||
constructor(private dataService: RiskInsightsDataService) {
|
||||
// All application summary changes
|
||||
this.dataService.reportResults$.subscribe((report) => {
|
||||
@@ -85,4 +88,8 @@ export class AllActivitiesService {
|
||||
setPasswordChangeProgressMetricHasProgressBar(hasProgressBar: boolean) {
|
||||
this.passwordChangeProgressMetricHasProgressBarSubject$.next(hasProgressBar);
|
||||
}
|
||||
|
||||
setTaskCreatedCount(count: number) {
|
||||
this.taskCreatedCountSubject$.next(count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,17 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { DefaultAdminTaskService } from "../../vault/services/default-admin-task.service";
|
||||
|
||||
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
|
||||
import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
import { AccessIntelligenceSecurityTasksService } from "./shared/security-tasks.service";
|
||||
|
||||
@NgModule({
|
||||
imports: [RiskInsightsComponent, AccessIntelligenceRoutingModule],
|
||||
@@ -89,6 +94,11 @@ import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
useClass: SecurityTasksApiService,
|
||||
deps: [ApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AccessIntelligenceSecurityTasksService,
|
||||
useClass: AccessIntelligenceSecurityTasksService,
|
||||
deps: [AllActivitiesService, DefaultAdminTaskService, ToastService, I18nService],
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class AccessIntelligenceModule {}
|
||||
|
||||
@@ -10,18 +10,11 @@ import {
|
||||
SecurityTasksApiService,
|
||||
TaskMetrics,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTaskType } from "@bitwarden/common/vault/tasks";
|
||||
import {
|
||||
ButtonModule,
|
||||
ProgressModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { ButtonModule, ProgressModule, TypographyModule } from "@bitwarden/components";
|
||||
|
||||
import { CreateTasksRequest } from "../../../vault/services/abstractions/admin-task.abstraction";
|
||||
import { DefaultAdminTaskService } from "../../../vault/services/default-admin-task.service";
|
||||
import { AccessIntelligenceSecurityTasksService } from "../shared/security-tasks.service";
|
||||
|
||||
export const RenderMode = {
|
||||
noCriticalApps: "noCriticalApps",
|
||||
@@ -34,7 +27,7 @@ export type RenderMode = (typeof RenderMode)[keyof typeof RenderMode];
|
||||
selector: "dirt-password-change-metric",
|
||||
imports: [CommonModule, TypographyModule, JslibModule, ProgressModule, ButtonModule],
|
||||
templateUrl: "./password-change-metric.component.html",
|
||||
providers: [DefaultAdminTaskService],
|
||||
providers: [AccessIntelligenceSecurityTasksService, DefaultAdminTaskService],
|
||||
})
|
||||
export class PasswordChangeMetricComponent implements OnInit {
|
||||
protected taskMetrics$ = new BehaviorSubject<TaskMetrics>({ totalTasks: 0, completedTasks: 0 });
|
||||
@@ -50,10 +43,10 @@ export class PasswordChangeMetricComponent implements OnInit {
|
||||
renderMode: RenderMode = "noCriticalApps";
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.activatedRoute.paramMap
|
||||
combineLatest([this.activatedRoute.paramMap, this.allActivitiesService.taskCreatedCount$])
|
||||
.pipe(
|
||||
switchMap((paramMap) => {
|
||||
const orgId = paramMap.get("organizationId");
|
||||
switchMap(([params, _]) => {
|
||||
const orgId = params.get("organizationId");
|
||||
if (orgId) {
|
||||
this.organizationId = orgId as OrganizationId;
|
||||
return this.securityTasksApiService.getTaskMetrics(this.organizationId);
|
||||
@@ -110,9 +103,7 @@ export class PasswordChangeMetricComponent implements OnInit {
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private securityTasksApiService: SecurityTasksApiService,
|
||||
private allActivitiesService: AllActivitiesService,
|
||||
private adminTaskService: DefaultAdminTaskService,
|
||||
protected toastService: ToastService,
|
||||
protected i18nService: I18nService,
|
||||
protected accessIntelligenceSecurityTasksService: AccessIntelligenceSecurityTasksService,
|
||||
) {}
|
||||
|
||||
get completedPercent(): number {
|
||||
@@ -161,44 +152,9 @@ export class PasswordChangeMetricComponent implements OnInit {
|
||||
}
|
||||
|
||||
async assignTasks() {
|
||||
const taskCount = await this.requestPasswordChange();
|
||||
this.taskMetrics$.next({
|
||||
totalTasks: this.totalTasks + taskCount,
|
||||
completedTasks: this.completedTasks,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: this method is shared between here and critical-applications.component.ts
|
||||
async requestPasswordChange() {
|
||||
const apps = this.allApplicationsDetails;
|
||||
const cipherIds = apps
|
||||
.filter((_) => _.atRiskPasswordCount > 0)
|
||||
.flatMap((app) => app.atRiskCipherIds);
|
||||
|
||||
const distinctCipherIds = Array.from(new Set(cipherIds));
|
||||
|
||||
const tasks: CreateTasksRequest[] = distinctCipherIds.map((cipherId) => ({
|
||||
cipherId: cipherId as CipherId,
|
||||
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||
}));
|
||||
|
||||
try {
|
||||
await this.adminTaskService.bulkCreateTasks(this.organizationId as OrganizationId, tasks);
|
||||
this.toastService.showToast({
|
||||
message: this.i18nService.t("notifiedMembers"),
|
||||
variant: "success",
|
||||
title: this.i18nService.t("success"),
|
||||
});
|
||||
|
||||
return tasks.length;
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
variant: "error",
|
||||
title: this.i18nService.t("error"),
|
||||
});
|
||||
}
|
||||
|
||||
return 0;
|
||||
await this.accessIntelligenceSecurityTasksService.assignTasks(
|
||||
this.organizationId,
|
||||
this.allApplicationsDetails,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,24 +9,25 @@ import { debounceTime, EMPTY, map, switchMap } from "rxjs";
|
||||
import { Security } from "@bitwarden/assets/svg";
|
||||
import {
|
||||
ApplicationHealthReportDetailEnriched,
|
||||
CriticalAppsService,
|
||||
RiskInsightsDataService,
|
||||
RiskInsightsReportService,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { createNewSummaryData } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
|
||||
import { OrganizationReportSummary } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTaskType } from "@bitwarden/common/vault/tasks";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { NoItemsModule, SearchModule, TableDataSource, ToastService } from "@bitwarden/components";
|
||||
import { CardComponent } from "@bitwarden/dirt-card";
|
||||
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { CreateTasksRequest } from "../../vault/services/abstractions/admin-task.abstraction";
|
||||
import { DefaultAdminTaskService } from "../../vault/services/default-admin-task.service";
|
||||
|
||||
import { AppTableRowScrollableComponent } from "./app-table-row-scrollable.component";
|
||||
import { RiskInsightsTabType } from "./risk-insights.component";
|
||||
import { AccessIntelligenceSecurityTasksService } from "./shared/security-tasks.service";
|
||||
|
||||
@Component({
|
||||
selector: "dirt-critical-applications",
|
||||
@@ -40,7 +41,7 @@ import { RiskInsightsTabType } from "./risk-insights.component";
|
||||
SharedModule,
|
||||
AppTableRowScrollableComponent,
|
||||
],
|
||||
providers: [DefaultAdminTaskService],
|
||||
providers: [AccessIntelligenceSecurityTasksService, DefaultAdminTaskService],
|
||||
})
|
||||
export class CriticalApplicationsComponent implements OnInit {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
@@ -60,8 +61,10 @@ export class CriticalApplicationsComponent implements OnInit {
|
||||
protected router: Router,
|
||||
protected toastService: ToastService,
|
||||
protected dataService: RiskInsightsDataService,
|
||||
protected criticalAppsService: CriticalAppsService,
|
||||
protected reportService: RiskInsightsReportService,
|
||||
protected i18nService: I18nService,
|
||||
private adminTaskService: DefaultAdminTaskService,
|
||||
private accessIntelligenceSecurityTasksService: AccessIntelligenceSecurityTasksService,
|
||||
) {
|
||||
this.searchControl.valueChanges
|
||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||
@@ -128,33 +131,12 @@ export class CriticalApplicationsComponent implements OnInit {
|
||||
};
|
||||
|
||||
async requestPasswordChange() {
|
||||
const apps = this.dataSource.data;
|
||||
const cipherIds = apps
|
||||
.filter((_) => _.atRiskPasswordCount > 0)
|
||||
.flatMap((app) => app.atRiskCipherIds);
|
||||
|
||||
const distinctCipherIds = Array.from(new Set(cipherIds));
|
||||
|
||||
const tasks: CreateTasksRequest[] = distinctCipherIds.map((cipherId) => ({
|
||||
cipherId: cipherId as CipherId,
|
||||
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||
}));
|
||||
|
||||
try {
|
||||
await this.adminTaskService.bulkCreateTasks(this.organizationId as OrganizationId, tasks);
|
||||
this.toastService.showToast({
|
||||
message: this.i18nService.t("notifiedMembers"),
|
||||
variant: "success",
|
||||
title: this.i18nService.t("success"),
|
||||
});
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
variant: "error",
|
||||
title: this.i18nService.t("error"),
|
||||
});
|
||||
}
|
||||
await this.accessIntelligenceSecurityTasksService.assignTasks(
|
||||
this.organizationId,
|
||||
this.dataSource.data,
|
||||
);
|
||||
}
|
||||
|
||||
showAppAtRiskMembers = async (applicationName: string) => {
|
||||
await this.dataService.setDrawerForAppAtRiskMembers(applicationName);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import {
|
||||
AllActivitiesService,
|
||||
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTaskType } from "@bitwarden/common/vault/tasks";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { DefaultAdminTaskService } from "../../../vault/services/default-admin-task.service";
|
||||
|
||||
import { AccessIntelligenceSecurityTasksService } from "./security-tasks.service";
|
||||
|
||||
describe("AccessIntelligenceSecurityTasksService", () => {
|
||||
let service: AccessIntelligenceSecurityTasksService;
|
||||
const defaultAdminTaskServiceSpy = mock<DefaultAdminTaskService>();
|
||||
const allActivitiesServiceSpy = mock<AllActivitiesService>();
|
||||
const toastServiceSpy = mock<ToastService>();
|
||||
const i18nServiceSpy = mock<I18nService>();
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = new AccessIntelligenceSecurityTasksService(
|
||||
allActivitiesServiceSpy,
|
||||
defaultAdminTaskServiceSpy,
|
||||
toastServiceSpy,
|
||||
i18nServiceSpy,
|
||||
);
|
||||
});
|
||||
|
||||
it("should be created", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("assignTasks", () => {
|
||||
it("should call requestPasswordChange and setTaskCreatedCount", async () => {
|
||||
const organizationId = "org-1" as OrganizationId;
|
||||
const apps = [
|
||||
{
|
||||
atRiskPasswordCount: 1,
|
||||
atRiskCipherIds: ["cid1"],
|
||||
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||
];
|
||||
const spy = jest.spyOn(service, "requestPasswordChange").mockResolvedValue(2);
|
||||
await service.assignTasks(organizationId, apps);
|
||||
expect(spy).toHaveBeenCalledWith(organizationId, apps);
|
||||
expect(allActivitiesServiceSpy.setTaskCreatedCount).toHaveBeenCalledWith(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestPasswordChange", () => {
|
||||
it("should create tasks for distinct cipher ids and show success toast", async () => {
|
||||
const organizationId = "org-2" as OrganizationId;
|
||||
const apps = [
|
||||
{
|
||||
atRiskPasswordCount: 2,
|
||||
atRiskCipherIds: ["cid1", "cid2"],
|
||||
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||
{
|
||||
atRiskPasswordCount: 1,
|
||||
atRiskCipherIds: ["cid2"],
|
||||
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||
];
|
||||
defaultAdminTaskServiceSpy.bulkCreateTasks.mockResolvedValue(undefined);
|
||||
i18nServiceSpy.t.mockImplementation((key) => key);
|
||||
|
||||
const result = await service.requestPasswordChange(organizationId, apps);
|
||||
|
||||
expect(defaultAdminTaskServiceSpy.bulkCreateTasks).toHaveBeenCalledWith(organizationId, [
|
||||
{ cipherId: "cid1", type: SecurityTaskType.UpdateAtRiskCredential },
|
||||
{ cipherId: "cid2", type: SecurityTaskType.UpdateAtRiskCredential },
|
||||
]);
|
||||
expect(toastServiceSpy.showToast).toHaveBeenCalledWith({
|
||||
message: "notifiedMembers",
|
||||
variant: "success",
|
||||
title: "success",
|
||||
});
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
|
||||
it("should show error toast and return 0 if bulkCreateTasks throws", async () => {
|
||||
const organizationId = "org-3" as OrganizationId;
|
||||
const apps = [
|
||||
{
|
||||
atRiskPasswordCount: 1,
|
||||
atRiskCipherIds: ["cid3"],
|
||||
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||
];
|
||||
defaultAdminTaskServiceSpy.bulkCreateTasks.mockRejectedValue(new Error("fail"));
|
||||
i18nServiceSpy.t.mockImplementation((key) => key);
|
||||
|
||||
const result = await service.requestPasswordChange(organizationId, apps);
|
||||
|
||||
expect(toastServiceSpy.showToast).toHaveBeenCalledWith({
|
||||
message: "unexpectedError",
|
||||
variant: "error",
|
||||
title: "error",
|
||||
});
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it("should not create any tasks if no apps have atRiskPasswordCount > 0", async () => {
|
||||
const organizationId = "org-4" as OrganizationId;
|
||||
const apps = [
|
||||
{
|
||||
atRiskPasswordCount: 0,
|
||||
atRiskCipherIds: ["cid4"],
|
||||
} as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||
];
|
||||
const result = await service.requestPasswordChange(organizationId, apps);
|
||||
|
||||
expect(defaultAdminTaskServiceSpy.bulkCreateTasks).toHaveBeenCalledWith(organizationId, []);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import {
|
||||
AllActivitiesService,
|
||||
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
|
||||
} from "@bitwarden/bit-common/dirt/reports/risk-insights";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SecurityTaskType } from "@bitwarden/common/vault/tasks";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { CreateTasksRequest } from "../../../vault/services/abstractions/admin-task.abstraction";
|
||||
import { DefaultAdminTaskService } from "../../../vault/services/default-admin-task.service";
|
||||
|
||||
@Injectable()
|
||||
export class AccessIntelligenceSecurityTasksService {
|
||||
constructor(
|
||||
private allActivitiesService: AllActivitiesService,
|
||||
private adminTaskService: DefaultAdminTaskService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
async assignTasks(
|
||||
organizationId: OrganizationId,
|
||||
apps: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[],
|
||||
) {
|
||||
const taskCount = await this.requestPasswordChange(organizationId, apps);
|
||||
this.allActivitiesService.setTaskCreatedCount(taskCount);
|
||||
}
|
||||
|
||||
// TODO: this method is shared between here and critical-applications.component.ts
|
||||
async requestPasswordChange(
|
||||
organizationId: OrganizationId,
|
||||
apps: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[],
|
||||
): Promise<number> {
|
||||
const cipherIds = apps
|
||||
.filter((_) => _.atRiskPasswordCount > 0)
|
||||
.flatMap((app) => app.atRiskCipherIds);
|
||||
|
||||
const distinctCipherIds = Array.from(new Set(cipherIds));
|
||||
|
||||
const tasks: CreateTasksRequest[] = distinctCipherIds.map((cipherId) => ({
|
||||
cipherId: cipherId as CipherId,
|
||||
type: SecurityTaskType.UpdateAtRiskCredential,
|
||||
}));
|
||||
|
||||
try {
|
||||
await this.adminTaskService.bulkCreateTasks(organizationId, tasks);
|
||||
this.toastService.showToast({
|
||||
message: this.i18nService.t("notifiedMembers"),
|
||||
variant: "success",
|
||||
title: this.i18nService.t("success"),
|
||||
});
|
||||
|
||||
return tasks.length;
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
variant: "error",
|
||||
title: this.i18nService.t("error"),
|
||||
});
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user