1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

[PM-14421] Access Intelligence: Introduce At-risk Passwords Page (#13044)

* [PM-14421] Add initial at risk password page component and route

* [PM-14421] Add new at-risk-password guard and update task service to consider feature flag for tasksEnabled$

* [PM-14421] Export vault observable utilities to be used outside of libs/vault

* [PM-14421] Implement at risk passwords page

* [PM-14421] Add temporary callout for at-risk tasks to browser vault view

* [PM-14421] Fix service registration after merge

* [PM-14421] Fix organization service usage after merge

* [PM-14421] Add autofill setting callout

* [PM-14421] Fix failing test

* [PM-14421] Change autofill setting check and toggle

* [PM-14421] Make autofill setting callout dismissal persistent

* [PM-14421] Fix tests

* [PM-14421] Fix button structure

* [PM-14421] Handle plural tasks i18n

* [PM-14421] Fix cipher service usage after refactor on main

* [PM-14421] Fix at-risk-password spec file
This commit is contained in:
Shane Melton
2025-02-12 13:28:20 -08:00
committed by GitHub
parent 6d61d08d44
commit 96260eda65
16 changed files with 763 additions and 23 deletions

View File

@@ -296,7 +296,12 @@ import {
DefaultUserAsymmetricKeysRegenerationApiService,
} from "@bitwarden/key-management";
import { SafeInjectionToken } from "@bitwarden/ui-common";
import { NewDeviceVerificationNoticeService, PasswordRepromptService } from "@bitwarden/vault";
import {
DefaultTaskService,
NewDeviceVerificationNoticeService,
PasswordRepromptService,
TaskService,
} from "@bitwarden/vault";
import {
VaultExportService,
VaultExportServiceAbstraction,
@@ -1463,6 +1468,11 @@ const safeProviders: SafeProvider[] = [
useClass: PasswordLoginStrategyData,
deps: [],
}),
safeProvider({
provide: TaskService,
useClass: DefaultTaskService,
deps: [StateProvider, ApiServiceAbstraction, OrganizationServiceAbstraction, ConfigService],
}),
];
@NgModule({

View File

@@ -5,6 +5,8 @@ export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directi
export { OrgIconDirective } from "./components/org-icon.directive";
export { CanDeleteCipherDirective } from "./components/can-delete-cipher.directive";
export * from "./utils/observable-utilities";
export * from "./cipher-view";
export * from "./cipher-form";
export {

View File

@@ -4,6 +4,7 @@ import { BehaviorSubject, firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid";
import { DefaultTaskService, SecurityTaskStatus } from "@bitwarden/vault";
@@ -18,18 +19,26 @@ describe("Default task service", () => {
const mockApiSend = jest.fn();
const mockGetAllOrgs$ = jest.fn();
const mockGetFeatureFlag$ = jest.fn();
let testBed: TestBed;
beforeEach(async () => {
mockApiSend.mockClear();
mockGetAllOrgs$.mockClear();
mockGetFeatureFlag$.mockClear();
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId));
testBed = TestBed.configureTestingModule({
imports: [],
providers: [
DefaultTaskService,
{
provide: ConfigService,
useValue: {
getFeatureFlag$: mockGetFeatureFlag$,
},
},
{
provide: StateProvider,
useValue: fakeStateProvider,
@@ -52,6 +61,7 @@ describe("Default task service", () => {
describe("tasksEnabled$", () => {
it("should emit true if any organization uses risk insights", async () => {
mockGetFeatureFlag$.mockReturnValue(new BehaviorSubject(true));
mockGetAllOrgs$.mockReturnValue(
new BehaviorSubject([
{
@@ -71,6 +81,7 @@ describe("Default task service", () => {
});
it("should emit false if no organization uses risk insights", async () => {
mockGetFeatureFlag$.mockReturnValue(new BehaviorSubject(true));
mockGetAllOrgs$.mockReturnValue(
new BehaviorSubject([
{
@@ -88,6 +99,23 @@ describe("Default task service", () => {
expect(result).toBe(false);
});
it("should emit false if the feature flag is off", async () => {
mockGetFeatureFlag$.mockReturnValue(new BehaviorSubject(false));
mockGetAllOrgs$.mockReturnValue(
new BehaviorSubject([
{
useRiskInsights: true,
},
] as Organization[]),
);
const { tasksEnabled$ } = testBed.inject(DefaultTaskService);
const result = await firstValueFrom(tasksEnabled$("user-id" as UserId));
expect(result).toBe(false);
});
});
describe("tasks$", () => {
@@ -100,7 +128,7 @@ describe("Default task service", () => {
] as SecurityTaskResponse[],
});
fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, null);
fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, null as any);
const { tasks$ } = testBed.inject(DefaultTaskService);
@@ -183,7 +211,11 @@ describe("Default task service", () => {
] as SecurityTaskResponse[],
});
const mock = fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, null);
const mock = fakeStateProvider.singleUser.mockFor(
"user-id" as UserId,
SECURITY_TASKS,
null as any,
);
const service = testBed.inject(DefaultTaskService);

View File

@@ -1,9 +1,11 @@
import { Injectable } from "@angular/core";
import { map, switchMap } from "rxjs";
import { combineLatest, map, switchMap } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid";
import { SecurityTask, SecurityTaskStatus, TaskService } from "@bitwarden/vault";
@@ -19,12 +21,16 @@ export class DefaultTaskService implements TaskService {
private stateProvider: StateProvider,
private apiService: ApiService,
private organizationService: OrganizationService,
private configService: ConfigService,
) {}
tasksEnabled$ = perUserCache$((userId) => {
return this.organizationService
.organizations$(userId)
.pipe(map((orgs) => orgs.some((o) => o.useRiskInsights)));
return combineLatest([
this.organizationService
.organizations$(userId)
.pipe(map((orgs) => orgs.some((o) => o.useRiskInsights))),
this.configService.getFeatureFlag$(FeatureFlag.SecurityTasks),
]).pipe(map(([atLeastOneOrgEnabled, flagEnabled]) => atLeastOneOrgEnabled && flagEnabled));
});
tasks$ = perUserCache$((userId) => {