Skip to content

Commit aba5f37

Browse files
committed
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
1 parent 089caf5 commit aba5f37

File tree

5 files changed

+120
-57
lines changed

5 files changed

+120
-57
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12152,5 +12152,11 @@
1215212152
"example": "5"
1215312153
}
1215412154
}
12155+
},
12156+
"confirmNoSelectedCriticalApplicationsTitle" : {
12157+
"message": "No critical applications are selected"
12158+
},
12159+
"confirmNoSelectedCriticalApplicationsDesc" : {
12160+
"message": "Are you sure you want to continue?"
1215512161
}
1215612162
}

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: 100 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ 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";
11-
import { from, switchMap, take } from "rxjs";
13+
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
14+
import { from, of, switchMap, take } from "rxjs";
1215

1316
import {
1417
ApplicationHealthReportDetail,
@@ -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,54 +213,63 @@ 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(
213-
takeUntilDestroyed(this.destroyRef), // Satisfy eslint rule
214-
take(1), // Handle unsubscribe for one off operation
215-
switchMap((criticalApplicationAtRiskCipherIds) => {
216-
return from(
217-
this.accessIntelligenceSecurityTasksService.requestPasswordChangeForCriticalApplications(
218-
this.dialogParams.organizationId,
219-
criticalApplicationAtRiskCipherIds,
220-
),
259+
takeUntilDestroyed(this.destroyRef), // Satisfy eslint rule,
260+
take(1),
261+
switchMap(() => {
262+
// Assign password change tasks for unassigned at-risk ciphers for critical applications
263+
return of(this.newUnassignedAtRiskCipherIds()).pipe(
264+
takeUntilDestroyed(this.destroyRef), // Satisfy eslint rule
265+
switchMap((criticalApplicationAtRiskCipherIds) => {
266+
return from(
267+
this.securityTasksService.requestPasswordChangeForCriticalApplications(
268+
this.dialogParams.organizationId,
269+
criticalApplicationAtRiskCipherIds,
270+
),
271+
);
272+
}),
221273
);
222274
}),
223275
)

0 commit comments

Comments
 (0)