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.