mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
[PM-16788] introduce generator metadata (#12757)
This commit is contained in:
61
libs/tools/generator/core/src/metadata/algorithm-metadata.ts
Normal file
61
libs/tools/generator/core/src/metadata/algorithm-metadata.ts
Normal file
@@ -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
libs/tools/generator/core/src/metadata/data.ts
Normal file
48
libs/tools/generator/core/src/metadata/data.ts
Normal file
@@ -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);
|
||||
@@ -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: "foo@example.com", defaultConstraints: {} };
|
||||
|
||||
const constraints = accountProfile.constraints.create([], context) as CatchallConstraints;
|
||||
|
||||
expect(constraints.domain).toEqual("example.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
70
libs/tools/generator/core/src/metadata/email/catchall.ts
Normal file
70
libs/tools/generator/core/src/metadata/email/catchall.ts
Normal file
@@ -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;
|
||||
@@ -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`
|
||||
@@ -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: "foo@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 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: "bar@example.com", defaultConstraints: {} };
|
||||
|
||||
const constraints = accountProfile.constraints.create([], context) as SubaddressConstraints;
|
||||
|
||||
expect(constraints.email).toEqual("bar@example.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
72
libs/tools/generator/core/src/metadata/email/plus-address.ts
Normal file
72
libs/tools/generator/core/src/metadata/email/plus-address.ts
Normal file
@@ -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;
|
||||
29
libs/tools/generator/core/src/metadata/generator-metadata.ts
Normal file
29
libs/tools/generator/core/src/metadata/generator-metadata.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { CredentialGenerator, GeneratorDependencyProvider } from "../types";
|
||||
|
||||
import { AlgorithmMetadata } from "./algorithm-metadata";
|
||||
import { Profile } from "./data";
|
||||
import { ProfileMetadata } from "./profile-metadata";
|
||||
|
||||
/** Extends the algorithm metadata with storage and engine configurations.
|
||||
* @example
|
||||
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
|
||||
* // to pattern test whether the credential describes a forwarder algorithm
|
||||
* const meta : CredentialGeneratorInfo = // ...
|
||||
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {};
|
||||
*/
|
||||
export type GeneratorMetadata<Options> = AlgorithmMetadata & {
|
||||
/** An algorithm that generates credentials when ran. */
|
||||
engine: {
|
||||
/** Factory for the generator
|
||||
*/
|
||||
create: (randomizer: GeneratorDependencyProvider) => CredentialGenerator<Options>;
|
||||
};
|
||||
|
||||
/** Defines parameters for credential generation */
|
||||
profiles: {
|
||||
/** profiles supported by this generator; when `undefined`,
|
||||
* the generator does not support the profile.
|
||||
*/
|
||||
[K in keyof typeof Profile]?: ProfileMetadata<Options>;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
|
||||
import { PasswordRandomizer } from "../../engine";
|
||||
import { PassphrasePolicyConstraints } from "../../policies";
|
||||
import { PassphraseGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
||||
import { Profile } from "../data";
|
||||
import { CoreProfileMetadata } from "../profile-metadata";
|
||||
import { isCoreProfile } from "../util";
|
||||
|
||||
import effPassphrase from "./eff-word-list";
|
||||
|
||||
const dependencyProvider = mock<GeneratorDependencyProvider>();
|
||||
|
||||
describe("password - eff words generator metadata", () => {
|
||||
describe("engine.create", () => {
|
||||
it("returns an email randomizer", () => {
|
||||
expect(effPassphrase.engine.create(dependencyProvider)).toBeInstanceOf(PasswordRandomizer);
|
||||
});
|
||||
});
|
||||
|
||||
describe("profiles[account]", () => {
|
||||
let accountProfile: CoreProfileMetadata<PassphraseGenerationOptions> = null;
|
||||
beforeEach(() => {
|
||||
const profile = effPassphrase.profiles[Profile.account];
|
||||
if (isCoreProfile(profile)) {
|
||||
accountProfile = profile;
|
||||
}
|
||||
});
|
||||
|
||||
describe("storage.options.deserializer", () => {
|
||||
it("returns its input", () => {
|
||||
const value: PassphraseGenerationOptions = { ...accountProfile.storage.initial };
|
||||
|
||||
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 passphrase policy constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
||||
|
||||
const constraints = accountProfile.constraints.create([], context);
|
||||
|
||||
expect(constraints).toBeInstanceOf(PassphrasePolicyConstraints);
|
||||
});
|
||||
|
||||
it("forwards the policy to the constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
||||
const policies = [
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
minNumberWords: 6,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
},
|
||||
},
|
||||
] as Policy[];
|
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context);
|
||||
|
||||
expect(constraints.constraints.numWords.min).toEqual(6);
|
||||
});
|
||||
|
||||
it("combines multiple policies in the constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
||||
const policies = [
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
minNumberWords: 6,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
minNumberWords: 3,
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
},
|
||||
},
|
||||
] as Policy[];
|
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context);
|
||||
|
||||
expect(constraints.constraints.numWords.min).toEqual(6);
|
||||
expect(constraints.constraints.capitalize.requiredValue).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
|
||||
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
|
||||
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
|
||||
|
||||
import { PasswordRandomizer } from "../../engine";
|
||||
import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies";
|
||||
import {
|
||||
CredentialGenerator,
|
||||
GeneratorDependencyProvider,
|
||||
PassphraseGenerationOptions,
|
||||
} from "../../types";
|
||||
import { Algorithm, Profile, Type } from "../data";
|
||||
import { GeneratorMetadata } from "../generator-metadata";
|
||||
|
||||
const passphrase: GeneratorMetadata<PassphraseGenerationOptions> = {
|
||||
id: Algorithm.passphrase,
|
||||
category: Type.password,
|
||||
weight: 110,
|
||||
i18nKeys: {
|
||||
name: "passphrase",
|
||||
credentialType: "passphrase",
|
||||
generateCredential: "generatePassphrase",
|
||||
credentialGenerated: "passphraseGenerated",
|
||||
copyCredential: "copyPassphrase",
|
||||
useCredential: "useThisPassphrase",
|
||||
},
|
||||
capabilities: {
|
||||
autogenerate: false,
|
||||
fields: [],
|
||||
},
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<PassphraseGenerationOptions> {
|
||||
return new PasswordRandomizer(dependencies.randomizer);
|
||||
},
|
||||
},
|
||||
profiles: {
|
||||
[Profile.account]: {
|
||||
type: "core",
|
||||
storage: {
|
||||
key: "passphraseGeneratorSettings",
|
||||
target: "object",
|
||||
format: "plain",
|
||||
classifier: new PublicClassifier<PassphraseGenerationOptions>([
|
||||
"numWords",
|
||||
"wordSeparator",
|
||||
"capitalize",
|
||||
"includeNumber",
|
||||
]),
|
||||
state: GENERATOR_DISK,
|
||||
initial: {
|
||||
numWords: 6,
|
||||
wordSeparator: "-",
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
},
|
||||
options: {
|
||||
deserializer(value) {
|
||||
return value;
|
||||
},
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
} satisfies ObjectKey<PassphraseGenerationOptions>,
|
||||
constraints: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
default: {
|
||||
wordSeparator: { maxLength: 1 },
|
||||
numWords: {
|
||||
min: 3,
|
||||
max: 20,
|
||||
recommendation: 6,
|
||||
},
|
||||
},
|
||||
create(policies, context) {
|
||||
const initial = {
|
||||
minNumberWords: 0,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
};
|
||||
const policy = policies.reduce(passphraseLeastPrivilege, initial);
|
||||
const constraints = new PassphrasePolicyConstraints(policy, context.defaultConstraints);
|
||||
return constraints;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default passphrase;
|
||||
@@ -0,0 +1,105 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
|
||||
import { PasswordRandomizer } from "../../engine";
|
||||
import { DynamicPasswordPolicyConstraints } from "../../policies";
|
||||
import { PasswordGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
||||
import { Profile } from "../data";
|
||||
import { CoreProfileMetadata } from "../profile-metadata";
|
||||
import { isCoreProfile } from "../util";
|
||||
|
||||
import password from "./random-password";
|
||||
|
||||
const dependencyProvider = mock<GeneratorDependencyProvider>();
|
||||
|
||||
describe("password - characters generator metadata", () => {
|
||||
describe("engine.create", () => {
|
||||
it("returns an email randomizer", () => {
|
||||
expect(password.engine.create(dependencyProvider)).toBeInstanceOf(PasswordRandomizer);
|
||||
});
|
||||
});
|
||||
|
||||
describe("profiles[account]", () => {
|
||||
let accountProfile: CoreProfileMetadata<PasswordGenerationOptions> = null;
|
||||
beforeEach(() => {
|
||||
const profile = password.profiles[Profile.account];
|
||||
if (isCoreProfile(profile)) {
|
||||
accountProfile = profile;
|
||||
}
|
||||
});
|
||||
|
||||
describe("storage.options.deserializer", () => {
|
||||
it("returns its input", () => {
|
||||
const value: PasswordGenerationOptions = { ...accountProfile.storage.initial };
|
||||
|
||||
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 passphrase policy constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
||||
|
||||
const constraints = accountProfile.constraints.create([], context);
|
||||
|
||||
expect(constraints).toBeInstanceOf(DynamicPasswordPolicyConstraints);
|
||||
});
|
||||
|
||||
it("forwards the policy to the constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
||||
const policies = [
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
enabled: true,
|
||||
data: {
|
||||
minLength: 10,
|
||||
capitalize: false,
|
||||
useNumbers: false,
|
||||
},
|
||||
},
|
||||
] as Policy[];
|
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context);
|
||||
|
||||
expect(constraints.constraints.length.min).toEqual(10);
|
||||
});
|
||||
|
||||
it("combines multiple policies in the constraints", () => {
|
||||
const context = { defaultConstraints: accountProfile.constraints.default };
|
||||
const policies = [
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
enabled: true,
|
||||
data: {
|
||||
minLength: 14,
|
||||
useSpecial: false,
|
||||
useNumbers: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: PolicyType.PasswordGenerator,
|
||||
enabled: true,
|
||||
data: {
|
||||
minLength: 10,
|
||||
useSpecial: true,
|
||||
includeNumber: false,
|
||||
},
|
||||
},
|
||||
] as Policy[];
|
||||
|
||||
const constraints = accountProfile.constraints.create(policies, context);
|
||||
|
||||
expect(constraints.constraints.length.min).toEqual(14);
|
||||
expect(constraints.constraints.special.requiredValue).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
|
||||
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
|
||||
import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
|
||||
import { PasswordRandomizer } from "../../engine";
|
||||
import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies";
|
||||
import {
|
||||
CredentialGenerator,
|
||||
GeneratorDependencyProvider,
|
||||
PasswordGeneratorSettings,
|
||||
} from "../../types";
|
||||
import { Algorithm, Profile, Type } from "../data";
|
||||
import { GeneratorMetadata } from "../generator-metadata";
|
||||
|
||||
const password: GeneratorMetadata<PasswordGeneratorSettings> = deepFreeze({
|
||||
id: Algorithm.password,
|
||||
category: Type.password,
|
||||
weight: 100,
|
||||
i18nKeys: {
|
||||
name: "password",
|
||||
generateCredential: "generatePassword",
|
||||
credentialGenerated: "passwordGenerated",
|
||||
credentialType: "password",
|
||||
copyCredential: "copyPassword",
|
||||
useCredential: "useThisPassword",
|
||||
},
|
||||
capabilities: {
|
||||
autogenerate: true,
|
||||
fields: [],
|
||||
},
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<PasswordGeneratorSettings> {
|
||||
return new PasswordRandomizer(dependencies.randomizer);
|
||||
},
|
||||
},
|
||||
profiles: {
|
||||
[Profile.account]: {
|
||||
type: "core",
|
||||
storage: {
|
||||
key: "passwordGeneratorSettings",
|
||||
target: "object",
|
||||
format: "plain",
|
||||
classifier: new PublicClassifier<PasswordGeneratorSettings>([
|
||||
"length",
|
||||
"ambiguous",
|
||||
"uppercase",
|
||||
"minUppercase",
|
||||
"lowercase",
|
||||
"minLowercase",
|
||||
"number",
|
||||
"minNumber",
|
||||
"special",
|
||||
"minSpecial",
|
||||
]),
|
||||
state: GENERATOR_DISK,
|
||||
initial: {
|
||||
length: 14,
|
||||
ambiguous: true,
|
||||
uppercase: true,
|
||||
minUppercase: 1,
|
||||
lowercase: true,
|
||||
minLowercase: 1,
|
||||
number: true,
|
||||
minNumber: 1,
|
||||
special: false,
|
||||
minSpecial: 0,
|
||||
},
|
||||
options: {
|
||||
deserializer(value) {
|
||||
return value;
|
||||
},
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
},
|
||||
constraints: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
default: {
|
||||
length: {
|
||||
min: 5,
|
||||
max: 128,
|
||||
recommendation: 14,
|
||||
},
|
||||
minNumber: {
|
||||
min: 0,
|
||||
max: 9,
|
||||
},
|
||||
minSpecial: {
|
||||
min: 0,
|
||||
max: 9,
|
||||
},
|
||||
},
|
||||
create(policies, context) {
|
||||
const initial = {
|
||||
minLength: 0,
|
||||
useUppercase: false,
|
||||
useLowercase: false,
|
||||
useNumbers: false,
|
||||
numberCount: 0,
|
||||
useSpecial: false,
|
||||
specialCount: 0,
|
||||
};
|
||||
const policy = policies.reduce(passwordLeastPrivilege, initial);
|
||||
const constraints = new DynamicPasswordPolicyConstraints(
|
||||
policy,
|
||||
context.defaultConstraints,
|
||||
);
|
||||
return constraints;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default password;
|
||||
80
libs/tools/generator/core/src/metadata/profile-metadata.ts
Normal file
80
libs/tools/generator/core/src/metadata/profile-metadata.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { SiteId } from "@bitwarden/common/tools/extension";
|
||||
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
|
||||
import { Constraints } from "@bitwarden/common/tools/types";
|
||||
|
||||
import { GeneratorConstraints } from "../types";
|
||||
|
||||
export type ProfileContext<Options> = {
|
||||
/** The email address for the current user;
|
||||
* `undefined` when no email is available.
|
||||
*/
|
||||
email?: string;
|
||||
|
||||
/** Default application limits for the profile */
|
||||
defaultConstraints: Constraints<Options>;
|
||||
};
|
||||
|
||||
type ProfileConstraints<Options> = {
|
||||
/** The key used to locate this profile's policies in the admin console.
|
||||
* When this type is undefined, no policy is defined for the profile.
|
||||
*/
|
||||
type?: PolicyType;
|
||||
|
||||
/** default application limits for this profile; these are overridden
|
||||
* by the policy
|
||||
*/
|
||||
default: Constraints<Options>;
|
||||
|
||||
/** Constructs generator constraints from a policy.
|
||||
* @param policies the administrative policy to apply to the provided constraints
|
||||
* When `type` is undefined then `policy` is `undefined` this is an empty array.
|
||||
* @param defaultConstraints application constraints; typically those defined in
|
||||
* the `default` member, above.
|
||||
* @returns the generator constraints to apply to this profile's options.
|
||||
*/
|
||||
create: (policies: Policy[], context: ProfileContext<Options>) => GeneratorConstraints<Options>;
|
||||
};
|
||||
|
||||
/** Generator profiles partition generator operations
|
||||
* according to where they're used within the password
|
||||
* manager. Core profiles store their data using the
|
||||
* generator's system storage.
|
||||
*/
|
||||
export type CoreProfileMetadata<Options> = {
|
||||
/** distinguishes profile metadata types */
|
||||
type: "core";
|
||||
|
||||
/** plaintext import buffer */
|
||||
import?: ObjectKey<Options, Record<string, never>, Options> & { format: "plain" };
|
||||
|
||||
/** persistent storage location */
|
||||
storage: ObjectKey<Options>;
|
||||
|
||||
/** policy enforced when saving the options */
|
||||
constraints: ProfileConstraints<Options>;
|
||||
};
|
||||
|
||||
/** Generator profiles partition generator operations
|
||||
* according to where they're used within the password
|
||||
* manager. Extension profiles store their data
|
||||
* using the extension system.
|
||||
*/
|
||||
export type ExtensionProfileMetadata<Options, Site extends SiteId> = {
|
||||
/** distinguishes profile metadata types */
|
||||
type: "extension";
|
||||
|
||||
/** The extension site described by this metadata */
|
||||
site: Site;
|
||||
|
||||
constraints: ProfileConstraints<Options>;
|
||||
};
|
||||
|
||||
/** Generator profiles partition generator operations
|
||||
* according to where they're used within the password
|
||||
* manager
|
||||
*/
|
||||
export type ProfileMetadata<Options> =
|
||||
| CoreProfileMetadata<Options>
|
||||
| ExtensionProfileMetadata<Options, "forwarder">;
|
||||
28
libs/tools/generator/core/src/metadata/type.ts
Normal file
28
libs/tools/generator/core/src/metadata/type.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
|
||||
import { AlgorithmsByType, Profile, Type } from "./data";
|
||||
|
||||
/** categorizes credentials according to their use-case outside of Bitwarden */
|
||||
export type CredentialType = keyof typeof Type;
|
||||
|
||||
/** categorizes credentials according to their expected use-case within Bitwarden */
|
||||
export type GeneratorProfile = keyof typeof Profile;
|
||||
|
||||
/** A type of password that may be generated by the credential generator. */
|
||||
export type PasswordAlgorithm = (typeof AlgorithmsByType.password)[number];
|
||||
|
||||
/** A type of username that may be generated by the credential generator. */
|
||||
export type UsernameAlgorithm = (typeof AlgorithmsByType.username)[number];
|
||||
|
||||
/** A type of email address that may be generated by the credential generator. */
|
||||
export type EmailAlgorithm = (typeof AlgorithmsByType.email)[number] | ForwarderExtensionId;
|
||||
|
||||
/** Identifies a forwarding service */
|
||||
export type ForwarderExtensionId = { forwarder: VendorId };
|
||||
|
||||
/** A type of credential that can be generated by the credential generator. */
|
||||
// this is defined in terms of `AlgorithmsByType` to typecheck the keys of
|
||||
// `AlgorithmsByType` against the keys of `CredentialType`.
|
||||
export type CredentialAlgorithm =
|
||||
| (typeof AlgorithmsByType)[CredentialType][number]
|
||||
| ForwarderExtensionId;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||
|
||||
import { UsernameRandomizer } from "../../engine";
|
||||
import { EffUsernameGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
||||
import { Profile } from "../data";
|
||||
import { CoreProfileMetadata } from "../profile-metadata";
|
||||
import { isCoreProfile } from "../util";
|
||||
|
||||
import effWordList from "./eff-word-list";
|
||||
|
||||
const dependencyProvider = mock<GeneratorDependencyProvider>();
|
||||
|
||||
describe("username - eff words generator metadata", () => {
|
||||
describe("engine.create", () => {
|
||||
it("returns an email randomizer", () => {
|
||||
expect(effWordList.engine.create(dependencyProvider)).toBeInstanceOf(UsernameRandomizer);
|
||||
});
|
||||
});
|
||||
|
||||
describe("profiles[account]", () => {
|
||||
let accountProfile: CoreProfileMetadata<EffUsernameGenerationOptions> = null;
|
||||
beforeEach(() => {
|
||||
const profile = effWordList.profiles[Profile.account];
|
||||
if (isCoreProfile(profile)) {
|
||||
accountProfile = profile;
|
||||
}
|
||||
});
|
||||
|
||||
describe("storage.options.deserializer", () => {
|
||||
it("returns its input", () => {
|
||||
const value: EffUsernameGenerationOptions = {
|
||||
wordCapitalize: true,
|
||||
wordIncludeNumber: true,
|
||||
};
|
||||
|
||||
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 effWordList constraints", () => {
|
||||
const context = { defaultConstraints: {} };
|
||||
|
||||
const constraints = accountProfile.constraints.create([], context);
|
||||
|
||||
expect(constraints).toBeInstanceOf(IdentityConstraint);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
|
||||
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
|
||||
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||
import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
|
||||
import { UsernameRandomizer } from "../../engine";
|
||||
import {
|
||||
CredentialGenerator,
|
||||
EffUsernameGenerationOptions,
|
||||
GeneratorDependencyProvider,
|
||||
} from "../../types";
|
||||
import { Algorithm, Profile, Type } from "../data";
|
||||
import { GeneratorMetadata } from "../generator-metadata";
|
||||
|
||||
const effWordList: GeneratorMetadata<EffUsernameGenerationOptions> = deepFreeze({
|
||||
id: Algorithm.username,
|
||||
category: Type.username,
|
||||
weight: 400,
|
||||
i18nKeys: {
|
||||
name: "randomWord",
|
||||
credentialType: "username",
|
||||
generateCredential: "generateUsername",
|
||||
credentialGenerated: "usernameGenerated",
|
||||
copyCredential: "copyUsername",
|
||||
useCredential: "useThisUsername",
|
||||
},
|
||||
capabilities: {
|
||||
autogenerate: true,
|
||||
fields: [],
|
||||
},
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<EffUsernameGenerationOptions> {
|
||||
return new UsernameRandomizer(dependencies.randomizer);
|
||||
},
|
||||
},
|
||||
profiles: {
|
||||
[Profile.account]: {
|
||||
type: "core",
|
||||
storage: {
|
||||
key: "effUsernameGeneratorSettings",
|
||||
target: "object",
|
||||
format: "plain",
|
||||
classifier: new PublicClassifier<EffUsernameGenerationOptions>([
|
||||
"wordCapitalize",
|
||||
"wordIncludeNumber",
|
||||
]),
|
||||
state: GENERATOR_DISK,
|
||||
initial: {
|
||||
wordCapitalize: false,
|
||||
wordIncludeNumber: false,
|
||||
website: null,
|
||||
},
|
||||
options: {
|
||||
deserializer: (value) => value,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
},
|
||||
constraints: {
|
||||
default: {},
|
||||
create(_policies, _context) {
|
||||
return new IdentityConstraint<EffUsernameGenerationOptions>();
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default effWordList;
|
||||
218
libs/tools/generator/core/src/metadata/util.spec.ts
Normal file
218
libs/tools/generator/core/src/metadata/util.spec.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
|
||||
import { Algorithm, AlgorithmsByType } from "./data";
|
||||
import { ProfileMetadata } from "./profile-metadata";
|
||||
import {
|
||||
isPasswordAlgorithm,
|
||||
isUsernameAlgorithm,
|
||||
isForwarderExtensionId,
|
||||
isEmailAlgorithm,
|
||||
isSameAlgorithm,
|
||||
isCoreProfile,
|
||||
isForwarderProfile,
|
||||
} from "./util";
|
||||
|
||||
describe("credential generator metadata utility functions", () => {
|
||||
describe("isPasswordAlgorithm", () => {
|
||||
it("returns `true` when the algorithm is a password algorithm", () => {
|
||||
for (const algorithm of AlgorithmsByType.password) {
|
||||
expect(isPasswordAlgorithm(algorithm)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns `false` when the algorithm is an email algorithm", () => {
|
||||
for (const algorithm of AlgorithmsByType.email) {
|
||||
expect(isPasswordAlgorithm(algorithm)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns `false` when the algorithm is a username algorithm", () => {
|
||||
for (const algorithm of AlgorithmsByType.username) {
|
||||
expect(isPasswordAlgorithm(algorithm)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns `false` when the algorithm is a forwarder extension", () => {
|
||||
expect(isPasswordAlgorithm({ forwarder: "bitwarden" as VendorId })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isUsernameAlgorithm", () => {
|
||||
it("returns `false` when the algorithm is a password algorithm", () => {
|
||||
for (const algorithm of AlgorithmsByType.password) {
|
||||
expect(isUsernameAlgorithm(algorithm)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns `false` when the algorithm is an email algorithm", () => {
|
||||
for (const algorithm of AlgorithmsByType.email) {
|
||||
expect(isUsernameAlgorithm(algorithm)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns `true` when the algorithm is a username algorithm", () => {
|
||||
for (const algorithm of AlgorithmsByType.username) {
|
||||
expect(isUsernameAlgorithm(algorithm)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns `false` when the algorithm is a forwarder extension", () => {
|
||||
expect(isUsernameAlgorithm({ forwarder: "bitwarden" as VendorId })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isForwarderExtensionId", () => {
|
||||
it("returns `false` when the algorithm is a password algorithm", () => {
|
||||
for (const algorithm of AlgorithmsByType.password) {
|
||||
expect(isForwarderExtensionId(algorithm)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns `false` when the algorithm is an email algorithm", () => {
|
||||
for (const algorithm of AlgorithmsByType.email) {
|
||||
expect(isForwarderExtensionId(algorithm)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns `false` when the algorithm is a username algorithm", () => {
|
||||
for (const algorithm of AlgorithmsByType.username) {
|
||||
expect(isForwarderExtensionId(algorithm)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns `true` when the algorithm is a forwarder extension", () => {
|
||||
expect(isForwarderExtensionId({ forwarder: "bitwarden" as VendorId })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isEmailAlgorithm", () => {
|
||||
it("returns `false` when the algorithm is a password algorithm", () => {
|
||||
for (const algorithm of AlgorithmsByType.password) {
|
||||
expect(isEmailAlgorithm(algorithm)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns `true` when the algorithm is an email algorithm", () => {
|
||||
for (const algorithm of AlgorithmsByType.email) {
|
||||
expect(isEmailAlgorithm(algorithm)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns `false` when the algorithm is a username algorithm", () => {
|
||||
for (const algorithm of AlgorithmsByType.username) {
|
||||
expect(isEmailAlgorithm(algorithm)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns `true` when the algorithm is a forwarder extension", () => {
|
||||
expect(isEmailAlgorithm({ forwarder: "bitwarden" as VendorId })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSameAlgorithm", () => {
|
||||
it("returns `true` when the algorithms are equal", () => {
|
||||
// identical object
|
||||
expect(isSameAlgorithm(Algorithm.catchall, Algorithm.catchall)).toBe(true);
|
||||
|
||||
// equal object
|
||||
expect(isSameAlgorithm(Algorithm.catchall, `${Algorithm.catchall}`)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns `false` when the algorithms are different", () => {
|
||||
// not an exhaustive list
|
||||
expect(isSameAlgorithm(Algorithm.catchall, Algorithm.passphrase)).toBe(false);
|
||||
expect(isSameAlgorithm(Algorithm.passphrase, Algorithm.password)).toBe(false);
|
||||
expect(isSameAlgorithm(Algorithm.password, Algorithm.plusAddress)).toBe(false);
|
||||
expect(isSameAlgorithm(Algorithm.plusAddress, Algorithm.username)).toBe(false);
|
||||
expect(isSameAlgorithm(Algorithm.username, Algorithm.passphrase)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns `true` when the algorithms refer to a forwarder with a matching vendor", () => {
|
||||
const someVendor = { forwarder: "bitwarden" as VendorId };
|
||||
const sameVendor = { forwarder: "bitwarden" as VendorId };
|
||||
expect(isSameAlgorithm(someVendor, sameVendor)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns `false` when the algorithms refer to a forwarder with a different vendor", () => {
|
||||
const someVendor = { forwarder: "bitwarden" as VendorId };
|
||||
const sameVendor = { forwarder: "bytewarden" as VendorId };
|
||||
expect(isSameAlgorithm(someVendor, sameVendor)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns `false` when the algorithms refer to a forwarder and a core algorithm", () => {
|
||||
const someVendor = { forwarder: "bitwarden" as VendorId };
|
||||
// not an exhaustive list
|
||||
expect(isSameAlgorithm(someVendor, Algorithm.plusAddress)).toBe(false);
|
||||
expect(isSameAlgorithm(Algorithm.username, someVendor)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCoreProfile", () => {
|
||||
it("returns `true` when the profile's type is `core`", () => {
|
||||
const profile: ProfileMetadata<object> = {
|
||||
type: "core",
|
||||
storage: null,
|
||||
constraints: {
|
||||
default: {},
|
||||
create: () => null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isCoreProfile(profile)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns `false` when the profile's type is `extension`", () => {
|
||||
const profile: ProfileMetadata<object> = {
|
||||
type: "extension",
|
||||
site: "forwarder",
|
||||
constraints: {
|
||||
default: {},
|
||||
create: () => null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isCoreProfile(profile)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isForwarderProfile", () => {
|
||||
it("returns `false` when the profile's type is `core`", () => {
|
||||
const profile: ProfileMetadata<object> = {
|
||||
type: "core",
|
||||
storage: null,
|
||||
constraints: {
|
||||
default: {},
|
||||
create: () => null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isForwarderProfile(profile)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns `true` when the profile's type is `extension` and the site is `forwarder`", () => {
|
||||
const profile: ProfileMetadata<object> = {
|
||||
type: "extension",
|
||||
site: "forwarder",
|
||||
constraints: {
|
||||
default: {},
|
||||
create: () => null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isForwarderProfile(profile)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns `false` when the profile's type is `extension` and the site is not `forwarder`", () => {
|
||||
const profile: ProfileMetadata<object> = {
|
||||
type: "extension",
|
||||
site: "not-a-forwarder" as any,
|
||||
constraints: {
|
||||
default: {},
|
||||
create: () => null,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isForwarderProfile(profile)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
60
libs/tools/generator/core/src/metadata/util.ts
Normal file
60
libs/tools/generator/core/src/metadata/util.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { AlgorithmsByType } from "./data";
|
||||
import { CoreProfileMetadata, ExtensionProfileMetadata, ProfileMetadata } from "./profile-metadata";
|
||||
import {
|
||||
CredentialAlgorithm,
|
||||
EmailAlgorithm,
|
||||
ForwarderExtensionId,
|
||||
PasswordAlgorithm,
|
||||
UsernameAlgorithm,
|
||||
} from "./type";
|
||||
|
||||
/** Returns true when the input algorithm is a password algorithm. */
|
||||
export function isPasswordAlgorithm(
|
||||
algorithm: CredentialAlgorithm,
|
||||
): algorithm is PasswordAlgorithm {
|
||||
return AlgorithmsByType.password.includes(algorithm as any);
|
||||
}
|
||||
|
||||
/** Returns true when the input algorithm is a username algorithm. */
|
||||
export function isUsernameAlgorithm(
|
||||
algorithm: CredentialAlgorithm,
|
||||
): algorithm is UsernameAlgorithm {
|
||||
return AlgorithmsByType.username.includes(algorithm as any);
|
||||
}
|
||||
|
||||
/** Returns true when the input algorithm is a forwarder integration. */
|
||||
export function isForwarderExtensionId(
|
||||
algorithm: CredentialAlgorithm,
|
||||
): algorithm is ForwarderExtensionId {
|
||||
return algorithm && typeof algorithm === "object" && "forwarder" in algorithm;
|
||||
}
|
||||
|
||||
/** Returns true when the input algorithm is an email algorithm. */
|
||||
export function isEmailAlgorithm(algorithm: CredentialAlgorithm): algorithm is EmailAlgorithm {
|
||||
return AlgorithmsByType.email.includes(algorithm as any) || isForwarderExtensionId(algorithm);
|
||||
}
|
||||
|
||||
/** Returns true when the algorithms are the same. */
|
||||
export function isSameAlgorithm(lhs: CredentialAlgorithm, rhs: CredentialAlgorithm) {
|
||||
if (lhs === rhs) {
|
||||
return true;
|
||||
} else if (isForwarderExtensionId(lhs) && isForwarderExtensionId(rhs)) {
|
||||
return lhs.forwarder === rhs.forwarder;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true when the input describes a core profile. */
|
||||
export function isCoreProfile<Options>(
|
||||
value: ProfileMetadata<Options>,
|
||||
): value is CoreProfileMetadata<Options> {
|
||||
return value.type === "core";
|
||||
}
|
||||
|
||||
/** Returns true when the input describes a forwarder extension profile. */
|
||||
export function isForwarderProfile<Options>(
|
||||
value: ProfileMetadata<Options>,
|
||||
): value is ExtensionProfileMetadata<Options, "forwarder"> {
|
||||
return value.type === "extension" && value.site === "forwarder";
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export class CatchallConstraints implements StateConstraints<CatchallGenerationO
|
||||
this.domain = parsed.groups.domain;
|
||||
}
|
||||
}
|
||||
private domain: string;
|
||||
readonly domain: string;
|
||||
|
||||
constraints: Readonly<Constraints<CatchallGenerationOptions>> = {};
|
||||
|
||||
|
||||
@@ -2,58 +2,58 @@
|
||||
*/
|
||||
export type PasswordGeneratorSettings = {
|
||||
/** The length of the password selected by the user */
|
||||
length: number;
|
||||
length?: number;
|
||||
|
||||
/** `true` when ambiguous characters may be included in the output.
|
||||
* `false` when ambiguous characters should not be included in the output.
|
||||
*/
|
||||
ambiguous: boolean;
|
||||
ambiguous?: boolean;
|
||||
|
||||
/** `true` when uppercase ASCII characters should be included in the output
|
||||
* This value defaults to `false.
|
||||
*/
|
||||
uppercase: boolean;
|
||||
uppercase?: boolean;
|
||||
|
||||
/** The minimum number of uppercase characters to include in the output.
|
||||
* The value is ignored when `uppercase` is `false`.
|
||||
* The value defaults to 1 when `uppercase` is `true`.
|
||||
*/
|
||||
minUppercase: number;
|
||||
minUppercase?: number;
|
||||
|
||||
/** `true` when lowercase ASCII characters should be included in the output.
|
||||
* This value defaults to `false`.
|
||||
*/
|
||||
lowercase: boolean;
|
||||
lowercase?: boolean;
|
||||
|
||||
/** The minimum number of lowercase characters to include in the output.
|
||||
* The value defaults to 1 when `lowercase` is `true`.
|
||||
* The value defaults to 0 when `lowercase` is `false`.
|
||||
*/
|
||||
minLowercase: number;
|
||||
minLowercase?: number;
|
||||
|
||||
/** Whether or not to include ASCII digits in the output
|
||||
* This value defaults to `true` when `minNumber` is at least 1.
|
||||
* This value defaults to `false` when `minNumber` is less than 1.
|
||||
*/
|
||||
number: boolean;
|
||||
number?: boolean;
|
||||
|
||||
/** The minimum number of digits to include in the output.
|
||||
* The value defaults to 1 when `number` is `true`.
|
||||
* The value defaults to 0 when `number` is `false`.
|
||||
*/
|
||||
minNumber: number;
|
||||
minNumber?: number;
|
||||
|
||||
/** Whether or not to include special characters in the output.
|
||||
* This value defaults to `true` when `minSpecial` is at least 1.
|
||||
* This value defaults to `false` when `minSpecial` is less than 1.
|
||||
*/
|
||||
special: boolean;
|
||||
special?: boolean;
|
||||
|
||||
/** The minimum number of special characters to include in the output.
|
||||
* This value defaults to 1 when `special` is `true`.
|
||||
* This value defaults to 0 when `special` is `false`.
|
||||
*/
|
||||
minSpecial: number;
|
||||
minSpecial?: number;
|
||||
};
|
||||
|
||||
/** Request format for password credential generation.
|
||||
|
||||
@@ -107,7 +107,7 @@ export function optionsToRandomAsciiRequest(options: PasswordGenerationOptions)
|
||||
DefaultPasswordGenerationOptions.special,
|
||||
DefaultPasswordGenerationOptions.minSpecial,
|
||||
),
|
||||
ambiguous: options.ambiguous ?? DefaultPasswordGenerationOptions.ambiguous,
|
||||
ambiguous: options.ambiguous ?? DefaultPasswordGenerationOptions.ambiguous!,
|
||||
all: 0,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user