From 3f40d72b5c49bd58c260d6fe26b56f149325e722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Thu, 27 Feb 2025 12:04:03 -0500 Subject: [PATCH] factor out generator metadata provider --- .../tools/integration/integration-metadata.ts | 4 +- .../generator/core/src/data/integrations.ts | 8 +- .../generator/core/src/integration/addy-io.ts | 3 +- .../core/src/integration/duck-duck-go.ts | 3 +- .../core/src/integration/fastmail.ts | 3 +- .../core/src/integration/firefox-relay.ts | 3 +- .../core/src/integration/forward-email.ts | 3 +- .../core/src/integration/simple-login.ts | 3 +- .../core/src/metadata/algorithm-metadata.ts | 16 +- .../core/src/metadata/email/catchall.spec.ts | 6 +- .../core/src/metadata/email/forwarder.ts | 79 +----- .../src/metadata/email/plus-address.spec.ts | 6 +- .../metadata/password/eff-word-list.spec.ts | 28 +- .../metadata/password/random-password.spec.ts | 12 +- .../metadata/username/eff-word-list.spec.ts | 6 +- .../policies/available-algorithms-policy.ts | 32 +-- .../services/generator-metadata-provider.ts | 244 ------------------ .../core/src/types/generator-type.ts | 8 +- 18 files changed, 49 insertions(+), 418 deletions(-) delete mode 100644 libs/tools/generator/core/src/services/generator-metadata-provider.ts diff --git a/libs/common/src/tools/integration/integration-metadata.ts b/libs/common/src/tools/integration/integration-metadata.ts index 2073b16feb0..e460aae828c 100644 --- a/libs/common/src/tools/integration/integration-metadata.ts +++ b/libs/common/src/tools/integration/integration-metadata.ts @@ -1,12 +1,10 @@ -import { VendorId } from "../extension"; - import { ExtensionPointId } from "./extension-point-id"; import { IntegrationId } from "./integration-id"; /** The capabilities and descriptive content for an integration */ export type IntegrationMetadata = { /** Uniquely identifies the integrator. */ - id: IntegrationId & VendorId; + id: IntegrationId; /** Brand name of the integrator. */ name: string; diff --git a/libs/tools/generator/core/src/data/integrations.ts b/libs/tools/generator/core/src/data/integrations.ts index ffe4676fcd7..21c883cae02 100644 --- a/libs/tools/generator/core/src/data/integrations.ts +++ b/libs/tools/generator/core/src/data/integrations.ts @@ -1,6 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { VendorId } from "@bitwarden/common/tools/extension"; import { IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings } from "@bitwarden/common/tools/integration/rpc"; @@ -30,11 +29,8 @@ export const Integrations = Object.freeze({ const integrations = new Map(Object.values(Integrations).map((i) => [i.id, i])); -export function getForwarderConfiguration( - id: IntegrationId | VendorId, -): ForwarderConfiguration { - // these casts are for compatibility; `IntegrationId` is the old form of `VendorId` - const maybeForwarder = integrations.get(id as string as IntegrationId & VendorId); +export function getForwarderConfiguration(id: IntegrationId): ForwarderConfiguration { + const maybeForwarder = integrations.get(id); if (maybeForwarder && "forwarder" in maybeForwarder) { return maybeForwarder as ForwarderConfiguration; diff --git a/libs/tools/generator/core/src/integration/addy-io.ts b/libs/tools/generator/core/src/integration/addy-io.ts index 631c5fdb510..d9f2b9f121d 100644 --- a/libs/tools/generator/core/src/integration/addy-io.ts +++ b/libs/tools/generator/core/src/integration/addy-io.ts @@ -3,7 +3,6 @@ import { GENERATOR_MEMORY, UserKeyDefinition, } from "@bitwarden/common/platform/state"; -import { VendorId } from "@bitwarden/common/tools/extension"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, @@ -101,7 +100,7 @@ const forwarder = Object.freeze({ export const AddyIo = Object.freeze({ // integration - id: "anonaddy" as IntegrationId & VendorId, + id: "anonaddy" as IntegrationId, name: "Addy.io", extends: ["forwarder"], diff --git a/libs/tools/generator/core/src/integration/duck-duck-go.ts b/libs/tools/generator/core/src/integration/duck-duck-go.ts index d2bd6173a14..0bcdd560503 100644 --- a/libs/tools/generator/core/src/integration/duck-duck-go.ts +++ b/libs/tools/generator/core/src/integration/duck-duck-go.ts @@ -3,7 +3,6 @@ import { GENERATOR_MEMORY, UserKeyDefinition, } from "@bitwarden/common/platform/state"; -import { VendorId } from "@bitwarden/common/tools/extension"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; @@ -90,7 +89,7 @@ const forwarder = Object.freeze({ // integration-wide configuration export const DuckDuckGo = Object.freeze({ - id: "duckduckgo" as IntegrationId & VendorId, + id: "duckduckgo" as IntegrationId, name: "DuckDuckGo", baseUrl: "https://quack.duckduckgo.com/api", selfHost: "never", diff --git a/libs/tools/generator/core/src/integration/fastmail.ts b/libs/tools/generator/core/src/integration/fastmail.ts index bfde1aa70f5..69b908badc9 100644 --- a/libs/tools/generator/core/src/integration/fastmail.ts +++ b/libs/tools/generator/core/src/integration/fastmail.ts @@ -5,7 +5,6 @@ import { GENERATOR_MEMORY, UserKeyDefinition, } from "@bitwarden/common/platform/state"; -import { VendorId } from "@bitwarden/common/tools/extension"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; @@ -160,7 +159,7 @@ const forwarder = Object.freeze({ // integration-wide configuration export const Fastmail = Object.freeze({ - id: "fastmail" as IntegrationId & VendorId, + id: "fastmail" as IntegrationId, name: "Fastmail", baseUrl: "https://api.fastmail.com", selfHost: "maybe", diff --git a/libs/tools/generator/core/src/integration/firefox-relay.ts b/libs/tools/generator/core/src/integration/firefox-relay.ts index 9f40a3631ff..ae65611905f 100644 --- a/libs/tools/generator/core/src/integration/firefox-relay.ts +++ b/libs/tools/generator/core/src/integration/firefox-relay.ts @@ -3,7 +3,6 @@ import { GENERATOR_MEMORY, UserKeyDefinition, } from "@bitwarden/common/platform/state"; -import { VendorId } from "@bitwarden/common/tools/extension"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; @@ -98,7 +97,7 @@ const forwarder = Object.freeze({ // integration-wide configuration export const FirefoxRelay = Object.freeze({ - id: "firefoxrelay" as IntegrationId & VendorId, + id: "firefoxrelay" as IntegrationId, name: "Firefox Relay", baseUrl: "https://relay.firefox.com/api", selfHost: "never", diff --git a/libs/tools/generator/core/src/integration/forward-email.ts b/libs/tools/generator/core/src/integration/forward-email.ts index 34b4602b94b..d67b8d588bf 100644 --- a/libs/tools/generator/core/src/integration/forward-email.ts +++ b/libs/tools/generator/core/src/integration/forward-email.ts @@ -3,7 +3,6 @@ import { GENERATOR_MEMORY, UserKeyDefinition, } from "@bitwarden/common/platform/state"; -import { VendorId } from "@bitwarden/common/tools/extension"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; @@ -102,7 +101,7 @@ const forwarder = Object.freeze({ export const ForwardEmail = Object.freeze({ // integration metadata - id: "forwardemail" as IntegrationId & VendorId, + id: "forwardemail" as IntegrationId, name: "Forward Email", extends: ["forwarder"], diff --git a/libs/tools/generator/core/src/integration/simple-login.ts b/libs/tools/generator/core/src/integration/simple-login.ts index efbac69cec2..1581f3861f5 100644 --- a/libs/tools/generator/core/src/integration/simple-login.ts +++ b/libs/tools/generator/core/src/integration/simple-login.ts @@ -3,7 +3,6 @@ import { GENERATOR_MEMORY, UserKeyDefinition, } from "@bitwarden/common/platform/state"; -import { VendorId } from "@bitwarden/common/tools/extension"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, @@ -104,7 +103,7 @@ const forwarder = Object.freeze({ // integration-wide configuration export const SimpleLogin = Object.freeze({ - id: "simplelogin" as IntegrationId & VendorId, + id: "simplelogin" as IntegrationId, name: "SimpleLogin", selfHost: "maybe", extends: ["forwarder"], diff --git a/libs/tools/generator/core/src/metadata/algorithm-metadata.ts b/libs/tools/generator/core/src/metadata/algorithm-metadata.ts index c07deef5535..f776dd76e54 100644 --- a/libs/tools/generator/core/src/metadata/algorithm-metadata.ts +++ b/libs/tools/generator/core/src/metadata/algorithm-metadata.ts @@ -1,7 +1,5 @@ import { CredentialAlgorithm, CredentialType } from "./type"; -type I18nKeyOrLiteral = string | { literal: string }; - /** Credential generator metadata common across credential generators */ export type AlgorithmMetadata = { /** Uniquely identifies the credential configuration @@ -25,25 +23,25 @@ export type AlgorithmMetadata = { /** Localization keys */ i18nKeys: { /** descriptive name of the algorithm */ - name: I18nKeyOrLiteral; + name: string; /** explanatory text for the algorithm */ - description?: I18nKeyOrLiteral; + description?: string; /** labels the generate action */ - generateCredential: I18nKeyOrLiteral; + generateCredential: string; /** message informing users when the generator produces a new credential */ - credentialGenerated: I18nKeyOrLiteral; + credentialGenerated: string; /* labels the action that assigns a generated value to a domain object */ - useCredential: I18nKeyOrLiteral; + useCredential: string; /** labels the generated output */ - credentialType: I18nKeyOrLiteral; + credentialType: string; /** labels the copy output action */ - copyCredential: I18nKeyOrLiteral; + copyCredential: string; }; /** fine-tunings for generator user experiences */ diff --git a/libs/tools/generator/core/src/metadata/email/catchall.spec.ts b/libs/tools/generator/core/src/metadata/email/catchall.spec.ts index d6cc1795e0b..f63f141842c 100644 --- a/libs/tools/generator/core/src/metadata/email/catchall.spec.ts +++ b/libs/tools/generator/core/src/metadata/email/catchall.spec.ts @@ -19,13 +19,11 @@ describe("email - catchall generator metadata", () => { }); describe("profiles[account]", () => { - let accountProfile: CoreProfileMetadata = null!; + let accountProfile: CoreProfileMetadata = null; beforeEach(() => { const profile = catchall.profiles[Profile.account]; - if (isCoreProfile(profile!)) { + if (isCoreProfile(profile)) { accountProfile = profile; - } else { - throw new Error("this branch should never run"); } }); diff --git a/libs/tools/generator/core/src/metadata/email/forwarder.ts b/libs/tools/generator/core/src/metadata/email/forwarder.ts index f4f150f33fa..1dfc219d466 100644 --- a/libs/tools/generator/core/src/metadata/email/forwarder.ts +++ b/libs/tools/generator/core/src/metadata/email/forwarder.ts @@ -1,75 +1,4 @@ -import { ExtensionMetadata, ExtensionStorageKey } from "@bitwarden/common/tools/extension/type"; -import { SelfHostedApiSettings } from "@bitwarden/common/tools/integration/rpc"; -import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; - -import { getForwarderConfiguration } from "../../data"; -import { EmailDomainSettings, EmailPrefixSettings } from "../../engine"; -import { Forwarder } from "../../engine/forwarder"; -import { GeneratorDependencyProvider } from "../../types"; -import { Profile, Type } from "../data"; -import { GeneratorMetadata } from "../generator-metadata"; -import { ForwarderProfileMetadata } from "../profile-metadata"; - -// These options are used by all forwarders; each forwarder uses a different set, -// as defined by `GeneratorMetadata.capabilities.fields`. -type ForwarderOptions = Partial; - -// update the extension metadata -export function toForwarderMetadata( - extension: ExtensionMetadata, -): GeneratorMetadata { - if (extension.site.id !== "forwarder") { - throw new Error( - `expected forwarder extension; received ${extension.site.id} (${extension.product.vendor.id})`, - ); - } - - const name = { literal: extension.product.name ?? extension.product.vendor.name }; - - const generator: GeneratorMetadata = { - id: { forwarder: extension.product.vendor.id }, - category: Type.email, - weight: 300, - i18nKeys: { - name, - description: "forwardedEmailDesc", - generateCredential: "generateEmail", - credentialGenerated: "emailGenerated", - useCredential: "useThisEmail", - credentialType: "email", - copyCredential: "copyEmail", - }, - capabilities: { - autogenerate: false, - fields: [...extension.requestedFields], - }, - engine: { - create(dependencies: GeneratorDependencyProvider) { - const config = getForwarderConfiguration(extension.product.vendor.id); - return new Forwarder(config, dependencies.client, dependencies.i18nService); - }, - }, - profiles: { - [Profile.account]: { - type: "extension", - site: "forwarder", - storage: { - key: "forwarder", - frame: 512, - options: { - deserializer: (value) => value, - clearOn: ["logout"], - }, - } satisfies ExtensionStorageKey, - constraints: { - default: {}, - create() { - return new IdentityConstraint(); - }, - }, - } satisfies ForwarderProfileMetadata, - }, - }; - - return generator; -} +// Forwarders are pending integration with the extension API +// +// They use the 300-block of weights and derive their metadata +// using logic similar to `toCredentialGeneratorConfiguration` diff --git a/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts b/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts index 063cb71c23a..2ac7645ed30 100644 --- a/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts +++ b/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts @@ -19,13 +19,11 @@ describe("email - plus address generator metadata", () => { }); describe("profiles[account]", () => { - let accountProfile: CoreProfileMetadata = null!; + let accountProfile: CoreProfileMetadata = null; beforeEach(() => { const profile = plusAddress.profiles[Profile.account]; - if (isCoreProfile(profile!)) { + if (isCoreProfile(profile)) { accountProfile = profile; - } else { - throw new Error("this branch should never run"); } }); diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts index e02d63d3d59..57961a60033 100644 --- a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts @@ -22,21 +22,19 @@ describe("password - eff words generator metadata", () => { }); describe("profiles[account]", () => { - let accountProfile: CoreProfileMetadata | null = null; + let accountProfile: CoreProfileMetadata = null; beforeEach(() => { const profile = effPassphrase.profiles[Profile.account]; - if (isCoreProfile(profile!)) { + if (isCoreProfile(profile)) { accountProfile = profile; - } else { - accountProfile = null; } }); describe("storage.options.deserializer", () => { it("returns its input", () => { - const value: PassphraseGenerationOptions = { ...accountProfile!.storage.initial }; + const value: PassphraseGenerationOptions = { ...accountProfile.storage.initial }; - const result = accountProfile!.storage.options.deserializer(value); + const result = accountProfile.storage.options.deserializer(value); expect(result).toBe(value); }); @@ -48,15 +46,15 @@ describe("password - eff words generator metadata", () => { // enclosed behaviors change. it("creates a passphrase policy constraints", () => { - const context = { defaultConstraints: accountProfile!.constraints.default }; + const context = { defaultConstraints: accountProfile.constraints.default }; - const constraints = accountProfile!.constraints.create([], context); + const constraints = accountProfile.constraints.create([], context); expect(constraints).toBeInstanceOf(PassphrasePolicyConstraints); }); it("forwards the policy to the constraints", () => { - const context = { defaultConstraints: accountProfile!.constraints.default }; + const context = { defaultConstraints: accountProfile.constraints.default }; const policies = [ { type: PolicyType.PasswordGenerator, @@ -68,13 +66,13 @@ describe("password - eff words generator metadata", () => { }, ] as Policy[]; - const constraints = accountProfile!.constraints.create(policies, context); + const constraints = accountProfile.constraints.create(policies, context); - expect(constraints.constraints.numWords?.min).toEqual(6); + expect(constraints.constraints.numWords.min).toEqual(6); }); it("combines multiple policies in the constraints", () => { - const context = { defaultConstraints: accountProfile!.constraints.default }; + const context = { defaultConstraints: accountProfile.constraints.default }; const policies = [ { type: PolicyType.PasswordGenerator, @@ -94,10 +92,10 @@ describe("password - eff words generator metadata", () => { }, ] as Policy[]; - const constraints = accountProfile!.constraints.create(policies, context); + const constraints = accountProfile.constraints.create(policies, context); - expect(constraints.constraints.numWords?.min).toEqual(6); - expect(constraints.constraints.capitalize?.requiredValue).toEqual(true); + expect(constraints.constraints.numWords.min).toEqual(6); + expect(constraints.constraints.capitalize.requiredValue).toEqual(true); }); }); }); diff --git a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts index 9e38c50ee2a..d91ceaac248 100644 --- a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts +++ b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts @@ -22,13 +22,11 @@ describe("password - characters generator metadata", () => { }); describe("profiles[account]", () => { - let accountProfile: CoreProfileMetadata = null!; + let accountProfile: CoreProfileMetadata = null; beforeEach(() => { const profile = password.profiles[Profile.account]; - if (isCoreProfile(profile!)) { + if (isCoreProfile(profile)) { accountProfile = profile; - } else { - throw new Error("this branch should never run"); } }); @@ -71,7 +69,7 @@ describe("password - characters generator metadata", () => { const constraints = accountProfile.constraints.create(policies, context); - expect(constraints.constraints.length?.min).toEqual(10); + expect(constraints.constraints.length.min).toEqual(10); }); it("combines multiple policies in the constraints", () => { @@ -99,8 +97,8 @@ describe("password - characters generator metadata", () => { const constraints = accountProfile.constraints.create(policies, context); - expect(constraints.constraints.length?.min).toEqual(14); - expect(constraints.constraints.special?.requiredValue).toEqual(true); + expect(constraints.constraints.length.min).toEqual(14); + expect(constraints.constraints.special.requiredValue).toEqual(true); }); }); }); diff --git a/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts index d47d5ec9fcb..aba9680a448 100644 --- a/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts +++ b/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts @@ -20,13 +20,11 @@ describe("username - eff words generator metadata", () => { }); describe("profiles[account]", () => { - let accountProfile: CoreProfileMetadata = null!; + let accountProfile: CoreProfileMetadata = null; beforeEach(() => { const profile = effWordList.profiles[Profile.account]; - if (isCoreProfile(profile!)) { + if (isCoreProfile(profile)) { accountProfile = profile; - } else { - throw new Error("this branch should never run"); } }); diff --git a/libs/tools/generator/core/src/policies/available-algorithms-policy.ts b/libs/tools/generator/core/src/policies/available-algorithms-policy.ts index 0c44a1a0408..f37a8b21a3f 100644 --- a/libs/tools/generator/core/src/policies/available-algorithms-policy.ts +++ b/libs/tools/generator/core/src/policies/available-algorithms-policy.ts @@ -5,41 +5,13 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; // implement ADR-0002 import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { - CredentialAlgorithm as LegacyAlgorithm, - EmailAlgorithms, - PasswordAlgorithms, - UsernameAlgorithms, -} from ".."; -import { CredentialAlgorithm } from "../metadata"; +import { CredentialAlgorithm, EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from ".."; /** Reduces policies to a set of available algorithms * @param policies the policies to reduce * @returns the resulting `AlgorithmAvailabilityPolicy` */ -export function availableAlgorithms(policies: Policy[]): LegacyAlgorithm[] { - const overridePassword = policies - .filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled) - .reduce( - (type, policy) => (type === "password" ? type : (policy.data.overridePasswordType ?? type)), - null as LegacyAlgorithm, - ); - - const policy: LegacyAlgorithm[] = [...EmailAlgorithms, ...UsernameAlgorithms]; - if (overridePassword) { - policy.push(overridePassword); - } else { - policy.push(...PasswordAlgorithms); - } - - return policy; -} - -/** Reduces policies to a set of available algorithms - * @param policies the policies to reduce - * @returns the resulting `AlgorithmAvailabilityPolicy` - */ -export function availableAlgorithms_vNext(policies: Policy[]): CredentialAlgorithm[] { +export function availableAlgorithms(policies: Policy[]): CredentialAlgorithm[] { const overridePassword = policies .filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled) .reduce( diff --git a/libs/tools/generator/core/src/services/generator-metadata-provider.ts b/libs/tools/generator/core/src/services/generator-metadata-provider.ts deleted file mode 100644 index 00d443d92ea..00000000000 --- a/libs/tools/generator/core/src/services/generator-metadata-provider.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { - Observable, - distinctUntilChanged, - filter, - first, - map, - shareReplay, - switchMap, - takeUntil, - withLatestFrom, -} from "rxjs"; - -import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { Account } from "@bitwarden/common/auth/abstractions/account.service"; -import { BoundDependency } from "@bitwarden/common/tools/dependencies"; -import { ExtensionSite } from "@bitwarden/common/tools/extension"; -import { SemanticLogger } from "@bitwarden/common/tools/log"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import { anyComplete } from "@bitwarden/common/tools/rx"; -import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; -import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider"; - -import { - GeneratorMetadata, - AlgorithmsByType, - CredentialAlgorithm, - CredentialType, - isForwarderExtensionId, - toForwarderMetadata, - Type, -} from "../metadata"; -import { availableAlgorithms_vNext } from "../policies/available-algorithms-policy"; -import { CredentialPreference } from "../types"; - -import { PREFERENCES } from "./credential-preferences"; - -type AlgorithmRequest = { algorithm: CredentialAlgorithm }; -type TypeRequest = { category: CredentialType }; -type MetadataRequest = Partial; - -/** Surfaces contextual information to credential generators */ -export class GeneratorMetadataProvider { - /** Instantiates the context provider - * @param providers dependency injectors for user state subjects - * @param policyService settings constraint lookups - */ - constructor( - private readonly providers: UserStateSubjectDependencyProvider, - private readonly system: SystemServiceProvider, - algorithms: GeneratorMetadata[], - ) { - this.log = providers.log({ type: "GeneratorMetadataProvider" }); - - const site = system.extension.site("forwarder"); - if (!site) { - this.log.panic("forwarder extension site not found"); - } - this.site = site; - - this.generators = new Map(algorithms.map((a) => [a.id, a] as const)); - } - - private readonly site: ExtensionSite; - private readonly log: SemanticLogger; - - private generators: Map>; - - // looks up a set of algorithms; does not enforce policy - algorithms(requested: AlgorithmRequest): CredentialAlgorithm[]; - algorithms(requested: TypeRequest): CredentialAlgorithm[]; - algorithms(requested: MetadataRequest): CredentialAlgorithm[]; - algorithms(requested: MetadataRequest): CredentialAlgorithm[] { - let algorithms: CredentialAlgorithm[]; - if (requested.category) { - let forwarders: CredentialAlgorithm[] = []; - if (requested.category === Type.email) { - forwarders = Array.from(this.site.extensions.keys()).map((forwarder) => ({ forwarder })); - } - - algorithms = AlgorithmsByType[requested.category].concat(forwarders); - } else if (requested.algorithm) { - algorithms = [requested.algorithm]; - } else { - this.log.panic(requested, "algorithm or category required"); - } - - return algorithms; - } - - // emits a function that returns `true` when the input algorithm is available - private isAvailable$( - dependencies: BoundDependency<"account", Account>, - ): Observable<(a: CredentialAlgorithm) => boolean> { - const account$ = dependencies.account$.pipe( - distinctUntilChanged((previous, current) => previous.id === current.id), - shareReplay({ bufferSize: 1, refCount: true }), - ); - - const available$ = account$.pipe( - switchMap((account) => { - const policies$ = this.system.policy.getAll$(PolicyType.PasswordGenerator, account.id).pipe( - map((p) => new Set(availableAlgorithms_vNext(p))), - // complete policy emissions otherwise `switchMap` holds `algorithms$` open indefinitely - takeUntil(anyComplete(account$)), - ); - return policies$; - }), - map((available) => (a: CredentialAlgorithm) => isForwarderExtensionId(a) || available.has(a)), - ); - - return available$; - } - - // looks up a set of algorithms; enforces policy - emits empty list when there's no algorithm available - available$( - requested: AlgorithmRequest, - dependencies: BoundDependency<"account", Account>, - ): Observable[]>; - available$( - requested: TypeRequest, - dependencies: BoundDependency<"account", Account>, - ): Observable[]>; - available$( - requested: MetadataRequest, - dependencies: BoundDependency<"account", Account>, - ): Observable[]> { - let available$: Observable; - if (requested.category) { - const { category } = requested; - - available$ = this.isAvailable$(dependencies).pipe( - map((isAvailable) => AlgorithmsByType[category].filter(isAvailable)), - ); - } else if (requested.algorithm) { - const { algorithm } = requested; - available$ = this.isAvailable$(dependencies).pipe( - map((isAvailable) => (isAvailable(algorithm) ? [algorithm] : [])), - ); - } else { - this.log.panic(requested, "algorithm or category required"); - } - - const result$ = available$.pipe( - map((available) => available.map((algorithm) => this.getMetadata(algorithm))), - ); - - return result$; - } - - // looks up a specific algorithm; enforces policy - observable completes without emission when there's no algorithm available. - algorithm$( - requested: AlgorithmRequest, - dependencies: BoundDependency<"account", Account>, - ): Observable>; - algorithm$( - requested: TypeRequest, - dependencies: BoundDependency<"account", Account>, - ): Observable>; - algorithm$( - requested: MetadataRequest, - dependencies: BoundDependency<"account", Account>, - ): Observable> { - const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true })); - - let algorithm$: Observable; - if (requested.category) { - this.log.debug(requested, "retrieving algorithm metadata by category"); - - const { category } = requested; - algorithm$ = this.preferences({ account$ }).pipe( - withLatestFrom(this.isAvailable$({ account$ })), - map(([preferences, isAvailable]) => { - let algorithm: CredentialAlgorithm | undefined = preferences[category].algorithm; - if (isAvailable(algorithm)) { - return algorithm; - } - - const algorithms = AlgorithmsByType[category]; - algorithm = algorithms.find(isAvailable)!; - this.log.debug( - { algorithm, category }, - "preference not available; defaulting the generator algorithm", - ); - - return algorithm; - }), - ); - } else if (requested.algorithm) { - this.log.debug(requested, "retrieving algorithm metadata by algorithm"); - - const { algorithm } = requested; - algorithm$ = this.isAvailable$({ account$ }).pipe( - map((isAvailable) => (isAvailable(algorithm) ? algorithm : undefined)), - first(), - ); - } else { - this.log.panic(requested, "algorithm or category required"); - } - - const result$ = algorithm$.pipe( - filter((value) => !!value), - map((algorithm) => this.getMetadata(algorithm!)), - ); - - return result$; - } - - private getMetadata(algorithm: CredentialAlgorithm) { - let result = null; - if (isForwarderExtensionId(algorithm)) { - const extension = this.site.extensions.get(algorithm.forwarder); - if (!extension) { - this.log.panic(algorithm, "extension not found"); - } - - result = toForwarderMetadata(extension); - } else { - result = this.generators.get(algorithm); - } - - if (!result) { - this.log.panic({ algorithm }, "failed to load metadata"); - } - - return result; - } - - /** Get a subject bound to credential generator preferences. - * @param dependencies.account$ identifies the account to which the preferences are bound - * @returns a subject bound to the user's preferences - * @remarks Preferences determine which algorithms are used when generating a - * credential from a credential category (e.g. `PassX` or `Username`). Preferences - * should not be used to hold navigation history. Use @bitwarden/generator-navigation - * instead. - */ - preferences( - dependencies: BoundDependency<"account", Account>, - ): UserStateSubject { - // FIXME: enforce policy - const subject = new UserStateSubject(PREFERENCES, this.providers, dependencies); - - return subject; - } -} diff --git a/libs/tools/generator/core/src/types/generator-type.ts b/libs/tools/generator/core/src/types/generator-type.ts index c75e4329610..5b74d17fa4a 100644 --- a/libs/tools/generator/core/src/types/generator-type.ts +++ b/libs/tools/generator/core/src/types/generator-type.ts @@ -1,8 +1,6 @@ -import { VendorId } from "@bitwarden/common/tools/extension"; import { IntegrationId } from "@bitwarden/common/tools/integration"; import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "../data/generator-types"; -import { AlgorithmsByType, CredentialType } from "../metadata"; /** A type of password that may be generated by the credential generator. */ export type PasswordAlgorithm = (typeof PasswordAlgorithms)[number]; @@ -13,7 +11,7 @@ export type UsernameAlgorithm = (typeof UsernameAlgorithms)[number]; /** A type of email address that may be generated by the credential generator. */ export type EmailAlgorithm = (typeof EmailAlgorithms)[number]; -export type ForwarderIntegration = { forwarder: IntegrationId & VendorId }; +export type ForwarderIntegration = { forwarder: IntegrationId }; /** Returns true when the input algorithm is a forwarder integration. */ export function isForwarderIntegration( @@ -76,8 +74,8 @@ export type CredentialCategory = keyof typeof CredentialCategories; /** The kind of credential to generate using a compound configuration. */ // FIXME: extend the preferences to include a preferred forwarder export type CredentialPreference = { - [Key in CredentialType & CredentialCategory]: { - algorithm: CredentialAlgorithm & (typeof AlgorithmsByType)[Key][number]; + [Key in CredentialCategory]: { + algorithm: (typeof CredentialCategories)[Key][number]; updated: Date; }; };