From eafe3dec679fcb1fb219545149f5aa37c9fb2332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Mon, 12 Feb 2024 10:27:47 -0500 Subject: [PATCH] [PM-5973] add catchall generation strategy (#7898) --- .../tools/generator/key-definition.spec.ts | 11 ++- .../src/tools/generator/key-definitions.ts | 10 +++ .../username/catchall-generator-options.ts | 10 +++ .../catchall-generator-strategy.spec.ts | 83 +++++++++++++++++++ .../username/catchall-generator-strategy.ts | 56 +++++++++++++ .../src/tools/generator/username/index.ts | 1 + 6 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 libs/common/src/tools/generator/username/catchall-generator-options.ts create mode 100644 libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts create mode 100644 libs/common/src/tools/generator/username/catchall-generator-strategy.ts diff --git a/libs/common/src/tools/generator/key-definition.spec.ts b/libs/common/src/tools/generator/key-definition.spec.ts index aa37e159ad..6ee9288820 100644 --- a/libs/common/src/tools/generator/key-definition.spec.ts +++ b/libs/common/src/tools/generator/key-definition.spec.ts @@ -1,6 +1,7 @@ import { ENCRYPTED_HISTORY, EFF_USERNAME_SETTINGS, + CATCHALL_SETTINGS, SUBADDRESS_SETTINGS, PASSPHRASE_SETTINGS, PASSWORD_SETTINGS, @@ -23,7 +24,7 @@ describe("Key definitions", () => { }); }); - describe("BASIC_LATIN_SETTINGS", () => { + describe("EFF_USERNAME_SETTINGS", () => { it("should pass through deserialization", () => { const value = {}; const result = EFF_USERNAME_SETTINGS.deserializer(value); @@ -31,6 +32,14 @@ describe("Key definitions", () => { }); }); + describe("CATCHALL_SETTINGS", () => { + it("should pass through deserialization", () => { + const value = {}; + const result = CATCHALL_SETTINGS.deserializer(value); + expect(result).toBe(value); + }); + }); + describe("SUBADDRESS_SETTINGS", () => { it("should pass through deserialization", () => { const value = {}; diff --git a/libs/common/src/tools/generator/key-definitions.ts b/libs/common/src/tools/generator/key-definitions.ts index 5f951d2f8a..fc51e430dd 100644 --- a/libs/common/src/tools/generator/key-definitions.ts +++ b/libs/common/src/tools/generator/key-definitions.ts @@ -3,6 +3,7 @@ import { GENERATOR_DISK, KeyDefinition } from "../../platform/state"; import { PassphraseGenerationOptions } from "./passphrase/passphrase-generation-options"; import { GeneratedPasswordHistory } from "./password/generated-password-history"; import { PasswordGenerationOptions } from "./password/password-generation-options"; +import { CatchallGenerationOptions } from "./username/catchall-generator-options"; import { EffUsernameGenerationOptions } from "./username/eff-username-generator-options"; import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; @@ -33,6 +34,15 @@ export const EFF_USERNAME_SETTINGS = new KeyDefinition( + GENERATOR_DISK, + "catchallGeneratorSettings", + { + deserializer: (value) => value, + }, +); + /** email subaddress generation options */ export const SUBADDRESS_SETTINGS = new KeyDefinition( GENERATOR_DISK, diff --git a/libs/common/src/tools/generator/username/catchall-generator-options.ts b/libs/common/src/tools/generator/username/catchall-generator-options.ts new file mode 100644 index 0000000000..7e9950ec45 --- /dev/null +++ b/libs/common/src/tools/generator/username/catchall-generator-options.ts @@ -0,0 +1,10 @@ +/** Settings supported when generating an email subaddress */ +export type CatchallGenerationOptions = { + type?: "random" | "website-name"; + domain?: string; +}; + +/** The default options for email subaddress generation. */ +export const DefaultCatchallOptions: Partial = Object.freeze({ + type: "random", +}); 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 new file mode 100644 index 0000000000..fb5f07520b --- /dev/null +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts @@ -0,0 +1,83 @@ +import { mock } from "jest-mock-extended"; + +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 { DefaultPolicyEvaluator } from "../default-policy-evaluator"; +import { CATCHALL_SETTINGS } from "../key-definitions"; + +import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; + +describe("Email subaddress list generation strategy", () => { + describe("evaluator()", () => { + it("should throw if the policy type is incorrect", () => { + const strategy = new CatchallGeneratorStrategy(null); + const policy = mock({ + type: PolicyType.DisableSend, + }); + + expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+")); + }); + + it("should map to the policy evaluator", () => { + const strategy = new CatchallGeneratorStrategy(null); + const policy = mock({ + type: PolicyType.PasswordGenerator, + data: { + minLength: 10, + }, + }); + + const evaluator = strategy.evaluator(policy); + + expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); + expect(evaluator.policy).toMatchObject({}); + }); + }); + + describe("disk", () => { + it("should use password settings key", () => { + const legacy = mock(); + const strategy = new CatchallGeneratorStrategy(legacy); + + expect(strategy.disk).toBe(CATCHALL_SETTINGS); + }); + }); + + describe("cache_ms", () => { + it("should be a positive non-zero number", () => { + const legacy = mock(); + const strategy = new CatchallGeneratorStrategy(legacy); + + expect(strategy.cache_ms).toBeGreaterThan(0); + }); + }); + + describe("policy", () => { + it("should use password generator policy", () => { + const legacy = mock(); + const strategy = new CatchallGeneratorStrategy(legacy); + + expect(strategy.policy).toBe(PolicyType.PasswordGenerator); + }); + }); + + describe("generate()", () => { + it("should call the legacy service with the given options", async () => { + const legacy = mock(); + const strategy = new CatchallGeneratorStrategy(legacy); + const options = { + type: "website-name" as const, + domain: "example.com", + }; + + await strategy.generate(options); + + expect(legacy.generateCatchall).toHaveBeenCalledWith({ + catchallType: "website-name" as const, + catchallDomain: "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 new file mode 100644 index 0000000000..86a7a01cd9 --- /dev/null +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts @@ -0,0 +1,56 @@ +import { PolicyType } from "../../../admin-console/enums"; +import { Policy } from "../../../admin-console/models/domain/policy"; +import { GeneratorStrategy } from "../abstractions"; +import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; +import { CATCHALL_SETTINGS } from "../key-definitions"; +import { NoPolicy } from "../no-policy"; + +import { CatchallGenerationOptions } from "./catchall-generator-options"; +import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; + +const ONE_MINUTE = 60 * 1000; + +/** Strategy for creating usernames using a catchall email address */ +export class CatchallGeneratorStrategy + implements GeneratorStrategy +{ + /** Instantiates the generation strategy + * @param usernameService generates a catchall address for a domain + */ + constructor(private usernameService: UsernameGenerationServiceAbstraction) {} + + /** {@link GeneratorStrategy.disk} */ + get disk() { + return CATCHALL_SETTINGS; + } + + /** {@link GeneratorStrategy.policy} */ + get policy() { + // Uses password generator since there aren't policies + // specific to usernames. + return PolicyType.PasswordGenerator; + } + + /** {@link GeneratorStrategy.cache_ms} */ + get cache_ms() { + return ONE_MINUTE; + } + + /** {@link GeneratorStrategy.evaluator} */ + evaluator(policy: Policy) { + if (policy.type !== this.policy) { + const details = `Expected: ${this.policy}. Received: ${policy.type}`; + throw Error("Mismatched policy type. " + details); + } + + return new DefaultPolicyEvaluator(); + } + + /** {@link GeneratorStrategy.generate} */ + generate(options: CatchallGenerationOptions) { + return this.usernameService.generateCatchall({ + catchallDomain: options.domain, + catchallType: options.type, + }); + } +} diff --git a/libs/common/src/tools/generator/username/index.ts b/libs/common/src/tools/generator/username/index.ts index 37d2180754..f9d5e3166e 100644 --- a/libs/common/src/tools/generator/username/index.ts +++ b/libs/common/src/tools/generator/username/index.ts @@ -1,4 +1,5 @@ export { EffUsernameGeneratorStrategy } from "./eff-username-generator-strategy"; +export { CatchallGeneratorStrategy } from "./catchall-generator-strategy"; export { SubaddressGeneratorStrategy } from "./subaddress-generator-strategy"; export { UsernameGeneratorOptions } from "./username-generation-options"; export { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";