Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-16788] revise generator metadata #12757

Merged
merged 9 commits into from
Jan 16, 2025
61 changes: 61 additions & 0 deletions libs/tools/generator/core/src/metadata/algorithm-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { CredentialAlgorithm, CredentialType } from "./type";

/** Credential generator metadata common across credential generators */
export type AlgorithmMetadata = {
/** Uniquely identifies the credential configuration
* @example
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
* // to pattern test whether the credential describes a forwarder algorithm
* const meta : AlgorithmMetadata = // ...
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {};
*/
id: CredentialAlgorithm;

/** The kind of credential generated by this configuration */
category: CredentialType;

/** Used to order credential algorithms for display purposes.
* Items with lesser weights appear before entries with greater
* weights (i.e. ascending sort).
*/
weight: number;

/** Localization keys */
i18nKeys: {
/** descriptive name of the algorithm */
name: string;

/** explanatory text for the algorithm */
description?: string;

/** labels the generate action */
generateCredential: string;

/** message informing users when the generator produces a new credential */
credentialGenerated: string;

/* labels the action that assigns a generated value to a domain object */
useCredential: string;

/** labels the generated output */
credentialType: string;

/** labels the copy output action */
copyCredential: string;
};

/** fine-tunings for generator user experiences */
capabilities: {
/** `true` when the generator supports autogeneration
* @remarks this property is useful when credential generation
* carries side effects, such as configuring a service external
* to Bitwarden.
*/
autogenerate: boolean;

/** Well-known fields to display on the options panel or collect from the environment.
* @remarks: at present, this is only used by forwarders
*/
fields: string[];
};
};
48 changes: 48 additions & 0 deletions libs/tools/generator/core/src/metadata/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { deepFreeze } from "@bitwarden/common/tools/util";

/** algorithms for generating credentials */
export const Algorithm = Object.freeze({
/** A password composed of random characters */
password: "password",

/** A password composed of random words from the EFF word list */
passphrase: "passphrase",

/** A username composed of words from the EFF word list */
username: "username",

/** An email username composed of random characters */
catchall: "catchall",

/** An email username composed of words from the EFF word list */
plusAddress: "subaddress",
} as const);

/** categorizes credentials according to their use-case outside of Bitwarden */
export const Type = Object.freeze({
password: "password",
username: "username",
email: "email",
} as const);

/** categorizes settings according to their expected use-case within Bitwarden */
export const Profile = Object.freeze({
/** account-level generator options. This is the default.
* @remarks these are the options displayed on the generator tab
*/
account: "account",

// FIXME: consider adding a profile for bitwarden's master password
});

/** Credential generation algorithms grouped by purpose. */
export const AlgorithmsByType = deepFreeze({
/** Algorithms that produce passwords */
[Type.password]: [Algorithm.password, Algorithm.passphrase] as const,

/** Algorithms that produce usernames */
[Type.username]: [Algorithm.username] as const,

/** Algorithms that produce email addresses */
[Type.email]: [Algorithm.catchall, Algorithm.plusAddress] as const,
} as const);
65 changes: 65 additions & 0 deletions libs/tools/generator/core/src/metadata/email/catchall.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { mock } from "jest-mock-extended";

import { EmailRandomizer } from "../../engine";
import { CatchallConstraints } from "../../policies/catchall-constraints";
import { CatchallGenerationOptions, GeneratorDependencyProvider } from "../../types";
import { Profile } from "../data";
import { CoreProfileMetadata } from "../profile-metadata";
import { isCoreProfile } from "../util";

import catchall from "./catchall";

const dependencyProvider = mock<GeneratorDependencyProvider>();

describe("email - catchall generator metadata", () => {
describe("engine.create", () => {
it("returns an email randomizer", () => {
expect(catchall.engine.create(dependencyProvider)).toBeInstanceOf(EmailRandomizer);
});
});

describe("profiles[account]", () => {
let accountProfile: CoreProfileMetadata<CatchallGenerationOptions> = null;
beforeEach(() => {
const profile = catchall.profiles[Profile.account];
if (isCoreProfile(profile)) {
accountProfile = profile;
}
});

describe("storage.options.deserializer", () => {
it("returns its input", () => {
const value: CatchallGenerationOptions = {
catchallType: "random",
catchallDomain: "example.com",
};

const result = accountProfile.storage.options.deserializer(value);

expect(result).toBe(value);
});
});

describe("constraints.create", () => {
// these tests check that the wiring is correct by exercising the behavior
// of functionality encapsulated by `create`. These methods may fail if the
// enclosed behaviors change.

it("creates a catchall constraints", () => {
const context = { defaultConstraints: {} };

const constraints = accountProfile.constraints.create([], context);

expect(constraints).toBeInstanceOf(CatchallConstraints);
});

it("extracts the domain from context.email", () => {
const context = { email: "[email protected]", defaultConstraints: {} };

const constraints = accountProfile.constraints.create([], context) as CatchallConstraints;

expect(constraints.domain).toEqual("example.com");
});
});
});
});
70 changes: 70 additions & 0 deletions libs/tools/generator/core/src/metadata/email/catchall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
import { deepFreeze } from "@bitwarden/common/tools/util";

import { EmailRandomizer } from "../../engine";
import { CatchallConstraints } from "../../policies/catchall-constraints";
import {
CatchallGenerationOptions,
CredentialGenerator,
GeneratorDependencyProvider,
} from "../../types";
import { Algorithm, Type, Profile } from "../data";
import { GeneratorMetadata } from "../generator-metadata";

