Skip to content

Commit add3e03

Browse files
Banrionmaxkpower
authored andcommitted
[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
1 parent 720adbb commit add3e03

File tree

5 files changed

+110
-52
lines changed

5 files changed

+110
-52
lines changed

apps/web/src/locales/en/messages.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12161,5 +12161,11 @@
1216112161
"example": "5"
1216212162
}
1216312163
}
12164+
},
12165+
"confirmNoSelectedCriticalApplicationsTitle": {
12166+
"message": "No critical applications are selected"
12167+
},
12168+
"confirmNoSelectedCriticalApplicationsDesc": {
12169+
"message": "Are you sure you want to continue?"
1216412170
}
1216512171
}

bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
</div>
4444

4545
<div class="tw-items-baseline tw-gap-2">
46-
<span bitTypography="body2">{{ "newPasswordsAtRisk" | i18n: atRiskPasswordCount() }}</span>
46+
<span bitTypography="body2">{{ "newPasswordsAtRisk" | i18n: unassignedCipherIds() }}</span>
4747
</div>
4848

4949
<div class="tw-mt-4">

bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/activity-cards/password-change-metric.component.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,37 +66,42 @@ export class PasswordChangeMetricComponent implements OnInit {
6666
readonly completedTasksCount = computed(
6767
() => this._tasks().filter((task) => task.status === SecurityTaskStatus.Completed).length,
6868
);
69-
readonly uncompletedTasksCount = computed(
70-
() => this._tasks().filter((task) => task.status == SecurityTaskStatus.Pending).length,
71-
);
7269
readonly completedTasksPercent = computed(() => {
7370
const total = this.tasksCount();
7471
// Account for case where there are no tasks to avoid NaN
7572
return total > 0 ? Math.round((this.completedTasksCount() / total) * 100) : 0;
7673
});
7774

78-
readonly atRiskPasswordCount = computed<number>(() => {
75+
readonly unassignedCipherIds = computed<number>(() => {
7976
const atRiskIds = this._atRiskCipherIds();
8077
const tasks = this._tasks();
8178

8279
if (tasks.length === 0) {
8380
return atRiskIds.length;
8481
}
8582

86-
const assignedIdSet = new Set(tasks.map((task) => task.cipherId));
83+
const inProgressTasks = tasks.filter((task) => task.status === SecurityTaskStatus.Pending);
84+
const assignedIdSet = new Set(inProgressTasks.map((task) => task.cipherId));
8785
const unassignedIds = atRiskIds.filter((id) => !assignedIdSet.has(id));
8886

8987
return unassignedIds.length;
9088
});
9189

90+
readonly atRiskPasswordCount = computed<number>(() => {
91+
const atRiskIds = this._atRiskCipherIds();
92+
const atRiskIdsSet = new Set(atRiskIds);
93+
94+
return atRiskIdsSet.size;
95+
});
96+
9297
readonly currentView = computed<PasswordChangeView>(() => {
9398
if (!this._hasCriticalApplications()) {
9499
return PasswordChangeView.EMPTY;
95100
}
96101
if (this.tasksCount() === 0) {
97102
return PasswordChangeView.NO_TASKS_ASSIGNED;
98103
}
99-
if (this.atRiskPasswordCount() > 0) {
104+
if (this.unassignedCipherIds() > 0) {
100105
return PasswordChangeView.NEW_TASKS_AVAILABLE;
101106
}
102107
return PasswordChangeView.PROGRESS;

bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838

3939
@if (currentView() === DialogView.AssignTasks) {
4040
<dirt-assign-tasks-view
41-
[criticalApplicationsCount]="atRiskCriticalApplicationsCount()"
42-
[totalApplicationsCount]="totalCriticalApplicationsCount()"
41+
[criticalApplicationsCount]="newAtRiskCriticalApplications().length"
42+
[totalApplicationsCount]="newCriticalApplications().length"
4343
[atRiskCriticalMembersCount]="atRiskCriticalMembersCount()"
4444
>
4545
</dirt-assign-tasks-view>

bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts

Lines changed: 90 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { CommonModule } from "@angular/common";
22
import {
33
ChangeDetectionStrategy,
44
Component,
5+
computed,
56
DestroyRef,
67
Inject,
78
inject,
9+
Injector,
10+
Signal,
811
signal,
912
} from "@angular/core";
10-
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
13+
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
1114
import { from, switchMap, take } from "rxjs";
1215

1316
import {
@@ -17,7 +20,8 @@ import {
1720
import { getUniqueMembers } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
1821
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
1922
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
20-
import { OrganizationId } from "@bitwarden/common/types/guid";
23+
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
24+
import { SecurityTask, SecurityTaskStatus } from "@bitwarden/common/vault/tasks";
2125
import {
2226
ButtonModule,
2327
DIALOG_DATA,
@@ -70,9 +74,9 @@ export type NewApplicationsDialogResultType =
7074
(typeof NewApplicationsDialogResultType)[keyof typeof NewApplicationsDialogResultType];
7175

7276
@Component({
77+
changeDetection: ChangeDetectionStrategy.OnPush,
7378
selector: "dirt-new-applications-dialog",
7479
templateUrl: "./new-applications-dialog.component.html",
75-
changeDetection: ChangeDetectionStrategy.OnPush,
7680
imports: [
7781
CommonModule,
7882
ButtonModule,
@@ -95,24 +99,63 @@ export class NewApplicationsDialogComponent {
9599
// Applications selected to save as critical applications
96100
protected readonly selectedApplications = signal<Set<string>>(new Set());
97101

98-
// Assign tasks variables
99-
readonly atRiskCriticalApplicationsCount = signal<number>(0);
100-
readonly totalCriticalApplicationsCount = signal<number>(0);
101-
readonly atRiskCriticalMembersCount = signal<number>(0);
102+
// Used to determine if there are unassigned at-risk cipher IDs
103+
private readonly _tasks!: Signal<SecurityTask[]>;
104+
105+
// Computed properties for selected applications
106+
protected readonly newCriticalApplications = computed(() => {
107+
return this.dialogParams.newApplications.filter((newApp) =>
108+
this.selectedApplications().has(newApp.applicationName),
109+
);
110+
});
111+
112+
// New at risk critical applications
113+
protected readonly newAtRiskCriticalApplications = computed(() => {
114+
return this.newCriticalApplications().filter((app) => app.atRiskPasswordCount > 0);
115+
});
116+
117+
// Count of unique members with at-risk passwords in newly marked critical applications
118+
protected readonly atRiskCriticalMembersCount = computed(() => {
119+
return getUniqueMembers(this.newCriticalApplications().flatMap((x) => x.atRiskMemberDetails))
120+
.length;
121+
});
122+
123+
protected readonly newUnassignedAtRiskCipherIds = computed<CipherId[]>(() => {
124+
const newAtRiskCipherIds = this.newCriticalApplications().flatMap((app) => app.atRiskCipherIds);
125+
const tasks = this._tasks();
126+
127+
if (tasks.length === 0) {
128+
return newAtRiskCipherIds;
129+
}
130+
131+
const inProgressTasks = tasks.filter((task) => task.status === SecurityTaskStatus.Pending);
132+
const assignedIdSet = new Set(inProgressTasks.map((task) => task.cipherId));
133+
const unassignedIds = newAtRiskCipherIds.filter((id) => !assignedIdSet.has(id));
134+
return unassignedIds;
135+
});
136+
102137
readonly saving = signal<boolean>(false);
103138

104139
// Loading states
105140
protected readonly markingAsCritical = signal<boolean>(false);
106141

107142
constructor(
108143
@Inject(DIALOG_DATA) protected dialogParams: NewApplicationsDialogData,
109-
private dialogRef: DialogRef<NewApplicationsDialogResultType>,
110144
private dataService: RiskInsightsDataService,
111-
private toastService: ToastService,
145+
private dialogRef: DialogRef<NewApplicationsDialogResultType>,
146+
private dialogService: DialogService,
112147
private i18nService: I18nService,
113-
private accessIntelligenceSecurityTasksService: AccessIntelligenceSecurityTasksService,
148+
private injector: Injector,
114149
private logService: LogService,
115-
) {}
150+
private securityTasksService: AccessIntelligenceSecurityTasksService,
151+
private toastService: ToastService,
152+
) {
153+
// Setup the _tasks signal by manually passing in the injector
154+
this._tasks = toSignal(this.securityTasksService.tasks$, {
155+
initialValue: [],
156+
injector: this.injector,
157+
});
158+
}
116159

117160
/**
118161
* Opens the new applications dialog
@@ -170,53 +213,57 @@ export class NewApplicationsDialogComponent {
170213
});
171214
}
172215

173-
handleMarkAsCritical() {
174-
if (this.markingAsCritical() || this.saving()) {
175-
return; // Prevent action if already processing
176-
}
177-
this.markingAsCritical.set(true);
178-
179-
const onlyNewCriticalApplications = this.dialogParams.newApplications.filter((newApp) =>
180-
this.selectedApplications().has(newApp.applicationName),
181-
);
182-
183-
// Count only critical applications that have at-risk passwords
184-
const atRiskCriticalApplicationsCount = onlyNewCriticalApplications.filter(
185-
(app) => app.atRiskPasswordCount > 0,
186-
).length;
187-
this.atRiskCriticalApplicationsCount.set(atRiskCriticalApplicationsCount);
188-
189-
// Total number of selected critical applications
190-
this.totalCriticalApplicationsCount.set(onlyNewCriticalApplications.length);
216+
// Checks if there are selected applications and proceeds to assign tasks
217+
async handleMarkAsCritical() {
218+
if (this.selectedApplications().size === 0) {
219+
const confirmed = await this.dialogService.openSimpleDialog({
220+
title: { key: "confirmNoSelectedCriticalApplicationsTitle" },
221+
content: { key: "confirmNoSelectedCriticalApplicationsDesc" },
222+
type: "warning",
223+
});
191224

192-
const atRiskCriticalMembersCount = getUniqueMembers(
193-
onlyNewCriticalApplications.flatMap((x) => x.atRiskMemberDetails),
194-
).length;
195-
this.atRiskCriticalMembersCount.set(atRiskCriticalMembersCount);
225+
if (!confirmed) {
226+
return;
227+
}
228+
}
196229

197-
this.currentView.set(DialogView.AssignTasks);
198-
this.markingAsCritical.set(false);
230+
// Skip the assign tasks view if there are no new unassigned at-risk cipher IDs
231+
if (this.newUnassignedAtRiskCipherIds().length === 0) {
232+
this.handleAssignTasks();
233+
} else {
234+
this.currentView.set(DialogView.AssignTasks);
235+
}
199236
}
200237

201-
/**
202-
* Handles the assign tasks button click
203-
*/
238+
// Saves the application review and assigns tasks for unassigned at-risk ciphers
204239
protected handleAssignTasks() {
205240
if (this.saving()) {
206241
return; // Prevent double-click
207242
}
208243
this.saving.set(true);
209244

245+
const reviewedDate = new Date();
246+
const updatedApplications = this.dialogParams.newApplications.map((app) => {
247+
const isCritical = this.selectedApplications().has(app.applicationName);
248+
return {
249+
applicationName: app.applicationName,
250+
isCritical,
251+
reviewedDate,
252+
};
253+
});
254+
210255
// Save the application review dates and critical markings
211-
this.dataService.criticalApplicationAtRiskCipherIds$
256+
this.dataService
257+
.saveApplicationReviewStatus(updatedApplications)
212258
.pipe(
213259
takeUntilDestroyed(this.destroyRef), // Satisfy eslint rule
214-
take(1), // Handle unsubscribe for one off operation
215-
switchMap((criticalApplicationAtRiskCipherIds) => {
260+
take(1),
261+
switchMap(() => {
262+
// Assign password change tasks for unassigned at-risk ciphers for critical applications
216263
return from(
217-
this.accessIntelligenceSecurityTasksService.requestPasswordChangeForCriticalApplications(
264+
this.securityTasksService.requestPasswordChangeForCriticalApplications(
218265
this.dialogParams.organizationId,
219-
criticalApplicationAtRiskCipherIds,
266+
this.newUnassignedAtRiskCipherIds(),
220267
),
221268
);
222269
}),

0 commit comments

Comments
 (0)