mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-11 13:30:39 +01:00
[PM-6523] generator service tuning (#8155)
* rename policy$ to evaluator$ * replace `ActiveUserState` with `SingleUserState` * implement `SingleUserState<T>` on `SecretState`
This commit is contained in:
parent
bf6fd39f15
commit
d87a8f9271
@ -2,4 +2,5 @@ export * from "./utils";
|
|||||||
export * from "./intercept-console";
|
export * from "./intercept-console";
|
||||||
export * from "./matchers";
|
export * from "./matchers";
|
||||||
export * from "./fake-state-provider";
|
export * from "./fake-state-provider";
|
||||||
|
export * from "./fake-state";
|
||||||
export * from "./fake-account-service";
|
export * from "./fake-account-service";
|
||||||
|
@ -4,7 +4,7 @@ export { DerivedState } from "./derived-state";
|
|||||||
export { GlobalState } from "./global-state";
|
export { GlobalState } from "./global-state";
|
||||||
export { StateProvider } from "./state.provider";
|
export { StateProvider } from "./state.provider";
|
||||||
export { GlobalStateProvider } from "./global-state.provider";
|
export { GlobalStateProvider } from "./global-state.provider";
|
||||||
export { ActiveUserState, SingleUserState } from "./user-state";
|
export { ActiveUserState, SingleUserState, CombinedState } from "./user-state";
|
||||||
export { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider";
|
export { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider";
|
||||||
export { KeyDefinition } from "./key-definition";
|
export { KeyDefinition } from "./key-definition";
|
||||||
export { StateUpdateOptions } from "./state-update-options";
|
export { StateUpdateOptions } from "./state-update-options";
|
||||||
|
@ -2,14 +2,18 @@ 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
|
||||||
import { Policy as AdminPolicy } from "../../../admin-console/models/domain/policy";
|
import { Policy as AdminPolicy } from "../../../admin-console/models/domain/policy";
|
||||||
import { KeyDefinition } from "../../../platform/state";
|
import { SingleUserState } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
|
|
||||||
import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
||||||
|
|
||||||
/** Tailors the generator service to generate a specific kind of credentials */
|
/** Tailors the generator service to generate a specific kind of credentials */
|
||||||
export abstract class GeneratorStrategy<Options, Policy> {
|
export abstract class GeneratorStrategy<Options, Policy> {
|
||||||
/** The key used when storing credentials on disk. */
|
/** Retrieve application state that persists across locks.
|
||||||
disk: KeyDefinition<Options>;
|
* @param userId: identifies the user state to retrieve
|
||||||
|
* @returns the strategy's durable user state
|
||||||
|
*/
|
||||||
|
durableState: (userId: UserId) => SingleUserState<Options>;
|
||||||
|
|
||||||
/** Identifies the policy enforced by the generator. */
|
/** Identifies the policy enforced by the generator. */
|
||||||
policy: PolicyType;
|
policy: PolicyType;
|
||||||
@ -19,7 +23,8 @@ export abstract class GeneratorStrategy<Options, Policy> {
|
|||||||
|
|
||||||
/** Creates an evaluator from a generator policy.
|
/** Creates an evaluator from a generator policy.
|
||||||
* @param policy The policy being evaluated.
|
* @param policy The policy being evaluated.
|
||||||
* @returns the policy evaluator.
|
* @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.
|
* @throws when the policy's type does not match the generator's policy type.
|
||||||
*/
|
*/
|
||||||
evaluator: (policy: AdminPolicy) => PolicyEvaluator<Policy, Options>;
|
evaluator: (policy: AdminPolicy) => PolicyEvaluator<Policy, Options>;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
|
|
||||||
import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
||||||
|
|
||||||
/** Generates credentials used for user authentication
|
/** Generates credentials used for user authentication
|
||||||
@ -9,19 +11,22 @@ import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
|||||||
export abstract class GeneratorService<Options, Policy> {
|
export abstract class GeneratorService<Options, Policy> {
|
||||||
/** An observable monitoring the options saved to disk.
|
/** An observable monitoring the options saved to disk.
|
||||||
* The observable updates when the options are saved.
|
* The observable updates when the options are saved.
|
||||||
|
* @param userId: Identifies the user making the request
|
||||||
*/
|
*/
|
||||||
options$: Observable<Options>;
|
options$: (userId: UserId) => Observable<Options>;
|
||||||
|
|
||||||
/** An observable monitoring the options used to enforce policy.
|
/** An observable monitoring the options used to enforce policy.
|
||||||
* The observable updates when the policy changes.
|
* The observable updates when the policy changes.
|
||||||
|
* @param userId: Identifies the user making the request
|
||||||
*/
|
*/
|
||||||
policy$: Observable<PolicyEvaluator<Policy, Options>>;
|
evaluator$: (userId: UserId) => Observable<PolicyEvaluator<Policy, Options>>;
|
||||||
|
|
||||||
/** Enforces the policy on the given options
|
/** Enforces the policy on the given options
|
||||||
|
* @param userId: Identifies the user making the request
|
||||||
* @param options the options to enforce the policy on
|
* @param options the options to enforce the policy on
|
||||||
* @returns a new instance of the options with the policy enforced
|
* @returns a new instance of the options with the policy enforced
|
||||||
*/
|
*/
|
||||||
enforcePolicy: (options: Options) => Promise<Options>;
|
enforcePolicy: (userId: UserId, options: Options) => Promise<Options>;
|
||||||
|
|
||||||
/** Generates credentials
|
/** Generates credentials
|
||||||
* @param options the options to generate credentials with
|
* @param options the options to generate credentials with
|
||||||
@ -30,8 +35,9 @@ export abstract class GeneratorService<Options, Policy> {
|
|||||||
generate: (options: Options) => Promise<string>;
|
generate: (options: Options) => Promise<string>;
|
||||||
|
|
||||||
/** Saves the given options to disk.
|
/** Saves the given options to disk.
|
||||||
|
* @param userId: Identifies the user making the request
|
||||||
* @param options the options to save
|
* @param options the options to save
|
||||||
* @returns a promise that resolves when the options are saved
|
* @returns a promise that resolves when the options are saved
|
||||||
*/
|
*/
|
||||||
saveOptions: (options: Options) => Promise<void>;
|
saveOptions: (userId: UserId, options: Options) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -6,42 +6,45 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { FakeActiveUserStateProvider, mockAccountServiceWith } 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";
|
||||||
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
|
||||||
import { Policy } from "../../admin-console/models/domain/policy";
|
import { Policy } from "../../admin-console/models/domain/policy";
|
||||||
import { Utils } from "../../platform/misc/utils";
|
import { SingleUserState } from "../../platform/state";
|
||||||
import { ActiveUserState, ActiveUserStateProvider, KeyDefinition } from "../../platform/state";
|
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
|
|
||||||
import { GeneratorStrategy, PolicyEvaluator } from "./abstractions";
|
import { GeneratorStrategy, PolicyEvaluator } from "./abstractions";
|
||||||
import { PASSPHRASE_SETTINGS, PASSWORD_SETTINGS } from "./key-definitions";
|
|
||||||
import { PasswordGenerationOptions } from "./password";
|
import { PasswordGenerationOptions } from "./password";
|
||||||
|
|
||||||
import { DefaultGeneratorService } from ".";
|
import { DefaultGeneratorService } from ".";
|
||||||
|
|
||||||
function mockPolicyService(config?: { data?: any; policy?: BehaviorSubject<Policy> }) {
|
function mockPolicyService(config?: { state?: BehaviorSubject<Policy> }) {
|
||||||
const state = mock<Policy>({ data: config?.data ?? {} });
|
|
||||||
const subject = config?.policy ?? new BehaviorSubject<Policy>(state);
|
|
||||||
|
|
||||||
const service = mock<PolicyService>();
|
const service = mock<PolicyService>();
|
||||||
service.get$.mockReturnValue(subject.asObservable());
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mockGeneratorStrategy(config?: {
|
function mockGeneratorStrategy(config?: {
|
||||||
disk?: KeyDefinition<any>;
|
userState?: SingleUserState<any>;
|
||||||
policy?: PolicyType;
|
policy?: PolicyType;
|
||||||
evaluator?: any;
|
evaluator?: any;
|
||||||
}) {
|
}) {
|
||||||
|
const durableState =
|
||||||
|
config?.userState ?? new FakeSingleUserState<PasswordGenerationOptions>(SomeUser);
|
||||||
const strategy = mock<GeneratorStrategy<any, any>>({
|
const strategy = mock<GeneratorStrategy<any, any>>({
|
||||||
// intentionally arbitrary so that tests that need to check
|
// intentionally arbitrary so that tests that need to check
|
||||||
// whether they're used properly are guaranteed to test
|
// whether they're used properly are guaranteed to test
|
||||||
// the value from `config`.
|
// the value from `config`.
|
||||||
disk: config?.disk ?? {},
|
durableState: jest.fn(() => durableState),
|
||||||
policy: config?.policy ?? PolicyType.DisableSend,
|
policy: config?.policy ?? PolicyType.DisableSend,
|
||||||
evaluator: jest.fn(() => config?.evaluator ?? mock<PolicyEvaluator<any, any>>()),
|
evaluator: jest.fn(() => config?.evaluator ?? mock<PolicyEvaluator<any, any>>()),
|
||||||
});
|
});
|
||||||
@ -49,129 +52,123 @@ function mockGeneratorStrategy(config?: {
|
|||||||
return strategy;
|
return strategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Use the fake instead, once it's updated to monitor its method calls.
|
const SomeUser = "some user" as UserId;
|
||||||
function mockStateProvider(): [
|
const AnotherUser = "another user" as UserId;
|
||||||
ActiveUserStateProvider,
|
|
||||||
ActiveUserState<PasswordGenerationOptions>,
|
|
||||||
] {
|
|
||||||
const state = mock<ActiveUserState<PasswordGenerationOptions>>();
|
|
||||||
const provider = mock<ActiveUserStateProvider>();
|
|
||||||
provider.get.mockReturnValue(state);
|
|
||||||
|
|
||||||
return [provider, state];
|
|
||||||
}
|
|
||||||
|
|
||||||
function fakeStateProvider(key: KeyDefinition<any>, initalValue: any): FakeActiveUserStateProvider {
|
|
||||||
const userId = Utils.newGuid() as UserId;
|
|
||||||
const acctService = mockAccountServiceWith(userId);
|
|
||||||
const provider = new FakeActiveUserStateProvider(acctService);
|
|
||||||
provider.mockFor(key.key, initalValue);
|
|
||||||
return provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Password generator service", () => {
|
describe("Password generator service", () => {
|
||||||
describe("constructor()", () => {
|
|
||||||
it("should initialize the password generator policy", () => {
|
|
||||||
const policy = mockPolicyService();
|
|
||||||
const strategy = mockGeneratorStrategy({ policy: PolicyType.PasswordGenerator });
|
|
||||||
|
|
||||||
new DefaultGeneratorService(strategy, policy, null);
|
|
||||||
|
|
||||||
expect(policy.get$).toHaveBeenCalledWith(PolicyType.PasswordGenerator);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("options$", () => {
|
describe("options$", () => {
|
||||||
it("should return the state from strategy.key", () => {
|
it("should retrieve durable state from the service", () => {
|
||||||
const policy = mockPolicyService();
|
const policy = mockPolicyService();
|
||||||
const strategy = mockGeneratorStrategy({ disk: PASSPHRASE_SETTINGS });
|
const userState = new FakeSingleUserState<PasswordGenerationOptions>(SomeUser);
|
||||||
const [state] = mockStateProvider();
|
const strategy = mockGeneratorStrategy({ userState });
|
||||||
const service = new DefaultGeneratorService(strategy, policy, state);
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
// invoke the getter. It returns the state but that's not important.
|
const result = service.options$(SomeUser);
|
||||||
service.options$;
|
|
||||||
|
|
||||||
expect(state.get).toHaveBeenCalledWith(PASSPHRASE_SETTINGS);
|
expect(strategy.durableState).toHaveBeenCalledWith(SomeUser);
|
||||||
|
expect(result).toBe(userState.state$);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("saveOptions()", () => {
|
describe("saveOptions()", () => {
|
||||||
it("should update the state at strategy.key", async () => {
|
|
||||||
const policy = mockPolicyService();
|
|
||||||
const [provider, state] = mockStateProvider();
|
|
||||||
const strategy = mockGeneratorStrategy({ disk: PASSWORD_SETTINGS });
|
|
||||||
const service = new DefaultGeneratorService(strategy, policy, provider);
|
|
||||||
|
|
||||||
await service.saveOptions({});
|
|
||||||
|
|
||||||
expect(provider.get).toHaveBeenCalledWith(PASSWORD_SETTINGS);
|
|
||||||
expect(state.update).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should trigger an options$ update", async () => {
|
it("should trigger an options$ update", async () => {
|
||||||
const policy = mockPolicyService();
|
const policy = mockPolicyService();
|
||||||
const strategy = mockGeneratorStrategy();
|
const userState = new FakeSingleUserState<PasswordGenerationOptions>(SomeUser, { length: 9 });
|
||||||
// using the fake here because we're testing that the update and the
|
const strategy = mockGeneratorStrategy({ userState });
|
||||||
// property are wired together. If we were to mock that, we'd be testing
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
// the mock configuration instead of the wiring.
|
|
||||||
const provider = fakeStateProvider(strategy.disk, { length: 9 });
|
|
||||||
const service = new DefaultGeneratorService(strategy, policy, provider);
|
|
||||||
|
|
||||||
await service.saveOptions({ length: 10 });
|
await service.saveOptions(SomeUser, { length: 10 });
|
||||||
|
await awaitAsync();
|
||||||
|
const options = await firstValueFrom(service.options$(SomeUser));
|
||||||
|
|
||||||
const options = await firstValueFrom(service.options$);
|
expect(strategy.durableState).toHaveBeenCalledWith(SomeUser);
|
||||||
expect(options).toEqual({ length: 10 });
|
expect(options).toEqual({ length: 10 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("policy$", () => {
|
describe("evaluator$", () => {
|
||||||
|
it("should initialize the password generator policy", async () => {
|
||||||
|
const policy = mockPolicyService();
|
||||||
|
const strategy = mockGeneratorStrategy({ policy: PolicyType.PasswordGenerator });
|
||||||
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
it("should map the policy using the generation strategy", async () => {
|
it("should map the policy using the generation strategy", async () => {
|
||||||
const policyService = mockPolicyService();
|
const policyService = mockPolicyService();
|
||||||
const evaluator = mock<PolicyEvaluator<any, any>>();
|
const evaluator = mock<PolicyEvaluator<any, any>>();
|
||||||
const strategy = mockGeneratorStrategy({ evaluator });
|
const strategy = mockGeneratorStrategy({ evaluator });
|
||||||
|
const service = new DefaultGeneratorService(strategy, policyService);
|
||||||
|
|
||||||
const service = new DefaultGeneratorService(strategy, policyService, null);
|
const policy = await firstValueFrom(service.evaluator$(SomeUser));
|
||||||
|
|
||||||
const policy = await firstValueFrom(service.policy$);
|
|
||||||
|
|
||||||
expect(policy).toBe(evaluator);
|
expect(policy).toBe(evaluator);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should update the evaluator when the password generator policy changes", async () => {
|
||||||
|
// set up dependencies
|
||||||
|
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
|
||||||
|
const firstEvaluator = mock<PolicyEvaluator<any, any>>();
|
||||||
|
strategy.evaluator.mockReturnValueOnce(firstEvaluator);
|
||||||
|
const secondEvaluator = mock<PolicyEvaluator<any, any>>();
|
||||||
|
strategy.evaluator.mockReturnValueOnce(secondEvaluator);
|
||||||
|
|
||||||
|
// act
|
||||||
|
const evaluator$ = service.evaluator$(SomeUser);
|
||||||
|
const firstResult = await firstValueFrom(evaluator$);
|
||||||
|
state.next(null);
|
||||||
|
const secondResult = await firstValueFrom(evaluator$);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(firstResult).toBe(firstEvaluator);
|
||||||
|
expect(secondResult).toBe(secondEvaluator);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cache the password generator policy", async () => {
|
||||||
|
const policy = mockPolicyService();
|
||||||
|
const strategy = mockGeneratorStrategy({ policy: PolicyType.PasswordGenerator });
|
||||||
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cache the password generator policy for each user", async () => {
|
||||||
|
const policy = mockPolicyService();
|
||||||
|
const strategy = mockGeneratorStrategy({ policy: PolicyType.PasswordGenerator });
|
||||||
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("enforcePolicy()", () => {
|
describe("enforcePolicy()", () => {
|
||||||
describe("should load the policy", () => {
|
|
||||||
it("from the cache by default", async () => {
|
|
||||||
const policy = mockPolicyService();
|
|
||||||
const strategy = mockGeneratorStrategy();
|
|
||||||
const service = new DefaultGeneratorService(strategy, policy, null);
|
|
||||||
|
|
||||||
await service.enforcePolicy({});
|
|
||||||
await service.enforcePolicy({});
|
|
||||||
|
|
||||||
expect(strategy.evaluator).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("from the policy service when the policy changes", async () => {
|
|
||||||
const policy = new BehaviorSubject<Policy>(mock<Policy>({ data: {} }));
|
|
||||||
const policyService = mockPolicyService({ policy });
|
|
||||||
const strategy = mockGeneratorStrategy();
|
|
||||||
const service = new DefaultGeneratorService(strategy, policyService, null);
|
|
||||||
|
|
||||||
await service.enforcePolicy({});
|
|
||||||
policy.next(mock<Policy>({ data: { some: "change" } }));
|
|
||||||
await service.enforcePolicy({});
|
|
||||||
|
|
||||||
expect(strategy.evaluator).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should evaluate the policy using the generation strategy", async () => {
|
it("should evaluate the policy using the generation strategy", async () => {
|
||||||
const policy = mockPolicyService();
|
const policy = mockPolicyService();
|
||||||
const evaluator = mock<PolicyEvaluator<any, any>>();
|
const evaluator = mock<PolicyEvaluator<any, any>>();
|
||||||
const strategy = mockGeneratorStrategy({ evaluator });
|
const strategy = mockGeneratorStrategy({ evaluator });
|
||||||
const service = new DefaultGeneratorService(strategy, policy, null);
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
await service.enforcePolicy({});
|
await service.enforcePolicy(SomeUser, {});
|
||||||
|
|
||||||
expect(evaluator.applyPolicy).toHaveBeenCalled();
|
expect(evaluator.applyPolicy).toHaveBeenCalled();
|
||||||
expect(evaluator.sanitize).toHaveBeenCalled();
|
expect(evaluator.sanitize).toHaveBeenCalled();
|
||||||
@ -182,7 +179,7 @@ describe("Password generator service", () => {
|
|||||||
it("should invoke the generation strategy", async () => {
|
it("should invoke the generation strategy", async () => {
|
||||||
const strategy = mockGeneratorStrategy();
|
const strategy = mockGeneratorStrategy();
|
||||||
const policy = mockPolicyService();
|
const policy = mockPolicyService();
|
||||||
const service = new DefaultGeneratorService(strategy, policy, null);
|
const service = new DefaultGeneratorService(strategy, policy);
|
||||||
|
|
||||||
await service.generate({});
|
await service.generate({});
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { firstValueFrom, map, share, timer, ReplaySubject, Observable } from "rx
|
|||||||
// 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
|
||||||
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { ActiveUserStateProvider } from "../../platform/state";
|
import { UserId } from "../../types/guid";
|
||||||
|
|
||||||
import { GeneratorStrategy, GeneratorService, PolicyEvaluator } from "./abstractions";
|
import { GeneratorStrategy, GeneratorService, PolicyEvaluator } from "./abstractions";
|
||||||
|
|
||||||
@ -13,45 +13,57 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic
|
|||||||
* @param strategy tailors the service to a specific generator type
|
* @param strategy tailors the service to a specific generator type
|
||||||
* (e.g. password, passphrase)
|
* (e.g. password, passphrase)
|
||||||
* @param policy provides the policy to enforce
|
* @param policy provides the policy to enforce
|
||||||
* @param state saves and loads password generation options to the location
|
|
||||||
* specified by the strategy
|
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private strategy: GeneratorStrategy<Options, Policy>,
|
private strategy: GeneratorStrategy<Options, Policy>,
|
||||||
private policy: PolicyService,
|
private policy: PolicyService,
|
||||||
private state: ActiveUserStateProvider,
|
) {}
|
||||||
) {
|
|
||||||
this._policy$ = this.policy.get$(this.strategy.policy).pipe(
|
private _evaluators$ = new Map<UserId, Observable<PolicyEvaluator<Policy, Options>>>();
|
||||||
|
|
||||||
|
/** {@link GeneratorService.options$()} */
|
||||||
|
options$(userId: UserId) {
|
||||||
|
return this.strategy.durableState(userId).state$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@link GeneratorService.saveOptions} */
|
||||||
|
async saveOptions(userId: UserId, options: Options): Promise<void> {
|
||||||
|
await this.strategy.durableState(userId).update(() => options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@link GeneratorService.evaluator$()} */
|
||||||
|
evaluator$(userId: UserId) {
|
||||||
|
let evaluator$ = this._evaluators$.get(userId);
|
||||||
|
|
||||||
|
if (!evaluator$) {
|
||||||
|
evaluator$ = this.createEvaluator(userId);
|
||||||
|
this._evaluators$.set(userId, evaluator$);
|
||||||
|
}
|
||||||
|
|
||||||
|
return evaluator$;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// cache evaluator in a replay subject to amortize creation cost
|
||||||
|
// and reduce GC pressure.
|
||||||
|
const evaluator$ = policies$.pipe(
|
||||||
map((policy) => this.strategy.evaluator(policy)),
|
map((policy) => this.strategy.evaluator(policy)),
|
||||||
share({
|
share({
|
||||||
// cache evaluator in a replay subject to amortize creation cost
|
|
||||||
// and reduce GC pressure.
|
|
||||||
connector: () => new ReplaySubject(1),
|
connector: () => new ReplaySubject(1),
|
||||||
resetOnRefCountZero: () => timer(this.strategy.cache_ms),
|
resetOnRefCountZero: () => timer(this.strategy.cache_ms),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return evaluator$;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _policy$: Observable<PolicyEvaluator<Policy, Options>>;
|
/** {@link GeneratorService.enforcePolicy()} */
|
||||||
|
async enforcePolicy(userId: UserId, options: Options): Promise<Options> {
|
||||||
/** {@link GeneratorService.options$} */
|
const policy = await firstValueFrom(this.evaluator$(userId));
|
||||||
get options$() {
|
|
||||||
return this.state.get(this.strategy.disk).state$;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorService.saveOptions} */
|
|
||||||
async saveOptions(options: Options): Promise<void> {
|
|
||||||
await this.state.get(this.strategy.disk).update(() => options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorService.policy$} */
|
|
||||||
get policy$() {
|
|
||||||
return this._policy$;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@link GeneratorService.enforcePolicy} */
|
|
||||||
async enforcePolicy(options: Options): Promise<Options> {
|
|
||||||
const policy = await firstValueFrom(this._policy$);
|
|
||||||
const evaluated = policy.applyPolicy(options);
|
const evaluated = policy.applyPolicy(options);
|
||||||
const sanitized = policy.sanitize(evaluated);
|
const sanitized = policy.sanitize(evaluated);
|
||||||
return sanitized;
|
return sanitized;
|
||||||
|
@ -9,15 +9,21 @@ 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
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
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 { PASSPHRASE_SETTINGS } from "../key-definitions";
|
||||||
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
||||||
|
|
||||||
|
import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy";
|
||||||
|
|
||||||
import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorStrategy } from ".";
|
import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorStrategy } from ".";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Password generation strategy", () => {
|
describe("Password generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("evaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it("should throw if the policy type is incorrect", () => {
|
||||||
const strategy = new PassphraseGeneratorStrategy(null);
|
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.DisableSend,
|
type: PolicyType.DisableSend,
|
||||||
});
|
});
|
||||||
@ -26,7 +32,7 @@ describe("Password generation strategy", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
it("should map to the policy evaluator", () => {
|
||||||
const strategy = new PassphraseGeneratorStrategy(null);
|
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
data: {
|
data: {
|
||||||
@ -45,21 +51,32 @@ describe("Password generation strategy", () => {
|
|||||||
includeNumber: true,
|
includeNumber: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should map `null` to a default policy evaluator", () => {
|
||||||
|
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||||
|
const evaluator = strategy.evaluator(null);
|
||||||
|
|
||||||
|
expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator);
|
||||||
|
expect(evaluator.policy).toMatchObject(DisabledPassphraseGeneratorPolicy);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("disk", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
const strategy = new PassphraseGeneratorStrategy(legacy, provider);
|
||||||
|
|
||||||
expect(strategy.disk).toBe(PASSPHRASE_SETTINGS);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
|
expect(provider.getUser).toHaveBeenCalledWith(SomeUser, PASSPHRASE_SETTINGS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@ -68,7 +85,7 @@ describe("Password generation strategy", () => {
|
|||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
@ -77,7 +94,7 @@ describe("Password generation strategy", () => {
|
|||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it("should call the legacy service with the given options", async () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
||||||
const options = {
|
const options = {
|
||||||
type: "passphrase",
|
type: "passphrase",
|
||||||
minNumberWords: 1,
|
minNumberWords: 1,
|
||||||
@ -92,7 +109,7 @@ describe("Password generation strategy", () => {
|
|||||||
|
|
||||||
it("should set the generation type to passphrase", async () => {
|
it("should set the generation type to passphrase", async () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
await strategy.generate({ type: "foo" } as any);
|
await strategy.generate({ type: "foo" } as any);
|
||||||
|
|
||||||
|
@ -3,12 +3,17 @@ 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
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
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 { PASSPHRASE_SETTINGS } from "../key-definitions";
|
||||||
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
||||||
|
|
||||||
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 { PassphraseGeneratorPolicy } from "./passphrase-generator-policy";
|
import {
|
||||||
|
DisabledPassphraseGeneratorPolicy,
|
||||||
|
PassphraseGeneratorPolicy,
|
||||||
|
} from "./passphrase-generator-policy";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
const ONE_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
@ -19,11 +24,14 @@ export class PassphraseGeneratorStrategy
|
|||||||
/** instantiates the password generator strategy.
|
/** instantiates the password generator strategy.
|
||||||
* @param legacy generates the passphrase
|
* @param legacy generates the passphrase
|
||||||
*/
|
*/
|
||||||
constructor(private legacy: PasswordGenerationServiceAbstraction) {}
|
constructor(
|
||||||
|
private legacy: PasswordGenerationServiceAbstraction,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.disk} */
|
/** {@link GeneratorStrategy.durableState} */
|
||||||
get disk() {
|
durableState(id: UserId) {
|
||||||
return PASSPHRASE_SETTINGS;
|
return this.stateProvider.getUser(id, PASSPHRASE_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
@ -37,6 +45,10 @@ export class PassphraseGeneratorStrategy
|
|||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.evaluator} */
|
||||||
evaluator(policy: Policy): PassphraseGeneratorOptionsEvaluator {
|
evaluator(policy: Policy): PassphraseGeneratorOptionsEvaluator {
|
||||||
|
if (!policy) {
|
||||||
|
return new PassphraseGeneratorOptionsEvaluator(DisabledPassphraseGeneratorPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
if (policy.type !== this.policy) {
|
if (policy.type !== this.policy) {
|
||||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||||
throw Error("Mismatched policy type. " + details);
|
throw Error("Mismatched policy type. " + details);
|
||||||
|
@ -9,18 +9,24 @@ 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
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
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 { PASSWORD_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
|
import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PasswordGenerationServiceAbstraction,
|
PasswordGenerationServiceAbstraction,
|
||||||
PasswordGeneratorOptionsEvaluator,
|
PasswordGeneratorOptionsEvaluator,
|
||||||
PasswordGeneratorStrategy,
|
PasswordGeneratorStrategy,
|
||||||
} from ".";
|
} from ".";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Password generation strategy", () => {
|
describe("Password generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("evaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it("should throw if the policy type is incorrect", () => {
|
||||||
const strategy = new PasswordGeneratorStrategy(null);
|
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.DisableSend,
|
type: PolicyType.DisableSend,
|
||||||
});
|
});
|
||||||
@ -29,7 +35,7 @@ describe("Password generation strategy", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
it("should map to the policy evaluator", () => {
|
||||||
const strategy = new PasswordGeneratorStrategy(null);
|
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
data: {
|
data: {
|
||||||
@ -56,21 +62,32 @@ describe("Password generation strategy", () => {
|
|||||||
specialCount: 1,
|
specialCount: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should map `null` to a default policy evaluator", () => {
|
||||||
|
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||||
|
const evaluator = strategy.evaluator(null);
|
||||||
|
|
||||||
|
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
|
||||||
|
expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("disk", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
const strategy = new PasswordGeneratorStrategy(legacy, provider);
|
||||||
|
|
||||||
expect(strategy.disk).toBe(PASSWORD_SETTINGS);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
|
expect(provider.getUser).toHaveBeenCalledWith(SomeUser, PASSWORD_SETTINGS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@ -79,7 +96,7 @@ describe("Password generation strategy", () => {
|
|||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
@ -88,7 +105,7 @@ describe("Password generation strategy", () => {
|
|||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it("should call the legacy service with the given options", async () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
||||||
const options = {
|
const options = {
|
||||||
type: "password",
|
type: "password",
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
@ -107,7 +124,7 @@ describe("Password generation strategy", () => {
|
|||||||
|
|
||||||
it("should set the generation type to password", async () => {
|
it("should set the generation type to password", async () => {
|
||||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
await strategy.generate({ type: "foo" } as any);
|
await strategy.generate({ type: "foo" } as any);
|
||||||
|
|
||||||
|
@ -3,12 +3,17 @@ 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
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
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 { PASSWORD_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
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";
|
||||||
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
||||||
import { PasswordGeneratorPolicy } from "./password-generator-policy";
|
import {
|
||||||
|
DisabledPasswordGeneratorPolicy,
|
||||||
|
PasswordGeneratorPolicy,
|
||||||
|
} from "./password-generator-policy";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
const ONE_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
@ -19,11 +24,14 @@ export class PasswordGeneratorStrategy
|
|||||||
/** instantiates the password generator strategy.
|
/** instantiates the password generator strategy.
|
||||||
* @param legacy generates the password
|
* @param legacy generates the password
|
||||||
*/
|
*/
|
||||||
constructor(private legacy: PasswordGenerationServiceAbstraction) {}
|
constructor(
|
||||||
|
private legacy: PasswordGenerationServiceAbstraction,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.disk} */
|
/** {@link GeneratorStrategy.durableState} */
|
||||||
get disk() {
|
durableState(id: UserId) {
|
||||||
return PASSWORD_SETTINGS;
|
return this.stateProvider.getUser(id, PASSWORD_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
@ -37,6 +45,10 @@ export class PasswordGeneratorStrategy
|
|||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.evaluator} */
|
||||||
evaluator(policy: Policy): PasswordGeneratorOptionsEvaluator {
|
evaluator(policy: Policy): PasswordGeneratorOptionsEvaluator {
|
||||||
|
if (!policy) {
|
||||||
|
return new PasswordGeneratorOptionsEvaluator(DisabledPasswordGeneratorPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
if (policy.type !== this.policy) {
|
if (policy.type !== this.policy) {
|
||||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||||
throw Error("Mismatched policy type. " + details);
|
throw Error("Mismatched policy type. " + details);
|
||||||
|
@ -83,7 +83,16 @@ describe("UserEncryptor", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("instance", () => {
|
describe("instance", () => {
|
||||||
it("gets a set value", async () => {
|
it("userId outputs the user input during construction", async () => {
|
||||||
|
const provider = await fakeStateProvider();
|
||||||
|
const encryptor = mockEncryptor();
|
||||||
|
|
||||||
|
const state = SecretState.from(SomeUser, FOOBAR_KEY, provider, encryptor);
|
||||||
|
|
||||||
|
expect(state.userId).toEqual(SomeUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("state$ gets a set value", async () => {
|
||||||
const provider = await fakeStateProvider();
|
const provider = await fakeStateProvider();
|
||||||
const encryptor = mockEncryptor();
|
const encryptor = mockEncryptor();
|
||||||
const state = SecretState.from(SomeUser, FOOBAR_KEY, provider, encryptor);
|
const state = SecretState.from(SomeUser, FOOBAR_KEY, provider, encryptor);
|
||||||
@ -96,6 +105,20 @@ describe("UserEncryptor", () => {
|
|||||||
expect(result).toEqual(value);
|
expect(result).toEqual(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("combinedState$ gets a set value with the userId", async () => {
|
||||||
|
const provider = await fakeStateProvider();
|
||||||
|
const encryptor = mockEncryptor();
|
||||||
|
const state = SecretState.from(SomeUser, FOOBAR_KEY, provider, encryptor);
|
||||||
|
const value = { foo: true, bar: false };
|
||||||
|
|
||||||
|
await state.update(() => value);
|
||||||
|
await awaitAsync();
|
||||||
|
const [userId, result] = await firstValueFrom(state.combinedState$);
|
||||||
|
|
||||||
|
expect(result).toEqual(value);
|
||||||
|
expect(userId).toEqual(SomeUser);
|
||||||
|
});
|
||||||
|
|
||||||
it("round-trips json-serializable values", async () => {
|
it("round-trips json-serializable values", async () => {
|
||||||
const provider = await fakeStateProvider();
|
const provider = await fakeStateProvider();
|
||||||
const encryptor = mockEncryptor();
|
const encryptor = mockEncryptor();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Observable, concatMap, of, zip } from "rxjs";
|
import { Observable, concatMap, of, zip, map } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||||
@ -9,6 +9,7 @@ import {
|
|||||||
SingleUserState,
|
SingleUserState,
|
||||||
StateProvider,
|
StateProvider,
|
||||||
StateUpdateOptions,
|
StateUpdateOptions,
|
||||||
|
CombinedState,
|
||||||
} from "../../../platform/state";
|
} from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
|
|
||||||
@ -37,7 +38,9 @@ type ClassifiedFormat<Disclosed> = {
|
|||||||
*
|
*
|
||||||
* DO NOT USE THIS for synchronized data.
|
* DO NOT USE THIS for synchronized data.
|
||||||
*/
|
*/
|
||||||
export class SecretState<Plaintext extends object, Disclosed> {
|
export class SecretState<Plaintext extends object, Disclosed>
|
||||||
|
implements SingleUserState<Plaintext>
|
||||||
|
{
|
||||||
// The constructor is private to avoid creating a circular dependency when
|
// The constructor is private to avoid creating a circular dependency when
|
||||||
// wiring the derived and secret states together.
|
// wiring the derived and secret states together.
|
||||||
private constructor(
|
private constructor(
|
||||||
@ -46,8 +49,23 @@ export class SecretState<Plaintext extends object, Disclosed> {
|
|||||||
private readonly plaintext: DerivedState<Plaintext>,
|
private readonly plaintext: DerivedState<Plaintext>,
|
||||||
) {
|
) {
|
||||||
this.state$ = plaintext.state$;
|
this.state$ = plaintext.state$;
|
||||||
|
this.combinedState$ = plaintext.state$.pipe(map((state) => [this.encrypted.userId, state]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@link SingleUserState.userId} */
|
||||||
|
get userId() {
|
||||||
|
return this.encrypted.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Observes changes to the decrypted secret state. The observer
|
||||||
|
* updates after the secret has been recorded to state storage.
|
||||||
|
* @returns `undefined` when the account is locked.
|
||||||
|
*/
|
||||||
|
readonly state$: Observable<Plaintext>;
|
||||||
|
|
||||||
|
/** {@link SingleUserState.combinedState$} */
|
||||||
|
readonly combinedState$: Observable<CombinedState<Plaintext>>;
|
||||||
|
|
||||||
/** Creates a secret state bound to an account encryptor. The account must be unlocked
|
/** Creates a secret state bound to an account encryptor. The account must be unlocked
|
||||||
* when this method is called.
|
* when this method is called.
|
||||||
* @param userId: the user to which the secret state is bound.
|
* @param userId: the user to which the secret state is bound.
|
||||||
@ -106,12 +124,6 @@ export class SecretState<Plaintext extends object, Disclosed> {
|
|||||||
return secretState;
|
return secretState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Observes changes to the decrypted secret state. The observer
|
|
||||||
* updates after the secret has been recorded to state storage.
|
|
||||||
* @returns `undefined` when the account is locked.
|
|
||||||
*/
|
|
||||||
readonly state$: Observable<Plaintext>;
|
|
||||||
|
|
||||||
/** Updates the secret stored by this state.
|
/** Updates the secret stored by this state.
|
||||||
* @param configureState a callback that returns an updated decrypted
|
* @param configureState a callback that returns an updated decrypted
|
||||||
* secret state. The callback receives the state's present value as its
|
* secret state. The callback receives the state's present value as its
|
||||||
|
@ -4,15 +4,19 @@ 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
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { CATCHALL_SETTINGS } from "../key-definitions";
|
import { CATCHALL_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Email subaddress list generation strategy", () => {
|
describe("Email subaddress list generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("evaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it("should throw if the policy type is incorrect", () => {
|
||||||
const strategy = new CatchallGeneratorStrategy(null);
|
const strategy = new CatchallGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.DisableSend,
|
type: PolicyType.DisableSend,
|
||||||
});
|
});
|
||||||
@ -21,7 +25,7 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
it("should map to the policy evaluator", () => {
|
||||||
const strategy = new CatchallGeneratorStrategy(null);
|
const strategy = new CatchallGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
data: {
|
data: {
|
||||||
@ -34,21 +38,31 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||||
expect(evaluator.policy).toMatchObject({});
|
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("disk", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new CatchallGeneratorStrategy(legacy);
|
const strategy = new CatchallGeneratorStrategy(legacy, provider);
|
||||||
|
|
||||||
expect(strategy.disk).toBe(CATCHALL_SETTINGS);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
|
expect(provider.getUser).toHaveBeenCalledWith(SomeUser, CATCHALL_SETTINGS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new CatchallGeneratorStrategy(legacy);
|
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@ -57,7 +71,7 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new CatchallGeneratorStrategy(legacy);
|
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
@ -66,7 +80,7 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it("should call the legacy service with the given options", async () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new CatchallGeneratorStrategy(legacy);
|
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
||||||
const options = {
|
const options = {
|
||||||
type: "website-name" as const,
|
type: "website-name" as const,
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { CATCHALL_SETTINGS } from "../key-definitions";
|
import { CATCHALL_SETTINGS } from "../key-definitions";
|
||||||
@ -17,11 +19,14 @@ export class CatchallGeneratorStrategy
|
|||||||
/** Instantiates the generation strategy
|
/** Instantiates the generation strategy
|
||||||
* @param usernameService generates a catchall address for a domain
|
* @param usernameService generates a catchall address for a domain
|
||||||
*/
|
*/
|
||||||
constructor(private usernameService: UsernameGenerationServiceAbstraction) {}
|
constructor(
|
||||||
|
private usernameService: UsernameGenerationServiceAbstraction,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.disk} */
|
/** {@link GeneratorStrategy.durableState} */
|
||||||
get disk() {
|
durableState(id: UserId) {
|
||||||
return CATCHALL_SETTINGS;
|
return this.stateProvider.getUser(id, CATCHALL_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
@ -38,6 +43,10 @@ export class CatchallGeneratorStrategy
|
|||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.evaluator} */
|
||||||
evaluator(policy: Policy) {
|
evaluator(policy: Policy) {
|
||||||
|
if (!policy) {
|
||||||
|
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
|
||||||
|
}
|
||||||
|
|
||||||
if (policy.type !== this.policy) {
|
if (policy.type !== this.policy) {
|
||||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||||
throw Error("Mismatched policy type. " + details);
|
throw Error("Mismatched policy type. " + details);
|
||||||
|
@ -4,15 +4,19 @@ 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
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("EFF long word list generation strategy", () => {
|
describe("EFF long word list generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("evaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it("should throw if the policy type is incorrect", () => {
|
||||||
const strategy = new EffUsernameGeneratorStrategy(null);
|
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.DisableSend,
|
type: PolicyType.DisableSend,
|
||||||
});
|
});
|
||||||
@ -21,7 +25,7 @@ describe("EFF long word list generation strategy", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
it("should map to the policy evaluator", () => {
|
||||||
const strategy = new EffUsernameGeneratorStrategy(null);
|
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
data: {
|
data: {
|
||||||
@ -34,21 +38,31 @@ describe("EFF long word list generation strategy", () => {
|
|||||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||||
expect(evaluator.policy).toMatchObject({});
|
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("disk", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new EffUsernameGeneratorStrategy(legacy);
|
const strategy = new EffUsernameGeneratorStrategy(legacy, provider);
|
||||||
|
|
||||||
expect(strategy.disk).toBe(EFF_USERNAME_SETTINGS);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
|
expect(provider.getUser).toHaveBeenCalledWith(SomeUser, EFF_USERNAME_SETTINGS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new EffUsernameGeneratorStrategy(legacy);
|
const strategy = new EffUsernameGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@ -57,7 +71,7 @@ describe("EFF long word list generation strategy", () => {
|
|||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new EffUsernameGeneratorStrategy(legacy);
|
const strategy = new EffUsernameGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
@ -66,7 +80,7 @@ describe("EFF long word list generation strategy", () => {
|
|||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it("should call the legacy service with the given options", async () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new EffUsernameGeneratorStrategy(legacy);
|
const strategy = new EffUsernameGeneratorStrategy(legacy, null);
|
||||||
const options = {
|
const options = {
|
||||||
wordCapitalize: false,
|
wordCapitalize: false,
|
||||||
wordIncludeNumber: false,
|
wordIncludeNumber: false,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||||
@ -17,11 +19,14 @@ export class EffUsernameGeneratorStrategy
|
|||||||
/** Instantiates the generation strategy
|
/** Instantiates the generation strategy
|
||||||
* @param usernameService generates a username from EFF word list
|
* @param usernameService generates a username from EFF word list
|
||||||
*/
|
*/
|
||||||
constructor(private usernameService: UsernameGenerationServiceAbstraction) {}
|
constructor(
|
||||||
|
private usernameService: UsernameGenerationServiceAbstraction,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.disk} */
|
/** {@link GeneratorStrategy.durableState} */
|
||||||
get disk() {
|
durableState(id: UserId) {
|
||||||
return EFF_USERNAME_SETTINGS;
|
return this.stateProvider.getUser(id, EFF_USERNAME_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
@ -38,6 +43,10 @@ export class EffUsernameGeneratorStrategy
|
|||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.evaluator} */
|
||||||
evaluator(policy: Policy) {
|
evaluator(policy: Policy) {
|
||||||
|
if (!policy) {
|
||||||
|
return new DefaultPolicyEvaluator<EffUsernameGenerationOptions>();
|
||||||
|
}
|
||||||
|
|
||||||
if (policy.type !== this.policy) {
|
if (policy.type !== this.policy) {
|
||||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||||
throw Error("Mismatched policy type. " + details);
|
throw Error("Mismatched policy type. " + details);
|
||||||
|
@ -4,15 +4,19 @@ 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
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||||
|
|
||||||
import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
|
||||||
describe("Email subaddress list generation strategy", () => {
|
describe("Email subaddress list generation strategy", () => {
|
||||||
describe("evaluator()", () => {
|
describe("evaluator()", () => {
|
||||||
it("should throw if the policy type is incorrect", () => {
|
it("should throw if the policy type is incorrect", () => {
|
||||||
const strategy = new SubaddressGeneratorStrategy(null);
|
const strategy = new SubaddressGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.DisableSend,
|
type: PolicyType.DisableSend,
|
||||||
});
|
});
|
||||||
@ -21,7 +25,7 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should map to the policy evaluator", () => {
|
it("should map to the policy evaluator", () => {
|
||||||
const strategy = new SubaddressGeneratorStrategy(null);
|
const strategy = new SubaddressGeneratorStrategy(null, null);
|
||||||
const policy = mock<Policy>({
|
const policy = mock<Policy>({
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
data: {
|
data: {
|
||||||
@ -34,21 +38,31 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator);
|
||||||
expect(evaluator.policy).toMatchObject({});
|
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("disk", () => {
|
describe("durableState", () => {
|
||||||
it("should use password settings key", () => {
|
it("should use password settings key", () => {
|
||||||
|
const provider = mock<StateProvider>();
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new SubaddressGeneratorStrategy(legacy);
|
const strategy = new SubaddressGeneratorStrategy(legacy, provider);
|
||||||
|
|
||||||
expect(strategy.disk).toBe(SUBADDRESS_SETTINGS);
|
strategy.durableState(SomeUser);
|
||||||
|
|
||||||
|
expect(provider.getUser).toHaveBeenCalledWith(SomeUser, SUBADDRESS_SETTINGS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cache_ms", () => {
|
describe("cache_ms", () => {
|
||||||
it("should be a positive non-zero number", () => {
|
it("should be a positive non-zero number", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new SubaddressGeneratorStrategy(legacy);
|
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@ -57,7 +71,7 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
describe("policy", () => {
|
describe("policy", () => {
|
||||||
it("should use password generator policy", () => {
|
it("should use password generator policy", () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new SubaddressGeneratorStrategy(legacy);
|
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
||||||
|
|
||||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||||
});
|
});
|
||||||
@ -66,7 +80,7 @@ describe("Email subaddress list generation strategy", () => {
|
|||||||
describe("generate()", () => {
|
describe("generate()", () => {
|
||||||
it("should call the legacy service with the given options", async () => {
|
it("should call the legacy service with the given options", async () => {
|
||||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||||
const strategy = new SubaddressGeneratorStrategy(legacy);
|
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
||||||
const options = {
|
const options = {
|
||||||
type: "website-name" as const,
|
type: "website-name" as const,
|
||||||
email: "someone@example.com",
|
email: "someone@example.com",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { PolicyType } from "../../../admin-console/enums";
|
import { PolicyType } from "../../../admin-console/enums";
|
||||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||||
|
import { StateProvider } from "../../../platform/state";
|
||||||
|
import { UserId } from "../../../types/guid";
|
||||||
import { GeneratorStrategy } from "../abstractions";
|
import { GeneratorStrategy } from "../abstractions";
|
||||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||||
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||||
@ -17,11 +19,14 @@ export class SubaddressGeneratorStrategy
|
|||||||
/** Instantiates the generation strategy
|
/** Instantiates the generation strategy
|
||||||
* @param usernameService generates an email subaddress from an email address
|
* @param usernameService generates an email subaddress from an email address
|
||||||
*/
|
*/
|
||||||
constructor(private usernameService: UsernameGenerationServiceAbstraction) {}
|
constructor(
|
||||||
|
private usernameService: UsernameGenerationServiceAbstraction,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.disk} */
|
/** {@link GeneratorStrategy.durableState} */
|
||||||
get disk() {
|
durableState(id: UserId) {
|
||||||
return SUBADDRESS_SETTINGS;
|
return this.stateProvider.getUser(id, SUBADDRESS_SETTINGS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@link GeneratorStrategy.policy} */
|
/** {@link GeneratorStrategy.policy} */
|
||||||
@ -38,6 +43,10 @@ export class SubaddressGeneratorStrategy
|
|||||||
|
|
||||||
/** {@link GeneratorStrategy.evaluator} */
|
/** {@link GeneratorStrategy.evaluator} */
|
||||||
evaluator(policy: Policy) {
|
evaluator(policy: Policy) {
|
||||||
|
if (!policy) {
|
||||||
|
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
|
||||||
|
}
|
||||||
|
|
||||||
if (policy.type !== this.policy) {
|
if (policy.type !== this.policy) {
|
||||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||||
throw Error("Mismatched policy type. " + details);
|
throw Error("Mismatched policy type. " + details);
|
||||||
|
Loading…
Reference in New Issue
Block a user