const catchall: GeneratorMetadata<CatchallGenerationOptions> = deepFreeze({
id: Algorithm.catchall,
category: Type.email,
weight: 210,
i18nKeys: {
name: "catchallEmail",
description: "catchallEmailDesc",
credentialType: "email",
generateCredential: "generateEmail",
credentialGenerated: "emailGenerated",
copyCredential: "copyEmail",
useCredential: "useThisEmail",
},
capabilities: {
autogenerate: true,
fields: [],
},
engine: {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<CatchallGenerationOptions> {
return new EmailRandomizer(dependencies.randomizer);
},
},
profiles: {
[Profile.account]: {
type: "core",
storage: {
key: "catchallGeneratorSettings",
target: "object",
format: "plain",
classifier: new PublicClassifier<CatchallGenerationOptions>([
"catchallType",
"catchallDomain",
]),
state: GENERATOR_DISK,
initial: {
catchallType: "random",
catchallDomain: "",
},
options: {
deserializer: (value) => value,
clearOn: ["logout"],
},
},
constraints: {
default: { catchallDomain: { minLength: 1 } },
create(_policies, context) {
return new CatchallConstraints(context.email ?? "");
},
},
},
},
});

export default catchall;
4 changes: 4 additions & 0 deletions libs/tools/generator/core/src/metadata/email/forwarder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Forwarders are pending integration with the extension API
//
// They use the 300-block of weights and derive their metadata
// using logic similar to `toCredentialGeneratorConfiguration`
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { mock } from "jest-mock-extended";

import { EmailRandomizer } from "../../engine";
import { SubaddressConstraints } from "../../policies/subaddress-constraints";
import { SubaddressGenerationOptions, GeneratorDependencyProvider } from "../../types";
import { Profile } from "../data";
import { CoreProfileMetadata } from "../profile-metadata";
import { isCoreProfile } from "../util";

import plusAddress from "./plus-address";

const dependencyProvider = mock<GeneratorDependencyProvider>();

describe("email - plus address generator metadata", () => {
describe("engine.create", () => {
it("returns an email randomizer", () => {
expect(plusAddress.engine.create(dependencyProvider)).toBeInstanceOf(EmailRandomizer);
});
});

describe("profiles[account]", () => {
let accountProfile: CoreProfileMetadata<SubaddressGenerationOptions> = null;
beforeEach(() => {
const profile = plusAddress.profiles[Profile.account];
if (isCoreProfile(profile)) {
accountProfile = profile;
}
});

describe("storage.options.deserializer", () => {
it("returns its input", () => {
const value: SubaddressGenerationOptions = {
subaddressType: "random",
subaddressEmail: "[email protected]",
};

const result = accountProfile.storage.options.deserializer(value);

expect(result).toBe(value);
});
});

describe("constraints.create", () => {
// these tests check that the wiring is correct by exercising the behavior
// of functionality encapsulated by `create`. These methods may fail if the
// enclosed behaviors change.

it("creates a subaddress constraints", () => {
const context = { defaultConstraints: {} };

const constraints = accountProfile.constraints.create([], context);

expect(constraints).toBeInstanceOf(SubaddressConstraints);
});

it("sets the constraint email to context.email", () => {
const context = { email: "[email protected]", defaultConstraints: {} };

const constraints = accountProfile.constraints.create([], context) as SubaddressConstraints;

expect(constraints.email).toEqual("[email protected]");
});
});
});
});
72 changes: 72 additions & 0 deletions libs/tools/generator/core/src/metadata/email/plus-address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
import { deepFreeze } from "@bitwarden/common/tools/util";

import { EmailRandomizer } from "../../engine";
import { SubaddressConstraints } from "../../policies/subaddress-constraints";
import {
CredentialGenerator,
GeneratorDependencyProvider,
SubaddressGenerationOptions,
} from "../../types";
import { Algorithm, Profile, Type } from "../data";
import { GeneratorMetadata } from "../generator-metadata";

const plusAddress: GeneratorMetadata<SubaddressGenerationOptions> = deepFreeze({
id: Algorithm.plusAddress,
category: Type.email,
weight: 200,
i18nKeys: {
name: "plusAddressedEmail",
description: "plusAddressedEmailDesc",
credentialType: "email",
generateCredential: "generateEmail",
credentialGenerated: "emailGenerated",
copyCredential: "copyEmail",
useCredential: "useThisEmail",
},
capabilities: {
autogenerate: true,
fields: [],
},
engine: {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<SubaddressGenerationOptions> {
return new EmailRandomizer(dependencies.randomizer);
},
},
profiles: {
[Profile.account]: {
type: "core",
storage: {
key: "subaddressGeneratorSettings",
target: "object",
format: "plain",
classifier: new PublicClassifier<SubaddressGenerationOptions>([
"subaddressType",
"subaddressEmail",
]),
state: GENERATOR_DISK,
initial: {
subaddressType: "random",
subaddressEmail: "",
},
options: {
deserializer(value) {
return value;
},
clearOn: ["logout"],
},
},
constraints: {
default: {},
create(_policy, context) {
return new SubaddressConstraints(context.email ?? "");
},
},
},
},
});

export default plusAddress;
Loading
Loading