diff --git a/libs/common/spec/index.ts b/libs/common/spec/index.ts index 4ba9f3d3939..72bd28aca4f 100644 --- a/libs/common/spec/index.ts +++ b/libs/common/spec/index.ts @@ -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"; diff --git a/libs/common/src/platform/state/index.ts b/libs/common/src/platform/state/index.ts index b457e14c2f3..79f5b4172fd 100644 --- a/libs/common/src/platform/state/index.ts +++ b/libs/common/src/platform/state/index.ts @@ -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"; diff --git a/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts index 2ec098bc418..f11c1d73009 100644 --- a/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts +++ b/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts @@ -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 { - /** The key used when storing credentials on disk. */ - disk: KeyDefinition; + /** 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; /** Identifies the policy enforced by the generator. */ policy: PolicyType; @@ -19,7 +23,8 @@ export abstract class GeneratorStrategy { /** 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; diff --git a/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts index e64e0787792..f1820ed707b 100644 --- a/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts +++ b/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts @@ -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 { /** 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$: (userId: UserId) => Observable; /** 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>; + evaluator$: (userId: UserId) => Observable>; /** 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; + enforcePolicy: (userId: UserId, options: Options) => Promise; /** Generates credentials * @param options the options to generate credentials with @@ -30,8 +35,9 @@ export abstract class GeneratorService { generate: (options: Options) => Promise; /** 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; + saveOptions: (userId: UserId, options: Options) => Promise; } diff --git a/libs/common/src/tools/generator/default-generator.service.spec.ts b/libs/common/src/tools/generator/default-generator.service.spec.ts index 4293b0168c0..84b8ff45303 100644 --- a/libs/common/src/tools/generator/default-generator.service.spec.ts +++ b/libs/common/src/tools/generator/default-generator.service.spec.ts @@ -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 }) { - const state = mock({ data: config?.data ?? {} }); - const subject = config?.policy ?? new BehaviorSubject(state); - +function mockPolicyService(config?: { state?: BehaviorSubject }) { const service = mock(); - service.get$.mockReturnValue(subject.asObservable()); + + // FIXME: swap out the mock return value when `getAll$` becomes available + const stateValue = config?.state ?? new BehaviorSubject(null); + service.get$.mockReturnValue(stateValue); + + // const stateValue = config?.state ?? new BehaviorSubject(null); + // service.getAll$.mockReturnValue(stateValue); return service; } function mockGeneratorStrategy(config?: { - disk?: KeyDefinition; + userState?: SingleUserState; policy?: PolicyType; evaluator?: any; }) { + const durableState = + config?.userState ?? new FakeSingleUserState(SomeUser); const strategy = mock>({ // 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>()), }); @@ -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, -] { - const state = mock>(); - const provider = mock(); - provider.get.mockReturnValue(state); - - return [provider, state]; -} - -function fakeStateProvider(key: KeyDefinition, 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(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(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>(); 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(null); + const policy = mockPolicyService({ state }); + const strategy = mockGeneratorStrategy(); + const service = new DefaultGeneratorService(strategy, policy); + + // model responses for the observable update + const firstEvaluator = mock>(); + strategy.evaluator.mockReturnValueOnce(firstEvaluator); + const secondEvaluator = mock>(); + 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(mock({ data: {} })); - const policyService = mockPolicyService({ policy }); - const strategy = mockGeneratorStrategy(); - const service = new DefaultGeneratorService(strategy, policyService, null); - - await service.enforcePolicy({}); - policy.next(mock({ 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>(); 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({}); diff --git a/libs/common/src/tools/generator/default-generator.service.ts b/libs/common/src/tools/generator/default-generator.service.ts index 55c15a23c2a..9c884ccefdc 100644 --- a/libs/common/src/tools/generator/default-generator.service.ts +++ b/libs/common/src/tools/generator/default-generator.service.ts @@ -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 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, private policy: PolicyService, - private state: ActiveUserStateProvider, - ) { - this._policy$ = this.policy.get$(this.strategy.policy).pipe( + ) {} + + private _evaluators$ = new Map>>(); + + /** {@link GeneratorService.options$()} */ + options$(userId: UserId) { + return this.strategy.durableState(userId).state$; + } + + /** {@link GeneratorService.saveOptions} */ + async saveOptions(userId: UserId, options: Options): Promise { + 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>; - - /** {@link GeneratorService.options$} */ - get options$() { - return this.state.get(this.strategy.disk).state$; - } - - /** {@link GeneratorService.saveOptions} */ - async saveOptions(options: Options): Promise { - 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 { - const policy = await firstValueFrom(this._policy$); + /** {@link GeneratorService.enforcePolicy()} */ + async enforcePolicy(userId: UserId, options: Options): Promise { + const policy = await firstValueFrom(this.evaluator$(userId)); const evaluated = policy.applyPolicy(options); const sanitized = policy.sanitize(evaluated); return sanitized; diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts index d355d2316d3..031ea05f014 100644 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts @@ -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({ 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({ 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(); const legacy = mock(); - 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(); - 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(); - 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(); - 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(); - const strategy = new PassphraseGeneratorStrategy(legacy); + const strategy = new PassphraseGeneratorStrategy(legacy, null); await strategy.generate({ type: "foo" } as any); diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts index 8e1d0d45987..d39f54b5765 100644 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts @@ -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); diff --git a/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts b/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts index e49d1d56712..6c213f8c543 100644 --- a/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts @@ -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({ 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({ 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(); const legacy = mock(); - 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(); - 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(); - 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(); - 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(); - const strategy = new PasswordGeneratorStrategy(legacy); + const strategy = new PasswordGeneratorStrategy(legacy, null); await strategy.generate({ type: "foo" } as any); diff --git a/libs/common/src/tools/generator/password/password-generator-strategy.ts b/libs/common/src/tools/generator/password/password-generator-strategy.ts index 70bf3087a86..223470c5869 100644 --- a/libs/common/src/tools/generator/password/password-generator-strategy.ts +++ b/libs/common/src/tools/generator/password/password-generator-strategy.ts @@ -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); diff --git a/libs/common/src/tools/generator/state/secret-state.spec.ts b/libs/common/src/tools/generator/state/secret-state.spec.ts index fa6288173a7..d4804fdb9b8 100644 --- a/libs/common/src/tools/generator/state/secret-state.spec.ts +++ b/libs/common/src/tools/generator/state/secret-state.spec.ts @@ -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(); diff --git a/libs/common/src/tools/generator/state/secret-state.ts b/libs/common/src/tools/generator/state/secret-state.ts index 88d0d95eaf3..62855c3280b 100644 --- a/libs/common/src/tools/generator/state/secret-state.ts +++ b/libs/common/src/tools/generator/state/secret-state.ts @@ -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 = { * * DO NOT USE THIS for synchronized data. */ -export class SecretState { +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 diff --git a/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts index fb5f07520b6..dafb55febab 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts @@ -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", diff --git a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts index 86a7a01cd99..aadca78b3b4 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts @@ -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); diff --git a/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts index 2433ae34f19..0fb5bf573c0 100644 --- a/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts @@ -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, diff --git a/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts b/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts index fb878c16c92..e0179895ae3 100644 --- a/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts @@ -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); diff --git a/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts index 2c0fa896bb7..105edd6b4df 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts @@ -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", diff --git a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts b/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts index eb364103133..1aba473476d 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts @@ -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);