mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
[PM-6556] reintroduce policy reduction for multi-org accounts (#8409)
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
@@ -21,13 +23,16 @@ export abstract class GeneratorStrategy<Options, Policy> {
|
|||||||
/** Length of time in milliseconds to cache the evaluator */
|
/** Length of time in milliseconds to cache the evaluator */
|
||||||
cache_ms: number;
|
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.
|
* @param policy The policy being evaluated.
|
||||||
* @returns the policy evaluator. If `policy` is is `null` or `undefined`,
|
* @returns the policy evaluator. If `policy` is is `null` or `undefined`,
|
||||||
* then the evaluator defaults to the application's limits.
|
* then the evaluator defaults to the application's limits.
|
||||||
* @throws when the policy's type does not match the generator's policy type.
|
* @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.
|
/** Generates credentials from the given options.
|
||||||
* @param options The options used to generate the credentials.
|
* @param options The options used to generate the credentials.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
import { BehaviorSubject, firstValueFrom, map, pipe } from "rxjs";
|
||||||
|
|
||||||
import { FakeSingleUserState, awaitAsync } from "../../../spec";
|
import { FakeSingleUserState, awaitAsync } from "../../../spec";
|
||||||
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
@@ -20,12 +20,12 @@ import { PasswordGenerationOptions } from "./password";
|
|||||||
|
|
||||||
import { DefaultGeneratorService } from ".";
|
import { DefaultGeneratorService } from ".";
|
||||||
|
|
||||||
function mockPolicyService(config?: { state?: BehaviorSubject<Policy> }) {
|
function mockPolicyService(config?: { state?: BehaviorSubject<Policy[]> }) {
|
||||||
const service = mock<PolicyService>();
|
const service = mock<PolicyService>();
|
||||||
|
|
||||||
// FIXME: swap out the mock return value when `getAll$` becomes available
|
// FIXME: swap out the mock return value when `getAll$` becomes available
|
||||||
const stateValue = config?.state ?? new BehaviorSubject<Policy>(null);
|
const stateValue = config?.state ?? new BehaviorSubject<Policy[]>([null]);
|
||||||
service.get$.mockReturnValue(stateValue);
|
service.getAll$.mockReturnValue(stateValue);
|
||||||
|
|
||||||
// const stateValue = config?.state ?? new BehaviorSubject<Policy[]>(null);
|
// const stateValue = config?.state ?? new BehaviorSubject<Policy[]>(null);
|
||||||
// service.getAll$.mockReturnValue(stateValue);
|
// service.getAll$.mockReturnValue(stateValue);
|
||||||
@@ -46,7 +46,9 @@ function mockGeneratorStrategy(config?: {
|
|||||||
// the value from `config`.
|
// the value from `config`.
|
||||||
durableState: jest.fn(() => durableState),
|
durableState: jest.fn(() => durableState),
|
||||||
policy: config?.policy ?? PolicyType.DisableSend,
|
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;
|
return strategy;
|
||||||
@@ -94,9 +96,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.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser);
|
||||||
expect(policy.get$).toHaveBeenCalledWith(PolicyType.PasswordGenerator);
|
|
||||||
//expect(policy.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should map the policy using the generation strategy", async () => {
|
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 () => {
|
it("should update the evaluator when the password generator policy changes", async () => {
|
||||||
// set up dependencies
|
// set up dependencies
|
||||||
const state = new BehaviorSubject<Policy>(null);
|
const state = new BehaviorSubject<Policy[]>([null]);
|
||||||
const policy = mockPolicyService({ state });
|
const policy = mockPolicyService({ state });
|
||||||
const strategy = mockGeneratorStrategy();
|
const strategy = mockGeneratorStrategy();
|
||||||
const service = new DefaultGeneratorService(strategy, policy);
|
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>>();
|
const firstEvaluator = mock<PolicyEvaluator<any, any>>();
|
||||||
strategy.evaluator.mockReturnValueOnce(firstEvaluator);
|
|
||||||
const secondEvaluator = mock<PolicyEvaluator<any, any>>();
|
const secondEvaluator = mock<PolicyEvaluator<any, any>>();
|
||||||
strategy.evaluator.mockReturnValueOnce(secondEvaluator);
|
const evaluators = [firstEvaluator, secondEvaluator];
|
||||||
|
strategy.toEvaluator.mockReturnValueOnce(pipe(map(() => evaluators.shift())));
|
||||||
|
|
||||||
// act
|
// act
|
||||||
const evaluator$ = service.evaluator$(SomeUser);
|
const evaluator$ = service.evaluator$(SomeUser);
|
||||||
const firstResult = await firstValueFrom(evaluator$);
|
const firstResult = await firstValueFrom(evaluator$);
|
||||||
state.next(null);
|
state.next([null]);
|
||||||
const secondResult = await firstValueFrom(evaluator$);
|
const secondResult = await firstValueFrom(evaluator$);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
@@ -142,9 +143,7 @@ describe("Password generator service", () => {
|
|||||||
await firstValueFrom(service.evaluator$(SomeUser));
|
await firstValueFrom(service.evaluator$(SomeUser));
|
||||||
await firstValueFrom(service.evaluator$(SomeUser));
|
await firstValueFrom(service.evaluator$(SomeUser));
|
||||||
|
|
||||||
// FIXME: swap out the expect when `getAll$` becomes available
|
expect(policy.getAll$).toHaveBeenCalledTimes(1);
|
||||||
expect(policy.get$).toHaveBeenCalledTimes(1);
|
|
||||||
//expect(policy.getAll$).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should cache the password generator policy for each user", async () => {
|
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$(SomeUser));
|
||||||
await firstValueFrom(service.evaluator$(AnotherUser));
|
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(1, PolicyType.PasswordGenerator, SomeUser);
|
expect(policy.getAll$).toHaveBeenNthCalledWith(2, PolicyType.PasswordGenerator, AnotherUser);
|
||||||
// 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
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
@@ -44,14 +44,12 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createEvaluator(userId: UserId) {
|
private createEvaluator(userId: UserId) {
|
||||||
// FIXME: when it becomes possible to get a user-specific policy observable
|
const evaluator$ = this.policy.getAll$(this.strategy.policy, userId).pipe(
|
||||||
// (`getAll$`) update this code to call it instead of `get$`.
|
// create the evaluator from the policies
|
||||||
const policies$ = this.policy.get$(this.strategy.policy);
|
this.strategy.toEvaluator(),
|
||||||
|
|
||||||
// cache evaluator in a replay subject to amortize creation cost
|
// cache evaluator in a replay subject to amortize creation cost
|
||||||
// and reduce GC pressure.
|
// and reduce GC pressure.
|
||||||
const evaluator$ = policies$.pipe(
|
|
||||||
map((policy) => this.strategy.evaluator(policy)),
|
|
||||||
share({
|
share({
|
||||||
connector: () => new ReplaySubject(1),
|
connector: () => new ReplaySubject(1),
|
||||||
resetOnRefCountZero: () => timer(this.strategy.cache_ms),
|
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. */
|
/** Policy options enforced during passphrase generation. */
|
||||||
export type PassphraseGeneratorPolicy = {
|
export type PassphraseGeneratorPolicy = {
|
||||||
minNumberWords: number;
|
minNumberWords: number;
|
||||||
@@ -11,3 +16,24 @@ export const DisabledPassphraseGeneratorPolicy: PassphraseGeneratorPolicy = Obje
|
|||||||
capitalize: false,
|
capitalize: false,
|
||||||
includeNumber: 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 { mock } from "jest-mock-extended";
|
||||||
|
import { of, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
@@ -21,17 +22,8 @@ import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorStrategy } from
|
|||||||
const SomeUser = "some user" as UserId;
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Password generation strategy", () => {
|
describe("Password generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("toEvaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it("should map to the policy evaluator", async () => {
|
||||||
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", () => {
|
|
||||||
const strategy = new PassphraseGeneratorStrategy(null, null);
|
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.PasswordGenerator,
|
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).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator);
|
||||||
expect(evaluator.policy).toMatchObject({
|
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]])(
|
||||||
const strategy = new PassphraseGeneratorStrategy(null, null);
|
"should map `%p` to a disabled password policy evaluator",
|
||||||
const evaluator = strategy.evaluator(null);
|
async (policies) => {
|
||||||
|
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||||
|
|
||||||
expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator);
|
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||||
expect(evaluator.policy).toMatchObject(DisabledPassphraseGeneratorPolicy);
|
const evaluator = await firstValueFrom(evaluator$);
|
||||||
});
|
|
||||||
|
expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator);
|
||||||
|
expect(evaluator.policy).toMatchObject(DisabledPassphraseGeneratorPolicy);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("durableState", () => {
|
describe("durableState", () => {
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
|
import { map, pipe } from "rxjs";
|
||||||
|
|
||||||
import { GeneratorStrategy } from "..";
|
import { GeneratorStrategy } from "..";
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
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 { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
||||||
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
||||||
|
import { reduceCollection } from "../reduce-collection.operator";
|
||||||
|
|
||||||
import { PassphraseGenerationOptions } from "./passphrase-generation-options";
|
import { PassphraseGenerationOptions } from "./passphrase-generation-options";
|
||||||
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
||||||
import {
|
import {
|
||||||
DisabledPassphraseGeneratorPolicy,
|
DisabledPassphraseGeneratorPolicy,
|
||||||
PassphraseGeneratorPolicy,
|
PassphraseGeneratorPolicy,
|
||||||
|
leastPrivilege,
|
||||||
} from "./passphrase-generator-policy";
|
} from "./passphrase-generator-policy";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
const ONE_MINUTE = 60 * 1000;
|
||||||
@@ -23,6 +24,7 @@ export class PassphraseGeneratorStrategy
|
|||||||
{
|
{
|
||||||
/** instantiates the password generator strategy.
|
/** instantiates the password generator strategy.
|
||||||
* @param legacy generates the passphrase
|
* @param legacy generates the passphrase
|
||||||
|
* @param stateProvider provides durable state
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private legacy: PasswordGenerationServiceAbstraction,
|
private legacy: PasswordGenerationServiceAbstraction,
|
||||||
@@ -39,26 +41,17 @@ export class PassphraseGeneratorStrategy
|
|||||||
return PolicyType.PasswordGenerator;
|
return PolicyType.PasswordGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link GeneratorStrategy.cache_ms} */
|
||||||
get cache_ms() {
|
get cache_ms() {
|
||||||
return ONE_MINUTE;
|
return ONE_MINUTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.toEvaluator} */
|
||||||
evaluator(policy: Policy): PassphraseGeneratorOptionsEvaluator {
|
toEvaluator() {
|
||||||
if (!policy) {
|
return pipe(
|
||||||
return new PassphraseGeneratorOptionsEvaluator(DisabledPassphraseGeneratorPolicy);
|
reduceCollection(leastPrivilege, DisabledPassphraseGeneratorPolicy),
|
||||||
}
|
map((policy) => new PassphraseGeneratorOptionsEvaluator(policy)),
|
||||||
|
);
|
||||||
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.generate} */
|
/** {@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. */
|
/** Policy options enforced during password generation. */
|
||||||
export type PasswordGeneratorPolicy = {
|
export type PasswordGeneratorPolicy = {
|
||||||
/** The minimum length of generated passwords.
|
/** The minimum length of generated passwords.
|
||||||
@@ -48,3 +53,25 @@ export const DisabledPasswordGeneratorPolicy: PasswordGeneratorPolicy = Object.f
|
|||||||
useSpecial: false,
|
useSpecial: false,
|
||||||
specialCount: 0,
|
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 { mock } from "jest-mock-extended";
|
||||||
|
import { of, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
@@ -24,17 +25,8 @@ import {
|
|||||||
const SomeUser = "some user" as UserId;
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Password generation strategy", () => {
|
describe("Password generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("toEvaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it("should map to a password policy evaluator", async () => {
|
||||||
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", () => {
|
|
||||||
const strategy = new PasswordGeneratorStrategy(null, null);
|
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.PasswordGenerator,
|
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).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
|
||||||
expect(evaluator.policy).toMatchObject({
|
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]])(
|
||||||
const strategy = new PasswordGeneratorStrategy(null, null);
|
"should map `%p` to a disabled password policy evaluator",
|
||||||
const evaluator = strategy.evaluator(null);
|
async (policies) => {
|
||||||
|
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||||
|
|
||||||
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
|
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||||
expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy);
|
const evaluator = await firstValueFrom(evaluator$);
|
||||||
});
|
|
||||||
|
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
|
||||||
|
expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("durableState", () => {
|
describe("durableState", () => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
import { map, pipe } from "rxjs";
|
||||||
|
|
||||||
import { GeneratorStrategy } from "..";
|
import { GeneratorStrategy } from "..";
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
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 { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { PASSWORD_SETTINGS } from "../key-definitions";
|
import { PASSWORD_SETTINGS } from "../key-definitions";
|
||||||
|
import { reduceCollection } from "../reduce-collection.operator";
|
||||||
|
|
||||||
import { PasswordGenerationOptions } from "./password-generation-options";
|
import { PasswordGenerationOptions } from "./password-generation-options";
|
||||||
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
||||||
@@ -13,6 +13,7 @@ import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-
|
|||||||
import {
|
import {
|
||||||
DisabledPasswordGeneratorPolicy,
|
DisabledPasswordGeneratorPolicy,
|
||||||
PasswordGeneratorPolicy,
|
PasswordGeneratorPolicy,
|
||||||
|
leastPrivilege,
|
||||||
} from "./password-generator-policy";
|
} from "./password-generator-policy";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
const ONE_MINUTE = 60 * 1000;
|
||||||
@@ -43,26 +44,12 @@ export class PasswordGeneratorStrategy
|
|||||||
return ONE_MINUTE;
|
return ONE_MINUTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.toEvaluator} */
|
||||||
evaluator(policy: Policy): PasswordGeneratorOptionsEvaluator {
|
toEvaluator() {
|
||||||
if (!policy) {
|
return pipe(
|
||||||
return new PasswordGeneratorOptionsEvaluator(DisabledPasswordGeneratorPolicy);
|
reduceCollection(leastPrivilege, DisabledPasswordGeneratorPolicy),
|
||||||
}
|
map((policy) => new PasswordGeneratorOptionsEvaluator(policy)),
|
||||||
|
);
|
||||||
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.generate} */
|
/** {@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 { mock } from "jest-mock-extended";
|
||||||
|
import { of, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
@@ -12,39 +13,26 @@ import { CATCHALL_SETTINGS } from "../key-definitions";
|
|||||||
import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
const SomeUser = "some user" as UserId;
|
||||||
|
const SomePolicy = mock<Policy>({
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
data: {
|
||||||
|
minLength: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe("Email subaddress list generation strategy", () => {
|
describe("Email subaddress list generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("toEvaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])(
|
||||||
const strategy = new CatchallGeneratorStrategy(null, null);
|
"should map any input (= %p) to the default policy evaluator",
|
||||||
const policy = mock<Policy>({
|
async (policies) => {
|
||||||
type: PolicyType.DisableSend,
|
const strategy = new CatchallGeneratorStrategy(null, null);
|
||||||
});
|
|
||||||
|
|
||||||
expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+"));
|
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||||
});
|
const evaluator = await firstValueFrom(evaluator$);
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||||
const strategy = new CatchallGeneratorStrategy(null, null);
|
},
|
||||||
const policy = 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", () => {
|
|
||||||
const strategy = new CatchallGeneratorStrategy(null, null);
|
|
||||||
const evaluator = strategy.evaluator(null);
|
|
||||||
|
|
||||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("durableState", () => {
|
describe("durableState", () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { map, pipe } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
@@ -41,18 +42,9 @@ export class CatchallGeneratorStrategy
|
|||||||
return ONE_MINUTE;
|
return ONE_MINUTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.toEvaluator} */
|
||||||
evaluator(policy: Policy) {
|
toEvaluator() {
|
||||||
if (!policy) {
|
return pipe(map((_) => new DefaultPolicyEvaluator<CatchallGenerationOptions>()));
|
||||||
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.generate} */
|
/** {@link GeneratorStrategy.generate} */
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { of, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// 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 ".";
|
import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
const SomeUser = "some user" as UserId;
|
||||||
|
const SomePolicy = mock<Policy>({
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
data: {
|
||||||
|
minLength: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe("EFF long word list generation strategy", () => {
|
describe("EFF long word list generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("toEvaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])(
|
||||||
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
"should map any input (= %p) to the default policy evaluator",
|
||||||
const policy = mock<Policy>({
|
async (policies) => {
|
||||||
type: PolicyType.DisableSend,
|
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
||||||
});
|
|
||||||
|
|
||||||
expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+"));
|
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||||
});
|
const evaluator = await firstValueFrom(evaluator$);
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||||
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
},
|
||||||
const policy = 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", () => {
|
|
||||||
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
|
||||||
const evaluator = strategy.evaluator(null);
|
|
||||||
|
|
||||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("durableState", () => {
|
describe("durableState", () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { map, pipe } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
@@ -41,18 +42,9 @@ export class EffUsernameGeneratorStrategy
|
|||||||
return ONE_MINUTE;
|
return ONE_MINUTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.toEvaluator} */
|
||||||
evaluator(policy: Policy) {
|
toEvaluator() {
|
||||||
if (!policy) {
|
return pipe(map((_) => new DefaultPolicyEvaluator<EffUsernameGenerationOptions>()));
|
||||||
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.generate} */
|
/** {@link GeneratorStrategy.generate} */
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { of, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
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 { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
@@ -29,6 +34,12 @@ class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
|
|||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
const SomeUser = "some user" as UserId;
|
||||||
const AnotherUser = "another user" as UserId;
|
const AnotherUser = "another user" as UserId;
|
||||||
|
const SomePolicy = mock<Policy>({
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
data: {
|
||||||
|
minLength: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe("ForwarderGeneratorStrategy", () => {
|
describe("ForwarderGeneratorStrategy", () => {
|
||||||
const encryptService = mock<EncryptService>();
|
const encryptService = mock<EncryptService>();
|
||||||
@@ -63,11 +74,17 @@ describe("ForwarderGeneratorStrategy", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("evaluator returns the default policy evaluator", () => {
|
describe("toEvaluator()", () => {
|
||||||
const strategy = new TestForwarder(null, null, null);
|
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 { PolicyType } from "../../../admin-console/enums";
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
|
||||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||||
import { KeyDefinition, SingleUserState, StateProvider } from "../../../platform/state";
|
import { KeyDefinition, SingleUserState, StateProvider } from "../../../platform/state";
|
||||||
@@ -81,8 +82,8 @@ export abstract class ForwarderGeneratorStrategy<
|
|||||||
/** Determine where forwarder configuration is stored */
|
/** Determine where forwarder configuration is stored */
|
||||||
protected abstract readonly key: KeyDefinition<Options>;
|
protected abstract readonly key: KeyDefinition<Options>;
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.toEvaluator} */
|
||||||
evaluator = (_policy: Policy) => {
|
toEvaluator = () => {
|
||||||
return new DefaultPolicyEvaluator<Options>();
|
return pipe(map((_) => new DefaultPolicyEvaluator<Options>()));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { of, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
@@ -12,39 +13,26 @@ import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
|||||||
import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
const SomeUser = "some user" as UserId;
|
||||||
|
const SomePolicy = mock<Policy>({
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
data: {
|
||||||
|
minLength: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe("Email subaddress list generation strategy", () => {
|
describe("Email subaddress list generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("toEvaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])(
|
||||||
const strategy = new SubaddressGeneratorStrategy(null, null);
|
"should map any input (= %p) to the default policy evaluator",
|
||||||
const policy = mock<Policy>({
|
async (policies) => {
|
||||||
type: PolicyType.DisableSend,
|
const strategy = new SubaddressGeneratorStrategy(null, null);
|
||||||
});
|
|
||||||
|
|
||||||
expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+"));
|
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||||
});
|
const evaluator = await firstValueFrom(evaluator$);
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||||
const strategy = new SubaddressGeneratorStrategy(null, null);
|
},
|
||||||
const policy = 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", () => {
|
|
||||||
const strategy = new SubaddressGeneratorStrategy(null, null);
|
|
||||||
const evaluator = strategy.evaluator(null);
|
|
||||||
|
|
||||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("durableState", () => {
|
describe("durableState", () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { map, pipe } from "rxjs";
|
||||||
|
|
||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
|
||||||
import { StateProvider } from "../../../platform/state";
|
import { StateProvider } from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
@@ -41,18 +42,9 @@ export class SubaddressGeneratorStrategy
|
|||||||
return ONE_MINUTE;
|
return ONE_MINUTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.toEvaluator} */
|
||||||
evaluator(policy: Policy) {
|
toEvaluator() {
|
||||||
if (!policy) {
|
return pipe(map((_) => new DefaultPolicyEvaluator<SubaddressGenerationOptions>()));
|
||||||
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.generate} */
|
/** {@link GeneratorStrategy.generate} */
|
||||||
|
|||||||
Reference in New Issue
Block a user