mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 06:43:35 +00:00
[PM-26410] Update autotype policy to include all org members (#16689)
* PM-26410 use policies$ to apply default behavior to all org members * linting error, remove unused imports
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
import { TestBed } from "@angular/core/testing";
|
import { TestBed } from "@angular/core/testing";
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, firstValueFrom, take, timeout, TimeoutError } from "rxjs";
|
import { BehaviorSubject, firstValueFrom, take } from "rxjs";
|
||||||
|
|
||||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
@@ -18,10 +20,10 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
|
|||||||
let policyService: MockProxy<InternalPolicyService>;
|
let policyService: MockProxy<InternalPolicyService>;
|
||||||
let configService: MockProxy<ConfigService>;
|
let configService: MockProxy<ConfigService>;
|
||||||
|
|
||||||
let mockAccountSubject: BehaviorSubject<{ id: UserId } | null>;
|
let mockAccountSubject: BehaviorSubject<Account | null>;
|
||||||
let mockFeatureFlagSubject: BehaviorSubject<boolean>;
|
let mockFeatureFlagSubject: BehaviorSubject<boolean>;
|
||||||
let mockAuthStatusSubject: BehaviorSubject<AuthenticationStatus>;
|
let mockAuthStatusSubject: BehaviorSubject<AuthenticationStatus>;
|
||||||
let mockPolicyAppliesSubject: BehaviorSubject<boolean>;
|
let mockPoliciesSubject: BehaviorSubject<Policy[]>;
|
||||||
|
|
||||||
const mockUserId = "user-123" as UserId;
|
const mockUserId = "user-123" as UserId;
|
||||||
|
|
||||||
@@ -36,7 +38,7 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
|
|||||||
mockAuthStatusSubject = new BehaviorSubject<AuthenticationStatus>(
|
mockAuthStatusSubject = new BehaviorSubject<AuthenticationStatus>(
|
||||||
AuthenticationStatus.Unlocked,
|
AuthenticationStatus.Unlocked,
|
||||||
);
|
);
|
||||||
mockPolicyAppliesSubject = new BehaviorSubject<boolean>(false);
|
mockPoliciesSubject = new BehaviorSubject<Policy[]>([]);
|
||||||
|
|
||||||
accountService = mock<AccountService>();
|
accountService = mock<AccountService>();
|
||||||
authService = mock<AuthService>();
|
authService = mock<AuthService>();
|
||||||
@@ -50,9 +52,7 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
|
|||||||
authService.authStatusFor$ = jest
|
authService.authStatusFor$ = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementation((_: UserId) => mockAuthStatusSubject.asObservable());
|
.mockImplementation((_: UserId) => mockAuthStatusSubject.asObservable());
|
||||||
policyService.policyAppliesToUser$ = jest
|
policyService.policies$ = jest.fn().mockReturnValue(mockPoliciesSubject.asObservable());
|
||||||
.fn()
|
|
||||||
.mockReturnValue(mockPolicyAppliesSubject.asObservable());
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -72,7 +72,7 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
|
|||||||
mockAccountSubject.complete();
|
mockAccountSubject.complete();
|
||||||
mockFeatureFlagSubject.complete();
|
mockFeatureFlagSubject.complete();
|
||||||
mockAuthStatusSubject.complete();
|
mockAuthStatusSubject.complete();
|
||||||
mockPolicyAppliesSubject.complete();
|
mockPoliciesSubject.complete();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("autotypeDefaultSetting$", () => {
|
describe("autotypeDefaultSetting$", () => {
|
||||||
@@ -82,11 +82,20 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
|
|||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not emit when no active account", async () => {
|
it("does not emit until an account appears", async () => {
|
||||||
mockAccountSubject.next(null);
|
mockAccountSubject.next(null);
|
||||||
await expect(
|
|
||||||
firstValueFrom(service.autotypeDefaultSetting$.pipe(timeout({ first: 30 }))),
|
mockAccountSubject.next({ id: mockUserId } as Account);
|
||||||
).rejects.toBeInstanceOf(TimeoutError);
|
mockAuthStatusSubject.next(AuthenticationStatus.Unlocked);
|
||||||
|
mockPoliciesSubject.next([
|
||||||
|
{
|
||||||
|
type: PolicyType.AutotypeDefaultSetting,
|
||||||
|
enabled: true,
|
||||||
|
} as Policy,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit null when user is not unlocked", async () => {
|
it("should emit null when user is not unlocked", async () => {
|
||||||
@@ -96,34 +105,56 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should emit null when no autotype policy exists", async () => {
|
it("should emit null when no autotype policy exists", async () => {
|
||||||
mockPolicyAppliesSubject.next(false);
|
mockPoliciesSubject.next([]);
|
||||||
const policy = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
const policy = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
expect(policy).toBeNull();
|
expect(policy).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit true when autotype policy is enabled", async () => {
|
it("should emit true when autotype policy is enabled", async () => {
|
||||||
mockPolicyAppliesSubject.next(true);
|
mockPoliciesSubject.next([
|
||||||
|
{
|
||||||
|
type: PolicyType.AutotypeDefaultSetting,
|
||||||
|
enabled: true,
|
||||||
|
} as Policy,
|
||||||
|
]);
|
||||||
const policyStatus = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
const policyStatus = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
expect(policyStatus).toBe(true);
|
expect(policyStatus).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit false when autotype policy is disabled", async () => {
|
it("should emit null when autotype policy is disabled", async () => {
|
||||||
mockPolicyAppliesSubject.next(false);
|
mockPoliciesSubject.next([
|
||||||
|
{
|
||||||
|
type: PolicyType.AutotypeDefaultSetting,
|
||||||
|
enabled: false,
|
||||||
|
} as Policy,
|
||||||
|
]);
|
||||||
const policyStatus = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
const policyStatus = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
expect(policyStatus).toBeNull();
|
expect(policyStatus).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit null when autotype policy does not apply", async () => {
|
it("should emit null when autotype policy does not apply", async () => {
|
||||||
mockPolicyAppliesSubject.next(false);
|
mockPoliciesSubject.next([
|
||||||
|
{
|
||||||
|
type: PolicyType.RequireSso,
|
||||||
|
enabled: true,
|
||||||
|
} as Policy,
|
||||||
|
]);
|
||||||
const policy = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
const policy = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
expect(policy).toBeNull();
|
expect(policy).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should react to authentication status changes", async () => {
|
it("should react to authentication status changes", async () => {
|
||||||
|
mockPoliciesSubject.next([
|
||||||
|
{
|
||||||
|
type: PolicyType.AutotypeDefaultSetting,
|
||||||
|
enabled: true,
|
||||||
|
} as Policy,
|
||||||
|
]);
|
||||||
|
|
||||||
// Expect one emission when unlocked
|
// Expect one emission when unlocked
|
||||||
mockAuthStatusSubject.next(AuthenticationStatus.Unlocked);
|
mockAuthStatusSubject.next(AuthenticationStatus.Unlocked);
|
||||||
const first = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
const first = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
expect(first).toBeNull();
|
expect(first).toBe(true);
|
||||||
|
|
||||||
// Expect null emission when locked
|
// Expect null emission when locked
|
||||||
mockAuthStatusSubject.next(AuthenticationStatus.Locked);
|
mockAuthStatusSubject.next(AuthenticationStatus.Locked);
|
||||||
@@ -134,33 +165,131 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
|
|||||||
it("should react to account changes", async () => {
|
it("should react to account changes", async () => {
|
||||||
const newUserId = "user-456" as UserId;
|
const newUserId = "user-456" as UserId;
|
||||||
|
|
||||||
|
mockPoliciesSubject.next([
|
||||||
|
{
|
||||||
|
type: PolicyType.AutotypeDefaultSetting,
|
||||||
|
enabled: true,
|
||||||
|
} as Policy,
|
||||||
|
]);
|
||||||
|
|
||||||
// First value for original user
|
// First value for original user
|
||||||
const firstValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
const firstValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
expect(firstValue).toBeNull();
|
expect(firstValue).toBe(true);
|
||||||
|
|
||||||
// Change account and expect a new emission
|
// Change account and expect a new emission
|
||||||
mockAccountSubject.next({
|
mockAccountSubject.next({
|
||||||
id: newUserId,
|
id: newUserId,
|
||||||
});
|
} as Account);
|
||||||
const secondValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
const secondValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
expect(secondValue).toBeNull();
|
expect(secondValue).toBe(true);
|
||||||
|
|
||||||
// Verify the auth lookup was switched to the new user
|
// Verify the auth lookup was switched to the new user
|
||||||
expect(authService.authStatusFor$).toHaveBeenCalledWith(newUserId);
|
expect(authService.authStatusFor$).toHaveBeenCalledWith(newUserId);
|
||||||
|
expect(policyService.policies$).toHaveBeenCalledWith(newUserId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should react to policy changes", async () => {
|
it("should react to policy changes", async () => {
|
||||||
mockPolicyAppliesSubject.next(false);
|
mockPoliciesSubject.next([]);
|
||||||
const nullValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
const nullValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
expect(nullValue).toBeNull();
|
expect(nullValue).toBeNull();
|
||||||
|
|
||||||
mockPolicyAppliesSubject.next(true);
|
mockPoliciesSubject.next([
|
||||||
|
{
|
||||||
|
type: PolicyType.AutotypeDefaultSetting,
|
||||||
|
enabled: true,
|
||||||
|
} as Policy,
|
||||||
|
]);
|
||||||
const trueValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
const trueValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
expect(trueValue).toBe(true);
|
expect(trueValue).toBe(true);
|
||||||
|
|
||||||
mockPolicyAppliesSubject.next(false);
|
mockPoliciesSubject.next([]);
|
||||||
const nullValueAgain = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
const nullValueAgain = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
expect(nullValueAgain).toBeNull();
|
expect(nullValueAgain).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("emits null again if the feature flag turns off after emitting", async () => {
|
||||||
|
mockPoliciesSubject.next([
|
||||||
|
{ type: PolicyType.AutotypeDefaultSetting, enabled: true } as Policy,
|
||||||
|
]);
|
||||||
|
expect(await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)))).toBe(true);
|
||||||
|
|
||||||
|
mockFeatureFlagSubject.next(false);
|
||||||
|
expect(await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)))).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replays the latest value to late subscribers", async () => {
|
||||||
|
mockPoliciesSubject.next([
|
||||||
|
{ type: PolicyType.AutotypeDefaultSetting, enabled: true } as Policy,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
|
|
||||||
|
const late = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
|
expect(late).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not re-emit when effective value is unchanged", async () => {
|
||||||
|
mockAccountSubject.next({ id: mockUserId } as Account);
|
||||||
|
mockAuthStatusSubject.next(AuthenticationStatus.Unlocked);
|
||||||
|
|
||||||
|
const policies = [
|
||||||
|
{
|
||||||
|
type: PolicyType.AutotypeDefaultSetting,
|
||||||
|
enabled: true,
|
||||||
|
} as Policy,
|
||||||
|
];
|
||||||
|
|
||||||
|
mockPoliciesSubject.next(policies);
|
||||||
|
const first = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
|
expect(first).toBe(true);
|
||||||
|
|
||||||
|
let emissionCount = 0;
|
||||||
|
const subscription = service.autotypeDefaultSetting$.subscribe(() => {
|
||||||
|
emissionCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
mockPoliciesSubject.next(policies);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
subscription.unsubscribe();
|
||||||
|
|
||||||
|
expect(emissionCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not emit policy values while locked; emits after unlocking", async () => {
|
||||||
|
mockAuthStatusSubject.next(AuthenticationStatus.Locked);
|
||||||
|
mockPoliciesSubject.next([
|
||||||
|
{ type: PolicyType.AutotypeDefaultSetting, enabled: true } as Policy,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)))).toBeNull();
|
||||||
|
|
||||||
|
mockAuthStatusSubject.next(AuthenticationStatus.Unlocked);
|
||||||
|
expect(await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits correctly if auth unlocks before policies arrive", async () => {
|
||||||
|
mockAccountSubject.next({ id: mockUserId } as Account);
|
||||||
|
mockAuthStatusSubject.next(AuthenticationStatus.Unlocked);
|
||||||
|
mockPoliciesSubject.next([
|
||||||
|
{
|
||||||
|
type: PolicyType.AutotypeDefaultSetting,
|
||||||
|
enabled: true,
|
||||||
|
} as Policy,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wires dependencies with initial user id", async () => {
|
||||||
|
mockPoliciesSubject.next([
|
||||||
|
{ type: PolicyType.AutotypeDefaultSetting, enabled: true } as Policy,
|
||||||
|
]);
|
||||||
|
await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
|
||||||
|
|
||||||
|
expect(authService.authStatusFor$).toHaveBeenCalledWith(mockUserId);
|
||||||
|
expect(policyService.policies$).toHaveBeenCalledWith(mockUserId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class DesktopAutotypeDefaultSettingPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.accountService.activeAccount$.pipe(
|
return this.accountService.activeAccount$.pipe(
|
||||||
filter((account) => account != null),
|
filter((account) => account != null && account.id != null),
|
||||||
getUserId,
|
getUserId,
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
switchMap((userId) => {
|
switchMap((userId) => {
|
||||||
@@ -43,10 +43,13 @@ export class DesktopAutotypeDefaultSettingPolicy {
|
|||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const policy$ = this.policyService
|
const policy$ = this.policyService.policies$(userId).pipe(
|
||||||
.policyAppliesToUser$(PolicyType.AutotypeDefaultSetting, userId)
|
map((policies) => {
|
||||||
.pipe(
|
const autotypePolicy = policies.find(
|
||||||
map((appliesToUser) => (appliesToUser ? true : null)),
|
(policy) => policy.type === PolicyType.AutotypeDefaultSetting && policy.enabled,
|
||||||
|
);
|
||||||
|
return autotypePolicy ? true : null;
|
||||||
|
}),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
shareReplay({ bufferSize: 1, refCount: true }),
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user