From 37c1a5ee178a34d80ad502ee2bb1965e96106cdd Mon Sep 17 00:00:00 2001
From: Leslie Tilton <23057410+Banrion@users.noreply.github.com>
Date: Thu, 13 Nov 2025 12:18:52 -0600
Subject: [PATCH] [PM-27933] Skip assign tasks view if no critical applications
are selected (#17351)
* Fix reviews not saving in new applications review. Skip assign page if no at risk passwords are to be assigned. Fix bug in password change widget
* Claude comment improvements
---
apps/web/src/locales/en/messages.json | 6 +
.../password-change-metric.component.html | 2 +-
.../password-change-metric.component.ts | 17 ++-
.../new-applications-dialog.component.html | 4 +-
.../new-applications-dialog.component.ts | 133 ++++++++++++------
5 files changed, 110 insertions(+), 52 deletions(-)
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 27faf6f4063..23c430feedd 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -12161,5 +12161,11 @@
"example": "5"
}
}
+ },
+ "confirmNoSelectedCriticalApplicationsTitle": {
+ "message": "No critical applications are selected"
+ },
+ "confirmNoSelectedCriticalApplicationsDesc": {
+ "message": "Are you sure you want to continue?"
}
}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.html
index 4b765a5502e..ab59a36aa6a 100644
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.html
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.html
@@ -43,7 +43,7 @@
- {{ "newPasswordsAtRisk" | i18n: atRiskPasswordCount() }}
+ {{ "newPasswordsAtRisk" | i18n: unassignedCipherIds() }}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts
index 509b3e1314a..30e1db7b438 100644
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts
@@ -66,16 +66,13 @@ export class PasswordChangeMetricComponent implements OnInit {
readonly completedTasksCount = computed(
() => this._tasks().filter((task) => task.status === SecurityTaskStatus.Completed).length,
);
- readonly uncompletedTasksCount = computed(
- () => this._tasks().filter((task) => task.status == SecurityTaskStatus.Pending).length,
- );
readonly completedTasksPercent = computed(() => {
const total = this.tasksCount();
// Account for case where there are no tasks to avoid NaN
return total > 0 ? Math.round((this.completedTasksCount() / total) * 100) : 0;
});
- readonly atRiskPasswordCount = computed
(() => {
+ readonly unassignedCipherIds = computed(() => {
const atRiskIds = this._atRiskCipherIds();
const tasks = this._tasks();
@@ -83,12 +80,20 @@ export class PasswordChangeMetricComponent implements OnInit {
return atRiskIds.length;
}
- const assignedIdSet = new Set(tasks.map((task) => task.cipherId));
+ const inProgressTasks = tasks.filter((task) => task.status === SecurityTaskStatus.Pending);
+ const assignedIdSet = new Set(inProgressTasks.map((task) => task.cipherId));
const unassignedIds = atRiskIds.filter((id) => !assignedIdSet.has(id));
return unassignedIds.length;
});
+ readonly atRiskPasswordCount = computed(() => {
+ const atRiskIds = this._atRiskCipherIds();
+ const atRiskIdsSet = new Set(atRiskIds);
+
+ return atRiskIdsSet.size;
+ });
+
readonly currentView = computed(() => {
if (!this._hasCriticalApplications()) {
return PasswordChangeView.EMPTY;
@@ -96,7 +101,7 @@ export class PasswordChangeMetricComponent implements OnInit {
if (this.tasksCount() === 0) {
return PasswordChangeView.NO_TASKS_ASSIGNED;
}
- if (this.atRiskPasswordCount() > 0) {
+ if (this.unassignedCipherIds() > 0) {
return PasswordChangeView.NEW_TASKS_AVAILABLE;
}
return PasswordChangeView.PROGRESS;
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.html
index 8e597234f14..09fb5cb7ad9 100644
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.html
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.html
@@ -38,8 +38,8 @@
@if (currentView() === DialogView.AssignTasks) {
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts
index e415fbf9ad0..8655baccda3 100644
--- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts
+++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts
@@ -2,12 +2,15 @@ import { CommonModule } from "@angular/common";
import {
ChangeDetectionStrategy,
Component,
+ computed,
DestroyRef,
Inject,
inject,
+ Injector,
+ Signal,
signal,
} from "@angular/core";
-import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
import { from, switchMap, take } from "rxjs";
import {
@@ -17,7 +20,8 @@ import {
import { getUniqueMembers } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
-import { OrganizationId } from "@bitwarden/common/types/guid";
+import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
+import { SecurityTask, SecurityTaskStatus } from "@bitwarden/common/vault/tasks";
import {
ButtonModule,
DIALOG_DATA,
@@ -70,9 +74,9 @@ export type NewApplicationsDialogResultType =
(typeof NewApplicationsDialogResultType)[keyof typeof NewApplicationsDialogResultType];
@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
selector: "dirt-new-applications-dialog",
templateUrl: "./new-applications-dialog.component.html",
- changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
ButtonModule,
@@ -95,10 +99,41 @@ export class NewApplicationsDialogComponent {
// Applications selected to save as critical applications
protected readonly selectedApplications = signal>(new Set());
- // Assign tasks variables
- readonly atRiskCriticalApplicationsCount = signal(0);
- readonly totalCriticalApplicationsCount = signal(0);
- readonly atRiskCriticalMembersCount = signal(0);
+ // Used to determine if there are unassigned at-risk cipher IDs
+ private readonly _tasks!: Signal;
+
+ // Computed properties for selected applications
+ protected readonly newCriticalApplications = computed(() => {
+ return this.dialogParams.newApplications.filter((newApp) =>
+ this.selectedApplications().has(newApp.applicationName),
+ );
+ });
+
+ // New at risk critical applications
+ protected readonly newAtRiskCriticalApplications = computed(() => {
+ return this.newCriticalApplications().filter((app) => app.atRiskPasswordCount > 0);
+ });
+
+ // Count of unique members with at-risk passwords in newly marked critical applications
+ protected readonly atRiskCriticalMembersCount = computed(() => {
+ return getUniqueMembers(this.newCriticalApplications().flatMap((x) => x.atRiskMemberDetails))
+ .length;
+ });
+
+ protected readonly newUnassignedAtRiskCipherIds = computed(() => {
+ const newAtRiskCipherIds = this.newCriticalApplications().flatMap((app) => app.atRiskCipherIds);
+ const tasks = this._tasks();
+
+ if (tasks.length === 0) {
+ return newAtRiskCipherIds;
+ }
+
+ const inProgressTasks = tasks.filter((task) => task.status === SecurityTaskStatus.Pending);
+ const assignedIdSet = new Set(inProgressTasks.map((task) => task.cipherId));
+ const unassignedIds = newAtRiskCipherIds.filter((id) => !assignedIdSet.has(id));
+ return unassignedIds;
+ });
+
readonly saving = signal(false);
// Loading states
@@ -106,13 +141,21 @@ export class NewApplicationsDialogComponent {
constructor(
@Inject(DIALOG_DATA) protected dialogParams: NewApplicationsDialogData,
- private dialogRef: DialogRef,
private dataService: RiskInsightsDataService,
- private toastService: ToastService,
+ private dialogRef: DialogRef,
+ private dialogService: DialogService,
private i18nService: I18nService,
- private accessIntelligenceSecurityTasksService: AccessIntelligenceSecurityTasksService,
+ private injector: Injector,
private logService: LogService,
- ) {}
+ private securityTasksService: AccessIntelligenceSecurityTasksService,
+ private toastService: ToastService,
+ ) {
+ // Setup the _tasks signal by manually passing in the injector
+ this._tasks = toSignal(this.securityTasksService.tasks$, {
+ initialValue: [],
+ injector: this.injector,
+ });
+ }
/**
* Opens the new applications dialog
@@ -170,53 +213,57 @@ export class NewApplicationsDialogComponent {
});
}
- handleMarkAsCritical() {
- if (this.markingAsCritical() || this.saving()) {
- return; // Prevent action if already processing
+ // Checks if there are selected applications and proceeds to assign tasks
+ async handleMarkAsCritical() {
+ if (this.selectedApplications().size === 0) {
+ const confirmed = await this.dialogService.openSimpleDialog({
+ title: { key: "confirmNoSelectedCriticalApplicationsTitle" },
+ content: { key: "confirmNoSelectedCriticalApplicationsDesc" },
+ type: "warning",
+ });
+
+ if (!confirmed) {
+ return;
+ }
}
- this.markingAsCritical.set(true);
- const onlyNewCriticalApplications = this.dialogParams.newApplications.filter((newApp) =>
- this.selectedApplications().has(newApp.applicationName),
- );
-
- // Count only critical applications that have at-risk passwords
- const atRiskCriticalApplicationsCount = onlyNewCriticalApplications.filter(
- (app) => app.atRiskPasswordCount > 0,
- ).length;
- this.atRiskCriticalApplicationsCount.set(atRiskCriticalApplicationsCount);
-
- // Total number of selected critical applications
- this.totalCriticalApplicationsCount.set(onlyNewCriticalApplications.length);
-
- const atRiskCriticalMembersCount = getUniqueMembers(
- onlyNewCriticalApplications.flatMap((x) => x.atRiskMemberDetails),
- ).length;
- this.atRiskCriticalMembersCount.set(atRiskCriticalMembersCount);
-
- this.currentView.set(DialogView.AssignTasks);
- this.markingAsCritical.set(false);
+ // Skip the assign tasks view if there are no new unassigned at-risk cipher IDs
+ if (this.newUnassignedAtRiskCipherIds().length === 0) {
+ this.handleAssignTasks();
+ } else {
+ this.currentView.set(DialogView.AssignTasks);
+ }
}
- /**
- * Handles the assign tasks button click
- */
+ // Saves the application review and assigns tasks for unassigned at-risk ciphers
protected handleAssignTasks() {
if (this.saving()) {
return; // Prevent double-click
}
this.saving.set(true);
+ const reviewedDate = new Date();
+ const updatedApplications = this.dialogParams.newApplications.map((app) => {
+ const isCritical = this.selectedApplications().has(app.applicationName);
+ return {
+ applicationName: app.applicationName,
+ isCritical,
+ reviewedDate,
+ };
+ });
+
// Save the application review dates and critical markings
- this.dataService.criticalApplicationAtRiskCipherIds$
+ this.dataService
+ .saveApplicationReviewStatus(updatedApplications)
.pipe(
takeUntilDestroyed(this.destroyRef), // Satisfy eslint rule
- take(1), // Handle unsubscribe for one off operation
- switchMap((criticalApplicationAtRiskCipherIds) => {
+ take(1),
+ switchMap(() => {
+ // Assign password change tasks for unassigned at-risk ciphers for critical applications
return from(
- this.accessIntelligenceSecurityTasksService.requestPasswordChangeForCriticalApplications(
+ this.securityTasksService.requestPasswordChangeForCriticalApplications(
this.dialogParams.organizationId,
- criticalApplicationAtRiskCipherIds,
+ this.newUnassignedAtRiskCipherIds(),
),
);
}),