mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[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
This commit is contained in:
@@ -12161,5 +12161,11 @@
|
|||||||
"example": "5"
|
"example": "5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"confirmNoSelectedCriticalApplicationsTitle": {
|
||||||
|
"message": "No critical applications are selected"
|
||||||
|
},
|
||||||
|
"confirmNoSelectedCriticalApplicationsDesc": {
|
||||||
|
"message": "Are you sure you want to continue?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tw-items-baseline tw-gap-2">
|
<div class="tw-items-baseline tw-gap-2">
|
||||||
<span bitTypography="body2">{{ "newPasswordsAtRisk" | i18n: atRiskPasswordCount() }}</span>
|
<span bitTypography="body2">{{ "newPasswordsAtRisk" | i18n: unassignedCipherIds() }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tw-mt-4">
|
<div class="tw-mt-4">
|
||||||
|
|||||||
@@ -66,16 +66,13 @@ export class PasswordChangeMetricComponent implements OnInit {
|
|||||||
readonly completedTasksCount = computed(
|
readonly completedTasksCount = computed(
|
||||||
() => this._tasks().filter((task) => task.status === SecurityTaskStatus.Completed).length,
|
() => this._tasks().filter((task) => task.status === SecurityTaskStatus.Completed).length,
|
||||||
);
|
);
|
||||||
readonly uncompletedTasksCount = computed(
|
|
||||||
() => this._tasks().filter((task) => task.status == SecurityTaskStatus.Pending).length,
|
|
||||||
);
|
|
||||||
readonly completedTasksPercent = computed(() => {
|
readonly completedTasksPercent = computed(() => {
|
||||||
const total = this.tasksCount();
|
const total = this.tasksCount();
|
||||||
// Account for case where there are no tasks to avoid NaN
|
// Account for case where there are no tasks to avoid NaN
|
||||||
return total > 0 ? Math.round((this.completedTasksCount() / total) * 100) : 0;
|
return total > 0 ? Math.round((this.completedTasksCount() / total) * 100) : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
readonly atRiskPasswordCount = computed<number>(() => {
|
readonly unassignedCipherIds = computed<number>(() => {
|
||||||
const atRiskIds = this._atRiskCipherIds();
|
const atRiskIds = this._atRiskCipherIds();
|
||||||
const tasks = this._tasks();
|
const tasks = this._tasks();
|
||||||
|
|
||||||
@@ -83,12 +80,20 @@ export class PasswordChangeMetricComponent implements OnInit {
|
|||||||
return atRiskIds.length;
|
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));
|
const unassignedIds = atRiskIds.filter((id) => !assignedIdSet.has(id));
|
||||||
|
|
||||||
return unassignedIds.length;
|
return unassignedIds.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
readonly atRiskPasswordCount = computed<number>(() => {
|
||||||
|
const atRiskIds = this._atRiskCipherIds();
|
||||||
|
const atRiskIdsSet = new Set(atRiskIds);
|
||||||
|
|
||||||
|
return atRiskIdsSet.size;
|
||||||
|
});
|
||||||
|
|
||||||
readonly currentView = computed<PasswordChangeView>(() => {
|
readonly currentView = computed<PasswordChangeView>(() => {
|
||||||
if (!this._hasCriticalApplications()) {
|
if (!this._hasCriticalApplications()) {
|
||||||
return PasswordChangeView.EMPTY;
|
return PasswordChangeView.EMPTY;
|
||||||
@@ -96,7 +101,7 @@ export class PasswordChangeMetricComponent implements OnInit {
|
|||||||
if (this.tasksCount() === 0) {
|
if (this.tasksCount() === 0) {
|
||||||
return PasswordChangeView.NO_TASKS_ASSIGNED;
|
return PasswordChangeView.NO_TASKS_ASSIGNED;
|
||||||
}
|
}
|
||||||
if (this.atRiskPasswordCount() > 0) {
|
if (this.unassignedCipherIds() > 0) {
|
||||||
return PasswordChangeView.NEW_TASKS_AVAILABLE;
|
return PasswordChangeView.NEW_TASKS_AVAILABLE;
|
||||||
}
|
}
|
||||||
return PasswordChangeView.PROGRESS;
|
return PasswordChangeView.PROGRESS;
|
||||||
|
|||||||
@@ -38,8 +38,8 @@
|
|||||||
|
|
||||||
@if (currentView() === DialogView.AssignTasks) {
|
@if (currentView() === DialogView.AssignTasks) {
|
||||||
<dirt-assign-tasks-view
|
<dirt-assign-tasks-view
|
||||||
[criticalApplicationsCount]="atRiskCriticalApplicationsCount()"
|
[criticalApplicationsCount]="newAtRiskCriticalApplications().length"
|
||||||
[totalApplicationsCount]="totalCriticalApplicationsCount()"
|
[totalApplicationsCount]="newCriticalApplications().length"
|
||||||
[atRiskCriticalMembersCount]="atRiskCriticalMembersCount()"
|
[atRiskCriticalMembersCount]="atRiskCriticalMembersCount()"
|
||||||
>
|
>
|
||||||
</dirt-assign-tasks-view>
|
</dirt-assign-tasks-view>
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import { CommonModule } from "@angular/common";
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
computed,
|
||||||
DestroyRef,
|
DestroyRef,
|
||||||
Inject,
|
Inject,
|
||||||
inject,
|
inject,
|
||||||
|
Injector,
|
||||||
|
Signal,
|
||||||
signal,
|
signal,
|
||||||
} from "@angular/core";
|
} 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 { from, switchMap, take } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +20,8 @@ import {
|
|||||||
import { getUniqueMembers } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
|
import { getUniqueMembers } from "@bitwarden/bit-common/dirt/reports/risk-insights/helpers";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 {
|
import {
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
DIALOG_DATA,
|
DIALOG_DATA,
|
||||||
@@ -70,9 +74,9 @@ export type NewApplicationsDialogResultType =
|
|||||||
(typeof NewApplicationsDialogResultType)[keyof typeof NewApplicationsDialogResultType];
|
(typeof NewApplicationsDialogResultType)[keyof typeof NewApplicationsDialogResultType];
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
selector: "dirt-new-applications-dialog",
|
selector: "dirt-new-applications-dialog",
|
||||||
templateUrl: "./new-applications-dialog.component.html",
|
templateUrl: "./new-applications-dialog.component.html",
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
@@ -95,10 +99,41 @@ export class NewApplicationsDialogComponent {
|
|||||||
// Applications selected to save as critical applications
|
// Applications selected to save as critical applications
|
||||||
protected readonly selectedApplications = signal<Set<string>>(new Set());
|
protected readonly selectedApplications = signal<Set<string>>(new Set());
|
||||||
|
|
||||||
// Assign tasks variables
|
// Used to determine if there are unassigned at-risk cipher IDs
|
||||||
readonly atRiskCriticalApplicationsCount = signal<number>(0);
|
private readonly _tasks!: Signal<SecurityTask[]>;
|
||||||
readonly totalCriticalApplicationsCount = signal<number>(0);
|
|
||||||
readonly atRiskCriticalMembersCount = signal<number>(0);
|
// 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<CipherId[]>(() => {
|
||||||
|
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<boolean>(false);
|
readonly saving = signal<boolean>(false);
|
||||||
|
|
||||||
// Loading states
|
// Loading states
|
||||||
@@ -106,13 +141,21 @@ export class NewApplicationsDialogComponent {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) protected dialogParams: NewApplicationsDialogData,
|
@Inject(DIALOG_DATA) protected dialogParams: NewApplicationsDialogData,
|
||||||
private dialogRef: DialogRef<NewApplicationsDialogResultType>,
|
|
||||||
private dataService: RiskInsightsDataService,
|
private dataService: RiskInsightsDataService,
|
||||||
private toastService: ToastService,
|
private dialogRef: DialogRef<NewApplicationsDialogResultType>,
|
||||||
|
private dialogService: DialogService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private accessIntelligenceSecurityTasksService: AccessIntelligenceSecurityTasksService,
|
private injector: Injector,
|
||||||
private logService: LogService,
|
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
|
* Opens the new applications dialog
|
||||||
@@ -170,53 +213,57 @@ export class NewApplicationsDialogComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMarkAsCritical() {
|
// Checks if there are selected applications and proceeds to assign tasks
|
||||||
if (this.markingAsCritical() || this.saving()) {
|
async handleMarkAsCritical() {
|
||||||
return; // Prevent action if already processing
|
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);
|
|
||||||
|
|
||||||
|
// 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);
|
this.currentView.set(DialogView.AssignTasks);
|
||||||
this.markingAsCritical.set(false);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Saves the application review and assigns tasks for unassigned at-risk ciphers
|
||||||
* Handles the assign tasks button click
|
|
||||||
*/
|
|
||||||
protected handleAssignTasks() {
|
protected handleAssignTasks() {
|
||||||
if (this.saving()) {
|
if (this.saving()) {
|
||||||
return; // Prevent double-click
|
return; // Prevent double-click
|
||||||
}
|
}
|
||||||
this.saving.set(true);
|
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
|
// Save the application review dates and critical markings
|
||||||
this.dataService.criticalApplicationAtRiskCipherIds$
|
this.dataService
|
||||||
|
.saveApplicationReviewStatus(updatedApplications)
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(this.destroyRef), // Satisfy eslint rule
|
takeUntilDestroyed(this.destroyRef), // Satisfy eslint rule
|
||||||
take(1), // Handle unsubscribe for one off operation
|
take(1),
|
||||||
switchMap((criticalApplicationAtRiskCipherIds) => {
|
switchMap(() => {
|
||||||
|
// Assign password change tasks for unassigned at-risk ciphers for critical applications
|
||||||
return from(
|
return from(
|
||||||
this.accessIntelligenceSecurityTasksService.requestPasswordChangeForCriticalApplications(
|
this.securityTasksService.requestPasswordChangeForCriticalApplications(
|
||||||
this.dialogParams.organizationId,
|
this.dialogParams.organizationId,
|
||||||
criticalApplicationAtRiskCipherIds,
|
this.newUnassignedAtRiskCipherIds(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user