From bb5f56838a8b9af599f8ab876ea9f0894a359c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Thu, 25 Jul 2024 12:08:18 -0400 Subject: [PATCH] [PM-8809] delete common generator code (#10218) --- .../generator-history.abstraction.ts | 53 -- ...enerator-navigation.service.abstraction.ts | 42 - .../generator-strategy.abstraction.ts | 42 - .../generator.service.abstraction.ts | 46 -- .../src/tools/generator/abstractions/index.ts | 5 - ...password-generation.service.abstraction.ts | 20 - .../policy-evaluator.abstraction.ts | 28 - .../generator/abstractions/randomizer.ts | 39 - ...username-generation.service.abstraction.ts | 15 - .../default-generator.service.spec.ts | 199 ----- .../generator/default-generator.service.ts | 72 -- .../default-policy-evaluator.spec.ts | 43 - .../generator/default-policy-evaluator.ts | 27 - .../src/tools/generator/generator-options.ts | 5 - .../src/tools/generator/generator-type.ts | 2 - .../history/generated-credential.spec.ts | 58 -- .../generator/history/generated-credential.ts | 47 -- .../src/tools/generator/history/index.ts | 2 - .../legacy-password-history-decryptor.ts | 29 - .../local-generator-history.service.spec.ts | 199 ----- .../local-generator-history.service.ts | 145 ---- .../src/tools/generator/history/options.ts | 10 - libs/common/src/tools/generator/index.ts | 6 - .../tools/generator/key-definition.spec.ts | 241 ------ .../src/tools/generator/key-definitions.ts | 234 ------ ...legacy-password-generation.service.spec.ts | 567 ------------- .../legacy-password-generation.service.ts | 426 ---------- ...legacy-username-generation.service.spec.ts | 748 ----------------- .../legacy-username-generation.service.ts | 390 --------- ...fault-generator-nativation.service.spec.ts | 100 --- .../default-generator-navigation.service.ts | 72 -- .../generator-navigation-evaluator.spec.ts | 64 -- .../generator-navigation-evaluator.ts | 43 - .../generator-navigation-policy.spec.ts | 63 -- .../navigation/generator-navigation-policy.ts | 39 - .../navigation/generator-navigation.ts | 26 - .../src/tools/generator/navigation/index.ts | 3 - libs/common/src/tools/generator/no-policy.ts | 2 - .../src/tools/generator/passphrase/index.ts | 8 - .../passphrase-generation-options.ts | 35 - ...phrase-generator-options-evaluator.spec.ts | 266 ------ .../passphrase-generator-options-evaluator.ts | 125 --- .../passphrase-generator-policy.spec.ts | 51 -- .../passphrase/passphrase-generator-policy.ts | 39 - .../passphrase-generator-strategy.spec.ts | 99 --- .../passphrase-generator-strategy.ts | 70 -- .../password/generated-password-history.ts | 9 - .../src/tools/generator/password/index.ts | 10 - .../password/password-generation-options.ts | 83 -- ...ssword-generator-options-evaluator.spec.ts | 770 ------------------ .../password-generator-options-evaluator.ts | 186 ----- .../password/password-generator-options.ts | 11 - .../password-generator-policy.spec.ts | 55 -- .../password/password-generator-policy.ts | 77 -- .../password-generator-strategy.spec.ts | 108 --- .../password/password-generator-strategy.ts | 128 --- libs/common/src/tools/generator/policies.ts | 48 -- libs/common/src/tools/generator/random.ts | 62 -- .../src/tools/generator/rx-operators.ts | 26 - .../username/catchall-generator-options.ts | 21 - .../catchall-generator-strategy.spec.ts | 75 -- .../username/catchall-generator-strategy.ts | 50 -- .../eff-username-generator-options.ts | 17 - .../eff-username-generator-strategy.spec.ts | 75 -- .../eff-username-generator-strategy.ts | 44 - .../email-forwarders/anon-addy-forwarder.ts | 49 -- .../duck-duck-go-forwarder.ts | 33 - .../email-forwarders/fastmail-forwarder.ts | 98 --- .../firefox-relay-forwarder.ts | 38 - .../forward-email-forwarder.ts | 49 -- .../email-forwarders/forwarder-options.ts | 25 - .../username/email-forwarders/forwarder.ts | 7 - .../username/email-forwarders/index.ts | 8 - .../simple-login-forwarder.ts | 44 - .../forwarder-generator-strategy.spec.ts | 109 --- .../username/forwarder-generator-strategy.ts | 92 --- .../username/forwarders/addy-io.spec.ts | 236 ------ .../generator/username/forwarders/addy-io.ts | 106 --- .../username/forwarders/duck-duck-go.spec.ts | 147 ---- .../username/forwarders/duck-duck-go.ts | 79 -- .../username/forwarders/fastmail.spec.ts | 284 ------- .../generator/username/forwarders/fastmail.ts | 156 ---- .../username/forwarders/firefox-relay.spec.ts | 150 ---- .../username/forwarders/firefox-relay.ts | 86 -- .../username/forwarders/forward-email.spec.ts | 280 ------- .../username/forwarders/forward-email.ts | 109 --- .../username/forwarders/mocks.jest.ts | 22 - .../username/forwarders/simple-login.spec.ts | 212 ----- .../username/forwarders/simple-login.ts | 93 --- .../src/tools/generator/username/index.ts | 5 - .../generator/username/options/constants.ts | 49 -- .../username/options/forwarder-options.ts | 72 -- .../username/options/generator-options.ts | 13 - .../tools/generator/username/options/index.ts | 1 - .../username/subaddress-generator-options.ts | 18 - .../subaddress-generator-strategy.spec.ts | 75 -- .../username/subaddress-generator-strategy.ts | 65 -- .../username/username-generation-options.ts | 23 - libs/common/src/tools/generator/util.ts | 41 - .../src/tools/generator/word-options.ts | 6 - 100 files changed, 9580 deletions(-) delete mode 100644 libs/common/src/tools/generator/abstractions/generator-history.abstraction.ts delete mode 100644 libs/common/src/tools/generator/abstractions/generator-navigation.service.abstraction.ts delete mode 100644 libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts delete mode 100644 libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts delete mode 100644 libs/common/src/tools/generator/abstractions/index.ts delete mode 100644 libs/common/src/tools/generator/abstractions/password-generation.service.abstraction.ts delete mode 100644 libs/common/src/tools/generator/abstractions/policy-evaluator.abstraction.ts delete mode 100644 libs/common/src/tools/generator/abstractions/randomizer.ts delete mode 100644 libs/common/src/tools/generator/abstractions/username-generation.service.abstraction.ts delete mode 100644 libs/common/src/tools/generator/default-generator.service.spec.ts delete mode 100644 libs/common/src/tools/generator/default-generator.service.ts delete mode 100644 libs/common/src/tools/generator/default-policy-evaluator.spec.ts delete mode 100644 libs/common/src/tools/generator/default-policy-evaluator.ts delete mode 100644 libs/common/src/tools/generator/generator-options.ts delete mode 100644 libs/common/src/tools/generator/generator-type.ts delete mode 100644 libs/common/src/tools/generator/history/generated-credential.spec.ts delete mode 100644 libs/common/src/tools/generator/history/generated-credential.ts delete mode 100644 libs/common/src/tools/generator/history/index.ts delete mode 100644 libs/common/src/tools/generator/history/legacy-password-history-decryptor.ts delete mode 100644 libs/common/src/tools/generator/history/local-generator-history.service.spec.ts delete mode 100644 libs/common/src/tools/generator/history/local-generator-history.service.ts delete mode 100644 libs/common/src/tools/generator/history/options.ts delete mode 100644 libs/common/src/tools/generator/index.ts delete mode 100644 libs/common/src/tools/generator/key-definition.spec.ts delete mode 100644 libs/common/src/tools/generator/key-definitions.ts delete mode 100644 libs/common/src/tools/generator/legacy-password-generation.service.spec.ts delete mode 100644 libs/common/src/tools/generator/legacy-password-generation.service.ts delete mode 100644 libs/common/src/tools/generator/legacy-username-generation.service.spec.ts delete mode 100644 libs/common/src/tools/generator/legacy-username-generation.service.ts delete mode 100644 libs/common/src/tools/generator/navigation/default-generator-nativation.service.spec.ts delete mode 100644 libs/common/src/tools/generator/navigation/default-generator-navigation.service.ts delete mode 100644 libs/common/src/tools/generator/navigation/generator-navigation-evaluator.spec.ts delete mode 100644 libs/common/src/tools/generator/navigation/generator-navigation-evaluator.ts delete mode 100644 libs/common/src/tools/generator/navigation/generator-navigation-policy.spec.ts delete mode 100644 libs/common/src/tools/generator/navigation/generator-navigation-policy.ts delete mode 100644 libs/common/src/tools/generator/navigation/generator-navigation.ts delete mode 100644 libs/common/src/tools/generator/navigation/index.ts delete mode 100644 libs/common/src/tools/generator/no-policy.ts delete mode 100644 libs/common/src/tools/generator/passphrase/index.ts delete mode 100644 libs/common/src/tools/generator/passphrase/passphrase-generation-options.ts delete mode 100644 libs/common/src/tools/generator/passphrase/passphrase-generator-options-evaluator.spec.ts delete mode 100644 libs/common/src/tools/generator/passphrase/passphrase-generator-options-evaluator.ts delete mode 100644 libs/common/src/tools/generator/passphrase/passphrase-generator-policy.spec.ts delete mode 100644 libs/common/src/tools/generator/passphrase/passphrase-generator-policy.ts delete mode 100644 libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts delete mode 100644 libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts delete mode 100644 libs/common/src/tools/generator/password/generated-password-history.ts delete mode 100644 libs/common/src/tools/generator/password/index.ts delete mode 100644 libs/common/src/tools/generator/password/password-generation-options.ts delete mode 100644 libs/common/src/tools/generator/password/password-generator-options-evaluator.spec.ts delete mode 100644 libs/common/src/tools/generator/password/password-generator-options-evaluator.ts delete mode 100644 libs/common/src/tools/generator/password/password-generator-options.ts delete mode 100644 libs/common/src/tools/generator/password/password-generator-policy.spec.ts delete mode 100644 libs/common/src/tools/generator/password/password-generator-policy.ts delete mode 100644 libs/common/src/tools/generator/password/password-generator-strategy.spec.ts delete mode 100644 libs/common/src/tools/generator/password/password-generator-strategy.ts delete mode 100644 libs/common/src/tools/generator/policies.ts delete mode 100644 libs/common/src/tools/generator/random.ts delete mode 100644 libs/common/src/tools/generator/rx-operators.ts delete mode 100644 libs/common/src/tools/generator/username/catchall-generator-options.ts delete mode 100644 libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts delete mode 100644 libs/common/src/tools/generator/username/catchall-generator-strategy.ts delete mode 100644 libs/common/src/tools/generator/username/eff-username-generator-options.ts delete mode 100644 libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts delete mode 100644 libs/common/src/tools/generator/username/eff-username-generator-strategy.ts delete mode 100644 libs/common/src/tools/generator/username/email-forwarders/anon-addy-forwarder.ts delete mode 100644 libs/common/src/tools/generator/username/email-forwarders/duck-duck-go-forwarder.ts delete mode 100644 libs/common/src/tools/generator/username/email-forwarders/fastmail-forwarder.ts delete mode 100644 libs/common/src/tools/generator/username/email-forwarders/firefox-relay-forwarder.ts delete mode 100644 libs/common/src/tools/generator/username/email-forwarders/forward-email-forwarder.ts delete mode 100644 libs/common/src/tools/generator/username/email-forwarders/forwarder-options.ts delete mode 100644 libs/common/src/tools/generator/username/email-forwarders/forwarder.ts delete mode 100644 libs/common/src/tools/generator/username/email-forwarders/index.ts delete mode 100644 libs/common/src/tools/generator/username/email-forwarders/simple-login-forwarder.ts delete mode 100644 libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts delete mode 100644 libs/common/src/tools/generator/username/forwarder-generator-strategy.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/addy-io.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/fastmail.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/firefox-relay.spec.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/firefox-relay.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/forward-email.spec.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/forward-email.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/mocks.jest.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts delete mode 100644 libs/common/src/tools/generator/username/forwarders/simple-login.ts delete mode 100644 libs/common/src/tools/generator/username/index.ts delete mode 100644 libs/common/src/tools/generator/username/options/constants.ts delete mode 100644 libs/common/src/tools/generator/username/options/forwarder-options.ts delete mode 100644 libs/common/src/tools/generator/username/options/generator-options.ts delete mode 100644 libs/common/src/tools/generator/username/options/index.ts delete mode 100644 libs/common/src/tools/generator/username/subaddress-generator-options.ts delete mode 100644 libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts delete mode 100644 libs/common/src/tools/generator/username/subaddress-generator-strategy.ts delete mode 100644 libs/common/src/tools/generator/username/username-generation-options.ts delete mode 100644 libs/common/src/tools/generator/util.ts delete mode 100644 libs/common/src/tools/generator/word-options.ts diff --git a/libs/common/src/tools/generator/abstractions/generator-history.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator-history.abstraction.ts deleted file mode 100644 index a1d358a13f8..00000000000 --- a/libs/common/src/tools/generator/abstractions/generator-history.abstraction.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Observable } from "rxjs"; - -import { UserId } from "../../../types/guid"; -import { GeneratedCredential, GeneratorCategory } from "../history"; - -/** Tracks the history of password generations. - * Each user gets their own store. - */ -export abstract class GeneratorHistoryService { - /** Tracks a new credential. When an item with the same `credential` value - * is found, this method does nothing. When the total number of items exceeds - * {@link HistoryServiceOptions.maxTotal}, then the oldest items exceeding the total - * are deleted. - * @param userId identifies the user storing the credential. - * @param credential stored by the history service. - * @param date when the credential was generated. If this is omitted, then the generator - * uses the date the credential was added to the store instead. - * @returns a promise that completes with the added credential. If the credential - * wasn't added, then the promise completes with `null`. - * @remarks this service is not suitable for use with vault items/ciphers. It models only - * a history of an individually generated credential, while a vault item's history - * may contain several credentials that are better modelled as atomic versions of the - * vault item itself. - */ - track: ( - userId: UserId, - credential: string, - category: GeneratorCategory, - date?: Date, - ) => Promise; - - /** Removes a matching credential from the history service. - * @param userId identifies the user taking the credential. - * @param credential to match in the history service. - * @returns A promise that completes with the credential read. If the credential wasn't found, - * the promise completes with null. - * @remarks this can be used to extract an entry when a credential is stored in the vault. - */ - take: (userId: UserId, credential: string) => Promise; - - /** Deletes a user's credential history. - * @param userId identifies the user taking the credential. - * @returns A promise that completes when the history is cleared. - */ - clear: (userId: UserId) => Promise; - - /** Lists all credentials for a user. - * @param userId identifies the user listing the credential. - * @remarks This field is eventually consistent with `track` and `take` operations. - * It is not guaranteed to immediately reflect those changes. - */ - credentials$: (userId: UserId) => Observable; -} diff --git a/libs/common/src/tools/generator/abstractions/generator-navigation.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator-navigation.service.abstraction.ts deleted file mode 100644 index e9fb7e0bb48..00000000000 --- a/libs/common/src/tools/generator/abstractions/generator-navigation.service.abstraction.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Observable } from "rxjs"; - -import { UserId } from "../../../types/guid"; -import { GeneratorNavigation } from "../navigation/generator-navigation"; -import { GeneratorNavigationPolicy } from "../navigation/generator-navigation-policy"; - -import { PolicyEvaluator } from "./policy-evaluator.abstraction"; - -/** Loads and stores generator navigational data - */ -export abstract class GeneratorNavigationService { - /** 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$: (userId: UserId) => Observable; - - /** Gets the default options. */ - defaults$: (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 - */ - 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: (userId: UserId, options: GeneratorNavigation) => Promise; - - /** Saves the navigation 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: (userId: UserId, options: GeneratorNavigation) => Promise; -} diff --git a/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts deleted file mode 100644 index 7bc0f21739f..00000000000 --- a/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Observable } from "rxjs"; - -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 { 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 { - /** 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; - - /** Gets the default options. */ - defaults$: (userId: UserId) => Observable; - - /** Identifies the policy enforced by the generator. */ - policy: PolicyType; - - /** Operator function that converts a policy collection observable to a single - * policy evaluator observable. - * @param policy The policy being evaluated. - * @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. - */ - toEvaluator: () => ( - source: Observable, - ) => Observable>; - - /** Generates credentials from the given options. - * @param options The options used to generate the credentials. - * @returns a promise that resolves to the generated credentials. - */ - generate: (options: Options) => Promise; -} diff --git a/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts deleted file mode 100644 index adb11655522..00000000000 --- a/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Observable } from "rxjs"; - -import { UserId } from "../../../types/guid"; - -import { PolicyEvaluator } from "./policy-evaluator.abstraction"; - -/** Generates credentials used for user authentication - * @typeParam Options the credential generation configuration - * @typeParam Policy the policy enforced by the generator - */ -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$: (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 - */ - evaluator$: (userId: UserId) => Observable>; - - /** Gets the default options. */ - defaults$: (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: (userId: UserId, options: Options) => Promise; - - /** Generates credentials - * @param options the options to generate credentials with - * @returns a promise that resolves with the generated credentials - */ - 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: (userId: UserId, options: Options) => Promise; -} diff --git a/libs/common/src/tools/generator/abstractions/index.ts b/libs/common/src/tools/generator/abstractions/index.ts deleted file mode 100644 index ef40dfd434f..00000000000 --- a/libs/common/src/tools/generator/abstractions/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { GeneratorHistoryService } from "./generator-history.abstraction"; -export { GeneratorNavigationService } from "./generator-navigation.service.abstraction"; -export { GeneratorService } from "./generator.service.abstraction"; -export { GeneratorStrategy } from "./generator-strategy.abstraction"; -export { PolicyEvaluator } from "./policy-evaluator.abstraction"; diff --git a/libs/common/src/tools/generator/abstractions/password-generation.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/password-generation.service.abstraction.ts deleted file mode 100644 index f6b5ca9cabe..00000000000 --- a/libs/common/src/tools/generator/abstractions/password-generation.service.abstraction.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Observable } from "rxjs"; - -import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options"; -import { GeneratedPasswordHistory } from "../password/generated-password-history"; -import { PasswordGeneratorOptions } from "../password/password-generator-options"; - -/** @deprecated Use {@link GeneratorService} with a password or passphrase {@link GeneratorStrategy} instead. */ -export abstract class PasswordGenerationServiceAbstraction { - generatePassword: (options: PasswordGeneratorOptions) => Promise; - generatePassphrase: (options: PasswordGeneratorOptions) => Promise; - getOptions: () => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>; - getOptions$: () => Observable<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>; - enforcePasswordGeneratorPoliciesOnOptions: ( - options: PasswordGeneratorOptions, - ) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>; - saveOptions: (options: PasswordGeneratorOptions) => Promise; - getHistory: () => Promise; - addHistory: (password: string) => Promise; - clear: (userId?: string) => Promise; -} diff --git a/libs/common/src/tools/generator/abstractions/policy-evaluator.abstraction.ts b/libs/common/src/tools/generator/abstractions/policy-evaluator.abstraction.ts deleted file mode 100644 index f4e9186c9c3..00000000000 --- a/libs/common/src/tools/generator/abstractions/policy-evaluator.abstraction.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** Applies policy to a generation request */ -export abstract class PolicyEvaluator { - /** The policy to enforce */ - policy: Policy; - - /** Returns true when a policy is being enforced by the evaluator. - * @remarks `applyPolicy` should be called when a policy is not in - * effect to enforce the application's default policy. - */ - policyInEffect: boolean; - - /** Apply policy to a set of options. - * @param options The options to build from. These options are not altered. - * @returns A complete generation request with policy applied. - * @remarks This method only applies policy overrides. - * Pass the result to `sanitize` to ensure consistency. - */ - applyPolicy: (options: PolicyTarget) => PolicyTarget; - - /** Ensures internal options consistency. - * @param options The options to cascade. These options are not altered. - * @returns A new generation request with cascade applied. - * @remarks This method fills null and undefined values by looking at - * pairs of flags and values (e.g. `number` and `minNumber`). If the flag - * and value are inconsistent, the flag cascades to the value. - */ - sanitize: (options: PolicyTarget) => PolicyTarget; -} diff --git a/libs/common/src/tools/generator/abstractions/randomizer.ts b/libs/common/src/tools/generator/abstractions/randomizer.ts deleted file mode 100644 index 33222477593..00000000000 --- a/libs/common/src/tools/generator/abstractions/randomizer.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { WordOptions } from "../word-options"; - -/** Entropy source for credential generation. */ -export interface Randomizer { - /** picks a random entry from a list. - * @param list random entry source. This must have at least one entry. - * @returns a promise that resolves with a random entry from the list. - */ - pick(list: Array): Promise; - - /** picks a random word from a list. - * @param list random entry source. This must have at least one entry. - * @param options customizes the output word - * @returns a promise that resolves with a random word from the list. - */ - pickWord(list: Array, options?: WordOptions): Promise; - - /** Shuffles a list of items - * @param list random entry source. This must have at least two entries. - * @param options.copy shuffles a copy of the input when this is true. - * Defaults to true. - * @returns a promise that resolves with the randomized list. - */ - shuffle(items: Array): Promise>; - - /** Generates a string containing random lowercase ASCII characters and numbers. - * @param length the number of characters to generate - * @returns a promise that resolves with the randomized string. - */ - chars(length: number): Promise; - - /** Selects an integer value from a range by randomly choosing it from - * a uniform distribution. - * @param min the minimum value in the range, inclusive. - * @param max the minimum value in the range, inclusive. - * @returns a promise that resolves with the randomized string. - */ - uniform(min: number, max: number): Promise; -} diff --git a/libs/common/src/tools/generator/abstractions/username-generation.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/username-generation.service.abstraction.ts deleted file mode 100644 index f11cbf02ed2..00000000000 --- a/libs/common/src/tools/generator/abstractions/username-generation.service.abstraction.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Observable } from "rxjs"; - -import { UsernameGeneratorOptions } from "../username/username-generation-options"; - -/** @deprecated Use {@link GeneratorService} with a username {@link GeneratorStrategy} instead. */ -export abstract class UsernameGenerationServiceAbstraction { - generateUsername: (options: UsernameGeneratorOptions) => Promise; - generateWord: (options: UsernameGeneratorOptions) => Promise; - generateSubaddress: (options: UsernameGeneratorOptions) => Promise; - generateCatchall: (options: UsernameGeneratorOptions) => Promise; - generateForwarded: (options: UsernameGeneratorOptions) => Promise; - getOptions: () => Promise; - getOptions$: () => Observable; - saveOptions: (options: UsernameGeneratorOptions) => 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 deleted file mode 100644 index 94d7d62fa8f..00000000000 --- a/libs/common/src/tools/generator/default-generator.service.spec.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * include structuredClone in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ - -import { mock } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom, map, pipe } from "rxjs"; - -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 { SingleUserState } from "../../platform/state"; -import { UserId } from "../../types/guid"; - -import { GeneratorStrategy, PolicyEvaluator } from "./abstractions"; -import { PasswordGenerationOptions } from "./password"; - -import { DefaultGeneratorService } from "."; - -function mockPolicyService(config?: { state?: BehaviorSubject }) { - const service = mock(); - - const stateValue = config?.state ?? new BehaviorSubject([null]); - service.getAll$.mockReturnValue(stateValue); - - return service; -} - -function mockGeneratorStrategy(config?: { - userState?: SingleUserState; - policy?: PolicyType; - evaluator?: any; - defaults?: 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`. - durableState: jest.fn(() => durableState), - defaults$: jest.fn(() => new BehaviorSubject(config?.defaults)), - policy: config?.policy ?? PolicyType.DisableSend, - toEvaluator: jest.fn(() => - pipe(map(() => config?.evaluator ?? mock>())), - ), - }); - - return strategy; -} - -const SomeUser = "some user" as UserId; -const AnotherUser = "another user" as UserId; - -describe("Password generator service", () => { - describe("options$", () => { - it("should retrieve durable state from the service", () => { - const policy = mockPolicyService(); - const userState = new FakeSingleUserState(SomeUser); - const strategy = mockGeneratorStrategy({ userState }); - const service = new DefaultGeneratorService(strategy, policy); - - const result = service.options$(SomeUser); - - expect(strategy.durableState).toHaveBeenCalledWith(SomeUser); - expect(result).toBe(userState.state$); - }); - }); - - describe("defaults$", () => { - it("should retrieve default state from the service", async () => { - const policy = mockPolicyService(); - const defaults = {}; - const strategy = mockGeneratorStrategy({ defaults }); - const service = new DefaultGeneratorService(strategy, policy); - - const result = await firstValueFrom(service.defaults$(SomeUser)); - - expect(strategy.defaults$).toHaveBeenCalledWith(SomeUser); - expect(result).toBe(defaults); - }); - }); - - describe("saveOptions()", () => { - it("should trigger an options$ update", async () => { - const policy = mockPolicyService(); - const userState = new FakeSingleUserState(SomeUser, { length: 9 }); - const strategy = mockGeneratorStrategy({ userState }); - const service = new DefaultGeneratorService(strategy, policy); - - await service.saveOptions(SomeUser, { length: 10 }); - await awaitAsync(); - const options = await firstValueFrom(service.options$(SomeUser)); - - expect(strategy.durableState).toHaveBeenCalledWith(SomeUser); - expect(options).toEqual({ length: 10 }); - }); - }); - - 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)); - - 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 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. The map is called multiple times, - // and the array shift ensures reference equality is maintained. - const firstEvaluator = mock>(); - const secondEvaluator = mock>(); - const evaluators = [firstEvaluator, secondEvaluator]; - strategy.toEvaluator.mockReturnValueOnce(pipe(map(() => evaluators.shift()))); - - // 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)); - - 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)); - - expect(policy.getAll$).toHaveBeenNthCalledWith(1, PolicyType.PasswordGenerator, SomeUser); - expect(policy.getAll$).toHaveBeenNthCalledWith(2, PolicyType.PasswordGenerator, AnotherUser); - }); - }); - - describe("enforcePolicy()", () => { - 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); - - await service.enforcePolicy(SomeUser, {}); - - expect(evaluator.applyPolicy).toHaveBeenCalled(); - expect(evaluator.sanitize).toHaveBeenCalled(); - }); - }); - - describe("generate()", () => { - it("should invoke the generation strategy", async () => { - const strategy = mockGeneratorStrategy(); - const policy = mockPolicyService(); - const service = new DefaultGeneratorService(strategy, policy); - - await service.generate({}); - - expect(strategy.generate).toHaveBeenCalled(); - }); - }); -}); diff --git a/libs/common/src/tools/generator/default-generator.service.ts b/libs/common/src/tools/generator/default-generator.service.ts deleted file mode 100644 index 9decb309bed..00000000000 --- a/libs/common/src/tools/generator/default-generator.service.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { firstValueFrom, Observable } from "rxjs"; - -// FIXME: use index.ts imports once policy abstractions and models -// implement ADR-0002 -import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; -import { UserId } from "../../types/guid"; - -import { GeneratorStrategy, GeneratorService, PolicyEvaluator } from "./abstractions"; - -/** {@link GeneratorServiceAbstraction} */ -export class DefaultGeneratorService implements GeneratorService { - /** Instantiates the generator service - * @param strategy tailors the service to a specific generator type - * (e.g. password, passphrase) - * @param policy provides the policy to enforce - */ - constructor( - private strategy: GeneratorStrategy, - private policy: PolicyService, - ) {} - - private _evaluators$ = new Map>>(); - - /** {@link GeneratorService.options$} */ - options$(userId: UserId) { - return this.strategy.durableState(userId).state$; - } - - /** {@link GeneratorService.defaults$} */ - defaults$(userId: UserId) { - return this.strategy.defaults$(userId); - } - - /** {@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) { - const evaluator$ = this.policy.getAll$(this.strategy.policy, userId).pipe( - // create the evaluator from the policies - this.strategy.toEvaluator(), - ); - - return evaluator$; - } - - /** {@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; - } - - /** {@link GeneratorService.generate} */ - async generate(options: Options): Promise { - return await this.strategy.generate(options); - } -} diff --git a/libs/common/src/tools/generator/default-policy-evaluator.spec.ts b/libs/common/src/tools/generator/default-policy-evaluator.spec.ts deleted file mode 100644 index d5d5e810285..00000000000 --- a/libs/common/src/tools/generator/default-policy-evaluator.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { DefaultPolicyEvaluator } from "./default-policy-evaluator"; - -describe("Password generator options builder", () => { - describe("policy", () => { - it("should return an empty object", () => { - const builder = new DefaultPolicyEvaluator(); - - expect(builder.policy).toEqual({}); - }); - }); - - describe("policyInEffect", () => { - it("should return false", () => { - const builder = new DefaultPolicyEvaluator(); - - expect(builder.policyInEffect).toEqual(false); - }); - }); - - describe("applyPolicy(options)", () => { - // All tests should freeze the options to ensure they are not modified - it("should return the input operations without altering them", () => { - const builder = new DefaultPolicyEvaluator(); - const options = Object.freeze({}); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions).toEqual(options); - }); - }); - - describe("sanitize(options)", () => { - // All tests should freeze the options to ensure they are not modified - it("should return the input options without altering them", () => { - const builder = new DefaultPolicyEvaluator(); - const options = Object.freeze({}); - - const sanitizedOptions = builder.sanitize(options); - - expect(sanitizedOptions).toEqual(options); - }); - }); -}); diff --git a/libs/common/src/tools/generator/default-policy-evaluator.ts b/libs/common/src/tools/generator/default-policy-evaluator.ts deleted file mode 100644 index d77ea2bbbc6..00000000000 --- a/libs/common/src/tools/generator/default-policy-evaluator.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { PolicyEvaluator } from "./abstractions"; -import { NoPolicy } from "./no-policy"; - -/** A policy evaluator that does not apply any policy */ -export class DefaultPolicyEvaluator - implements PolicyEvaluator -{ - /** {@link PolicyEvaluator.policy} */ - get policy() { - return {}; - } - - /** {@link PolicyEvaluator.policyInEffect} */ - get policyInEffect() { - return false; - } - - /** {@link PolicyEvaluator.applyPolicy} */ - applyPolicy(options: PolicyTarget) { - return options; - } - - /** {@link PolicyEvaluator.sanitize} */ - sanitize(options: PolicyTarget) { - return options; - } -} diff --git a/libs/common/src/tools/generator/generator-options.ts b/libs/common/src/tools/generator/generator-options.ts deleted file mode 100644 index d3d08025fae..00000000000 --- a/libs/common/src/tools/generator/generator-options.ts +++ /dev/null @@ -1,5 +0,0 @@ -// this export provided solely for backwards compatibility -export { - /** @deprecated use `GeneratorNavigation` from './navigation' instead. */ - GeneratorNavigation as GeneratorOptions, -} from "./navigation/generator-navigation"; diff --git a/libs/common/src/tools/generator/generator-type.ts b/libs/common/src/tools/generator/generator-type.ts deleted file mode 100644 index f17eeb9c92b..00000000000 --- a/libs/common/src/tools/generator/generator-type.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** The kind of credential being generated. */ -export type GeneratorType = "password" | "passphrase" | "username"; diff --git a/libs/common/src/tools/generator/history/generated-credential.spec.ts b/libs/common/src/tools/generator/history/generated-credential.spec.ts deleted file mode 100644 index 170030bad17..00000000000 --- a/libs/common/src/tools/generator/history/generated-credential.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { GeneratorCategory, GeneratedCredential } from "./"; - -describe("GeneratedCredential", () => { - describe("constructor", () => { - it("assigns credential", () => { - const result = new GeneratedCredential("example", "passphrase", new Date(100)); - - expect(result.credential).toEqual("example"); - }); - - it("assigns category", () => { - const result = new GeneratedCredential("example", "passphrase", new Date(100)); - - expect(result.category).toEqual("passphrase"); - }); - - it("passes through date parameters", () => { - const result = new GeneratedCredential("example", "password", new Date(100)); - - expect(result.generationDate).toEqual(new Date(100)); - }); - - it("converts numeric dates to Dates", () => { - const result = new GeneratedCredential("example", "password", 100); - - expect(result.generationDate).toEqual(new Date(100)); - }); - }); - - it("toJSON converts from a credential into a JSON object", () => { - const credential = new GeneratedCredential("example", "password", new Date(100)); - - const result = credential.toJSON(); - - expect(result).toEqual({ - credential: "example", - category: "password" as GeneratorCategory, - generationDate: 100, - }); - }); - - it("fromJSON converts Json objects into credentials", () => { - const jsonValue = { - credential: "example", - category: "password" as GeneratorCategory, - generationDate: 100, - }; - - const result = GeneratedCredential.fromJSON(jsonValue); - - expect(result).toBeInstanceOf(GeneratedCredential); - expect(result).toEqual({ - credential: "example", - category: "password", - generationDate: new Date(100), - }); - }); -}); diff --git a/libs/common/src/tools/generator/history/generated-credential.ts b/libs/common/src/tools/generator/history/generated-credential.ts deleted file mode 100644 index 59a9623bf7e..00000000000 --- a/libs/common/src/tools/generator/history/generated-credential.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Jsonify } from "type-fest"; - -import { GeneratorCategory } from "./options"; - -/** A credential generation result */ -export class GeneratedCredential { - /** - * Instantiates a generated credential - * @param credential The value of the generated credential (e.g. a password) - * @param category The kind of credential - * @param generationDate The date that the credential was generated. - * Numeric values should are interpreted using {@link Date.valueOf} - * semantics. - */ - constructor( - readonly credential: string, - readonly category: GeneratorCategory, - generationDate: Date | number, - ) { - if (typeof generationDate === "number") { - this.generationDate = new Date(generationDate); - } else { - this.generationDate = generationDate; - } - } - - /** The date that the credential was generated */ - generationDate: Date; - - /** Constructs a credential from its `toJSON` representation */ - static fromJSON(jsonValue: Jsonify) { - return new GeneratedCredential( - jsonValue.credential, - jsonValue.category, - jsonValue.generationDate, - ); - } - - /** Serializes a credential to a JSON-compatible object */ - toJSON() { - return { - credential: this.credential, - category: this.category, - generationDate: this.generationDate.valueOf(), - }; - } -} diff --git a/libs/common/src/tools/generator/history/index.ts b/libs/common/src/tools/generator/history/index.ts deleted file mode 100644 index 1952a849af2..00000000000 --- a/libs/common/src/tools/generator/history/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { GeneratorCategory } from "./options"; -export { GeneratedCredential } from "./generated-credential"; diff --git a/libs/common/src/tools/generator/history/legacy-password-history-decryptor.ts b/libs/common/src/tools/generator/history/legacy-password-history-decryptor.ts deleted file mode 100644 index 6c59ca837cd..00000000000 --- a/libs/common/src/tools/generator/history/legacy-password-history-decryptor.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CryptoService } from "../../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; -import { EncString } from "../../../platform/models/domain/enc-string"; -import { UserId } from "../../../types/guid"; -import { GeneratedPasswordHistory } from "../password/generated-password-history"; - -/** Strategy that decrypts a password history */ -export class LegacyPasswordHistoryDecryptor { - constructor( - private userId: UserId, - private cryptoService: CryptoService, - private encryptService: EncryptService, - ) {} - - /** Decrypts a password history. */ - async decrypt(history: GeneratedPasswordHistory[]): Promise { - const key = await this.cryptoService.getUserKey(this.userId); - - const promises = (history ?? []).map(async (item) => { - const encrypted = new EncString(item.password); - const decrypted = await this.encryptService.decryptToUtf8(encrypted, key); - return new GeneratedPasswordHistory(decrypted, item.date); - }); - - const decrypted = await Promise.all(promises); - - return decrypted; - } -} diff --git a/libs/common/src/tools/generator/history/local-generator-history.service.spec.ts b/libs/common/src/tools/generator/history/local-generator-history.service.spec.ts deleted file mode 100644 index 9640016584a..00000000000 --- a/libs/common/src/tools/generator/history/local-generator-history.service.spec.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; - -import { FakeStateProvider, awaitAsync, mockAccountServiceWith } from "../../../../spec"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; -import { EncString } from "../../../platform/models/domain/enc-string"; -import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; -import { CsprngArray } from "../../../types/csprng"; -import { UserId } from "../../../types/guid"; -import { UserKey } from "../../../types/key"; - -import { LocalGeneratorHistoryService } from "./local-generator-history.service"; - -const SomeUser = "SomeUser" as UserId; -const AnotherUser = "AnotherUser" as UserId; - -describe("LocalGeneratorHistoryService", () => { - const encryptService = mock(); - const keyService = mock(); - const userKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as UserKey; - - beforeEach(() => { - encryptService.encrypt.mockImplementation((p) => Promise.resolve(p as unknown as EncString)); - encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c.encryptedString)); - keyService.getUserKey.mockImplementation(() => Promise.resolve(userKey)); - keyService.getInMemoryUserKeyFor$.mockImplementation(() => of(true as unknown as UserKey)); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("credential$", () => { - it("returns an empty list when no credentials are stored", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - - const result = await firstValueFrom(history.credentials$(SomeUser)); - - expect(result).toEqual([]); - }); - }); - - describe("track", () => { - it("stores a password", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - - await history.track(SomeUser, "example", "password"); - await awaitAsync(); - const [result] = await firstValueFrom(history.credentials$(SomeUser)); - - expect(result).toMatchObject({ credential: "example", category: "password" }); - }); - - it("stores a passphrase", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - - await history.track(SomeUser, "example", "passphrase"); - await awaitAsync(); - const [result] = await firstValueFrom(history.credentials$(SomeUser)); - - expect(result).toMatchObject({ credential: "example", category: "passphrase" }); - }); - - it("stores a specific date when one is provided", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - - await history.track(SomeUser, "example", "password", new Date(100)); - await awaitAsync(); - const [result] = await firstValueFrom(history.credentials$(SomeUser)); - - expect(result).toEqual({ - credential: "example", - category: "password", - generationDate: new Date(100), - }); - }); - - it("skips storing a credential when it's already stored (ignores category)", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - - await history.track(SomeUser, "example", "password"); - await history.track(SomeUser, "example", "password"); - await history.track(SomeUser, "example", "passphrase"); - await awaitAsync(); - const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser)); - - expect(firstResult).toMatchObject({ credential: "example", category: "password" }); - expect(secondResult).toBeUndefined(); - }); - - it("stores multiple credentials when the credential value is different", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - - await history.track(SomeUser, "secondResult", "password"); - await history.track(SomeUser, "firstResult", "password"); - await awaitAsync(); - const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser)); - - expect(firstResult).toMatchObject({ credential: "firstResult", category: "password" }); - expect(secondResult).toMatchObject({ credential: "secondResult", category: "password" }); - }); - - it("removes history items exceeding maxTotal configuration", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider, { - maxTotal: 1, - }); - - await history.track(SomeUser, "removed result", "password"); - await history.track(SomeUser, "example", "password"); - await awaitAsync(); - const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser)); - - expect(firstResult).toMatchObject({ credential: "example", category: "password" }); - expect(secondResult).toBeUndefined(); - }); - - it("stores history items in per-user collections", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider, { - maxTotal: 1, - }); - - await history.track(SomeUser, "some user example", "password"); - await history.track(AnotherUser, "another user example", "password"); - await awaitAsync(); - const [someFirstResult, someSecondResult] = await firstValueFrom( - history.credentials$(SomeUser), - ); - const [anotherFirstResult, anotherSecondResult] = await firstValueFrom( - history.credentials$(AnotherUser), - ); - - expect(someFirstResult).toMatchObject({ - credential: "some user example", - category: "password", - }); - expect(someSecondResult).toBeUndefined(); - expect(anotherFirstResult).toMatchObject({ - credential: "another user example", - category: "password", - }); - expect(anotherSecondResult).toBeUndefined(); - }); - }); - - describe("take", () => { - it("returns null when there are no credentials stored", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - - const result = await history.take(SomeUser, "example"); - - expect(result).toBeNull(); - }); - - it("returns null when the credential wasn't found", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - await history.track(SomeUser, "example", "password"); - - const result = await history.take(SomeUser, "not found"); - - expect(result).toBeNull(); - }); - - it("returns a matching credential", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - await history.track(SomeUser, "example", "password"); - - const result = await history.take(SomeUser, "example"); - - expect(result).toMatchObject({ - credential: "example", - category: "password", - }); - }); - - it("removes a matching credential", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); - await history.track(SomeUser, "example", "password"); - - await history.take(SomeUser, "example"); - await awaitAsync(); - const results = await firstValueFrom(history.credentials$(SomeUser)); - - expect(results).toEqual([]); - }); - }); -}); diff --git a/libs/common/src/tools/generator/history/local-generator-history.service.ts b/libs/common/src/tools/generator/history/local-generator-history.service.ts deleted file mode 100644 index 85aa599946f..00000000000 --- a/libs/common/src/tools/generator/history/local-generator-history.service.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { map } from "rxjs"; - -import { CryptoService } from "../../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; -import { SingleUserState, StateProvider } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; -import { BufferedState } from "../../state/buffered-state"; -import { PaddedDataPacker } from "../../state/padded-data-packer"; -import { SecretState } from "../../state/secret-state"; -import { UserKeyEncryptor } from "../../state/user-key-encryptor"; -import { GeneratorHistoryService } from "../abstractions/generator-history.abstraction"; -import { GENERATOR_HISTORY, GENERATOR_HISTORY_BUFFER } from "../key-definitions"; - -import { GeneratedCredential } from "./generated-credential"; -import { LegacyPasswordHistoryDecryptor } from "./legacy-password-history-decryptor"; -import { GeneratorCategory, HistoryServiceOptions } from "./options"; - -const OPTIONS_FRAME_SIZE = 2048; - -/** Tracks the history of password generations local to a device. - * {@link GeneratorHistoryService} - */ -export class LocalGeneratorHistoryService extends GeneratorHistoryService { - constructor( - private readonly encryptService: EncryptService, - private readonly keyService: CryptoService, - private readonly stateProvider: StateProvider, - private readonly options: HistoryServiceOptions = { maxTotal: 100 }, - ) { - super(); - } - - private _credentialStates = new Map>(); - - /** {@link GeneratorHistoryService.track} */ - track = async (userId: UserId, credential: string, category: GeneratorCategory, date?: Date) => { - const state = this.getCredentialState(userId); - let result: GeneratedCredential = null; - - await state.update( - (credentials) => { - credentials = credentials ?? []; - - // add the result - result = new GeneratedCredential(credential, category, date ?? Date.now()); - credentials.unshift(result); - - // trim history - const removeAt = Math.max(0, this.options.maxTotal); - credentials.splice(removeAt, Infinity); - - return credentials; - }, - { - shouldUpdate: (credentials) => - !(credentials?.some((f) => f.credential === credential) ?? false), - }, - ); - - return result; - }; - - /** {@link GeneratorHistoryService.take} */ - take = async (userId: UserId, credential: string) => { - const state = this.getCredentialState(userId); - let credentialIndex: number; - let result: GeneratedCredential = null; - - await state.update( - (credentials) => { - credentials = credentials ?? []; - - [result] = credentials.splice(credentialIndex, 1); - return credentials; - }, - { - shouldUpdate: (credentials) => { - credentialIndex = credentials?.findIndex((f) => f.credential === credential) ?? -1; - return credentialIndex >= 0; - }, - }, - ); - - return result; - }; - - /** {@link GeneratorHistoryService.take} */ - clear = async (userId: UserId) => { - const state = this.getCredentialState(userId); - const result = (await state.update(() => null)) ?? []; - return result; - }; - - /** {@link GeneratorHistoryService.credentials$} */ - credentials$ = (userId: UserId) => { - return this.getCredentialState(userId).state$.pipe(map((credentials) => credentials ?? [])); - }; - - private getCredentialState(userId: UserId) { - let state = this._credentialStates.get(userId); - - if (!state) { - state = this.createSecretState(userId); - this._credentialStates.set(userId, state); - } - - return state; - } - - private createSecretState(userId: UserId): SingleUserState { - // construct the encryptor - const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE); - const encryptor = new UserKeyEncryptor(this.encryptService, this.keyService, packer); - - // construct the durable state - const state = SecretState.from< - GeneratedCredential[], - number, - GeneratedCredential, - Record, - GeneratedCredential - >(userId, GENERATOR_HISTORY, this.stateProvider, encryptor); - - // decryptor is just an algorithm, but it can't run until the key is available; - // providing it via an observable makes running it early impossible - const decryptor = new LegacyPasswordHistoryDecryptor( - userId, - this.keyService, - this.encryptService, - ); - const decryptor$ = this.keyService - .getInMemoryUserKeyFor$(userId) - .pipe(map((key) => key && decryptor)); - - // move data from the old password history once decryptor is available - const buffer = new BufferedState( - this.stateProvider, - GENERATOR_HISTORY_BUFFER, - state, - decryptor$, - ); - - return buffer; - } -} diff --git a/libs/common/src/tools/generator/history/options.ts b/libs/common/src/tools/generator/history/options.ts deleted file mode 100644 index 53716ec33ab..00000000000 --- a/libs/common/src/tools/generator/history/options.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** Kinds of credentials that can be stored by the history service */ -export type GeneratorCategory = "password" | "passphrase"; - -/** Configuration options for the history service */ -export type HistoryServiceOptions = { - /** Total number of records retained across all types. - * @remarks Setting this to 0 or less disables history completely. - * */ - maxTotal: number; -}; diff --git a/libs/common/src/tools/generator/index.ts b/libs/common/src/tools/generator/index.ts deleted file mode 100644 index 9df054a502b..00000000000 --- a/libs/common/src/tools/generator/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./abstractions/index"; -export * from "./password/index"; - -export { DefaultGeneratorService } from "./default-generator.service"; -export { legacyPasswordGenerationServiceFactory } from "./legacy-password-generation.service"; -export { legacyUsernameGenerationServiceFactory } from "./legacy-username-generation.service"; diff --git a/libs/common/src/tools/generator/key-definition.spec.ts b/libs/common/src/tools/generator/key-definition.spec.ts deleted file mode 100644 index d4992af0b11..00000000000 --- a/libs/common/src/tools/generator/key-definition.spec.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { mock } from "jest-mock-extended"; - -import { GeneratedCredential } from "./history"; -import { LegacyPasswordHistoryDecryptor } from "./history/legacy-password-history-decryptor"; -import { - EFF_USERNAME_SETTINGS, - CATCHALL_SETTINGS, - SUBADDRESS_SETTINGS, - PASSPHRASE_SETTINGS, - PASSWORD_SETTINGS, - SIMPLE_LOGIN_FORWARDER, - FORWARD_EMAIL_FORWARDER, - FIREFOX_RELAY_FORWARDER, - FASTMAIL_FORWARDER, - DUCK_DUCK_GO_FORWARDER, - ADDY_IO_FORWARDER, - GENERATOR_SETTINGS, - ADDY_IO_BUFFER, - DUCK_DUCK_GO_BUFFER, - FASTMAIL_BUFFER, - FIREFOX_RELAY_BUFFER, - FORWARD_EMAIL_BUFFER, - SIMPLE_LOGIN_BUFFER, - GENERATOR_HISTORY_BUFFER, -} from "./key-definitions"; -import { GeneratedPasswordHistory } from "./password"; - -describe("Key definitions", () => { - describe("GENERATOR_SETTINGS", () => { - it("should pass through deserialization", () => { - const value = {}; - const result = GENERATOR_SETTINGS.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("PASSWORD_SETTINGS", () => { - it("should pass through deserialization", () => { - const value = {}; - const result = PASSWORD_SETTINGS.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("PASSPHRASE_SETTINGS", () => { - it("should pass through deserialization", () => { - const value = {}; - const result = PASSPHRASE_SETTINGS.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("EFF_USERNAME_SETTINGS", () => { - it("should pass through deserialization", () => { - const value = { website: null as string }; - const result = EFF_USERNAME_SETTINGS.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("CATCHALL_SETTINGS", () => { - it("should pass through deserialization", () => { - const value = { website: null as string }; - const result = CATCHALL_SETTINGS.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("SUBADDRESS_SETTINGS", () => { - it("should pass through deserialization", () => { - const value = { website: null as string }; - const result = SUBADDRESS_SETTINGS.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("ADDY_IO_FORWARDER", () => { - it("should pass through deserialization", () => { - const value: any = {}; - const result = ADDY_IO_FORWARDER.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("DUCK_DUCK_GO_FORWARDER", () => { - it("should pass through deserialization", () => { - const value: any = {}; - const result = DUCK_DUCK_GO_FORWARDER.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("FASTMAIL_FORWARDER", () => { - it("should pass through deserialization", () => { - const value: any = {}; - const result = FASTMAIL_FORWARDER.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("FIREFOX_RELAY_FORWARDER", () => { - it("should pass through deserialization", () => { - const value: any = {}; - const result = FIREFOX_RELAY_FORWARDER.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("FORWARD_EMAIL_FORWARDER", () => { - it("should pass through deserialization", () => { - const value: any = {}; - const result = FORWARD_EMAIL_FORWARDER.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("SIMPLE_LOGIN_FORWARDER", () => { - it("should pass through deserialization", () => { - const value: any = {}; - const result = SIMPLE_LOGIN_FORWARDER.deserializer(value); - expect(result).toBe(value); - }); - }); - - describe("ADDY_IO_BUFFER", () => { - it("should pass through deserialization", () => { - const value: any = {}; - - const result = ADDY_IO_BUFFER.options.deserializer(value); - - expect(result).toBe(value); - }); - }); - - describe("DUCK_DUCK_GO_BUFFER", () => { - it("should pass through deserialization", () => { - const value: any = {}; - - const result = DUCK_DUCK_GO_BUFFER.options.deserializer(value); - - expect(result).toBe(value); - }); - }); - - describe("FASTMAIL_BUFFER", () => { - it("should pass through deserialization", () => { - const value: any = {}; - - const result = FASTMAIL_BUFFER.options.deserializer(value); - - expect(result).toBe(value); - }); - }); - - describe("FIREFOX_RELAY_BUFFER", () => { - it("should pass through deserialization", () => { - const value: any = {}; - - const result = FIREFOX_RELAY_BUFFER.options.deserializer(value); - - expect(result).toBe(value); - }); - }); - - describe("FORWARD_EMAIL_BUFFER", () => { - it("should pass through deserialization", () => { - const value: any = {}; - - const result = FORWARD_EMAIL_BUFFER.options.deserializer(value); - - expect(result).toBe(value); - }); - }); - - describe("SIMPLE_LOGIN_BUFFER", () => { - it("should pass through deserialization", () => { - const value: any = {}; - - const result = SIMPLE_LOGIN_BUFFER.options.deserializer(value); - - expect(result).toBe(value); - }); - }); - - describe("GENERATOR_HISTORY_BUFFER", () => { - describe("options.deserializer", () => { - it("should deserialize generated password history", () => { - const value: any = [{ password: "foo", date: 1 }]; - - const [result] = GENERATOR_HISTORY_BUFFER.options.deserializer(value); - - expect(result).toEqual(value[0]); - expect(result).toBeInstanceOf(GeneratedPasswordHistory); - }); - - it.each([[undefined], [null]])("should ignore nullish (= %p) history", (value: any) => { - const result = GENERATOR_HISTORY_BUFFER.options.deserializer(value); - - expect(result).toEqual(undefined); - }); - }); - - it("should map generated password history to generated credentials", async () => { - const value: any = [new GeneratedPasswordHistory("foo", 1)]; - const decryptor = mock({ - decrypt(value) { - return Promise.resolve(value); - }, - }); - - const [result] = await GENERATOR_HISTORY_BUFFER.map(value, decryptor); - - expect(result).toEqual({ - credential: "foo", - category: "password", - generationDate: new Date(1), - }); - expect(result).toBeInstanceOf(GeneratedCredential); - }); - - describe("isValid", () => { - it("should accept histories with at least one entry", async () => { - const value: any = [new GeneratedPasswordHistory("foo", 1)]; - const decryptor = {} as any; - - const result = await GENERATOR_HISTORY_BUFFER.isValid(value, decryptor); - - expect(result).toEqual(true); - }); - - it("should reject histories with no entries", async () => { - const value: any = []; - const decryptor = {} as any; - - const result = await GENERATOR_HISTORY_BUFFER.isValid(value, decryptor); - - expect(result).toEqual(false); - }); - }); - }); -}); diff --git a/libs/common/src/tools/generator/key-definitions.ts b/libs/common/src/tools/generator/key-definitions.ts deleted file mode 100644 index ccf1ca0d520..00000000000 --- a/libs/common/src/tools/generator/key-definitions.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { Jsonify } from "type-fest"; - -import { GENERATOR_DISK, UserKeyDefinition } from "../../platform/state"; -import { BufferedKeyDefinition } from "../state/buffered-key-definition"; -import { SecretClassifier } from "../state/secret-classifier"; -import { SecretKeyDefinition } from "../state/secret-key-definition"; - -import { GeneratedCredential } from "./history/generated-credential"; -import { LegacyPasswordHistoryDecryptor } from "./history/legacy-password-history-decryptor"; -import { GeneratorNavigation } from "./navigation/generator-navigation"; -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 { - ApiOptions, - EmailDomainOptions, - EmailPrefixOptions, - SelfHostedApiOptions, -} from "./username/options/forwarder-options"; -import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; - -/** plaintext password generation options */ -export const GENERATOR_SETTINGS = new UserKeyDefinition( - GENERATOR_DISK, - "generatorSettings", - { - deserializer: (value) => value, - clearOn: ["logout"], - }, -); - -/** plaintext password generation options */ -export const PASSWORD_SETTINGS = new UserKeyDefinition( - GENERATOR_DISK, - "passwordGeneratorSettings", - { - deserializer: (value) => value, - clearOn: [], - }, -); - -/** plaintext passphrase generation options */ -export const PASSPHRASE_SETTINGS = new UserKeyDefinition( - GENERATOR_DISK, - "passphraseGeneratorSettings", - { - deserializer: (value) => value, - clearOn: [], - }, -); - -/** plaintext username generation options */ -export const EFF_USERNAME_SETTINGS = new UserKeyDefinition( - GENERATOR_DISK, - "effUsernameGeneratorSettings", - { - deserializer: (value) => value, - clearOn: [], - }, -); - -/** plaintext configuration for a domain catch-all address. */ -export const CATCHALL_SETTINGS = new UserKeyDefinition( - GENERATOR_DISK, - "catchallGeneratorSettings", - { - deserializer: (value) => value, - clearOn: [], - }, -); - -/** plaintext configuration for an email subaddress. */ -export const SUBADDRESS_SETTINGS = new UserKeyDefinition( - GENERATOR_DISK, - "subaddressGeneratorSettings", - { - deserializer: (value) => value, - clearOn: [], - }, -); - -/** backing store configuration for {@link Forwarders.AddyIo} */ -export const ADDY_IO_FORWARDER = new UserKeyDefinition( - GENERATOR_DISK, - "addyIoForwarder", - { - deserializer: (value) => value, - clearOn: [], - }, -); - -/** backing store configuration for {@link Forwarders.DuckDuckGo} */ -export const DUCK_DUCK_GO_FORWARDER = new UserKeyDefinition( - GENERATOR_DISK, - "duckDuckGoForwarder", - { - deserializer: (value) => value, - clearOn: [], - }, -); - -/** backing store configuration for {@link Forwarders.FastMail} */ -export const FASTMAIL_FORWARDER = new UserKeyDefinition( - GENERATOR_DISK, - "fastmailForwarder", - { - deserializer: (value) => value, - clearOn: [], - }, -); - -/** backing store configuration for {@link Forwarders.FireFoxRelay} */ -export const FIREFOX_RELAY_FORWARDER = new UserKeyDefinition( - GENERATOR_DISK, - "firefoxRelayForwarder", - { - deserializer: (value) => value, - clearOn: [], - }, -); - -/** backing store configuration for {@link Forwarders.ForwardEmail} */ -export const FORWARD_EMAIL_FORWARDER = new UserKeyDefinition( - GENERATOR_DISK, - "forwardEmailForwarder", - { - deserializer: (value) => value, - clearOn: [], - }, -); - -/** backing store configuration for {@link forwarders.SimpleLogin} */ -export const SIMPLE_LOGIN_FORWARDER = new UserKeyDefinition( - GENERATOR_DISK, - "simpleLoginForwarder", - { - deserializer: (value) => value, - clearOn: [], - }, -); - -/** backing store configuration for {@link Forwarders.AddyIo} */ -export const ADDY_IO_BUFFER = new BufferedKeyDefinition( - GENERATOR_DISK, - "addyIoBuffer", - { - deserializer: (value) => value, - clearOn: ["logout"], - }, -); - -/** backing store configuration for {@link Forwarders.DuckDuckGo} */ -export const DUCK_DUCK_GO_BUFFER = new BufferedKeyDefinition( - GENERATOR_DISK, - "duckDuckGoBuffer", - { - deserializer: (value) => value, - clearOn: ["logout"], - }, -); - -/** backing store configuration for {@link Forwarders.FastMail} */ -export const FASTMAIL_BUFFER = new BufferedKeyDefinition( - GENERATOR_DISK, - "fastmailBuffer", - { - deserializer: (value) => value, - clearOn: ["logout"], - }, -); - -/** backing store configuration for {@link Forwarders.FireFoxRelay} */ -export const FIREFOX_RELAY_BUFFER = new BufferedKeyDefinition( - GENERATOR_DISK, - "firefoxRelayBuffer", - { - deserializer: (value) => value, - clearOn: ["logout"], - }, -); - -/** backing store configuration for {@link Forwarders.ForwardEmail} */ -export const FORWARD_EMAIL_BUFFER = new BufferedKeyDefinition( - GENERATOR_DISK, - "forwardEmailBuffer", - { - deserializer: (value) => value, - clearOn: ["logout"], - }, -); - -/** backing store configuration for {@link forwarders.SimpleLogin} */ -export const SIMPLE_LOGIN_BUFFER = new BufferedKeyDefinition( - GENERATOR_DISK, - "simpleLoginBuffer", - { - deserializer: (value) => value, - clearOn: ["logout"], - }, -); - -/** encrypted password generation history */ -export const GENERATOR_HISTORY = SecretKeyDefinition.array( - GENERATOR_DISK, - "localGeneratorHistory", - SecretClassifier.allSecret(), - { - deserializer: GeneratedCredential.fromJSON, - clearOn: ["logout"], - }, -); - -/** encrypted password generation history subject to migration */ -export const GENERATOR_HISTORY_BUFFER = new BufferedKeyDefinition< - GeneratedPasswordHistory[], - GeneratedCredential[], - LegacyPasswordHistoryDecryptor ->(GENERATOR_DISK, "localGeneratorHistoryBuffer", { - deserializer(history) { - const items = history as Jsonify[]; - return items?.map((h) => new GeneratedPasswordHistory(h.password, h.date)); - }, - async isValid(history) { - return history.length ? true : false; - }, - async map(history, decryptor) { - const credentials = await decryptor.decrypt(history); - const mapped = credentials.map((c) => new GeneratedCredential(c.password, "password", c.date)); - return mapped; - }, - clearOn: ["logout"], -}); diff --git a/libs/common/src/tools/generator/legacy-password-generation.service.spec.ts b/libs/common/src/tools/generator/legacy-password-generation.service.spec.ts deleted file mode 100644 index c86bb9f8b04..00000000000 --- a/libs/common/src/tools/generator/legacy-password-generation.service.spec.ts +++ /dev/null @@ -1,567 +0,0 @@ -/** - * include structuredClone in test environment. - * @jest-environment ../../../shared/test.environment.ts - */ -import { mock } from "jest-mock-extended"; -import { of } from "rxjs"; - -import { mockAccountServiceWith } from "../../../spec"; -import { UserId } from "../../types/guid"; - -import { - GeneratorHistoryService, - GeneratorNavigationService, - GeneratorService, -} from "./abstractions"; -import { GeneratedCredential } from "./history"; -import { LegacyPasswordGenerationService } from "./legacy-password-generation.service"; -import { DefaultGeneratorNavigation, GeneratorNavigation } from "./navigation/generator-navigation"; -import { GeneratorNavigationEvaluator } from "./navigation/generator-navigation-evaluator"; -import { GeneratorNavigationPolicy } from "./navigation/generator-navigation-policy"; -import { - DefaultPassphraseGenerationOptions, - PassphraseGenerationOptions, - PassphraseGeneratorOptionsEvaluator, - PassphraseGeneratorPolicy, -} from "./passphrase"; -import { DisabledPassphraseGeneratorPolicy } from "./passphrase/passphrase-generator-policy"; -import { - DefaultPasswordGenerationOptions, - GeneratedPasswordHistory, - PasswordGenerationOptions, - PasswordGeneratorOptions, - PasswordGeneratorOptionsEvaluator, - PasswordGeneratorPolicy, -} from "./password"; -import { DisabledPasswordGeneratorPolicy } from "./password/password-generator-policy"; - -const SomeUser = "some user" as UserId; - -function createPassphraseGenerator( - options: PassphraseGenerationOptions = {}, - policy: PassphraseGeneratorPolicy = DisabledPassphraseGeneratorPolicy, -) { - let savedOptions = options; - const generator = mock>({ - evaluator$(id: UserId) { - const evaluator = new PassphraseGeneratorOptionsEvaluator(policy); - return of(evaluator); - }, - options$(id: UserId) { - return of(savedOptions); - }, - defaults$(id: UserId) { - return of(DefaultPassphraseGenerationOptions); - }, - saveOptions(userId, options) { - savedOptions = options; - return Promise.resolve(); - }, - }); - - return generator; -} - -function createPasswordGenerator( - options: PasswordGenerationOptions = {}, - policy: PasswordGeneratorPolicy = DisabledPasswordGeneratorPolicy, -) { - let savedOptions = options; - const generator = mock>({ - evaluator$(id: UserId) { - const evaluator = new PasswordGeneratorOptionsEvaluator(policy); - return of(evaluator); - }, - options$(id: UserId) { - return of(savedOptions); - }, - defaults$(id: UserId) { - return of(DefaultPasswordGenerationOptions); - }, - saveOptions(userId, options) { - savedOptions = options; - return Promise.resolve(); - }, - }); - - return generator; -} - -function createNavigationGenerator( - options: GeneratorNavigation = {}, - policy: GeneratorNavigationPolicy = {}, -) { - let savedOptions = options; - const generator = mock({ - evaluator$(id: UserId) { - const evaluator = new GeneratorNavigationEvaluator(policy); - return of(evaluator); - }, - options$(id: UserId) { - return of(savedOptions); - }, - defaults$(id: UserId) { - return of(DefaultGeneratorNavigation); - }, - saveOptions: jest.fn((userId, options) => { - savedOptions = options; - return Promise.resolve(); - }), - }); - - return generator; -} - -describe("LegacyPasswordGenerationService", () => { - // NOTE: in all tests, `null` constructor arguments are not used by the test. - // They're set to `null` to avoid setting up unnecessary mocks. - - describe("generatePassword", () => { - it("invokes the inner password generator to generate passwords", async () => { - const innerPassword = createPasswordGenerator(); - const generator = new LegacyPasswordGenerationService(null, null, innerPassword, null, null); - const options = { type: "password" } as PasswordGeneratorOptions; - - await generator.generatePassword(options); - - expect(innerPassword.generate).toHaveBeenCalledWith(options); - }); - - it("invokes the inner passphrase generator to generate passphrases", async () => { - const innerPassphrase = createPassphraseGenerator(); - const generator = new LegacyPasswordGenerationService( - null, - null, - null, - innerPassphrase, - null, - ); - const options = { type: "passphrase" } as PasswordGeneratorOptions; - - await generator.generatePassword(options); - - expect(innerPassphrase.generate).toHaveBeenCalledWith(options); - }); - }); - - describe("generatePassphrase", () => { - it("invokes the inner passphrase generator", async () => { - const innerPassphrase = createPassphraseGenerator(); - const generator = new LegacyPasswordGenerationService( - null, - null, - null, - innerPassphrase, - null, - ); - const options = {} as PasswordGeneratorOptions; - - await generator.generatePassphrase(options); - - expect(innerPassphrase.generate).toHaveBeenCalledWith(options); - }); - }); - - describe("getOptions", () => { - it("combines options from its inner services", async () => { - const innerPassword = createPasswordGenerator({ - length: 29, - minLength: 20, - ambiguous: false, - uppercase: true, - minUppercase: 1, - lowercase: false, - minLowercase: 2, - number: true, - minNumber: 3, - special: false, - minSpecial: 0, - }); - const innerPassphrase = createPassphraseGenerator({ - numWords: 10, - wordSeparator: "-", - capitalize: true, - includeNumber: false, - }); - const navigation = createNavigationGenerator({ - type: "passphrase", - username: "word", - forwarder: "simplelogin", - }); - const accountService = mockAccountServiceWith(SomeUser); - const generator = new LegacyPasswordGenerationService( - accountService, - navigation, - innerPassword, - innerPassphrase, - null, - ); - - const [result] = await generator.getOptions(); - - expect(result).toEqual({ - type: "passphrase", - length: 29, - minLength: 5, - ambiguous: false, - uppercase: true, - minUppercase: 1, - lowercase: false, - minLowercase: 0, - number: true, - minNumber: 3, - special: false, - minSpecial: 0, - numWords: 10, - wordSeparator: "-", - capitalize: true, - includeNumber: false, - policyUpdated: true, - }); - }); - - it("sets default options when an inner service lacks a value", async () => { - const innerPassword = createPasswordGenerator(null); - const innerPassphrase = createPassphraseGenerator(null); - const navigation = createNavigationGenerator(null); - const accountService = mockAccountServiceWith(SomeUser); - const generator = new LegacyPasswordGenerationService( - accountService, - navigation, - innerPassword, - innerPassphrase, - null, - ); - - const [result] = await generator.getOptions(); - - expect(result).toEqual({ - type: DefaultGeneratorNavigation.type, - ...DefaultPassphraseGenerationOptions, - ...DefaultPasswordGenerationOptions, - minLowercase: 1, - minUppercase: 1, - policyUpdated: true, - }); - }); - - it("combines policies from its inner services", async () => { - const innerPassword = createPasswordGenerator( - {}, - { - minLength: 20, - numberCount: 10, - specialCount: 11, - useUppercase: true, - useLowercase: false, - useNumbers: true, - useSpecial: false, - }, - ); - const innerPassphrase = createPassphraseGenerator( - {}, - { - minNumberWords: 5, - capitalize: true, - includeNumber: false, - }, - ); - const accountService = mockAccountServiceWith(SomeUser); - const navigation = createNavigationGenerator( - {}, - { - defaultType: "password", - }, - ); - const generator = new LegacyPasswordGenerationService( - accountService, - navigation, - innerPassword, - innerPassphrase, - null, - ); - - const [, policy] = await generator.getOptions(); - - expect(policy).toEqual({ - defaultType: "password", - minLength: 20, - numberCount: 10, - specialCount: 11, - useUppercase: true, - useLowercase: false, - useNumbers: true, - useSpecial: false, - minNumberWords: 5, - capitalize: true, - includeNumber: false, - }); - }); - }); - - describe("enforcePasswordGeneratorPoliciesOnOptions", () => { - it("returns its options parameter with password policy applied", async () => { - const innerPassword = createPasswordGenerator( - {}, - { - minLength: 15, - numberCount: 5, - specialCount: 5, - useUppercase: true, - useLowercase: true, - useNumbers: true, - useSpecial: true, - }, - ); - const innerPassphrase = createPassphraseGenerator(); - const accountService = mockAccountServiceWith(SomeUser); - const navigation = createNavigationGenerator(); - const options = { - type: "password" as const, - }; - const generator = new LegacyPasswordGenerationService( - accountService, - navigation, - innerPassword, - innerPassphrase, - null, - ); - - const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options); - - expect(result).toBe(options); - expect(result).toMatchObject({ - length: 15, - minLength: 15, - minLowercase: 1, - minNumber: 5, - minUppercase: 1, - minSpecial: 5, - uppercase: true, - lowercase: true, - number: true, - special: true, - }); - }); - - it("returns its options parameter with passphrase policy applied", async () => { - const innerPassword = createPasswordGenerator(); - const innerPassphrase = createPassphraseGenerator( - {}, - { - minNumberWords: 5, - capitalize: true, - includeNumber: true, - }, - ); - const accountService = mockAccountServiceWith(SomeUser); - const navigation = createNavigationGenerator(); - const options = { - type: "passphrase" as const, - }; - const generator = new LegacyPasswordGenerationService( - accountService, - navigation, - innerPassword, - innerPassphrase, - null, - ); - - const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options); - - expect(result).toBe(options); - expect(result).toMatchObject({ - numWords: 5, - capitalize: true, - includeNumber: true, - }); - }); - - it("returns the applied policy", async () => { - const innerPassword = createPasswordGenerator( - {}, - { - minLength: 20, - numberCount: 10, - specialCount: 11, - useUppercase: true, - useLowercase: false, - useNumbers: true, - useSpecial: false, - }, - ); - const innerPassphrase = createPassphraseGenerator( - {}, - { - minNumberWords: 5, - capitalize: true, - includeNumber: false, - }, - ); - const accountService = mockAccountServiceWith(SomeUser); - const navigation = createNavigationGenerator( - {}, - { - defaultType: "password", - }, - ); - const generator = new LegacyPasswordGenerationService( - accountService, - navigation, - innerPassword, - innerPassphrase, - null, - ); - - const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({}); - - expect(policy).toEqual({ - defaultType: "password", - minLength: 20, - numberCount: 10, - specialCount: 11, - useUppercase: true, - useLowercase: false, - useNumbers: true, - useSpecial: false, - minNumberWords: 5, - capitalize: true, - includeNumber: false, - }); - }); - }); - - describe("saveOptions", () => { - it("loads saved password options", async () => { - const innerPassword = createPasswordGenerator(); - const innerPassphrase = createPassphraseGenerator(); - const navigation = createNavigationGenerator(); - const accountService = mockAccountServiceWith(SomeUser); - const generator = new LegacyPasswordGenerationService( - accountService, - navigation, - innerPassword, - innerPassphrase, - null, - ); - const options = { - type: "password" as const, - length: 29, - minLength: 5, - ambiguous: false, - uppercase: true, - minUppercase: 1, - lowercase: false, - minLowercase: 0, - number: true, - minNumber: 3, - special: false, - minSpecial: 0, - }; - await generator.saveOptions(options); - - const [result] = await generator.getOptions(); - - expect(result).toMatchObject(options); - }); - - it("loads saved passphrase options", async () => { - const innerPassword = createPasswordGenerator(); - const innerPassphrase = createPassphraseGenerator(); - const navigation = createNavigationGenerator(); - const accountService = mockAccountServiceWith(SomeUser); - const generator = new LegacyPasswordGenerationService( - accountService, - navigation, - innerPassword, - innerPassphrase, - null, - ); - const options = { - type: "passphrase" as const, - numWords: 10, - wordSeparator: "-", - capitalize: true, - includeNumber: false, - }; - await generator.saveOptions(options); - - const [result] = await generator.getOptions(); - - expect(result).toMatchObject(options); - }); - - it("preserves saved navigation options", async () => { - const innerPassword = createPasswordGenerator(); - const innerPassphrase = createPassphraseGenerator(); - const navigation = createNavigationGenerator({ - type: "password", - username: "forwarded", - forwarder: "firefoxrelay", - }); - const accountService = mockAccountServiceWith(SomeUser); - const generator = new LegacyPasswordGenerationService( - accountService, - navigation, - innerPassword, - innerPassphrase, - null, - ); - const options = { - type: "passphrase" as const, - numWords: 10, - wordSeparator: "-", - capitalize: true, - includeNumber: false, - }; - - await generator.saveOptions(options); - - expect(navigation.saveOptions).toHaveBeenCalledWith(SomeUser, { - type: "passphrase", - username: "forwarded", - forwarder: "firefoxrelay", - }); - }); - }); - - describe("getHistory", () => { - it("gets the active user's history from the history service", async () => { - const history = mock(); - history.credentials$.mockReturnValue( - of([new GeneratedCredential("foo", "password", new Date(100))]), - ); - const accountService = mockAccountServiceWith(SomeUser); - const generator = new LegacyPasswordGenerationService( - accountService, - null, - null, - null, - history, - ); - - const result = await generator.getHistory(); - - expect(history.credentials$).toHaveBeenCalledWith(SomeUser); - expect(result).toEqual([new GeneratedPasswordHistory("foo", 100)]); - }); - }); - - describe("addHistory", () => { - it("adds a history item as a password credential", async () => { - const history = mock(); - const accountService = mockAccountServiceWith(SomeUser); - const generator = new LegacyPasswordGenerationService( - accountService, - null, - null, - null, - history, - ); - - await generator.addHistory("foo"); - - expect(history.track).toHaveBeenCalledWith(SomeUser, "foo", "password"); - }); - }); -}); diff --git a/libs/common/src/tools/generator/legacy-password-generation.service.ts b/libs/common/src/tools/generator/legacy-password-generation.service.ts deleted file mode 100644 index d69d4d2dc06..00000000000 --- a/libs/common/src/tools/generator/legacy-password-generation.service.ts +++ /dev/null @@ -1,426 +0,0 @@ -import { - concatMap, - zip, - map, - firstValueFrom, - combineLatest, - pairwise, - of, - concat, - Observable, - filter, - timeout, -} from "rxjs"; - -import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; -import { PasswordGeneratorPolicyOptions } from "../../admin-console/models/domain/password-generator-policy-options"; -import { AccountService } from "../../auth/abstractions/account.service"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; -import { StateProvider } from "../../platform/state"; - -import { - GeneratorHistoryService, - GeneratorService, - GeneratorNavigationService, - PolicyEvaluator, -} from "./abstractions"; -import { PasswordGenerationServiceAbstraction } from "./abstractions/password-generation.service.abstraction"; -import { DefaultGeneratorService } from "./default-generator.service"; -import { GeneratedCredential } from "./history"; -import { LocalGeneratorHistoryService } from "./history/local-generator-history.service"; -import { GeneratorNavigation } from "./navigation"; -import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service"; -import { GeneratorNavigationPolicy } from "./navigation/generator-navigation-policy"; -import { - PassphraseGenerationOptions, - PassphraseGeneratorPolicy, - PassphraseGeneratorStrategy, -} from "./passphrase"; -import { - GeneratedPasswordHistory, - PasswordGenerationOptions, - PasswordGeneratorOptions, - PasswordGeneratorPolicy, - PasswordGeneratorStrategy, -} from "./password"; -import { CryptoServiceRandomizer } from "./random"; - -type MappedOptions = { - generator: GeneratorNavigation; - password: PasswordGenerationOptions; - passphrase: PassphraseGenerationOptions; - policyUpdated: boolean; -}; - -export function legacyPasswordGenerationServiceFactory( - encryptService: EncryptService, - cryptoService: CryptoService, - policyService: PolicyService, - accountService: AccountService, - stateProvider: StateProvider, -): PasswordGenerationServiceAbstraction { - const randomizer = new CryptoServiceRandomizer(cryptoService); - - const passwords = new DefaultGeneratorService( - new PasswordGeneratorStrategy(randomizer, stateProvider), - policyService, - ); - - const passphrases = new DefaultGeneratorService( - new PassphraseGeneratorStrategy(randomizer, stateProvider), - policyService, - ); - - const navigation = new DefaultGeneratorNavigationService(stateProvider, policyService); - - const history = new LocalGeneratorHistoryService(encryptService, cryptoService, stateProvider); - - return new LegacyPasswordGenerationService( - accountService, - navigation, - passwords, - passphrases, - history, - ); -} - -/** Adapts the generator 2.0 design to 1.0 angular services. */ -export class LegacyPasswordGenerationService implements PasswordGenerationServiceAbstraction { - constructor( - private readonly accountService: AccountService, - private readonly navigation: GeneratorNavigationService, - private readonly passwords: GeneratorService< - PasswordGenerationOptions, - PasswordGeneratorPolicy - >, - private readonly passphrases: GeneratorService< - PassphraseGenerationOptions, - PassphraseGeneratorPolicy - >, - private readonly history: GeneratorHistoryService, - ) {} - - generatePassword(options: PasswordGeneratorOptions) { - if (options.type === "password") { - return this.passwords.generate(options); - } else { - return this.passphrases.generate(options); - } - } - - generatePassphrase(options: PasswordGeneratorOptions) { - return this.passphrases.generate(options); - } - - private getRawOptions$() { - // give the typechecker a nudge to avoid "implicit any" errors - type RawOptionsIntermediateType = [ - PasswordGenerationOptions, - PasswordGenerationOptions, - [PolicyEvaluator, number], - PassphraseGenerationOptions, - PassphraseGenerationOptions, - [PolicyEvaluator, number], - GeneratorNavigation, - GeneratorNavigation, - [PolicyEvaluator, number], - ]; - - function withSequenceNumber(observable$: Observable) { - return observable$.pipe(map((evaluator, i) => [evaluator, i] as const)); - } - - // initial array ensures that destructuring never fails; sequence numbers - // set to `-1` so that the first update reflects that the policy changed from - // "unknown" to "whatever was provided by the service". This needs to be called - // each time the active user changes or the `concat` will block. - function initial$() { - const initial: RawOptionsIntermediateType = [ - null, - null, - [null, -1], - null, - null, - [null, -1], - null, - null, - [null, -1], - ]; - - return of(initial); - } - - function intermediatePairsToRawOptions([previous, current]: [ - RawOptionsIntermediateType, - RawOptionsIntermediateType, - ]) { - const [, , [, passwordPrevious], , , [, passphrasePrevious], , , [, generatorPrevious]] = - previous; - const [ - passwordOptions, - passwordDefaults, - [passwordEvaluator, passwordCurrent], - passphraseOptions, - passphraseDefaults, - [passphraseEvaluator, passphraseCurrent], - generatorOptions, - generatorDefaults, - [generatorEvaluator, generatorCurrent], - ] = current; - - // when any of the sequence numbers change, the emission occurs as the result of - // a policy update - const policyEmitted = - passwordPrevious < passwordCurrent || - passphrasePrevious < passphraseCurrent || - generatorPrevious < generatorCurrent; - - const result = [ - passwordOptions, - passwordDefaults, - passwordEvaluator, - passphraseOptions, - passphraseDefaults, - passphraseEvaluator, - generatorOptions, - generatorDefaults, - generatorEvaluator, - policyEmitted, - ] as const; - - return result; - } - - // look upon my works, ye mighty, and despair! - const rawOptions$ = this.accountService.activeAccount$.pipe( - concatMap((activeUser) => - concat( - initial$(), - combineLatest([ - this.passwords.options$(activeUser.id), - this.passwords.defaults$(activeUser.id), - withSequenceNumber(this.passwords.evaluator$(activeUser.id)), - this.passphrases.options$(activeUser.id), - this.passphrases.defaults$(activeUser.id), - withSequenceNumber(this.passphrases.evaluator$(activeUser.id)), - this.navigation.options$(activeUser.id), - this.navigation.defaults$(activeUser.id), - withSequenceNumber(this.navigation.evaluator$(activeUser.id)), - ]), - ), - ), - pairwise(), - map(intermediatePairsToRawOptions), - ); - - return rawOptions$; - } - - getOptions$() { - const options$ = this.getRawOptions$().pipe( - map( - ([ - passwordOptions, - passwordDefaults, - passwordEvaluator, - passphraseOptions, - passphraseDefaults, - passphraseEvaluator, - generatorOptions, - generatorDefaults, - generatorEvaluator, - policyUpdated, - ]) => { - const passwordOptionsWithPolicy = passwordEvaluator.applyPolicy( - passwordOptions ?? passwordDefaults, - ); - const passphraseOptionsWithPolicy = passphraseEvaluator.applyPolicy( - passphraseOptions ?? passphraseDefaults, - ); - const generatorOptionsWithPolicy = generatorEvaluator.applyPolicy( - generatorOptions ?? generatorDefaults, - ); - - const options = this.toPasswordGeneratorOptions({ - password: passwordEvaluator.sanitize(passwordOptionsWithPolicy), - passphrase: passphraseEvaluator.sanitize(passphraseOptionsWithPolicy), - generator: generatorEvaluator.sanitize(generatorOptionsWithPolicy), - policyUpdated, - }); - - const policy = Object.assign( - new PasswordGeneratorPolicyOptions(), - passwordEvaluator.policy, - passphraseEvaluator.policy, - generatorEvaluator.policy, - ); - - return [options, policy] as [PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]; - }, - ), - ); - - return options$; - } - - async getOptions() { - return await firstValueFrom(this.getOptions$()); - } - - async enforcePasswordGeneratorPoliciesOnOptions(options: PasswordGeneratorOptions) { - const options$ = this.accountService.activeAccount$.pipe( - concatMap((activeUser) => - zip( - this.passwords.evaluator$(activeUser.id), - this.passphrases.evaluator$(activeUser.id), - this.navigation.evaluator$(activeUser.id), - ), - ), - map(([passwordEvaluator, passphraseEvaluator, navigationEvaluator]) => { - const policy = Object.assign( - new PasswordGeneratorPolicyOptions(), - passwordEvaluator.policy, - passphraseEvaluator.policy, - navigationEvaluator.policy, - ); - - const navigationApplied = navigationEvaluator.applyPolicy(options); - const navigationSanitized = { - ...options, - ...navigationEvaluator.sanitize(navigationApplied), - }; - if (options.type === "password") { - const applied = passwordEvaluator.applyPolicy(navigationSanitized); - const sanitized = passwordEvaluator.sanitize(applied); - return [sanitized, policy]; - } else { - const applied = passphraseEvaluator.applyPolicy(navigationSanitized); - const sanitized = passphraseEvaluator.sanitize(applied); - return [sanitized, policy]; - } - }), - ); - - const [sanitized, policy] = await firstValueFrom(options$); - return [ - // callers assume this function updates the options parameter - Object.assign(options, sanitized), - policy, - ] as [PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]; - } - - async saveOptions(options: PasswordGeneratorOptions) { - const stored = this.toStoredOptions(options); - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - - // generator settings needs to preserve whether password or passphrase is selected, - // so `navigationOptions` is mutated. - const navigationOptions$ = zip( - this.navigation.options$(activeAccount.id), - this.navigation.defaults$(activeAccount.id), - ).pipe(map(([options, defaults]) => options ?? defaults)); - let navigationOptions = await firstValueFrom(navigationOptions$); - navigationOptions = Object.assign(navigationOptions, stored.generator); - await this.navigation.saveOptions(activeAccount.id, navigationOptions); - - // overwrite all other settings with latest values - await this.passwords.saveOptions(activeAccount.id, stored.password); - await this.passphrases.saveOptions(activeAccount.id, stored.passphrase); - } - - private toStoredOptions(options: PasswordGeneratorOptions): MappedOptions { - return { - generator: { - type: options.type, - }, - password: { - length: options.length, - minLength: options.minLength, - ambiguous: options.ambiguous, - uppercase: options.uppercase, - minUppercase: options.minUppercase, - lowercase: options.lowercase, - minLowercase: options.minLowercase, - number: options.number, - minNumber: options.minNumber, - special: options.special, - minSpecial: options.minSpecial, - }, - passphrase: { - numWords: options.numWords, - wordSeparator: options.wordSeparator, - capitalize: options.capitalize, - includeNumber: options.includeNumber, - }, - policyUpdated: false, - }; - } - - private toPasswordGeneratorOptions(options: MappedOptions): PasswordGeneratorOptions { - return { - type: options.generator.type, - length: options.password.length, - minLength: options.password.minLength, - ambiguous: options.password.ambiguous, - uppercase: options.password.uppercase, - minUppercase: options.password.minUppercase, - lowercase: options.password.lowercase, - minLowercase: options.password.minLowercase, - number: options.password.number, - minNumber: options.password.minNumber, - special: options.password.special, - minSpecial: options.password.minSpecial, - numWords: options.passphrase.numWords, - wordSeparator: options.passphrase.wordSeparator, - capitalize: options.passphrase.capitalize, - includeNumber: options.passphrase.includeNumber, - policyUpdated: options.policyUpdated, - }; - } - - getHistory() { - const history = this.accountService.activeAccount$.pipe( - concatMap((account) => this.history.credentials$(account.id)), - timeout({ - // timeout after 1 second - each: 1000, - with() { - return []; - }, - }), - map((history) => history.map(toGeneratedPasswordHistory)), - ); - - return firstValueFrom(history); - } - - async addHistory(password: string) { - const account = await firstValueFrom(this.accountService.activeAccount$); - if (account?.id) { - // legacy service doesn't distinguish credential types - await this.history.track(account.id, password, "password"); - } - } - - clear() { - const history$ = this.accountService.activeAccount$.pipe( - filter((account) => !!account?.id), - concatMap((account) => this.history.clear(account.id)), - timeout({ - // timeout after 1 second - each: 1000, - with() { - return []; - }, - }), - map((history) => history.map(toGeneratedPasswordHistory)), - ); - - return firstValueFrom(history$); - } -} - -function toGeneratedPasswordHistory(value: GeneratedCredential) { - return new GeneratedPasswordHistory(value.credential, value.generationDate.valueOf()); -} diff --git a/libs/common/src/tools/generator/legacy-username-generation.service.spec.ts b/libs/common/src/tools/generator/legacy-username-generation.service.spec.ts deleted file mode 100644 index 8b5c8b81e5a..00000000000 --- a/libs/common/src/tools/generator/legacy-username-generation.service.spec.ts +++ /dev/null @@ -1,748 +0,0 @@ -import { mock } from "jest-mock-extended"; -import { of } from "rxjs"; - -import { mockAccountServiceWith } from "../../../spec"; -import { UserId } from "../../types/guid"; - -import { GeneratorNavigationService, GeneratorService } from "./abstractions"; -import { DefaultPolicyEvaluator } from "./default-policy-evaluator"; -import { LegacyUsernameGenerationService } from "./legacy-username-generation.service"; -import { DefaultGeneratorNavigation, GeneratorNavigation } from "./navigation/generator-navigation"; -import { GeneratorNavigationEvaluator } from "./navigation/generator-navigation-evaluator"; -import { GeneratorNavigationPolicy } from "./navigation/generator-navigation-policy"; -import { NoPolicy } from "./no-policy"; -import { UsernameGeneratorOptions } from "./username"; -import { - CatchallGenerationOptions, - DefaultCatchallOptions, -} from "./username/catchall-generator-options"; -import { - DefaultEffUsernameOptions, - EffUsernameGenerationOptions, -} from "./username/eff-username-generator-options"; -import { DefaultAddyIoOptions } from "./username/forwarders/addy-io"; -import { DefaultDuckDuckGoOptions } from "./username/forwarders/duck-duck-go"; -import { DefaultFastmailOptions } from "./username/forwarders/fastmail"; -import { DefaultFirefoxRelayOptions } from "./username/forwarders/firefox-relay"; -import { DefaultForwardEmailOptions } from "./username/forwarders/forward-email"; -import { DefaultSimpleLoginOptions } from "./username/forwarders/simple-login"; -import { Forwarders } from "./username/options/constants"; -import { - ApiOptions, - EmailDomainOptions, - EmailPrefixOptions, - SelfHostedApiOptions, -} from "./username/options/forwarder-options"; -import { - DefaultSubaddressOptions, - SubaddressGenerationOptions, -} from "./username/subaddress-generator-options"; - -const SomeUser = "userId" as UserId; - -function createGenerator(options: Options, defaults: Options) { - let savedOptions = options; - const generator = mock>({ - evaluator$(id: UserId) { - const evaluator = new DefaultPolicyEvaluator(); - return of(evaluator); - }, - options$(id: UserId) { - return of(savedOptions); - }, - defaults$(id: UserId) { - return of(defaults); - }, - saveOptions: jest.fn((userId, options) => { - savedOptions = options; - return Promise.resolve(); - }), - }); - - return generator; -} - -function createNavigationGenerator( - options: GeneratorNavigation = {}, - policy: GeneratorNavigationPolicy = {}, -) { - let savedOptions = options; - const generator = mock({ - evaluator$(id: UserId) { - const evaluator = new GeneratorNavigationEvaluator(policy); - return of(evaluator); - }, - options$(id: UserId) { - return of(savedOptions); - }, - defaults$(id: UserId) { - return of(DefaultGeneratorNavigation); - }, - saveOptions: jest.fn((userId, options) => { - savedOptions = options; - return Promise.resolve(); - }), - }); - - return generator; -} - -describe("LegacyUsernameGenerationService", () => { - // NOTE: in all tests, `null` constructor arguments are not used by the test. - // They're set to `null` to avoid setting up unnecessary mocks. - describe("generateUserName", () => { - it("should generate a catchall username", async () => { - const options = { type: "catchall" } as UsernameGeneratorOptions; - const catchall = createGenerator(null, null); - catchall.generate.mockResolvedValue("catchall@example.com"); - const generator = new LegacyUsernameGenerationService( - null, - null, - catchall, - null, - null, - null, - null, - null, - null, - null, - null, - ); - - const result = await generator.generateUsername(options); - - expect(catchall.generate).toHaveBeenCalledWith(options); - expect(result).toBe("catchall@example.com"); - }); - - it("should generate an EFF word username", async () => { - const options = { type: "word" } as UsernameGeneratorOptions; - const effWord = createGenerator(null, null); - effWord.generate.mockResolvedValue("eff word"); - const generator = new LegacyUsernameGenerationService( - null, - null, - null, - effWord, - null, - null, - null, - null, - null, - null, - null, - ); - - const result = await generator.generateUsername(options); - - expect(effWord.generate).toHaveBeenCalledWith(options); - expect(result).toBe("eff word"); - }); - - it("should generate a subaddress username", async () => { - const options = { type: "subaddress" } as UsernameGeneratorOptions; - const subaddress = createGenerator(null, null); - subaddress.generate.mockResolvedValue("subaddress@example.com"); - const generator = new LegacyUsernameGenerationService( - null, - null, - null, - null, - subaddress, - null, - null, - null, - null, - null, - null, - ); - - const result = await generator.generateUsername(options); - - expect(subaddress.generate).toHaveBeenCalledWith(options); - expect(result).toBe("subaddress@example.com"); - }); - - it("should generate a forwarder username", async () => { - // set up an arbitrary forwarder for the username test; all forwarders tested in their own tests - const options = { - type: "forwarded", - forwardedService: Forwarders.AddyIo.id, - } as UsernameGeneratorOptions; - const addyIo = createGenerator(null, null); - addyIo.generate.mockResolvedValue("addyio@example.com"); - const generator = new LegacyUsernameGenerationService( - null, - null, - null, - null, - null, - addyIo, - null, - null, - null, - null, - null, - ); - - const result = await generator.generateUsername(options); - - expect(addyIo.generate).toHaveBeenCalledWith({}); - expect(result).toBe("addyio@example.com"); - }); - }); - - describe("generateCatchall", () => { - it("should generate a catchall username", async () => { - const options = { type: "catchall" } as UsernameGeneratorOptions; - const catchall = createGenerator(null, null); - catchall.generate.mockResolvedValue("catchall@example.com"); - const generator = new LegacyUsernameGenerationService( - null, - null, - catchall, - null, - null, - null, - null, - null, - null, - null, - null, - ); - - const result = await generator.generateCatchall(options); - - expect(catchall.generate).toHaveBeenCalledWith(options); - expect(result).toBe("catchall@example.com"); - }); - }); - - describe("generateSubaddress", () => { - it("should generate a subaddress username", async () => { - const options = { type: "subaddress" } as UsernameGeneratorOptions; - const subaddress = createGenerator(null, null); - subaddress.generate.mockResolvedValue("subaddress@example.com"); - const generator = new LegacyUsernameGenerationService( - null, - null, - null, - null, - subaddress, - null, - null, - null, - null, - null, - null, - ); - - const result = await generator.generateSubaddress(options); - - expect(subaddress.generate).toHaveBeenCalledWith(options); - expect(result).toBe("subaddress@example.com"); - }); - }); - - describe("generateForwarded", () => { - it("should generate a AddyIo username", async () => { - const options = { - forwardedService: Forwarders.AddyIo.id, - forwardedAnonAddyApiToken: "token", - forwardedAnonAddyBaseUrl: "https://example.com", - forwardedAnonAddyDomain: "example.com", - website: "example.com", - } as UsernameGeneratorOptions; - const addyIo = createGenerator(null, null); - addyIo.generate.mockResolvedValue("addyio@example.com"); - const generator = new LegacyUsernameGenerationService( - null, - null, - null, - null, - null, - addyIo, - null, - null, - null, - null, - null, - ); - - const result = await generator.generateForwarded(options); - - expect(addyIo.generate).toHaveBeenCalledWith({ - token: "token", - baseUrl: "https://example.com", - domain: "example.com", - website: "example.com", - }); - expect(result).toBe("addyio@example.com"); - }); - - it("should generate a DuckDuckGo username", async () => { - const options = { - forwardedService: Forwarders.DuckDuckGo.id, - forwardedDuckDuckGoToken: "token", - website: "example.com", - } as UsernameGeneratorOptions; - const duckDuckGo = createGenerator(null, null); - duckDuckGo.generate.mockResolvedValue("ddg@example.com"); - const generator = new LegacyUsernameGenerationService( - null, - null, - null, - null, - null, - null, - duckDuckGo, - null, - null, - null, - null, - ); - - const result = await generator.generateForwarded(options); - - expect(duckDuckGo.generate).toHaveBeenCalledWith({ - token: "token", - website: "example.com", - }); - expect(result).toBe("ddg@example.com"); - }); - - it("should generate a Fastmail username", async () => { - const options = { - forwardedService: Forwarders.Fastmail.id, - forwardedFastmailApiToken: "token", - website: "example.com", - } as UsernameGeneratorOptions; - const fastmail = createGenerator(null, null); - fastmail.generate.mockResolvedValue("fastmail@example.com"); - const generator = new LegacyUsernameGenerationService( - null, - null, - null, - null, - null, - null, - null, - fastmail, - null, - null, - null, - ); - - const result = await generator.generateForwarded(options); - - expect(fastmail.generate).toHaveBeenCalledWith({ - token: "token", - website: "example.com", - }); - expect(result).toBe("fastmail@example.com"); - }); - - it("should generate a FirefoxRelay username", async () => { - const options = { - forwardedService: Forwarders.FirefoxRelay.id, - forwardedFirefoxApiToken: "token", - website: "example.com", - } as UsernameGeneratorOptions; - const firefoxRelay = createGenerator(null, null); - firefoxRelay.generate.mockResolvedValue("firefoxrelay@example.com"); - const generator = new LegacyUsernameGenerationService( - null, - null, - null, - null, - null, - null, - null, - null, - firefoxRelay, - null, - null, - ); - - const result = await generator.generateForwarded(options); - - expect(firefoxRelay.generate).toHaveBeenCalledWith({ - token: "token", - website: "example.com", - }); - expect(result).toBe("firefoxrelay@example.com"); - }); - - it("should generate a ForwardEmail username", async () => { - const options = { - forwardedService: Forwarders.ForwardEmail.id, - forwardedForwardEmailApiToken: "token", - forwardedForwardEmailDomain: "example.com", - website: "example.com", - } as UsernameGeneratorOptions; - const forwardEmail = createGenerator(null, null); - forwardEmail.generate.mockResolvedValue("forwardemail@example.com"); - const generator = new LegacyUsernameGenerationService( - null, - null, - null, - null, - null, - null, - null, - null, - null, - forwardEmail, - null, - ); - - const result = await generator.generateForwarded(options); - - expect(forwardEmail.generate).toHaveBeenCalledWith({ - token: "token", - domain: "example.com", - website: "example.com", - }); - expect(result).toBe("forwardemail@example.com"); - }); - - it("should generate a SimpleLogin username", async () => { - const options = { - forwardedService: Forwarders.SimpleLogin.id, - forwardedSimpleLoginApiKey: "token", - forwardedSimpleLoginBaseUrl: "https://example.com", - website: "example.com", - } as UsernameGeneratorOptions; - const simpleLogin = createGenerator(null, null); - simpleLogin.generate.mockResolvedValue("simplelogin@example.com"); - const generator = new LegacyUsernameGenerationService( - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - simpleLogin, - ); - - const result = await generator.generateForwarded(options); - - expect(simpleLogin.generate).toHaveBeenCalledWith({ - token: "token", - baseUrl: "https://example.com", - website: "example.com", - }); - expect(result).toBe("simplelogin@example.com"); - }); - }); - - describe("getOptions", () => { - it("combines options from its inner generators", async () => { - const account = mockAccountServiceWith(SomeUser); - - const navigation = createNavigationGenerator({ - type: "username", - username: "catchall", - forwarder: Forwarders.AddyIo.id, - }); - - const catchall = createGenerator( - { - catchallDomain: "example.com", - catchallType: "random", - website: null, - }, - null, - ); - - const effUsername = createGenerator( - { - wordCapitalize: true, - wordIncludeNumber: false, - website: null, - }, - null, - ); - - const subaddress = createGenerator( - { - subaddressType: "random", - subaddressEmail: "foo@example.com", - website: null, - }, - null, - ); - - const addyIo = createGenerator( - { - token: "addyIoToken", - domain: "addyio.example.com", - baseUrl: "https://addyio.api.example.com", - website: null, - }, - null, - ); - - const duckDuckGo = createGenerator( - { - token: "ddgToken", - website: null, - }, - null, - ); - - const fastmail = createGenerator( - { - token: "fastmailToken", - domain: "fastmail.example.com", - prefix: "foo", - website: null, - }, - null, - ); - - const firefoxRelay = createGenerator( - { - token: "firefoxToken", - website: null, - }, - null, - ); - - const forwardEmail = createGenerator( - { - token: "forwardEmailToken", - domain: "example.com", - website: null, - }, - null, - ); - - const simpleLogin = createGenerator( - { - token: "simpleLoginToken", - baseUrl: "https://simplelogin.api.example.com", - website: null, - }, - null, - ); - - const generator = new LegacyUsernameGenerationService( - account, - navigation, - catchall, - effUsername, - subaddress, - addyIo, - duckDuckGo, - fastmail, - firefoxRelay, - forwardEmail, - simpleLogin, - ); - - const result = await generator.getOptions(); - - expect(result).toEqual({ - type: "catchall", - wordCapitalize: true, - wordIncludeNumber: false, - subaddressType: "random", - subaddressEmail: "foo@example.com", - catchallType: "random", - catchallDomain: "example.com", - forwardedService: Forwarders.AddyIo.id, - forwardedAnonAddyApiToken: "addyIoToken", - forwardedAnonAddyDomain: "addyio.example.com", - forwardedAnonAddyBaseUrl: "https://addyio.api.example.com", - forwardedDuckDuckGoToken: "ddgToken", - forwardedFirefoxApiToken: "firefoxToken", - forwardedFastmailApiToken: "fastmailToken", - forwardedForwardEmailApiToken: "forwardEmailToken", - forwardedForwardEmailDomain: "example.com", - forwardedSimpleLoginApiKey: "simpleLoginToken", - forwardedSimpleLoginBaseUrl: "https://simplelogin.api.example.com", - }); - }); - - it("sets default options when an inner service lacks a value", async () => { - const account = mockAccountServiceWith(SomeUser); - const navigation = createNavigationGenerator(null); - const catchall = createGenerator(null, DefaultCatchallOptions); - const effUsername = createGenerator( - null, - DefaultEffUsernameOptions, - ); - const subaddress = createGenerator( - null, - DefaultSubaddressOptions, - ); - const addyIo = createGenerator( - null, - DefaultAddyIoOptions, - ); - const duckDuckGo = createGenerator(null, DefaultDuckDuckGoOptions); - const fastmail = createGenerator( - null, - DefaultFastmailOptions, - ); - const firefoxRelay = createGenerator(null, DefaultFirefoxRelayOptions); - const forwardEmail = createGenerator( - null, - DefaultForwardEmailOptions, - ); - const simpleLogin = createGenerator(null, DefaultSimpleLoginOptions); - - const generator = new LegacyUsernameGenerationService( - account, - navigation, - catchall, - effUsername, - subaddress, - addyIo, - duckDuckGo, - fastmail, - firefoxRelay, - forwardEmail, - simpleLogin, - ); - - const result = await generator.getOptions(); - - expect(result).toEqual({ - type: DefaultGeneratorNavigation.username, - catchallType: DefaultCatchallOptions.catchallType, - catchallDomain: DefaultCatchallOptions.catchallDomain, - wordCapitalize: DefaultEffUsernameOptions.wordCapitalize, - wordIncludeNumber: DefaultEffUsernameOptions.wordIncludeNumber, - subaddressType: DefaultSubaddressOptions.subaddressType, - subaddressEmail: DefaultSubaddressOptions.subaddressEmail, - forwardedService: DefaultGeneratorNavigation.forwarder, - forwardedAnonAddyApiToken: DefaultAddyIoOptions.token, - forwardedAnonAddyDomain: DefaultAddyIoOptions.domain, - forwardedAnonAddyBaseUrl: DefaultAddyIoOptions.baseUrl, - forwardedDuckDuckGoToken: DefaultDuckDuckGoOptions.token, - forwardedFastmailApiToken: DefaultFastmailOptions.token, - forwardedFirefoxApiToken: DefaultFirefoxRelayOptions.token, - forwardedForwardEmailApiToken: DefaultForwardEmailOptions.token, - forwardedForwardEmailDomain: DefaultForwardEmailOptions.domain, - forwardedSimpleLoginApiKey: DefaultSimpleLoginOptions.token, - forwardedSimpleLoginBaseUrl: DefaultSimpleLoginOptions.baseUrl, - }); - }); - }); - - describe("saveOptions", () => { - it("saves option sets to its inner generators", async () => { - const account = mockAccountServiceWith(SomeUser); - const navigation = createNavigationGenerator({ type: "password" }); - const catchall = createGenerator(null, null); - const effUsername = createGenerator(null, null); - const subaddress = createGenerator(null, null); - const addyIo = createGenerator(null, null); - const duckDuckGo = createGenerator(null, null); - const fastmail = createGenerator(null, null); - const firefoxRelay = createGenerator(null, null); - const forwardEmail = createGenerator(null, null); - const simpleLogin = createGenerator(null, null); - - const generator = new LegacyUsernameGenerationService( - account, - navigation, - catchall, - effUsername, - subaddress, - addyIo, - duckDuckGo, - fastmail, - firefoxRelay, - forwardEmail, - simpleLogin, - ); - - await generator.saveOptions({ - type: "catchall", - wordCapitalize: true, - wordIncludeNumber: false, - subaddressType: "random", - subaddressEmail: "foo@example.com", - catchallType: "random", - catchallDomain: "example.com", - forwardedService: Forwarders.AddyIo.id, - forwardedAnonAddyApiToken: "addyIoToken", - forwardedAnonAddyDomain: "addyio.example.com", - forwardedAnonAddyBaseUrl: "https://addyio.api.example.com", - forwardedDuckDuckGoToken: "ddgToken", - forwardedFirefoxApiToken: "firefoxToken", - forwardedFastmailApiToken: "fastmailToken", - forwardedForwardEmailApiToken: "forwardEmailToken", - forwardedForwardEmailDomain: "example.com", - forwardedSimpleLoginApiKey: "simpleLoginToken", - forwardedSimpleLoginBaseUrl: "https://simplelogin.api.example.com", - website: null, - }); - - expect(navigation.saveOptions).toHaveBeenCalledWith(SomeUser, { - type: "password", - username: "catchall", - forwarder: Forwarders.AddyIo.id, - }); - - expect(catchall.saveOptions).toHaveBeenCalledWith(SomeUser, { - catchallDomain: "example.com", - catchallType: "random", - website: null, - }); - - expect(effUsername.saveOptions).toHaveBeenCalledWith(SomeUser, { - wordCapitalize: true, - wordIncludeNumber: false, - website: null, - }); - - expect(subaddress.saveOptions).toHaveBeenCalledWith(SomeUser, { - subaddressType: "random", - subaddressEmail: "foo@example.com", - website: null, - }); - - expect(addyIo.saveOptions).toHaveBeenCalledWith(SomeUser, { - token: "addyIoToken", - domain: "addyio.example.com", - baseUrl: "https://addyio.api.example.com", - website: null, - }); - - expect(duckDuckGo.saveOptions).toHaveBeenCalledWith(SomeUser, { - token: "ddgToken", - website: null, - }); - - expect(fastmail.saveOptions).toHaveBeenCalledWith(SomeUser, { - token: "fastmailToken", - website: null, - }); - - expect(firefoxRelay.saveOptions).toHaveBeenCalledWith(SomeUser, { - token: "firefoxToken", - website: null, - }); - - expect(forwardEmail.saveOptions).toHaveBeenCalledWith(SomeUser, { - token: "forwardEmailToken", - domain: "example.com", - website: null, - }); - - expect(simpleLogin.saveOptions).toHaveBeenCalledWith(SomeUser, { - token: "simpleLoginToken", - baseUrl: "https://simplelogin.api.example.com", - website: null, - }); - }); - }); -}); diff --git a/libs/common/src/tools/generator/legacy-username-generation.service.ts b/libs/common/src/tools/generator/legacy-username-generation.service.ts deleted file mode 100644 index aaa6bc2c806..00000000000 --- a/libs/common/src/tools/generator/legacy-username-generation.service.ts +++ /dev/null @@ -1,390 +0,0 @@ -import { zip, firstValueFrom, map, concatMap, combineLatest } from "rxjs"; - -import { ApiService } from "../../abstractions/api.service"; -import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "../../auth/abstractions/account.service"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../platform/abstractions/encrypt.service"; -import { I18nService } from "../../platform/abstractions/i18n.service"; -import { StateProvider } from "../../platform/state"; - -import { GeneratorService, GeneratorNavigationService } from "./abstractions"; -import { UsernameGenerationServiceAbstraction } from "./abstractions/username-generation.service.abstraction"; -import { DefaultGeneratorService } from "./default-generator.service"; -import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service"; -import { GeneratorNavigation } from "./navigation/generator-navigation"; -import { NoPolicy } from "./no-policy"; -import { CryptoServiceRandomizer } from "./random"; -import { - CatchallGeneratorStrategy, - SubaddressGeneratorStrategy, - EffUsernameGeneratorStrategy, -} from "./username"; -import { CatchallGenerationOptions } from "./username/catchall-generator-options"; -import { EffUsernameGenerationOptions } from "./username/eff-username-generator-options"; -import { AddyIoForwarder } from "./username/forwarders/addy-io"; -import { DuckDuckGoForwarder } from "./username/forwarders/duck-duck-go"; -import { FastmailForwarder } from "./username/forwarders/fastmail"; -import { FirefoxRelayForwarder } from "./username/forwarders/firefox-relay"; -import { ForwardEmailForwarder } from "./username/forwarders/forward-email"; -import { SimpleLoginForwarder } from "./username/forwarders/simple-login"; -import { Forwarders } from "./username/options/constants"; -import { - ApiOptions, - EmailDomainOptions, - EmailPrefixOptions, - RequestOptions, - SelfHostedApiOptions, -} from "./username/options/forwarder-options"; -import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; -import { UsernameGeneratorOptions } from "./username/username-generation-options"; - -type MappedOptions = { - generator: GeneratorNavigation; - algorithms: { - catchall: CatchallGenerationOptions; - effUsername: EffUsernameGenerationOptions; - subaddress: SubaddressGenerationOptions; - }; - forwarders: { - addyIo: SelfHostedApiOptions & EmailDomainOptions & RequestOptions; - duckDuckGo: ApiOptions & RequestOptions; - fastmail: ApiOptions & EmailPrefixOptions & RequestOptions; - firefoxRelay: ApiOptions & RequestOptions; - forwardEmail: ApiOptions & EmailDomainOptions & RequestOptions; - simpleLogin: SelfHostedApiOptions & RequestOptions; - }; -}; - -export function legacyUsernameGenerationServiceFactory( - apiService: ApiService, - i18nService: I18nService, - cryptoService: CryptoService, - encryptService: EncryptService, - policyService: PolicyService, - accountService: AccountService, - stateProvider: StateProvider, -): UsernameGenerationServiceAbstraction { - const randomizer = new CryptoServiceRandomizer(cryptoService); - - const effUsername = new DefaultGeneratorService( - new EffUsernameGeneratorStrategy(randomizer, stateProvider), - policyService, - ); - - const subaddress = new DefaultGeneratorService( - new SubaddressGeneratorStrategy(randomizer, stateProvider), - policyService, - ); - - const catchall = new DefaultGeneratorService( - new CatchallGeneratorStrategy(randomizer, stateProvider), - policyService, - ); - - const addyIo = new DefaultGeneratorService( - new AddyIoForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider), - policyService, - ); - - const duckDuckGo = new DefaultGeneratorService( - new DuckDuckGoForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider), - policyService, - ); - - const fastmail = new DefaultGeneratorService( - new FastmailForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider), - policyService, - ); - - const firefoxRelay = new DefaultGeneratorService( - new FirefoxRelayForwarder( - apiService, - i18nService, - encryptService, - cryptoService, - stateProvider, - ), - policyService, - ); - - const forwardEmail = new DefaultGeneratorService( - new ForwardEmailForwarder( - apiService, - i18nService, - encryptService, - cryptoService, - stateProvider, - ), - policyService, - ); - - const simpleLogin = new DefaultGeneratorService( - new SimpleLoginForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider), - policyService, - ); - - const navigation = new DefaultGeneratorNavigationService(stateProvider, policyService); - - return new LegacyUsernameGenerationService( - accountService, - navigation, - catchall, - effUsername, - subaddress, - addyIo, - duckDuckGo, - fastmail, - firefoxRelay, - forwardEmail, - simpleLogin, - ); -} - -/** Adapts the generator 2.0 design to 1.0 angular services. */ -export class LegacyUsernameGenerationService implements UsernameGenerationServiceAbstraction { - constructor( - private readonly accountService: AccountService, - private readonly navigation: GeneratorNavigationService, - private readonly catchall: GeneratorService, - private readonly effUsername: GeneratorService, - private readonly subaddress: GeneratorService, - private readonly addyIo: GeneratorService, - private readonly duckDuckGo: GeneratorService, - private readonly fastmail: GeneratorService, - private readonly firefoxRelay: GeneratorService, - private readonly forwardEmail: GeneratorService, - private readonly simpleLogin: GeneratorService, - ) {} - - generateUsername(options: UsernameGeneratorOptions) { - if (options.type === "catchall") { - return this.generateCatchall(options); - } else if (options.type === "subaddress") { - return this.generateSubaddress(options); - } else if (options.type === "forwarded") { - return this.generateForwarded(options); - } else { - return this.generateWord(options); - } - } - - generateWord(options: UsernameGeneratorOptions) { - return this.effUsername.generate(options); - } - - generateSubaddress(options: UsernameGeneratorOptions) { - return this.subaddress.generate(options); - } - - generateCatchall(options: UsernameGeneratorOptions) { - return this.catchall.generate(options); - } - - generateForwarded(options: UsernameGeneratorOptions) { - if (!options.forwardedService) { - return null; - } - - const stored = this.toStoredOptions(options); - switch (options.forwardedService) { - case Forwarders.AddyIo.id: - return this.addyIo.generate(stored.forwarders.addyIo); - case Forwarders.DuckDuckGo.id: - return this.duckDuckGo.generate(stored.forwarders.duckDuckGo); - case Forwarders.Fastmail.id: - return this.fastmail.generate(stored.forwarders.fastmail); - case Forwarders.FirefoxRelay.id: - return this.firefoxRelay.generate(stored.forwarders.firefoxRelay); - case Forwarders.ForwardEmail.id: - return this.forwardEmail.generate(stored.forwarders.forwardEmail); - case Forwarders.SimpleLogin.id: - return this.simpleLogin.generate(stored.forwarders.simpleLogin); - } - } - - getOptions$() { - // look upon my works, ye mighty, and despair! - const options$ = this.accountService.activeAccount$.pipe( - concatMap((account) => - combineLatest([ - this.navigation.options$(account.id), - this.navigation.defaults$(account.id), - this.catchall.options$(account.id), - this.catchall.defaults$(account.id), - this.effUsername.options$(account.id), - this.effUsername.defaults$(account.id), - this.subaddress.options$(account.id), - this.subaddress.defaults$(account.id), - this.addyIo.options$(account.id), - this.addyIo.defaults$(account.id), - this.duckDuckGo.options$(account.id), - this.duckDuckGo.defaults$(account.id), - this.fastmail.options$(account.id), - this.fastmail.defaults$(account.id), - this.firefoxRelay.options$(account.id), - this.firefoxRelay.defaults$(account.id), - this.forwardEmail.options$(account.id), - this.forwardEmail.defaults$(account.id), - this.simpleLogin.options$(account.id), - this.simpleLogin.defaults$(account.id), - ]), - ), - map( - ([ - generatorOptions, - generatorDefaults, - catchallOptions, - catchallDefaults, - effUsernameOptions, - effUsernameDefaults, - subaddressOptions, - subaddressDefaults, - addyIoOptions, - addyIoDefaults, - duckDuckGoOptions, - duckDuckGoDefaults, - fastmailOptions, - fastmailDefaults, - firefoxRelayOptions, - firefoxRelayDefaults, - forwardEmailOptions, - forwardEmailDefaults, - simpleLoginOptions, - simpleLoginDefaults, - ]) => - this.toUsernameOptions({ - generator: generatorOptions ?? generatorDefaults, - algorithms: { - catchall: catchallOptions ?? catchallDefaults, - effUsername: effUsernameOptions ?? effUsernameDefaults, - subaddress: subaddressOptions ?? subaddressDefaults, - }, - forwarders: { - addyIo: addyIoOptions ?? addyIoDefaults, - duckDuckGo: duckDuckGoOptions ?? duckDuckGoDefaults, - fastmail: fastmailOptions ?? fastmailDefaults, - firefoxRelay: firefoxRelayOptions ?? firefoxRelayDefaults, - forwardEmail: forwardEmailOptions ?? forwardEmailDefaults, - simpleLogin: simpleLoginOptions ?? simpleLoginDefaults, - }, - }), - ), - ); - - return options$; - } - - getOptions() { - return firstValueFrom(this.getOptions$()); - } - - async saveOptions(options: UsernameGeneratorOptions) { - const stored = this.toStoredOptions(options); - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - - // generator settings needs to preserve whether password or passphrase is selected, - // so `navigationOptions` is mutated. - const navigationOptions$ = zip( - this.navigation.options$(activeAccount.id), - this.navigation.defaults$(activeAccount.id), - ).pipe(map(([options, defaults]) => options ?? defaults)); - let navigationOptions = await firstValueFrom(navigationOptions$); - navigationOptions = Object.assign(navigationOptions, stored.generator); - await this.navigation.saveOptions(activeAccount.id, navigationOptions); - - // overwrite all other settings with latest values - await Promise.all([ - this.catchall.saveOptions(activeAccount.id, stored.algorithms.catchall), - this.effUsername.saveOptions(activeAccount.id, stored.algorithms.effUsername), - this.subaddress.saveOptions(activeAccount.id, stored.algorithms.subaddress), - this.addyIo.saveOptions(activeAccount.id, stored.forwarders.addyIo), - this.duckDuckGo.saveOptions(activeAccount.id, stored.forwarders.duckDuckGo), - this.fastmail.saveOptions(activeAccount.id, stored.forwarders.fastmail), - this.firefoxRelay.saveOptions(activeAccount.id, stored.forwarders.firefoxRelay), - this.forwardEmail.saveOptions(activeAccount.id, stored.forwarders.forwardEmail), - this.simpleLogin.saveOptions(activeAccount.id, stored.forwarders.simpleLogin), - ]); - } - - private toStoredOptions(options: UsernameGeneratorOptions) { - const forwarders = { - addyIo: { - baseUrl: options.forwardedAnonAddyBaseUrl, - token: options.forwardedAnonAddyApiToken, - domain: options.forwardedAnonAddyDomain, - website: options.website, - }, - duckDuckGo: { - token: options.forwardedDuckDuckGoToken, - website: options.website, - }, - fastmail: { - token: options.forwardedFastmailApiToken, - website: options.website, - }, - firefoxRelay: { - token: options.forwardedFirefoxApiToken, - website: options.website, - }, - forwardEmail: { - token: options.forwardedForwardEmailApiToken, - domain: options.forwardedForwardEmailDomain, - website: options.website, - }, - simpleLogin: { - token: options.forwardedSimpleLoginApiKey, - baseUrl: options.forwardedSimpleLoginBaseUrl, - website: options.website, - }, - }; - - const generator = { - username: options.type, - forwarder: options.forwardedService, - }; - - const algorithms = { - effUsername: { - wordCapitalize: options.wordCapitalize, - wordIncludeNumber: options.wordIncludeNumber, - website: options.website, - }, - subaddress: { - subaddressType: options.subaddressType, - subaddressEmail: options.subaddressEmail, - website: options.website, - }, - catchall: { - catchallType: options.catchallType, - catchallDomain: options.catchallDomain, - website: options.website, - }, - }; - - return { generator, algorithms, forwarders } as MappedOptions; - } - - private toUsernameOptions(options: MappedOptions) { - return { - type: options.generator.username, - wordCapitalize: options.algorithms.effUsername.wordCapitalize, - wordIncludeNumber: options.algorithms.effUsername.wordIncludeNumber, - subaddressType: options.algorithms.subaddress.subaddressType, - subaddressEmail: options.algorithms.subaddress.subaddressEmail, - catchallType: options.algorithms.catchall.catchallType, - catchallDomain: options.algorithms.catchall.catchallDomain, - forwardedService: options.generator.forwarder, - forwardedAnonAddyApiToken: options.forwarders.addyIo.token, - forwardedAnonAddyDomain: options.forwarders.addyIo.domain, - forwardedAnonAddyBaseUrl: options.forwarders.addyIo.baseUrl, - forwardedDuckDuckGoToken: options.forwarders.duckDuckGo.token, - forwardedFirefoxApiToken: options.forwarders.firefoxRelay.token, - forwardedFastmailApiToken: options.forwarders.fastmail.token, - forwardedForwardEmailApiToken: options.forwarders.forwardEmail.token, - forwardedForwardEmailDomain: options.forwarders.forwardEmail.domain, - forwardedSimpleLoginApiKey: options.forwarders.simpleLogin.token, - forwardedSimpleLoginBaseUrl: options.forwarders.simpleLogin.baseUrl, - } as UsernameGeneratorOptions; - } -} diff --git a/libs/common/src/tools/generator/navigation/default-generator-nativation.service.spec.ts b/libs/common/src/tools/generator/navigation/default-generator-nativation.service.spec.ts deleted file mode 100644 index 6853542bb7a..00000000000 --- a/libs/common/src/tools/generator/navigation/default-generator-nativation.service.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * include structuredClone in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ -import { mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; - -import { FakeStateProvider, mockAccountServiceWith } 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 { UserId } from "../../../types/guid"; -import { GENERATOR_SETTINGS } from "../key-definitions"; - -import { - GeneratorNavigationEvaluator, - DefaultGeneratorNavigationService, - DefaultGeneratorNavigation, -} from "./"; - -const SomeUser = "some user" as UserId; - -describe("DefaultGeneratorNavigationService", () => { - describe("options$", () => { - it("emits options", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const settings = { type: "password" as const }; - await stateProvider.setUserState(GENERATOR_SETTINGS, settings, SomeUser); - const navigation = new DefaultGeneratorNavigationService(stateProvider, null); - - const result = await firstValueFrom(navigation.options$(SomeUser)); - - expect(result).toEqual(settings); - }); - }); - - describe("defaults$", () => { - it("emits default options", async () => { - const navigation = new DefaultGeneratorNavigationService(null, null); - - const result = await firstValueFrom(navigation.defaults$(SomeUser)); - - expect(result).toEqual(DefaultGeneratorNavigation); - }); - }); - - describe("evaluator$", () => { - it("emits a GeneratorNavigationEvaluator", async () => { - const policyService = mock({ - getAll$() { - return of([]); - }, - }); - const navigation = new DefaultGeneratorNavigationService(null, policyService); - - const result = await firstValueFrom(navigation.evaluator$(SomeUser)); - - expect(result).toBeInstanceOf(GeneratorNavigationEvaluator); - }); - }); - - describe("enforcePolicy", () => { - it("applies policy", async () => { - const policyService = mock({ - getAll$(_type: PolicyType, _user: UserId) { - return of([ - new Policy({ - id: "" as any, - organizationId: "" as any, - enabled: true, - type: PolicyType.PasswordGenerator, - data: { defaultType: "password" }, - }), - ]); - }, - }); - const navigation = new DefaultGeneratorNavigationService(null, policyService); - const options = {}; - - const result = await navigation.enforcePolicy(SomeUser, options); - - expect(result).toMatchObject({ type: "password" }); - }); - }); - - describe("saveOptions", () => { - it("updates options$", async () => { - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - const navigation = new DefaultGeneratorNavigationService(stateProvider, null); - const settings = { type: "password" as const }; - - await navigation.saveOptions(SomeUser, settings); - const result = await firstValueFrom(navigation.options$(SomeUser)); - - expect(result).toEqual(settings); - }); - }); -}); diff --git a/libs/common/src/tools/generator/navigation/default-generator-navigation.service.ts b/libs/common/src/tools/generator/navigation/default-generator-navigation.service.ts deleted file mode 100644 index a24f8012711..00000000000 --- a/libs/common/src/tools/generator/navigation/default-generator-navigation.service.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { BehaviorSubject, Observable, firstValueFrom, map } from "rxjs"; - -import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "../../../admin-console/enums"; -import { StateProvider } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; -import { distinctIfShallowMatch, reduceCollection } from "../../rx"; -import { GeneratorNavigationService } from "../abstractions/generator-navigation.service.abstraction"; -import { GENERATOR_SETTINGS } from "../key-definitions"; - -import { DefaultGeneratorNavigation, GeneratorNavigation } from "./generator-navigation"; -import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator"; -import { DisabledGeneratorNavigationPolicy, preferPassword } from "./generator-navigation-policy"; - -export class DefaultGeneratorNavigationService implements GeneratorNavigationService { - /** instantiates the password generator strategy. - * @param stateProvider provides durable state - * @param policy provides the policy to enforce - */ - constructor( - private readonly stateProvider: StateProvider, - private readonly policy: PolicyService, - ) {} - - /** 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$(userId: UserId): Observable { - return this.stateProvider.getUserState$(GENERATOR_SETTINGS, userId); - } - - /** Gets the default options. */ - defaults$(userId: UserId): Observable { - return new BehaviorSubject({ ...DefaultGeneratorNavigation }); - } - - /** An observable monitoring the options used to enforce policy. - * The observable updates when the policy changes. - * @param userId: Identifies the user making the request - */ - evaluator$(userId: UserId) { - const evaluator$ = this.policy.getAll$(PolicyType.PasswordGenerator, userId).pipe( - reduceCollection(preferPassword, DisabledGeneratorNavigationPolicy), - distinctIfShallowMatch(), - map((policy) => new GeneratorNavigationEvaluator(policy)), - ); - - return evaluator$; - } - - /** 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 - */ - async enforcePolicy(userId: UserId, options: GeneratorNavigation) { - const evaluator = await firstValueFrom(this.evaluator$(userId)); - const applied = evaluator.applyPolicy(options); - const sanitized = evaluator.sanitize(applied); - return sanitized; - } - - /** Saves the navigation 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 - */ - async saveOptions(userId: UserId, options: GeneratorNavigation): Promise { - await this.stateProvider.setUserState(GENERATOR_SETTINGS, options, userId); - } -} diff --git a/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.spec.ts b/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.spec.ts deleted file mode 100644 index 58560fb5a04..00000000000 --- a/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { DefaultGeneratorNavigation } from "./generator-navigation"; -import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator"; - -describe("GeneratorNavigationEvaluator", () => { - describe("policyInEffect", () => { - it.each([["passphrase"], ["password"]] as const)( - "returns true if the policy has a defaultType (= %p)", - (defaultType) => { - const evaluator = new GeneratorNavigationEvaluator({ defaultType }); - - expect(evaluator.policyInEffect).toEqual(true); - }, - ); - - it.each([[undefined], [null], ["" as any]])( - "returns false if the policy has a falsy defaultType (= %p)", - (defaultType) => { - const evaluator = new GeneratorNavigationEvaluator({ defaultType }); - - expect(evaluator.policyInEffect).toEqual(false); - }, - ); - }); - - describe("applyPolicy", () => { - it("returns the input options", () => { - const evaluator = new GeneratorNavigationEvaluator(null); - const options = { type: "password" as const }; - - const result = evaluator.applyPolicy(options); - - expect(result).toEqual(options); - }); - }); - - describe("sanitize", () => { - it.each([["passphrase"], ["password"]] as const)( - "defaults options to the policy's default type (= %p) when a policy is in effect", - (defaultType) => { - const evaluator = new GeneratorNavigationEvaluator({ defaultType }); - - const result = evaluator.sanitize({}); - - expect(result).toEqual({ type: defaultType }); - }, - ); - - it("defaults options to the default generator navigation type when a policy is not in effect", () => { - const evaluator = new GeneratorNavigationEvaluator(null); - - const result = evaluator.sanitize({}); - - expect(result.type).toEqual(DefaultGeneratorNavigation.type); - }); - - it("retains the options type when it is set", () => { - const evaluator = new GeneratorNavigationEvaluator({ defaultType: "passphrase" }); - - const result = evaluator.sanitize({ type: "password" }); - - expect(result).toEqual({ type: "password" }); - }); - }); -}); diff --git a/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.ts b/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.ts deleted file mode 100644 index e580f130b53..00000000000 --- a/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { PolicyEvaluator } from "../abstractions"; - -import { DefaultGeneratorNavigation, GeneratorNavigation } from "./generator-navigation"; -import { GeneratorNavigationPolicy } from "./generator-navigation-policy"; - -/** Enforces policy for generator navigation options. - */ -export class GeneratorNavigationEvaluator - implements PolicyEvaluator -{ - /** Instantiates the evaluator. - * @param policy The policy applied by the evaluator. When this conflicts with - * the defaults, the policy takes precedence. - */ - constructor(readonly policy: GeneratorNavigationPolicy) {} - - /** {@link PolicyEvaluator.policyInEffect} */ - get policyInEffect(): boolean { - return this.policy?.defaultType ? true : false; - } - - /** Apply policy to the input options. - * @param options The options to build from. These options are not altered. - * @returns A new password generation request with policy applied. - */ - applyPolicy(options: GeneratorNavigation): GeneratorNavigation { - return options; - } - - /** Ensures internal options consistency. - * @param options The options to cascade. These options are not altered. - * @returns A passphrase generation request with cascade applied. - */ - sanitize(options: GeneratorNavigation): GeneratorNavigation { - const defaultType = this.policyInEffect - ? this.policy.defaultType - : DefaultGeneratorNavigation.type; - return { - ...options, - type: options.type ?? defaultType, - }; - } -} diff --git a/libs/common/src/tools/generator/navigation/generator-navigation-policy.spec.ts b/libs/common/src/tools/generator/navigation/generator-navigation-policy.spec.ts deleted file mode 100644 index ed8fe731a75..00000000000 --- a/libs/common/src/tools/generator/navigation/generator-navigation-policy.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { PolicyType } from "../../../admin-console/enums"; -// FIXME: use index.ts imports once policy abstractions and models -// implement ADR-0002 -import { Policy } from "../../../admin-console/models/domain/policy"; -import { PolicyId } from "../../../types/guid"; - -import { DisabledGeneratorNavigationPolicy, preferPassword } from "./generator-navigation-policy"; - -function createPolicy( - data: any, - type: PolicyType = PolicyType.PasswordGenerator, - enabled: boolean = true, -) { - return new Policy({ - id: "id" as PolicyId, - organizationId: "organizationId", - data, - enabled, - type, - }); -} - -describe("leastPrivilege", () => { - it("should return the accumulator when the policy type does not apply", () => { - const policy = createPolicy({}, PolicyType.RequireSso); - - const result = preferPassword(DisabledGeneratorNavigationPolicy, policy); - - expect(result).toEqual(DisabledGeneratorNavigationPolicy); - }); - - it("should return the accumulator when the policy is not enabled", () => { - const policy = createPolicy({}, PolicyType.PasswordGenerator, false); - - const result = preferPassword(DisabledGeneratorNavigationPolicy, policy); - - expect(result).toEqual(DisabledGeneratorNavigationPolicy); - }); - - it("should take the %p from the policy", () => { - const policy = createPolicy({ defaultType: "passphrase" }); - - const result = preferPassword({ ...DisabledGeneratorNavigationPolicy }, policy); - - expect(result).toEqual({ defaultType: "passphrase" }); - }); - - it("should override passphrase with password", () => { - const policy = createPolicy({ defaultType: "password" }); - - const result = preferPassword({ defaultType: "passphrase" }, policy); - - expect(result).toEqual({ defaultType: "password" }); - }); - - it("should not override password", () => { - const policy = createPolicy({ defaultType: "passphrase" }); - - const result = preferPassword({ defaultType: "password" }, policy); - - expect(result).toEqual({ defaultType: "password" }); - }); -}); diff --git a/libs/common/src/tools/generator/navigation/generator-navigation-policy.ts b/libs/common/src/tools/generator/navigation/generator-navigation-policy.ts deleted file mode 100644 index 25c2a73337e..00000000000 --- a/libs/common/src/tools/generator/navigation/generator-navigation-policy.ts +++ /dev/null @@ -1,39 +0,0 @@ -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 { GeneratorType } from "../generator-type"; - -/** Policy settings affecting password generator navigation */ -export type GeneratorNavigationPolicy = { - /** The type of generator that should be shown by default when opening - * the password generator. - */ - defaultType?: GeneratorType; -}; - -/** Reduces a policy into an accumulator by preferring the password generator - * type to other generator types. - * @param acc the accumulator - * @param policy the policy to reduce - * @returns the resulting `GeneratorNavigationPolicy` - */ -export function preferPassword( - acc: GeneratorNavigationPolicy, - policy: Policy, -): GeneratorNavigationPolicy { - const isEnabled = policy.type === PolicyType.PasswordGenerator && policy.enabled; - if (!isEnabled) { - return acc; - } - - const isOverridable = acc.defaultType !== "password" && policy.data.defaultType; - const result = isOverridable ? { ...acc, defaultType: policy.data.defaultType } : acc; - - return result; -} - -/** The default options for password generation policy. */ -export const DisabledGeneratorNavigationPolicy: GeneratorNavigationPolicy = Object.freeze({ - defaultType: undefined, -}); diff --git a/libs/common/src/tools/generator/navigation/generator-navigation.ts b/libs/common/src/tools/generator/navigation/generator-navigation.ts deleted file mode 100644 index 6a07385286d..00000000000 --- a/libs/common/src/tools/generator/navigation/generator-navigation.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { GeneratorType } from "../generator-type"; -import { ForwarderId } from "../username/options"; -import { UsernameGeneratorType } from "../username/options/generator-options"; - -/** Stores credential generator UI state. */ - -export type GeneratorNavigation = { - /** The kind of credential being generated. - * @remarks The legacy generator only supports "password" and "passphrase". - * The componentized generator supports all values. - */ - type?: GeneratorType; - - /** When `type === "username"`, this stores the username algorithm. */ - username?: UsernameGeneratorType; - - /** When `username === "forwarded"`, this stores the forwarder implementation. */ - forwarder?: ForwarderId | ""; -}; -/** The default options for password generation. */ - -export const DefaultGeneratorNavigation: Partial = Object.freeze({ - type: "password", - username: "word", - forwarder: "", -}); diff --git a/libs/common/src/tools/generator/navigation/index.ts b/libs/common/src/tools/generator/navigation/index.ts deleted file mode 100644 index 86194f471af..00000000000 --- a/libs/common/src/tools/generator/navigation/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator"; -export { DefaultGeneratorNavigationService } from "./default-generator-navigation.service"; -export { GeneratorNavigation, DefaultGeneratorNavigation } from "./generator-navigation"; diff --git a/libs/common/src/tools/generator/no-policy.ts b/libs/common/src/tools/generator/no-policy.ts deleted file mode 100644 index 00ffc6098c2..00000000000 --- a/libs/common/src/tools/generator/no-policy.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** Type representing an absence of policy. */ -export type NoPolicy = Record; diff --git a/libs/common/src/tools/generator/passphrase/index.ts b/libs/common/src/tools/generator/passphrase/index.ts deleted file mode 100644 index 3bbe9253017..00000000000 --- a/libs/common/src/tools/generator/passphrase/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// password generator "v2" interfaces -export { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator"; -export { PassphraseGeneratorPolicy } from "./passphrase-generator-policy"; -export { PassphraseGeneratorStrategy } from "./passphrase-generator-strategy"; -export { - DefaultPassphraseGenerationOptions, - PassphraseGenerationOptions, -} from "./passphrase-generation-options"; diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generation-options.ts b/libs/common/src/tools/generator/passphrase/passphrase-generation-options.ts deleted file mode 100644 index 8d6e8eedabd..00000000000 --- a/libs/common/src/tools/generator/passphrase/passphrase-generation-options.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** Request format for passphrase credential generation. - * The members of this type may be `undefined` when the user is - * generating a password. - */ -export type PassphraseGenerationOptions = { - /** The number of words to include in the passphrase. - * This value defaults to 3. - */ - numWords?: number; - - /** The ASCII separator character to use between words in the passphrase. - * This value defaults to a dash. - * If multiple characters appear in the string, only the first character is used. - */ - wordSeparator?: string; - - /** `true` when the first character of every word should be capitalized. - * This value defaults to `false`. - */ - capitalize?: boolean; - - /** `true` when a number should be included in the passphrase. - * This value defaults to `false`. - */ - includeNumber?: boolean; -}; - -/** The default options for passphrase generation. */ -export const DefaultPassphraseGenerationOptions: Partial = - Object.freeze({ - numWords: 3, - wordSeparator: "-", - capitalize: false, - includeNumber: false, - }); diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-options-evaluator.spec.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-options-evaluator.spec.ts deleted file mode 100644 index b587afbd6e4..00000000000 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-options-evaluator.spec.ts +++ /dev/null @@ -1,266 +0,0 @@ -/** - * include structuredClone in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ -import { PassphraseGenerationOptions } from "./passphrase-generation-options"; -import { - DefaultBoundaries, - PassphraseGeneratorOptionsEvaluator, -} from "./passphrase-generator-options-evaluator"; -import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy"; - -describe("Password generator options builder", () => { - describe("constructor()", () => { - it("should set the policy object to a copy of the input policy", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - policy.minNumberWords = 10; // arbitrary change for deep equality check - - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - - expect(builder.policy).toEqual(policy); - expect(builder.policy).not.toBe(policy); - }); - - it("should set default boundaries when a default policy is used", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - - expect(builder.numWords).toEqual(DefaultBoundaries.numWords); - }); - - it.each([1, 2])( - "should use the default word boundaries when they are greater than `policy.minNumberWords` (= %i)", - (minNumberWords) => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - policy.minNumberWords = minNumberWords; - - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - - expect(builder.numWords).toEqual(DefaultBoundaries.numWords); - }, - ); - - it.each([8, 12, 18])( - "should use `policy.minNumberWords` (= %i) when it is greater than the default minimum words", - (minNumberWords) => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - policy.minNumberWords = minNumberWords; - - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - - expect(builder.numWords.min).toEqual(minNumberWords); - expect(builder.numWords.max).toEqual(DefaultBoundaries.numWords.max); - }, - ); - - it.each([150, 300, 9000])( - "should use `policy.minNumberWords` (= %i) when it is greater than the default boundaries", - (minNumberWords) => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - policy.minNumberWords = minNumberWords; - - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - - expect(builder.numWords.min).toEqual(minNumberWords); - expect(builder.numWords.max).toEqual(minNumberWords); - }, - ); - }); - - describe("policyInEffect", () => { - it("should return false when the policy has no effect", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - - expect(builder.policyInEffect).toEqual(false); - }); - - it("should return true when the policy has a numWords greater than the default boundary", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - policy.minNumberWords = DefaultBoundaries.numWords.min + 1; - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - - expect(builder.policyInEffect).toEqual(true); - }); - - it("should return true when the policy has capitalize enabled", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - policy.capitalize = true; - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - - expect(builder.policyInEffect).toEqual(true); - }); - - it("should return true when the policy has includeNumber enabled", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - policy.includeNumber = true; - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - - expect(builder.policyInEffect).toEqual(true); - }); - }); - - describe("applyPolicy(options)", () => { - // All tests should freeze the options to ensure they are not modified - - it("should set `capitalize` to `false` when the policy does not override it", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({}); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.capitalize).toBe(false); - }); - - it("should set `capitalize` to `true` when the policy overrides it", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - policy.capitalize = true; - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ capitalize: false }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.capitalize).toBe(true); - }); - - it("should set `includeNumber` to false when the policy does not override it", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({}); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.includeNumber).toBe(false); - }); - - it("should set `includeNumber` to true when the policy overrides it", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - policy.includeNumber = true; - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ includeNumber: false }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.includeNumber).toBe(true); - }); - - it("should set `numWords` to the minimum value when it isn't supplied", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({}); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.numWords).toBe(builder.numWords.min); - }); - - it.each([1, 2])( - "should set `numWords` (= %i) to the minimum value when it is less than the minimum", - (numWords) => { - expect(numWords).toBeLessThan(DefaultBoundaries.numWords.min); - - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ numWords }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.numWords).toBe(builder.numWords.min); - }, - ); - - it.each([3, 8, 18, 20])( - "should set `numWords` (= %i) to the input value when it is within the boundaries", - (numWords) => { - expect(numWords).toBeGreaterThanOrEqual(DefaultBoundaries.numWords.min); - expect(numWords).toBeLessThanOrEqual(DefaultBoundaries.numWords.max); - - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ numWords }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.numWords).toBe(numWords); - }, - ); - - it.each([21, 30, 50, 100])( - "should set `numWords` (= %i) to the maximum value when it is greater than the maximum", - (numWords) => { - expect(numWords).toBeGreaterThan(DefaultBoundaries.numWords.max); - - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ numWords }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.numWords).toBe(builder.numWords.max); - }, - ); - - it("should preserve unknown properties", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ - unknown: "property", - another: "unknown property", - }) as PassphraseGenerationOptions; - - const sanitizedOptions: any = builder.applyPolicy(options); - - expect(sanitizedOptions.unknown).toEqual("property"); - expect(sanitizedOptions.another).toEqual("unknown property"); - }); - }); - - describe("sanitize(options)", () => { - // All tests should freeze the options to ensure they are not modified - - it("should return the input options without altering them", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ wordSeparator: "%" }); - - const sanitizedOptions = builder.sanitize(options); - - expect(sanitizedOptions).toEqual(options); - }); - - it("should set `wordSeparator` to '-' when it isn't supplied and there is no policy override", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({}); - - const sanitizedOptions = builder.sanitize(options); - - expect(sanitizedOptions.wordSeparator).toEqual("-"); - }); - - it("should leave `wordSeparator` as the empty string '' when it is the empty string", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ wordSeparator: "" }); - - const sanitizedOptions = builder.sanitize(options); - - expect(sanitizedOptions.wordSeparator).toEqual(""); - }); - - it("should preserve unknown properties", () => { - const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy); - const builder = new PassphraseGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ - unknown: "property", - another: "unknown property", - }) as PassphraseGenerationOptions; - - const sanitizedOptions: any = builder.sanitize(options); - - expect(sanitizedOptions.unknown).toEqual("property"); - expect(sanitizedOptions.another).toEqual("unknown property"); - }); - }); -}); diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-options-evaluator.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-options-evaluator.ts deleted file mode 100644 index cdde114914d..00000000000 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-options-evaluator.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { PolicyEvaluator } from "../abstractions/policy-evaluator.abstraction"; - -import { - DefaultPassphraseGenerationOptions, - PassphraseGenerationOptions, -} from "./passphrase-generation-options"; -import { PassphraseGeneratorPolicy } from "./passphrase-generator-policy"; - -type Boundary = { - readonly min: number; - readonly max: number; -}; - -function initializeBoundaries() { - const numWords = Object.freeze({ - min: 3, - max: 20, - }); - - return Object.freeze({ - numWords, - }); -} - -/** Immutable default boundaries for passphrase generation. - * These are used when the policy does not override a value. - */ -export const DefaultBoundaries = initializeBoundaries(); - -/** Enforces policy for passphrase generation options. - */ -export class PassphraseGeneratorOptionsEvaluator - implements PolicyEvaluator -{ - // This design is not ideal, but it is a step towards a more robust passphrase - // generator. Ideally, `sanitize` would be implemented on an options class, - // and `applyPolicy` would be implemented on a policy class, "mise en place". - // - // The current design of the passphrase generator, unfortunately, would require - // a substantial rewrite to make this feasible. Hopefully this change can be - // applied when the passphrase generator is ported to rust. - - /** Policy applied by the evaluator. - */ - readonly policy: PassphraseGeneratorPolicy; - - /** Boundaries for the number of words allowed in the password. - */ - readonly numWords: Boundary; - - /** Instantiates the evaluator. - * @param policy The policy applied by the evaluator. When this conflicts with - * the defaults, the policy takes precedence. - */ - constructor(policy: PassphraseGeneratorPolicy) { - function createBoundary(value: number, defaultBoundary: Boundary): Boundary { - const boundary = { - min: Math.max(defaultBoundary.min, value), - max: Math.max(defaultBoundary.max, value), - }; - - return boundary; - } - - this.policy = structuredClone(policy); - this.numWords = createBoundary(policy.minNumberWords, DefaultBoundaries.numWords); - } - - /** {@link PolicyEvaluator.policyInEffect} */ - get policyInEffect(): boolean { - const policies = [ - this.policy.capitalize, - this.policy.includeNumber, - this.policy.minNumberWords > DefaultBoundaries.numWords.min, - ]; - - return policies.includes(true); - } - - /** Apply policy to the input options. - * @param options The options to build from. These options are not altered. - * @returns A new password generation request with policy applied. - */ - applyPolicy(options: PassphraseGenerationOptions): PassphraseGenerationOptions { - function fitToBounds(value: number, boundaries: Boundary) { - const { min, max } = boundaries; - - const withUpperBound = Math.min(value ?? boundaries.min, max); - const withLowerBound = Math.max(withUpperBound, min); - - return withLowerBound; - } - - // apply policy overrides - const capitalize = this.policy.capitalize || options.capitalize || false; - const includeNumber = this.policy.includeNumber || options.includeNumber || false; - - // apply boundaries - const numWords = fitToBounds(options.numWords, this.numWords); - - return { - ...options, - numWords, - capitalize, - includeNumber, - }; - } - - /** Ensures internal options consistency. - * @param options The options to cascade. These options are not altered. - * @returns A passphrase generation request with cascade applied. - */ - sanitize(options: PassphraseGenerationOptions): PassphraseGenerationOptions { - // ensure words are separated by a single character or the empty string - const wordSeparator = - options.wordSeparator === "" - ? "" - : (options.wordSeparator?.[0] ?? DefaultPassphraseGenerationOptions.wordSeparator); - - return { - ...options, - wordSeparator, - }; - } -} diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.spec.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.spec.ts deleted file mode 100644 index 991b2ae3024..00000000000 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { PolicyType } from "../../../admin-console/enums"; -// FIXME: use index.ts imports once policy abstractions and models -// implement ADR-0002 -import { Policy } from "../../../admin-console/models/domain/policy"; -import { PolicyId } from "../../../types/guid"; - -import { DisabledPassphraseGeneratorPolicy, leastPrivilege } from "./passphrase-generator-policy"; - -function createPolicy( - data: any, - type: PolicyType = PolicyType.PasswordGenerator, - enabled: boolean = true, -) { - return new Policy({ - id: "id" as PolicyId, - organizationId: "organizationId", - data, - enabled, - type, - }); -} - -describe("leastPrivilege", () => { - it("should return the accumulator when the policy type does not apply", () => { - const policy = createPolicy({}, PolicyType.RequireSso); - - const result = leastPrivilege(DisabledPassphraseGeneratorPolicy, policy); - - expect(result).toEqual(DisabledPassphraseGeneratorPolicy); - }); - - it("should return the accumulator when the policy is not enabled", () => { - const policy = createPolicy({}, PolicyType.PasswordGenerator, false); - - const result = leastPrivilege(DisabledPassphraseGeneratorPolicy, policy); - - expect(result).toEqual(DisabledPassphraseGeneratorPolicy); - }); - - it.each([ - ["minNumberWords", 10], - ["capitalize", true], - ["includeNumber", true], - ])("should take the %p from the policy", (input, value) => { - const policy = createPolicy({ [input]: value }); - - const result = leastPrivilege(DisabledPassphraseGeneratorPolicy, policy); - - expect(result).toEqual({ ...DisabledPassphraseGeneratorPolicy, [input]: value }); - }); -}); diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.ts deleted file mode 100644 index db616f16c05..00000000000 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { PolicyType } from "../../../admin-console/enums"; -// FIXME: use index.ts imports once policy abstractions and models -// implement ADR-0002 -import { Policy } from "../../../admin-console/models/domain/policy"; - -/** Policy options enforced during passphrase generation. */ -export type PassphraseGeneratorPolicy = { - minNumberWords: number; - capitalize: boolean; - includeNumber: boolean; -}; - -/** The default options for password generation policy. */ -export const DisabledPassphraseGeneratorPolicy: PassphraseGeneratorPolicy = Object.freeze({ - minNumberWords: 0, - capitalize: false, - includeNumber: false, -}); - -/** Reduces a policy into an accumulator by accepting the most restrictive - * values from each policy. - * @param acc the accumulator - * @param policy the policy to reduce - * @returns the most restrictive values between the policy and accumulator. - */ -export function leastPrivilege( - acc: PassphraseGeneratorPolicy, - policy: Policy, -): PassphraseGeneratorPolicy { - if (policy.type !== PolicyType.PasswordGenerator) { - return acc; - } - - return { - minNumberWords: Math.max(acc.minNumberWords, policy.data.minNumberWords ?? acc.minNumberWords), - capitalize: policy.data.capitalize || acc.capitalize, - includeNumber: policy.data.includeNumber || acc.includeNumber, - }; -} 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 deleted file mode 100644 index 429f81175a8..00000000000 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * include structuredClone in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ -import { mock } from "jest-mock-extended"; -import { of, firstValueFrom } from "rxjs"; - -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 { Randomizer } from "../abstractions/randomizer"; -import { PASSPHRASE_SETTINGS } from "../key-definitions"; - -import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy"; - -import { - DefaultPassphraseGenerationOptions, - PassphraseGeneratorOptionsEvaluator, - PassphraseGeneratorStrategy, -} from "."; - -const SomeUser = "some user" as UserId; - -describe("Password generation strategy", () => { - describe("toEvaluator()", () => { - it("should map to the policy evaluator", async () => { - const strategy = new PassphraseGeneratorStrategy(null, null); - const policy = mock({ - type: PolicyType.PasswordGenerator, - data: { - minNumberWords: 10, - capitalize: true, - includeNumber: true, - }, - }); - - const evaluator$ = of([policy]).pipe(strategy.toEvaluator()); - const evaluator = await firstValueFrom(evaluator$); - - expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator); - expect(evaluator.policy).toMatchObject({ - minNumberWords: 10, - capitalize: true, - includeNumber: true, - }); - }); - - it.each([[[]], [null], [undefined]])( - "should map `%p` to a disabled password policy evaluator", - async (policies) => { - const strategy = new PassphraseGeneratorStrategy(null, null); - - const evaluator$ = of(policies).pipe(strategy.toEvaluator()); - const evaluator = await firstValueFrom(evaluator$); - - expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator); - expect(evaluator.policy).toMatchObject(DisabledPassphraseGeneratorPolicy); - }, - ); - }); - - describe("durableState", () => { - it("should use password settings key", () => { - const provider = mock(); - const randomizer = mock(); - const strategy = new PassphraseGeneratorStrategy(randomizer, provider); - - strategy.durableState(SomeUser); - - expect(provider.getUser).toHaveBeenCalledWith(SomeUser, PASSPHRASE_SETTINGS); - }); - }); - - describe("defaults$", () => { - it("should return the default subaddress options", async () => { - const strategy = new PassphraseGeneratorStrategy(null, null); - - const result = await firstValueFrom(strategy.defaults$(SomeUser)); - - expect(result).toEqual(DefaultPassphraseGenerationOptions); - }); - }); - - describe("policy", () => { - it("should use password generator policy", () => { - const randomizer = mock(); - const strategy = new PassphraseGeneratorStrategy(randomizer, null); - - expect(strategy.policy).toBe(PolicyType.PasswordGenerator); - }); - }); - - describe("generate()", () => { - it.todo("should generate a password using the given options"); - }); -}); diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts deleted file mode 100644 index bf381845704..00000000000 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { GeneratorStrategy } from ".."; -import { PolicyType } from "../../../admin-console/enums"; -import { EFFLongWordList } from "../../../platform/misc/wordlist"; -import { StateProvider } from "../../../platform/state"; -import { Randomizer } from "../abstractions/randomizer"; -import { PASSPHRASE_SETTINGS } from "../key-definitions"; -import { Policies } from "../policies"; -import { mapPolicyToEvaluator } from "../rx-operators"; -import { clone$PerUserId, sharedStateByUserId } from "../util"; - -import { - PassphraseGenerationOptions, - DefaultPassphraseGenerationOptions, -} from "./passphrase-generation-options"; -import { PassphraseGeneratorPolicy } from "./passphrase-generator-policy"; - -/** Generates passphrases composed of random words */ -export class PassphraseGeneratorStrategy - implements GeneratorStrategy -{ - /** instantiates the password generator strategy. - * @param legacy generates the passphrase - * @param stateProvider provides durable state - */ - constructor( - private randomizer: Randomizer, - private stateProvider: StateProvider, - ) {} - - // configuration - durableState = sharedStateByUserId(PASSPHRASE_SETTINGS, this.stateProvider); - defaults$ = clone$PerUserId(DefaultPassphraseGenerationOptions); - readonly policy = PolicyType.PasswordGenerator; - toEvaluator() { - return mapPolicyToEvaluator(Policies.Passphrase); - } - - // algorithm - async generate(options: PassphraseGenerationOptions): Promise { - const o = { ...DefaultPassphraseGenerationOptions, ...options }; - if (o.numWords == null || o.numWords <= 2) { - o.numWords = DefaultPassphraseGenerationOptions.numWords; - } - if (o.capitalize == null) { - o.capitalize = false; - } - if (o.includeNumber == null) { - o.includeNumber = false; - } - - // select which word gets the number, if any - let luckyNumber = -1; - if (o.includeNumber) { - luckyNumber = await this.randomizer.uniform(0, o.numWords - 1); - } - - // generate the passphrase - const wordList = new Array(o.numWords); - for (let i = 0; i < o.numWords; i++) { - const word = await this.randomizer.pickWord(EFFLongWordList, { - titleCase: o.capitalize, - number: i === luckyNumber, - }); - - wordList[i] = word; - } - - return wordList.join(o.wordSeparator); - } -} diff --git a/libs/common/src/tools/generator/password/generated-password-history.ts b/libs/common/src/tools/generator/password/generated-password-history.ts deleted file mode 100644 index b4cc9b22fa6..00000000000 --- a/libs/common/src/tools/generator/password/generated-password-history.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class GeneratedPasswordHistory { - password: string; - date: number; - - constructor(password: string, date: number) { - this.password = password; - this.date = date; - } -} diff --git a/libs/common/src/tools/generator/password/index.ts b/libs/common/src/tools/generator/password/index.ts deleted file mode 100644 index 7e16a2c442a..00000000000 --- a/libs/common/src/tools/generator/password/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// password generator "v2" interfaces -export * from "./password-generation-options"; -export { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator"; -export { PasswordGeneratorPolicy } from "./password-generator-policy"; -export { PasswordGeneratorStrategy } from "./password-generator-strategy"; - -// legacy interfaces -export { PasswordGeneratorOptions } from "./password-generator-options"; -export { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; -export { GeneratedPasswordHistory } from "./generated-password-history"; diff --git a/libs/common/src/tools/generator/password/password-generation-options.ts b/libs/common/src/tools/generator/password/password-generation-options.ts deleted file mode 100644 index a48eeb77c6e..00000000000 --- a/libs/common/src/tools/generator/password/password-generation-options.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { DefaultBoundaries } from "./password-generator-options-evaluator"; - -/** Request format for password credential generation. - * All members of this type may be `undefined` when the user is - * generating a passphrase. - * - * @remarks The name of this type is a bit of a misnomer. This type - * it is used with the "password generator" types. The name - * `PasswordGeneratorOptions` is already in use by legacy code. - */ -export type PasswordGenerationOptions = { - /** The length of the password selected by the user */ - length?: number; - - /** The minimum length of the password. This defaults to 5, and increases - * to ensure `minLength` is at least as large as the sum of the other minimums. - */ - minLength?: number; - - /** `true` when ambiguous characters may be included in the output. - * `false` when ambiguous characters should not be included in the output. - */ - ambiguous?: boolean; - - /** `true` when uppercase ASCII characters should be included in the output - * This value defaults to `false. - */ - uppercase?: boolean; - - /** The minimum number of uppercase characters to include in the output. - * The value is ignored when `uppercase` is `false`. - * The value defaults to 1 when `uppercase` is `true`. - */ - minUppercase?: number; - - /** `true` when lowercase ASCII characters should be included in the output. - * This value defaults to `false`. - */ - lowercase?: boolean; - - /** The minimum number of lowercase characters to include in the output. - * The value defaults to 1 when `lowercase` is `true`. - * The value defaults to 0 when `lowercase` is `false`. - */ - minLowercase?: number; - - /** Whether or not to include ASCII digits in the output - * This value defaults to `true` when `minNumber` is at least 1. - * This value defaults to `false` when `minNumber` is less than 1. - */ - number?: boolean; - - /** The minimum number of digits to include in the output. - * The value defaults to 1 when `number` is `true`. - * The value defaults to 0 when `number` is `false`. - */ - minNumber?: number; - - /** Whether or not to include special characters in the output. - * This value defaults to `true` when `minSpecial` is at least 1. - * This value defaults to `false` when `minSpecial` is less than 1. - */ - special?: boolean; - - /** The minimum number of special characters to include in the output. - * This value defaults to 1 when `special` is `true`. - * This value defaults to 0 when `special` is `false`. - */ - minSpecial?: number; -}; - -/** The default options for password generation. */ -export const DefaultPasswordGenerationOptions: Partial = Object.freeze({ - length: 14, - minLength: DefaultBoundaries.length.min, - ambiguous: true, - uppercase: true, - lowercase: true, - number: true, - minNumber: 1, - special: false, - minSpecial: 0, -}); diff --git a/libs/common/src/tools/generator/password/password-generator-options-evaluator.spec.ts b/libs/common/src/tools/generator/password/password-generator-options-evaluator.spec.ts deleted file mode 100644 index 1b3f2289204..00000000000 --- a/libs/common/src/tools/generator/password/password-generator-options-evaluator.spec.ts +++ /dev/null @@ -1,770 +0,0 @@ -/** - * include structuredClone in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ - -import { DefaultBoundaries } from "./password-generator-options-evaluator"; -import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy"; - -import { PasswordGenerationOptions, PasswordGeneratorOptionsEvaluator } from "."; - -describe("Password generator options builder", () => { - const defaultOptions = Object.freeze({ minLength: 0 }); - - describe("constructor()", () => { - it("should set the policy object to a copy of the input policy", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.minLength = 10; // arbitrary change for deep equality check - - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.policy).toEqual(policy); - expect(builder.policy).not.toBe(policy); - }); - - it("should set default boundaries when a default policy is used", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.length).toEqual(DefaultBoundaries.length); - expect(builder.minDigits).toEqual(DefaultBoundaries.minDigits); - expect(builder.minSpecialCharacters).toEqual(DefaultBoundaries.minSpecialCharacters); - }); - - it.each([1, 2, 3, 4])( - "should use the default length boundaries when they are greater than `policy.minLength` (= %i)", - (minLength) => { - expect(minLength).toBeLessThan(DefaultBoundaries.length.min); - - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.minLength = minLength; - - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.length).toEqual(DefaultBoundaries.length); - }, - ); - - it.each([8, 20, 100])( - "should use `policy.minLength` (= %i) when it is greater than the default minimum length", - (expectedLength) => { - expect(expectedLength).toBeGreaterThan(DefaultBoundaries.length.min); - expect(expectedLength).toBeLessThanOrEqual(DefaultBoundaries.length.max); - - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.minLength = expectedLength; - - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.length.min).toEqual(expectedLength); - expect(builder.length.max).toEqual(DefaultBoundaries.length.max); - }, - ); - - it.each([150, 300, 9000])( - "should use `policy.minLength` (= %i) when it is greater than the default boundaries", - (expectedLength) => { - expect(expectedLength).toBeGreaterThan(DefaultBoundaries.length.max); - - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.minLength = expectedLength; - - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.length.min).toEqual(expectedLength); - expect(builder.length.max).toEqual(expectedLength); - }, - ); - - it.each([3, 5, 8, 9])( - "should use `policy.numberCount` (= %i) when it is greater than the default minimum digits", - (expectedMinDigits) => { - expect(expectedMinDigits).toBeGreaterThan(DefaultBoundaries.minDigits.min); - expect(expectedMinDigits).toBeLessThanOrEqual(DefaultBoundaries.minDigits.max); - - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.numberCount = expectedMinDigits; - - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.minDigits.min).toEqual(expectedMinDigits); - expect(builder.minDigits.max).toEqual(DefaultBoundaries.minDigits.max); - }, - ); - - it.each([10, 20, 400])( - "should use `policy.numberCount` (= %i) when it is greater than the default digit boundaries", - (expectedMinDigits) => { - expect(expectedMinDigits).toBeGreaterThan(DefaultBoundaries.minDigits.max); - - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.numberCount = expectedMinDigits; - - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.minDigits.min).toEqual(expectedMinDigits); - expect(builder.minDigits.max).toEqual(expectedMinDigits); - }, - ); - - it.each([2, 4, 6])( - "should use `policy.specialCount` (= %i) when it is greater than the default minimum special characters", - (expectedSpecialCharacters) => { - expect(expectedSpecialCharacters).toBeGreaterThan( - DefaultBoundaries.minSpecialCharacters.min, - ); - expect(expectedSpecialCharacters).toBeLessThanOrEqual( - DefaultBoundaries.minSpecialCharacters.max, - ); - - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.specialCount = expectedSpecialCharacters; - - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.minSpecialCharacters.min).toEqual(expectedSpecialCharacters); - expect(builder.minSpecialCharacters.max).toEqual( - DefaultBoundaries.minSpecialCharacters.max, - ); - }, - ); - - it.each([10, 20, 400])( - "should use `policy.specialCount` (= %i) when it is greater than the default special characters boundaries", - (expectedSpecialCharacters) => { - expect(expectedSpecialCharacters).toBeGreaterThan( - DefaultBoundaries.minSpecialCharacters.max, - ); - - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.specialCount = expectedSpecialCharacters; - - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.minSpecialCharacters.min).toEqual(expectedSpecialCharacters); - expect(builder.minSpecialCharacters.max).toEqual(expectedSpecialCharacters); - }, - ); - - it.each([ - [8, 6, 2], - [6, 2, 4], - [16, 8, 8], - ])( - "should ensure the minimum length (= %i) is at least the sum of minimums (= %i + %i)", - (expectedLength, numberCount, specialCount) => { - expect(expectedLength).toBeGreaterThanOrEqual(DefaultBoundaries.length.min); - - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.numberCount = numberCount; - policy.specialCount = specialCount; - - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.length.min).toBeGreaterThanOrEqual(expectedLength); - }, - ); - }); - - describe("policyInEffect", () => { - it("should return false when the policy has no effect", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.policyInEffect).toEqual(false); - }); - - it("should return true when the policy has a minlength greater than the default boundary", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.minLength = DefaultBoundaries.length.min + 1; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.policyInEffect).toEqual(true); - }); - - it("should return true when the policy has a number count greater than the default boundary", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.numberCount = DefaultBoundaries.minDigits.min + 1; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.policyInEffect).toEqual(true); - }); - - it("should return true when the policy has a special character count greater than the default boundary", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.specialCount = DefaultBoundaries.minSpecialCharacters.min + 1; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.policyInEffect).toEqual(true); - }); - - it("should return true when the policy has uppercase enabled", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.useUppercase = true; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.policyInEffect).toEqual(true); - }); - - it("should return true when the policy has lowercase enabled", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.useLowercase = true; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.policyInEffect).toEqual(true); - }); - - it("should return true when the policy has numbers enabled", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.useNumbers = true; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.policyInEffect).toEqual(true); - }); - - it("should return true when the policy has special characters enabled", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.useSpecial = true; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - - expect(builder.policyInEffect).toEqual(true); - }); - }); - - describe("applyPolicy(options)", () => { - // All tests should freeze the options to ensure they are not modified - - it.each([ - [false, false], - [true, true], - [false, undefined], - ])( - "should set `options.uppercase` to '%s' when `policy.useUppercase` is false and `options.uppercase` is '%s'", - (expectedUppercase, uppercase) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.useUppercase = false; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, uppercase }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.uppercase).toEqual(expectedUppercase); - }, - ); - - it.each([false, true, undefined])( - "should set `options.uppercase` (= %s) to true when `policy.useUppercase` is true", - (uppercase) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.useUppercase = true; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, uppercase }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.uppercase).toEqual(true); - }, - ); - - it.each([ - [false, false], - [true, true], - [false, undefined], - ])( - "should set `options.lowercase` to '%s' when `policy.useLowercase` is false and `options.lowercase` is '%s'", - (expectedLowercase, lowercase) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.useLowercase = false; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, lowercase }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.lowercase).toEqual(expectedLowercase); - }, - ); - - it.each([false, true, undefined])( - "should set `options.lowercase` (= %s) to true when `policy.useLowercase` is true", - (lowercase) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.useLowercase = true; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, lowercase }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.lowercase).toEqual(true); - }, - ); - - it.each([ - [false, false], - [true, true], - [false, undefined], - ])( - "should set `options.number` to '%s' when `policy.useNumbers` is false and `options.number` is '%s'", - (expectedNumber, number) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.useNumbers = false; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, number }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.number).toEqual(expectedNumber); - }, - ); - - it.each([false, true, undefined])( - "should set `options.number` (= %s) to true when `policy.useNumbers` is true", - (number) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.useNumbers = true; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, number }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.number).toEqual(true); - }, - ); - - it.each([ - [false, false], - [true, true], - [false, undefined], - ])( - "should set `options.special` to '%s' when `policy.useSpecial` is false and `options.special` is '%s'", - (expectedSpecial, special) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.useSpecial = false; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, special }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.special).toEqual(expectedSpecial); - }, - ); - - it.each([false, true, undefined])( - "should set `options.special` (= %s) to true when `policy.useSpecial` is true", - (special) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.useSpecial = true; - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, special }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.special).toEqual(true); - }, - ); - - it.each([1, 2, 3, 4])( - "should set `options.length` (= %i) to the minimum it is less than the minimum length", - (length) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - expect(length).toBeLessThan(builder.length.min); - - const options = Object.freeze({ ...defaultOptions, length }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.length).toEqual(builder.length.min); - }, - ); - - it.each([5, 10, 50, 100, 128])( - "should not change `options.length` (= %i) when it is within the boundaries", - (length) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - expect(length).toBeGreaterThanOrEqual(builder.length.min); - expect(length).toBeLessThanOrEqual(builder.length.max); - - const options = Object.freeze({ ...defaultOptions, length }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.length).toEqual(length); - }, - ); - - it.each([129, 500, 9000])( - "should set `options.length` (= %i) to the maximum length when it is exceeded", - (length) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - expect(length).toBeGreaterThan(builder.length.max); - - const options = Object.freeze({ ...defaultOptions, length }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.length).toEqual(builder.length.max); - }, - ); - - it.each([ - [true, 1], - [true, 3], - [true, 600], - [false, 0], - [false, -2], - [false, -600], - ])( - "should set `options.number === %s` when `options.minNumber` (= %i) is set to a value greater than 0", - (expectedNumber, minNumber) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, minNumber }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.number).toEqual(expectedNumber); - }, - ); - - it("should set `options.minNumber` to the minimum value when `options.number` is true", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, number: true }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.minNumber).toEqual(builder.minDigits.min); - }); - - it("should set `options.minNumber` to 0 when `options.number` is false", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, number: false }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.minNumber).toEqual(0); - }); - - it.each([1, 2, 3, 4])( - "should set `options.minNumber` (= %i) to the minimum it is less than the minimum number", - (minNumber) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.numberCount = 5; // arbitrary value greater than minNumber - expect(minNumber).toBeLessThan(policy.numberCount); - - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, minNumber }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.minNumber).toEqual(builder.minDigits.min); - }, - ); - - it.each([1, 3, 5, 7, 9])( - "should not change `options.minNumber` (= %i) when it is within the boundaries", - (minNumber) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - expect(minNumber).toBeGreaterThanOrEqual(builder.minDigits.min); - expect(minNumber).toBeLessThanOrEqual(builder.minDigits.max); - - const options = Object.freeze({ ...defaultOptions, minNumber }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.minNumber).toEqual(minNumber); - }, - ); - - it.each([10, 20, 400])( - "should set `options.minNumber` (= %i) to the maximum digit boundary when it is exceeded", - (minNumber) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - expect(minNumber).toBeGreaterThan(builder.minDigits.max); - - const options = Object.freeze({ ...defaultOptions, minNumber }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.minNumber).toEqual(builder.minDigits.max); - }, - ); - - it.each([ - [true, 1], - [true, 3], - [true, 600], - [false, 0], - [false, -2], - [false, -600], - ])( - "should set `options.special === %s` when `options.minSpecial` (= %i) is set to a value greater than 0", - (expectedSpecial, minSpecial) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, minSpecial }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.special).toEqual(expectedSpecial); - }, - ); - - it("should set `options.minSpecial` to the minimum value when `options.special` is true", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, special: true }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.minSpecial).toEqual(builder.minDigits.min); - }); - - it("should set `options.minSpecial` to 0 when `options.special` is false", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, special: false }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.minSpecial).toEqual(0); - }); - - it.each([1, 2, 3, 4])( - "should set `options.minSpecial` (= %i) to the minimum it is less than the minimum special characters", - (minSpecial) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - policy.specialCount = 5; // arbitrary value greater than minSpecial - expect(minSpecial).toBeLessThan(policy.specialCount); - - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ ...defaultOptions, minSpecial }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.minSpecial).toEqual(builder.minSpecialCharacters.min); - }, - ); - - it.each([1, 3, 5, 7, 9])( - "should not change `options.minSpecial` (= %i) when it is within the boundaries", - (minSpecial) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - expect(minSpecial).toBeGreaterThanOrEqual(builder.minSpecialCharacters.min); - expect(minSpecial).toBeLessThanOrEqual(builder.minSpecialCharacters.max); - - const options = Object.freeze({ ...defaultOptions, minSpecial }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.minSpecial).toEqual(minSpecial); - }, - ); - - it.each([10, 20, 400])( - "should set `options.minSpecial` (= %i) to the maximum special character boundary when it is exceeded", - (minSpecial) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - expect(minSpecial).toBeGreaterThan(builder.minSpecialCharacters.max); - - const options = Object.freeze({ ...defaultOptions, minSpecial }); - - const sanitizedOptions = builder.applyPolicy(options); - - expect(sanitizedOptions.minSpecial).toEqual(builder.minSpecialCharacters.max); - }, - ); - - it("should preserve unknown properties", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ - unknown: "property", - another: "unknown property", - }) as PasswordGenerationOptions; - - const sanitizedOptions: any = builder.applyPolicy(options); - - expect(sanitizedOptions.unknown).toEqual("property"); - expect(sanitizedOptions.another).toEqual("unknown property"); - }); - }); - - describe("sanitize(options)", () => { - // All tests should freeze the options to ensure they are not modified - - it.each([ - [1, true], - [0, false], - ])( - "should output `options.minLowercase === %i` when `options.lowercase` is %s", - (expectedMinLowercase, lowercase) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ lowercase, ...defaultOptions }); - - const actual = builder.sanitize(options); - - expect(actual.minLowercase).toEqual(expectedMinLowercase); - }, - ); - - it.each([ - [1, true], - [0, false], - ])( - "should output `options.minUppercase === %i` when `options.uppercase` is %s", - (expectedMinUppercase, uppercase) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ uppercase, ...defaultOptions }); - - const actual = builder.sanitize(options); - - expect(actual.minUppercase).toEqual(expectedMinUppercase); - }, - ); - - it.each([ - [1, true], - [0, false], - ])( - "should output `options.minNumber === %i` when `options.number` is %s and `options.minNumber` is not set", - (expectedMinNumber, number) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ number, ...defaultOptions }); - - const actual = builder.sanitize(options); - - expect(actual.minNumber).toEqual(expectedMinNumber); - }, - ); - - it.each([ - [true, 3], - [true, 2], - [true, 1], - [false, 0], - ])( - "should output `options.number === %s` when `options.minNumber` is %i and `options.number` is not set", - (expectedNumber, minNumber) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ minNumber, ...defaultOptions }); - - const actual = builder.sanitize(options); - - expect(actual.number).toEqual(expectedNumber); - }, - ); - - it.each([ - [true, 1], - [false, 0], - ])( - "should output `options.minSpecial === %i` when `options.special` is %s and `options.minSpecial` is not set", - (special, expectedMinSpecial) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ special, ...defaultOptions }); - - const actual = builder.sanitize(options); - - expect(actual.minSpecial).toEqual(expectedMinSpecial); - }, - ); - - it.each([ - [3, true], - [2, true], - [1, true], - [0, false], - ])( - "should output `options.special === %s` when `options.minSpecial` is %i and `options.special` is not set", - (minSpecial, expectedSpecial) => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ minSpecial, ...defaultOptions }); - - const actual = builder.sanitize(options); - - expect(actual.special).toEqual(expectedSpecial); - }, - ); - - it.each([ - [0, 0, 0, 0], - [1, 1, 0, 0], - [0, 0, 1, 1], - [1, 1, 1, 1], - ])( - "should set `options.minLength` to the minimum boundary when the sum of minimums (%i + %i + %i + %i) is less than the default minimum length.", - (minLowercase, minUppercase, minNumber, minSpecial) => { - const sumOfMinimums = minLowercase + minUppercase + minNumber + minSpecial; - expect(sumOfMinimums).toBeLessThan(DefaultBoundaries.length.min); - - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ - minLowercase, - minUppercase, - minNumber, - minSpecial, - ...defaultOptions, - }); - - const actual = builder.sanitize(options); - - expect(actual.minLength).toEqual(builder.length.min); - }, - ); - - it.each([ - [12, 3, 3, 3, 3], - [8, 2, 2, 2, 2], - [9, 3, 3, 3, 0], - ])( - "should set `options.minLength === %i` to the sum of minimums (%i + %i + %i + %i) when the sum is at least the default minimum length.", - (expectedMinLength, minLowercase, minUppercase, minNumber, minSpecial) => { - expect(expectedMinLength).toBeGreaterThanOrEqual(DefaultBoundaries.length.min); - - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ - minLowercase, - minUppercase, - minNumber, - minSpecial, - ...defaultOptions, - }); - - const actual = builder.sanitize(options); - - expect(actual.minLength).toEqual(expectedMinLength); - }, - ); - - it("should preserve unknown properties", () => { - const policy = Object.assign({}, DisabledPasswordGeneratorPolicy); - const builder = new PasswordGeneratorOptionsEvaluator(policy); - const options = Object.freeze({ - unknown: "property", - another: "unknown property", - }) as PasswordGenerationOptions; - - const sanitizedOptions: any = builder.sanitize(options); - - expect(sanitizedOptions.unknown).toEqual("property"); - expect(sanitizedOptions.another).toEqual("unknown property"); - }); - }); -}); diff --git a/libs/common/src/tools/generator/password/password-generator-options-evaluator.ts b/libs/common/src/tools/generator/password/password-generator-options-evaluator.ts deleted file mode 100644 index 79cb0a9b8ee..00000000000 --- a/libs/common/src/tools/generator/password/password-generator-options-evaluator.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { PolicyEvaluator } from "../abstractions/policy-evaluator.abstraction"; - -import { PasswordGenerationOptions } from "./password-generation-options"; -import { PasswordGeneratorPolicy } from "./password-generator-policy"; - -function initializeBoundaries() { - const length = Object.freeze({ - min: 5, - max: 128, - }); - - const minDigits = Object.freeze({ - min: 0, - max: 9, - }); - - const minSpecialCharacters = Object.freeze({ - min: 0, - max: 9, - }); - - return Object.freeze({ - length, - minDigits, - minSpecialCharacters, - }); -} - -/** Immutable default boundaries for password generation. - * These are used when the policy does not override a value. - */ -export const DefaultBoundaries = initializeBoundaries(); - -type Boundary = { - readonly min: number; - readonly max: number; -}; - -/** Enforces policy for password generation. - */ -export class PasswordGeneratorOptionsEvaluator - implements PolicyEvaluator -{ - // This design is not ideal, but it is a step towards a more robust password - // generator. Ideally, `sanitize` would be implemented on an options class, - // and `applyPolicy` would be implemented on a policy class, "mise en place". - // - // The current design of the password generator, unfortunately, would require - // a substantial rewrite to make this feasible. Hopefully this change can be - // applied when the password generator is ported to rust. - - /** Boundaries for the password length. This is always large enough - * to accommodate the minimum number of digits and special characters. - */ - readonly length: Boundary; - - /** Boundaries for the minimum number of digits allowed in the password. - */ - readonly minDigits: Boundary; - - /** Boundaries for the minimum number of special characters allowed - * in the password. - */ - readonly minSpecialCharacters: Boundary; - - /** Policy applied by the evaluator. - */ - readonly policy: PasswordGeneratorPolicy; - - /** Instantiates the evaluator. - * @param policy The policy applied by the evaluator. When this conflicts with - * the defaults, the policy takes precedence. - */ - constructor(policy: PasswordGeneratorPolicy) { - function createBoundary(value: number, defaultBoundary: Boundary): Boundary { - const boundary = { - min: Math.max(defaultBoundary.min, value), - max: Math.max(defaultBoundary.max, value), - }; - - return boundary; - } - - this.policy = structuredClone(policy); - this.minDigits = createBoundary(policy.numberCount, DefaultBoundaries.minDigits); - this.minSpecialCharacters = createBoundary( - policy.specialCount, - DefaultBoundaries.minSpecialCharacters, - ); - - // the overall length should be at least as long as the sum of the minimums - const minConsistentLength = this.minDigits.min + this.minSpecialCharacters.min; - const minPolicyLength = policy.minLength > 0 ? policy.minLength : DefaultBoundaries.length.min; - const minLength = Math.max(minPolicyLength, minConsistentLength, DefaultBoundaries.length.min); - - this.length = { - min: minLength, - max: Math.max(DefaultBoundaries.length.max, minLength), - }; - } - - /** {@link PolicyEvaluator.policyInEffect} */ - get policyInEffect(): boolean { - const policies = [ - this.policy.useUppercase, - this.policy.useLowercase, - this.policy.useNumbers, - this.policy.useSpecial, - this.policy.minLength > DefaultBoundaries.length.min, - this.policy.numberCount > DefaultBoundaries.minDigits.min, - this.policy.specialCount > DefaultBoundaries.minSpecialCharacters.min, - ]; - - return policies.includes(true); - } - - /** {@link PolicyEvaluator.applyPolicy} */ - applyPolicy(options: PasswordGenerationOptions): PasswordGenerationOptions { - function fitToBounds(value: number, boundaries: Boundary) { - const { min, max } = boundaries; - - const withUpperBound = Math.min(value || 0, max); - const withLowerBound = Math.max(withUpperBound, min); - - return withLowerBound; - } - - // apply policy overrides - const uppercase = this.policy.useUppercase || options.uppercase || false; - const lowercase = this.policy.useLowercase || options.lowercase || false; - - // these overrides can cascade numeric fields to boolean fields - const number = this.policy.useNumbers || options.number || options.minNumber > 0; - const special = this.policy.useSpecial || options.special || options.minSpecial > 0; - - // apply boundaries; the boundaries can cascade boolean fields to numeric fields - const length = fitToBounds(options.length, this.length); - const minNumber = fitToBounds(options.minNumber, this.minDigits); - const minSpecial = fitToBounds(options.minSpecial, this.minSpecialCharacters); - - return { - ...options, - length, - uppercase, - lowercase, - number, - minNumber, - special, - minSpecial, - }; - } - - /** {@link PolicyEvaluator.sanitize} */ - sanitize(options: PasswordGenerationOptions): PasswordGenerationOptions { - function cascade(enabled: boolean, value: number): [boolean, number] { - const enabledResult = enabled ?? value > 0; - const valueResult = enabledResult ? value || 1 : 0; - - return [enabledResult, valueResult]; - } - - const [lowercase, minLowercase] = cascade(options.lowercase, options.minLowercase); - const [uppercase, minUppercase] = cascade(options.uppercase, options.minUppercase); - const [number, minNumber] = cascade(options.number, options.minNumber); - const [special, minSpecial] = cascade(options.special, options.minSpecial); - - // minimums can only increase the length - const minConsistentLength = minLowercase + minUppercase + minNumber + minSpecial; - const minLength = Math.max(minConsistentLength, this.length.min); - const length = Math.max(options.length ?? minLength, minLength); - - return { - ...options, - length, - minLength, - lowercase, - minLowercase, - uppercase, - minUppercase, - number, - minNumber, - special, - minSpecial, - }; - } -} diff --git a/libs/common/src/tools/generator/password/password-generator-options.ts b/libs/common/src/tools/generator/password/password-generator-options.ts deleted file mode 100644 index 04a2f8c77a6..00000000000 --- a/libs/common/src/tools/generator/password/password-generator-options.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { GeneratorNavigation } from "../navigation/generator-navigation"; -import { PassphraseGenerationOptions } from "../passphrase/passphrase-generation-options"; - -import { PasswordGenerationOptions } from "./password-generation-options"; - -/** Request format for credential generation. - * This type includes all properties suitable for reactive data binding. - */ -export type PasswordGeneratorOptions = PasswordGenerationOptions & - PassphraseGenerationOptions & - GeneratorNavigation & { policyUpdated?: boolean }; diff --git a/libs/common/src/tools/generator/password/password-generator-policy.spec.ts b/libs/common/src/tools/generator/password/password-generator-policy.spec.ts deleted file mode 100644 index 206d88741b0..00000000000 --- a/libs/common/src/tools/generator/password/password-generator-policy.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { PolicyType } from "../../../admin-console/enums"; -// FIXME: use index.ts imports once policy abstractions and models -// implement ADR-0002 -import { Policy } from "../../../admin-console/models/domain/policy"; -import { PolicyId } from "../../../types/guid"; - -import { DisabledPasswordGeneratorPolicy, leastPrivilege } from "./password-generator-policy"; - -function createPolicy( - data: any, - type: PolicyType = PolicyType.PasswordGenerator, - enabled: boolean = true, -) { - return new Policy({ - id: "id" as PolicyId, - organizationId: "organizationId", - data, - enabled, - type, - }); -} - -describe("leastPrivilege", () => { - it("should return the accumulator when the policy type does not apply", () => { - const policy = createPolicy({}, PolicyType.RequireSso); - - const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy); - - expect(result).toEqual(DisabledPasswordGeneratorPolicy); - }); - - it("should return the accumulator when the policy is not enabled", () => { - const policy = createPolicy({}, PolicyType.PasswordGenerator, false); - - const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy); - - expect(result).toEqual(DisabledPasswordGeneratorPolicy); - }); - - it.each([ - ["minLength", 10, "minLength"], - ["useUpper", true, "useUppercase"], - ["useLower", true, "useLowercase"], - ["useNumbers", true, "useNumbers"], - ["minNumbers", 10, "numberCount"], - ["useSpecial", true, "useSpecial"], - ["minSpecial", 10, "specialCount"], - ])("should take the %p from the policy", (input, value, expected) => { - const policy = createPolicy({ [input]: value }); - - const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy); - - expect(result).toEqual({ ...DisabledPasswordGeneratorPolicy, [expected]: value }); - }); -}); diff --git a/libs/common/src/tools/generator/password/password-generator-policy.ts b/libs/common/src/tools/generator/password/password-generator-policy.ts deleted file mode 100644 index 7de6b49788d..00000000000 --- a/libs/common/src/tools/generator/password/password-generator-policy.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { PolicyType } from "../../../admin-console/enums"; -// FIXME: use index.ts imports once policy abstractions and models -// implement ADR-0002 -import { Policy } from "../../../admin-console/models/domain/policy"; - -/** Policy options enforced during password generation. */ -export type PasswordGeneratorPolicy = { - /** The minimum length of generated passwords. - * When this is less than or equal to zero, it is ignored. - * If this is less than the total number of characters required by - * the policy's other settings, then it is ignored. - */ - minLength: number; - - /** When this is true, an uppercase character must be part of - * the generated password. - */ - useUppercase: boolean; - - /** When this is true, a lowercase character must be part of - * the generated password. - */ - useLowercase: boolean; - - /** When this is true, at least one digit must be part of the generated - * password. - */ - useNumbers: boolean; - - /** The quantity of digits to include in the generated password. - * When this is less than or equal to zero, it is ignored. - */ - numberCount: number; - - /** When this is true, at least one digit must be part of the generated - * password. - */ - useSpecial: boolean; - - /** The quantity of special characters to include in the generated - * password. When this is less than or equal to zero, it is ignored. - */ - specialCount: number; -}; - -/** The default options for password generation policy. */ -export const DisabledPasswordGeneratorPolicy: PasswordGeneratorPolicy = Object.freeze({ - minLength: 0, - useUppercase: false, - useLowercase: false, - useNumbers: false, - numberCount: 0, - useSpecial: false, - specialCount: 0, -}); - -/** Reduces a policy into an accumulator by accepting the most restrictive - * values from each policy. - * @param acc the accumulator - * @param policy the policy to reduce - * @returns the most restrictive values between the policy and accumulator. - */ -export function leastPrivilege(acc: PasswordGeneratorPolicy, policy: Policy) { - if (policy.type !== PolicyType.PasswordGenerator || !policy.enabled) { - return acc; - } - - return { - minLength: Math.max(acc.minLength, policy.data.minLength ?? acc.minLength), - useUppercase: policy.data.useUpper || acc.useUppercase, - useLowercase: policy.data.useLower || acc.useLowercase, - useNumbers: policy.data.useNumbers || acc.useNumbers, - numberCount: Math.max(acc.numberCount, policy.data.minNumbers ?? acc.numberCount), - useSpecial: policy.data.useSpecial || acc.useSpecial, - specialCount: Math.max(acc.specialCount, policy.data.minSpecial ?? acc.specialCount), - }; -} 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 deleted file mode 100644 index 668dd818e25..00000000000 --- a/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * include structuredClone in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ - -import { mock } from "jest-mock-extended"; -import { of, firstValueFrom } from "rxjs"; - -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 { Randomizer } from "../abstractions/randomizer"; -import { PASSWORD_SETTINGS } from "../key-definitions"; - -import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy"; - -import { - DefaultPasswordGenerationOptions, - PasswordGeneratorOptionsEvaluator, - PasswordGeneratorStrategy, -} from "."; - -const SomeUser = "some user" as UserId; - -describe("Password generation strategy", () => { - describe("toEvaluator()", () => { - it("should map to a password policy evaluator", async () => { - const strategy = new PasswordGeneratorStrategy(null, null); - const policy = mock({ - type: PolicyType.PasswordGenerator, - data: { - minLength: 10, - useUpper: true, - useLower: true, - useNumbers: true, - minNumbers: 1, - useSpecial: true, - minSpecial: 1, - }, - }); - - const evaluator$ = of([policy]).pipe(strategy.toEvaluator()); - const evaluator = await firstValueFrom(evaluator$); - - expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator); - expect(evaluator.policy).toMatchObject({ - minLength: 10, - useUppercase: true, - useLowercase: true, - useNumbers: true, - numberCount: 1, - useSpecial: true, - specialCount: 1, - }); - }); - - it.each([[[]], [null], [undefined]])( - "should map `%p` to a disabled password policy evaluator", - async (policies) => { - const strategy = new PasswordGeneratorStrategy(null, null); - - const evaluator$ = of(policies).pipe(strategy.toEvaluator()); - const evaluator = await firstValueFrom(evaluator$); - - expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator); - expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy); - }, - ); - }); - - describe("durableState", () => { - it("should use password settings key", () => { - const provider = mock(); - const randomizer = mock(); - const strategy = new PasswordGeneratorStrategy(randomizer, provider); - - strategy.durableState(SomeUser); - - expect(provider.getUser).toHaveBeenCalledWith(SomeUser, PASSWORD_SETTINGS); - }); - }); - - describe("defaults$", () => { - it("should return the default subaddress options", async () => { - const strategy = new PasswordGeneratorStrategy(null, null); - - const result = await firstValueFrom(strategy.defaults$(SomeUser)); - - expect(result).toEqual(DefaultPasswordGenerationOptions); - }); - }); - - describe("policy", () => { - it("should use password generator policy", () => { - const randomizer = mock(); - const strategy = new PasswordGeneratorStrategy(randomizer, null); - - expect(strategy.policy).toBe(PolicyType.PasswordGenerator); - }); - }); - - describe("generate()", () => { - it.todo("should generate a password using the given options"); - }); -}); diff --git a/libs/common/src/tools/generator/password/password-generator-strategy.ts b/libs/common/src/tools/generator/password/password-generator-strategy.ts deleted file mode 100644 index 075c331e066..00000000000 --- a/libs/common/src/tools/generator/password/password-generator-strategy.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { GeneratorStrategy } from ".."; -import { PolicyType } from "../../../admin-console/enums"; -import { StateProvider } from "../../../platform/state"; -import { Randomizer } from "../abstractions/randomizer"; -import { PASSWORD_SETTINGS } from "../key-definitions"; -import { Policies } from "../policies"; -import { mapPolicyToEvaluator } from "../rx-operators"; -import { clone$PerUserId, sharedStateByUserId } from "../util"; - -import { - DefaultPasswordGenerationOptions, - PasswordGenerationOptions, -} from "./password-generation-options"; -import { PasswordGeneratorPolicy } from "./password-generator-policy"; - -/** Generates passwords composed of random characters */ -export class PasswordGeneratorStrategy - implements GeneratorStrategy -{ - /** instantiates the password generator strategy. - * @param legacy generates the password - */ - constructor( - private randomizer: Randomizer, - private stateProvider: StateProvider, - ) {} - - // configuration - durableState = sharedStateByUserId(PASSWORD_SETTINGS, this.stateProvider); - defaults$ = clone$PerUserId(DefaultPasswordGenerationOptions); - readonly policy = PolicyType.PasswordGenerator; - toEvaluator() { - return mapPolicyToEvaluator(Policies.Password); - } - - // algorithm - async generate(options: PasswordGenerationOptions): Promise { - const o = { ...DefaultPasswordGenerationOptions, ...options }; - let positions: string[] = []; - if (o.lowercase && o.minLowercase > 0) { - for (let i = 0; i < o.minLowercase; i++) { - positions.push("l"); - } - } - if (o.uppercase && o.minUppercase > 0) { - for (let i = 0; i < o.minUppercase; i++) { - positions.push("u"); - } - } - if (o.number && o.minNumber > 0) { - for (let i = 0; i < o.minNumber; i++) { - positions.push("n"); - } - } - if (o.special && o.minSpecial > 0) { - for (let i = 0; i < o.minSpecial; i++) { - positions.push("s"); - } - } - while (positions.length < o.length) { - positions.push("a"); - } - - // shuffle - positions = await this.randomizer.shuffle(positions); - - // build out the char sets - let allCharSet = ""; - - let lowercaseCharSet = "abcdefghijkmnopqrstuvwxyz"; - if (o.ambiguous) { - lowercaseCharSet += "l"; - } - if (o.lowercase) { - allCharSet += lowercaseCharSet; - } - - let uppercaseCharSet = "ABCDEFGHJKLMNPQRSTUVWXYZ"; - if (o.ambiguous) { - uppercaseCharSet += "IO"; - } - if (o.uppercase) { - allCharSet += uppercaseCharSet; - } - - let numberCharSet = "23456789"; - if (o.ambiguous) { - numberCharSet += "01"; - } - if (o.number) { - allCharSet += numberCharSet; - } - - const specialCharSet = "!@#$%^&*"; - if (o.special) { - allCharSet += specialCharSet; - } - - let password = ""; - for (let i = 0; i < o.length; i++) { - let positionChars: string; - switch (positions[i]) { - case "l": - positionChars = lowercaseCharSet; - break; - case "u": - positionChars = uppercaseCharSet; - break; - case "n": - positionChars = numberCharSet; - break; - case "s": - positionChars = specialCharSet; - break; - case "a": - positionChars = allCharSet; - break; - default: - break; - } - - const randomCharIndex = await this.randomizer.uniform(0, positionChars.length - 1); - password += positionChars.charAt(randomCharIndex); - } - - return password; - } -} diff --git a/libs/common/src/tools/generator/policies.ts b/libs/common/src/tools/generator/policies.ts deleted file mode 100644 index 27521f0eebe..00000000000 --- a/libs/common/src/tools/generator/policies.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Policy as AdminPolicy } from "@bitwarden/common/admin-console/models/domain/policy"; - -import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorPolicy } from "./passphrase"; -import { - DisabledPassphraseGeneratorPolicy, - leastPrivilege as passphraseLeastPrivilege, -} from "./passphrase/passphrase-generator-policy"; -import { PasswordGeneratorOptionsEvaluator, PasswordGeneratorPolicy } from "./password"; -import { - DisabledPasswordGeneratorPolicy, - leastPrivilege as passwordLeastPrivilege, -} from "./password/password-generator-policy"; - -/** Determines how to construct a password generator policy */ -export type PolicyConfiguration = { - /** The value of the policy when it is not in effect. */ - disabledValue: Policy; - - /** Combines multiple policies set by the administrative console into - * a single policy. - */ - combine: (acc: Policy, policy: AdminPolicy) => Policy; - - /** Converts policy service data into an actionable policy. - */ - createEvaluator: (policy: Policy) => Evaluator; -}; - -const PASSPHRASE = Object.freeze({ - disabledValue: DisabledPassphraseGeneratorPolicy, - combine: passphraseLeastPrivilege, - createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy), -} as PolicyConfiguration); - -const PASSWORD = Object.freeze({ - disabledValue: DisabledPasswordGeneratorPolicy, - combine: passwordLeastPrivilege, - createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy), -} as PolicyConfiguration); - -/** Policy configurations */ -export const Policies = Object.freeze({ - /** Passphrase policy configuration */ - Passphrase: PASSPHRASE, - - /** Passphrase policy configuration */ - Password: PASSWORD, -}); diff --git a/libs/common/src/tools/generator/random.ts b/libs/common/src/tools/generator/random.ts deleted file mode 100644 index f2a4673d4c0..00000000000 --- a/libs/common/src/tools/generator/random.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; - -import { Randomizer } from "./abstractions/randomizer"; -import { WordOptions } from "./word-options"; - -/** A randomizer backed by a CryptoService. */ -export class CryptoServiceRandomizer implements Randomizer { - constructor(private crypto: CryptoService) {} - - async pick(list: Array) { - const index = await this.uniform(0, list.length - 1); - return list[index]; - } - - async pickWord(list: Array, options?: WordOptions) { - let word = await this.pick(list); - - if (options?.titleCase ?? false) { - word = word.charAt(0).toUpperCase() + word.slice(1); - } - - if (options?.number ?? false) { - const num = await this.crypto.randomNumber(1, 9); - word = word + num.toString(); - } - - return word; - } - - // ref: https://stackoverflow.com/a/12646864/1090359 - async shuffle(items: Array, options?: { copy?: boolean }) { - const shuffled = (options?.copy ?? true) ? [...items] : items; - - for (let i = shuffled.length - 1; i > 0; i--) { - const j = await this.uniform(0, i); - [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; - } - - return shuffled; - } - - async chars(length: number) { - let str = ""; - const charSet = "abcdefghijklmnopqrstuvwxyz1234567890"; - for (let i = 0; i < length; i++) { - const randomCharIndex = await this.uniform(0, charSet.length - 1); - str += charSet.charAt(randomCharIndex); - } - return str; - } - - async uniform(min: number, max: number) { - return this.crypto.randomNumber(min, max); - } - - // ref: https://stackoverflow.com/a/10073788 - private zeroPad(number: string, width: number) { - return number.length >= width - ? number - : new Array(width - number.length + 1).join("0") + number; - } -} diff --git a/libs/common/src/tools/generator/rx-operators.ts b/libs/common/src/tools/generator/rx-operators.ts deleted file mode 100644 index 77934953447..00000000000 --- a/libs/common/src/tools/generator/rx-operators.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { map, pipe } from "rxjs"; - -import { reduceCollection, distinctIfShallowMatch } from "@bitwarden/common/tools/rx"; - -import { DefaultPolicyEvaluator } from "./default-policy-evaluator"; -import { PolicyConfiguration } from "./policies"; - -/** Maps an administrative console policy to a policy evaluator using the provided configuration. - * @param configuration the configuration that constructs the evaluator. - */ -export function mapPolicyToEvaluator( - configuration: PolicyConfiguration, -) { - return pipe( - reduceCollection(configuration.combine, configuration.disabledValue), - distinctIfShallowMatch(), - map(configuration.createEvaluator), - ); -} - -/** Constructs a method that maps a policy to the default (no-op) policy. */ -export function newDefaultEvaluator() { - return () => { - return pipe(map((_) => new DefaultPolicyEvaluator())); - }; -} diff --git a/libs/common/src/tools/generator/username/catchall-generator-options.ts b/libs/common/src/tools/generator/username/catchall-generator-options.ts deleted file mode 100644 index bddf98f7576..00000000000 --- a/libs/common/src/tools/generator/username/catchall-generator-options.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { RequestOptions } from "./options/forwarder-options"; -import { UsernameGenerationMode } from "./options/generator-options"; - -/** Settings supported when generating an email subaddress */ -export type CatchallGenerationOptions = { - /** selects the generation algorithm for the catchall email address. */ - catchallType?: UsernameGenerationMode; - - /** The domain part of the generated email address. - * @example If the domain is `domain.io` and the generated username - * is `jd`, then the generated email address will be `jd@mydomain.io` - */ - catchallDomain?: string; -} & RequestOptions; - -/** The default options for catchall address generation. */ -export const DefaultCatchallOptions: CatchallGenerationOptions = Object.freeze({ - catchallType: "random", - catchallDomain: "", - website: null, -}); 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 deleted file mode 100644 index 45e87160817..00000000000 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { mock } from "jest-mock-extended"; -import { of, firstValueFrom } from "rxjs"; - -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 { Randomizer } from "../abstractions/randomizer"; -import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; -import { CATCHALL_SETTINGS } from "../key-definitions"; - -import { DefaultCatchallOptions } from "./catchall-generator-options"; - -import { CatchallGeneratorStrategy } from "."; - -const SomeUser = "some user" as UserId; -const SomePolicy = mock({ - type: PolicyType.PasswordGenerator, - data: { - minLength: 10, - }, -}); - -describe("Email subaddress list generation strategy", () => { - describe("toEvaluator()", () => { - it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])( - "should map any input (= %p) to the default policy evaluator", - async (policies) => { - const strategy = new CatchallGeneratorStrategy(null, null); - - const evaluator$ = of(policies).pipe(strategy.toEvaluator()); - const evaluator = await firstValueFrom(evaluator$); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - }, - ); - }); - - describe("durableState", () => { - it("should use password settings key", () => { - const provider = mock(); - const randomizer = mock(); - const strategy = new CatchallGeneratorStrategy(randomizer, provider); - - strategy.durableState(SomeUser); - - expect(provider.getUser).toHaveBeenCalledWith(SomeUser, CATCHALL_SETTINGS); - }); - }); - - describe("defaults$", () => { - it("should return the default subaddress options", async () => { - const strategy = new CatchallGeneratorStrategy(null, null); - - const result = await firstValueFrom(strategy.defaults$(SomeUser)); - - expect(result).toEqual(DefaultCatchallOptions); - }); - }); - - describe("policy", () => { - it("should use password generator policy", () => { - const randomizer = mock(); - const strategy = new CatchallGeneratorStrategy(randomizer, null); - - expect(strategy.policy).toBe(PolicyType.PasswordGenerator); - }); - }); - - describe("generate()", () => { - it.todo("generate catchall email addresses"); - }); -}); diff --git a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts deleted file mode 100644 index fb015a596ff..00000000000 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { PolicyType } from "../../../admin-console/enums"; -import { StateProvider } from "../../../platform/state"; -import { GeneratorStrategy } from "../abstractions"; -import { Randomizer } from "../abstractions/randomizer"; -import { CATCHALL_SETTINGS } from "../key-definitions"; -import { NoPolicy } from "../no-policy"; -import { newDefaultEvaluator } from "../rx-operators"; -import { clone$PerUserId, sharedStateByUserId } from "../util"; - -import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options"; - -/** 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 random: Randomizer, - private stateProvider: StateProvider, - private defaultOptions: CatchallGenerationOptions = DefaultCatchallOptions, - ) {} - - // configuration - durableState = sharedStateByUserId(CATCHALL_SETTINGS, this.stateProvider); - defaults$ = clone$PerUserId(this.defaultOptions); - toEvaluator = newDefaultEvaluator(); - readonly policy = PolicyType.PasswordGenerator; - - // algorithm - async generate(options: CatchallGenerationOptions) { - const o = Object.assign({}, DefaultCatchallOptions, options); - - if (o.catchallDomain == null || o.catchallDomain === "") { - return null; - } - if (o.catchallType == null) { - o.catchallType = "random"; - } - - let startString = ""; - if (o.catchallType === "random") { - startString = await this.random.chars(8); - } else if (o.catchallType === "website-name") { - startString = o.website; - } - return startString + "@" + o.catchallDomain; - } -} diff --git a/libs/common/src/tools/generator/username/eff-username-generator-options.ts b/libs/common/src/tools/generator/username/eff-username-generator-options.ts deleted file mode 100644 index 07890b3d55e..00000000000 --- a/libs/common/src/tools/generator/username/eff-username-generator-options.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { RequestOptions } from "./options/forwarder-options"; - -/** Settings supported when generating a username using the EFF word list */ -export type EffUsernameGenerationOptions = { - /** when true, the word is capitalized */ - wordCapitalize?: boolean; - - /** when true, a random number is appended to the username */ - wordIncludeNumber?: boolean; -} & RequestOptions; - -/** The default options for EFF long word generation. */ -export const DefaultEffUsernameOptions: EffUsernameGenerationOptions = Object.freeze({ - wordCapitalize: false, - wordIncludeNumber: false, - website: null, -}); 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 deleted file mode 100644 index 128b69e6734..00000000000 --- a/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { mock } from "jest-mock-extended"; -import { of, firstValueFrom } from "rxjs"; - -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 { Randomizer } from "../abstractions/randomizer"; -import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; -import { EFF_USERNAME_SETTINGS } from "../key-definitions"; - -import { DefaultEffUsernameOptions } from "./eff-username-generator-options"; - -import { EffUsernameGeneratorStrategy } from "."; - -const SomeUser = "some user" as UserId; -const SomePolicy = mock({ - type: PolicyType.PasswordGenerator, - data: { - minLength: 10, - }, -}); - -describe("EFF long word list generation strategy", () => { - describe("toEvaluator()", () => { - it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])( - "should map any input (= %p) to the default policy evaluator", - async (policies) => { - const strategy = new EffUsernameGeneratorStrategy(null, null); - - const evaluator$ = of(policies).pipe(strategy.toEvaluator()); - const evaluator = await firstValueFrom(evaluator$); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - }, - ); - }); - - describe("durableState", () => { - it("should use password settings key", () => { - const provider = mock(); - const randomizer = mock(); - const strategy = new EffUsernameGeneratorStrategy(randomizer, provider); - - strategy.durableState(SomeUser); - - expect(provider.getUser).toHaveBeenCalledWith(SomeUser, EFF_USERNAME_SETTINGS); - }); - }); - - describe("defaults$", () => { - it("should return the default subaddress options", async () => { - const strategy = new EffUsernameGeneratorStrategy(null, null); - - const result = await firstValueFrom(strategy.defaults$(SomeUser)); - - expect(result).toEqual(DefaultEffUsernameOptions); - }); - }); - - describe("policy", () => { - it("should use password generator policy", () => { - const randomizer = mock(); - const strategy = new EffUsernameGeneratorStrategy(randomizer, null); - - expect(strategy.policy).toBe(PolicyType.PasswordGenerator); - }); - }); - - describe("generate()", () => { - it.todo("generate username tests"); - }); -}); 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 deleted file mode 100644 index abd8e6b226c..00000000000 --- a/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; - -import { PolicyType } from "../../../admin-console/enums"; -import { StateProvider } from "../../../platform/state"; -import { GeneratorStrategy } from "../abstractions"; -import { Randomizer } from "../abstractions/randomizer"; -import { EFF_USERNAME_SETTINGS } from "../key-definitions"; -import { NoPolicy } from "../no-policy"; -import { newDefaultEvaluator } from "../rx-operators"; -import { clone$PerUserId, sharedStateByUserId } from "../util"; - -import { - DefaultEffUsernameOptions, - EffUsernameGenerationOptions, -} from "./eff-username-generator-options"; - -/** Strategy for creating usernames from the EFF wordlist */ -export class EffUsernameGeneratorStrategy - implements GeneratorStrategy -{ - /** Instantiates the generation strategy - * @param usernameService generates a username from EFF word list - */ - constructor( - private random: Randomizer, - private stateProvider: StateProvider, - private defaultOptions: EffUsernameGenerationOptions = DefaultEffUsernameOptions, - ) {} - - // configuration - durableState = sharedStateByUserId(EFF_USERNAME_SETTINGS, this.stateProvider); - defaults$ = clone$PerUserId(this.defaultOptions); - toEvaluator = newDefaultEvaluator(); - readonly policy = PolicyType.PasswordGenerator; - - // algorithm - async generate(options: EffUsernameGenerationOptions) { - const word = await this.random.pickWord(EFFLongWordList, { - titleCase: options.wordCapitalize ?? DefaultEffUsernameOptions.wordCapitalize, - number: options.wordIncludeNumber ?? DefaultEffUsernameOptions.wordIncludeNumber, - }); - return word; - } -} diff --git a/libs/common/src/tools/generator/username/email-forwarders/anon-addy-forwarder.ts b/libs/common/src/tools/generator/username/email-forwarders/anon-addy-forwarder.ts deleted file mode 100644 index c50326e1fa4..00000000000 --- a/libs/common/src/tools/generator/username/email-forwarders/anon-addy-forwarder.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; - -import { Forwarder } from "./forwarder"; -import { ForwarderOptions } from "./forwarder-options"; - -export class AnonAddyForwarder implements Forwarder { - async generate(apiService: ApiService, options: ForwarderOptions): Promise { - if (options.apiKey == null || options.apiKey === "") { - throw "Invalid addy.io API token."; - } - if (options.anonaddy?.domain == null || options.anonaddy.domain === "") { - throw "Invalid addy.io domain."; - } - if (options.anonaddy?.baseUrl == null || options.anonaddy.baseUrl === "") { - throw "Invalid addy.io url."; - } - - const requestInit: RequestInit = { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authorization: "Bearer " + options.apiKey, - "Content-Type": "application/json", - "X-Requested-With": "XMLHttpRequest", - }), - }; - const url = options.anonaddy.baseUrl + "/api/v1/aliases"; - requestInit.body = JSON.stringify({ - domain: options.anonaddy.domain, - description: - (options.website != null ? "Website: " + options.website + ". " : "") + - "Generated by Bitwarden.", - }); - const request = new Request(url, requestInit); - const response = await apiService.nativeFetch(request); - if (response.status === 200 || response.status === 201) { - const json = await response.json(); - return json?.data?.email; - } - if (response.status === 401) { - throw "Invalid addy.io API token."; - } - if (response?.statusText != null) { - throw "addy.io error:\n" + response.statusText; - } - throw "Unknown addy.io error occurred."; - } -} diff --git a/libs/common/src/tools/generator/username/email-forwarders/duck-duck-go-forwarder.ts b/libs/common/src/tools/generator/username/email-forwarders/duck-duck-go-forwarder.ts deleted file mode 100644 index 45e4f8f3a40..00000000000 --- a/libs/common/src/tools/generator/username/email-forwarders/duck-duck-go-forwarder.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; - -import { Forwarder } from "./forwarder"; -import { ForwarderOptions } from "./forwarder-options"; - -export class DuckDuckGoForwarder implements Forwarder { - async generate(apiService: ApiService, options: ForwarderOptions): Promise { - if (options.apiKey == null || options.apiKey === "") { - throw "Invalid DuckDuckGo API token."; - } - const requestInit: RequestInit = { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authorization: "Bearer " + options.apiKey, - "Content-Type": "application/json", - }), - }; - const url = "https://quack.duckduckgo.com/api/email/addresses"; - const request = new Request(url, requestInit); - const response = await apiService.nativeFetch(request); - if (response.status === 200 || response.status === 201) { - const json = await response.json(); - if (json.address) { - return `${json.address}@duck.com`; - } - } else if (response.status === 401) { - throw "Invalid DuckDuckGo API token."; - } - throw "Unknown DuckDuckGo error occurred."; - } -} diff --git a/libs/common/src/tools/generator/username/email-forwarders/fastmail-forwarder.ts b/libs/common/src/tools/generator/username/email-forwarders/fastmail-forwarder.ts deleted file mode 100644 index 09e518c3d70..00000000000 --- a/libs/common/src/tools/generator/username/email-forwarders/fastmail-forwarder.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; - -import { Forwarder } from "./forwarder"; -import { ForwarderOptions } from "./forwarder-options"; - -export class FastmailForwarder implements Forwarder { - async generate(apiService: ApiService, options: ForwarderOptions): Promise { - if (options.apiKey == null || options.apiKey === "") { - throw "Invalid Fastmail API token."; - } - - const accountId = await this.getAccountId(apiService, options); - if (accountId == null || accountId === "") { - throw "Unable to obtain Fastmail masked email account ID."; - } - - const forDomain = options.website || ""; - - const requestInit: RequestInit = { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authorization: "Bearer " + options.apiKey, - "Content-Type": "application/json", - }), - }; - const url = "https://api.fastmail.com/jmap/api/"; - requestInit.body = JSON.stringify({ - using: ["https://www.fastmail.com/dev/maskedemail", "urn:ietf:params:jmap:core"], - methodCalls: [ - [ - "MaskedEmail/set", - { - accountId: accountId, - create: { - "new-masked-email": { - state: "enabled", - description: "", - forDomain: forDomain, - emailPrefix: options.fastmail.prefix, - }, - }, - }, - "0", - ], - ], - }); - const request = new Request(url, requestInit); - const response = await apiService.nativeFetch(request); - if (response.status === 200) { - const json = await response.json(); - if ( - json.methodResponses != null && - json.methodResponses.length > 0 && - json.methodResponses[0].length > 0 - ) { - if (json.methodResponses[0][0] === "MaskedEmail/set") { - if (json.methodResponses[0][1]?.created?.["new-masked-email"] != null) { - return json.methodResponses[0][1]?.created?.["new-masked-email"]?.email; - } - if (json.methodResponses[0][1]?.notCreated?.["new-masked-email"] != null) { - throw ( - "Fastmail error: " + - json.methodResponses[0][1]?.notCreated?.["new-masked-email"]?.description - ); - } - } else if (json.methodResponses[0][0] === "error") { - throw "Fastmail error: " + json.methodResponses[0][1]?.description; - } - } - } - if (response.status === 401 || response.status === 403) { - throw "Invalid Fastmail API token."; - } - throw "Unknown Fastmail error occurred."; - } - - private async getAccountId(apiService: ApiService, options: ForwarderOptions): Promise { - const requestInit: RequestInit = { - cache: "no-store", - method: "GET", - headers: new Headers({ - Authorization: "Bearer " + options.apiKey, - }), - }; - const url = "https://api.fastmail.com/.well-known/jmap"; - const request = new Request(url, requestInit); - const response = await apiService.nativeFetch(request); - if (response.status === 200) { - const json = await response.json(); - if (json.primaryAccounts != null) { - return json.primaryAccounts["https://www.fastmail.com/dev/maskedemail"]; - } - } - return null; - } -} diff --git a/libs/common/src/tools/generator/username/email-forwarders/firefox-relay-forwarder.ts b/libs/common/src/tools/generator/username/email-forwarders/firefox-relay-forwarder.ts deleted file mode 100644 index b15a912eabc..00000000000 --- a/libs/common/src/tools/generator/username/email-forwarders/firefox-relay-forwarder.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; - -import { Forwarder } from "./forwarder"; -import { ForwarderOptions } from "./forwarder-options"; - -export class FirefoxRelayForwarder implements Forwarder { - async generate(apiService: ApiService, options: ForwarderOptions): Promise { - if (options.apiKey == null || options.apiKey === "") { - throw "Invalid Firefox Relay API token."; - } - const requestInit: RequestInit = { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authorization: "Token " + options.apiKey, - "Content-Type": "application/json", - }), - }; - const url = "https://relay.firefox.com/api/v1/relayaddresses/"; - requestInit.body = JSON.stringify({ - enabled: true, - generated_for: options.website, - description: - (options.website != null ? options.website + " - " : "") + "Generated by Bitwarden.", - }); - const request = new Request(url, requestInit); - const response = await apiService.nativeFetch(request); - if (response.status === 200 || response.status === 201) { - const json = await response.json(); - return json?.full_address; - } - if (response.status === 401) { - throw "Invalid Firefox Relay API token."; - } - throw "Unknown Firefox Relay error occurred."; - } -} diff --git a/libs/common/src/tools/generator/username/email-forwarders/forward-email-forwarder.ts b/libs/common/src/tools/generator/username/email-forwarders/forward-email-forwarder.ts deleted file mode 100644 index 98801c9e3da..00000000000 --- a/libs/common/src/tools/generator/username/email-forwarders/forward-email-forwarder.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; -import { Utils } from "../../../../platform/misc/utils"; - -import { Forwarder } from "./forwarder"; -import { ForwarderOptions } from "./forwarder-options"; - -export class ForwardEmailForwarder implements Forwarder { - async generate(apiService: ApiService, options: ForwarderOptions): Promise { - if (options.apiKey == null || options.apiKey === "") { - throw "Invalid Forward Email API key."; - } - if (options.forwardemail?.domain == null || options.forwardemail.domain === "") { - throw "Invalid Forward Email domain."; - } - const requestInit: RequestInit = { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authorization: "Basic " + Utils.fromUtf8ToB64(options.apiKey + ":"), - "Content-Type": "application/json", - }), - }; - const url = `https://api.forwardemail.net/v1/domains/${options.forwardemail.domain}/aliases`; - requestInit.body = JSON.stringify({ - labels: options.website, - description: - (options.website != null ? "Website: " + options.website + ". " : "") + - "Generated by Bitwarden.", - }); - const request = new Request(url, requestInit); - const response = await apiService.nativeFetch(request); - if (response.status === 200 || response.status === 201) { - const json = await response.json(); - return json?.name + "@" + (json?.domain?.name || options.forwardemail.domain); - } - if (response.status === 401) { - throw "Invalid Forward Email API key."; - } - const json = await response.json(); - if (json?.message != null) { - throw "Forward Email error:\n" + json.message; - } - if (json?.error != null) { - throw "Forward Email error:\n" + json.error; - } - throw "Unknown Forward Email error occurred."; - } -} diff --git a/libs/common/src/tools/generator/username/email-forwarders/forwarder-options.ts b/libs/common/src/tools/generator/username/email-forwarders/forwarder-options.ts deleted file mode 100644 index 00d1717bf60..00000000000 --- a/libs/common/src/tools/generator/username/email-forwarders/forwarder-options.ts +++ /dev/null @@ -1,25 +0,0 @@ -export class ForwarderOptions { - apiKey: string; - website: string; - fastmail = new FastmailForwarderOptions(); - anonaddy = new AnonAddyForwarderOptions(); - forwardemail = new ForwardEmailForwarderOptions(); - simplelogin = new SimpleLoginForwarderOptions(); -} - -export class FastmailForwarderOptions { - prefix: string; -} - -export class AnonAddyForwarderOptions { - domain: string; - baseUrl: string; -} - -export class ForwardEmailForwarderOptions { - domain: string; -} - -export class SimpleLoginForwarderOptions { - baseUrl: string; -} diff --git a/libs/common/src/tools/generator/username/email-forwarders/forwarder.ts b/libs/common/src/tools/generator/username/email-forwarders/forwarder.ts deleted file mode 100644 index e51fb35c3d9..00000000000 --- a/libs/common/src/tools/generator/username/email-forwarders/forwarder.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; - -import { ForwarderOptions } from "./forwarder-options"; - -export interface Forwarder { - generate(apiService: ApiService, options: ForwarderOptions): Promise; -} diff --git a/libs/common/src/tools/generator/username/email-forwarders/index.ts b/libs/common/src/tools/generator/username/email-forwarders/index.ts deleted file mode 100644 index d102cc236ee..00000000000 --- a/libs/common/src/tools/generator/username/email-forwarders/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { AnonAddyForwarder } from "./anon-addy-forwarder"; -export { DuckDuckGoForwarder } from "./duck-duck-go-forwarder"; -export { FastmailForwarder } from "./fastmail-forwarder"; -export { FirefoxRelayForwarder } from "./firefox-relay-forwarder"; -export { Forwarder } from "./forwarder"; -export { ForwarderOptions } from "./forwarder-options"; -export { SimpleLoginForwarder } from "./simple-login-forwarder"; -export { ForwardEmailForwarder } from "./forward-email-forwarder"; diff --git a/libs/common/src/tools/generator/username/email-forwarders/simple-login-forwarder.ts b/libs/common/src/tools/generator/username/email-forwarders/simple-login-forwarder.ts deleted file mode 100644 index 4d5b7749d49..00000000000 --- a/libs/common/src/tools/generator/username/email-forwarders/simple-login-forwarder.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; - -import { Forwarder } from "./forwarder"; -import { ForwarderOptions } from "./forwarder-options"; - -export class SimpleLoginForwarder implements Forwarder { - async generate(apiService: ApiService, options: ForwarderOptions): Promise { - if (options.apiKey == null || options.apiKey === "") { - throw "Invalid SimpleLogin API key."; - } - const requestInit: RequestInit = { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authentication: options.apiKey, - "Content-Type": "application/json", - }), - }; - let url = options.simplelogin.baseUrl + "/api/alias/random/new"; - if (options.website != null) { - url += "?hostname=" + options.website; - } - requestInit.body = JSON.stringify({ - note: - (options.website != null ? "Website: " + options.website + ". " : "") + - "Generated by Bitwarden.", - }); - const request = new Request(url, requestInit); - const response = await apiService.nativeFetch(request); - if (response.status === 200 || response.status === 201) { - const json = await response.json(); - return json.alias; - } - if (response.status === 401) { - throw "Invalid SimpleLogin API key."; - } - const json = await response.json(); - if (json?.error != null) { - throw "SimpleLogin error:" + json.error; - } - throw "Unknown SimpleLogin error occurred."; - } -} diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts deleted file mode 100644 index e78b432bfb6..00000000000 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { mock } from "jest-mock-extended"; -import { of, firstValueFrom } from "rxjs"; - -import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; -import { PolicyType } from "../../../admin-console/enums"; -// FIXME: use index.ts imports once policy abstractions and models -// implement ADR-0002 -import { Policy } from "../../../admin-console/models/domain/policy"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; -import { StateProvider } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; -import { UserKey } from "../../../types/key"; -import { BufferedState } from "../../state/buffered-state"; -import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; -import { DUCK_DUCK_GO_FORWARDER, DUCK_DUCK_GO_BUFFER } from "../key-definitions"; - -import { ForwarderGeneratorStrategy } from "./forwarder-generator-strategy"; -import { DefaultDuckDuckGoOptions } from "./forwarders/duck-duck-go"; -import { ApiOptions } from "./options/forwarder-options"; - -class TestForwarder extends ForwarderGeneratorStrategy { - constructor( - encryptService: EncryptService, - keyService: CryptoService, - stateProvider: StateProvider, - ) { - super(encryptService, keyService, stateProvider, { website: null, token: "" }); - } - - get key() { - // arbitrary. - return DUCK_DUCK_GO_FORWARDER; - } - - get rolloverKey() { - return DUCK_DUCK_GO_BUFFER; - } - - defaults$ = (userId: UserId) => { - return of(DefaultDuckDuckGoOptions); - }; -} - -const SomeUser = "some user" as UserId; -const AnotherUser = "another user" as UserId; -const SomePolicy = mock({ - type: PolicyType.PasswordGenerator, - data: { - minLength: 10, - }, -}); - -describe("ForwarderGeneratorStrategy", () => { - const encryptService = mock(); - const keyService = mock(); - const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); - - beforeEach(() => { - const keyAvailable = of({} as UserKey); - keyService.getInMemoryUserKeyFor$.mockReturnValue(keyAvailable); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("durableState", () => { - it("constructs a secret state", () => { - const strategy = new TestForwarder(encryptService, keyService, stateProvider); - - const result = strategy.durableState(SomeUser); - - expect(result).toBeInstanceOf(BufferedState); - }); - - it("returns the same secret state for a single user", () => { - const strategy = new TestForwarder(encryptService, keyService, stateProvider); - - const firstResult = strategy.durableState(SomeUser); - const secondResult = strategy.durableState(SomeUser); - - expect(firstResult).toBe(secondResult); - }); - - it("returns a different secret state for a different user", () => { - const strategy = new TestForwarder(encryptService, keyService, stateProvider); - - const firstResult = strategy.durableState(SomeUser); - const secondResult = strategy.durableState(AnotherUser); - - expect(firstResult).not.toBe(secondResult); - }); - }); - - describe("toEvaluator()", () => { - it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])( - "should map any input (= %p) to the default policy evaluator", - async (policies) => { - const strategy = new TestForwarder(encryptService, keyService, stateProvider); - - const evaluator$ = of(policies).pipe(strategy.toEvaluator()); - const evaluator = await firstValueFrom(evaluator$); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - }, - ); - }); -}); diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts deleted file mode 100644 index 4655a3fb72a..00000000000 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { map } from "rxjs"; - -import { PolicyType } from "../../../admin-console/enums"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../../platform/abstractions/encrypt.service"; -import { SingleUserState, StateProvider, UserKeyDefinition } from "../../../platform/state"; -import { UserId } from "../../../types/guid"; -import { BufferedKeyDefinition } from "../../state/buffered-key-definition"; -import { BufferedState } from "../../state/buffered-state"; -import { PaddedDataPacker } from "../../state/padded-data-packer"; -import { SecretClassifier } from "../../state/secret-classifier"; -import { SecretKeyDefinition } from "../../state/secret-key-definition"; -import { SecretState } from "../../state/secret-state"; -import { UserKeyEncryptor } from "../../state/user-key-encryptor"; -import { GeneratorStrategy } from "../abstractions"; -import { NoPolicy } from "../no-policy"; -import { newDefaultEvaluator } from "../rx-operators"; -import { clone$PerUserId, sharedByUserId } from "../util"; - -import { ApiOptions } from "./options/forwarder-options"; - -const OPTIONS_FRAME_SIZE = 512; - -/** An email forwarding service configurable through an API. */ -export abstract class ForwarderGeneratorStrategy< - Options extends ApiOptions, -> extends GeneratorStrategy { - /** Initializes the generator strategy - * @param encryptService protects sensitive forwarder options - * @param keyService looks up the user key when protecting data. - * @param stateProvider creates the durable state for options storage - */ - constructor( - private readonly encryptService: EncryptService, - private readonly keyService: CryptoService, - private stateProvider: StateProvider, - private readonly defaultOptions: Options, - ) { - super(); - } - - /** configures forwarder secret storage */ - protected abstract readonly key: UserKeyDefinition; - - /** configures forwarder import buffer */ - protected abstract readonly rolloverKey: BufferedKeyDefinition; - - // configuration - readonly policy = PolicyType.PasswordGenerator; - defaults$ = clone$PerUserId(this.defaultOptions); - toEvaluator = newDefaultEvaluator(); - durableState = sharedByUserId((userId) => this.getUserSecrets(userId)); - - // per-user encrypted state - private getUserSecrets(userId: UserId): SingleUserState { - // construct the encryptor - const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE); - const encryptor = new UserKeyEncryptor(this.encryptService, this.keyService, packer); - - // always exclude request properties - const classifier = SecretClassifier.allSecret().exclude("website"); - - // Derive the secret key definition - const key = SecretKeyDefinition.value(this.key.stateDefinition, this.key.key, classifier, { - deserializer: (d) => this.key.deserializer(d), - cleanupDelayMs: this.key.cleanupDelayMs, - clearOn: this.key.clearOn, - }); - - // the type parameter is explicit because type inference fails for `Omit` - const secretState = SecretState.from< - Options, - void, - Options, - Record, - Omit - >(userId, key, this.stateProvider, encryptor); - - // rollover should occur once the user key is available for decryption - const canDecrypt$ = this.keyService - .getInMemoryUserKeyFor$(userId) - .pipe(map((key) => key !== null)); - const rolloverState = new BufferedState( - this.stateProvider, - this.rolloverKey, - secretState, - canDecrypt$, - ); - - return rolloverState; - } -} diff --git a/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts b/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts deleted file mode 100644 index f42ca23c113..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts +++ /dev/null @@ -1,236 +0,0 @@ -/** - * include Request in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ -import { firstValueFrom } from "rxjs"; - -import { UserId } from "../../../../types/guid"; -import { ADDY_IO_FORWARDER } from "../../key-definitions"; -import { Forwarders } from "../options/constants"; - -import { AddyIoForwarder, DefaultAddyIoOptions } from "./addy-io"; -import { mockApiService, mockI18nService } from "./mocks.jest"; - -const SomeUser = "some user" as UserId; - -describe("Addy.io Forwarder", () => { - it("key returns the Addy IO forwarder key", () => { - const forwarder = new AddyIoForwarder(null, null, null, null, null); - - expect(forwarder.key).toBe(ADDY_IO_FORWARDER); - }); - - describe("defaults$", () => { - it("should return the default subaddress options", async () => { - const strategy = new AddyIoForwarder(null, null, null, null, null); - - const result = await firstValueFrom(strategy.defaults$(SomeUser)); - - expect(result).toEqual(DefaultAddyIoOptions); - }); - }); - - describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { - it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token, - domain: "example.com", - baseUrl: "https://api.example.com", - }), - ).rejects.toEqual("forwaderInvalidToken"); - - expect(apiService.nativeFetch).not.toHaveBeenCalled(); - expect(i18nService.t).toHaveBeenCalledWith("forwaderInvalidToken", Forwarders.AddyIo.name); - }); - - it.each([null, ""])( - "throws an error if the domain is missing (domain = %p)", - async (domain) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain, - baseUrl: "https://api.example.com", - }), - ).rejects.toEqual("forwarderNoDomain"); - - expect(apiService.nativeFetch).not.toHaveBeenCalled(); - expect(i18nService.t).toHaveBeenCalledWith("forwarderNoDomain", Forwarders.AddyIo.name); - }, - ); - - it.each([null, ""])( - "throws an error if the baseUrl is missing (baseUrl = %p)", - async (baseUrl) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - baseUrl, - }), - ).rejects.toEqual("forwarderNoUrl"); - - expect(apiService.nativeFetch).not.toHaveBeenCalled(); - expect(i18nService.t).toHaveBeenCalledWith("forwarderNoUrl", Forwarders.AddyIo.name); - }, - ); - - it.each([ - ["forwarderGeneratedByWithWebsite", "provided", "bitwarden.com", "bitwarden.com"], - ["forwarderGeneratedByWithWebsite", "provided", "httpbin.org", "httpbin.org"], - ["forwarderGeneratedBy", "not provided", null, ""], - ["forwarderGeneratedBy", "not provided", "", ""], - ])( - "describes the website with %p when the website is %s (= %p)", - async (translationKey, _ignored, website, expectedWebsite) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); - - await forwarder.generate({ - website, - token: "token", - domain: "example.com", - baseUrl: "https://api.example.com", - }); - - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenCalledWith(translationKey, expectedWebsite); - }, - ); - - it.each([ - ["jane.doe@example.com", 201], - ["john.doe@example.com", 201], - ["jane.doe@example.com", 200], - ["john.doe@example.com", 200], - ])( - "returns the generated email address (= %p) if the request is successful (status = %p)", - async (email, status) => { - const apiService = mockApiService(status, { data: { email } }); - const i18nService = mockI18nService(); - - const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); - - const result = await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - baseUrl: "https://api.example.com", - }); - - expect(result).toEqual(email); - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - }, - ); - - it("throws an invalid token error if the request fails with a 401", async () => { - const apiService = mockApiService(401, {}); - const i18nService = mockI18nService(); - - const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - baseUrl: "https://api.example.com", - }), - ).rejects.toEqual("forwaderInvalidToken"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwaderInvalidToken", - Forwarders.AddyIo.name, - ); - }); - - it("throws an unknown error if the request fails and no status is provided", async () => { - const apiService = mockApiService(500, {}); - const i18nService = mockI18nService(); - - const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - baseUrl: "https://api.example.com", - }), - ).rejects.toEqual("forwarderUnknownError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwarderUnknownError", - Forwarders.AddyIo.name, - ); - }); - - it.each([ - [100, "Continue"], - [202, "Accepted"], - [300, "Multiple Choices"], - [418, "I'm a teapot"], - [500, "Internal Server Error"], - [600, "Unknown Status"], - ])( - "throws an error with the status text if the request returns any other status code (= %i) and a status (= %p) is provided", - async (statusCode, statusText) => { - const apiService = mockApiService(statusCode, {}, statusText); - const i18nService = mockI18nService(); - - const forwarder = new AddyIoForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - baseUrl: "https://api.example.com", - }), - ).rejects.toEqual("forwarderError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwarderError", - Forwarders.AddyIo.name, - statusText, - ); - }, - ); - }); -}); diff --git a/libs/common/src/tools/generator/username/forwarders/addy-io.ts b/libs/common/src/tools/generator/username/forwarders/addy-io.ts deleted file mode 100644 index ecf60da195c..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/addy-io.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; -import { CryptoService } from "../../../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; -import { I18nService } from "../../../../platform/abstractions/i18n.service"; -import { StateProvider } from "../../../../platform/state"; -import { ADDY_IO_FORWARDER, ADDY_IO_BUFFER } from "../../key-definitions"; -import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; -import { Forwarders } from "../options/constants"; -import { EmailDomainOptions, SelfHostedApiOptions } from "../options/forwarder-options"; - -export const DefaultAddyIoOptions: SelfHostedApiOptions & EmailDomainOptions = Object.freeze({ - website: null, - baseUrl: "https://app.addy.io", - token: "", - domain: "", -}); - -/** Generates a forwarding address for addy.io (formerly anon addy) */ -export class AddyIoForwarder extends ForwarderGeneratorStrategy< - SelfHostedApiOptions & EmailDomainOptions -> { - /** Instantiates the forwarder - * @param apiService used for ajax requests to the forwarding service - * @param i18nService used to look up error strings - * @param encryptService protects sensitive forwarder options - * @param keyService looks up the user key when protecting data. - * @param stateProvider creates the durable state for options storage - */ - constructor( - private apiService: ApiService, - private i18nService: I18nService, - encryptService: EncryptService, - keyService: CryptoService, - stateProvider: StateProvider, - ) { - super(encryptService, keyService, stateProvider, DefaultAddyIoOptions); - } - - // configuration - readonly key = ADDY_IO_FORWARDER; - readonly rolloverKey = ADDY_IO_BUFFER; - - // request - generate = async (options: SelfHostedApiOptions & EmailDomainOptions) => { - if (!options.token || options.token === "") { - const error = this.i18nService.t("forwaderInvalidToken", Forwarders.AddyIo.name); - throw error; - } - if (!options.domain || options.domain === "") { - const error = this.i18nService.t("forwarderNoDomain", Forwarders.AddyIo.name); - throw error; - } - if (!options.baseUrl || options.baseUrl === "") { - const error = this.i18nService.t("forwarderNoUrl", Forwarders.AddyIo.name); - throw error; - } - - let descriptionId = "forwarderGeneratedByWithWebsite"; - if (!options.website || options.website === "") { - descriptionId = "forwarderGeneratedBy"; - } - const description = this.i18nService.t(descriptionId, options.website ?? ""); - - const url = options.baseUrl + "/api/v1/aliases"; - const request = new Request(url, { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authorization: "Bearer " + options.token, - "Content-Type": "application/json", - "X-Requested-With": "XMLHttpRequest", - }), - body: JSON.stringify({ - domain: options.domain, - description, - }), - }); - - const response = await this.apiService.nativeFetch(request); - if (response.status === 200 || response.status === 201) { - const json = await response.json(); - return json?.data?.email; - } else if (response.status === 401) { - const error = this.i18nService.t("forwaderInvalidToken", Forwarders.AddyIo.name); - throw error; - } else if (response?.statusText) { - const error = this.i18nService.t( - "forwarderError", - Forwarders.AddyIo.name, - response.statusText, - ); - throw error; - } else { - const error = this.i18nService.t("forwarderUnknownError", Forwarders.AddyIo.name); - throw error; - } - }; -} - -export const DefaultOptions = Object.freeze({ - website: null, - baseUrl: "https://app.addy.io", - domain: "", - token: "", -}); diff --git a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts deleted file mode 100644 index b836ca2bef7..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -/** - * include Request in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ -import { firstValueFrom } from "rxjs"; - -import { UserId } from "../../../../types/guid"; -import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions"; -import { Forwarders } from "../options/constants"; - -import { DuckDuckGoForwarder, DefaultDuckDuckGoOptions } from "./duck-duck-go"; -import { mockApiService, mockI18nService } from "./mocks.jest"; - -const SomeUser = "some user" as UserId; - -describe("DuckDuckGo Forwarder", () => { - it("key returns the Duck Duck Go forwarder key", () => { - const forwarder = new DuckDuckGoForwarder(null, null, null, null, null); - - expect(forwarder.key).toBe(DUCK_DUCK_GO_FORWARDER); - }); - - describe("defaults$", () => { - it("should return the default subaddress options", async () => { - const strategy = new DuckDuckGoForwarder(null, null, null, null, null); - - const result = await firstValueFrom(strategy.defaults$(SomeUser)); - - expect(result).toEqual(DefaultDuckDuckGoOptions); - }); - }); - - describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { - it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new DuckDuckGoForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token, - }), - ).rejects.toEqual("forwaderInvalidToken"); - - expect(apiService.nativeFetch).not.toHaveBeenCalled(); - expect(i18nService.t).toHaveBeenCalledWith( - "forwaderInvalidToken", - Forwarders.DuckDuckGo.name, - ); - }); - - it.each([ - ["jane.doe@duck.com", 201, "jane.doe"], - ["john.doe@duck.com", 201, "john.doe"], - ["jane.doe@duck.com", 200, "jane.doe"], - ["john.doe@duck.com", 200, "john.doe"], - ])( - "returns the generated email address (= %p) if the request is successful (status = %p)", - async (email, status, address) => { - const apiService = mockApiService(status, { address }); - const i18nService = mockI18nService(); - - const forwarder = new DuckDuckGoForwarder(apiService, i18nService, null, null, null); - - const result = await forwarder.generate({ - website: null, - token: "token", - }); - - expect(result).toEqual(email); - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - }, - ); - - it("throws an invalid token error if the request fails with a 401", async () => { - const apiService = mockApiService(401, {}); - const i18nService = mockI18nService(); - - const forwarder = new DuckDuckGoForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - }), - ).rejects.toEqual("forwaderInvalidToken"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenCalledWith( - "forwaderInvalidToken", - Forwarders.DuckDuckGo.name, - ); - }); - - it("throws an unknown error if the request is successful but an address isn't present", async () => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new DuckDuckGoForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - }), - ).rejects.toEqual("forwarderUnknownError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenCalledWith( - "forwarderUnknownError", - Forwarders.DuckDuckGo.name, - ); - }); - - it.each([100, 202, 300, 418, 500, 600])( - "throws an unknown error if the request returns any other status code (= %i)", - async (statusCode) => { - const apiService = mockApiService(statusCode, {}); - const i18nService = mockI18nService(); - - const forwarder = new DuckDuckGoForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - }), - ).rejects.toEqual("forwarderUnknownError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenCalledWith( - "forwarderUnknownError", - Forwarders.DuckDuckGo.name, - ); - }, - ); - }); -}); diff --git a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts deleted file mode 100644 index 492105dfdfd..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; -import { CryptoService } from "../../../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; -import { I18nService } from "../../../../platform/abstractions/i18n.service"; -import { StateProvider } from "../../../../platform/state"; -import { DUCK_DUCK_GO_FORWARDER, DUCK_DUCK_GO_BUFFER } from "../../key-definitions"; -import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; -import { Forwarders } from "../options/constants"; -import { ApiOptions } from "../options/forwarder-options"; - -export const DefaultDuckDuckGoOptions: ApiOptions = Object.freeze({ - website: null, - token: "", -}); - -/** Generates a forwarding address for DuckDuckGo */ -export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy { - /** Instantiates the forwarder - * @param apiService used for ajax requests to the forwarding service - * @param i18nService used to look up error strings - * @param encryptService protects sensitive forwarder options - * @param keyService looks up the user key when protecting data. - * @param stateProvider creates the durable state for options storage - */ - constructor( - private apiService: ApiService, - private i18nService: I18nService, - encryptService: EncryptService, - keyService: CryptoService, - stateProvider: StateProvider, - ) { - super(encryptService, keyService, stateProvider, DefaultDuckDuckGoOptions); - } - - // configuration - readonly key = DUCK_DUCK_GO_FORWARDER; - readonly rolloverKey = DUCK_DUCK_GO_BUFFER; - - // request - generate = async (options: ApiOptions): Promise => { - if (!options.token || options.token === "") { - const error = this.i18nService.t("forwaderInvalidToken", Forwarders.DuckDuckGo.name); - throw error; - } - - const url = "https://quack.duckduckgo.com/api/email/addresses"; - const request = new Request(url, { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authorization: "Bearer " + options.token, - "Content-Type": "application/json", - }), - }); - - const response = await this.apiService.nativeFetch(request); - if (response.status === 200 || response.status === 201) { - const json = await response.json(); - if (json.address) { - return `${json.address}@duck.com`; - } else { - const error = this.i18nService.t("forwarderUnknownError", Forwarders.DuckDuckGo.name); - throw error; - } - } else if (response.status === 401) { - const error = this.i18nService.t("forwaderInvalidToken", Forwarders.DuckDuckGo.name); - throw error; - } else { - const error = this.i18nService.t("forwarderUnknownError", Forwarders.DuckDuckGo.name); - throw error; - } - }; -} - -export const DefaultOptions = Object.freeze({ - website: null, - token: "", -}); diff --git a/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts b/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts deleted file mode 100644 index 895f32f7eeb..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts +++ /dev/null @@ -1,284 +0,0 @@ -/** - * include Request in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ -import { firstValueFrom } from "rxjs"; - -import { ApiService } from "../../../../abstractions/api.service"; -import { UserId } from "../../../../types/guid"; -import { FASTMAIL_FORWARDER } from "../../key-definitions"; -import { Forwarders } from "../options/constants"; - -import { FastmailForwarder, DefaultFastmailOptions } from "./fastmail"; -import { mockI18nService } from "./mocks.jest"; - -const SomeUser = "some user" as UserId; - -type MockResponse = { status: number; body: any }; - -// fastmail calls nativeFetch first to resolve the accountId, -// then it calls nativeFetch again to create the forwarding address. -// The common mock doesn't work here, because this test needs to return multiple responses -function mockApiService(accountId: MockResponse, forwardingAddress: MockResponse) { - function response(r: MockResponse) { - return { - status: r.status, - json: jest.fn().mockImplementation(() => Promise.resolve(r.body)), - }; - } - - return { - nativeFetch: jest - .fn() - .mockImplementationOnce((r: Request) => response(accountId)) - .mockImplementationOnce((r: Request) => response(forwardingAddress)), - } as unknown as ApiService; -} - -const EmptyResponse: MockResponse = Object.freeze({ - status: 200, - body: Object.freeze({}), -}); - -const AccountIdSuccess: MockResponse = Object.freeze({ - status: 200, - body: Object.freeze({ - primaryAccounts: Object.freeze({ - "https://www.fastmail.com/dev/maskedemail": "accountId", - }), - }), -}); - -// the tests -describe("Fastmail Forwarder", () => { - it("key returns the Fastmail forwarder key", () => { - const forwarder = new FastmailForwarder(null, null, null, null, null); - - expect(forwarder.key).toBe(FASTMAIL_FORWARDER); - }); - - describe("defaults$", () => { - it("should return the default subaddress options", async () => { - const strategy = new FastmailForwarder(null, null, null, null, null); - - const result = await firstValueFrom(strategy.defaults$(SomeUser)); - - expect(result).toEqual(DefaultFastmailOptions); - }); - }); - - describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { - it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { - const apiService = mockApiService(AccountIdSuccess, EmptyResponse); - const i18nService = mockI18nService(); - - const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token, - domain: "example.com", - prefix: "prefix", - }), - ).rejects.toEqual("forwaderInvalidToken"); - - expect(apiService.nativeFetch).not.toHaveBeenCalled(); - expect(i18nService.t).toHaveBeenCalledWith("forwaderInvalidToken", Forwarders.Fastmail.name); - }); - - it.each([401, 403])( - "throws a no account id error if the accountId request responds with a status other than 200", - async (status) => { - const apiService = mockApiService({ status, body: {} }, EmptyResponse); - const i18nService = mockI18nService(); - - const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - prefix: "prefix", - }), - ).rejects.toEqual("forwarderNoAccountId"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenCalledWith( - "forwarderNoAccountId", - Forwarders.Fastmail.name, - ); - }, - ); - - it.each([ - ["jane.doe@example.com", 200], - ["john.doe@example.com", 200], - ])( - "returns the generated email address (= %p) if both requests are successful (status = %p)", - async (email, status) => { - const apiService = mockApiService(AccountIdSuccess, { - status, - body: { - methodResponses: [["MaskedEmail/set", { created: { "new-masked-email": { email } } }]], - }, - }); - const i18nService = mockI18nService(); - - const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); - - const result = await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - prefix: "prefix", - }); - - expect(result).toEqual(email); - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - }, - ); - - it.each([ - [ - "It turned inside out!", - [ - "MaskedEmail/set", - { notCreated: { "new-masked-email": { description: "It turned inside out!" } } }, - ], - ], - ["And then it exploded!", ["error", { description: "And then it exploded!" }]], - ])( - "throws a forwarder error (= %p) if both requests are successful (status = %p) but masked email creation fails", - async (description, response) => { - const apiService = mockApiService(AccountIdSuccess, { - status: 200, - body: { - methodResponses: [response], - }, - }); - const i18nService = mockI18nService(); - - const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - prefix: "prefix", - }), - ).rejects.toEqual("forwarderError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenCalledWith( - "forwarderError", - Forwarders.Fastmail.name, - description, - ); - }, - ); - - it.each([401, 403])( - "throws an invalid token error if the jmap request fails with a %i", - async (status) => { - const apiService = mockApiService(AccountIdSuccess, { status, body: {} }); - const i18nService = mockI18nService(); - - const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - prefix: "prefix", - }), - ).rejects.toEqual("forwaderInvalidToken"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenCalledWith( - "forwaderInvalidToken", - Forwarders.Fastmail.name, - ); - }, - ); - - it.each([ - null, - [], - [[]], - [["MaskedEmail/not-a-real-op"]], - [["MaskedEmail/set", null]], - [["MaskedEmail/set", { created: null }]], - [["MaskedEmail/set", { created: { "new-masked-email": null } }]], - [["MaskedEmail/set", { notCreated: null }]], - [["MaskedEmail/set", { notCreated: { "new-masked-email": null } }]], - ])( - "throws an unknown error if the jmap request is malformed (= %p)", - async (responses: any) => { - const apiService = mockApiService(AccountIdSuccess, { - status: 200, - body: { - methodResponses: responses, - }, - }); - const i18nService = mockI18nService(); - - const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - prefix: "prefix", - }), - ).rejects.toEqual("forwarderUnknownError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenCalledWith( - "forwarderUnknownError", - Forwarders.Fastmail.name, - ); - }, - ); - - it.each([100, 202, 300, 418, 500, 600])( - "throws an unknown error if the request returns any other status code (= %i)", - async (statusCode) => { - const apiService = mockApiService(AccountIdSuccess, { status: statusCode, body: {} }); - const i18nService = mockI18nService(); - - const forwarder = new FastmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - prefix: "prefix", - }), - ).rejects.toEqual("forwarderUnknownError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenCalledWith( - "forwarderUnknownError", - Forwarders.Fastmail.name, - ); - }, - ); - }); -}); diff --git a/libs/common/src/tools/generator/username/forwarders/fastmail.ts b/libs/common/src/tools/generator/username/forwarders/fastmail.ts deleted file mode 100644 index 0c4e0e2cfd2..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/fastmail.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; -import { CryptoService } from "../../../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; -import { I18nService } from "../../../../platform/abstractions/i18n.service"; -import { StateProvider } from "../../../../platform/state"; -import { FASTMAIL_FORWARDER, FASTMAIL_BUFFER } from "../../key-definitions"; -import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; -import { Forwarders } from "../options/constants"; -import { EmailPrefixOptions, ApiOptions } from "../options/forwarder-options"; - -export const DefaultFastmailOptions: ApiOptions & EmailPrefixOptions = Object.freeze({ - website: "", - domain: "", - prefix: "", - token: "", -}); - -/** Generates a forwarding address for Fastmail */ -export class FastmailForwarder extends ForwarderGeneratorStrategy { - /** Instantiates the forwarder - * @param apiService used for ajax requests to the forwarding service - * @param i18nService used to look up error strings - * @param encryptService protects sensitive forwarder options - * @param keyService looks up the user key when protecting data. - * @param stateProvider creates the durable state for options storage - */ - constructor( - private apiService: ApiService, - private i18nService: I18nService, - encryptService: EncryptService, - keyService: CryptoService, - stateProvider: StateProvider, - ) { - super(encryptService, keyService, stateProvider, DefaultFastmailOptions); - } - - // configuration - readonly key = FASTMAIL_FORWARDER; - readonly rolloverKey = FASTMAIL_BUFFER; - - // request - generate = async (options: ApiOptions & EmailPrefixOptions) => { - if (!options.token || options.token === "") { - const error = this.i18nService.t("forwaderInvalidToken", Forwarders.Fastmail.name); - throw error; - } - - const accountId = await this.getAccountId(options); - if (!accountId || accountId === "") { - const error = this.i18nService.t("forwarderNoAccountId", Forwarders.Fastmail.name); - throw error; - } - - const body = JSON.stringify({ - using: ["https://www.fastmail.com/dev/maskedemail", "urn:ietf:params:jmap:core"], - methodCalls: [ - [ - "MaskedEmail/set", - { - accountId: accountId, - create: { - "new-masked-email": { - state: "enabled", - description: "", - forDomain: options.website ?? "", - emailPrefix: options.prefix, - }, - }, - }, - "0", - ], - ], - }); - - const requestInit: RequestInit = { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authorization: "Bearer " + options.token, - "Content-Type": "application/json", - }), - body, - }; - - const url = "https://api.fastmail.com/jmap/api/"; - const request = new Request(url, requestInit); - - const response = await this.apiService.nativeFetch(request); - if (response.status === 200) { - const json = await response.json(); - if ( - json.methodResponses != null && - json.methodResponses.length > 0 && - json.methodResponses[0].length > 0 - ) { - if (json.methodResponses[0][0] === "MaskedEmail/set") { - if (json.methodResponses[0][1]?.created?.["new-masked-email"] != null) { - return json.methodResponses[0][1]?.created?.["new-masked-email"]?.email; - } - if (json.methodResponses[0][1]?.notCreated?.["new-masked-email"] != null) { - const errorDescription = - json.methodResponses[0][1]?.notCreated?.["new-masked-email"]?.description; - const error = this.i18nService.t( - "forwarderError", - Forwarders.Fastmail.name, - errorDescription, - ); - throw error; - } - } else if (json.methodResponses[0][0] === "error") { - const errorDescription = json.methodResponses[0][1]?.description; - const error = this.i18nService.t( - "forwarderError", - Forwarders.Fastmail.name, - errorDescription, - ); - throw error; - } - } - } else if (response.status === 401 || response.status === 403) { - const error = this.i18nService.t("forwaderInvalidToken", Forwarders.Fastmail.name); - throw error; - } - - const error = this.i18nService.t("forwarderUnknownError", Forwarders.Fastmail.name); - throw error; - }; - - private async getAccountId(options: ApiOptions): Promise { - const requestInit: RequestInit = { - cache: "no-store", - method: "GET", - headers: new Headers({ - Authorization: "Bearer " + options.token, - }), - }; - const url = "https://api.fastmail.com/.well-known/jmap"; - const request = new Request(url, requestInit); - const response = await this.apiService.nativeFetch(request); - if (response.status === 200) { - const json = await response.json(); - if (json.primaryAccounts != null) { - return json.primaryAccounts["https://www.fastmail.com/dev/maskedemail"]; - } - } - return null; - } -} - -export const DefaultOptions = Object.freeze({ - website: null, - domain: "", - prefix: "", - token: "", -}); diff --git a/libs/common/src/tools/generator/username/forwarders/firefox-relay.spec.ts b/libs/common/src/tools/generator/username/forwarders/firefox-relay.spec.ts deleted file mode 100644 index 7d712f73322..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/firefox-relay.spec.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * include Request in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ -import { firstValueFrom } from "rxjs"; - -import { UserId } from "../../../../types/guid"; -import { FIREFOX_RELAY_FORWARDER } from "../../key-definitions"; -import { Forwarders } from "../options/constants"; - -import { FirefoxRelayForwarder, DefaultFirefoxRelayOptions } from "./firefox-relay"; -import { mockApiService, mockI18nService } from "./mocks.jest"; - -const SomeUser = "some user" as UserId; - -describe("Firefox Relay Forwarder", () => { - it("key returns the Firefox Relay forwarder key", () => { - const forwarder = new FirefoxRelayForwarder(null, null, null, null, null); - - expect(forwarder.key).toBe(FIREFOX_RELAY_FORWARDER); - }); - - describe("defaults$", () => { - it("should return the default subaddress options", async () => { - const strategy = new FirefoxRelayForwarder(null, null, null, null, null); - - const result = await firstValueFrom(strategy.defaults$(SomeUser)); - - expect(result).toEqual(DefaultFirefoxRelayOptions); - }); - }); - - describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { - it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new FirefoxRelayForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token, - }), - ).rejects.toEqual("forwaderInvalidToken"); - - expect(apiService.nativeFetch).not.toHaveBeenCalled(); - expect(i18nService.t).toHaveBeenCalledWith( - "forwaderInvalidToken", - Forwarders.FirefoxRelay.name, - ); - }); - - it.each([ - ["forwarderGeneratedByWithWebsite", "provided", "bitwarden.com", "bitwarden.com"], - ["forwarderGeneratedByWithWebsite", "provided", "httpbin.org", "httpbin.org"], - ["forwarderGeneratedBy", "not provided", null, ""], - ["forwarderGeneratedBy", "not provided", "", ""], - ])( - "describes the website with %p when the website is %s (= %p)", - async (translationKey, _ignored, website, expectedWebsite) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new FirefoxRelayForwarder(apiService, i18nService, null, null, null); - - await forwarder.generate({ - website, - token: "token", - }); - - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenCalledWith(translationKey, expectedWebsite); - }, - ); - - it.each([ - ["jane.doe@duck.com", 201], - ["john.doe@duck.com", 201], - ["jane.doe@duck.com", 200], - ["john.doe@duck.com", 200], - ])( - "returns the generated email address (= %p) if the request is successful (status = %p)", - async (full_address, status) => { - const apiService = mockApiService(status, { full_address }); - const i18nService = mockI18nService(); - - const forwarder = new FirefoxRelayForwarder(apiService, i18nService, null, null, null); - - const result = await forwarder.generate({ - website: null, - token: "token", - }); - - expect(result).toEqual(full_address); - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - }, - ); - - it("throws an invalid token error if the request fails with a 401", async () => { - const apiService = mockApiService(401, {}); - const i18nService = mockI18nService(); - - const forwarder = new FirefoxRelayForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - }), - ).rejects.toEqual("forwaderInvalidToken"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwaderInvalidToken", - Forwarders.FirefoxRelay.name, - ); - }); - - it.each([100, 202, 300, 418, 500, 600])( - "throws an unknown error if the request returns any other status code (= %i)", - async (statusCode) => { - const apiService = mockApiService(statusCode, {}); - const i18nService = mockI18nService(); - - const forwarder = new FirefoxRelayForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - }), - ).rejects.toEqual("forwarderUnknownError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwarderUnknownError", - Forwarders.FirefoxRelay.name, - ); - }, - ); - }); -}); diff --git a/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts b/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts deleted file mode 100644 index 1beb2dde4f8..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; -import { CryptoService } from "../../../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; -import { I18nService } from "../../../../platform/abstractions/i18n.service"; -import { StateProvider } from "../../../../platform/state"; -import { FIREFOX_RELAY_FORWARDER, FIREFOX_RELAY_BUFFER } from "../../key-definitions"; -import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; -import { Forwarders } from "../options/constants"; -import { ApiOptions } from "../options/forwarder-options"; - -export const DefaultFirefoxRelayOptions: ApiOptions = Object.freeze({ - website: null, - token: "", -}); - -/** Generates a forwarding address for Firefox Relay */ -export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy { - /** Instantiates the forwarder - * @param apiService used for ajax requests to the forwarding service - * @param i18nService used to look up error strings - * @param encryptService protects sensitive forwarder options - * @param keyService looks up the user key when protecting data. - * @param stateProvider creates the durable state for options storage - */ - constructor( - private apiService: ApiService, - private i18nService: I18nService, - encryptService: EncryptService, - keyService: CryptoService, - stateProvider: StateProvider, - ) { - super(encryptService, keyService, stateProvider, DefaultFirefoxRelayOptions); - } - - // configuration - readonly key = FIREFOX_RELAY_FORWARDER; - readonly rolloverKey = FIREFOX_RELAY_BUFFER; - - // request - generate = async (options: ApiOptions) => { - if (!options.token || options.token === "") { - const error = this.i18nService.t("forwaderInvalidToken", Forwarders.FirefoxRelay.name); - throw error; - } - - const url = "https://relay.firefox.com/api/v1/relayaddresses/"; - - let descriptionId = "forwarderGeneratedByWithWebsite"; - if (!options.website || options.website === "") { - descriptionId = "forwarderGeneratedBy"; - } - const description = this.i18nService.t(descriptionId, options.website ?? ""); - - const request = new Request(url, { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authorization: "Token " + options.token, - "Content-Type": "application/json", - }), - body: JSON.stringify({ - enabled: true, - generated_for: options.website, - description, - }), - }); - - const response = await this.apiService.nativeFetch(request); - if (response.status === 401) { - const error = this.i18nService.t("forwaderInvalidToken", Forwarders.FirefoxRelay.name); - throw error; - } else if (response.status === 200 || response.status === 201) { - const json = await response.json(); - return json.full_address; - } else { - const error = this.i18nService.t("forwarderUnknownError", Forwarders.FirefoxRelay.name); - throw error; - } - }; -} - -export const DefaultOptions = Object.freeze({ - website: null, - token: "", -}); diff --git a/libs/common/src/tools/generator/username/forwarders/forward-email.spec.ts b/libs/common/src/tools/generator/username/forwarders/forward-email.spec.ts deleted file mode 100644 index 23c4bef64ab..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/forward-email.spec.ts +++ /dev/null @@ -1,280 +0,0 @@ -/** - * include Request in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ -import { firstValueFrom } from "rxjs"; - -import { UserId } from "../../../../types/guid"; -import { FORWARD_EMAIL_FORWARDER } from "../../key-definitions"; -import { Forwarders } from "../options/constants"; - -import { ForwardEmailForwarder, DefaultForwardEmailOptions } from "./forward-email"; -import { mockApiService, mockI18nService } from "./mocks.jest"; - -const SomeUser = "some user" as UserId; - -describe("ForwardEmail Forwarder", () => { - it("key returns the Forward Email forwarder key", () => { - const forwarder = new ForwardEmailForwarder(null, null, null, null, null); - - expect(forwarder.key).toBe(FORWARD_EMAIL_FORWARDER); - }); - - describe("defaults$", () => { - it("should return the default subaddress options", async () => { - const strategy = new ForwardEmailForwarder(null, null, null, null, null); - - const result = await firstValueFrom(strategy.defaults$(SomeUser)); - - expect(result).toEqual(DefaultForwardEmailOptions); - }); - }); - - describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { - it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token, - domain: "example.com", - }), - ).rejects.toEqual("forwaderInvalidToken"); - - expect(apiService.nativeFetch).not.toHaveBeenCalled(); - expect(i18nService.t).toHaveBeenCalledWith( - "forwaderInvalidToken", - Forwarders.ForwardEmail.name, - ); - }); - - it.each([null, ""])( - "throws an error if the domain is missing (domain = %p)", - async (domain) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain, - }), - ).rejects.toEqual("forwarderNoDomain"); - - expect(apiService.nativeFetch).not.toHaveBeenCalled(); - expect(i18nService.t).toHaveBeenCalledWith( - "forwarderNoDomain", - Forwarders.ForwardEmail.name, - ); - }, - ); - - it.each([ - ["forwarderGeneratedByWithWebsite", "provided", "bitwarden.com", "bitwarden.com"], - ["forwarderGeneratedByWithWebsite", "provided", "httpbin.org", "httpbin.org"], - ["forwarderGeneratedBy", "not provided", null, ""], - ["forwarderGeneratedBy", "not provided", "", ""], - ])( - "describes the website with %p when the website is %s (= %p)", - async (translationKey, _ignored, website, expectedWebsite) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); - - await forwarder.generate({ - website, - token: "token", - domain: "example.com", - }); - - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenCalledWith(translationKey, expectedWebsite); - }, - ); - - it.each([ - ["jane.doe@example.com", 201, { name: "jane.doe", domain: { name: "example.com" } }], - ["jane.doe@example.com", 201, { name: "jane.doe" }], - ["john.doe@example.com", 201, { name: "john.doe", domain: { name: "example.com" } }], - ["john.doe@example.com", 201, { name: "john.doe" }], - ["jane.doe@example.com", 200, { name: "jane.doe", domain: { name: "example.com" } }], - ["jane.doe@example.com", 200, { name: "jane.doe" }], - ["john.doe@example.com", 200, { name: "john.doe", domain: { name: "example.com" } }], - ["john.doe@example.com", 200, { name: "john.doe" }], - ])( - "returns the generated email address (= %p) if the request is successful (status = %p)", - async (email, status, response) => { - const apiService = mockApiService(status, response); - const i18nService = mockI18nService(); - - const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); - - const result = await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - }); - - expect(result).toEqual(email); - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - }, - ); - - it("throws an invalid token error if the request fails with a 401", async () => { - const apiService = mockApiService(401, {}); - const i18nService = mockI18nService(); - - const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - }), - ).rejects.toEqual("forwaderInvalidToken"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwaderInvalidToken", - Forwarders.ForwardEmail.name, - undefined, - ); - }); - - it("throws an invalid token error with a message if the request fails with a 401 and message", async () => { - const apiService = mockApiService(401, { message: "A message" }); - const i18nService = mockI18nService(); - - const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - }), - ).rejects.toEqual("forwaderInvalidTokenWithMessage"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwaderInvalidTokenWithMessage", - Forwarders.ForwardEmail.name, - "A message", - ); - }); - - it.each([{}, null])( - "throws an unknown error if the request fails and no status (= %p) is provided", - async (json) => { - const apiService = mockApiService(500, json); - const i18nService = mockI18nService(); - - const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - }), - ).rejects.toEqual("forwarderUnknownError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwarderUnknownError", - Forwarders.ForwardEmail.name, - ); - }, - ); - - it.each([ - [100, "Continue"], - [202, "Accepted"], - [300, "Multiple Choices"], - [418, "I'm a teapot"], - [500, "Internal Server Error"], - [600, "Unknown Status"], - ])( - "throws an error with the status text if the request returns any other status code (= %i) and a status (= %p) is provided", - async (statusCode, message) => { - const apiService = mockApiService(statusCode, { message }); - const i18nService = mockI18nService(); - - const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - }), - ).rejects.toEqual("forwarderError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwarderError", - Forwarders.ForwardEmail.name, - message, - ); - }, - ); - - it.each([ - [100, "Continue"], - [202, "Accepted"], - [300, "Multiple Choices"], - [418, "I'm a teapot"], - [500, "Internal Server Error"], - [600, "Unknown Status"], - ])( - "throws an error with the status text if the request returns any other status code (= %i) and a status (= %p) is provided", - async (statusCode, error) => { - const apiService = mockApiService(statusCode, { error }); - const i18nService = mockI18nService(); - - const forwarder = new ForwardEmailForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - domain: "example.com", - }), - ).rejects.toEqual("forwarderError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwarderError", - Forwarders.ForwardEmail.name, - error, - ); - }, - ); - }); -}); diff --git a/libs/common/src/tools/generator/username/forwarders/forward-email.ts b/libs/common/src/tools/generator/username/forwarders/forward-email.ts deleted file mode 100644 index 20dfe012915..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/forward-email.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; -import { CryptoService } from "../../../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; -import { I18nService } from "../../../../platform/abstractions/i18n.service"; -import { Utils } from "../../../../platform/misc/utils"; -import { StateProvider } from "../../../../platform/state"; -import { FORWARD_EMAIL_FORWARDER, FORWARD_EMAIL_BUFFER } from "../../key-definitions"; -import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; -import { Forwarders } from "../options/constants"; -import { EmailDomainOptions, ApiOptions } from "../options/forwarder-options"; - -export const DefaultForwardEmailOptions: ApiOptions & EmailDomainOptions = Object.freeze({ - website: null, - token: "", - domain: "", -}); - -/** Generates a forwarding address for Forward Email */ -export class ForwardEmailForwarder extends ForwarderGeneratorStrategy< - ApiOptions & EmailDomainOptions -> { - /** Instantiates the forwarder - * @param apiService used for ajax requests to the forwarding service - * @param i18nService used to look up error strings - * @param encryptService protects sensitive forwarder options - * @param keyService looks up the user key when protecting data. - * @param stateProvider creates the durable state for options storage - */ - constructor( - private apiService: ApiService, - private i18nService: I18nService, - encryptService: EncryptService, - keyService: CryptoService, - stateProvider: StateProvider, - ) { - super(encryptService, keyService, stateProvider, DefaultForwardEmailOptions); - } - - // configuration - readonly key = FORWARD_EMAIL_FORWARDER; - readonly rolloverKey = FORWARD_EMAIL_BUFFER; - - // request - generate = async (options: ApiOptions & EmailDomainOptions) => { - if (!options.token || options.token === "") { - const error = this.i18nService.t("forwaderInvalidToken", Forwarders.ForwardEmail.name); - throw error; - } - if (!options.domain || options.domain === "") { - const error = this.i18nService.t("forwarderNoDomain", Forwarders.ForwardEmail.name); - throw error; - } - - const url = `https://api.forwardemail.net/v1/domains/${options.domain}/aliases`; - - let descriptionId = "forwarderGeneratedByWithWebsite"; - if (!options.website || options.website === "") { - descriptionId = "forwarderGeneratedBy"; - } - const description = this.i18nService.t(descriptionId, options.website ?? ""); - - const request = new Request(url, { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authorization: "Basic " + Utils.fromUtf8ToB64(options.token + ":"), - "Content-Type": "application/json", - }), - body: JSON.stringify({ - labels: options.website, - description, - }), - }); - - const response = await this.apiService.nativeFetch(request); - const json = await response.json(); - - if (response.status === 401) { - const messageKey = - "message" in json ? "forwaderInvalidTokenWithMessage" : "forwaderInvalidToken"; - const error = this.i18nService.t(messageKey, Forwarders.ForwardEmail.name, json.message); - throw error; - } else if (response.status === 200 || response.status === 201) { - const { name, domain } = await response.json(); - const domainPart = domain?.name || options.domain; - return `${name}@${domainPart}`; - } else if (json?.message) { - const error = this.i18nService.t( - "forwarderError", - Forwarders.ForwardEmail.name, - json.message, - ); - throw error; - } else if (json?.error) { - const error = this.i18nService.t("forwarderError", Forwarders.ForwardEmail.name, json.error); - throw error; - } else { - const error = this.i18nService.t("forwarderUnknownError", Forwarders.ForwardEmail.name); - throw error; - } - }; -} - -export const DefaultOptions = Object.freeze({ - website: null, - token: "", - domain: "", -}); diff --git a/libs/common/src/tools/generator/username/forwarders/mocks.jest.ts b/libs/common/src/tools/generator/username/forwarders/mocks.jest.ts deleted file mode 100644 index 768014a77d7..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/mocks.jest.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; -import { I18nService } from "../../../../platform/abstractions/i18n.service"; - -/** a mock {@link ApiService} that returns a fetch-like response with a given status and body */ -export function mockApiService(status: number, body: any, statusText?: string) { - return { - nativeFetch: jest.fn().mockImplementation((r: Request) => { - return { - status, - statusText, - json: jest.fn().mockImplementation(() => Promise.resolve(body)), - }; - }), - } as unknown as ApiService; -} - -/** a mock {@link I18nService} that returns the translation key */ -export function mockI18nService() { - return { - t: jest.fn().mockImplementation((key: string) => key), - } as unknown as I18nService; -} diff --git a/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts b/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts deleted file mode 100644 index c53e7832706..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * include Request in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ -import { firstValueFrom } from "rxjs"; - -import { UserId } from "../../../../types/guid"; -import { SIMPLE_LOGIN_FORWARDER } from "../../key-definitions"; -import { Forwarders } from "../options/constants"; - -import { mockApiService, mockI18nService } from "./mocks.jest"; -import { SimpleLoginForwarder, DefaultSimpleLoginOptions } from "./simple-login"; - -const SomeUser = "some user" as UserId; - -describe("SimpleLogin Forwarder", () => { - it("key returns the Simple Login forwarder key", () => { - const forwarder = new SimpleLoginForwarder(null, null, null, null, null); - - expect(forwarder.key).toBe(SIMPLE_LOGIN_FORWARDER); - }); - - describe("defaults$", () => { - it("should return the default subaddress options", async () => { - const strategy = new SimpleLoginForwarder(null, null, null, null, null); - - const result = await firstValueFrom(strategy.defaults$(SomeUser)); - - expect(result).toEqual(DefaultSimpleLoginOptions); - }); - }); - - describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { - it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token, - baseUrl: "https://api.example.com", - }), - ).rejects.toEqual("forwaderInvalidToken"); - - expect(apiService.nativeFetch).not.toHaveBeenCalled(); - expect(i18nService.t).toHaveBeenCalledWith( - "forwaderInvalidToken", - Forwarders.SimpleLogin.name, - ); - }); - - it.each([null, ""])( - "throws an error if the baseUrl is missing (baseUrl = %p)", - async (baseUrl) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - baseUrl, - }), - ).rejects.toEqual("forwarderNoUrl"); - - expect(apiService.nativeFetch).not.toHaveBeenCalled(); - expect(i18nService.t).toHaveBeenCalledWith("forwarderNoUrl", Forwarders.SimpleLogin.name); - }, - ); - - it.each([ - ["forwarderGeneratedByWithWebsite", "provided", "bitwarden.com", "bitwarden.com"], - ["forwarderGeneratedByWithWebsite", "provided", "httpbin.org", "httpbin.org"], - ["forwarderGeneratedBy", "not provided", null, ""], - ["forwarderGeneratedBy", "not provided", "", ""], - ])( - "describes the website with %p when the website is %s (= %p)", - async (translationKey, _ignored, website, expectedWebsite) => { - const apiService = mockApiService(200, {}); - const i18nService = mockI18nService(); - - const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); - - await forwarder.generate({ - website, - token: "token", - baseUrl: "https://api.example.com", - }); - - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenCalledWith(translationKey, expectedWebsite); - }, - ); - - it.each([ - ["jane.doe@example.com", 201], - ["john.doe@example.com", 201], - ["jane.doe@example.com", 200], - ["john.doe@example.com", 200], - ])( - "returns the generated email address (= %p) if the request is successful (status = %p)", - async (alias, status) => { - const apiService = mockApiService(status, { alias }); - const i18nService = mockI18nService(); - - const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); - - const result = await forwarder.generate({ - website: null, - token: "token", - baseUrl: "https://api.example.com", - }); - - expect(result).toEqual(alias); - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - }, - ); - - it("throws an invalid token error if the request fails with a 401", async () => { - const apiService = mockApiService(401, {}); - const i18nService = mockI18nService(); - - const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - baseUrl: "https://api.example.com", - }), - ).rejects.toEqual("forwaderInvalidToken"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwaderInvalidToken", - Forwarders.SimpleLogin.name, - ); - }); - - it.each([{}, null])( - "throws an unknown error if the request fails and no status (=%p) is provided", - async (body) => { - const apiService = mockApiService(500, body); - const i18nService = mockI18nService(); - - const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - baseUrl: "https://api.example.com", - }), - ).rejects.toEqual("forwarderUnknownError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwarderUnknownError", - Forwarders.SimpleLogin.name, - ); - }, - ); - - it.each([ - [100, "Continue"], - [202, "Accepted"], - [300, "Multiple Choices"], - [418, "I'm a teapot"], - [500, "Internal Server Error"], - [600, "Unknown Status"], - ])( - "throws an error with the status text if the request returns any other status code (= %i) and a status (= %p) is provided", - async (statusCode, error) => { - const apiService = mockApiService(statusCode, { error }); - const i18nService = mockI18nService(); - - const forwarder = new SimpleLoginForwarder(apiService, i18nService, null, null, null); - - await expect( - async () => - await forwarder.generate({ - website: null, - token: "token", - baseUrl: "https://api.example.com", - }), - ).rejects.toEqual("forwarderError"); - - expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); - // counting instances is terribly flaky over changes, but jest doesn't have a better way to do this - expect(i18nService.t).toHaveBeenNthCalledWith( - 2, - "forwarderError", - Forwarders.SimpleLogin.name, - error, - ); - }, - ); - }); -}); diff --git a/libs/common/src/tools/generator/username/forwarders/simple-login.ts b/libs/common/src/tools/generator/username/forwarders/simple-login.ts deleted file mode 100644 index 593c7346419..00000000000 --- a/libs/common/src/tools/generator/username/forwarders/simple-login.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { ApiService } from "../../../../abstractions/api.service"; -import { CryptoService } from "../../../../platform/abstractions/crypto.service"; -import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; -import { I18nService } from "../../../../platform/abstractions/i18n.service"; -import { StateProvider } from "../../../../platform/state"; -import { SIMPLE_LOGIN_FORWARDER, SIMPLE_LOGIN_BUFFER } from "../../key-definitions"; -import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; -import { Forwarders } from "../options/constants"; -import { SelfHostedApiOptions } from "../options/forwarder-options"; - -export const DefaultSimpleLoginOptions: SelfHostedApiOptions = Object.freeze({ - website: null, - baseUrl: "https://app.simplelogin.io", - token: "", -}); - -/** Generates a forwarding address for Simple Login */ -export class SimpleLoginForwarder extends ForwarderGeneratorStrategy { - /** Instantiates the forwarder - * @param apiService used for ajax requests to the forwarding service - * @param i18nService used to look up error strings - * @param encryptService protects sensitive forwarder options - * @param keyService looks up the user key when protecting data. - * @param stateProvider creates the durable state for options storage - */ - constructor( - private apiService: ApiService, - private i18nService: I18nService, - encryptService: EncryptService, - keyService: CryptoService, - stateProvider: StateProvider, - ) { - super(encryptService, keyService, stateProvider, DefaultSimpleLoginOptions); - } - - // configuration - readonly key = SIMPLE_LOGIN_FORWARDER; - readonly rolloverKey = SIMPLE_LOGIN_BUFFER; - - // request - generate = async (options: SelfHostedApiOptions) => { - if (!options.token || options.token === "") { - const error = this.i18nService.t("forwaderInvalidToken", Forwarders.SimpleLogin.name); - throw error; - } - if (!options.baseUrl || options.baseUrl === "") { - const error = this.i18nService.t("forwarderNoUrl", Forwarders.SimpleLogin.name); - throw error; - } - - let url = options.baseUrl + "/api/alias/random/new"; - let noteId = "forwarderGeneratedBy"; - if (options.website && options.website !== "") { - url += "?hostname=" + options.website; - noteId = "forwarderGeneratedByWithWebsite"; - } - const note = this.i18nService.t(noteId, options.website ?? ""); - - const request = new Request(url, { - redirect: "manual", - cache: "no-store", - method: "POST", - headers: new Headers({ - Authentication: options.token, - "Content-Type": "application/json", - }), - body: JSON.stringify({ note }), - }); - - const response = await this.apiService.nativeFetch(request); - if (response.status === 401) { - const error = this.i18nService.t("forwaderInvalidToken", Forwarders.SimpleLogin.name); - throw error; - } - - const json = await response.json(); - if (response.status === 200 || response.status === 201) { - return json.alias; - } else if (json?.error) { - const error = this.i18nService.t("forwarderError", Forwarders.SimpleLogin.name, json.error); - throw error; - } else { - const error = this.i18nService.t("forwarderUnknownError", Forwarders.SimpleLogin.name); - throw error; - } - }; -} - -export const DefaultOptions = Object.freeze({ - website: null, - baseUrl: "https://app.simplelogin.io", - token: "", -}); diff --git a/libs/common/src/tools/generator/username/index.ts b/libs/common/src/tools/generator/username/index.ts deleted file mode 100644 index a9d8e676086..00000000000 --- a/libs/common/src/tools/generator/username/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -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 "../abstractions/username-generation.service.abstraction"; diff --git a/libs/common/src/tools/generator/username/options/constants.ts b/libs/common/src/tools/generator/username/options/constants.ts deleted file mode 100644 index ab584effd58..00000000000 --- a/libs/common/src/tools/generator/username/options/constants.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ForwarderMetadata } from "./forwarder-options"; - -/** Metadata about an email forwarding service. - * @remarks This is used to populate the forwarder selection list - * and to identify forwarding services in error messages. - */ -export const Forwarders = Object.freeze({ - /** For https://addy.io/ */ - AddyIo: Object.freeze({ - id: "anonaddy", - name: "Addy.io", - validForSelfHosted: true, - } as ForwarderMetadata), - - /** For https://duckduckgo.com/email/ */ - DuckDuckGo: Object.freeze({ - id: "duckduckgo", - name: "DuckDuckGo", - validForSelfHosted: false, - } as ForwarderMetadata), - - /** For https://www.fastmail.com. */ - Fastmail: Object.freeze({ - id: "fastmail", - name: "Fastmail", - validForSelfHosted: true, - } as ForwarderMetadata), - - /** For https://relay.firefox.com/ */ - FirefoxRelay: Object.freeze({ - id: "firefoxrelay", - name: "Firefox Relay", - validForSelfHosted: false, - } as ForwarderMetadata), - - /** For https://forwardemail.net/ */ - ForwardEmail: Object.freeze({ - id: "forwardemail", - name: "Forward Email", - validForSelfHosted: true, - } as ForwarderMetadata), - - /** For https://simplelogin.io/ */ - SimpleLogin: Object.freeze({ - id: "simplelogin", - name: "SimpleLogin", - validForSelfHosted: true, - } as ForwarderMetadata), -}); diff --git a/libs/common/src/tools/generator/username/options/forwarder-options.ts b/libs/common/src/tools/generator/username/options/forwarder-options.ts deleted file mode 100644 index f36a58a0db4..00000000000 --- a/libs/common/src/tools/generator/username/options/forwarder-options.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** Identifiers for email forwarding services. - * @remarks These are used to select forwarder-specific options. - * The must be kept in sync with the forwarder implementations. - */ -export type ForwarderId = - | "anonaddy" - | "duckduckgo" - | "fastmail" - | "firefoxrelay" - | "forwardemail" - | "simplelogin"; - -/** Metadata format for email forwarding services. */ -export type ForwarderMetadata = { - /** The unique identifier for the forwarder. */ - id: ForwarderId; - - /** The name of the service the forwarder queries. */ - name: string; - - /** Whether the forwarder is valid for self-hosted instances of Bitwarden. */ - validForSelfHosted: boolean; -}; - -/** Options common to all forwarder APIs */ -export type ApiOptions = { - /** bearer token that authenticates bitwarden to the forwarder. - * This is required to issue an API request. - */ - token?: string; -} & RequestOptions; - -/** Options that provide contextual information about the application state - * when a forwarder is invoked. - * @remarks these fields should always be omitted when saving options. - */ -export type RequestOptions = { - /** @param website The domain of the website the generated email is used - * within. This should be set to `null` when the request is not specific - * to any website. - */ - website: string | null; -}; - -/** Api configuration for forwarders that support self-hosted installations. */ -export type SelfHostedApiOptions = ApiOptions & { - /** The base URL of the forwarder's API. - * When this is empty, the forwarder's default production API is used. - */ - baseUrl: string; -}; - -/** Api configuration for forwarders that support custom domains. */ -export type EmailDomainOptions = { - /** The domain part of the generated email address. - * @remarks The domain should be authorized by the forwarder before - * submitting a request through bitwarden. - * @example If the domain is `domain.io` and the generated username - * is `jd`, then the generated email address will be `jd@mydomain.io` - */ - domain: string; -}; - -/** Api configuration for forwarders that support custom email parts. */ -export type EmailPrefixOptions = EmailDomainOptions & { - /** A prefix joined to the generated email address' username. - * @example If the prefix is `foo`, the generated username is `bar`, - * and the domain is `domain.io`, then the generated email address is ` - * then the generated username is `foobar@domain.io`. - */ - prefix: string; -}; diff --git a/libs/common/src/tools/generator/username/options/generator-options.ts b/libs/common/src/tools/generator/username/options/generator-options.ts deleted file mode 100644 index 3df5709ed32..00000000000 --- a/libs/common/src/tools/generator/username/options/generator-options.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** ways you can generate usernames - * "word" generates a username from the eff word list - * "subaddress" creates a subaddress of an email. - * "catchall" uses a domain's catchall address - * "forwarded" uses an email forwarding service - */ -export type UsernameGeneratorType = "word" | "subaddress" | "catchall" | "forwarded"; - -/** Several username generators support two generation modes - * "random" selects one or more random words from the EFF word list - * "website-name" includes the domain in the generated username - */ -export type UsernameGenerationMode = "random" | "website-name"; diff --git a/libs/common/src/tools/generator/username/options/index.ts b/libs/common/src/tools/generator/username/options/index.ts deleted file mode 100644 index b2d4066c871..00000000000 --- a/libs/common/src/tools/generator/username/options/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ForwarderId, ForwarderMetadata } from "./forwarder-options"; diff --git a/libs/common/src/tools/generator/username/subaddress-generator-options.ts b/libs/common/src/tools/generator/username/subaddress-generator-options.ts deleted file mode 100644 index dc38b2a6ea0..00000000000 --- a/libs/common/src/tools/generator/username/subaddress-generator-options.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { RequestOptions } from "./options/forwarder-options"; -import { UsernameGenerationMode } from "./options/generator-options"; - -/** Settings supported when generating an email subaddress */ -export type SubaddressGenerationOptions = { - /** selects the generation algorithm for the catchall email address. */ - subaddressType?: UsernameGenerationMode; - - /** the email address the subaddress is applied to. */ - subaddressEmail?: string; -} & RequestOptions; - -/** The default options for email subaddress generation. */ -export const DefaultSubaddressOptions: SubaddressGenerationOptions = Object.freeze({ - subaddressType: "random", - subaddressEmail: "", - website: null, -}); 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 deleted file mode 100644 index ba1d5aa2b8f..00000000000 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { mock } from "jest-mock-extended"; -import { of, firstValueFrom } from "rxjs"; - -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 { Randomizer } from "../abstractions/randomizer"; -import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; -import { SUBADDRESS_SETTINGS } from "../key-definitions"; - -import { DefaultSubaddressOptions } from "./subaddress-generator-options"; - -import { SubaddressGeneratorStrategy } from "."; - -const SomeUser = "some user" as UserId; -const SomePolicy = mock({ - type: PolicyType.PasswordGenerator, - data: { - minLength: 10, - }, -}); - -describe("Email subaddress list generation strategy", () => { - describe("toEvaluator()", () => { - it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])( - "should map any input (= %p) to the default policy evaluator", - async (policies) => { - const strategy = new SubaddressGeneratorStrategy(null, null); - - const evaluator$ = of(policies).pipe(strategy.toEvaluator()); - const evaluator = await firstValueFrom(evaluator$); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - }, - ); - }); - - describe("durableState", () => { - it("should use password settings key", () => { - const provider = mock(); - const randomizer = mock(); - const strategy = new SubaddressGeneratorStrategy(randomizer, provider); - - strategy.durableState(SomeUser); - - expect(provider.getUser).toHaveBeenCalledWith(SomeUser, SUBADDRESS_SETTINGS); - }); - }); - - describe("defaults$", () => { - it("should return the default subaddress options", async () => { - const strategy = new SubaddressGeneratorStrategy(null, null); - - const result = await firstValueFrom(strategy.defaults$(SomeUser)); - - expect(result).toEqual(DefaultSubaddressOptions); - }); - }); - - describe("policy", () => { - it("should use password generator policy", () => { - const randomizer = mock(); - const strategy = new SubaddressGeneratorStrategy(randomizer, null); - - expect(strategy.policy).toBe(PolicyType.PasswordGenerator); - }); - }); - - describe("generate()", () => { - it.todo("generate email subaddress tests"); - }); -}); diff --git a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts b/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts deleted file mode 100644 index e44735c2131..00000000000 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { PolicyType } from "../../../admin-console/enums"; -import { StateProvider } from "../../../platform/state"; -import { GeneratorStrategy } from "../abstractions"; -import { Randomizer } from "../abstractions/randomizer"; -import { SUBADDRESS_SETTINGS } from "../key-definitions"; -import { NoPolicy } from "../no-policy"; -import { newDefaultEvaluator } from "../rx-operators"; -import { clone$PerUserId, sharedStateByUserId } from "../util"; - -import { - DefaultSubaddressOptions, - SubaddressGenerationOptions, -} from "./subaddress-generator-options"; - -/** Strategy for creating an email subaddress - * @remarks The subaddress is the part following the `+`. - * For example, if the email address is `jd+xyz@domain.io`, - * the subaddress is `xyz`. - */ -export class SubaddressGeneratorStrategy - implements GeneratorStrategy -{ - /** Instantiates the generation strategy - * @param usernameService generates an email subaddress from an email address - */ - constructor( - private random: Randomizer, - private stateProvider: StateProvider, - private defaultOptions: SubaddressGenerationOptions = DefaultSubaddressOptions, - ) {} - - // configuration - durableState = sharedStateByUserId(SUBADDRESS_SETTINGS, this.stateProvider); - defaults$ = clone$PerUserId(this.defaultOptions); - toEvaluator = newDefaultEvaluator(); - readonly policy = PolicyType.PasswordGenerator; - - // algorithm - async generate(options: SubaddressGenerationOptions) { - const o = Object.assign({}, DefaultSubaddressOptions, options); - - const subaddressEmail = o.subaddressEmail; - if (subaddressEmail == null || subaddressEmail.length < 3) { - return o.subaddressEmail; - } - const atIndex = subaddressEmail.indexOf("@"); - if (atIndex < 1 || atIndex >= subaddressEmail.length - 1) { - return subaddressEmail; - } - if (o.subaddressType == null) { - o.subaddressType = "random"; - } - - const emailBeginning = subaddressEmail.substr(0, atIndex); - const emailEnding = subaddressEmail.substr(atIndex + 1, subaddressEmail.length); - - let subaddressString = ""; - if (o.subaddressType === "random") { - subaddressString = await this.random.chars(8); - } else if (o.subaddressType === "website-name") { - subaddressString = o.website; - } - return emailBeginning + "+" + subaddressString + "@" + emailEnding; - } -} diff --git a/libs/common/src/tools/generator/username/username-generation-options.ts b/libs/common/src/tools/generator/username/username-generation-options.ts deleted file mode 100644 index b52b4c0848b..00000000000 --- a/libs/common/src/tools/generator/username/username-generation-options.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CatchallGenerationOptions } from "./catchall-generator-options"; -import { EffUsernameGenerationOptions } from "./eff-username-generator-options"; -import { ForwarderId, RequestOptions } from "./options/forwarder-options"; -import { UsernameGeneratorType } from "./options/generator-options"; -import { SubaddressGenerationOptions } from "./subaddress-generator-options"; - -export type UsernameGeneratorOptions = EffUsernameGenerationOptions & - SubaddressGenerationOptions & - CatchallGenerationOptions & - RequestOptions & { - type?: UsernameGeneratorType; - forwardedService?: ForwarderId | ""; - forwardedAnonAddyApiToken?: string; - forwardedAnonAddyDomain?: string; - forwardedAnonAddyBaseUrl?: string; - forwardedDuckDuckGoToken?: string; - forwardedFirefoxApiToken?: string; - forwardedFastmailApiToken?: string; - forwardedForwardEmailApiToken?: string; - forwardedForwardEmailDomain?: string; - forwardedSimpleLoginApiKey?: string; - forwardedSimpleLoginBaseUrl?: string; - }; diff --git a/libs/common/src/tools/generator/util.ts b/libs/common/src/tools/generator/util.ts deleted file mode 100644 index ee526fc6786..00000000000 --- a/libs/common/src/tools/generator/util.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { BehaviorSubject } from "rxjs"; - -import { SingleUserState, StateProvider, UserKeyDefinition } from "../../platform/state"; -import { UserId } from "../../types/guid"; - -/** construct a method that outputs a copy of `defaultValue` as an observable. */ -export function clone$PerUserId(defaultValue: Value) { - const _subjects = new Map>(); - - return (key: UserId) => { - let value = _subjects.get(key); - - if (value === undefined) { - value = new BehaviorSubject({ ...defaultValue }); - _subjects.set(key, value); - } - - return value.asObservable(); - }; -} - -/** construct a method that caches user-specific states by userid. */ -export function sharedByUserId(create: (userId: UserId) => SingleUserState) { - const _subjects = new Map>(); - - return (key: UserId) => { - let value = _subjects.get(key); - - if (value === undefined) { - value = create(key); - _subjects.set(key, value); - } - - return value; - }; -} - -/** construct a method that loads a user-specific state from the provider. */ -export function sharedStateByUserId(key: UserKeyDefinition, provider: StateProvider) { - return (id: UserId) => provider.getUser(id, key); -} diff --git a/libs/common/src/tools/generator/word-options.ts b/libs/common/src/tools/generator/word-options.ts deleted file mode 100644 index 1c98d0bac8d..00000000000 --- a/libs/common/src/tools/generator/word-options.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type WordOptions = { - /** set the first letter uppercase */ - titleCase?: boolean; - /** append a number */ - number?: boolean; -};