From b8d5d3c0a67fbd9b48634f72da59fa261b2b48db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Fri, 27 Dec 2024 11:32:36 -0500 Subject: [PATCH] rename new integration types to extension types The first pass of the integration logic mixed names between "integration" and "extension". For example, the remote service has an "IntegrationId" whereas the part of the password manager it extended has an "ExtensionPointId". The new types have a more consistent approach. The remote service now distinguishes the integrated product from the vendor providing it, and the logic within the password manager is now all the "extension". The specific location being extended is now called the "site" or "extension site". The main reason "extension" was chosen over "integration" is simply so that the new and old types remain distinct in the folder hierarchy. --- libs/common/src/tools/dependencies.ts | 2 +- .../vendor => extension/data}/addy-io.ts | 11 +- .../src/tools/extension/data/duck-duck-go.ts | 23 +++ .../vendor => extension/data}/fastmail.ts | 11 +- .../data}/forward-email.ts | 11 +- .../vendor => extension/data}/mozilla.ts | 11 +- .../vendor => extension/data}/simple-login.ts | 11 +- .../src/tools/extension/data/vendor-info.ts | 29 +++ .../src/tools/extension/extension-site.ts | 20 ++ .../src/tools/extension/extension.service.ts | 15 ++ .../metadata/data.ts | 0 .../metadata/default-extension-registry.ts | 178 ++++++++++++++++++ .../extension-registry.abstraction.ts | 70 +++++++ .../metadata/extension.ts | 4 +- .../src/tools/extension/metadata/factory.ts | 36 ++++ .../metadata/type.ts | 57 ++++-- .../src/tools/integration/integration-key.ts | 48 ----- .../tools/integration/integration.service.ts | 58 ------ .../src/tools/integration/metadata/factory.ts | 35 ---- .../tools/integration/metadata/registry.ts | 87 --------- .../metadata/vendor/duck-duck-go.ts | 22 --- libs/common/src/tools/util.ts | 19 ++ .../generator/core/src/integration/type.ts | 49 +++++ .../core/src/metadata/generator-metadata.ts | 1 + 24 files changed, 519 insertions(+), 289 deletions(-) rename libs/common/src/tools/{integration/metadata/vendor => extension/data}/addy-io.ts (50%) create mode 100644 libs/common/src/tools/extension/data/duck-duck-go.ts rename libs/common/src/tools/{integration/metadata/vendor => extension/data}/fastmail.ts (51%) rename libs/common/src/tools/{integration/metadata/vendor => extension/data}/forward-email.ts (50%) rename libs/common/src/tools/{integration/metadata/vendor => extension/data}/mozilla.ts (50%) rename libs/common/src/tools/{integration/metadata/vendor => extension/data}/simple-login.ts (51%) create mode 100644 libs/common/src/tools/extension/data/vendor-info.ts create mode 100644 libs/common/src/tools/extension/extension-site.ts create mode 100644 libs/common/src/tools/extension/extension.service.ts rename libs/common/src/tools/{integration => extension}/metadata/data.ts (100%) create mode 100644 libs/common/src/tools/extension/metadata/default-extension-registry.ts create mode 100644 libs/common/src/tools/extension/metadata/extension-registry.abstraction.ts rename libs/common/src/tools/{integration => extension}/metadata/extension.ts (61%) create mode 100644 libs/common/src/tools/extension/metadata/factory.ts rename libs/common/src/tools/{integration => extension}/metadata/type.ts (54%) delete mode 100644 libs/common/src/tools/integration/integration-key.ts delete mode 100644 libs/common/src/tools/integration/integration.service.ts delete mode 100644 libs/common/src/tools/integration/metadata/factory.ts delete mode 100644 libs/common/src/tools/integration/metadata/registry.ts delete mode 100644 libs/common/src/tools/integration/metadata/vendor/duck-duck-go.ts create mode 100644 libs/common/src/tools/util.ts create mode 100644 libs/tools/generator/core/src/integration/type.ts diff --git a/libs/common/src/tools/dependencies.ts b/libs/common/src/tools/dependencies.ts index e919567af01..a1a3633721e 100644 --- a/libs/common/src/tools/dependencies.ts +++ b/libs/common/src/tools/dependencies.ts @@ -5,7 +5,7 @@ import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { OrganizationEncryptor } from "./cryptography/organization-encryptor.abstraction"; import { UserEncryptor } from "./cryptography/user-encryptor.abstraction"; -import { VendorId } from "./integration/metadata/type"; +import { VendorId } from "./extension/metadata/type"; /** error emitted when the `SingleUserDependency` changes Ids */ export type UserChangedError = { diff --git a/libs/common/src/tools/integration/metadata/vendor/addy-io.ts b/libs/common/src/tools/extension/data/addy-io.ts similarity index 50% rename from libs/common/src/tools/integration/metadata/vendor/addy-io.ts rename to libs/common/src/tools/extension/data/addy-io.ts index 677deec95fd..4fb72ef2cf8 100644 --- a/libs/common/src/tools/integration/metadata/vendor/addy-io.ts +++ b/libs/common/src/tools/extension/data/addy-io.ts @@ -1,19 +1,20 @@ -import { Field, Vendor } from "../data"; -import { Extension } from "../extension"; -import { IntegrationMetadata } from "../type"; +import { Field, Vendor } from "../metadata/data"; +import { Extension } from "../metadata/extension"; +import { ExtensionMetadata } from "../metadata/type"; export const AddyIo = { id: Vendor.anonaddy, name: "Addy.io", }; -export const AddyIoIntegrations: IntegrationMetadata[] = [ +export const AddyIoIntegrations: ExtensionMetadata[] = [ { - extension: Extension.forwarder, + site: Extension.forwarder, product: { vendor: AddyIo, }, host: { + authorization: "bearer", selfHost: "maybe", baseUrl: "https://app.addy.io", }, diff --git a/libs/common/src/tools/extension/data/duck-duck-go.ts b/libs/common/src/tools/extension/data/duck-duck-go.ts new file mode 100644 index 00000000000..231e0ba50f3 --- /dev/null +++ b/libs/common/src/tools/extension/data/duck-duck-go.ts @@ -0,0 +1,23 @@ +import { Field, Vendor } from "../metadata/data"; +import { Extension } from "../metadata/extension"; +import { ExtensionMetadata } from "../metadata/type"; + +export const DuckDuckGo = { + id: Vendor.duckduckgo, + name: "DuckDuckGo", +}; + +export const DuckDuckGoIntegrations: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: DuckDuckGo, + }, + host: { + authorization: "bearer", + selfHost: "never", + baseUrl: "https://quack.duckduckgo.com/api", + }, + requestedFields: [Field.token], + }, +]; diff --git a/libs/common/src/tools/integration/metadata/vendor/fastmail.ts b/libs/common/src/tools/extension/data/fastmail.ts similarity index 51% rename from libs/common/src/tools/integration/metadata/vendor/fastmail.ts rename to libs/common/src/tools/extension/data/fastmail.ts index 839ba740e90..65e6bd163ea 100644 --- a/libs/common/src/tools/integration/metadata/vendor/fastmail.ts +++ b/libs/common/src/tools/extension/data/fastmail.ts @@ -1,6 +1,6 @@ -import { Field, Vendor } from "../data"; -import { Extension } from "../extension"; -import { IntegrationMetadata } from "../type"; +import { Field, Vendor } from "../metadata/data"; +import { Extension } from "../metadata/extension"; +import { ExtensionMetadata } from "../metadata/type"; export const Fastmail = { id: Vendor.fastmail, @@ -8,13 +8,14 @@ export const Fastmail = { }; // integration-wide configuration -export const FastmailIntegrations: IntegrationMetadata[] = [ +export const FastmailIntegrations: ExtensionMetadata[] = [ { - extension: Extension.forwarder, + site: Extension.forwarder, product: { vendor: Fastmail, }, host: { + authorization: "bearer", selfHost: "maybe", baseUrl: "https://api.fastmail.com", }, diff --git a/libs/common/src/tools/integration/metadata/vendor/forward-email.ts b/libs/common/src/tools/extension/data/forward-email.ts similarity index 50% rename from libs/common/src/tools/integration/metadata/vendor/forward-email.ts rename to libs/common/src/tools/extension/data/forward-email.ts index d3d2a23b492..b7e1085fb80 100644 --- a/libs/common/src/tools/integration/metadata/vendor/forward-email.ts +++ b/libs/common/src/tools/extension/data/forward-email.ts @@ -1,19 +1,20 @@ -import { Field, Vendor } from "../data"; -import { Extension } from "../extension"; -import { IntegrationMetadata } from "../type"; +import { Field, Vendor } from "../metadata/data"; +import { Extension } from "../metadata/extension"; +import { ExtensionMetadata } from "../metadata/type"; export const ForwardEmail = { id: Vendor.forwardemail, name: "Forward Email", }; -export const ForwardEmailIntegrations: IntegrationMetadata[] = [ +export const ForwardEmailIntegrations: ExtensionMetadata[] = [ { - extension: Extension.forwarder, + site: Extension.forwarder, product: { vendor: ForwardEmail, }, host: { + authorization: "basic-username", selfHost: "never", baseUrl: "https://api.forwardemail.net", }, diff --git a/libs/common/src/tools/integration/metadata/vendor/mozilla.ts b/libs/common/src/tools/extension/data/mozilla.ts similarity index 50% rename from libs/common/src/tools/integration/metadata/vendor/mozilla.ts rename to libs/common/src/tools/extension/data/mozilla.ts index 1f95e863b0e..2c3f394743e 100644 --- a/libs/common/src/tools/integration/metadata/vendor/mozilla.ts +++ b/libs/common/src/tools/extension/data/mozilla.ts @@ -1,20 +1,21 @@ -import { Field, Vendor } from "../data"; -import { Extension } from "../extension"; -import { IntegrationMetadata } from "../type"; +import { Field, Vendor } from "../metadata/data"; +import { Extension } from "../metadata/extension"; +import { ExtensionMetadata } from "../metadata/type"; export const Mozilla = { id: Vendor.mozilla, name: "Mozilla", }; -export const MozillaIntegrations: IntegrationMetadata[] = [ +export const MozillaIntegrations: ExtensionMetadata[] = [ { - extension: Extension.forwarder, + site: Extension.forwarder, product: { vendor: Mozilla, name: "Firefox Relay", }, host: { + authorization: "token", selfHost: "never", baseUrl: "https://relay.firefox.com/api", }, diff --git a/libs/common/src/tools/integration/metadata/vendor/simple-login.ts b/libs/common/src/tools/extension/data/simple-login.ts similarity index 51% rename from libs/common/src/tools/integration/metadata/vendor/simple-login.ts rename to libs/common/src/tools/extension/data/simple-login.ts index 3f046e83573..bb0684b2e50 100644 --- a/libs/common/src/tools/integration/metadata/vendor/simple-login.ts +++ b/libs/common/src/tools/extension/data/simple-login.ts @@ -1,19 +1,20 @@ -import { Field, Vendor } from "../data"; -import { Extension } from "../extension"; -import { IntegrationMetadata, VendorMetadata } from "../type"; +import { Field, Vendor } from "../metadata/data"; +import { Extension } from "../metadata/extension"; +import { ExtensionMetadata, VendorMetadata } from "../metadata/type"; export const SimpleLogin: VendorMetadata = { id: Vendor.simplelogin, name: "SimpleLogin", }; -export const SimpleLoginIntegrations: IntegrationMetadata[] = [ +export const SimpleLoginIntegrations: ExtensionMetadata[] = [ { - extension: Extension.forwarder, + site: Extension.forwarder, product: { vendor: SimpleLogin, }, host: { + authentication: true, selfHost: "maybe", baseUrl: "https://app.simplelogin.io", }, diff --git a/libs/common/src/tools/extension/data/vendor-info.ts b/libs/common/src/tools/extension/data/vendor-info.ts new file mode 100644 index 00000000000..0d1d722fc76 --- /dev/null +++ b/libs/common/src/tools/extension/data/vendor-info.ts @@ -0,0 +1,29 @@ +import { Vendor } from "../metadata/data"; +import { VendorMetadata } from "../metadata/type"; + +export const VendorInfo: Record = { + [Vendor.anonaddy]: { + id: Vendor.anonaddy, + name: "Addy.io", + }, + [Vendor.duckduckgo]: { + id: Vendor.duckduckgo, + name: "DuckDuckGo", + }, + [Vendor.fastmail]: { + id: Vendor.fastmail, + name: "Fastmail", + }, + [Vendor.mozilla]: { + id: Vendor.mozilla, + name: "Mozilla", + }, + [Vendor.forwardemail]: { + id: Vendor.forwardemail, + name: "Forward Email", + }, + [Vendor.simplelogin]: { + id: Vendor.simplelogin, + name: "SimpleLogin", + }, +}; diff --git a/libs/common/src/tools/extension/extension-site.ts b/libs/common/src/tools/extension/extension-site.ts new file mode 100644 index 00000000000..988f6cdcb33 --- /dev/null +++ b/libs/common/src/tools/extension/extension-site.ts @@ -0,0 +1,20 @@ +import { deepFreeze } from "../util"; + +import { ExtensionMetadata, SiteMetadata, VendorId } from "./metadata/type"; + +/** Describes the capabilities of an extension site. + * This type is immutable. + */ +export class ExtensionSite { + /** instantiate the extension site + * @param site describes the extension site + * @param vendors describes the available vendors + * @param extensions describes the available extensions + */ + constructor( + readonly site: Readonly, + readonly extensions: ReadonlyMap>, + ) { + deepFreeze(this); + } +} diff --git a/libs/common/src/tools/extension/extension.service.ts b/libs/common/src/tools/extension/extension.service.ts new file mode 100644 index 00000000000..cadb2a7926b --- /dev/null +++ b/libs/common/src/tools/extension/extension.service.ts @@ -0,0 +1,15 @@ +import { StateProvider } from "@bitwarden/common/platform/state"; + +import { LegacyEncryptorProvider } from "../cryptography/legacy-encryptor-provider"; + +import { DefaultIntegrationRegistry } from "./metadata/default-extension-registry"; + +export class ExtensionService { + constructor( + private readonly registry: DefaultIntegrationRegistry, + private readonly stateProvider: StateProvider, + private readonly encryptorProvider: LegacyEncryptorProvider, + ) {} + + // TODO: implement the service +} diff --git a/libs/common/src/tools/integration/metadata/data.ts b/libs/common/src/tools/extension/metadata/data.ts similarity index 100% rename from libs/common/src/tools/integration/metadata/data.ts rename to libs/common/src/tools/extension/metadata/data.ts diff --git a/libs/common/src/tools/extension/metadata/default-extension-registry.ts b/libs/common/src/tools/extension/metadata/default-extension-registry.ts new file mode 100644 index 00000000000..d2e695c41f7 --- /dev/null +++ b/libs/common/src/tools/extension/metadata/default-extension-registry.ts @@ -0,0 +1,178 @@ +import { ExtensionSite } from "../extension-site"; + +import { ExtensionPermission, ExtensionRegistry } from "./extension-registry.abstraction"; +import { + SiteMetadata, + SiteId, + ExtensionMetadata, + ExtensionSet, + VendorId, + VendorMetadata, +} from "./type"; + +/** Tracks extension points and the integrations that extend them. */ +export class DefaultIntegrationRegistry implements ExtensionRegistry { + private allRule: ExtensionPermission = "default"; + + private siteRegistrations = new Map(); + private siteRules = new Map(); + + private vendorRegistrations = new Map(); + private vendorRules = new Map(); + + private integrations = new Array(); + private siteIntegrationsByVendor = new Map>(); + private vendorIntegrationsBySite = new Map>(); + + registerSite(meta: SiteMetadata): this { + if (!this.siteRegistrations.has(meta.id)) { + this.siteRegistrations.set(meta.id, meta); + } + + return this; + } + + sites() { + const sites: { site: SiteMetadata; permission?: ExtensionPermission }[] = []; + + for (const [k, site] of this.siteRegistrations.entries()) { + const s: (typeof sites)[number] = { site }; + const permission = this.siteRules.get(k); + if (permission) { + s.permission = permission; + } + + sites.push(s); + } + + return sites; + } + + registerVendor(meta: VendorMetadata): this { + if (!this.vendorRegistrations.has(meta.id)) { + this.vendorRegistrations.set(meta.id, meta); + } + + return this; + } + + vendors() { + const vendors: { vendor: VendorMetadata; permission?: ExtensionPermission }[] = []; + + for (const [k, vendor] of this.vendorRegistrations.entries()) { + const s: (typeof vendors)[number] = { vendor }; + const permission = this.vendorRules.get(k); + if (permission) { + s.permission = permission; + } + + vendors.push(s); + } + + return vendors; + } + + setPermission(set: ExtensionSet, permission: ExtensionPermission): this { + if ("all" in set && set.all) { + this.allRule = permission; + } else if ("vendor" in set) { + this.vendorRules.set(set.vendor, permission); + } else if ("site" in set) { + this.siteRules.set(set.site, permission); + } else { + throw new Error(`Unrecognized integration set received: ${JSON.stringify(set)}.`); + } + + return this; + } + + permission(set: ExtensionSet) { + if ("all" in set && set.all) { + return this.allRule; + } else if ("vendor" in set) { + return this.vendorRules.get(set.vendor); + } else if ("site" in set) { + return this.siteRules.get(set.site); + } else { + return undefined; + } + } + + permissions() { + const rules: { set: ExtensionSet; permission: ExtensionPermission }[] = []; + rules.push({ set: { all: true }, permission: this.allRule }); + + for (const [site, permission] of this.siteRules.entries()) { + rules.push({ set: { site }, permission }); + } + + for (const [vendor, permission] of this.vendorRules.entries()) { + rules.push({ set: { vendor }, permission }); + } + + return rules; + } + + registerExtension(meta: ExtensionMetadata): this { + if (!this.siteRegistrations.has(meta.site.id)) { + throw new Error(`Unrecognized site: ${meta.site.id}`); + } else if (!this.vendorRegistrations.has(meta.product.vendor.id)) { + throw new Error(`Unrecognized vendor: ${meta.product.vendor.id}`); + } + + // is the integration registered? + const vendorMap = + this.vendorIntegrationsBySite.get(meta.site.id) ?? new Map(); + if (vendorMap.has(meta.product.vendor.id)) { + return; + } + + // if not, register it + const siteMap = + this.siteIntegrationsByVendor.get(meta.product.vendor.id) ?? new Map(); + const index = this.integrations.push(meta) - 1; + vendorMap.set(meta.product.vendor.id, index); + siteMap.set(meta.site.id, index); + + return this; + } + + build(id: SiteId): ExtensionSite | undefined { + const site = this.siteRegistrations.get(id); + if (!site) { + return undefined; + } + + if (this.allRule === "deny") { + return new ExtensionSite(site, new Map()); + } + + const extensions = new Map(); + const entries = this.vendorIntegrationsBySite.get(id)?.entries() ?? ([] as const); + for (const [vendor, maybeIndex] of entries) { + // prepare rules + const vendorRule = this.vendorRules.get(vendor) ?? this.allRule; + const siteRule = this.siteRules.get(id) ?? this.allRule; + const rules = [vendorRule, siteRule, this.allRule]; + + // evaluate rules + const extension = rules.includes("deny") + ? undefined + : rules.includes("allow") + ? this.integrations[maybeIndex] + : rules.includes("none") + ? undefined + : rules.includes("default") + ? this.integrations[maybeIndex] + : undefined; + + // the presence of an extension indicates it's accessible + if (extension) { + extensions.set(vendor, extension); + } + } + + const extensionSite = new ExtensionSite(site, extensions); + return extensionSite; + } +} diff --git a/libs/common/src/tools/extension/metadata/extension-registry.abstraction.ts b/libs/common/src/tools/extension/metadata/extension-registry.abstraction.ts new file mode 100644 index 00000000000..50d267d4214 --- /dev/null +++ b/libs/common/src/tools/extension/metadata/extension-registry.abstraction.ts @@ -0,0 +1,70 @@ +import { ExtensionSite } from "../extension-site"; + +import { SiteMetadata, ExtensionMetadata, ExtensionSet, VendorMetadata, SiteId } from "./type"; + +/** Permission levels for metadata. + * * default - unless a rule denies access, allow it. This is the + * default permission. + * * none - unless a rule allows access, deny it. + * * allow - access is explicitly granted to use an integration. + * * deny - access is explicitly prohibited for this integration. This + * rule overrides allow rules. + */ +export type ExtensionPermission = "default" | "none" | "allow" | "deny"; + +export abstract class ExtensionRegistry { + /** Registers a site supporting extensibility. + * @param site identifies the site being extended + * @param meta configures the extension site + * @return self for method chaining. + */ + registerSite: (meta: SiteMetadata) => this; + + /** List all registered extension sites with their integration rule, if any. + * @returns a list of all extension sites. `rule` is defined when the site + * is associated with an integration rule. + */ + sites: () => { site: SiteMetadata; permission?: ExtensionPermission }[]; + + /** Registers a vendor providing an integration + * @param site - identifies the site being extended + * @param meta - configures the extension site + * @return self for method chaining. + */ + registerVendor: (meta: VendorMetadata) => this; + + /** List all registered vendors with their integration rule, if any. + * @returns a list of all extension sites. `rule` is defined when the site + * is associated with an integration rule. + */ + vendors: () => { vendor: VendorMetadata; permission?: ExtensionPermission }[]; + + /** Registers an integration provided by a vendor to an extension site. + * The vendor and site MUST be registered before the integration. + * @param site - identifies the site being extended + * @param meta - configures the extension site + * @return self for method chaining. + */ + registerExtension: (meta: ExtensionMetadata) => this; + + /** Registers a rule. Only 1 rule can be registered for each integration set. + * The last-registered rule wins. + * @param set the collection of integrations affected by the rule + * @param permission the permission for the collection + * @return self for method chaining. + */ + setPermission: (set: ExtensionSet, permission: ExtensionPermission) => this; + + /** Retrieves the current rule for the given integration set or undefined if a rule + * doesn't exist. */ + permission: (set: ExtensionSet) => ExtensionPermission | undefined; + + /** Returns all registered integration rules. */ + permissions: () => { set: ExtensionSet; permission: ExtensionPermission }[]; + + /** Creates a point-in-time snapshot of the registry's contents with integration + * permissions applied for the provided SiteId. + * @returns the extension site, or `undefined` if the site is not registered. + */ + build: (id: SiteId) => ExtensionSite | undefined; +} diff --git a/libs/common/src/tools/integration/metadata/extension.ts b/libs/common/src/tools/extension/metadata/extension.ts similarity index 61% rename from libs/common/src/tools/integration/metadata/extension.ts rename to libs/common/src/tools/extension/metadata/extension.ts index 8b98dc31428..b2d95d59304 100644 --- a/libs/common/src/tools/integration/metadata/extension.ts +++ b/libs/common/src/tools/extension/metadata/extension.ts @@ -1,7 +1,7 @@ import { Field, Site } from "./data"; -import { ExtensionMetadata } from "./type"; +import { SiteMetadata } from "./type"; -export const Extension: Record = { +export const Extension: Record = { [Site.forwarder]: { id: Site.forwarder, availableFields: [Field.baseUrl, Field.domain, Field.prefix, Field.token], diff --git a/libs/common/src/tools/extension/metadata/factory.ts b/libs/common/src/tools/extension/metadata/factory.ts new file mode 100644 index 00000000000..bb83ce3b9e9 --- /dev/null +++ b/libs/common/src/tools/extension/metadata/factory.ts @@ -0,0 +1,36 @@ +import { AddyIo, AddyIoIntegrations } from "../data/addy-io"; +import { DuckDuckGo, DuckDuckGoIntegrations } from "../data/duck-duck-go"; +import { Fastmail, FastmailIntegrations } from "../data/fastmail"; +import { ForwardEmail, ForwardEmailIntegrations } from "../data/forward-email"; +import { Mozilla, MozillaIntegrations } from "../data/mozilla"; +import { SimpleLogin, SimpleLoginIntegrations } from "../data/simple-login"; + +import { DefaultIntegrationRegistry } from "./default-extension-registry"; +import { Extension } from "./extension"; +import { ExtensionMetadata, VendorMetadata } from "./type"; + +/** Constructs the integration registry */ +export function buildRegistry() { + // FIXME: find a better way to build the registry than a hard-coded factory function + function registerAll(vendor: VendorMetadata, integrations: ExtensionMetadata[]) { + registry.registerVendor(vendor); + for (const integration of integrations) { + registry.registerExtension(integration); + } + } + + const registry = new DefaultIntegrationRegistry(); + + for (const site of Reflect.ownKeys(Extension) as string[]) { + registry.registerSite(Extension[site]); + } + + registerAll(AddyIo, AddyIoIntegrations); + registerAll(DuckDuckGo, DuckDuckGoIntegrations); + registerAll(Fastmail, FastmailIntegrations); + registerAll(ForwardEmail, ForwardEmailIntegrations); + registerAll(Mozilla, MozillaIntegrations); + registerAll(SimpleLogin, SimpleLoginIntegrations); + + return registry; +} diff --git a/libs/common/src/tools/integration/metadata/type.ts b/libs/common/src/tools/extension/metadata/type.ts similarity index 54% rename from libs/common/src/tools/integration/metadata/type.ts rename to libs/common/src/tools/extension/metadata/type.ts index 9e277123dbc..616febe9ca6 100644 --- a/libs/common/src/tools/integration/metadata/type.ts +++ b/libs/common/src/tools/extension/metadata/type.ts @@ -1,33 +1,50 @@ import { FieldsBySite, Site, VendorsByExtension } from "./data"; /** well-known name for a feature extensible through an integration. */ -export type ExtensionSite = keyof typeof Site; +export type SiteId = keyof typeof Site; /** well-known name for a field surfaced from an extension site to a vendor. */ -export type DisclosedField = (typeof FieldsBySite)[ExtensionSite][number]; +export type DisclosedField = (typeof FieldsBySite)[SiteId][number]; /** Identifies a vendor integrated into bitwarden */ -export type VendorId = (typeof VendorsByExtension)[ExtensionSite][number]; +export type VendorId = (typeof VendorsByExtension)[SiteId][number]; /** The capabilities and descriptive content for an integration */ -export type ExtensionMetadata = { +export type SiteMetadata = { /** Uniquely identifies the integrator. */ - id: ExtensionSite; + id: SiteId; /** Lists the fields disclosed by the extension to the vendor */ availableFields: DisclosedField[]; }; +type TokenHeader = + | { + /** Transmit the token as the value of an `Authentication` header */ + authentication: true; + } + | { + /** Transmit the token as an `Authorization` header and a formatted value + * * `bearer` uses OAUTH-2.0 bearer token format + * * `token` prefixes the token with "Token" + * * `basic-username` uses HTTP Basic authentication format, encoding the + * token as the username. + */ + authorization: "bearer" | "token" | "basic-username"; + }; + /** Catalogues an integration's hosting status. * selfHost: "never" always uses the service's base URL * selfHost: "maybe" allows the user to override the service's * base URL with their own. * selfHost: "always" requires a base URL. */ -export type ApiHost = - | { selfHost: "never"; baseUrl: string } - | { selfHost: "maybe"; baseUrl: string } - | { selfHost: "always" }; +export type ApiHost = TokenHeader & + ( + | { selfHost: "never"; baseUrl: string } + | { selfHost: "maybe"; baseUrl: string } + | { selfHost: "always" } + ); /** The capabilities and descriptive content for an integration */ export type VendorMetadata = { @@ -39,9 +56,9 @@ export type VendorMetadata = { }; /** Describes an integration provided by a vendor */ -export type IntegrationMetadata = { +export type ExtensionMetadata = { /** The part of Bitwarden extended by the vendor's services */ - extension: ExtensionMetadata; + site: SiteMetadata; /** Product description */ product: { @@ -61,3 +78,21 @@ export type IntegrationMetadata = { */ requestedFields: DisclosedField[]; }; + +/** Identifies a collection of integrations. + */ +export type ExtensionSet = + | { + /** A set of integrations sharing an extension point */ + site: SiteId; + } + | { + /** A set of integrations sharing a vendor */ + vendor: VendorId; + } + | { + /** The total set of integrations. This is used to set a categorical + * rule affecting all integrations. + */ + all: true; + }; diff --git a/libs/common/src/tools/integration/integration-key.ts b/libs/common/src/tools/integration/integration-key.ts deleted file mode 100644 index 436e78da866..00000000000 --- a/libs/common/src/tools/integration/integration-key.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Jsonify } from "type-fest"; - -import { INTEGRATION_DISK } from "@bitwarden/common/platform/state"; - -import { PrivateClassifier } from "../private-classifier"; -import { ObjectKey } from "../state/object-key"; - -import { IntegrationRegistry } from "./metadata/registry"; -import { ExtensionSite, VendorId } from "./metadata/type"; - -export class IntegrationKey { - constructor( - private site: ExtensionSite, - private deserialize: (json: Jsonify) => T, - private initial?: T, - ) {} - - /** Constructs an object key - * @remarks this method should only be called by the integration service. - * It is not provided for general use. - */ - toObjectKey(vendor: VendorId, registry: IntegrationRegistry): ObjectKey { - const integration = registry.getIntegration(this.site, vendor); - - // an integrator can only store fields that are exported from the - // extension point *and* that were requested by the extension. All - // of the data stored by the extension is, for the moment, private. - const fields: any[] = integration.extension.availableFields.filter((available) => - integration.requestedFields.includes(available), - ); - const classifier = new PrivateClassifier(fields); - - const objectKey: ObjectKey = { - target: "object", - key: `${integration.extension.id}.${integration.product.vendor.id}`, - state: INTEGRATION_DISK, - classifier, - format: "classified", - options: { - deserializer: this.deserialize, - clearOn: ["logout"], - }, - initial: this.initial, - }; - - return objectKey; - } -} diff --git a/libs/common/src/tools/integration/integration.service.ts b/libs/common/src/tools/integration/integration.service.ts deleted file mode 100644 index dd46bb53df0..00000000000 --- a/libs/common/src/tools/integration/integration.service.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { BehaviorSubject, combineLatest, firstValueFrom, map, Observable } from "rxjs"; - -import { StateProvider } from "@bitwarden/common/platform/state"; - -import { LegacyEncryptorProvider } from "../cryptography/legacy-encryptor-provider"; -import { SingleUserDependency, SingleVendorDependency } from "../dependencies"; -import { UserStateSubject } from "../state/user-state-subject"; - -import { IntegrationKey } from "./integration-key"; -import { IntegrationRegistry } from "./metadata/registry"; -import { ExtensionSite, IntegrationMetadata } from "./metadata/type"; - -const DEFAULT_INTEGRATION_STORAGE_FRAME = 512; - -export class IntegrationService { - constructor( - private readonly registry: IntegrationRegistry, - private readonly stateProvider: StateProvider, - private readonly encryptorProvider: LegacyEncryptorProvider, - ) {} - - /** Observe the integrations available at an extension site */ - integrations$(site: ExtensionSite): Observable { - const integrations = this.registry.getIntegrations(site); - - // slot into a behavior subject so that the integration data is always available - // downstream; once dynamic registrations are supported this could be extended - // to publish updated lists without changing the signature - const integrations$ = new BehaviorSubject(integrations); - - return integrations$.asObservable(); - } - - /** Create a subject monitoring the integration's private data store */ - state$( - key: IntegrationKey, - dependencies: SingleUserDependency & SingleVendorDependency, - ): Promise> { - const singleUserEncryptor$ = this.encryptorProvider.userEncryptor$( - DEFAULT_INTEGRATION_STORAGE_FRAME, - dependencies, - ); - - const result$ = combineLatest([singleUserEncryptor$, dependencies.singleVendorId$]).pipe( - map(([encryptor, vendor]) => { - const subject = new UserStateSubject>( - key.toObjectKey(vendor, this.registry), - (key) => this.stateProvider.getUser(encryptor.userId, key), - { singleUserEncryptor$ }, - ); - - return subject; - }), - ); - - return firstValueFrom(result$); - } -} diff --git a/libs/common/src/tools/integration/metadata/factory.ts b/libs/common/src/tools/integration/metadata/factory.ts deleted file mode 100644 index 41a3c0214e7..00000000000 --- a/libs/common/src/tools/integration/metadata/factory.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Extension } from "./extension"; -import { IntegrationRegistry } from "./registry"; -import { IntegrationMetadata, VendorMetadata } from "./type"; -import { AddyIo, AddyIoIntegrations } from "./vendor/addy-io"; -import { DuckDuckGo, DuckDuckGoIntegrations } from "./vendor/duck-duck-go"; -import { Fastmail, FastmailIntegrations } from "./vendor/fastmail"; -import { ForwardEmail, ForwardEmailIntegrations } from "./vendor/forward-email"; -import { Mozilla, MozillaIntegrations } from "./vendor/mozilla"; -import { SimpleLogin, SimpleLoginIntegrations } from "./vendor/simple-login"; - -/** Constructs the integration registry */ -export function buildRegistry() { - // FIXME: find a better way to build the registry than a hard-coded factory function - function registerAll(vendor: VendorMetadata, integrations: IntegrationMetadata[]) { - registry.registerVendor(vendor); - for (const integration of integrations) { - registry.registerIntegration(integration); - } - } - - const registry = new IntegrationRegistry(); - - for (const site of Reflect.ownKeys(Extension) as string[]) { - registry.registerExtension(Extension[site]); - } - - registerAll(AddyIo, AddyIoIntegrations); - registerAll(DuckDuckGo, DuckDuckGoIntegrations); - registerAll(Fastmail, FastmailIntegrations); - registerAll(ForwardEmail, ForwardEmailIntegrations); - registerAll(Mozilla, MozillaIntegrations); - registerAll(SimpleLogin, SimpleLoginIntegrations); - - return registry; -} diff --git a/libs/common/src/tools/integration/metadata/registry.ts b/libs/common/src/tools/integration/metadata/registry.ts deleted file mode 100644 index 7708445520a..00000000000 --- a/libs/common/src/tools/integration/metadata/registry.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ExtensionPointId } from "../extension-point-id"; - -import { - ExtensionMetadata, - ExtensionSite, - IntegrationMetadata, - VendorId, - VendorMetadata, -} from "./type"; - -/** Tracks extension points and the integrations that extend them. */ -export class IntegrationRegistry { - private extensions = new Map(); - private vendors = new Map(); - private integrations = new Map>(); - - /** Registers a site supporting extensibility. - * @param site - identifies the site being extended - * @param meta - configures the extension site - * @return self for method chaining. - */ - registerExtension(meta: ExtensionMetadata): IntegrationRegistry { - if (!this.extensions.has(meta.id)) { - this.extensions.set(meta.id, meta); - } - - return this; - } - - getExtension(site: ExtensionPointId): ExtensionMetadata | undefined { - const extension = this.extensions.get(site); - return extension ? extension : undefined; - } - - /** Registers a site supporting extensibility. - * @param site - identifies the site being extended - * @param meta - configures the extension site - * @return self for method chaining. - */ - registerVendor(meta: VendorMetadata): IntegrationRegistry { - if (!this.vendors.has(meta.id)) { - this.vendors.set(meta.id, meta); - } - - return this; - } - - getVendor(vendorId: VendorId): VendorMetadata | undefined { - const vendor = this.vendors.get(vendorId); - return vendor ? vendor : undefined; - } - - /** Registers an integration provided by a vendor to an extension site. - * The vendor and site MUST be registered before the integration. - * @param site - identifies the site being extended - * @param meta - configures the extension site - * @return self for method chaining. - */ - registerIntegration(meta: IntegrationMetadata): IntegrationRegistry { - if (!this.extensions.has(meta.extension.id)) { - throw new Error(`Unrecognized site: ${meta.extension.id}`); - } else if (!this.vendors.has(meta.product.vendor.id)) { - throw new Error(`Unrecognized vendor: ${meta.product.vendor.id}`); - } - - let integrations = this.integrations.get(meta.extension.id); - if (!integrations) { - integrations = new Map(); - this.integrations.set(meta.extension.id, integrations); - } - - if (!integrations.has(meta.product.vendor.id)) { - integrations.set(meta.product.vendor.id, meta); - } - - return this; - } - - getIntegration(site: ExtensionPointId, vendor: VendorId): IntegrationMetadata | undefined { - return this.integrations.get(site)?.get(vendor) ?? undefined; - } - - getIntegrations(site: ExtensionPointId): IntegrationMetadata[] { - const integrations = Array.from(this.integrations.get(site)?.values() ?? []); - return integrations; - } -} diff --git a/libs/common/src/tools/integration/metadata/vendor/duck-duck-go.ts b/libs/common/src/tools/integration/metadata/vendor/duck-duck-go.ts deleted file mode 100644 index a3475a55fd3..00000000000 --- a/libs/common/src/tools/integration/metadata/vendor/duck-duck-go.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Field, Vendor } from "../data"; -import { Extension } from "../extension"; -import { IntegrationMetadata } from "../type"; - -export const DuckDuckGo = { - id: Vendor.duckduckgo, - name: "DuckDuckGo", -}; - -export const DuckDuckGoIntegrations: IntegrationMetadata[] = [ - { - extension: Extension.forwarder, - product: { - vendor: DuckDuckGo, - }, - host: { - selfHost: "never", - baseUrl: "https://quack.duckduckgo.com/api", - }, - requestedFields: [Field.token], - }, -]; diff --git a/libs/common/src/tools/util.ts b/libs/common/src/tools/util.ts new file mode 100644 index 00000000000..9a3a14c1c83 --- /dev/null +++ b/libs/common/src/tools/util.ts @@ -0,0 +1,19 @@ +/** Recursively freeze an object's own keys + * @param value the value to freeze + * @returns `value` + * @remarks this function is derived from MDN's `deepFreeze`, which + * has been committed to the public domain. + */ +export function deepFreeze(value: T): Readonly { + const keys = Reflect.ownKeys(value) as (keyof T)[]; + + for (const key of keys) { + const own = value[key]; + + if ((own && typeof own === "object") || typeof own === "function") { + deepFreeze(own); + } + } + + return Object.freeze(value); +} diff --git a/libs/tools/generator/core/src/integration/type.ts b/libs/tools/generator/core/src/integration/type.ts new file mode 100644 index 00000000000..86d84a51ce8 --- /dev/null +++ b/libs/tools/generator/core/src/integration/type.ts @@ -0,0 +1,49 @@ +import { RpcConfiguration } from "@bitwarden/common/tools/integration/rpc"; +import { GenerationRequest } from "@bitwarden/common/tools/types"; + +import { ForwarderContext } from "../engine"; +import { GeneratorMetadata } from "../metadata/generator-metadata"; +import { NoPolicy } from "../types"; + +/** Fields that may be requested by a forwarder integration */ +export type ForwarderSettings = { + /** bearer token that authenticates bitwarden to the forwarder. + * This is required to issue an API request. + */ + token: string; + + /** The base URL of the forwarder's API. + * When this is empty, the forwarder's default production API is used. + */ + baseUrl: string; + + /** 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@domain.io` + */ + domain: string; + + /** 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; +}; + +/** Forwarder-specific static definition */ +export type ForwarderMetadata = GeneratorMetadata & { + /** createForwardingEmail RPC definition */ + createForwardingEmail: RpcConfiguration< + GenerationRequest, + ForwarderContext, + string + >; + + /** getAccountId RPC definition; the response updates `accountId` which has a + * structural mixin type `RequestAccount`. + */ + getAccountId?: RpcConfiguration, string>; +}; diff --git a/libs/tools/generator/core/src/metadata/generator-metadata.ts b/libs/tools/generator/core/src/metadata/generator-metadata.ts index 6e393d65d04..1cc8aa42df9 100644 --- a/libs/tools/generator/core/src/metadata/generator-metadata.ts +++ b/libs/tools/generator/core/src/metadata/generator-metadata.ts @@ -47,6 +47,7 @@ export type GeneratorMetadata = AlgorithmMetadata & { }; }; + /** Defines parameters for policy transformations */ policy: { /** Combines multiple policies set by the administrative console into * a single policy.