mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 15:23:33 +00:00
[AC-2008] [AC-2123] [Pt 2] Transition PolicyService to use StateProvider (#7977)
* fully wire up StateProvider within PolicyService * migrate old policy data to new location * minor update to existing interfaces
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { PolicyType } from "../../enums";
|
||||
import { PolicyData } from "../../models/data/policy.data";
|
||||
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
|
||||
@@ -10,9 +11,9 @@ import { PolicyResponse } from "../../models/response/policy.response";
|
||||
|
||||
export abstract class PolicyService {
|
||||
/**
|
||||
* All {@link Policy} objects for the active user (from sync data).
|
||||
* May include policies that are disabled or otherwise do not apply to the user.
|
||||
* @see {@link get$} or {@link policyAppliesToActiveUser$} if you want to know when a policy applies to a user.
|
||||
* All policies for the active user from sync data.
|
||||
* May include policies that are disabled or otherwise do not apply to the user. Be careful using this!
|
||||
* Consider using {@link get$} or {@link getAll$} instead, which will only return policies that should be enforced against the user.
|
||||
*/
|
||||
policies$: Observable<Policy[]>;
|
||||
|
||||
@@ -20,37 +21,33 @@ export abstract class PolicyService {
|
||||
* @returns the first {@link Policy} found that applies to the active user.
|
||||
* A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
|
||||
* @param policyType the {@link PolicyType} to search for
|
||||
* @param policyFilter Optional predicate to apply when filtering policies
|
||||
* @see {@link getAll$} if you need all policies of a given type
|
||||
*/
|
||||
get$: (policyType: PolicyType, policyFilter?: (policy: Policy) => boolean) => Observable<Policy>;
|
||||
get$: (policyType: PolicyType) => Observable<Policy>;
|
||||
|
||||
/**
|
||||
* @returns all {@link Policy} objects of a given type that apply to the specified user (or the active user if not specified).
|
||||
* A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
|
||||
* @param policyType the {@link PolicyType} to search for
|
||||
*/
|
||||
getAll$: (policyType: PolicyType, userId?: UserId) => Observable<Policy[]>;
|
||||
|
||||
/**
|
||||
* All {@link Policy} objects for the specified user (from sync data).
|
||||
* May include policies that are disabled or otherwise do not apply to the user.
|
||||
* @see {@link policyAppliesToUser} if you want to know when a policy applies to the user.
|
||||
* @deprecated Use {@link policies$} instead
|
||||
* Consider using {@link getAll$} instead, which will only return policies that should be enforced against the user.
|
||||
*/
|
||||
getAll: (type?: PolicyType, userId?: string) => Promise<Policy[]>;
|
||||
getAll: (policyType: PolicyType) => Promise<Policy[]>;
|
||||
|
||||
/**
|
||||
* @returns true if the {@link PolicyType} applies to the current user, otherwise false.
|
||||
* @remarks A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
|
||||
* @returns true if a policy of the specified type applies to the active user, otherwise false.
|
||||
* A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
|
||||
* This does not take into account the policy's configuration - if that is important, use {@link getAll$} to get the
|
||||
* {@link Policy} objects and then filter by Policy.data.
|
||||
*/
|
||||
policyAppliesToActiveUser$: (
|
||||
policyType: PolicyType,
|
||||
policyFilter?: (policy: Policy) => boolean,
|
||||
) => Observable<boolean>;
|
||||
policyAppliesToActiveUser$: (policyType: PolicyType) => Observable<boolean>;
|
||||
|
||||
/**
|
||||
* @returns true if the {@link PolicyType} applies to the specified user, otherwise false.
|
||||
* @remarks A policy "applies" if it is enabled and the user is not exempt (e.g. because they are an Owner).
|
||||
* @see {@link policyAppliesToActiveUser$} if you only want to know about the current user.
|
||||
*/
|
||||
policyAppliesToUser: (
|
||||
policyType: PolicyType,
|
||||
policyFilter?: (policy: Policy) => boolean,
|
||||
userId?: string,
|
||||
) => Promise<boolean>;
|
||||
policyAppliesToUser: (policyType: PolicyType) => Promise<boolean>;
|
||||
|
||||
// Policy specific interfaces
|
||||
|
||||
@@ -93,7 +90,7 @@ export abstract class PolicyService {
|
||||
}
|
||||
|
||||
export abstract class InternalPolicyService extends PolicyService {
|
||||
upsert: (policy: PolicyData) => Promise<any>;
|
||||
upsert: (policy: PolicyData) => Promise<void>;
|
||||
replace: (policies: { [id: string]: PolicyData }) => Promise<void>;
|
||||
clear: (userId?: string) => Promise<any>;
|
||||
clear: (userId?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { PolicyId } from "../../../types/guid";
|
||||
import { PolicyType } from "../../enums";
|
||||
import { PolicyResponse } from "../response/policy.response";
|
||||
|
||||
export class PolicyData {
|
||||
id: string;
|
||||
id: PolicyId;
|
||||
organizationId: string;
|
||||
type: PolicyType;
|
||||
data: Record<string, string | number | boolean>;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Domain from "../../../platform/models/domain/domain-base";
|
||||
import { PolicyId } from "../../../types/guid";
|
||||
import { PolicyType } from "../../enums";
|
||||
import { PolicyData } from "../data/policy.data";
|
||||
|
||||
export class Policy extends Domain {
|
||||
id: string;
|
||||
id: PolicyId;
|
||||
organizationId: string;
|
||||
type: PolicyType;
|
||||
data: any;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { PolicyId } from "../../../types/guid";
|
||||
import { PolicyType } from "../../enums";
|
||||
|
||||
export class PolicyResponse extends BaseResponse {
|
||||
id: string;
|
||||
id: PolicyId;
|
||||
organizationId: string;
|
||||
type: PolicyType;
|
||||
data: any;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
||||
import { FakeActiveUserState } from "../../../../spec/fake-state";
|
||||
@@ -19,71 +19,51 @@ import { ResetPasswordPolicyOptions } from "../../../admin-console/models/domain
|
||||
import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
|
||||
import { POLICIES, PolicyService } from "../../../admin-console/services/policy/policy.service";
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||
import { ContainerService } from "../../../platform/services/container.service";
|
||||
import { StateService } from "../../../platform/services/state.service";
|
||||
import { PolicyId, UserId } from "../../../types/guid";
|
||||
|
||||
describe("PolicyService", () => {
|
||||
let policyService: PolicyService;
|
||||
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let activeAccount: BehaviorSubject<string>;
|
||||
let activeAccountUnlocked: BehaviorSubject<boolean>;
|
||||
let activeUserState: FakeActiveUserState<Record<PolicyId, PolicyData>>;
|
||||
|
||||
let policyService: PolicyService;
|
||||
|
||||
beforeEach(() => {
|
||||
stateService = mock<StateService>();
|
||||
|
||||
const accountService = mockAccountServiceWith("userId" as UserId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
organizationService = mock<OrganizationService>();
|
||||
organizationService.getAll
|
||||
.calledWith("user")
|
||||
.mockResolvedValue([
|
||||
new Organization(
|
||||
organizationData(
|
||||
"test-organization",
|
||||
true,
|
||||
true,
|
||||
OrganizationUserStatusType.Accepted,
|
||||
false,
|
||||
),
|
||||
),
|
||||
]);
|
||||
organizationService.getAll.calledWith(undefined).mockResolvedValue([]);
|
||||
organizationService.getAll.calledWith(null).mockResolvedValue([]);
|
||||
activeAccount = new BehaviorSubject("123");
|
||||
activeAccountUnlocked = new BehaviorSubject(true);
|
||||
stateService.getDecryptedPolicies.calledWith({ userId: "user" }).mockResolvedValue(null);
|
||||
stateService.getEncryptedPolicies.calledWith({ userId: "user" }).mockResolvedValue({
|
||||
"1": policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, {
|
||||
minutes: 14,
|
||||
}),
|
||||
});
|
||||
stateService.getEncryptedPolicies.mockResolvedValue({
|
||||
"1": policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, {
|
||||
minutes: 14,
|
||||
}),
|
||||
});
|
||||
stateService.activeAccount$ = activeAccount;
|
||||
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
|
||||
stateService.getUserId.mockResolvedValue("user");
|
||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
|
||||
|
||||
policyService = new PolicyService(stateService, stateProvider, organizationService);
|
||||
});
|
||||
activeUserState = stateProvider.activeUser.getFake(POLICIES);
|
||||
organizationService.organizations$ = of([
|
||||
// User
|
||||
organization("org1", true, true, OrganizationUserStatusType.Confirmed, false),
|
||||
// Owner
|
||||
organization(
|
||||
"org2",
|
||||
true,
|
||||
true,
|
||||
OrganizationUserStatusType.Confirmed,
|
||||
false,
|
||||
OrganizationUserType.Owner,
|
||||
),
|
||||
// Does not use policies
|
||||
organization("org3", true, false, OrganizationUserStatusType.Confirmed, false),
|
||||
// Another User
|
||||
organization("org4", true, true, OrganizationUserStatusType.Confirmed, false),
|
||||
// Another User
|
||||
organization("org5", true, true, OrganizationUserStatusType.Confirmed, false),
|
||||
]);
|
||||
|
||||
afterEach(() => {
|
||||
activeAccount.complete();
|
||||
activeAccountUnlocked.complete();
|
||||
policyService = new PolicyService(stateProvider, organizationService);
|
||||
});
|
||||
|
||||
it("upsert", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, { minutes: 14 }),
|
||||
]),
|
||||
);
|
||||
|
||||
await policyService.upsert(policyData("99", "test-organization", PolicyType.DisableSend, true));
|
||||
|
||||
expect(await firstValueFrom(policyService.policies$)).toEqual([
|
||||
@@ -104,6 +84,12 @@ describe("PolicyService", () => {
|
||||
});
|
||||
|
||||
it("replace", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, { minutes: 14 }),
|
||||
]),
|
||||
);
|
||||
|
||||
await policyService.replace({
|
||||
"2": policyData("2", "test-organization", PolicyType.DisableSend, true),
|
||||
});
|
||||
@@ -118,37 +104,63 @@ describe("PolicyService", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("locking should clear", async () => {
|
||||
activeAccountUnlocked.next(false);
|
||||
// Sleep for 100ms to avoid timing issues
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
expect((await firstValueFrom(policyService.policies$)).length).toBe(0);
|
||||
});
|
||||
|
||||
describe("clear", () => {
|
||||
it("null userId", async () => {
|
||||
beforeEach(() => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, {
|
||||
minutes: 14,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("clears state for the active user", async () => {
|
||||
await policyService.clear();
|
||||
|
||||
expect(stateService.setEncryptedPolicies).toBeCalledTimes(1);
|
||||
|
||||
expect((await firstValueFrom(policyService.policies$)).length).toBe(0);
|
||||
expect(await firstValueFrom(policyService.policies$)).toEqual([]);
|
||||
expect(await firstValueFrom(activeUserState.state$)).toEqual(null);
|
||||
expect(stateProvider.activeUser.getFake(POLICIES).nextMock).toHaveBeenCalledWith([
|
||||
"userId",
|
||||
null,
|
||||
]);
|
||||
});
|
||||
|
||||
it("matching userId", async () => {
|
||||
await policyService.clear("user");
|
||||
it("clears state for an inactive user", async () => {
|
||||
const inactiveUserId = "someOtherUserId" as UserId;
|
||||
const inactiveUserState = stateProvider.singleUser.getFake(inactiveUserId, POLICIES);
|
||||
inactiveUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("10", "another-test-organization", PolicyType.PersonalOwnership, true),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(stateService.setEncryptedPolicies).toBeCalledTimes(1);
|
||||
await policyService.clear(inactiveUserId);
|
||||
|
||||
expect((await firstValueFrom(policyService.policies$)).length).toBe(0);
|
||||
});
|
||||
// Active user is not affected
|
||||
const expectedActiveUserPolicy: Partial<Policy> = {
|
||||
id: "1" as PolicyId,
|
||||
organizationId: "test-organization",
|
||||
type: PolicyType.MaximumVaultTimeout,
|
||||
enabled: true,
|
||||
data: { minutes: 14 },
|
||||
};
|
||||
expect(await firstValueFrom(policyService.policies$)).toEqual([expectedActiveUserPolicy]);
|
||||
expect(await firstValueFrom(activeUserState.state$)).toEqual({
|
||||
"1": expectedActiveUserPolicy,
|
||||
});
|
||||
expect(stateProvider.activeUser.getFake(POLICIES).nextMock).not.toHaveBeenCalled();
|
||||
|
||||
it("mismatching userId", async () => {
|
||||
await policyService.clear("12");
|
||||
|
||||
expect(stateService.setEncryptedPolicies).toBeCalledTimes(1);
|
||||
|
||||
expect((await firstValueFrom(policyService.policies$)).length).toBe(1);
|
||||
// Non-active user is cleared
|
||||
expect(
|
||||
await firstValueFrom(
|
||||
policyService.getAll$(PolicyType.PersonalOwnership, "someOtherUserId" as UserId),
|
||||
),
|
||||
).toEqual([]);
|
||||
expect(await firstValueFrom(inactiveUserState.state$)).toEqual(null);
|
||||
expect(
|
||||
stateProvider.singleUser.getFake("someOtherUserId" as UserId, POLICIES).nextMock,
|
||||
).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -313,300 +325,260 @@ describe("PolicyService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("get$", () => {
|
||||
it("returns the specified PolicyType", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.get$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: "policy2",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not return disabled policies", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, false),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.get$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("does not return policies that do not apply to the user because the user's role is exempt", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy2", "org2", PolicyType.DisablePersonalVaultExport, false),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.get$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("does not return policies for organizations that do not use policies", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org3", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy2", "org2", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(policyService.get$(PolicyType.ActivateAutofill));
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAll$", () => {
|
||||
it("returns the specified PolicyTypes", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.getAll$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "policy1",
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy3",
|
||||
organizationId: "org5",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not return disabled policies", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, false), // disabled
|
||||
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.getAll$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "policy1",
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not return policies that do not apply to the user because the user's role is exempt", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy4", "org2", PolicyType.DisablePersonalVaultExport, true), // owner
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.getAll$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "policy1",
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy3",
|
||||
organizationId: "org5",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not return policies for organizations that do not use policies", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org3", PolicyType.DisablePersonalVaultExport, true), // does not use policies
|
||||
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.getAll$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "policy1",
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policyAppliesToActiveUser$", () => {
|
||||
it("MasterPassword does not apply", async () => {
|
||||
const result = await firstValueFrom(
|
||||
policyService.policyAppliesToActiveUser$(PolicyType.MasterPassword),
|
||||
it("returns true when the policyType applies to the user", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("MaximumVaultTimeout applies", async () => {
|
||||
const result = await firstValueFrom(
|
||||
policyService.policyAppliesToActiveUser$(PolicyType.MaximumVaultTimeout),
|
||||
);
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it("PolicyFilter filters result", async () => {
|
||||
const result = await firstValueFrom(
|
||||
policyService.policyAppliesToActiveUser$(PolicyType.MaximumVaultTimeout, (p) => false),
|
||||
);
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("DisablePersonalVaultExport does not apply", async () => {
|
||||
const result = await firstValueFrom(
|
||||
policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual(false);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policyAppliesToUser", () => {
|
||||
it("MasterPassword does not apply", async () => {
|
||||
const result = await policyService.policyAppliesToUser(
|
||||
PolicyType.MasterPassword,
|
||||
null,
|
||||
"user",
|
||||
it("returns false when policyType is disabled", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, false), // disabled
|
||||
]),
|
||||
);
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("MaximumVaultTimeout applies", async () => {
|
||||
const result = await policyService.policyAppliesToUser(
|
||||
PolicyType.MaximumVaultTimeout,
|
||||
null,
|
||||
"user",
|
||||
const result = await firstValueFrom(
|
||||
policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual(true);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("PolicyFilter filters result", async () => {
|
||||
const result = await policyService.policyAppliesToUser(
|
||||
PolicyType.MaximumVaultTimeout,
|
||||
(p) => false,
|
||||
"user",
|
||||
it("returns false when the policyType does not apply to the user because the user's role is exempt", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy4", "org2", PolicyType.DisablePersonalVaultExport, true), // owner
|
||||
]),
|
||||
);
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("DisablePersonalVaultExport does not apply", async () => {
|
||||
const result = await policyService.policyAppliesToUser(
|
||||
PolicyType.DisablePersonalVaultExport,
|
||||
null,
|
||||
"user",
|
||||
const result = await firstValueFrom(
|
||||
policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: remove this nesting once fully migrated to StateProvider
|
||||
describe("stateProvider methods", () => {
|
||||
let policyState$: FakeActiveUserState<Record<PolicyId, PolicyData>>;
|
||||
|
||||
beforeEach(() => {
|
||||
policyState$ = stateProvider.activeUser.getFake(POLICIES);
|
||||
organizationService.organizations$ = new BehaviorSubject([
|
||||
// User
|
||||
organization("org1", true, true, OrganizationUserStatusType.Confirmed, false),
|
||||
// Owner
|
||||
organization(
|
||||
"org2",
|
||||
true,
|
||||
true,
|
||||
OrganizationUserStatusType.Confirmed,
|
||||
false,
|
||||
OrganizationUserType.Owner,
|
||||
),
|
||||
// Does not use policies
|
||||
organization("org3", true, false, OrganizationUserStatusType.Confirmed, false),
|
||||
// Another User
|
||||
organization("org4", true, true, OrganizationUserStatusType.Confirmed, false),
|
||||
// Another User
|
||||
organization("org5", true, true, OrganizationUserStatusType.Confirmed, false),
|
||||
]);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
describe("get_vNext$", () => {
|
||||
it("returns the specified PolicyType", async () => {
|
||||
policyState$.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
it("returns false for organizations that do not use policies", async () => {
|
||||
activeUserState.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org3", PolicyType.DisablePersonalVaultExport, true), // does not use policies
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.get_vNext$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
const result = await firstValueFrom(
|
||||
policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: "policy2",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not return disabled policies", async () => {
|
||||
policyState$.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy2", "org1", PolicyType.DisablePersonalVaultExport, false),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.get_vNext$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("does not return policies that do not apply to the user because the user's role is exempt", async () => {
|
||||
policyState$.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy2", "org2", PolicyType.DisablePersonalVaultExport, false),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.get_vNext$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("does not return policies for organizations that do not use policies", async () => {
|
||||
policyState$.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org3", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy2", "org2", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(policyService.get_vNext$(PolicyType.ActivateAutofill));
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAll_vNext$", () => {
|
||||
it("returns the specified PolicyTypes", async () => {
|
||||
policyState$.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.getAll_vNext$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "policy1",
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy3",
|
||||
organizationId: "org5",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not return disabled policies", async () => {
|
||||
policyState$.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, false), // disabled
|
||||
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.getAll_vNext$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "policy1",
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not return policies that do not apply to the user because the user's role is exempt", async () => {
|
||||
policyState$.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org5", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy4", "org2", PolicyType.DisablePersonalVaultExport, true), // owner
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.getAll_vNext$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "policy1",
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy3",
|
||||
organizationId: "org5",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not return policies for organizations that do not use policies", async () => {
|
||||
policyState$.nextState(
|
||||
arrayToRecord([
|
||||
policyData("policy1", "org4", PolicyType.DisablePersonalVaultExport, true),
|
||||
policyData("policy2", "org1", PolicyType.ActivateAutofill, true),
|
||||
policyData("policy3", "org3", PolicyType.DisablePersonalVaultExport, true), // does not use policies
|
||||
policyData("policy4", "org1", PolicyType.DisablePersonalVaultExport, true),
|
||||
]),
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
policyService.getAll_vNext$(PolicyType.DisablePersonalVaultExport),
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "policy1",
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -618,7 +590,7 @@ describe("PolicyService", () => {
|
||||
data?: any,
|
||||
) {
|
||||
const policyData = new PolicyData({} as any);
|
||||
policyData.id = id;
|
||||
policyData.id = id as PolicyId;
|
||||
policyData.organizationId = organizationId;
|
||||
policyData.type = type;
|
||||
policyData.enabled = enabled;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { BehaviorSubject, combineLatest, concatMap, map, Observable, of } from "rxjs";
|
||||
import { combineLatest, firstValueFrom, map, Observable, of } from "rxjs";
|
||||
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { KeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state";
|
||||
import { PolicyId, UserId } from "../../../types/guid";
|
||||
import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction";
|
||||
@@ -23,42 +21,19 @@ export const POLICIES = KeyDefinition.record<PolicyData, PolicyId>(POLICIES_DISK
|
||||
});
|
||||
|
||||
export class PolicyService implements InternalPolicyServiceAbstraction {
|
||||
protected _policies: BehaviorSubject<Policy[]> = new BehaviorSubject([]);
|
||||
|
||||
policies$ = this._policies.asObservable();
|
||||
|
||||
private activeUserPolicyState = this.stateProvider.getActive(POLICIES);
|
||||
activeUserPolicies$ = this.activeUserPolicyState.state$.pipe(
|
||||
private activeUserPolicies$ = this.activeUserPolicyState.state$.pipe(
|
||||
map((policyData) => policyRecordToArray(policyData)),
|
||||
);
|
||||
|
||||
policies$ = this.activeUserPolicies$;
|
||||
|
||||
constructor(
|
||||
protected stateService: StateService,
|
||||
private stateProvider: StateProvider,
|
||||
private organizationService: OrganizationService,
|
||||
) {
|
||||
this.stateService.activeAccountUnlocked$
|
||||
.pipe(
|
||||
concatMap(async (unlocked) => {
|
||||
if (Utils.global.bitwardenContainerService == null) {
|
||||
return;
|
||||
}
|
||||
) {}
|
||||
|
||||
if (!unlocked) {
|
||||
this._policies.next([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await this.stateService.getEncryptedPolicies();
|
||||
|
||||
await this.updateObservables(data);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
// --- StateProvider methods - not yet wired up
|
||||
get_vNext$(policyType: PolicyType) {
|
||||
get$(policyType: PolicyType) {
|
||||
const filteredPolicies$ = this.activeUserPolicies$.pipe(
|
||||
map((policies) => policies.filter((p) => p.type === policyType)),
|
||||
);
|
||||
@@ -71,7 +46,7 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
getAll_vNext$(policyType: PolicyType, userId?: UserId) {
|
||||
getAll$(policyType: PolicyType, userId?: UserId) {
|
||||
const filteredPolicies$ = this.stateProvider.getUserState$(POLICIES, userId).pipe(
|
||||
map((policyData) => policyRecordToArray(policyData)),
|
||||
map((policies) => policies.filter((p) => p.type === policyType)),
|
||||
@@ -82,8 +57,18 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
policyAppliesToActiveUser_vNext$(policyType: PolicyType) {
|
||||
return this.get_vNext$(policyType).pipe(map((policy) => policy != null));
|
||||
async getAll(policyType: PolicyType) {
|
||||
return await firstValueFrom(
|
||||
this.policies$.pipe(map((policies) => policies.filter((p) => p.type === policyType))),
|
||||
);
|
||||
}
|
||||
|
||||
policyAppliesToActiveUser$(policyType: PolicyType) {
|
||||
return this.get$(policyType).pipe(map((policy) => policy != null));
|
||||
}
|
||||
|
||||
async policyAppliesToUser(policyType: PolicyType) {
|
||||
return await firstValueFrom(this.policyAppliesToActiveUser$(policyType));
|
||||
}
|
||||
|
||||
private enforcedPolicyFilter(policies: Policy[], organizations: Organization[]) {
|
||||
@@ -105,45 +90,6 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
|
||||
);
|
||||
});
|
||||
}
|
||||
// --- End StateProvider methods
|
||||
|
||||
get$(policyType: PolicyType, policyFilter?: (policy: Policy) => boolean): Observable<Policy> {
|
||||
return this.policies$.pipe(
|
||||
concatMap(async (policies) => {
|
||||
const userId = await this.stateService.getUserId();
|
||||
const appliesToCurrentUser = await this.checkPoliciesThatApplyToUser(
|
||||
policies,
|
||||
policyType,
|
||||
policyFilter,
|
||||
userId,
|
||||
);
|
||||
if (appliesToCurrentUser) {
|
||||
return policies.find((policy) => policy.type === policyType && policy.enabled);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getAll(type?: PolicyType, userId?: string): Promise<Policy[]> {
|
||||
let response: Policy[] = [];
|
||||
const decryptedPolicies = await this.stateService.getDecryptedPolicies({ userId: userId });
|
||||
if (decryptedPolicies != null) {
|
||||
response = decryptedPolicies;
|
||||
} else {
|
||||
const diskPolicies = await this.stateService.getEncryptedPolicies({ userId: userId });
|
||||
for (const id in diskPolicies) {
|
||||
if (Object.prototype.hasOwnProperty.call(diskPolicies, id)) {
|
||||
response.push(new Policy(diskPolicies[id]));
|
||||
}
|
||||
}
|
||||
await this.stateService.setDecryptedPolicies(response, { userId: userId });
|
||||
}
|
||||
if (type != null) {
|
||||
return response.filter((policy) => policy.type === type);
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
masterPasswordPolicyOptions$(policies?: Policy[]): Observable<MasterPasswordPolicyOptions> {
|
||||
const observable = policies ? of(policies) : this.policies$;
|
||||
@@ -205,15 +151,6 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
policyAppliesToActiveUser$(policyType: PolicyType, policyFilter?: (policy: Policy) => boolean) {
|
||||
return this.policies$.pipe(
|
||||
concatMap(async (policies) => {
|
||||
const userId = await this.stateService.getUserId();
|
||||
return await this.checkPoliciesThatApplyToUser(policies, policyType, policyFilter, userId);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
evaluateMasterPassword(
|
||||
passwordStrength: number,
|
||||
newPassword: string,
|
||||
@@ -288,68 +225,20 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
|
||||
return policiesResponse.data.map((response) => this.mapPolicyFromResponse(response));
|
||||
}
|
||||
|
||||
async policyAppliesToUser(
|
||||
policyType: PolicyType,
|
||||
policyFilter?: (policy: Policy) => boolean,
|
||||
userId?: string,
|
||||
) {
|
||||
const policies = await this.getAll(policyType, userId);
|
||||
|
||||
return this.checkPoliciesThatApplyToUser(policies, policyType, policyFilter, userId);
|
||||
}
|
||||
|
||||
async upsert(policy: PolicyData): Promise<any> {
|
||||
let policies = await this.stateService.getEncryptedPolicies();
|
||||
if (policies == null) {
|
||||
policies = {};
|
||||
}
|
||||
|
||||
policies[policy.id] = policy;
|
||||
|
||||
await this.updateObservables(policies);
|
||||
await this.stateService.setDecryptedPolicies(null);
|
||||
await this.stateService.setEncryptedPolicies(policies);
|
||||
async upsert(policy: PolicyData): Promise<void> {
|
||||
await this.activeUserPolicyState.update((policies) => {
|
||||
policies ??= {};
|
||||
policies[policy.id] = policy;
|
||||
return policies;
|
||||
});
|
||||
}
|
||||
|
||||
async replace(policies: { [id: string]: PolicyData }): Promise<void> {
|
||||
await this.updateObservables(policies);
|
||||
await this.stateService.setDecryptedPolicies(null);
|
||||
await this.stateService.setEncryptedPolicies(policies);
|
||||
await this.activeUserPolicyState.update(() => policies);
|
||||
}
|
||||
|
||||
async clear(userId?: string): Promise<void> {
|
||||
if (userId == null || userId == (await this.stateService.getUserId())) {
|
||||
this._policies.next([]);
|
||||
}
|
||||
await this.stateService.setDecryptedPolicies(null, { userId: userId });
|
||||
await this.stateService.setEncryptedPolicies(null, { userId: userId });
|
||||
}
|
||||
|
||||
private async updateObservables(policiesMap: { [id: string]: PolicyData }) {
|
||||
const policies = Object.values(policiesMap || {}).map((f) => new Policy(f));
|
||||
|
||||
this._policies.next(policies);
|
||||
}
|
||||
|
||||
private async checkPoliciesThatApplyToUser(
|
||||
policies: Policy[],
|
||||
policyType: PolicyType,
|
||||
policyFilter?: (policy: Policy) => boolean,
|
||||
userId?: string,
|
||||
) {
|
||||
const organizations = await this.organizationService.getAll(userId);
|
||||
const filteredPolicies = policies.filter(
|
||||
(p) => p.type === policyType && p.enabled && (policyFilter == null || policyFilter(p)),
|
||||
);
|
||||
const policySet = new Set(filteredPolicies.map((p) => p.organizationId));
|
||||
|
||||
return organizations.some(
|
||||
(o) =>
|
||||
o.status >= OrganizationUserStatusType.Accepted &&
|
||||
o.usePolicies &&
|
||||
policySet.has(o.id) &&
|
||||
!this.isExemptFromPolicy(policyType, o),
|
||||
);
|
||||
async clear(userId?: UserId): Promise<void> {
|
||||
await this.stateProvider.setUserState(POLICIES, null, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user