mirror of
https://github.com/bitwarden/browser
synced 2026-02-09 05:00:10 +00:00
Revert "factor out generator metadata provider"
This reverts commit 3f40d72b5c.
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { VendorId } from "../extension";
|
||||
|
||||
import { ExtensionPointId } from "./extension-point-id";
|
||||
import { IntegrationId } from "./integration-id";
|
||||
|
||||
/** The capabilities and descriptive content for an integration */
|
||||
export type IntegrationMetadata = {
|
||||
/** Uniquely identifies the integrator. */
|
||||
id: IntegrationId;
|
||||
id: IntegrationId & VendorId;
|
||||
|
||||
/** Brand name of the integrator. */
|
||||
name: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import { ApiSettings } from "@bitwarden/common/tools/integration/rpc";
|
||||
|
||||
@@ -29,8 +30,11 @@ export const Integrations = Object.freeze({
|
||||
|
||||
const integrations = new Map(Object.values(Integrations).map((i) => [i.id, i]));
|
||||
|
||||
export function getForwarderConfiguration(id: IntegrationId): ForwarderConfiguration<ApiSettings> {
|
||||
const maybeForwarder = integrations.get(id);
|
||||
export function getForwarderConfiguration(
|
||||
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) {
|
||||
return maybeForwarder as ForwarderConfiguration<ApiSettings>;
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
GENERATOR_MEMORY,
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import {
|
||||
ApiSettings,
|
||||
@@ -100,7 +101,7 @@ const forwarder = Object.freeze({
|
||||
|
||||
export const AddyIo = Object.freeze({
|
||||
// integration
|
||||
id: "anonaddy" as IntegrationId,
|
||||
id: "anonaddy" as IntegrationId & VendorId,
|
||||
name: "Addy.io",
|
||||
extends: ["forwarder"],
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
GENERATOR_MEMORY,
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||
@@ -89,7 +90,7 @@ const forwarder = Object.freeze({
|
||||
|
||||
// integration-wide configuration
|
||||
export const DuckDuckGo = Object.freeze({
|
||||
id: "duckduckgo" as IntegrationId,
|
||||
id: "duckduckgo" as IntegrationId & VendorId,
|
||||
name: "DuckDuckGo",
|
||||
baseUrl: "https://quack.duckduckgo.com/api",
|
||||
selfHost: "never",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
GENERATOR_MEMORY,
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||
@@ -159,7 +160,7 @@ const forwarder = Object.freeze({
|
||||
|
||||
// integration-wide configuration
|
||||
export const Fastmail = Object.freeze({
|
||||
id: "fastmail" as IntegrationId,
|
||||
id: "fastmail" as IntegrationId & VendorId,
|
||||
name: "Fastmail",
|
||||
baseUrl: "https://api.fastmail.com",
|
||||
selfHost: "maybe",
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
GENERATOR_MEMORY,
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||
@@ -97,7 +98,7 @@ const forwarder = Object.freeze({
|
||||
|
||||
// integration-wide configuration
|
||||
export const FirefoxRelay = Object.freeze({
|
||||
id: "firefoxrelay" as IntegrationId,
|
||||
id: "firefoxrelay" as IntegrationId & VendorId,
|
||||
name: "Firefox Relay",
|
||||
baseUrl: "https://relay.firefox.com/api",
|
||||
selfHost: "never",
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
GENERATOR_MEMORY,
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||
@@ -101,7 +102,7 @@ const forwarder = Object.freeze({
|
||||
|
||||
export const ForwardEmail = Object.freeze({
|
||||
// integration metadata
|
||||
id: "forwardemail" as IntegrationId,
|
||||
id: "forwardemail" as IntegrationId & VendorId,
|
||||
name: "Forward Email",
|
||||
extends: ["forwarder"],
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
GENERATOR_MEMORY,
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import {
|
||||
ApiSettings,
|
||||
@@ -103,7 +104,7 @@ const forwarder = Object.freeze({
|
||||
|
||||
// integration-wide configuration
|
||||
export const SimpleLogin = Object.freeze({
|
||||
id: "simplelogin" as IntegrationId,
|
||||
id: "simplelogin" as IntegrationId & VendorId,
|
||||
name: "SimpleLogin",
|
||||
selfHost: "maybe",
|
||||
extends: ["forwarder"],
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { CredentialAlgorithm, CredentialType } from "./type";
|
||||
|
||||
type I18nKeyOrLiteral = string | { literal: string };
|
||||
|
||||
/** Credential generator metadata common across credential generators */
|
||||
export type AlgorithmMetadata = {
|
||||
/** Uniquely identifies the credential configuration
|
||||
@@ -23,25 +25,25 @@ export type AlgorithmMetadata = {
|
||||
/** Localization keys */
|
||||
i18nKeys: {
|
||||
/** descriptive name of the algorithm */
|
||||
name: string;
|
||||
name: I18nKeyOrLiteral;
|
||||
|
||||
/** explanatory text for the algorithm */
|
||||
description?: string;
|
||||
description?: I18nKeyOrLiteral;
|
||||
|
||||
/** labels the generate action */
|
||||
generateCredential: string;
|
||||
generateCredential: I18nKeyOrLiteral;
|
||||
|
||||
/** 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 */
|
||||
useCredential: string;
|
||||
useCredential: I18nKeyOrLiteral;
|
||||
|
||||
/** labels the generated output */
|
||||
credentialType: string;
|
||||
credentialType: I18nKeyOrLiteral;
|
||||
|
||||
/** labels the copy output action */
|
||||
copyCredential: string;
|
||||
copyCredential: I18nKeyOrLiteral;
|
||||
};
|
||||
|
||||
/** fine-tunings for generator user experiences */
|
||||
|
||||
@@ -19,11 +19,13 @@ describe("email - catchall generator metadata", () => {
|
||||
});
|
||||
|
||||
describe("profiles[account]", () => {
|
||||
let accountProfile: CoreProfileMetadata<CatchallGenerationOptions> = null;
|
||||
let accountProfile: CoreProfileMetadata<CatchallGenerationOptions> = null!;
|
||||
beforeEach(() => {
|
||||
const profile = catchall.profiles[Profile.account];
|
||||
if (isCoreProfile(profile)) {
|
||||
if (isCoreProfile(profile!)) {
|
||||
accountProfile = profile;
|
||||
} else {
|
||||
throw new Error("this branch should never run");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,75 @@
|
||||
// Forwarders are pending integration with the extension API
|
||||
//
|
||||
// They use the 300-block of weights and derive their metadata
|
||||
// using logic similar to `toCredentialGeneratorConfiguration`
|
||||
import { ExtensionMetadata, ExtensionStorageKey } from "@bitwarden/common/tools/extension/type";
|
||||
import { SelfHostedApiSettings } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||
|
||||
import { getForwarderConfiguration } from "../../data";
|
||||
import { EmailDomainSettings, EmailPrefixSettings } from "../../engine";
|
||||
import { Forwarder } from "../../engine/forwarder";
|
||||
import { GeneratorDependencyProvider } from "../../types";
|
||||
import { Profile, Type } from "../data";
|
||||
import { GeneratorMetadata } from "../generator-metadata";
|
||||
import { ForwarderProfileMetadata } from "../profile-metadata";
|
||||
|
||||
// These options are used by all forwarders; each forwarder uses a different set,
|
||||
// as defined by `GeneratorMetadata<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]", () => {
|
||||
let accountProfile: CoreProfileMetadata<SubaddressGenerationOptions> = null;
|
||||
let accountProfile: CoreProfileMetadata<SubaddressGenerationOptions> = null!;
|
||||
beforeEach(() => {
|
||||
const profile = plusAddress.profiles[Profile.account];
|
||||
if (isCoreProfile(profile)) {
|
||||
if (isCoreProfile(profile!)) {
|
||||
accountProfile = profile;
|
||||
} else {
|
||||
throw new Error("this branch should never run");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -22,19 +22,21 @@ describe("password - eff words generator metadata", () => {
|
||||
});
|
||||
|
||||
describe("profiles[account]", () => {
|
||||
let accountProfile: CoreProfileMetadata<PassphraseGenerationOptions> = null;
|
||||
let accountProfile: CoreProfileMetadata<PassphraseGenerationOptions> | null = null;
|
||||
beforeEach(() => {
|
||||
const profile = effPassphrase.profiles[Profile.account];
|
||||
if (isCoreProfile(profile)) {
|
||||
if (isCoreProfile(profile!)) {
|
||||
accountProfile = profile;
|
||||
} else {
|
||||
accountProfile = null;
|
||||
}
|
||||
});
|
||||
|
||||
describe("storage.options.deserializer", () => {
|
||||
it("returns its input", () => {
|
||||
const value: PassphraseGenerationOptions = { ...accountProfile.storage.initial };
|
||||
const value: PassphraseGenerationOptions = { ...accountProfile!.storage.initial };
|
||||
|
||||
const result = accountProfile.storage.options.deserializer(value);
|
||||
const result = accountProfile!.storage.options.deserializer(value);
|
||||
|
||||
expect(result).toBe(value);
|
||||
});
|
||||
@@ -46,15 +48,15 @@ describe("password - eff words generator metadata", () => {
|
||||
// enclosed behaviors change.
|
||||
|
||||
it("creates a passphrase policy constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
||||
const context = { defaultConstraints: accountProfile!.constraints.default };
|
||||
|
||||
const constraints = accountProfile.constraints.create([], context);
|
||||
const constraints = accountProfile!.constraints.create([], context);
|
||||
|
||||
expect(constraints).toBeInstanceOf(PassphrasePolicyConstraints);
|
||||
});
|
||||
|
||||
it("forwards the policy to the constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
||||
const context = { defaultConstraints: accountProfile!.constraints.default };
|
||||
const policies = [
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
@@ -66,13 +68,13 @@ describe("password - eff words generator metadata", () => {
|
||||
},
|
||||
] as Policy[];
|
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context);
|
||||
const constraints = accountProfile!.constraints.create(policies, context);
|
||||
|
||||
expect(constraints.constraints.numWords.min).toEqual(6);
|
||||
expect(constraints.constraints.numWords?.min).toEqual(6);
|
||||
});
|
||||
|
||||
it("combines multiple policies in the constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
||||
const context = { defaultConstraints: accountProfile!.constraints.default };
|
||||
const policies = [
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
@@ -92,10 +94,10 @@ describe("password - eff words generator metadata", () => {
|
||||
},
|
||||
] as Policy[];
|
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context);
|
||||
const constraints = accountProfile!.constraints.create(policies, context);
|
||||
|
||||
expect(constraints.constraints.numWords.min).toEqual(6);
|
||||
expect(constraints.constraints.capitalize.requiredValue).toEqual(true);
|
||||
expect(constraints.constraints.numWords?.min).toEqual(6);
|
||||
expect(constraints.constraints.capitalize?.requiredValue).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,11 +22,13 @@ describe("password - characters generator metadata", () => {
|
||||
});
|
||||
|
||||
describe("profiles[account]", () => {
|
||||
let accountProfile: CoreProfileMetadata<PasswordGenerationOptions> = null;
|
||||
let accountProfile: CoreProfileMetadata<PasswordGenerationOptions> = null!;
|
||||
beforeEach(() => {
|
||||
const profile = password.profiles[Profile.account];
|
||||
if (isCoreProfile(profile)) {
|
||||
if (isCoreProfile(profile!)) {
|
||||
accountProfile = profile;
|
||||
} else {
|
||||
throw new Error("this branch should never run");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,7 +71,7 @@ describe("password - characters generator metadata", () => {
|
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context);
|
||||
|
||||
expect(constraints.constraints.length.min).toEqual(10);
|
||||
expect(constraints.constraints.length?.min).toEqual(10);
|
||||
});
|
||||
|
||||
it("combines multiple policies in the constraints", () => {
|
||||
@@ -97,8 +99,8 @@ describe("password - characters generator metadata", () => {
|
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context);
|
||||
|
||||
expect(constraints.constraints.length.min).toEqual(14);
|
||||
expect(constraints.constraints.special.requiredValue).toEqual(true);
|
||||
expect(constraints.constraints.length?.min).toEqual(14);
|
||||
expect(constraints.constraints.special?.requiredValue).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,11 +20,13 @@ describe("username - eff words generator metadata", () => {
|
||||
});
|
||||
|
||||
describe("profiles[account]", () => {
|
||||
let accountProfile: CoreProfileMetadata<EffUsernameGenerationOptions> = null;
|
||||
let accountProfile: CoreProfileMetadata<EffUsernameGenerationOptions> = null!;
|
||||
beforeEach(() => {
|
||||
const profile = effWordList.profiles[Profile.account];
|
||||
if (isCoreProfile(profile)) {
|
||||
if (isCoreProfile(profile!)) {
|
||||
accountProfile = profile;
|
||||
} else {
|
||||
throw new Error("this branch should never run");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,13 +5,41 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
// implement ADR-0002
|
||||
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
|
||||
* @param policies the policies to reduce
|
||||
* @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
|
||||
.filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled)
|
||||
.reduce(
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import {
|
||||
Observable,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
first,
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BoundDependency } from "@bitwarden/common/tools/dependencies";
|
||||
import { ExtensionSite } from "@bitwarden/common/tools/extension";
|
||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import { anyComplete } from "@bitwarden/common/tools/rx";
|
||||
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
||||
|
||||
import {
|
||||
GeneratorMetadata,
|
||||
AlgorithmsByType,
|
||||
CredentialAlgorithm,
|
||||
CredentialType,
|
||||
isForwarderExtensionId,
|
||||
toForwarderMetadata,
|
||||
Type,
|
||||
} from "../metadata";
|
||||
import { availableAlgorithms_vNext } from "../policies/available-algorithms-policy";
|
||||
import { CredentialPreference } from "../types";
|
||||
|
||||
import { PREFERENCES } from "./credential-preferences";
|
||||
|
||||
type AlgorithmRequest = { algorithm: CredentialAlgorithm };
|
||||
type TypeRequest = { category: CredentialType };
|
||||
type MetadataRequest = Partial<AlgorithmRequest & TypeRequest>;
|
||||
|
||||
/** Surfaces contextual information to credential generators */
|
||||
export class GeneratorMetadataProvider {
|
||||
/** Instantiates the context provider
|
||||
* @param providers dependency injectors for user state subjects
|
||||
* @param policyService settings constraint lookups
|
||||
*/
|
||||
constructor(
|
||||
private readonly providers: UserStateSubjectDependencyProvider,
|
||||
private readonly system: SystemServiceProvider,
|
||||
algorithms: GeneratorMetadata<object>[],
|
||||
) {
|
||||
this.log = providers.log({ type: "GeneratorMetadataProvider" });
|
||||
|
||||
const site = system.extension.site("forwarder");
|
||||
if (!site) {
|
||||
this.log.panic("forwarder extension site not found");
|
||||
}
|
||||
this.site = site;
|
||||
|
||||
this.generators = new Map(algorithms.map((a) => [a.id, a] as const));
|
||||
}
|
||||
|
||||
private readonly site: ExtensionSite;
|
||||
private readonly log: SemanticLogger;
|
||||
|
||||
private generators: Map<CredentialAlgorithm, GeneratorMetadata<unknown & object>>;
|
||||
|
||||
// looks up a set of algorithms; does not enforce policy
|
||||
algorithms(requested: AlgorithmRequest): CredentialAlgorithm[];
|
||||
algorithms(requested: TypeRequest): CredentialAlgorithm[];
|
||||
algorithms(requested: MetadataRequest): CredentialAlgorithm[];
|
||||
algorithms(requested: MetadataRequest): CredentialAlgorithm[] {
|
||||
let algorithms: CredentialAlgorithm[];
|
||||
if (requested.category) {
|
||||
let forwarders: CredentialAlgorithm[] = [];
|
||||
if (requested.category === Type.email) {
|
||||
forwarders = Array.from(this.site.extensions.keys()).map((forwarder) => ({ forwarder }));
|
||||
}
|
||||
|
||||
algorithms = AlgorithmsByType[requested.category].concat(forwarders);
|
||||
} else if (requested.algorithm) {
|
||||
algorithms = [requested.algorithm];
|
||||
} else {
|
||||
this.log.panic(requested, "algorithm or category required");
|
||||
}
|
||||
|
||||
return algorithms;
|
||||
}
|
||||
|
||||
// emits a function that returns `true` when the input algorithm is available
|
||||
private isAvailable$(
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): Observable<(a: CredentialAlgorithm) => boolean> {
|
||||
const account$ = dependencies.account$.pipe(
|
||||
distinctUntilChanged((previous, current) => previous.id === current.id),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
const available$ = account$.pipe(
|
||||
switchMap((account) => {
|
||||
const policies$ = this.system.policy.getAll$(PolicyType.PasswordGenerator, account.id).pipe(
|
||||
map((p) => new Set(availableAlgorithms_vNext(p))),
|
||||
// complete policy emissions otherwise `switchMap` holds `algorithms$` open indefinitely
|
||||
takeUntil(anyComplete(account$)),
|
||||
);
|
||||
return policies$;
|
||||
}),
|
||||
map((available) => (a: CredentialAlgorithm) => isForwarderExtensionId(a) || available.has(a)),
|
||||
);
|
||||
|
||||
return available$;
|
||||
}
|
||||
|
||||
// looks up a set of algorithms; enforces policy - emits empty list when there's no algorithm available
|
||||
available$(
|
||||
requested: AlgorithmRequest,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): Observable<GeneratorMetadata<object>[]>;
|
||||
available$(
|
||||
requested: TypeRequest,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): Observable<GeneratorMetadata<object>[]>;
|
||||
available$(
|
||||
requested: MetadataRequest,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): Observable<GeneratorMetadata<object>[]> {
|
||||
let available$: Observable<CredentialAlgorithm[]>;
|
||||
if (requested.category) {
|
||||
const { category } = requested;
|
||||
|
||||
available$ = this.isAvailable$(dependencies).pipe(
|
||||
map((isAvailable) => AlgorithmsByType[category].filter(isAvailable)),
|
||||
);
|
||||
} else if (requested.algorithm) {
|
||||
const { algorithm } = requested;
|
||||
available$ = this.isAvailable$(dependencies).pipe(
|
||||
map((isAvailable) => (isAvailable(algorithm) ? [algorithm] : [])),
|
||||
);
|
||||
} else {
|
||||
this.log.panic(requested, "algorithm or category required");
|
||||
}
|
||||
|
||||
const result$ = available$.pipe(
|
||||
map((available) => available.map((algorithm) => this.getMetadata(algorithm))),
|
||||
);
|
||||
|
||||
return result$;
|
||||
}
|
||||
|
||||
// looks up a specific algorithm; enforces policy - observable completes without emission when there's no algorithm available.
|
||||
algorithm$(
|
||||
requested: AlgorithmRequest,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): Observable<GeneratorMetadata<object>>;
|
||||
algorithm$(
|
||||
requested: TypeRequest,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): Observable<GeneratorMetadata<object>>;
|
||||
algorithm$(
|
||||
requested: MetadataRequest,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): Observable<GeneratorMetadata<object>> {
|
||||
const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
|
||||
let algorithm$: Observable<CredentialAlgorithm | undefined>;
|
||||
if (requested.category) {
|
||||
this.log.debug(requested, "retrieving algorithm metadata by category");
|
||||
|
||||
const { category } = requested;
|
||||
algorithm$ = this.preferences({ account$ }).pipe(
|
||||
withLatestFrom(this.isAvailable$({ account$ })),
|
||||
map(([preferences, isAvailable]) => {
|
||||
let algorithm: CredentialAlgorithm | undefined = preferences[category].algorithm;
|
||||
if (isAvailable(algorithm)) {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
const algorithms = AlgorithmsByType[category];
|
||||
algorithm = algorithms.find(isAvailable)!;
|
||||
this.log.debug(
|
||||
{ algorithm, category },
|
||||
"preference not available; defaulting the generator algorithm",
|
||||
);
|
||||
|
||||
return algorithm;
|
||||
}),
|
||||
);
|
||||
} else if (requested.algorithm) {
|
||||
this.log.debug(requested, "retrieving algorithm metadata by algorithm");
|
||||
|
||||
const { algorithm } = requested;
|
||||
algorithm$ = this.isAvailable$({ account$ }).pipe(
|
||||
map((isAvailable) => (isAvailable(algorithm) ? algorithm : undefined)),
|
||||
first(),
|
||||
);
|
||||
} else {
|
||||
this.log.panic(requested, "algorithm or category required");
|
||||
}
|
||||
|
||||
const result$ = algorithm$.pipe(
|
||||
filter((value) => !!value),
|
||||
map((algorithm) => this.getMetadata(algorithm!)),
|
||||
);
|
||||
|
||||
return result$;
|
||||
}
|
||||
|
||||
private getMetadata(algorithm: CredentialAlgorithm) {
|
||||
let result = null;
|
||||
if (isForwarderExtensionId(algorithm)) {
|
||||
const extension = this.site.extensions.get(algorithm.forwarder);
|
||||
if (!extension) {
|
||||
this.log.panic(algorithm, "extension not found");
|
||||
}
|
||||
|
||||
result = toForwarderMetadata(extension);
|
||||
} else {
|
||||
result = this.generators.get(algorithm);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
this.log.panic({ algorithm }, "failed to load metadata");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Get a subject bound to credential generator preferences.
|
||||
* @param dependencies.account$ identifies the account to which the preferences are bound
|
||||
* @returns a subject bound to the user's preferences
|
||||
* @remarks Preferences determine which algorithms are used when generating a
|
||||
* credential from a credential category (e.g. `PassX` or `Username`). Preferences
|
||||
* should not be used to hold navigation history. Use @bitwarden/generator-navigation
|
||||
* instead.
|
||||
*/
|
||||
preferences(
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): UserStateSubject<CredentialPreference> {
|
||||
// FIXME: enforce policy
|
||||
const subject = new UserStateSubject(PREFERENCES, this.providers, dependencies);
|
||||
|
||||
return subject;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
|
||||
import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "../data/generator-types";
|
||||
import { AlgorithmsByType, CredentialType } from "../metadata";
|
||||
|
||||
/** A type of password that may be generated by the credential generator. */
|
||||
export type PasswordAlgorithm = (typeof PasswordAlgorithms)[number];
|
||||
@@ -11,7 +13,7 @@ export type UsernameAlgorithm = (typeof UsernameAlgorithms)[number];
|
||||
/** A type of email address that may be generated by the credential generator. */
|
||||
export type EmailAlgorithm = (typeof EmailAlgorithms)[number];
|
||||
|
||||
export type ForwarderIntegration = { forwarder: IntegrationId };
|
||||
export type ForwarderIntegration = { forwarder: IntegrationId & VendorId };
|
||||
|
||||
/** Returns true when the input algorithm is a forwarder integration. */
|
||||
export function isForwarderIntegration(
|
||||
@@ -74,8 +76,8 @@ export type CredentialCategory = keyof typeof CredentialCategories;
|
||||
/** The kind of credential to generate using a compound configuration. */
|
||||
// FIXME: extend the preferences to include a preferred forwarder
|
||||
export type CredentialPreference = {
|
||||
[Key in CredentialCategory]: {
|
||||
algorithm: (typeof CredentialCategories)[Key][number];
|
||||
[Key in CredentialType & CredentialCategory]: {
|
||||
algorithm: CredentialAlgorithm & (typeof AlgorithmsByType)[Key][number];
|
||||
updated: Date;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user