mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-26053] Create Autotype Desktop Default Setting Policy for use within the desktop autotype service (#16537)
* add policy type enum * desktop autotype service which emits an observable * add desktop autotype default setting policy to the app constructor * update service module to include DesktopAutotypeDefaultSettingPolicy * flag the service * add tests * address comments, switch to null remove false, update tests
This commit is contained in:
@@ -77,6 +77,7 @@ import { KeyService, BiometricStateService } from "@bitwarden/key-management";
|
||||
import { AddEditFolderDialogComponent, AddEditFolderDialogResult } from "@bitwarden/vault";
|
||||
|
||||
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
import { DesktopAutotypeDefaultSettingPolicy } from "../autofill/services/desktop-autotype-policy.service";
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
|
||||
|
||||
@@ -177,6 +178,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private readonly documentLangSetter: DocumentLangSetter,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private readonly tokenService: TokenService,
|
||||
private desktopAutotypeDefaultSettingPolicy: DesktopAutotypeDefaultSettingPolicy,
|
||||
) {
|
||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||
|
||||
|
||||
@@ -37,7 +37,10 @@ import {
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import {
|
||||
PolicyService as PolicyServiceAbstraction,
|
||||
InternalPolicyService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import {
|
||||
AccountService,
|
||||
AccountService as AccountServiceAbstraction,
|
||||
@@ -112,6 +115,7 @@ import { DesktopLoginComponentService } from "../../auth/login/desktop-login-com
|
||||
import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
|
||||
import { DesktopAutotypeDefaultSettingPolicy } from "../../autofill/services/desktop-autotype-policy.service";
|
||||
import { DesktopAutotypeService } from "../../autofill/services/desktop-autotype.service";
|
||||
import { DesktopFido2UserInterfaceService } from "../../autofill/services/desktop-fido2-user-interface.service";
|
||||
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
|
||||
@@ -466,6 +470,11 @@ const safeProviders: SafeProvider[] = [
|
||||
BillingAccountProfileStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DesktopAutotypeDefaultSettingPolicy,
|
||||
useClass: DesktopAutotypeDefaultSettingPolicy,
|
||||
deps: [AccountServiceAbstraction, AuthServiceAbstraction, InternalPolicyService, ConfigService],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, take, timeout, TimeoutError } from "rxjs";
|
||||
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Account, UserId } from "@bitwarden/common/platform/models/domain/account";
|
||||
|
||||
import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service";
|
||||
|
||||
describe("DesktopAutotypeDefaultSettingPolicy", () => {
|
||||
let service: DesktopAutotypeDefaultSettingPolicy;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let policyService: MockProxy<InternalPolicyService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
let mockAccountSubject: BehaviorSubject<{ id: UserId } | null>;
|
||||
let mockFeatureFlagSubject: BehaviorSubject<boolean>;
|
||||
let mockAuthStatusSubject: BehaviorSubject<AuthenticationStatus>;
|
||||
let mockPolicyAppliesSubject: BehaviorSubject<boolean>;
|
||||
|
||||
const mockUserId = "user-123" as UserId;
|
||||
|
||||
beforeEach(() => {
|
||||
mockAccountSubject = new BehaviorSubject<Account | null>({
|
||||
id: mockUserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
});
|
||||
mockFeatureFlagSubject = new BehaviorSubject<boolean>(true);
|
||||
mockAuthStatusSubject = new BehaviorSubject<AuthenticationStatus>(
|
||||
AuthenticationStatus.Unlocked,
|
||||
);
|
||||
mockPolicyAppliesSubject = new BehaviorSubject<boolean>(false);
|
||||
|
||||
accountService = mock<AccountService>();
|
||||
authService = mock<AuthService>();
|
||||
policyService = mock<InternalPolicyService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
accountService.activeAccount$ = mockAccountSubject.asObservable();
|
||||
configService.getFeatureFlag$ = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockFeatureFlagSubject.asObservable());
|
||||
authService.authStatusFor$ = jest
|
||||
.fn()
|
||||
.mockImplementation((_: UserId) => mockAuthStatusSubject.asObservable());
|
||||
policyService.policyAppliesToUser$ = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockPolicyAppliesSubject.asObservable());
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DesktopAutotypeDefaultSettingPolicy,
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: InternalPolicyService, useValue: policyService },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(DesktopAutotypeDefaultSettingPolicy);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockAccountSubject.complete();
|
||||
mockFeatureFlagSubject.complete();
|
||||
mockAuthStatusSubject.complete();
|
||||
mockPolicyAppliesSubject.complete();
|
||||
});
|
||||
|
||||
describe("autotypeDefaultSetting$", () => {
|
||||
it("should emit null when feature flag is disabled", async () => {
|
||||
mockFeatureFlagSubject.next(false);
|
||||
const result = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should not emit when no active account", async () => {
|
||||
mockAccountSubject.next(null);
|
||||
await expect(
|
||||
firstValueFrom(service.autotypeDefaultSetting$.pipe(timeout({ first: 30 }))),
|
||||
).rejects.toBeInstanceOf(TimeoutError);
|
||||
});
|
||||
|
||||
it("should emit null when user is not unlocked", async () => {
|
||||
mockAuthStatusSubject.next(AuthenticationStatus.Locked);
|
||||
const result = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should emit null when no autotype policy exists", async () => {
|
||||
mockPolicyAppliesSubject.next(false);
|
||||
const policy = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(policy).toBeNull();
|
||||
});
|
||||
|
||||
it("should emit true when autotype policy is enabled", async () => {
|
||||
mockPolicyAppliesSubject.next(true);
|
||||
const policyStatus = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(policyStatus).toBe(true);
|
||||
});
|
||||
|
||||
it("should emit false when autotype policy is disabled", async () => {
|
||||
mockPolicyAppliesSubject.next(false);
|
||||
const policyStatus = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(policyStatus).toBeNull();
|
||||
});
|
||||
|
||||
it("should emit null when autotype policy does not apply", async () => {
|
||||
mockPolicyAppliesSubject.next(false);
|
||||
const policy = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(policy).toBeNull();
|
||||
});
|
||||
|
||||
it("should react to authentication status changes", async () => {
|
||||
// Expect one emission when unlocked
|
||||
mockAuthStatusSubject.next(AuthenticationStatus.Unlocked);
|
||||
const first = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(first).toBeNull();
|
||||
|
||||
// Expect null emission when locked
|
||||
mockAuthStatusSubject.next(AuthenticationStatus.Locked);
|
||||
const lockedResult = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(lockedResult).toBeNull();
|
||||
});
|
||||
|
||||
it("should react to account changes", async () => {
|
||||
const newUserId = "user-456" as UserId;
|
||||
|
||||
// First value for original user
|
||||
const firstValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(firstValue).toBeNull();
|
||||
|
||||
// Change account and expect a new emission
|
||||
mockAccountSubject.next({
|
||||
id: newUserId,
|
||||
});
|
||||
const secondValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(secondValue).toBeNull();
|
||||
|
||||
// Verify the auth lookup was switched to the new user
|
||||
expect(authService.authStatusFor$).toHaveBeenCalledWith(newUserId);
|
||||
});
|
||||
|
||||
it("should react to policy changes", async () => {
|
||||
mockPolicyAppliesSubject.next(false);
|
||||
const nullValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(nullValue).toBeNull();
|
||||
|
||||
mockPolicyAppliesSubject.next(true);
|
||||
const trueValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(trueValue).toBe(true);
|
||||
|
||||
mockPolicyAppliesSubject.next(false);
|
||||
const nullValueAgain = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||
expect(nullValueAgain).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Observable, of } from "rxjs";
|
||||
import { distinctUntilChanged, filter, map, shareReplay, switchMap } from "rxjs/operators";
|
||||
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class DesktopAutotypeDefaultSettingPolicy {
|
||||
constructor(
|
||||
private readonly accountService: AccountService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly policyService: InternalPolicyService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Emits the autotype policy enabled status (true | false | null) when account is unlocked and WindowsDesktopAutotype is enabled.
|
||||
* - true: autotype policy exists and is enabled
|
||||
* - null: no autotype policy exists for the user's organization
|
||||
*/
|
||||
readonly autotypeDefaultSetting$: Observable<boolean | null> = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype)
|
||||
.pipe(
|
||||
switchMap((autotypeFeatureEnabled) => {
|
||||
if (!autotypeFeatureEnabled) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
filter((account) => account != null),
|
||||
getUserId,
|
||||
distinctUntilChanged(),
|
||||
switchMap((userId) => {
|
||||
const isUnlocked$ = this.authService.authStatusFor$(userId).pipe(
|
||||
map((status) => status === AuthenticationStatus.Unlocked),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const policy$ = this.policyService
|
||||
.policyAppliesToUser$(PolicyType.AutotypeDefaultSetting, userId)
|
||||
.pipe(
|
||||
map((appliesToUser) => (appliesToUser ? true : null)),
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
return isUnlocked$.pipe(switchMap((unlocked) => (unlocked ? policy$ : of(null))));
|
||||
}),
|
||||
);
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user