mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-22 16:29:09 +01:00
[PM-6556] reintroduce policy reduction for multi-org accounts (#8409)
This commit is contained in:
parent
da14d01062
commit
d000f081da
@ -1,3 +1,5 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
@ -21,13 +23,16 @@ export abstract class GeneratorStrategy<Options, Policy> {
|
||||
/** Length of time in milliseconds to cache the evaluator */
|
||||
cache_ms: number;
|
||||
|
||||
/** Creates an evaluator from a generator policy.
|
||||
/** Operator function that converts a policy collection observable to a single
|
||||
* policy evaluator observable.
|
||||
* @param policy The policy being evaluated.
|
||||
* @returns the policy evaluator. If `policy` is is `null` or `undefined`,
|
||||
* then the evaluator defaults to the application's limits.
|
||||
* @throws when the policy's type does not match the generator's policy type.
|
||||
*/
|
||||
evaluator: (policy: AdminPolicy) => PolicyEvaluator<Policy, Options>;
|
||||
toEvaluator: () => (
|
||||
source: Observable<AdminPolicy[]>,
|
||||
) => Observable<PolicyEvaluator<Policy, Options>>;
|
||||
|
||||
/** Generates credentials from the given options.
|
||||
* @param options The options used to generate the credentials.
|
||||
|
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
import { BehaviorSubject, firstValueFrom, map, pipe } from "rxjs";
|
||||
|
||||
import { FakeSingleUserState, awaitAsync } from "../../../spec";
|
||||
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@ -20,12 +20,12 @@ import { PasswordGenerationOptions } from "./password";
|
||||
|
||||
import { DefaultGeneratorService } from ".";
|
||||
|
||||
function mockPolicyService(config?: { state?: BehaviorSubject<Policy> }) {
|
||||
function mockPolicyService(config?: { state?: BehaviorSubject<Policy[]> }) {
|
||||
const service = mock<PolicyService>();
|
||||
|
||||
// FIXME: swap out the mock return value when `getAll$` becomes available
|
||||
const stateValue = config?.state ?? new BehaviorSubject<Policy>(null);
|
||||
service.get$.mockReturnValue(stateValue);
|
||||
const stateValue = config?.state ?? new BehaviorSubject<Policy[]>([null]);
|
||||
service.getAll$.mockReturnValue(stateValue);
|
||||
|
||||
// const stateValue = config?.state ?? new BehaviorSubject<Policy[]>(null);
|
||||
// service.getAll$.mockReturnValue(stateValue);
|
||||
@ -46,7 +46,9 @@ function mockGeneratorStrategy(config?: {
|
||||
// the value from `config`.
|
||||
durableState: jest.fn(() => durableState),
|
||||
policy: config?.policy ?? PolicyType.DisableSend,
|
||||
evaluator: jest.fn(() => config?.evaluator ?? mock<PolicyEvaluator<any, any>>()),
|
||||
toEvaluator: jest.fn(() =>
|
||||
pipe(map(() => config?.evaluator ?? mock<PolicyEvaluator<any, any>>())),
|
||||
),
|
||||
});
|
||||
|
||||
return strategy;
|
||||
@ -94,9 +96,7 @@ describe("Password generator service", () => {
|
||||
|
||||
await firstValueFrom(service.evaluator$(SomeUser));
|
||||
|
||||
// FIXME: swap out the expect when `getAll$` becomes available
|
||||
expect(policy.get$).toHaveBeenCalledWith(PolicyType.PasswordGenerator);
|
||||
//expect(policy.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser);
|
||||
expect(policy.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser);
|
||||
});
|
||||
|
||||
it("should map the policy using the generation strategy", async () => {
|
||||
@ -112,21 +112,22 @@ describe("Password generator service", () => {
|
||||
|
||||
it("should update the evaluator when the password generator policy changes", async () => {
|
||||
// set up dependencies
|
||||
const state = new BehaviorSubject<Policy>(null);
|
||||
const state = new BehaviorSubject<Policy[]>([null]);
|
||||
const policy = mockPolicyService({ state });
|
||||
const strategy = mockGeneratorStrategy();
|
||||
const service = new DefaultGeneratorService(strategy, policy);
|
||||
|
||||
// model responses for the observable update
|
||||
// model responses for the observable update. The map is called multiple times,
|
||||
// and the array shift ensures reference equality is maintained.
|
||||
const firstEvaluator = mock<PolicyEvaluator<any, any>>();
|
||||
strategy.evaluator.mockReturnValueOnce(firstEvaluator);
|
||||
const secondEvaluator = mock<PolicyEvaluator<any, any>>();
|
||||
strategy.evaluator.mockReturnValueOnce(secondEvaluator);
|
||||
const evaluators = [firstEvaluator, secondEvaluator];
|
||||
strategy.toEvaluator.mockReturnValueOnce(pipe(map(() => evaluators.shift())));
|
||||
|
||||
// act
|
||||
const evaluator$ = service.evaluator$(SomeUser);
|
||||
const firstResult = await firstValueFrom(evaluator$);
|
||||
state.next(null);
|
||||
state.next([null]);
|
||||
const secondResult = await firstValueFrom(evaluator$);
|
||||
|
||||
// assert
|
||||
@ -142,9 +143,7 @@ describe("Password generator service", () => {
|
||||
await firstValueFrom(service.evaluator$(SomeUser));
|
||||
await firstValueFrom(service.evaluator$(SomeUser));
|
||||
|
||||
// FIXME: swap out the expect when `getAll$` becomes available
|
||||
expect(policy.get$).toHaveBeenCalledTimes(1);
|
||||
//expect(policy.getAll$).toHaveBeenCalledTimes(1);
|
||||
expect(policy.getAll$).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should cache the password generator policy for each user", async () => {
|
||||
@ -155,9 +154,8 @@ describe("Password generator service", () => {
|
||||
await firstValueFrom(service.evaluator$(SomeUser));
|
||||
await firstValueFrom(service.evaluator$(AnotherUser));
|
||||
|
||||
// FIXME: enable this test when `getAll$` becomes available
|
||||
// expect(policy.getAll$).toHaveBeenNthCalledWith(1, PolicyType.PasswordGenerator, SomeUser);
|
||||
// expect(policy.getAll$).toHaveBeenNthCalledWith(2, PolicyType.PasswordGenerator, AnotherUser);
|
||||
expect(policy.getAll$).toHaveBeenNthCalledWith(1, PolicyType.PasswordGenerator, SomeUser);
|
||||
expect(policy.getAll$).toHaveBeenNthCalledWith(2, PolicyType.PasswordGenerator, AnotherUser);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { firstValueFrom, map, share, timer, ReplaySubject, Observable } from "rxjs";
|
||||
import { firstValueFrom, share, timer, ReplaySubject, Observable } from "rxjs";
|
||||
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
@ -44,14 +44,12 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic
|
||||
}
|
||||
|
||||
private createEvaluator(userId: UserId) {
|
||||
// FIXME: when it becomes possible to get a user-specific policy observable
|
||||
// (`getAll$`) update this code to call it instead of `get$`.
|
||||
const policies$ = this.policy.get$(this.strategy.policy);
|
||||
const evaluator$ = this.policy.getAll$(this.strategy.policy, userId).pipe(
|
||||
// create the evaluator from the policies
|
||||
this.strategy.toEvaluator(),
|
||||
|
||||
// cache evaluator in a replay subject to amortize creation cost
|
||||
// and reduce GC pressure.
|
||||
const evaluator$ = policies$.pipe(
|
||||
map((policy) => this.strategy.evaluator(policy)),
|
||||
share({
|
||||
connector: () => new ReplaySubject(1),
|
||||
resetOnRefCountZero: () => timer(this.strategy.cache_ms),
|
||||
|
@ -0,0 +1,51 @@
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { PolicyId } from "../../../types/guid";
|
||||
|
||||
import { DisabledPassphraseGeneratorPolicy, leastPrivilege } from "./passphrase-generator-policy";
|
||||
|
||||
function createPolicy(
|
||||
data: any,
|
||||
type: PolicyType = PolicyType.PasswordGenerator,
|
||||
enabled: boolean = true,
|
||||
) {
|
||||
return new Policy({
|
||||
id: "id" as PolicyId,
|
||||
organizationId: "organizationId",
|
||||
data,
|
||||
enabled,
|
||||
type,
|
||||
});
|
||||
}
|
||||
|
||||
describe("leastPrivilege", () => {
|
||||
it("should return the accumulator when the policy type does not apply", () => {
|
||||
const policy = createPolicy({}, PolicyType.RequireSso);
|
||||
|
||||
const result = leastPrivilege(DisabledPassphraseGeneratorPolicy, policy);
|
||||
|
||||
expect(result).toEqual(DisabledPassphraseGeneratorPolicy);
|
||||
});
|
||||
|
||||
it("should return the accumulator when the policy is not enabled", () => {
|
||||
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
|
||||
|
||||
const result = leastPrivilege(DisabledPassphraseGeneratorPolicy, policy);
|
||||
|
||||
expect(result).toEqual(DisabledPassphraseGeneratorPolicy);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["minNumberWords", 10],
|
||||
["capitalize", true],
|
||||
["includeNumber", true],
|
||||
])("should take the %p from the policy", (input, value) => {
|
||||
const policy = createPolicy({ [input]: value });
|
||||
|
||||
const result = leastPrivilege(DisabledPassphraseGeneratorPolicy, policy);
|
||||
|
||||
expect(result).toEqual({ ...DisabledPassphraseGeneratorPolicy, [input]: value });
|
||||
});
|
||||
});
|
@ -1,3 +1,8 @@
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
|
||||
/** Policy options enforced during passphrase generation. */
|
||||
export type PassphraseGeneratorPolicy = {
|
||||
minNumberWords: number;
|
||||
@ -11,3 +16,24 @@ export const DisabledPassphraseGeneratorPolicy: PassphraseGeneratorPolicy = Obje
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
});
|
||||
|
||||
/** Reduces a policy into an accumulator by accepting the most restrictive
|
||||
* values from each policy.
|
||||
* @param acc the accumulator
|
||||
* @param policy the policy to reduce
|
||||
* @returns the most restrictive values between the policy and accumulator.
|
||||
*/
|
||||
export function leastPrivilege(
|
||||
acc: PassphraseGeneratorPolicy,
|
||||
policy: Policy,
|
||||
): PassphraseGeneratorPolicy {
|
||||
if (policy.type !== PolicyType.PasswordGenerator) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return {
|
||||
minNumberWords: Math.max(acc.minNumberWords, policy.data.minNumberWords ?? acc.minNumberWords),
|
||||
capitalize: policy.data.capitalize || acc.capitalize,
|
||||
includeNumber: policy.data.includeNumber || acc.includeNumber,
|
||||
};
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of, firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
@ -21,17 +22,8 @@ import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorStrategy } from
|
||||
const SomeUser = "some user" as UserId;
|
||||
|
||||
describe("Password generation strategy", () => {
|
||||
describe("evaluator()", () => {
|
||||
it("should throw if the policy type is incorrect", () => {
|
||||
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.DisableSend,
|
||||
});
|
||||
|
||||
expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+"));
|
||||
});
|
||||
|
||||
it("should map to the policy evaluator", () => {
|
||||
describe("toEvaluator()", () => {
|
||||
it("should map to the policy evaluator", async () => {
|
||||
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
@ -42,7 +34,8 @@ describe("Password generation strategy", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const evaluator = strategy.evaluator(policy);
|
||||
const evaluator$ = of([policy]).pipe(strategy.toEvaluator());
|
||||
const evaluator = await firstValueFrom(evaluator$);
|
||||
|
||||
expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator);
|
||||
expect(evaluator.policy).toMatchObject({
|
||||
@ -52,13 +45,18 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should map `null` to a default policy evaluator", () => {
|
||||
it.each([[[]], [null], [undefined]])(
|
||||
"should map `%p` to a disabled password policy evaluator",
|
||||
async (policies) => {
|
||||
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||
const evaluator = strategy.evaluator(null);
|
||||
|
||||
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||
const evaluator = await firstValueFrom(evaluator$);
|
||||
|
||||
expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator);
|
||||
expect(evaluator.policy).toMatchObject(DisabledPassphraseGeneratorPolicy);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("durableState", () => {
|
||||
|
@ -1,18 +1,19 @@
|
||||
import { map, pipe } from "rxjs";
|
||||
|
||||
import { GeneratorStrategy } from "..";
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
||||
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
||||
import { reduceCollection } from "../reduce-collection.operator";
|
||||
|
||||
import { PassphraseGenerationOptions } from "./passphrase-generation-options";
|
||||
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
||||
import {
|
||||
DisabledPassphraseGeneratorPolicy,
|
||||
PassphraseGeneratorPolicy,
|
||||
leastPrivilege,
|
||||
} from "./passphrase-generator-policy";
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
@ -23,6 +24,7 @@ export class PassphraseGeneratorStrategy
|
||||
{
|
||||
/** instantiates the password generator strategy.
|
||||
* @param legacy generates the passphrase
|
||||
* @param stateProvider provides durable state
|
||||
*/
|
||||
constructor(
|
||||
private legacy: PasswordGenerationServiceAbstraction,
|
||||
@ -39,26 +41,17 @@ export class PassphraseGeneratorStrategy
|
||||
return PolicyType.PasswordGenerator;
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.cache_ms} */
|
||||
get cache_ms() {
|
||||
return ONE_MINUTE;
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.evaluator} */
|
||||
evaluator(policy: Policy): PassphraseGeneratorOptionsEvaluator {
|
||||
if (!policy) {
|
||||
return new PassphraseGeneratorOptionsEvaluator(DisabledPassphraseGeneratorPolicy);
|
||||
}
|
||||
|
||||
if (policy.type !== this.policy) {
|
||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||
throw Error("Mismatched policy type. " + details);
|
||||
}
|
||||
|
||||
return new PassphraseGeneratorOptionsEvaluator({
|
||||
minNumberWords: policy.data.minNumberWords,
|
||||
capitalize: policy.data.capitalize,
|
||||
includeNumber: policy.data.includeNumber,
|
||||
});
|
||||
/** {@link GeneratorStrategy.toEvaluator} */
|
||||
toEvaluator() {
|
||||
return pipe(
|
||||
reduceCollection(leastPrivilege, DisabledPassphraseGeneratorPolicy),
|
||||
map((policy) => new PassphraseGeneratorOptionsEvaluator(policy)),
|
||||
);
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.generate} */
|
||||
|
@ -0,0 +1,55 @@
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { PolicyId } from "../../../types/guid";
|
||||
|
||||
import { DisabledPasswordGeneratorPolicy, leastPrivilege } from "./password-generator-policy";
|
||||
|
||||
function createPolicy(
|
||||
data: any,
|
||||
type: PolicyType = PolicyType.PasswordGenerator,
|
||||
enabled: boolean = true,
|
||||
) {
|
||||
return new Policy({
|
||||
id: "id" as PolicyId,
|
||||
organizationId: "organizationId",
|
||||
data,
|
||||
enabled,
|
||||
type,
|
||||
});
|
||||
}
|
||||
|
||||
describe("leastPrivilege", () => {
|
||||
it("should return the accumulator when the policy type does not apply", () => {
|
||||
const policy = createPolicy({}, PolicyType.RequireSso);
|
||||
|
||||
const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy);
|
||||
|
||||
expect(result).toEqual(DisabledPasswordGeneratorPolicy);
|
||||
});
|
||||
|
||||
it("should return the accumulator when the policy is not enabled", () => {
|
||||
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
|
||||
|
||||
const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy);
|
||||
|
||||
expect(result).toEqual(DisabledPasswordGeneratorPolicy);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["minLength", 10, "minLength"],
|
||||
["useUpper", true, "useUppercase"],
|
||||
["useLower", true, "useLowercase"],
|
||||
["useNumbers", true, "useNumbers"],
|
||||
["minNumbers", 10, "numberCount"],
|
||||
["useSpecial", true, "useSpecial"],
|
||||
["minSpecial", 10, "specialCount"],
|
||||
])("should take the %p from the policy", (input, value, expected) => {
|
||||
const policy = createPolicy({ [input]: value });
|
||||
|
||||
const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy);
|
||||
|
||||
expect(result).toEqual({ ...DisabledPasswordGeneratorPolicy, [expected]: value });
|
||||
});
|
||||
});
|
@ -1,3 +1,8 @@
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
|
||||
/** Policy options enforced during password generation. */
|
||||
export type PasswordGeneratorPolicy = {
|
||||
/** The minimum length of generated passwords.
|
||||
@ -48,3 +53,25 @@ export const DisabledPasswordGeneratorPolicy: PasswordGeneratorPolicy = Object.f
|
||||
useSpecial: false,
|
||||
specialCount: 0,
|
||||
});
|
||||
|
||||
/** Reduces a policy into an accumulator by accepting the most restrictive
|
||||
* values from each policy.
|
||||
* @param acc the accumulator
|
||||
* @param policy the policy to reduce
|
||||
* @returns the most restrictive values between the policy and accumulator.
|
||||
*/
|
||||
export function leastPrivilege(acc: PasswordGeneratorPolicy, policy: Policy) {
|
||||
if (policy.type !== PolicyType.PasswordGenerator || !policy.enabled) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return {
|
||||
minLength: Math.max(acc.minLength, policy.data.minLength ?? acc.minLength),
|
||||
useUppercase: policy.data.useUpper || acc.useUppercase,
|
||||
useLowercase: policy.data.useLower || acc.useLowercase,
|
||||
useNumbers: policy.data.useNumbers || acc.useNumbers,
|
||||
numberCount: Math.max(acc.numberCount, policy.data.minNumbers ?? acc.numberCount),
|
||||
useSpecial: policy.data.useSpecial || acc.useSpecial,
|
||||
specialCount: Math.max(acc.specialCount, policy.data.minSpecial ?? acc.specialCount),
|
||||
};
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of, firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
@ -24,17 +25,8 @@ import {
|
||||
const SomeUser = "some user" as UserId;
|
||||
|
||||
describe("Password generation strategy", () => {
|
||||
describe("evaluator()", () => {
|
||||
it("should throw if the policy type is incorrect", () => {
|
||||
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.DisableSend,
|
||||
});
|
||||
|
||||
expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+"));
|
||||
});
|
||||
|
||||
it("should map to the policy evaluator", () => {
|
||||
describe("toEvaluator()", () => {
|
||||
it("should map to a password policy evaluator", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
@ -49,7 +41,8 @@ describe("Password generation strategy", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const evaluator = strategy.evaluator(policy);
|
||||
const evaluator$ = of([policy]).pipe(strategy.toEvaluator());
|
||||
const evaluator = await firstValueFrom(evaluator$);
|
||||
|
||||
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
|
||||
expect(evaluator.policy).toMatchObject({
|
||||
@ -63,13 +56,18 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should map `null` to a default policy evaluator", () => {
|
||||
it.each([[[]], [null], [undefined]])(
|
||||
"should map `%p` to a disabled password policy evaluator",
|
||||
async (policies) => {
|
||||
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||
const evaluator = strategy.evaluator(null);
|
||||
|
||||
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||
const evaluator = await firstValueFrom(evaluator$);
|
||||
|
||||
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
|
||||
expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("durableState", () => {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { map, pipe } from "rxjs";
|
||||
|
||||
import { GeneratorStrategy } from "..";
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { PASSWORD_SETTINGS } from "../key-definitions";
|
||||
import { reduceCollection } from "../reduce-collection.operator";
|
||||
|
||||
import { PasswordGenerationOptions } from "./password-generation-options";
|
||||
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
||||
@ -13,6 +13,7 @@ import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-
|
||||
import {
|
||||
DisabledPasswordGeneratorPolicy,
|
||||
PasswordGeneratorPolicy,
|
||||
leastPrivilege,
|
||||
} from "./password-generator-policy";
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
@ -43,26 +44,12 @@ export class PasswordGeneratorStrategy
|
||||
return ONE_MINUTE;
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.evaluator} */
|
||||
evaluator(policy: Policy): PasswordGeneratorOptionsEvaluator {
|
||||
if (!policy) {
|
||||
return new PasswordGeneratorOptionsEvaluator(DisabledPasswordGeneratorPolicy);
|
||||
}
|
||||
|
||||
if (policy.type !== this.policy) {
|
||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||
throw Error("Mismatched policy type. " + details);
|
||||
}
|
||||
|
||||
return new PasswordGeneratorOptionsEvaluator({
|
||||
minLength: policy.data.minLength,
|
||||
useUppercase: policy.data.useUpper,
|
||||
useLowercase: policy.data.useLower,
|
||||
useNumbers: policy.data.useNumbers,
|
||||
numberCount: policy.data.minNumbers,
|
||||
useSpecial: policy.data.useSpecial,
|
||||
specialCount: policy.data.minSpecial,
|
||||
});
|
||||
/** {@link GeneratorStrategy.toEvaluator} */
|
||||
toEvaluator() {
|
||||
return pipe(
|
||||
reduceCollection(leastPrivilege, DisabledPasswordGeneratorPolicy),
|
||||
map((policy) => new PasswordGeneratorOptionsEvaluator(policy)),
|
||||
);
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.generate} */
|
||||
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* include structuredClone in test environment.
|
||||
* @jest-environment ../../../../shared/test.environment.ts
|
||||
*/
|
||||
|
||||
import { of, firstValueFrom } from "rxjs";
|
||||
|
||||
import { reduceCollection } from "./reduce-collection.operator";
|
||||
|
||||
describe("reduceCollection", () => {
|
||||
it.each([[null], [undefined], [[]]])(
|
||||
"should return the default value when the collection is %p",
|
||||
async (value: number[]) => {
|
||||
const reduce = (acc: number, value: number) => acc + value;
|
||||
const source$ = of(value);
|
||||
|
||||
const result$ = source$.pipe(reduceCollection(reduce, 100));
|
||||
const result = await firstValueFrom(result$);
|
||||
|
||||
expect(result).toEqual(100);
|
||||
},
|
||||
);
|
||||
|
||||
it("should reduce the collection to a single value", async () => {
|
||||
const reduce = (acc: number, value: number) => acc + value;
|
||||
const source$ = of([1, 2, 3]);
|
||||
|
||||
const result$ = source$.pipe(reduceCollection(reduce, 0));
|
||||
const result = await firstValueFrom(result$);
|
||||
|
||||
expect(result).toEqual(6);
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
import { map, OperatorFunction } from "rxjs";
|
||||
|
||||
/**
|
||||
* An observable operator that reduces an emitted collection to a single object,
|
||||
* returning a default if all items are ignored.
|
||||
* @param reduce The reduce function to apply to the filtered collection. The
|
||||
* first argument is the accumulator, and the second is the current item. The
|
||||
* return value is the new accumulator.
|
||||
* @param defaultValue The default value to return if the collection is empty. The
|
||||
* default value is also the initial value of the accumulator.
|
||||
*/
|
||||
export function reduceCollection<Item, Accumulator>(
|
||||
reduce: (acc: Accumulator, value: Item) => Accumulator,
|
||||
defaultValue: Accumulator,
|
||||
): OperatorFunction<Item[], Accumulator> {
|
||||
return map((values: Item[]) => {
|
||||
const reduced = (values ?? []).reduce(reduce, structuredClone(defaultValue));
|
||||
return reduced;
|
||||
});
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of, firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
@ -12,39 +13,26 @@ import { CATCHALL_SETTINGS } from "../key-definitions";
|
||||
import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
|
||||
describe("Email subaddress list generation strategy", () => {
|
||||
describe("evaluator()", () => {
|
||||
it("should throw if the policy type is incorrect", () => {
|
||||
const strategy = new CatchallGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.DisableSend,
|
||||
});
|
||||
|
||||
expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+"));
|
||||
});
|
||||
|
||||
it("should map to the policy evaluator", () => {
|
||||
const strategy = new CatchallGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
const SomePolicy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
minLength: 10,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const evaluator = strategy.evaluator(policy);
|
||||
|
||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||
expect(evaluator.policy).toMatchObject({});
|
||||
});
|
||||
|
||||
it("should map `null` to a default policy evaluator", () => {
|
||||
describe("Email subaddress list generation strategy", () => {
|
||||
describe("toEvaluator()", () => {
|
||||
it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])(
|
||||
"should map any input (= %p) to the default policy evaluator",
|
||||
async (policies) => {
|
||||
const strategy = new CatchallGeneratorStrategy(null, null);
|
||||
const evaluator = strategy.evaluator(null);
|
||||
|
||||
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||
const evaluator = await firstValueFrom(evaluator$);
|
||||
|
||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("durableState", () => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { map, pipe } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
@ -41,18 +42,9 @@ export class CatchallGeneratorStrategy
|
||||
return ONE_MINUTE;
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.evaluator} */
|
||||
evaluator(policy: Policy) {
|
||||
if (!policy) {
|
||||
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
|
||||
}
|
||||
|
||||
if (policy.type !== this.policy) {
|
||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||
throw Error("Mismatched policy type. " + details);
|
||||
}
|
||||
|
||||
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
|
||||
/** {@link GeneratorStrategy.toEvaluator} */
|
||||
toEvaluator() {
|
||||
return pipe(map((_) => new DefaultPolicyEvaluator<CatchallGenerationOptions>()));
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.generate} */
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of, firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
@ -12,39 +13,26 @@ import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||
import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
|
||||
describe("EFF long word list generation strategy", () => {
|
||||
describe("evaluator()", () => {
|
||||
it("should throw if the policy type is incorrect", () => {
|
||||
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.DisableSend,
|
||||
});
|
||||
|
||||
expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+"));
|
||||
});
|
||||
|
||||
it("should map to the policy evaluator", () => {
|
||||
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
const SomePolicy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
minLength: 10,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const evaluator = strategy.evaluator(policy);
|
||||
|
||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||
expect(evaluator.policy).toMatchObject({});
|
||||
});
|
||||
|
||||
it("should map `null` to a default policy evaluator", () => {
|
||||
describe("EFF long word list generation strategy", () => {
|
||||
describe("toEvaluator()", () => {
|
||||
it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])(
|
||||
"should map any input (= %p) to the default policy evaluator",
|
||||
async (policies) => {
|
||||
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
||||
const evaluator = strategy.evaluator(null);
|
||||
|
||||
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||
const evaluator = await firstValueFrom(evaluator$);
|
||||
|
||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("durableState", () => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { map, pipe } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
@ -41,18 +42,9 @@ export class EffUsernameGeneratorStrategy
|
||||
return ONE_MINUTE;
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.evaluator} */
|
||||
evaluator(policy: Policy) {
|
||||
if (!policy) {
|
||||
return new DefaultPolicyEvaluator<EffUsernameGenerationOptions>();
|
||||
}
|
||||
|
||||
if (policy.type !== this.policy) {
|
||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||
throw Error("Mismatched policy type. " + details);
|
||||
}
|
||||
|
||||
return new DefaultPolicyEvaluator<EffUsernameGenerationOptions>();
|
||||
/** {@link GeneratorStrategy.toEvaluator} */
|
||||
toEvaluator() {
|
||||
return pipe(map((_) => new DefaultPolicyEvaluator<EffUsernameGenerationOptions>()));
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.generate} */
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of, firstValueFrom } from "rxjs";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
@ -29,6 +34,12 @@ class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
const AnotherUser = "another user" as UserId;
|
||||
const SomePolicy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
minLength: 10,
|
||||
},
|
||||
});
|
||||
|
||||
describe("ForwarderGeneratorStrategy", () => {
|
||||
const encryptService = mock<EncryptService>();
|
||||
@ -63,11 +74,17 @@ describe("ForwarderGeneratorStrategy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("evaluator returns the default policy evaluator", () => {
|
||||
const strategy = new TestForwarder(null, null, null);
|
||||
describe("toEvaluator()", () => {
|
||||
it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])(
|
||||
"should map any input (= %p) to the default policy evaluator",
|
||||
async (policies) => {
|
||||
const strategy = new TestForwarder(encryptService, keyService, stateProvider);
|
||||
|
||||
const result = strategy.evaluator(null);
|
||||
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||
const evaluator = await firstValueFrom(evaluator$);
|
||||
|
||||
expect(result).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { map, pipe } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||
import { KeyDefinition, SingleUserState, StateProvider } from "../../../platform/state";
|
||||
@ -81,8 +82,8 @@ export abstract class ForwarderGeneratorStrategy<
|
||||
/** Determine where forwarder configuration is stored */
|
||||
protected abstract readonly key: KeyDefinition<Options>;
|
||||
|
||||
/** {@link GeneratorStrategy.evaluator} */
|
||||
evaluator = (_policy: Policy) => {
|
||||
return new DefaultPolicyEvaluator<Options>();
|
||||
/** {@link GeneratorStrategy.toEvaluator} */
|
||||
toEvaluator = () => {
|
||||
return pipe(map((_) => new DefaultPolicyEvaluator<Options>()));
|
||||
};
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of, firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
@ -12,39 +13,26 @@ import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||
import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
|
||||
describe("Email subaddress list generation strategy", () => {
|
||||
describe("evaluator()", () => {
|
||||
it("should throw if the policy type is incorrect", () => {
|
||||
const strategy = new SubaddressGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.DisableSend,
|
||||
});
|
||||
|
||||
expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+"));
|
||||
});
|
||||
|
||||
it("should map to the policy evaluator", () => {
|
||||
const strategy = new SubaddressGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
const SomePolicy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
minLength: 10,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const evaluator = strategy.evaluator(policy);
|
||||
|
||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||
expect(evaluator.policy).toMatchObject({});
|
||||
});
|
||||
|
||||
it("should map `null` to a default policy evaluator", () => {
|
||||
describe("Email subaddress list generation strategy", () => {
|
||||
describe("toEvaluator()", () => {
|
||||
it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])(
|
||||
"should map any input (= %p) to the default policy evaluator",
|
||||
async (policies) => {
|
||||
const strategy = new SubaddressGeneratorStrategy(null, null);
|
||||
const evaluator = strategy.evaluator(null);
|
||||
|
||||
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||
const evaluator = await firstValueFrom(evaluator$);
|
||||
|
||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("durableState", () => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { map, pipe } from "rxjs";
|
||||
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
@ -41,18 +42,9 @@ export class SubaddressGeneratorStrategy
|
||||
return ONE_MINUTE;
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.evaluator} */
|
||||
evaluator(policy: Policy) {
|
||||
if (!policy) {
|
||||
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
|
||||
}
|
||||
|
||||
if (policy.type !== this.policy) {
|
||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||
throw Error("Mismatched policy type. " + details);
|
||||
}
|
||||
|
||||
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
|
||||
/** {@link GeneratorStrategy.toEvaluator} */
|
||||
toEvaluator() {
|
||||
return pipe(map((_) => new DefaultPolicyEvaluator<SubaddressGenerationOptions>()));
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.generate} */
|
||||
|
Loading…
Reference in New Issue
Block a user