mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +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 { AddEditFolderDialogComponent, AddEditFolderDialogResult } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
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 { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||||
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
|
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
|
||||||
|
|
||||||
@@ -177,6 +178,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private readonly documentLangSetter: DocumentLangSetter,
|
private readonly documentLangSetter: DocumentLangSetter,
|
||||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
private readonly tokenService: TokenService,
|
private readonly tokenService: TokenService,
|
||||||
|
private desktopAutotypeDefaultSettingPolicy: DesktopAutotypeDefaultSettingPolicy,
|
||||||
) {
|
) {
|
||||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ import {
|
|||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
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 {
|
import {
|
||||||
AccountService,
|
AccountService,
|
||||||
AccountService as AccountServiceAbstraction,
|
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 { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service";
|
||||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||||
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.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 { DesktopAutotypeService } from "../../autofill/services/desktop-autotype.service";
|
||||||
import { DesktopFido2UserInterfaceService } from "../../autofill/services/desktop-fido2-user-interface.service";
|
import { DesktopFido2UserInterfaceService } from "../../autofill/services/desktop-fido2-user-interface.service";
|
||||||
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
|
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
|
||||||
@@ -466,6 +470,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
BillingAccountProfileStateService,
|
BillingAccountProfileStateService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: DesktopAutotypeDefaultSettingPolicy,
|
||||||
|
useClass: DesktopAutotypeDefaultSettingPolicy,
|
||||||
|
deps: [AccountServiceAbstraction, AuthServiceAbstraction, InternalPolicyService, ConfigService],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@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