1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-21041] Fix cipher view security tasks fetching (#14569)

* [PM-21041] Add taskEnabled$ dependency to tasks$ observable

* [PM-21041] Rework cipher view component to only check tasks for organization Login type ciphers

- Remove dependency on feature flag check (handled by tasks$ observable now)
- Add try/catch in case of request failures to avoid breaking component initialization

* [PM-21041] Remove now redundant taskEnabled$ chain

* [PM-21041] Fix tests
This commit is contained in:
Shane Melton
2025-05-01 11:22:32 -07:00
committed by GitHub
parent de6b58c10a
commit cba5f826d6
6 changed files with 147 additions and 73 deletions

View File

@@ -108,6 +108,34 @@ describe("Default task service", () => {
});
describe("tasks$", () => {
beforeEach(() => {
mockGetFeatureFlag$.mockReturnValue(new BehaviorSubject(true));
mockGetAllOrgs$.mockReturnValue(
new BehaviorSubject([
{
useRiskInsights: true,
},
] as Organization[]),
);
});
it("should return an empty array if tasks are not enabled", async () => {
mockGetAllOrgs$.mockReturnValue(
new BehaviorSubject([
{
useRiskInsights: false,
},
] as Organization[]),
);
const { tasks$ } = service;
const result = await firstValueFrom(tasks$("user-id" as UserId));
expect(result.length).toBe(0);
expect(mockApiSend).not.toHaveBeenCalled();
});
it("should fetch tasks from the API when the state is null", async () => {
mockApiSend.mockResolvedValue({
data: [
@@ -153,6 +181,34 @@ describe("Default task service", () => {
});
describe("pendingTasks$", () => {
beforeEach(() => {
mockGetFeatureFlag$.mockReturnValue(new BehaviorSubject(true));
mockGetAllOrgs$.mockReturnValue(
new BehaviorSubject([
{
useRiskInsights: true,
},
] as Organization[]),
);
});
it("should return an empty array if tasks are not enabled", async () => {
mockGetAllOrgs$.mockReturnValue(
new BehaviorSubject([
{
useRiskInsights: false,
},
] as Organization[]),
);
const { pendingTasks$ } = service;
const result = await firstValueFrom(pendingTasks$("user-id" as UserId));
expect(result.length).toBe(0);
expect(mockApiSend).not.toHaveBeenCalled();
});
it("should filter tasks to only pending tasks", async () => {
fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [
{

View File

@@ -1,4 +1,14 @@
import { combineLatest, filter, map, merge, Observable, of, Subscription, switchMap } from "rxjs";
import {
combineLatest,
filter,
map,
merge,
Observable,
of,
Subscription,
switchMap,
distinctUntilChanged,
} from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -45,20 +55,30 @@ export class DefaultTaskService implements TaskService {
.organizations$(userId)
.pipe(map((orgs) => orgs.some((o) => o.useRiskInsights))),
this.configService.getFeatureFlag$(FeatureFlag.SecurityTasks),
]).pipe(map(([atLeastOneOrgEnabled, flagEnabled]) => atLeastOneOrgEnabled && flagEnabled));
]).pipe(
map(([atLeastOneOrgEnabled, flagEnabled]) => atLeastOneOrgEnabled && flagEnabled),
distinctUntilChanged(),
);
});
tasks$ = perUserCache$((userId) => {
return this.taskState(userId).state$.pipe(
switchMap(async (tasks) => {
if (tasks == null) {
await this.fetchTasksFromApi(userId);
return null;
return this.tasksEnabled$(userId).pipe(
switchMap((enabled) => {
if (!enabled) {
return of([]);
}
return tasks;
return this.taskState(userId).state$.pipe(
switchMap(async (tasks) => {
if (tasks == null) {
await this.fetchTasksFromApi(userId);
return null;
}
return tasks;
}),
filterOutNullish(),
map((tasks) => tasks.map((t) => new SecurityTask(t))),
);
}),
filterOutNullish(),
map((tasks) => tasks.map((t) => new SecurityTask(t))),
);
});

View File

@@ -3,19 +3,18 @@
{{ "cardExpiredMessage" | i18n }}
</bit-callout>
<ng-container *ngIf="isSecurityTasksEnabled$ | async">
<bit-callout
*ngIf="cipher?.login.uris.length > 0 && hadPendingChangePasswordTask"
type="warning"
[title]="''"
>
<i class="bwi bwi-exclamation-triangle tw-text-warning" aria-hidden="true"></i>
<a bitLink href="#" appStopClick (click)="launchChangePassword()">
{{ "changeAtRiskPassword" | i18n }}
<i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
</a>
</bit-callout>
</ng-container>
<bit-callout
*ngIf="cipher?.login.uris.length > 0 && hadPendingChangePasswordTask"
type="warning"
[title]="''"
>
<i class="bwi bwi-exclamation-triangle tw-text-warning" aria-hidden="true"></i>
<a bitLink href="#" appStopClick (click)="launchChangePassword()">
{{ "changeAtRiskPassword" | i18n }}
<i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
</a>
</bit-callout>
<!-- HELPER TEXT -->
<p
class="tw-text-sm tw-text-muted"
@@ -40,9 +39,7 @@
*ngIf="hasLogin"
[cipher]="cipher"
[activeUserId]="activeUserId$ | async"
[hadPendingChangePasswordTask]="
hadPendingChangePasswordTask && (isSecurityTasksEnabled$ | async)
"
[hadPendingChangePasswordTask]="hadPendingChangePasswordTask"
(handleChangePassword)="launchChangePassword()"
></app-login-credentials-view>

View File

@@ -12,12 +12,12 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { isCardExpired } from "@bitwarden/common/autofill/utils";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherId, CollectionId, EmergencyAccessId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
@@ -80,7 +80,6 @@ export class CipherViewComponent implements OnChanges, OnDestroy {
private destroyed$: Subject<void> = new Subject();
cardIsExpired: boolean = false;
hadPendingChangePasswordTask: boolean = false;
isSecurityTasksEnabled$ = this.configService.getFeatureFlag$(FeatureFlag.SecurityTasks);
constructor(
private organizationService: OrganizationService,
@@ -90,8 +89,8 @@ export class CipherViewComponent implements OnChanges, OnDestroy {
private defaultTaskService: TaskService,
private platformUtilsService: PlatformUtilsService,
private changeLoginPasswordService: ChangeLoginPasswordService,
private configService: ConfigService,
private cipherService: CipherService,
private logService: LogService,
) {}
async ngOnChanges() {
@@ -158,20 +157,15 @@ export class CipherViewComponent implements OnChanges, OnDestroy {
const userId = await firstValueFrom(this.activeUserId$);
// Show Tasks for Manage and Edit permissions
// Using cipherService to see if user has access to cipher in a non-AC context to address with Edit Except Password permissions
const allCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
const cipherServiceCipher = allCiphers[this.cipher?.id as CipherId];
if (cipherServiceCipher?.edit && cipherServiceCipher?.viewPassword) {
await this.checkPendingChangePasswordTasks(userId);
}
if (this.cipher.organizationId && userId) {
if (this.cipher.organizationId) {
this.organization$ = this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(this.cipher.organizationId))
.pipe(takeUntil(this.destroyed$));
if (this.cipher.type === CipherType.Login) {
await this.checkPendingChangePasswordTasks(userId);
}
}
if (this.cipher.folderId) {
@@ -182,17 +176,28 @@ export class CipherViewComponent implements OnChanges, OnDestroy {
}
async checkPendingChangePasswordTasks(userId: UserId): Promise<void> {
if (!(await firstValueFrom(this.isSecurityTasksEnabled$))) {
return;
try {
// Show Tasks for Manage and Edit permissions
// Using cipherService to see if user has access to cipher in a non-AC context to address with Edit Except Password permissions
const allCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
const cipherServiceCipher = allCiphers[this.cipher?.id as CipherId];
if (!cipherServiceCipher?.edit || !cipherServiceCipher?.viewPassword) {
this.hadPendingChangePasswordTask = false;
return;
}
const tasks = await firstValueFrom(this.defaultTaskService.pendingTasks$(userId));
this.hadPendingChangePasswordTask = tasks?.some((task) => {
return (
task.cipherId === this.cipher?.id && task.type === SecurityTaskType.UpdateAtRiskCredential
);
});
} catch (error) {
this.hadPendingChangePasswordTask = false;
this.logService.error("Failed to retrieve change password tasks for cipher", error);
}
const tasks = await firstValueFrom(this.defaultTaskService.pendingTasks$(userId));
this.hadPendingChangePasswordTask = tasks?.some((task) => {
return (
task.cipherId === this.cipher?.id && task.type === SecurityTaskType.UpdateAtRiskCredential
);
});
}
launchChangePassword = async () => {