mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-21 16:18:28 +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 "./matchers";
|
||||
export * from "./fake-state-provider";
|
||||
export * from "./fake-state";
|
||||
export * from "./fake-account-service";
|
||||
|
@ -4,7 +4,7 @@ export { DerivedState } from "./derived-state";
|
||||
export { GlobalState } from "./global-state";
|
||||
export { StateProvider } from "./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 { KeyDefinition } from "./key-definition";
|
||||
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
|
||||
// implement ADR-0002
|
||||
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";
|
||||
|
||||
/** Tailors the generator service to generate a specific kind of credentials */
|
||||
export abstract class GeneratorStrategy<Options, Policy> {
|
||||
/** The key used when storing credentials on disk. */
|
||||
disk: KeyDefinition<Options>;
|
||||
/** Retrieve application state that persists across locks.
|
||||
* @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. */
|
||||
policy: PolicyType;
|
||||
@ -19,7 +23,8 @@ export abstract class GeneratorStrategy<Options, Policy> {
|
||||
|
||||
/** Creates an evaluator from a generator policy.
|
||||
* @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.
|
||||
*/
|
||||
evaluator: (policy: AdminPolicy) => PolicyEvaluator<Policy, Options>;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../../types/guid";
|
||||
|
||||
import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
||||
|
||||
/** Generates credentials used for user authentication
|
||||
@ -9,19 +11,22 @@ import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
||||
export abstract class GeneratorService<Options, Policy> {
|
||||
/** An observable monitoring the options saved to disk.
|
||||
* 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.
|
||||
* 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
|
||||
* @param userId: Identifies the user making the request
|
||||
* @param options the options to enforce the policy on
|
||||
* @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
|
||||
* @param options the options to generate credentials with
|
||||
@ -30,8 +35,9 @@ export abstract class GeneratorService<Options, Policy> {
|
||||
generate: (options: Options) => Promise<string>;
|
||||
|
||||
/** Saves the given options to disk.
|
||||
* @param userId: Identifies the user making the request
|
||||
* @param options the options to save
|
||||
* @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 { 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 { 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 { Utils } from "../../platform/misc/utils";
|
||||
import { ActiveUserState, ActiveUserStateProvider, KeyDefinition } from "../../platform/state";
|
||||
import { SingleUserState } from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
|
||||
import { GeneratorStrategy, PolicyEvaluator } from "./abstractions";
|
||||
import { PASSPHRASE_SETTINGS, PASSWORD_SETTINGS } from "./key-definitions";
|
||||
import { PasswordGenerationOptions } from "./password";
|
||||
|
||||
import { DefaultGeneratorService } from ".";
|
||||
|
||||
function mockPolicyService(config?: { data?: any; policy?: BehaviorSubject<Policy> }) {
|
||||
const state = mock<Policy>({ data: config?.data ?? {} });
|
||||
const subject = config?.policy ?? new BehaviorSubject<Policy>(state);
|
||||
|
||||
function mockPolicyService(config?: { state?: BehaviorSubject<Policy> }) {
|
||||
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;
|
||||
}
|
||||
|
||||
function mockGeneratorStrategy(config?: {
|
||||
disk?: KeyDefinition<any>;
|
||||
userState?: SingleUserState<any>;
|
||||
policy?: PolicyType;
|
||||
evaluator?: any;
|
||||
}) {
|
||||
const durableState =
|
||||
config?.userState ?? new FakeSingleUserState<PasswordGenerationOptions>(SomeUser);
|
||||
const strategy = mock<GeneratorStrategy<any, any>>({
|
||||
// intentionally arbitrary so that tests that need to check
|
||||
// whether they're used properly are guaranteed to test
|
||||
// the value from `config`.
|
||||
disk: config?.disk ?? {},
|
||||
durableState: jest.fn(() => durableState),
|
||||
policy: config?.policy ?? PolicyType.DisableSend,
|
||||
evaluator: jest.fn(() => config?.evaluator ?? mock<PolicyEvaluator<any, any>>()),
|
||||
});
|
||||
@ -49,129 +52,123 @@ function mockGeneratorStrategy(config?: {
|
||||
return strategy;
|
||||
}
|
||||
|
||||
// FIXME: Use the fake instead, once it's updated to monitor its method calls.
|
||||
function mockStateProvider(): [
|
||||
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;
|
||||
}
|
||||
const SomeUser = "some user" as UserId;
|
||||
const AnotherUser = "another user" as UserId;
|
||||
|
||||
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$", () => {
|
||||
it("should return the state from strategy.key", () => {
|
||||
it("should retrieve durable state from the service", () => {
|
||||
const policy = mockPolicyService();
|
||||
const strategy = mockGeneratorStrategy({ disk: PASSPHRASE_SETTINGS });
|
||||
const [state] = mockStateProvider();
|
||||
const service = new DefaultGeneratorService(strategy, policy, state);
|
||||
const userState = new FakeSingleUserState<PasswordGenerationOptions>(SomeUser);
|
||||
const strategy = mockGeneratorStrategy({ userState });
|
||||
const service = new DefaultGeneratorService(strategy, policy);
|
||||
|
||||
// invoke the getter. It returns the state but that's not important.
|
||||
service.options$;
|
||||
const result = service.options$(SomeUser);
|
||||
|
||||
expect(state.get).toHaveBeenCalledWith(PASSPHRASE_SETTINGS);
|
||||
expect(strategy.durableState).toHaveBeenCalledWith(SomeUser);
|
||||
expect(result).toBe(userState.state$);
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const policy = mockPolicyService();
|
||||
const strategy = mockGeneratorStrategy();
|
||||
// using the fake here because we're testing that the update and the
|
||||
// property are wired together. If we were to mock that, we'd be testing
|
||||
// the mock configuration instead of the wiring.
|
||||
const provider = fakeStateProvider(strategy.disk, { length: 9 });
|
||||
const service = new DefaultGeneratorService(strategy, policy, provider);
|
||||
const userState = new FakeSingleUserState<PasswordGenerationOptions>(SomeUser, { length: 9 });
|
||||
const strategy = mockGeneratorStrategy({ userState });
|
||||
const service = new DefaultGeneratorService(strategy, policy);
|
||||
|
||||
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 });
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const policyService = mockPolicyService();
|
||||
const evaluator = mock<PolicyEvaluator<any, any>>();
|
||||
const strategy = mockGeneratorStrategy({ evaluator });
|
||||
const service = new DefaultGeneratorService(strategy, policyService);
|
||||
|
||||
const service = new DefaultGeneratorService(strategy, policyService, null);
|
||||
|
||||
const policy = await firstValueFrom(service.policy$);
|
||||
const policy = await firstValueFrom(service.evaluator$(SomeUser));
|
||||
|
||||
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("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 () => {
|
||||
const policy = mockPolicyService();
|
||||
const evaluator = mock<PolicyEvaluator<any, any>>();
|
||||
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.sanitize).toHaveBeenCalled();
|
||||
@ -182,7 +179,7 @@ describe("Password generator service", () => {
|
||||
it("should invoke the generation strategy", async () => {
|
||||
const strategy = mockGeneratorStrategy();
|
||||
const policy = mockPolicyService();
|
||||
const service = new DefaultGeneratorService(strategy, policy, null);
|
||||
const service = new DefaultGeneratorService(strategy, policy);
|
||||
|
||||
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
|
||||
// implement ADR-0002
|
||||
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";
|
||||
|
||||
@ -13,45 +13,57 @@ export class DefaultGeneratorService<Options, Policy> implements GeneratorServic
|
||||
* @param strategy tailors the service to a specific generator type
|
||||
* (e.g. password, passphrase)
|
||||
* @param policy provides the policy to enforce
|
||||
* @param state saves and loads password generation options to the location
|
||||
* specified by the strategy
|
||||
*/
|
||||
constructor(
|
||||
private strategy: GeneratorStrategy<Options, Policy>,
|
||||
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)),
|
||||
share({
|
||||
// cache evaluator in a replay subject to amortize creation cost
|
||||
// and reduce GC pressure.
|
||||
connector: () => new ReplaySubject(1),
|
||||
resetOnRefCountZero: () => timer(this.strategy.cache_ms),
|
||||
}),
|
||||
);
|
||||
|
||||
return evaluator$;
|
||||
}
|
||||
|
||||
private _policy$: Observable<PolicyEvaluator<Policy, Options>>;
|
||||
|
||||
/** {@link GeneratorService.options$} */
|
||||
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$);
|
||||
/** {@link GeneratorService.enforcePolicy()} */
|
||||
async enforcePolicy(userId: UserId, options: Options): Promise<Options> {
|
||||
const policy = await firstValueFrom(this.evaluator$(userId));
|
||||
const evaluated = policy.applyPolicy(options);
|
||||
const sanitized = policy.sanitize(evaluated);
|
||||
return sanitized;
|
||||
|
@ -9,15 +9,21 @@ import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
||||
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
||||
|
||||
import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy";
|
||||
|
||||
import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorStrategy } from ".";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
|
||||
describe("Password generation strategy", () => {
|
||||
describe("evaluator()", () => {
|
||||
it("should throw if the policy type is incorrect", () => {
|
||||
const strategy = new PassphraseGeneratorStrategy(null);
|
||||
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.DisableSend,
|
||||
});
|
||||
@ -26,7 +32,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should map to the policy evaluator", () => {
|
||||
const strategy = new PassphraseGeneratorStrategy(null);
|
||||
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
@ -45,21 +51,32 @@ describe("Password generation strategy", () => {
|
||||
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", () => {
|
||||
const provider = mock<StateProvider>();
|
||||
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", () => {
|
||||
it("should be a positive non-zero number", () => {
|
||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
||||
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
||||
|
||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||
});
|
||||
@ -68,7 +85,7 @@ describe("Password generation strategy", () => {
|
||||
describe("policy", () => {
|
||||
it("should use password generator policy", () => {
|
||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
||||
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
||||
|
||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||
});
|
||||
@ -77,7 +94,7 @@ describe("Password generation strategy", () => {
|
||||
describe("generate()", () => {
|
||||
it("should call the legacy service with the given options", async () => {
|
||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
||||
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
||||
const options = {
|
||||
type: "passphrase",
|
||||
minNumberWords: 1,
|
||||
@ -92,7 +109,7 @@ describe("Password generation strategy", () => {
|
||||
|
||||
it("should set the generation type to passphrase", async () => {
|
||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||
const strategy = new PassphraseGeneratorStrategy(legacy);
|
||||
const strategy = new PassphraseGeneratorStrategy(legacy, null);
|
||||
|
||||
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
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { PASSPHRASE_SETTINGS } from "../key-definitions";
|
||||
import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction";
|
||||
|
||||
import { PassphraseGenerationOptions } from "./passphrase-generation-options";
|
||||
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;
|
||||
|
||||
@ -19,11 +24,14 @@ export class PassphraseGeneratorStrategy
|
||||
/** instantiates the password generator strategy.
|
||||
* @param legacy generates the passphrase
|
||||
*/
|
||||
constructor(private legacy: PasswordGenerationServiceAbstraction) {}
|
||||
constructor(
|
||||
private legacy: PasswordGenerationServiceAbstraction,
|
||||
private stateProvider: StateProvider,
|
||||
) {}
|
||||
|
||||
/** {@link GeneratorStrategy.disk} */
|
||||
get disk() {
|
||||
return PASSPHRASE_SETTINGS;
|
||||
/** {@link GeneratorStrategy.durableState} */
|
||||
durableState(id: UserId) {
|
||||
return this.stateProvider.getUser(id, PASSPHRASE_SETTINGS);
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.policy} */
|
||||
@ -37,6 +45,10 @@ export class PassphraseGeneratorStrategy
|
||||
|
||||
/** {@link GeneratorStrategy.evaluator} */
|
||||
evaluator(policy: Policy): PassphraseGeneratorOptionsEvaluator {
|
||||
if (!policy) {
|
||||
return new PassphraseGeneratorOptionsEvaluator(DisabledPassphraseGeneratorPolicy);
|
||||
}
|
||||
|
||||
if (policy.type !== this.policy) {
|
||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||
throw Error("Mismatched policy type. " + details);
|
||||
|
@ -9,18 +9,24 @@ import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { PASSWORD_SETTINGS } from "../key-definitions";
|
||||
|
||||
import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy";
|
||||
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PasswordGeneratorOptionsEvaluator,
|
||||
PasswordGeneratorStrategy,
|
||||
} from ".";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
|
||||
describe("Password generation strategy", () => {
|
||||
describe("evaluator()", () => {
|
||||
it("should throw if the policy type is incorrect", () => {
|
||||
const strategy = new PasswordGeneratorStrategy(null);
|
||||
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.DisableSend,
|
||||
});
|
||||
@ -29,7 +35,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should map to the policy evaluator", () => {
|
||||
const strategy = new PasswordGeneratorStrategy(null);
|
||||
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
@ -56,21 +62,32 @@ describe("Password generation strategy", () => {
|
||||
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", () => {
|
||||
const provider = mock<StateProvider>();
|
||||
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", () => {
|
||||
it("should be a positive non-zero number", () => {
|
||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
||||
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
||||
|
||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||
});
|
||||
@ -79,7 +96,7 @@ describe("Password generation strategy", () => {
|
||||
describe("policy", () => {
|
||||
it("should use password generator policy", () => {
|
||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
||||
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
||||
|
||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||
});
|
||||
@ -88,7 +105,7 @@ describe("Password generation strategy", () => {
|
||||
describe("generate()", () => {
|
||||
it("should call the legacy service with the given options", async () => {
|
||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
||||
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
||||
const options = {
|
||||
type: "password",
|
||||
minLength: 1,
|
||||
@ -107,7 +124,7 @@ describe("Password generation strategy", () => {
|
||||
|
||||
it("should set the generation type to password", async () => {
|
||||
const legacy = mock<PasswordGenerationServiceAbstraction>();
|
||||
const strategy = new PasswordGeneratorStrategy(legacy);
|
||||
const strategy = new PasswordGeneratorStrategy(legacy, null);
|
||||
|
||||
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
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { PASSWORD_SETTINGS } from "../key-definitions";
|
||||
|
||||
import { PasswordGenerationOptions } from "./password-generation-options";
|
||||
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
|
||||
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;
|
||||
|
||||
@ -19,11 +24,14 @@ export class PasswordGeneratorStrategy
|
||||
/** instantiates the password generator strategy.
|
||||
* @param legacy generates the password
|
||||
*/
|
||||
constructor(private legacy: PasswordGenerationServiceAbstraction) {}
|
||||
constructor(
|
||||
private legacy: PasswordGenerationServiceAbstraction,
|
||||
private stateProvider: StateProvider,
|
||||
) {}
|
||||
|
||||
/** {@link GeneratorStrategy.disk} */
|
||||
get disk() {
|
||||
return PASSWORD_SETTINGS;
|
||||
/** {@link GeneratorStrategy.durableState} */
|
||||
durableState(id: UserId) {
|
||||
return this.stateProvider.getUser(id, PASSWORD_SETTINGS);
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.policy} */
|
||||
@ -37,6 +45,10 @@ export class PasswordGeneratorStrategy
|
||||
|
||||
/** {@link GeneratorStrategy.evaluator} */
|
||||
evaluator(policy: Policy): PasswordGeneratorOptionsEvaluator {
|
||||
if (!policy) {
|
||||
return new PasswordGeneratorOptionsEvaluator(DisabledPasswordGeneratorPolicy);
|
||||
}
|
||||
|
||||
if (policy.type !== this.policy) {
|
||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||
throw Error("Mismatched policy type. " + details);
|
||||
|
@ -83,7 +83,16 @@ describe("UserEncryptor", () => {
|
||||
});
|
||||
|
||||
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 encryptor = mockEncryptor();
|
||||
const state = SecretState.from(SomeUser, FOOBAR_KEY, provider, encryptor);
|
||||
@ -96,6 +105,20 @@ describe("UserEncryptor", () => {
|
||||
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 () => {
|
||||
const provider = await fakeStateProvider();
|
||||
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 { EncString } from "../../../platform/models/domain/enc-string";
|
||||
@ -9,6 +9,7 @@ import {
|
||||
SingleUserState,
|
||||
StateProvider,
|
||||
StateUpdateOptions,
|
||||
CombinedState,
|
||||
} from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
|
||||
@ -37,7 +38,9 @@ type ClassifiedFormat<Disclosed> = {
|
||||
*
|
||||
* 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
|
||||
// wiring the derived and secret states together.
|
||||
private constructor(
|
||||
@ -46,8 +49,23 @@ export class SecretState<Plaintext extends object, Disclosed> {
|
||||
private readonly plaintext: DerivedState<Plaintext>,
|
||||
) {
|
||||
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
|
||||
* when this method is called.
|
||||
* @param userId: the user to which the secret state is bound.
|
||||
@ -106,12 +124,6 @@ export class SecretState<Plaintext extends object, Disclosed> {
|
||||
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.
|
||||
* @param configureState a callback that returns an updated decrypted
|
||||
* 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
|
||||
// implement ADR-0002
|
||||
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 { CATCHALL_SETTINGS } from "../key-definitions";
|
||||
|
||||
import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
|
||||
describe("Email subaddress list generation strategy", () => {
|
||||
describe("evaluator()", () => {
|
||||
it("should throw if the policy type is incorrect", () => {
|
||||
const strategy = new CatchallGeneratorStrategy(null);
|
||||
const strategy = new CatchallGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.DisableSend,
|
||||
});
|
||||
@ -21,7 +25,7 @@ describe("Email subaddress list generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should map to the policy evaluator", () => {
|
||||
const strategy = new CatchallGeneratorStrategy(null);
|
||||
const strategy = new CatchallGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
@ -34,21 +38,31 @@ describe("Email subaddress list generation strategy", () => {
|
||||
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("disk", () => {
|
||||
describe("durableState", () => {
|
||||
it("should use password settings key", () => {
|
||||
const provider = mock<StateProvider>();
|
||||
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", () => {
|
||||
it("should be a positive non-zero number", () => {
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new CatchallGeneratorStrategy(legacy);
|
||||
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
||||
|
||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||
});
|
||||
@ -57,7 +71,7 @@ describe("Email subaddress list generation strategy", () => {
|
||||
describe("policy", () => {
|
||||
it("should use password generator policy", () => {
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new CatchallGeneratorStrategy(legacy);
|
||||
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
||||
|
||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||
});
|
||||
@ -66,7 +80,7 @@ describe("Email subaddress list generation strategy", () => {
|
||||
describe("generate()", () => {
|
||||
it("should call the legacy service with the given options", async () => {
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new CatchallGeneratorStrategy(legacy);
|
||||
const strategy = new CatchallGeneratorStrategy(legacy, null);
|
||||
const options = {
|
||||
type: "website-name" as const,
|
||||
domain: "example.com",
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||
import { CATCHALL_SETTINGS } from "../key-definitions";
|
||||
@ -17,11 +19,14 @@ export class CatchallGeneratorStrategy
|
||||
/** Instantiates the generation strategy
|
||||
* @param usernameService generates a catchall address for a domain
|
||||
*/
|
||||
constructor(private usernameService: UsernameGenerationServiceAbstraction) {}
|
||||
constructor(
|
||||
private usernameService: UsernameGenerationServiceAbstraction,
|
||||
private stateProvider: StateProvider,
|
||||
) {}
|
||||
|
||||
/** {@link GeneratorStrategy.disk} */
|
||||
get disk() {
|
||||
return CATCHALL_SETTINGS;
|
||||
/** {@link GeneratorStrategy.durableState} */
|
||||
durableState(id: UserId) {
|
||||
return this.stateProvider.getUser(id, CATCHALL_SETTINGS);
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.policy} */
|
||||
@ -38,6 +43,10 @@ export class CatchallGeneratorStrategy
|
||||
|
||||
/** {@link GeneratorStrategy.evaluator} */
|
||||
evaluator(policy: Policy) {
|
||||
if (!policy) {
|
||||
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
|
||||
}
|
||||
|
||||
if (policy.type !== this.policy) {
|
||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||
throw Error("Mismatched policy type. " + details);
|
||||
|
@ -4,15 +4,19 @@ import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||
|
||||
import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
|
||||
describe("EFF long word list generation strategy", () => {
|
||||
describe("evaluator()", () => {
|
||||
it("should throw if the policy type is incorrect", () => {
|
||||
const strategy = new EffUsernameGeneratorStrategy(null);
|
||||
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.DisableSend,
|
||||
});
|
||||
@ -21,7 +25,7 @@ describe("EFF long word list generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should map to the policy evaluator", () => {
|
||||
const strategy = new EffUsernameGeneratorStrategy(null);
|
||||
const strategy = new EffUsernameGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
@ -34,21 +38,31 @@ describe("EFF long word list generation strategy", () => {
|
||||
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("disk", () => {
|
||||
describe("durableState", () => {
|
||||
it("should use password settings key", () => {
|
||||
const provider = mock<StateProvider>();
|
||||
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", () => {
|
||||
it("should be a positive non-zero number", () => {
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new EffUsernameGeneratorStrategy(legacy);
|
||||
const strategy = new EffUsernameGeneratorStrategy(legacy, null);
|
||||
|
||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||
});
|
||||
@ -57,7 +71,7 @@ describe("EFF long word list generation strategy", () => {
|
||||
describe("policy", () => {
|
||||
it("should use password generator policy", () => {
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new EffUsernameGeneratorStrategy(legacy);
|
||||
const strategy = new EffUsernameGeneratorStrategy(legacy, null);
|
||||
|
||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||
});
|
||||
@ -66,7 +80,7 @@ describe("EFF long word list generation strategy", () => {
|
||||
describe("generate()", () => {
|
||||
it("should call the legacy service with the given options", async () => {
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new EffUsernameGeneratorStrategy(legacy);
|
||||
const strategy = new EffUsernameGeneratorStrategy(legacy, null);
|
||||
const options = {
|
||||
wordCapitalize: false,
|
||||
wordIncludeNumber: false,
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||
import { EFF_USERNAME_SETTINGS } from "../key-definitions";
|
||||
@ -17,11 +19,14 @@ export class EffUsernameGeneratorStrategy
|
||||
/** Instantiates the generation strategy
|
||||
* @param usernameService generates a username from EFF word list
|
||||
*/
|
||||
constructor(private usernameService: UsernameGenerationServiceAbstraction) {}
|
||||
constructor(
|
||||
private usernameService: UsernameGenerationServiceAbstraction,
|
||||
private stateProvider: StateProvider,
|
||||
) {}
|
||||
|
||||
/** {@link GeneratorStrategy.disk} */
|
||||
get disk() {
|
||||
return EFF_USERNAME_SETTINGS;
|
||||
/** {@link GeneratorStrategy.durableState} */
|
||||
durableState(id: UserId) {
|
||||
return this.stateProvider.getUser(id, EFF_USERNAME_SETTINGS);
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.policy} */
|
||||
@ -38,6 +43,10 @@ export class EffUsernameGeneratorStrategy
|
||||
|
||||
/** {@link GeneratorStrategy.evaluator} */
|
||||
evaluator(policy: Policy) {
|
||||
if (!policy) {
|
||||
return new DefaultPolicyEvaluator<EffUsernameGenerationOptions>();
|
||||
}
|
||||
|
||||
if (policy.type !== this.policy) {
|
||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||
throw Error("Mismatched policy type. " + details);
|
||||
|
@ -4,15 +4,19 @@ import { PolicyType } from "../../../admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||
|
||||
import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from ".";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
|
||||
describe("Email subaddress list generation strategy", () => {
|
||||
describe("evaluator()", () => {
|
||||
it("should throw if the policy type is incorrect", () => {
|
||||
const strategy = new SubaddressGeneratorStrategy(null);
|
||||
const strategy = new SubaddressGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.DisableSend,
|
||||
});
|
||||
@ -21,7 +25,7 @@ describe("Email subaddress list generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should map to the policy evaluator", () => {
|
||||
const strategy = new SubaddressGeneratorStrategy(null);
|
||||
const strategy = new SubaddressGeneratorStrategy(null, null);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
@ -34,21 +38,31 @@ describe("Email subaddress list generation strategy", () => {
|
||||
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("disk", () => {
|
||||
describe("durableState", () => {
|
||||
it("should use password settings key", () => {
|
||||
const provider = mock<StateProvider>();
|
||||
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", () => {
|
||||
it("should be a positive non-zero number", () => {
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new SubaddressGeneratorStrategy(legacy);
|
||||
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
||||
|
||||
expect(strategy.cache_ms).toBeGreaterThan(0);
|
||||
});
|
||||
@ -57,7 +71,7 @@ describe("Email subaddress list generation strategy", () => {
|
||||
describe("policy", () => {
|
||||
it("should use password generator policy", () => {
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new SubaddressGeneratorStrategy(legacy);
|
||||
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
||||
|
||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||
});
|
||||
@ -66,7 +80,7 @@ describe("Email subaddress list generation strategy", () => {
|
||||
describe("generate()", () => {
|
||||
it("should call the legacy service with the given options", async () => {
|
||||
const legacy = mock<UsernameGenerationServiceAbstraction>();
|
||||
const strategy = new SubaddressGeneratorStrategy(legacy);
|
||||
const strategy = new SubaddressGeneratorStrategy(legacy, null);
|
||||
const options = {
|
||||
type: "website-name" as const,
|
||||
email: "someone@example.com",
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
|
||||
import { SUBADDRESS_SETTINGS } from "../key-definitions";
|
||||
@ -17,11 +19,14 @@ export class SubaddressGeneratorStrategy
|
||||
/** Instantiates the generation strategy
|
||||
* @param usernameService generates an email subaddress from an email address
|
||||
*/
|
||||
constructor(private usernameService: UsernameGenerationServiceAbstraction) {}
|
||||
constructor(
|
||||
private usernameService: UsernameGenerationServiceAbstraction,
|
||||
private stateProvider: StateProvider,
|
||||
) {}
|
||||
|
||||
/** {@link GeneratorStrategy.disk} */
|
||||
get disk() {
|
||||
return SUBADDRESS_SETTINGS;
|
||||
/** {@link GeneratorStrategy.durableState} */
|
||||
durableState(id: UserId) {
|
||||
return this.stateProvider.getUser(id, SUBADDRESS_SETTINGS);
|
||||
}
|
||||
|
||||
/** {@link GeneratorStrategy.policy} */
|
||||
@ -38,6 +43,10 @@ export class SubaddressGeneratorStrategy
|
||||
|
||||
/** {@link GeneratorStrategy.evaluator} */
|
||||
evaluator(policy: Policy) {
|
||||
if (!policy) {
|
||||
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
|
||||
}
|
||||
|
||||
if (policy.type !== this.policy) {
|
||||
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
|
||||
throw Error("Mismatched policy type. " + details);
|
||||
|
Loading…
Reference in New Issue
Block a user