mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-18665] introduce metadata provider (#13744)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||||
|
import { VendorId } from "../extension";
|
||||||
|
|
||||||
import { IntegrationContext } from "./integration-context";
|
import { IntegrationContext } from "./integration-context";
|
||||||
import { IntegrationId } from "./integration-id";
|
import { IntegrationId } from "./integration-id";
|
||||||
@@ -8,7 +9,7 @@ import { IntegrationMetadata } from "./integration-metadata";
|
|||||||
|
|
||||||
const EXAMPLE_META = Object.freeze({
|
const EXAMPLE_META = Object.freeze({
|
||||||
// arbitrary
|
// arbitrary
|
||||||
id: "simplelogin" as IntegrationId,
|
id: "simplelogin" as IntegrationId & VendorId,
|
||||||
name: "Example",
|
name: "Example",
|
||||||
// arbitrary
|
// arbitrary
|
||||||
extends: ["forwarder"],
|
extends: ["forwarder"],
|
||||||
@@ -34,7 +35,7 @@ describe("IntegrationContext", () => {
|
|||||||
|
|
||||||
it("throws when the baseurl isn't defined in metadata", () => {
|
it("throws when the baseurl isn't defined in metadata", () => {
|
||||||
const noBaseUrl: IntegrationMetadata = {
|
const noBaseUrl: IntegrationMetadata = {
|
||||||
id: "simplelogin" as IntegrationId, // arbitrary
|
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
|
||||||
name: "Example",
|
name: "Example",
|
||||||
extends: ["forwarder"], // arbitrary
|
extends: ["forwarder"], // arbitrary
|
||||||
selfHost: "maybe",
|
selfHost: "maybe",
|
||||||
@@ -56,7 +57,7 @@ describe("IntegrationContext", () => {
|
|||||||
|
|
||||||
it("ignores settings when selfhost is 'never'", () => {
|
it("ignores settings when selfhost is 'never'", () => {
|
||||||
const selfHostNever: IntegrationMetadata = {
|
const selfHostNever: IntegrationMetadata = {
|
||||||
id: "simplelogin" as IntegrationId, // arbitrary
|
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
|
||||||
name: "Example",
|
name: "Example",
|
||||||
extends: ["forwarder"], // arbitrary
|
extends: ["forwarder"], // arbitrary
|
||||||
baseUrl: "example.com",
|
baseUrl: "example.com",
|
||||||
@@ -71,7 +72,7 @@ describe("IntegrationContext", () => {
|
|||||||
|
|
||||||
it("always reads the settings when selfhost is 'always'", () => {
|
it("always reads the settings when selfhost is 'always'", () => {
|
||||||
const selfHostAlways: IntegrationMetadata = {
|
const selfHostAlways: IntegrationMetadata = {
|
||||||
id: "simplelogin" as IntegrationId, // arbitrary
|
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
|
||||||
name: "Example",
|
name: "Example",
|
||||||
extends: ["forwarder"], // arbitrary
|
extends: ["forwarder"], // arbitrary
|
||||||
baseUrl: "example.com",
|
baseUrl: "example.com",
|
||||||
@@ -86,7 +87,7 @@ describe("IntegrationContext", () => {
|
|||||||
|
|
||||||
it("fails when the settings are empty and selfhost is 'always'", () => {
|
it("fails when the settings are empty and selfhost is 'always'", () => {
|
||||||
const selfHostAlways: IntegrationMetadata = {
|
const selfHostAlways: IntegrationMetadata = {
|
||||||
id: "simplelogin" as IntegrationId, // arbitrary
|
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
|
||||||
name: "Example",
|
name: "Example",
|
||||||
extends: ["forwarder"], // arbitrary
|
extends: ["forwarder"], // arbitrary
|
||||||
baseUrl: "example.com",
|
baseUrl: "example.com",
|
||||||
@@ -101,7 +102,7 @@ describe("IntegrationContext", () => {
|
|||||||
|
|
||||||
it("reads from the metadata by default when selfhost is 'maybe'", () => {
|
it("reads from the metadata by default when selfhost is 'maybe'", () => {
|
||||||
const selfHostMaybe: IntegrationMetadata = {
|
const selfHostMaybe: IntegrationMetadata = {
|
||||||
id: "simplelogin" as IntegrationId, // arbitrary
|
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
|
||||||
name: "Example",
|
name: "Example",
|
||||||
extends: ["forwarder"], // arbitrary
|
extends: ["forwarder"], // arbitrary
|
||||||
baseUrl: "example.com",
|
baseUrl: "example.com",
|
||||||
@@ -117,7 +118,7 @@ describe("IntegrationContext", () => {
|
|||||||
|
|
||||||
it("overrides the metadata when selfhost is 'maybe'", () => {
|
it("overrides the metadata when selfhost is 'maybe'", () => {
|
||||||
const selfHostMaybe: IntegrationMetadata = {
|
const selfHostMaybe: IntegrationMetadata = {
|
||||||
id: "simplelogin" as IntegrationId, // arbitrary
|
id: "simplelogin" as IntegrationId & VendorId, // arbitrary
|
||||||
name: "Example",
|
name: "Example",
|
||||||
extends: ["forwarder"], // arbitrary
|
extends: ["forwarder"], // arbitrary
|
||||||
baseUrl: "example.com",
|
baseUrl: "example.com",
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { VendorId } from "../extension";
|
||||||
|
|
||||||
import { ExtensionPointId } from "./extension-point-id";
|
import { ExtensionPointId } from "./extension-point-id";
|
||||||
import { IntegrationId } from "./integration-id";
|
import { IntegrationId } from "./integration-id";
|
||||||
|
|
||||||
/** The capabilities and descriptive content for an integration */
|
/** The capabilities and descriptive content for an integration */
|
||||||
export type IntegrationMetadata = {
|
export type IntegrationMetadata = {
|
||||||
/** Uniquely identifies the integrator. */
|
/** Uniquely identifies the integrator. */
|
||||||
id: IntegrationId;
|
id: IntegrationId & VendorId;
|
||||||
|
|
||||||
/** Brand name of the integrator. */
|
/** Brand name of the integrator. */
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ export class DisabledSemanticLogger implements SemanticLogger {
|
|||||||
|
|
||||||
error<T>(_content: Jsonify<T>, _message?: string): void {}
|
error<T>(_content: Jsonify<T>, _message?: string): void {}
|
||||||
|
|
||||||
panic<T>(_content: Jsonify<T>, message?: string): never {
|
panic<T>(content: Jsonify<T>, message?: string): never {
|
||||||
|
if (typeof content === "string" && !message) {
|
||||||
|
throw new Error(content);
|
||||||
|
} else {
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface SemanticLogger {
|
|||||||
*/
|
*/
|
||||||
debug(message: string): void;
|
debug(message: string): void;
|
||||||
|
|
||||||
|
// FIXME: replace Jsonify<T> parameter with structural logging schema type
|
||||||
/** Logs the content at debug priority.
|
/** Logs the content at debug priority.
|
||||||
* Debug messages are used for diagnostics, and are typically disabled
|
* Debug messages are used for diagnostics, and are typically disabled
|
||||||
* in production builds.
|
* in production builds.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||||
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import { ApiSettings } from "@bitwarden/common/tools/integration/rpc";
|
import { ApiSettings } from "@bitwarden/common/tools/integration/rpc";
|
||||||
|
|
||||||
@@ -29,8 +30,11 @@ export const Integrations = Object.freeze({
|
|||||||
|
|
||||||
const integrations = new Map(Object.values(Integrations).map((i) => [i.id, i]));
|
const integrations = new Map(Object.values(Integrations).map((i) => [i.id, i]));
|
||||||
|
|
||||||
export function getForwarderConfiguration(id: IntegrationId): ForwarderConfiguration<ApiSettings> {
|
export function getForwarderConfiguration(
|
||||||
const maybeForwarder = integrations.get(id);
|
id: IntegrationId | VendorId,
|
||||||
|
): ForwarderConfiguration<ApiSettings> {
|
||||||
|
// these casts are for compatibility; `IntegrationId` is the old form of `VendorId`
|
||||||
|
const maybeForwarder = integrations.get(id as string as IntegrationId & VendorId);
|
||||||
|
|
||||||
if (maybeForwarder && "forwarder" in maybeForwarder) {
|
if (maybeForwarder && "forwarder" in maybeForwarder) {
|
||||||
return maybeForwarder as ForwarderConfiguration<ApiSettings>;
|
return maybeForwarder as ForwarderConfiguration<ApiSettings>;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
GENERATOR_MEMORY,
|
GENERATOR_MEMORY,
|
||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
|
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import {
|
import {
|
||||||
ApiSettings,
|
ApiSettings,
|
||||||
@@ -100,7 +101,7 @@ const forwarder = Object.freeze({
|
|||||||
|
|
||||||
export const AddyIo = Object.freeze({
|
export const AddyIo = Object.freeze({
|
||||||
// integration
|
// integration
|
||||||
id: "anonaddy" as IntegrationId,
|
id: "anonaddy" as IntegrationId & VendorId,
|
||||||
name: "Addy.io",
|
name: "Addy.io",
|
||||||
extends: ["forwarder"],
|
extends: ["forwarder"],
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
GENERATOR_MEMORY,
|
GENERATOR_MEMORY,
|
||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
|
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||||
@@ -89,7 +90,7 @@ const forwarder = Object.freeze({
|
|||||||
|
|
||||||
// integration-wide configuration
|
// integration-wide configuration
|
||||||
export const DuckDuckGo = Object.freeze({
|
export const DuckDuckGo = Object.freeze({
|
||||||
id: "duckduckgo" as IntegrationId,
|
id: "duckduckgo" as IntegrationId & VendorId,
|
||||||
name: "DuckDuckGo",
|
name: "DuckDuckGo",
|
||||||
baseUrl: "https://quack.duckduckgo.com/api",
|
baseUrl: "https://quack.duckduckgo.com/api",
|
||||||
selfHost: "never",
|
selfHost: "never",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
GENERATOR_MEMORY,
|
GENERATOR_MEMORY,
|
||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
|
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||||
@@ -159,7 +160,7 @@ const forwarder = Object.freeze({
|
|||||||
|
|
||||||
// integration-wide configuration
|
// integration-wide configuration
|
||||||
export const Fastmail = Object.freeze({
|
export const Fastmail = Object.freeze({
|
||||||
id: "fastmail" as IntegrationId,
|
id: "fastmail" as IntegrationId & VendorId,
|
||||||
name: "Fastmail",
|
name: "Fastmail",
|
||||||
baseUrl: "https://api.fastmail.com",
|
baseUrl: "https://api.fastmail.com",
|
||||||
selfHost: "maybe",
|
selfHost: "maybe",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
GENERATOR_MEMORY,
|
GENERATOR_MEMORY,
|
||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
|
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||||
@@ -97,7 +98,7 @@ const forwarder = Object.freeze({
|
|||||||
|
|
||||||
// integration-wide configuration
|
// integration-wide configuration
|
||||||
export const FirefoxRelay = Object.freeze({
|
export const FirefoxRelay = Object.freeze({
|
||||||
id: "firefoxrelay" as IntegrationId,
|
id: "firefoxrelay" as IntegrationId & VendorId,
|
||||||
name: "Firefox Relay",
|
name: "Firefox Relay",
|
||||||
baseUrl: "https://relay.firefox.com/api",
|
baseUrl: "https://relay.firefox.com/api",
|
||||||
selfHost: "never",
|
selfHost: "never",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
GENERATOR_MEMORY,
|
GENERATOR_MEMORY,
|
||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
|
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||||
@@ -101,7 +102,7 @@ const forwarder = Object.freeze({
|
|||||||
|
|
||||||
export const ForwardEmail = Object.freeze({
|
export const ForwardEmail = Object.freeze({
|
||||||
// integration metadata
|
// integration metadata
|
||||||
id: "forwardemail" as IntegrationId,
|
id: "forwardemail" as IntegrationId & VendorId,
|
||||||
name: "Forward Email",
|
name: "Forward Email",
|
||||||
extends: ["forwarder"],
|
extends: ["forwarder"],
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
GENERATOR_MEMORY,
|
GENERATOR_MEMORY,
|
||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
|
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import {
|
import {
|
||||||
ApiSettings,
|
ApiSettings,
|
||||||
@@ -103,7 +104,7 @@ const forwarder = Object.freeze({
|
|||||||
|
|
||||||
// integration-wide configuration
|
// integration-wide configuration
|
||||||
export const SimpleLogin = Object.freeze({
|
export const SimpleLogin = Object.freeze({
|
||||||
id: "simplelogin" as IntegrationId,
|
id: "simplelogin" as IntegrationId & VendorId,
|
||||||
name: "SimpleLogin",
|
name: "SimpleLogin",
|
||||||
selfHost: "maybe",
|
selfHost: "maybe",
|
||||||
extends: ["forwarder"],
|
extends: ["forwarder"],
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { CredentialAlgorithm, CredentialType } from "./type";
|
import { CredentialAlgorithm, CredentialType } from "./type";
|
||||||
|
|
||||||
|
type I18nKeyOrLiteral = string | { literal: string };
|
||||||
|
|
||||||
/** Credential generator metadata common across credential generators */
|
/** Credential generator metadata common across credential generators */
|
||||||
export type AlgorithmMetadata = {
|
export type AlgorithmMetadata = {
|
||||||
/** Uniquely identifies the credential configuration
|
/** Uniquely identifies the credential configuration
|
||||||
@@ -23,25 +25,25 @@ export type AlgorithmMetadata = {
|
|||||||
/** Localization keys */
|
/** Localization keys */
|
||||||
i18nKeys: {
|
i18nKeys: {
|
||||||
/** descriptive name of the algorithm */
|
/** descriptive name of the algorithm */
|
||||||
name: string;
|
name: I18nKeyOrLiteral;
|
||||||
|
|
||||||
/** explanatory text for the algorithm */
|
/** explanatory text for the algorithm */
|
||||||
description?: string;
|
description?: I18nKeyOrLiteral;
|
||||||
|
|
||||||
/** labels the generate action */
|
/** labels the generate action */
|
||||||
generateCredential: string;
|
generateCredential: I18nKeyOrLiteral;
|
||||||
|
|
||||||
/** message informing users when the generator produces a new credential */
|
/** message informing users when the generator produces a new credential */
|
||||||
credentialGenerated: string;
|
credentialGenerated: I18nKeyOrLiteral;
|
||||||
|
|
||||||
/* labels the action that assigns a generated value to a domain object */
|
/* labels the action that assigns a generated value to a domain object */
|
||||||
useCredential: string;
|
useCredential: I18nKeyOrLiteral;
|
||||||
|
|
||||||
/** labels the generated output */
|
/** labels the generated output */
|
||||||
credentialType: string;
|
credentialType: I18nKeyOrLiteral;
|
||||||
|
|
||||||
/** labels the copy output action */
|
/** labels the copy output action */
|
||||||
copyCredential: string;
|
copyCredential: I18nKeyOrLiteral;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** fine-tunings for generator user experiences */
|
/** fine-tunings for generator user experiences */
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ describe("email - catchall generator metadata", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("profiles[account]", () => {
|
describe("profiles[account]", () => {
|
||||||
let accountProfile: CoreProfileMetadata<CatchallGenerationOptions> = null;
|
let accountProfile: CoreProfileMetadata<CatchallGenerationOptions> = null!;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const profile = catchall.profiles[Profile.account];
|
const profile = catchall.profiles[Profile.account];
|
||||||
if (isCoreProfile(profile)) {
|
if (isCoreProfile(profile!)) {
|
||||||
accountProfile = profile;
|
accountProfile = profile;
|
||||||
|
} else {
|
||||||
|
throw new Error("this branch should never run");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,75 @@
|
|||||||
// Forwarders are pending integration with the extension API
|
import { ExtensionMetadata, ExtensionStorageKey } from "@bitwarden/common/tools/extension/type";
|
||||||
//
|
import { SelfHostedApiSettings } from "@bitwarden/common/tools/integration/rpc";
|
||||||
// They use the 300-block of weights and derive their metadata
|
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||||
// using logic similar to `toCredentialGeneratorConfiguration`
|
|
||||||
|
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<T>.capabilities.fields`.
|
||||||
|
type ForwarderOptions = Partial<EmailDomainSettings & EmailPrefixSettings & SelfHostedApiSettings>;
|
||||||
|
|
||||||
|
// update the extension metadata
|
||||||
|
export function toForwarderMetadata(
|
||||||
|
extension: ExtensionMetadata,
|
||||||
|
): GeneratorMetadata<ForwarderOptions> {
|
||||||
|
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<ForwarderOptions> = {
|
||||||
|
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<ForwarderOptions>,
|
||||||
|
constraints: {
|
||||||
|
default: {},
|
||||||
|
create() {
|
||||||
|
return new IdentityConstraint<ForwarderOptions>();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies ForwarderProfileMetadata<ForwarderOptions>,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return generator;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ describe("email - plus address generator metadata", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("profiles[account]", () => {
|
describe("profiles[account]", () => {
|
||||||
let accountProfile: CoreProfileMetadata<SubaddressGenerationOptions> = null;
|
let accountProfile: CoreProfileMetadata<SubaddressGenerationOptions> = null!;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const profile = plusAddress.profiles[Profile.account];
|
const profile = plusAddress.profiles[Profile.account];
|
||||||
if (isCoreProfile(profile)) {
|
if (isCoreProfile(profile!)) {
|
||||||
accountProfile = profile;
|
accountProfile = profile;
|
||||||
|
} else {
|
||||||
|
throw new Error("this branch should never run");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
import { AlgorithmsByType as ABT } from "./data";
|
import {
|
||||||
|
Algorithm as AlgorithmData,
|
||||||
|
AlgorithmsByType as AlgorithmsByTypeData,
|
||||||
|
Type as TypeData,
|
||||||
|
} from "./data";
|
||||||
import { CredentialType, CredentialAlgorithm } from "./type";
|
import { CredentialType, CredentialAlgorithm } from "./type";
|
||||||
|
|
||||||
// `CredentialAlgorithm` is defined in terms of `ABT`; supplying
|
// `CredentialAlgorithm` is defined in terms of `ABT`; supplying
|
||||||
// type information in the barrel file breaks a circular dependency.
|
// type information in the barrel file breaks a circular dependency.
|
||||||
/** Credential generation algorithms grouped by purpose. */
|
/** Credential generation algorithms grouped by purpose. */
|
||||||
export const AlgorithmsByType: Record<CredentialType, ReadonlyArray<CredentialAlgorithm>> = ABT;
|
export const AlgorithmsByType: Record<
|
||||||
|
CredentialType,
|
||||||
|
ReadonlyArray<CredentialAlgorithm>
|
||||||
|
> = AlgorithmsByTypeData;
|
||||||
|
export const Algorithms: ReadonlyArray<CredentialAlgorithm> = Object.freeze(
|
||||||
|
Object.values(AlgorithmData),
|
||||||
|
);
|
||||||
|
export const Types: ReadonlyArray<CredentialType> = Object.freeze(Object.values(TypeData));
|
||||||
|
|
||||||
export { Profile, Type } from "./data";
|
export { Profile, Type, Algorithm } from "./data";
|
||||||
|
export { toForwarderMetadata } from "./email/forwarder";
|
||||||
export { GeneratorMetadata } from "./generator-metadata";
|
export { GeneratorMetadata } from "./generator-metadata";
|
||||||
export { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "./profile-metadata";
|
export { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "./profile-metadata";
|
||||||
export { GeneratorProfile, CredentialAlgorithm, CredentialType } from "./type";
|
export { GeneratorProfile, CredentialAlgorithm, CredentialType } from "./type";
|
||||||
|
|||||||
@@ -22,19 +22,21 @@ describe("password - eff words generator metadata", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("profiles[account]", () => {
|
describe("profiles[account]", () => {
|
||||||
let accountProfile: CoreProfileMetadata<PassphraseGenerationOptions> = null;
|
let accountProfile: CoreProfileMetadata<PassphraseGenerationOptions> | null = null;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const profile = effPassphrase.profiles[Profile.account];
|
const profile = effPassphrase.profiles[Profile.account];
|
||||||
if (isCoreProfile(profile)) {
|
if (isCoreProfile(profile!)) {
|
||||||
accountProfile = profile;
|
accountProfile = profile;
|
||||||
|
} else {
|
||||||
|
accountProfile = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("storage.options.deserializer", () => {
|
describe("storage.options.deserializer", () => {
|
||||||
it("returns its input", () => {
|
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);
|
expect(result).toBe(value);
|
||||||
});
|
});
|
||||||
@@ -46,15 +48,15 @@ describe("password - eff words generator metadata", () => {
|
|||||||
// enclosed behaviors change.
|
// enclosed behaviors change.
|
||||||
|
|
||||||
it("creates a passphrase policy constraints", () => {
|
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);
|
expect(constraints).toBeInstanceOf(PassphrasePolicyConstraints);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("forwards the policy to the constraints", () => {
|
it("forwards the policy to the constraints", () => {
|
||||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
const context = { defaultConstraints: accountProfile!.constraints.default };
|
||||||
const policies = [
|
const policies = [
|
||||||
{
|
{
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
@@ -66,13 +68,13 @@ describe("password - eff words generator metadata", () => {
|
|||||||
},
|
},
|
||||||
] as Policy[];
|
] 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", () => {
|
it("combines multiple policies in the constraints", () => {
|
||||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
const context = { defaultConstraints: accountProfile!.constraints.default };
|
||||||
const policies = [
|
const policies = [
|
||||||
{
|
{
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
@@ -92,10 +94,10 @@ describe("password - eff words generator metadata", () => {
|
|||||||
},
|
},
|
||||||
] as Policy[];
|
] 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);
|
||||||
expect(constraints.constraints.capitalize.requiredValue).toEqual(true);
|
expect(constraints.constraints.capitalize?.requiredValue).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ describe("password - characters generator metadata", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("profiles[account]", () => {
|
describe("profiles[account]", () => {
|
||||||
let accountProfile: CoreProfileMetadata<PasswordGenerationOptions> = null;
|
let accountProfile: CoreProfileMetadata<PasswordGenerationOptions> = null!;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const profile = password.profiles[Profile.account];
|
const profile = password.profiles[Profile.account];
|
||||||
if (isCoreProfile(profile)) {
|
if (isCoreProfile(profile!)) {
|
||||||
accountProfile = profile;
|
accountProfile = profile;
|
||||||
|
} else {
|
||||||
|
throw new Error("this branch should never run");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ describe("password - characters generator metadata", () => {
|
|||||||
|
|
||||||
const constraints = accountProfile.constraints.create(policies, context);
|
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", () => {
|
it("combines multiple policies in the constraints", () => {
|
||||||
@@ -97,8 +99,8 @@ describe("password - characters generator metadata", () => {
|
|||||||
|
|
||||||
const constraints = accountProfile.constraints.create(policies, context);
|
const constraints = accountProfile.constraints.create(policies, context);
|
||||||
|
|
||||||
expect(constraints.constraints.length.min).toEqual(14);
|
expect(constraints.constraints.length?.min).toEqual(14);
|
||||||
expect(constraints.constraints.special.requiredValue).toEqual(true);
|
expect(constraints.constraints.special?.requiredValue).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ describe("username - eff words generator metadata", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("profiles[account]", () => {
|
describe("profiles[account]", () => {
|
||||||
let accountProfile: CoreProfileMetadata<EffUsernameGenerationOptions> = null;
|
let accountProfile: CoreProfileMetadata<EffUsernameGenerationOptions> = null!;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const profile = effWordList.profiles[Profile.account];
|
const profile = effWordList.profiles[Profile.account];
|
||||||
if (isCoreProfile(profile)) {
|
if (isCoreProfile(profile!)) {
|
||||||
accountProfile = profile;
|
accountProfile = profile;
|
||||||
|
} else {
|
||||||
|
throw new Error("this branch should never run");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,41 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
|||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
|
|
||||||
import { CredentialAlgorithm, EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "..";
|
import {
|
||||||
|
CredentialAlgorithm as LegacyAlgorithm,
|
||||||
|
EmailAlgorithms,
|
||||||
|
PasswordAlgorithms,
|
||||||
|
UsernameAlgorithms,
|
||||||
|
} from "..";
|
||||||
|
import { CredentialAlgorithm } from "../metadata";
|
||||||
|
|
||||||
/** Reduces policies to a set of available algorithms
|
/** Reduces policies to a set of available algorithms
|
||||||
* @param policies the policies to reduce
|
* @param policies the policies to reduce
|
||||||
* @returns the resulting `AlgorithmAvailabilityPolicy`
|
* @returns the resulting `AlgorithmAvailabilityPolicy`
|
||||||
*/
|
*/
|
||||||
export function availableAlgorithms(policies: Policy[]): CredentialAlgorithm[] {
|
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[] {
|
||||||
const overridePassword = policies
|
const overridePassword = policies
|
||||||
.filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled)
|
.filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled)
|
||||||
.reduce(
|
.reduce(
|
||||||
|
|||||||
@@ -0,0 +1,438 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject, ReplaySubject, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
|
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
|
||||||
|
import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction";
|
||||||
|
import {
|
||||||
|
ExtensionMetadata,
|
||||||
|
ExtensionSite,
|
||||||
|
Site,
|
||||||
|
SiteId,
|
||||||
|
SiteMetadata,
|
||||||
|
} from "@bitwarden/common/tools/extension";
|
||||||
|
import { ExtensionService } from "@bitwarden/common/tools/extension/extension.service";
|
||||||
|
import { Bitwarden } from "@bitwarden/common/tools/extension/vendor/bitwarden";
|
||||||
|
import { disabledSemanticLoggerProvider } from "@bitwarden/common/tools/log";
|
||||||
|
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||||
|
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||||
|
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
||||||
|
import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { FakeAccountService, FakeStateProvider } from "../../../../../common/spec";
|
||||||
|
import { Algorithm, AlgorithmsByType, CredentialAlgorithm, Type, Types } from "../metadata";
|
||||||
|
import catchall from "../metadata/email/catchall";
|
||||||
|
import plusAddress from "../metadata/email/plus-address";
|
||||||
|
import passphrase from "../metadata/password/eff-word-list";
|
||||||
|
import password from "../metadata/password/random-password";
|
||||||
|
import effWordList from "../metadata/username/eff-word-list";
|
||||||
|
import { CredentialPreference } from "../types";
|
||||||
|
|
||||||
|
import { PREFERENCES } from "./credential-preferences";
|
||||||
|
import { GeneratorMetadataProvider } from "./generator-metadata-provider";
|
||||||
|
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
const SomeAccount = {
|
||||||
|
id: SomeUser,
|
||||||
|
email: "someone@example.com",
|
||||||
|
emailVerified: true,
|
||||||
|
name: "Someone",
|
||||||
|
};
|
||||||
|
const SomeAccount$ = new BehaviorSubject<Account>(SomeAccount);
|
||||||
|
|
||||||
|
const SomeEncryptor: UserEncryptor = {
|
||||||
|
userId: SomeUser,
|
||||||
|
|
||||||
|
encrypt(secret) {
|
||||||
|
const tmp: any = secret;
|
||||||
|
return Promise.resolve({ foo: `encrypt(${tmp.foo})` } as any);
|
||||||
|
},
|
||||||
|
|
||||||
|
decrypt(secret) {
|
||||||
|
const tmp: any = JSON.parse(secret.encryptedString!);
|
||||||
|
return Promise.resolve({ foo: `decrypt(${tmp.foo})` } as any);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SomeAccountService = new FakeAccountService({
|
||||||
|
[SomeUser]: SomeAccount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const SomeStateProvider = new FakeStateProvider(SomeAccountService);
|
||||||
|
|
||||||
|
const SystemProvider = {
|
||||||
|
encryptor: {
|
||||||
|
userEncryptor$: () => {
|
||||||
|
return new BehaviorSubject({ encryptor: SomeEncryptor, userId: SomeUser }).asObservable();
|
||||||
|
},
|
||||||
|
organizationEncryptor$() {
|
||||||
|
throw new Error("`organizationEncryptor$` should never be invoked.");
|
||||||
|
},
|
||||||
|
} as LegacyEncryptorProvider,
|
||||||
|
state: SomeStateProvider,
|
||||||
|
log: disabledSemanticLoggerProvider,
|
||||||
|
} as UserStateSubjectDependencyProvider;
|
||||||
|
|
||||||
|
const SomeSiteId: SiteId = Site.forwarder;
|
||||||
|
|
||||||
|
const SomeSite: SiteMetadata = Object.freeze({
|
||||||
|
id: SomeSiteId,
|
||||||
|
availableFields: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const SomePolicyService = mock<PolicyService>();
|
||||||
|
|
||||||
|
const SomeExtensionService = mock<ExtensionService>();
|
||||||
|
|
||||||
|
const ApplicationProvider = {
|
||||||
|
/** Policy configured by the administrative console */
|
||||||
|
policy: SomePolicyService,
|
||||||
|
|
||||||
|
/** Client extension metadata and profile access */
|
||||||
|
extension: SomeExtensionService,
|
||||||
|
|
||||||
|
/** Event monitoring and diagnostic interfaces */
|
||||||
|
log: disabledSemanticLoggerProvider,
|
||||||
|
} as SystemServiceProvider;
|
||||||
|
|
||||||
|
describe("GeneratorMetadataProvider", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
SomeExtensionService.site.mockImplementation(() => new ExtensionSite(SomeSite, new Map()));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("constructor", () => {
|
||||||
|
it("throws when the forwarder site isn't defined by the extension service", () => {
|
||||||
|
SomeExtensionService.site.mockReturnValue(undefined);
|
||||||
|
expect(() => new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, [])).toThrow(
|
||||||
|
"forwarder extension site not found",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("metadata", () => {
|
||||||
|
it("returns algorithm metadata", async () => {
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, [
|
||||||
|
password,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const metadata = provider.metadata(password.id);
|
||||||
|
|
||||||
|
expect(metadata).toEqual(password);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns forwarder metadata", async () => {
|
||||||
|
const extensionMetadata: ExtensionMetadata = {
|
||||||
|
site: SomeSite,
|
||||||
|
product: { vendor: Bitwarden },
|
||||||
|
host: { authentication: true, selfHost: "maybe", baseUrl: "https://www.example.com" },
|
||||||
|
requestedFields: [],
|
||||||
|
};
|
||||||
|
const application = {
|
||||||
|
...ApplicationProvider,
|
||||||
|
extension: mock<ExtensionService>({
|
||||||
|
site: () => new ExtensionSite(SomeSite, new Map([[Bitwarden.id, extensionMetadata]])),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, application, []);
|
||||||
|
|
||||||
|
const metadata = provider.metadata({ forwarder: Bitwarden.id });
|
||||||
|
|
||||||
|
expect(metadata.id).toEqual({ forwarder: Bitwarden.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("panics when metadata not found", async () => {
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
|
|
||||||
|
expect(() => provider.metadata("not found" as any)).toThrow("metadata not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("panics when an extension not found", async () => {
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
|
|
||||||
|
expect(() => provider.metadata({ forwarder: "not found" as any })).toThrow(
|
||||||
|
"extension not found",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("types", () => {
|
||||||
|
it("returns the credential types", async () => {
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
|
|
||||||
|
const result = provider.types();
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.arrayContaining(Types));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("algorithms", () => {
|
||||||
|
it("returns the password category's algorithms", () => {
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
|
|
||||||
|
const result = provider.algorithms({ type: Type.password });
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.arrayContaining(AlgorithmsByType[Type.password]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the username category's algorithms", () => {
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
|
|
||||||
|
const result = provider.algorithms({ type: Type.username });
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.arrayContaining(AlgorithmsByType[Type.username]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the email category's algorithms", () => {
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
|
|
||||||
|
const result = provider.algorithms({ type: Type.email });
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.arrayContaining(AlgorithmsByType[Type.email]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes forwarder vendors in the email category's algorithms", () => {
|
||||||
|
const extensionMetadata: ExtensionMetadata = {
|
||||||
|
site: SomeSite,
|
||||||
|
product: { vendor: Bitwarden },
|
||||||
|
host: { authentication: true, selfHost: "maybe", baseUrl: "https://www.example.com" },
|
||||||
|
requestedFields: [],
|
||||||
|
};
|
||||||
|
const application = {
|
||||||
|
...ApplicationProvider,
|
||||||
|
extension: mock<ExtensionService>({
|
||||||
|
site: () => new ExtensionSite(SomeSite, new Map([[Bitwarden.id, extensionMetadata]])),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, application, []);
|
||||||
|
|
||||||
|
const result = provider.algorithms({ type: Type.email });
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.arrayContaining([{ forwarder: Bitwarden.id }]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[Algorithm.catchall],
|
||||||
|
[Algorithm.passphrase],
|
||||||
|
[Algorithm.password],
|
||||||
|
[Algorithm.plusAddress],
|
||||||
|
[Algorithm.username],
|
||||||
|
])("returns explicit algorithms (=%p)", (algorithm) => {
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
|
|
||||||
|
const result = provider.algorithms({ algorithm });
|
||||||
|
|
||||||
|
expect(result).toEqual([algorithm]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns explicit forwarders", () => {
|
||||||
|
const extensionMetadata: ExtensionMetadata = {
|
||||||
|
site: SomeSite,
|
||||||
|
product: { vendor: Bitwarden },
|
||||||
|
host: { authentication: true, selfHost: "maybe", baseUrl: "https://www.example.com" },
|
||||||
|
requestedFields: [],
|
||||||
|
};
|
||||||
|
const application = {
|
||||||
|
...ApplicationProvider,
|
||||||
|
extension: mock<ExtensionService>({
|
||||||
|
site: () => new ExtensionSite(SomeSite, new Map([[Bitwarden.id, extensionMetadata]])),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, application, []);
|
||||||
|
|
||||||
|
const result = provider.algorithms({ algorithm: { forwarder: Bitwarden.id } });
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.arrayContaining([{ forwarder: Bitwarden.id }]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array when the algorithm is invalid", () => {
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
|
|
||||||
|
// `any` cast required because this test subverts the type system
|
||||||
|
const result = provider.algorithms({ algorithm: "an invalid algorithm" as any });
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array when the forwarder is invalid", () => {
|
||||||
|
const extensionMetadata: ExtensionMetadata = {
|
||||||
|
site: SomeSite,
|
||||||
|
product: { vendor: Bitwarden },
|
||||||
|
host: { authentication: true, selfHost: "maybe", baseUrl: "https://www.example.com" },
|
||||||
|
requestedFields: [],
|
||||||
|
};
|
||||||
|
const application = {
|
||||||
|
...ApplicationProvider,
|
||||||
|
extension: mock<ExtensionService>({
|
||||||
|
site: () => new ExtensionSite(SomeSite, new Map([[Bitwarden.id, extensionMetadata]])),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, application, []);
|
||||||
|
|
||||||
|
// `any` cast required because this test subverts the type system
|
||||||
|
const result = provider.algorithms({
|
||||||
|
algorithm: { forwarder: "an invalid forwarder" as any },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("panics when neither an algorithm nor a category is specified", () => {
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
|
|
||||||
|
// `any` cast required because this test subverts the type system
|
||||||
|
expect(() => provider.algorithms({} as any)).toThrow("algorithm or type required");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("algorithms$", () => {
|
||||||
|
it.each([
|
||||||
|
[Algorithm.catchall, catchall],
|
||||||
|
[Algorithm.username, effWordList],
|
||||||
|
[Algorithm.password, password],
|
||||||
|
])("gets a specific algorithm", async (algorithm, metadata) => {
|
||||||
|
SomePolicyService.getAll$.mockReturnValue(new BehaviorSubject([]));
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, [
|
||||||
|
metadata,
|
||||||
|
]);
|
||||||
|
const result = new ReplaySubject<CredentialAlgorithm[]>(1);
|
||||||
|
|
||||||
|
provider.algorithms$({ algorithm }, { account$: SomeAccount$ }).subscribe(result);
|
||||||
|
|
||||||
|
await expect(firstValueFrom(result)).resolves.toEqual([algorithm]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[Type.email, [catchall, plusAddress]],
|
||||||
|
[Type.username, [effWordList]],
|
||||||
|
[Type.password, [password, passphrase]],
|
||||||
|
])("gets a category of algorithms", async (category, metadata) => {
|
||||||
|
SomePolicyService.getAll$.mockReturnValue(new BehaviorSubject([]));
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, metadata);
|
||||||
|
const result = new ReplaySubject<CredentialAlgorithm[]>(1);
|
||||||
|
|
||||||
|
provider.algorithms$({ type: category }, { account$: SomeAccount$ }).subscribe(result);
|
||||||
|
|
||||||
|
const expectedAlgorithms = expect.arrayContaining(metadata.map((m) => m.id));
|
||||||
|
await expect(firstValueFrom(result)).resolves.toEqual(expectedAlgorithms);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits algorithms blocked by policy", async () => {
|
||||||
|
const policy = new Policy({
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
enabled: true,
|
||||||
|
data: {
|
||||||
|
overridePasswordType: Algorithm.password,
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
SomePolicyService.getAll$.mockReturnValue(new BehaviorSubject([policy]));
|
||||||
|
const metadata = [password, passphrase];
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, metadata);
|
||||||
|
const algorithmResult = new ReplaySubject<CredentialAlgorithm[]>(1);
|
||||||
|
const categoryResult = new ReplaySubject<CredentialAlgorithm[]>(1);
|
||||||
|
|
||||||
|
provider
|
||||||
|
.algorithms$({ algorithm: Algorithm.passphrase }, { account$: SomeAccount$ })
|
||||||
|
.subscribe(algorithmResult);
|
||||||
|
provider
|
||||||
|
.algorithms$({ type: Type.password }, { account$: SomeAccount$ })
|
||||||
|
.subscribe(categoryResult);
|
||||||
|
|
||||||
|
await expect(firstValueFrom(algorithmResult)).resolves.toEqual([]);
|
||||||
|
await expect(firstValueFrom(categoryResult)).resolves.toEqual([password.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits algorithms whose metadata is unavailable", async () => {
|
||||||
|
SomePolicyService.getAll$.mockReturnValue(new BehaviorSubject([]));
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, [
|
||||||
|
password,
|
||||||
|
]);
|
||||||
|
const algorithmResult = new ReplaySubject<CredentialAlgorithm[]>(1);
|
||||||
|
const categoryResult = new ReplaySubject<CredentialAlgorithm[]>(1);
|
||||||
|
|
||||||
|
provider
|
||||||
|
.algorithms$({ algorithm: Algorithm.passphrase }, { account$: SomeAccount$ })
|
||||||
|
.subscribe(algorithmResult);
|
||||||
|
provider
|
||||||
|
.algorithms$({ type: Type.password }, { account$: SomeAccount$ })
|
||||||
|
.subscribe(categoryResult);
|
||||||
|
|
||||||
|
await expect(firstValueFrom(algorithmResult)).resolves.toEqual([]);
|
||||||
|
await expect(firstValueFrom(categoryResult)).resolves.toEqual([password.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("panics when neither algorithm nor category are specified", () => {
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
|
|
||||||
|
expect(() => provider.algorithms$({} as any, { account$: SomeAccount$ })).toThrow(
|
||||||
|
"algorithm or type required",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("preference$", () => {
|
||||||
|
const preferences: CredentialPreference = deepFreeze({
|
||||||
|
[Type.email]: { algorithm: Algorithm.catchall, updated: new Date() },
|
||||||
|
[Type.username]: { algorithm: Algorithm.username, updated: new Date() },
|
||||||
|
[Type.password]: { algorithm: Algorithm.password, updated: new Date() },
|
||||||
|
});
|
||||||
|
beforeEach(async () => {
|
||||||
|
await SomeStateProvider.setUserState(PREFERENCES, preferences, SomeAccount.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[Type.email, catchall],
|
||||||
|
[Type.username, effWordList],
|
||||||
|
[Type.password, password],
|
||||||
|
])("emits the user's %s preference", async (type, metadata) => {
|
||||||
|
SomePolicyService.getAll$.mockReturnValue(new BehaviorSubject([]));
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, [
|
||||||
|
metadata,
|
||||||
|
]);
|
||||||
|
const result = new ReplaySubject<CredentialAlgorithm | undefined>(1);
|
||||||
|
|
||||||
|
provider.preference$(type, { account$: SomeAccount$ }).subscribe(result);
|
||||||
|
|
||||||
|
await expect(firstValueFrom(result)).resolves.toEqual(preferences[type].algorithm);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits a default when the user's preference is unavailable", async () => {
|
||||||
|
SomePolicyService.getAll$.mockReturnValue(new BehaviorSubject([]));
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, [
|
||||||
|
plusAddress,
|
||||||
|
]);
|
||||||
|
const result = new ReplaySubject<CredentialAlgorithm | undefined>(1);
|
||||||
|
|
||||||
|
// precondition: the preferred email is excluded from the provided metadata
|
||||||
|
expect(preferences.email.algorithm).not.toEqual(plusAddress.id);
|
||||||
|
|
||||||
|
provider.preference$(Type.email, { account$: SomeAccount$ }).subscribe(result);
|
||||||
|
|
||||||
|
await expect(firstValueFrom(result)).resolves.toEqual(plusAddress.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits undefined when the user's preference is unavailable and there is no metadata", async () => {
|
||||||
|
SomePolicyService.getAll$.mockReturnValue(new BehaviorSubject([]));
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
|
const result = new ReplaySubject<CredentialAlgorithm | undefined>(1);
|
||||||
|
|
||||||
|
provider.preference$(Type.email, { account$: SomeAccount$ }).subscribe(result);
|
||||||
|
|
||||||
|
await expect(firstValueFrom(result)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("preferences", () => {
|
||||||
|
it("returns a user state subject", () => {
|
||||||
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
|
|
||||||
|
const subject = provider.preferences({ account$: SomeAccount$ });
|
||||||
|
|
||||||
|
expect(subject).toBeInstanceOf(UserStateSubject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
combineLatestWith,
|
||||||
|
distinctUntilChanged,
|
||||||
|
map,
|
||||||
|
shareReplay,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
} 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, pin } 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,
|
||||||
|
Algorithms,
|
||||||
|
Types,
|
||||||
|
} from "../metadata";
|
||||||
|
import { availableAlgorithms_vNext } from "../policies/available-algorithms-policy";
|
||||||
|
import { CredentialPreference } from "../types";
|
||||||
|
import {
|
||||||
|
AlgorithmRequest,
|
||||||
|
TypeRequest,
|
||||||
|
MetadataRequest,
|
||||||
|
isAlgorithmRequest,
|
||||||
|
isTypeRequest,
|
||||||
|
} from "../types/metadata-request";
|
||||||
|
|
||||||
|
import { PREFERENCES } from "./credential-preferences";
|
||||||
|
|
||||||
|
/** Surfaces contextual information to credential generators */
|
||||||
|
export class GeneratorMetadataProvider {
|
||||||
|
/** Instantiates the context provider
|
||||||
|
* @param system dependency providers for user state subjects
|
||||||
|
* @param application dependency providers for system services
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private readonly system: UserStateSubjectDependencyProvider,
|
||||||
|
private readonly application: SystemServiceProvider,
|
||||||
|
algorithms: ReadonlyArray<GeneratorMetadata<object>>,
|
||||||
|
) {
|
||||||
|
this.log = system.log({ type: "GeneratorMetadataProvider" });
|
||||||
|
|
||||||
|
const site = application.extension.site("forwarder");
|
||||||
|
if (!site) {
|
||||||
|
this.log.panic("forwarder extension site not found");
|
||||||
|
}
|
||||||
|
this.site = site;
|
||||||
|
|
||||||
|
this._metadata = new Map(algorithms.map((a) => [a.id, a] as const));
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly site: ExtensionSite;
|
||||||
|
private readonly log: SemanticLogger;
|
||||||
|
|
||||||
|
private _metadata: Map<CredentialAlgorithm, GeneratorMetadata<unknown & object>>;
|
||||||
|
|
||||||
|
/** Retrieve an algorithm's generator metadata
|
||||||
|
* @param algorithm identifies the algorithm
|
||||||
|
* @returns the algorithm's generator metadata
|
||||||
|
* @throws when the algorithm doesn't identify a known metadata entry
|
||||||
|
*/
|
||||||
|
metadata(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._metadata.get(algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
this.log.panic({ algorithm }, "metadata not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** retrieve credential types */
|
||||||
|
types(): ReadonlyArray<CredentialType> {
|
||||||
|
return Types;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieve the credential algorithm ids that match the request.
|
||||||
|
* @param requested when this has a `type` property, the method
|
||||||
|
* returns all algorithms with the same credential type. When this has an `algorithm`
|
||||||
|
* property, the method returns 0 or 1 matching algorithms.
|
||||||
|
* @returns the matching algorithms. This method always returns an array;
|
||||||
|
* the array is empty when no algorithms match the input criteria.
|
||||||
|
* @throws when neither `requested.algorithm` nor `requested.type` contains
|
||||||
|
* a value.
|
||||||
|
* @remarks this method enforces technical requirements only.
|
||||||
|
* If you want these algorithms with policy controls applied, use `algorithms$`.
|
||||||
|
*/
|
||||||
|
algorithms(requested: AlgorithmRequest): CredentialAlgorithm[];
|
||||||
|
algorithms(requested: TypeRequest): CredentialAlgorithm[];
|
||||||
|
algorithms(requested: MetadataRequest): CredentialAlgorithm[] {
|
||||||
|
let algorithms: CredentialAlgorithm[];
|
||||||
|
if (isTypeRequest(requested)) {
|
||||||
|
let forwarders: CredentialAlgorithm[] = [];
|
||||||
|
if (requested.type === Type.email) {
|
||||||
|
forwarders = Array.from(this.site.extensions.keys()).map((forwarder) => ({ forwarder }));
|
||||||
|
}
|
||||||
|
|
||||||
|
algorithms = AlgorithmsByType[requested.type].concat(forwarders);
|
||||||
|
} else if (isAlgorithmRequest(requested) && isForwarderExtensionId(requested.algorithm)) {
|
||||||
|
algorithms = this.site.extensions.has(requested.algorithm.forwarder)
|
||||||
|
? [requested.algorithm]
|
||||||
|
: [];
|
||||||
|
} else if (isAlgorithmRequest(requested)) {
|
||||||
|
algorithms = Algorithms.includes(requested.algorithm) ? [requested.algorithm] : [];
|
||||||
|
} else {
|
||||||
|
this.log.panic(requested, "algorithm or type 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 id$ = dependencies.account$.pipe(
|
||||||
|
map((account) => account.id),
|
||||||
|
pin(),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const available$ = id$.pipe(
|
||||||
|
switchMap((id) => {
|
||||||
|
const policies$ = this.application.policy.getAll$(PolicyType.PasswordGenerator, id).pipe(
|
||||||
|
map((p) => availableAlgorithms_vNext(p).filter((a) => this._metadata.has(a))),
|
||||||
|
map((p) => new Set(p)),
|
||||||
|
// complete policy emissions otherwise `switchMap` holds `available$` open indefinitely
|
||||||
|
takeUntil(anyComplete(id$)),
|
||||||
|
);
|
||||||
|
return policies$;
|
||||||
|
}),
|
||||||
|
map(
|
||||||
|
(available) =>
|
||||||
|
function (a: CredentialAlgorithm) {
|
||||||
|
return isForwarderExtensionId(a) || available.has(a);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return available$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retrieve credential algorithms filtered by the user's active policy.
|
||||||
|
* @param requested when this has a `type` property, the method
|
||||||
|
* returns all algorithms with a matching credential type. When this has an `algorithm`
|
||||||
|
* property, the method returns 0 or 1 matching algorithms.
|
||||||
|
* @param dependencies.account the account requesting algorithm access;
|
||||||
|
* this parameter controls which policy, if any, is applied.
|
||||||
|
* @returns an observable that emits matching algorithms. When no algorithms
|
||||||
|
* match the request, an empty array is emitted.
|
||||||
|
* @throws when neither `requested.algorithm` nor `requested.type` contains
|
||||||
|
* a value.
|
||||||
|
* @remarks this method applies policy controls. In particular, it excludes
|
||||||
|
* algorithms prohibited by a policy control. If you want lists of algorithms
|
||||||
|
* supported by the client, use `algorithms`.
|
||||||
|
*/
|
||||||
|
algorithms$(
|
||||||
|
requested: AlgorithmRequest,
|
||||||
|
dependencies: BoundDependency<"account", Account>,
|
||||||
|
): Observable<CredentialAlgorithm[]>;
|
||||||
|
algorithms$(
|
||||||
|
requested: TypeRequest,
|
||||||
|
dependencies: BoundDependency<"account", Account>,
|
||||||
|
): Observable<CredentialAlgorithm[]>;
|
||||||
|
algorithms$(
|
||||||
|
requested: MetadataRequest,
|
||||||
|
dependencies: BoundDependency<"account", Account>,
|
||||||
|
): Observable<CredentialAlgorithm[]> {
|
||||||
|
if (isTypeRequest(requested)) {
|
||||||
|
const { type } = requested;
|
||||||
|
return this.isAvailable$(dependencies).pipe(
|
||||||
|
map((isAvailable) => this.algorithms({ type }).filter(isAvailable)),
|
||||||
|
);
|
||||||
|
} else if (isAlgorithmRequest(requested)) {
|
||||||
|
const { algorithm } = requested;
|
||||||
|
return this.isAvailable$(dependencies).pipe(
|
||||||
|
map((isAvailable) => (isAvailable(algorithm) ? [algorithm] : [])),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.log.panic(requested, "algorithm or type required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preference$(type: CredentialType, dependencies: BoundDependency<"account", Account>) {
|
||||||
|
const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||||
|
|
||||||
|
const algorithm$ = this.preferences({ account$ }).pipe(
|
||||||
|
combineLatestWith(this.isAvailable$({ account$ })),
|
||||||
|
map(([preferences, isAvailable]) => {
|
||||||
|
const algorithm: CredentialAlgorithm = preferences[type].algorithm;
|
||||||
|
if (isAvailable(algorithm)) {
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
const algorithms = type ? this.algorithms({ type: type }) : [];
|
||||||
|
// `?? null` because logging types must be `Jsonify<T>`
|
||||||
|
const defaultAlgorithm = algorithms.find(isAvailable) ?? null;
|
||||||
|
this.log.debug(
|
||||||
|
{ algorithm, defaultAlgorithm, credentialType: type },
|
||||||
|
"preference not available; defaulting the generator algorithm",
|
||||||
|
);
|
||||||
|
|
||||||
|
// `?? undefined` so that interface is ADR-14 compliant
|
||||||
|
return defaultAlgorithm ?? undefined;
|
||||||
|
}),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return algorithm$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 type (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<CredentialPreference> {
|
||||||
|
// FIXME: enforce policy
|
||||||
|
const subject = new UserStateSubject(PREFERENCES, this.system, dependencies);
|
||||||
|
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -133,7 +133,9 @@ export type CredentialGeneratorConfiguration<Settings, Policy> = CredentialGener
|
|||||||
};
|
};
|
||||||
/** Defines the stored parameters for credential generation */
|
/** Defines the stored parameters for credential generation */
|
||||||
settings: {
|
settings: {
|
||||||
/** value used when an account's settings haven't been initialized */
|
/** value used when an account's settings haven't been initialized
|
||||||
|
* @deprecated use `ObjectKey.initial` for your desired storage property instead
|
||||||
|
*/
|
||||||
initial: Readonly<Partial<Settings>>;
|
initial: Readonly<Partial<Settings>>;
|
||||||
|
|
||||||
/** Application-global constraints that apply to account settings */
|
/** Application-global constraints that apply to account settings */
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||||
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
|
|
||||||
import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "../data/generator-types";
|
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. */
|
/** A type of password that may be generated by the credential generator. */
|
||||||
export type PasswordAlgorithm = (typeof PasswordAlgorithms)[number];
|
export type PasswordAlgorithm = (typeof PasswordAlgorithms)[number];
|
||||||
@@ -11,7 +13,7 @@ export type UsernameAlgorithm = (typeof UsernameAlgorithms)[number];
|
|||||||
/** A type of email address that may be generated by the credential generator. */
|
/** A type of email address that may be generated by the credential generator. */
|
||||||
export type EmailAlgorithm = (typeof EmailAlgorithms)[number];
|
export type EmailAlgorithm = (typeof EmailAlgorithms)[number];
|
||||||
|
|
||||||
export type ForwarderIntegration = { forwarder: IntegrationId };
|
export type ForwarderIntegration = { forwarder: IntegrationId & VendorId };
|
||||||
|
|
||||||
/** Returns true when the input algorithm is a forwarder integration. */
|
/** Returns true when the input algorithm is a forwarder integration. */
|
||||||
export function isForwarderIntegration(
|
export function isForwarderIntegration(
|
||||||
@@ -74,8 +76,8 @@ export type CredentialCategory = keyof typeof CredentialCategories;
|
|||||||
/** The kind of credential to generate using a compound configuration. */
|
/** The kind of credential to generate using a compound configuration. */
|
||||||
// FIXME: extend the preferences to include a preferred forwarder
|
// FIXME: extend the preferences to include a preferred forwarder
|
||||||
export type CredentialPreference = {
|
export type CredentialPreference = {
|
||||||
[Key in CredentialCategory]: {
|
[Key in CredentialType & CredentialCategory]: {
|
||||||
algorithm: (typeof CredentialCategories)[Key][number];
|
algorithm: CredentialAlgorithm & (typeof AlgorithmsByType)[Key][number];
|
||||||
updated: Date;
|
updated: Date;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
13
libs/tools/generator/core/src/types/metadata-request.ts
Normal file
13
libs/tools/generator/core/src/types/metadata-request.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { CredentialAlgorithm, CredentialType } from "../metadata";
|
||||||
|
|
||||||
|
export type AlgorithmRequest = { algorithm: CredentialAlgorithm };
|
||||||
|
export type TypeRequest = { type: CredentialType };
|
||||||
|
export type MetadataRequest = Partial<AlgorithmRequest & TypeRequest>;
|
||||||
|
|
||||||
|
export function isAlgorithmRequest(request: MetadataRequest): request is AlgorithmRequest {
|
||||||
|
return !!request.algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTypeRequest(request: MetadataRequest): request is TypeRequest {
|
||||||
|
return !!request.type;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user