mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-5611] username generator panel (#11201)
* add username and email engines to generators * introduce username and email settings components * introduce generator algorithm metadata * inline generator policies * wait until settings are available during generation
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { CredentialPreference } from "../types";
|
||||
|
||||
import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "./generator-types";
|
||||
|
||||
export const DefaultCredentialPreferences: CredentialPreference = Object.freeze({
|
||||
email: Object.freeze({
|
||||
algorithm: EmailAlgorithms[0],
|
||||
updated: new Date(0),
|
||||
}),
|
||||
password: Object.freeze({
|
||||
algorithm: PasswordAlgorithms[0],
|
||||
updated: new Date(0),
|
||||
}),
|
||||
username: Object.freeze({
|
||||
algorithm: UsernameAlgorithms[0],
|
||||
updated: new Date(0),
|
||||
}),
|
||||
});
|
||||
@@ -1,5 +1,15 @@
|
||||
/** Types of passwords that may be configured by the password generator */
|
||||
export const PasswordTypes = Object.freeze(["password", "passphrase"] as const);
|
||||
/** Types of passwords that may be generated by the credential generator */
|
||||
export const PasswordAlgorithms = Object.freeze(["password", "passphrase"] as const);
|
||||
|
||||
/** Types of generators that may be configured by the password generator */
|
||||
export const GeneratorTypes = Object.freeze([...PasswordTypes, "username"] as const);
|
||||
/** Types of usernames that may be generated by the credential generator */
|
||||
export const UsernameAlgorithms = Object.freeze(["username"] as const);
|
||||
|
||||
/** Types of email addresses that may be generated by the credential generator */
|
||||
export const EmailAlgorithms = Object.freeze(["catchall", "forwarder", "subaddress"] as const);
|
||||
|
||||
/** All types of credentials that may be generated by the credential generator */
|
||||
export const CredentialAlgorithms = Object.freeze([
|
||||
...PasswordAlgorithms,
|
||||
...UsernameAlgorithms,
|
||||
...EmailAlgorithms,
|
||||
] as const);
|
||||
|
||||
@@ -1,23 +1,51 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||
|
||||
import { Randomizer } from "../abstractions";
|
||||
import { PasswordRandomizer } from "../engine";
|
||||
import { PASSPHRASE_SETTINGS, PASSWORD_SETTINGS } from "../strategies/storage";
|
||||
import { EmailRandomizer, PasswordRandomizer, UsernameRandomizer } from "../engine";
|
||||
import {
|
||||
DefaultPolicyEvaluator,
|
||||
DynamicPasswordPolicyConstraints,
|
||||
PassphraseGeneratorOptionsEvaluator,
|
||||
passphraseLeastPrivilege,
|
||||
PassphrasePolicyConstraints,
|
||||
PasswordGeneratorOptionsEvaluator,
|
||||
passwordLeastPrivilege,
|
||||
} from "../policies";
|
||||
import {
|
||||
CATCHALL_SETTINGS,
|
||||
EFF_USERNAME_SETTINGS,
|
||||
PASSPHRASE_SETTINGS,
|
||||
PASSWORD_SETTINGS,
|
||||
SUBADDRESS_SETTINGS,
|
||||
} from "../strategies/storage";
|
||||
import {
|
||||
CatchallGenerationOptions,
|
||||
CredentialGenerator,
|
||||
CredentialGeneratorConfiguration,
|
||||
EffUsernameGenerationOptions,
|
||||
NoPolicy,
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGeneratorPolicy,
|
||||
PasswordGenerationOptions,
|
||||
PasswordGeneratorPolicy,
|
||||
SubaddressGenerationOptions,
|
||||
} from "../types";
|
||||
import { CredentialGeneratorConfiguration } from "../types/credential-generator-configuration";
|
||||
|
||||
import { DefaultCatchallOptions } from "./default-catchall-options";
|
||||
import { DefaultEffUsernameOptions } from "./default-eff-username-options";
|
||||
import { DefaultPassphraseBoundaries } from "./default-passphrase-boundaries";
|
||||
import { DefaultPassphraseGenerationOptions } from "./default-passphrase-generation-options";
|
||||
import { DefaultPasswordBoundaries } from "./default-password-boundaries";
|
||||
import { DefaultPasswordGenerationOptions } from "./default-password-generation-options";
|
||||
import { Policies } from "./policies";
|
||||
import { DefaultSubaddressOptions } from "./default-subaddress-generator-options";
|
||||
|
||||
const PASSPHRASE = Object.freeze({
|
||||
category: "passphrase",
|
||||
id: "passphrase",
|
||||
category: "password",
|
||||
nameKey: "passphrase",
|
||||
onlyOnRequest: false,
|
||||
engine: {
|
||||
create(randomizer: Randomizer): CredentialGenerator<PassphraseGenerationOptions> {
|
||||
return new PasswordRandomizer(randomizer);
|
||||
@@ -34,14 +62,27 @@ const PASSPHRASE = Object.freeze({
|
||||
},
|
||||
account: PASSPHRASE_SETTINGS,
|
||||
},
|
||||
policy: Policies.Passphrase,
|
||||
policy: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: Object.freeze({
|
||||
minNumberWords: 0,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
}),
|
||||
combine: passphraseLeastPrivilege,
|
||||
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
|
||||
toConstraints: (policy) => new PassphrasePolicyConstraints(policy),
|
||||
},
|
||||
} satisfies CredentialGeneratorConfiguration<
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGeneratorPolicy
|
||||
>);
|
||||
|
||||
const PASSWORD = Object.freeze({
|
||||
id: "password",
|
||||
category: "password",
|
||||
nameKey: "password",
|
||||
onlyOnRequest: false,
|
||||
engine: {
|
||||
create(randomizer: Randomizer): CredentialGenerator<PasswordGenerationOptions> {
|
||||
return new PasswordRandomizer(randomizer);
|
||||
@@ -65,14 +106,129 @@ const PASSWORD = Object.freeze({
|
||||
},
|
||||
account: PASSWORD_SETTINGS,
|
||||
},
|
||||
policy: Policies.Password,
|
||||
policy: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: Object.freeze({
|
||||
minLength: 0,
|
||||
useUppercase: false,
|
||||
useLowercase: false,
|
||||
useNumbers: false,
|
||||
numberCount: 0,
|
||||
useSpecial: false,
|
||||
specialCount: 0,
|
||||
}),
|
||||
combine: passwordLeastPrivilege,
|
||||
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
|
||||
toConstraints: (policy) => new DynamicPasswordPolicyConstraints(policy),
|
||||
},
|
||||
} satisfies CredentialGeneratorConfiguration<PasswordGenerationOptions, PasswordGeneratorPolicy>);
|
||||
|
||||
const USERNAME = Object.freeze({
|
||||
id: "username",
|
||||
category: "username",
|
||||
nameKey: "randomWord",
|
||||
onlyOnRequest: false,
|
||||
engine: {
|
||||
create(randomizer: Randomizer): CredentialGenerator<EffUsernameGenerationOptions> {
|
||||
return new UsernameRandomizer(randomizer);
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: DefaultEffUsernameOptions,
|
||||
constraints: {},
|
||||
account: EFF_USERNAME_SETTINGS,
|
||||
},
|
||||
policy: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {},
|
||||
combine(_acc: NoPolicy, _policy: Policy) {
|
||||
return {};
|
||||
},
|
||||
createEvaluator(_policy: NoPolicy) {
|
||||
return new DefaultPolicyEvaluator<EffUsernameGenerationOptions>();
|
||||
},
|
||||
toConstraints(_policy: NoPolicy) {
|
||||
return new IdentityConstraint<EffUsernameGenerationOptions>();
|
||||
},
|
||||
},
|
||||
} satisfies CredentialGeneratorConfiguration<EffUsernameGenerationOptions, NoPolicy>);
|
||||
|
||||
const CATCHALL = Object.freeze({
|
||||
id: "catchall",
|
||||
category: "email",
|
||||
nameKey: "catchallEmail",
|
||||
descriptionKey: "catchallEmailDesc",
|
||||
onlyOnRequest: false,
|
||||
engine: {
|
||||
create(randomizer: Randomizer): CredentialGenerator<CatchallGenerationOptions> {
|
||||
return new EmailRandomizer(randomizer);
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: DefaultCatchallOptions,
|
||||
constraints: { catchallDomain: { minLength: 1 } },
|
||||
account: CATCHALL_SETTINGS,
|
||||
},
|
||||
policy: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {},
|
||||
combine(_acc: NoPolicy, _policy: Policy) {
|
||||
return {};
|
||||
},
|
||||
createEvaluator(_policy: NoPolicy) {
|
||||
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
|
||||
},
|
||||
toConstraints(_policy: NoPolicy) {
|
||||
return new IdentityConstraint<CatchallGenerationOptions>();
|
||||
},
|
||||
},
|
||||
} satisfies CredentialGeneratorConfiguration<CatchallGenerationOptions, NoPolicy>);
|
||||
|
||||
const SUBADDRESS = Object.freeze({
|
||||
id: "subaddress",
|
||||
category: "email",
|
||||
nameKey: "plusAddressedEmail",
|
||||
descriptionKey: "plusAddressedEmailDesc",
|
||||
onlyOnRequest: false,
|
||||
engine: {
|
||||
create(randomizer: Randomizer): CredentialGenerator<SubaddressGenerationOptions> {
|
||||
return new EmailRandomizer(randomizer);
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: DefaultSubaddressOptions,
|
||||
constraints: {},
|
||||
account: SUBADDRESS_SETTINGS,
|
||||
},
|
||||
policy: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {},
|
||||
combine(_acc: NoPolicy, _policy: Policy) {
|
||||
return {};
|
||||
},
|
||||
createEvaluator(_policy: NoPolicy) {
|
||||
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
|
||||
},
|
||||
toConstraints(_policy: NoPolicy) {
|
||||
return new IdentityConstraint<SubaddressGenerationOptions>();
|
||||
},
|
||||
},
|
||||
} satisfies CredentialGeneratorConfiguration<SubaddressGenerationOptions, NoPolicy>);
|
||||
|
||||
/** Generator configurations */
|
||||
export const Generators = Object.freeze({
|
||||
/** Passphrase generator configuration */
|
||||
Passphrase: PASSPHRASE,
|
||||
passphrase: PASSPHRASE,
|
||||
|
||||
/** Password generator configuration */
|
||||
Password: PASSWORD,
|
||||
password: PASSWORD,
|
||||
|
||||
/** Username generator configuration */
|
||||
username: USERNAME,
|
||||
|
||||
/** Catchall email generator configuration */
|
||||
catchall: CATCHALL,
|
||||
|
||||
/** Email subaddress generator configuration */
|
||||
subaddress: SUBADDRESS,
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from "./default-eff-username-options";
|
||||
export * from "./default-firefox-relay-options";
|
||||
export * from "./default-passphrase-generation-options";
|
||||
export * from "./default-password-generation-options";
|
||||
export * from "./default-credential-preferences";
|
||||
export * from "./default-subaddress-generator-options";
|
||||
export * from "./default-simple-login-options";
|
||||
export * from "./forwarders";
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
|
||||
import {
|
||||
DynamicPasswordPolicyConstraints,
|
||||
passphraseLeastPrivilege,
|
||||
passwordLeastPrivilege,
|
||||
PassphraseGeneratorOptionsEvaluator,
|
||||
PassphrasePolicyConstraints,
|
||||
PasswordGeneratorOptionsEvaluator,
|
||||
} from "../policies";
|
||||
import {
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGeneratorPolicy,
|
||||
@@ -16,39 +6,18 @@ import {
|
||||
PolicyConfiguration,
|
||||
} from "../types";
|
||||
|
||||
const PASSPHRASE = Object.freeze({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: Object.freeze({
|
||||
minNumberWords: 0,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
}),
|
||||
combine: passphraseLeastPrivilege,
|
||||
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
|
||||
toConstraints: (policy) => new PassphrasePolicyConstraints(policy),
|
||||
} as PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGenerationOptions>);
|
||||
import { Generators } from "./generators";
|
||||
|
||||
const PASSWORD = Object.freeze({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: Object.freeze({
|
||||
minLength: 0,
|
||||
useUppercase: false,
|
||||
useLowercase: false,
|
||||
useNumbers: false,
|
||||
numberCount: 0,
|
||||
useSpecial: false,
|
||||
specialCount: 0,
|
||||
}),
|
||||
combine: passwordLeastPrivilege,
|
||||
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
|
||||
toConstraints: (policy) => new DynamicPasswordPolicyConstraints(policy),
|
||||
} as PolicyConfiguration<PasswordGeneratorPolicy, PasswordGenerationOptions>);
|
||||
|
||||
/** Policy configurations */
|
||||
/** Policy configurations
|
||||
* @deprecated use Generator.*.policy instead
|
||||
*/
|
||||
export const Policies = Object.freeze({
|
||||
Passphrase: Generators.passphrase.policy,
|
||||
Password: Generators.password.policy,
|
||||
} satisfies {
|
||||
/** Passphrase policy configuration */
|
||||
Passphrase: PASSPHRASE,
|
||||
Passphrase: PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGenerationOptions>;
|
||||
|
||||
/** Passphrase policy configuration */
|
||||
Password: PASSWORD,
|
||||
/** Password policy configuration */
|
||||
Password: PolicyConfiguration<PasswordGeneratorPolicy, PasswordGenerationOptions>;
|
||||
});
|
||||
|
||||
@@ -208,4 +208,40 @@ describe("EmailRandomizer", () => {
|
||||
expect(randomizer.pickWord).toHaveBeenCalledWith(expectedWordList, { titleCase: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("generate", () => {
|
||||
it("processes catchall generation options", async () => {
|
||||
const email = new EmailRandomizer(randomizer);
|
||||
|
||||
const result = await email.generate(
|
||||
{},
|
||||
{
|
||||
catchallDomain: "example.com",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.category).toEqual("catchall");
|
||||
});
|
||||
|
||||
it("processes subaddress generation options", async () => {
|
||||
const email = new EmailRandomizer(randomizer);
|
||||
|
||||
const result = await email.generate(
|
||||
{},
|
||||
{
|
||||
subaddressEmail: "foo@example.com",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.category).toEqual("subaddress");
|
||||
});
|
||||
|
||||
it("throws when it cannot recognize the options type", async () => {
|
||||
const email = new EmailRandomizer(randomizer);
|
||||
|
||||
const result = email.generate({}, {});
|
||||
|
||||
await expect(result).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
||||
|
||||
import {
|
||||
CatchallGenerationOptions,
|
||||
CredentialGenerator,
|
||||
GeneratedCredential,
|
||||
SubaddressGenerationOptions,
|
||||
} from "../types";
|
||||
|
||||
import { Randomizer } from "./abstractions";
|
||||
import { SUBADDRESS_PARSER } from "./data";
|
||||
|
||||
/** Generation algorithms that produce randomized email addresses */
|
||||
export class EmailRandomizer {
|
||||
export class EmailRandomizer
|
||||
implements
|
||||
CredentialGenerator<CatchallGenerationOptions>,
|
||||
CredentialGenerator<SubaddressGenerationOptions>
|
||||
{
|
||||
/** Instantiates the email randomizer
|
||||
* @param random data source for random data
|
||||
*/
|
||||
@@ -96,4 +108,37 @@ export class EmailRandomizer {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
generate(
|
||||
request: GenerationRequest,
|
||||
settings: CatchallGenerationOptions,
|
||||
): Promise<GeneratedCredential>;
|
||||
generate(
|
||||
request: GenerationRequest,
|
||||
settings: SubaddressGenerationOptions,
|
||||
): Promise<GeneratedCredential>;
|
||||
async generate(
|
||||
_request: GenerationRequest,
|
||||
settings: CatchallGenerationOptions | SubaddressGenerationOptions,
|
||||
) {
|
||||
if (isCatchallGenerationOptions(settings)) {
|
||||
const email = await this.randomAsciiCatchall(settings.catchallDomain);
|
||||
|
||||
return new GeneratedCredential(email, "catchall", Date.now());
|
||||
} else if (isSubaddressGenerationOptions(settings)) {
|
||||
const email = await this.randomAsciiSubaddress(settings.subaddressEmail);
|
||||
|
||||
return new GeneratedCredential(email, "subaddress", Date.now());
|
||||
}
|
||||
|
||||
throw new Error("Invalid settings received by generator.");
|
||||
}
|
||||
}
|
||||
|
||||
function isCatchallGenerationOptions(settings: any): settings is CatchallGenerationOptions {
|
||||
return "catchallDomain" in (settings ?? {});
|
||||
}
|
||||
|
||||
function isSubaddressGenerationOptions(settings: any): settings is SubaddressGenerationOptions {
|
||||
return "subaddressEmail" in (settings ?? {});
|
||||
}
|
||||
|
||||
@@ -102,4 +102,27 @@ describe("UsernameRandomizer", () => {
|
||||
expect(randomizer.pickWord).toHaveBeenNthCalledWith(2, EFFLongWordList, { titleCase: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("generate", () => {
|
||||
it("processes username generation options", async () => {
|
||||
const username = new UsernameRandomizer(randomizer);
|
||||
|
||||
const result = await username.generate(
|
||||
{},
|
||||
{
|
||||
wordIncludeNumber: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.category).toEqual("username");
|
||||
});
|
||||
|
||||
it("throws when it cannot recognize the options type", async () => {
|
||||
const username = new UsernameRandomizer(randomizer);
|
||||
|
||||
const result = username.generate({}, {});
|
||||
|
||||
await expect(result).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
||||
|
||||
import { CredentialGenerator, EffUsernameGenerationOptions, GeneratedCredential } from "../types";
|
||||
|
||||
import { Randomizer } from "./abstractions";
|
||||
import { WordsRequest } from "./types";
|
||||
|
||||
/** Generation algorithms that produce randomized usernames */
|
||||
export class UsernameRandomizer {
|
||||
export class UsernameRandomizer implements CredentialGenerator<EffUsernameGenerationOptions> {
|
||||
/** Instantiates the username randomizer
|
||||
* @param random data source for random data
|
||||
*/
|
||||
@@ -44,4 +47,21 @@ export class UsernameRandomizer {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async generate(_request: GenerationRequest, settings: EffUsernameGenerationOptions) {
|
||||
if (isEffUsernameGenerationOptions(settings)) {
|
||||
const username = await this.randomWords({
|
||||
digits: settings.wordIncludeNumber ? 1 : 0,
|
||||
casing: settings.wordCapitalize ? "TitleCase" : "lowercase",
|
||||
});
|
||||
|
||||
return new GeneratedCredential(username, "username", Date.now());
|
||||
}
|
||||
|
||||
throw new Error("Invalid settings received by generator.");
|
||||
}
|
||||
}
|
||||
|
||||
function isEffUsernameGenerationOptions(settings: any): settings is EffUsernameGenerationOptions {
|
||||
return "wordIncludeNumber" in (settings ?? {});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { PolicyId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CredentialAlgorithms, PasswordAlgorithms } from "../data";
|
||||
|
||||
import { availableAlgorithms } from "./available-algorithms-policy";
|
||||
|
||||
describe("availableAlgorithmsPolicy", () => {
|
||||
it("returns all algorithms", () => {
|
||||
const result = availableAlgorithms([]);
|
||||
|
||||
for (const expected of CredentialAlgorithms) {
|
||||
expect(result).toContain(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([["password"], ["passphrase"]])("enforces a %p override", (override) => {
|
||||
const policy = new Policy({
|
||||
id: "" as PolicyId,
|
||||
organizationId: "",
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
overridePasswordType: override,
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const result = availableAlgorithms([policy]);
|
||||
|
||||
expect(result).toContain(override);
|
||||
|
||||
for (const expected of PasswordAlgorithms.filter((a) => a !== override)) {
|
||||
expect(result).not.toContain(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([["password"], ["passphrase"]])("combines %p overrides", (override) => {
|
||||
const policy = new Policy({
|
||||
id: "" as PolicyId,
|
||||
organizationId: "",
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
overridePasswordType: override,
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const result = availableAlgorithms([policy, policy]);
|
||||
|
||||
expect(result).toContain(override);
|
||||
|
||||
for (const expected of PasswordAlgorithms.filter((a) => a !== override)) {
|
||||
expect(result).not.toContain(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("overrides passphrase policies with password policies", () => {
|
||||
const password = new Policy({
|
||||
id: "" as PolicyId,
|
||||
organizationId: "",
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
overridePasswordType: "password",
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
const passphrase = new Policy({
|
||||
id: "" as PolicyId,
|
||||
organizationId: "",
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
overridePasswordType: "passphrase",
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const result = availableAlgorithms([password, passphrase]);
|
||||
|
||||
expect(result).toContain("password");
|
||||
|
||||
for (const expected of PasswordAlgorithms.filter((a) => a !== "password")) {
|
||||
expect(result).not.toContain(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores unrelated policies", () => {
|
||||
const policy = new Policy({
|
||||
id: "" as PolicyId,
|
||||
organizationId: "",
|
||||
type: PolicyType.ActivateAutofill,
|
||||
data: {
|
||||
some: "policy",
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const result = availableAlgorithms([policy]);
|
||||
|
||||
for (const expected of CredentialAlgorithms) {
|
||||
expect(result).toContain(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores disabled policies", () => {
|
||||
const policy = new Policy({
|
||||
id: "" as PolicyId,
|
||||
organizationId: "",
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
some: "policy",
|
||||
},
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const result = availableAlgorithms([policy]);
|
||||
|
||||
for (const expected of CredentialAlgorithms) {
|
||||
expect(result).toContain(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores policies without `overridePasswordType`", () => {
|
||||
const policy = new Policy({
|
||||
id: "" as PolicyId,
|
||||
organizationId: "",
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
some: "policy",
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const result = availableAlgorithms([policy]);
|
||||
|
||||
for (const expected of CredentialAlgorithms) {
|
||||
expect(result).toContain(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import {
|
||||
CredentialAlgorithm,
|
||||
EmailAlgorithms,
|
||||
PasswordAlgorithms,
|
||||
UsernameAlgorithms,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
/** Reduces policies to a set of available algorithms
|
||||
* @param policies the policies to reduce
|
||||
* @returns the resulting `AlgorithmAvailabilityPolicy`
|
||||
*/
|
||||
export function availableAlgorithms(policies: Policy[]): CredentialAlgorithm[] {
|
||||
const overridePassword = policies
|
||||
.filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled)
|
||||
.reduce(
|
||||
(type, policy) => (type === "password" ? type : (policy.data.overridePasswordType ?? type)),
|
||||
null as CredentialAlgorithm,
|
||||
);
|
||||
|
||||
const policy: CredentialAlgorithm[] = [...EmailAlgorithms, ...UsernameAlgorithms];
|
||||
if (overridePassword) {
|
||||
policy.push(overridePassword);
|
||||
} else {
|
||||
policy.push(...PasswordAlgorithms);
|
||||
}
|
||||
|
||||
return policy;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-opti
|
||||
describe("Password generator options builder", () => {
|
||||
describe("constructor()", () => {
|
||||
it("should set the policy object to a copy of the input policy", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
policy.minNumberWords = 10; // arbitrary change for deep equality check
|
||||
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
@@ -25,7 +25,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([1, 2])(
|
||||
"should use the default word boundaries when they are greater than `policy.minNumberWords` (= %i)",
|
||||
(minNumberWords) => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
policy.minNumberWords = minNumberWords;
|
||||
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
@@ -37,7 +37,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([8, 12, 18])(
|
||||
"should use `policy.minNumberWords` (= %i) when it is greater than the default minimum words",
|
||||
(minNumberWords) => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
policy.minNumberWords = minNumberWords;
|
||||
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
@@ -50,7 +50,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([150, 300, 9000])(
|
||||
"should use `policy.minNumberWords` (= %i) when it is greater than the default boundaries",
|
||||
(minNumberWords) => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
policy.minNumberWords = minNumberWords;
|
||||
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
@@ -70,7 +70,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has a numWords greater than the default boundary", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
policy.minNumberWords = DefaultPassphraseBoundaries.numWords.min + 1;
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -78,7 +78,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has capitalize enabled", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
policy.capitalize = true;
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -86,7 +86,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has includeNumber enabled", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
policy.includeNumber = true;
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should set `capitalize` to `true` when the policy overrides it", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
policy.capitalize = true;
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ capitalize: false });
|
||||
@@ -129,7 +129,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should set `includeNumber` to true when the policy overrides it", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
policy.includeNumber = true;
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ includeNumber: false });
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("Password generator options builder", () => {
|
||||
|
||||
describe("constructor()", () => {
|
||||
it("should set the policy object to a copy of the input policy", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.minLength = 10; // arbitrary change for deep equality check
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -32,7 +32,7 @@ describe("Password generator options builder", () => {
|
||||
(minLength) => {
|
||||
expect(minLength).toBeLessThan(DefaultPasswordBoundaries.length.min);
|
||||
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.minLength = minLength;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -47,7 +47,7 @@ describe("Password generator options builder", () => {
|
||||
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.min);
|
||||
expect(expectedLength).toBeLessThanOrEqual(DefaultPasswordBoundaries.length.max);
|
||||
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.minLength = expectedLength;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -62,7 +62,7 @@ describe("Password generator options builder", () => {
|
||||
(expectedLength) => {
|
||||
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.max);
|
||||
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.minLength = expectedLength;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -78,7 +78,7 @@ describe("Password generator options builder", () => {
|
||||
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.min);
|
||||
expect(expectedMinDigits).toBeLessThanOrEqual(DefaultPasswordBoundaries.minDigits.max);
|
||||
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.numberCount = expectedMinDigits;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -93,7 +93,7 @@ describe("Password generator options builder", () => {
|
||||
(expectedMinDigits) => {
|
||||
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.max);
|
||||
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.numberCount = expectedMinDigits;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -113,7 +113,7 @@ describe("Password generator options builder", () => {
|
||||
DefaultPasswordBoundaries.minSpecialCharacters.max,
|
||||
);
|
||||
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.specialCount = expectedSpecialCharacters;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -132,7 +132,7 @@ describe("Password generator options builder", () => {
|
||||
DefaultPasswordBoundaries.minSpecialCharacters.max,
|
||||
);
|
||||
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.specialCount = expectedSpecialCharacters;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -151,7 +151,7 @@ describe("Password generator options builder", () => {
|
||||
(expectedLength, numberCount, specialCount) => {
|
||||
expect(expectedLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
|
||||
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.numberCount = numberCount;
|
||||
policy.specialCount = specialCount;
|
||||
|
||||
@@ -171,7 +171,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has a minlength greater than the default boundary", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.minLength = DefaultPasswordBoundaries.length.min + 1;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -179,7 +179,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has a number count greater than the default boundary", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.numberCount = DefaultPasswordBoundaries.minDigits.min + 1;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -187,7 +187,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has a special character count greater than the default boundary", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.specialCount = DefaultPasswordBoundaries.minSpecialCharacters.min + 1;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -195,7 +195,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has uppercase enabled", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.useUppercase = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -203,7 +203,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has lowercase enabled", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.useLowercase = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -211,7 +211,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has numbers enabled", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.useNumbers = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -219,7 +219,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has special characters enabled", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.useSpecial = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -237,7 +237,7 @@ describe("Password generator options builder", () => {
|
||||
])(
|
||||
"should set `options.uppercase` to '%s' when `policy.useUppercase` is false and `options.uppercase` is '%s'",
|
||||
(expectedUppercase, uppercase) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.useUppercase = false;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, uppercase });
|
||||
@@ -251,7 +251,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([false, true, undefined])(
|
||||
"should set `options.uppercase` (= %s) to true when `policy.useUppercase` is true",
|
||||
(uppercase) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.useUppercase = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, uppercase });
|
||||
@@ -269,7 +269,7 @@ describe("Password generator options builder", () => {
|
||||
])(
|
||||
"should set `options.lowercase` to '%s' when `policy.useLowercase` is false and `options.lowercase` is '%s'",
|
||||
(expectedLowercase, lowercase) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.useLowercase = false;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, lowercase });
|
||||
@@ -283,7 +283,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([false, true, undefined])(
|
||||
"should set `options.lowercase` (= %s) to true when `policy.useLowercase` is true",
|
||||
(lowercase) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.useLowercase = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, lowercase });
|
||||
@@ -301,7 +301,7 @@ describe("Password generator options builder", () => {
|
||||
])(
|
||||
"should set `options.number` to '%s' when `policy.useNumbers` is false and `options.number` is '%s'",
|
||||
(expectedNumber, number) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.useNumbers = false;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, number });
|
||||
@@ -315,7 +315,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([false, true, undefined])(
|
||||
"should set `options.number` (= %s) to true when `policy.useNumbers` is true",
|
||||
(number) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.useNumbers = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, number });
|
||||
@@ -333,7 +333,7 @@ describe("Password generator options builder", () => {
|
||||
])(
|
||||
"should set `options.special` to '%s' when `policy.useSpecial` is false and `options.special` is '%s'",
|
||||
(expectedSpecial, special) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.useSpecial = false;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, special });
|
||||
@@ -347,7 +347,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([false, true, undefined])(
|
||||
"should set `options.special` (= %s) to true when `policy.useSpecial` is true",
|
||||
(special) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.useSpecial = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, special });
|
||||
@@ -447,7 +447,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([1, 2, 3, 4])(
|
||||
"should set `options.minNumber` (= %i) to the minimum it is less than the minimum number",
|
||||
(minNumber) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.numberCount = 5; // arbitrary value greater than minNumber
|
||||
expect(minNumber).toBeLessThan(policy.numberCount);
|
||||
|
||||
@@ -534,7 +534,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([1, 2, 3, 4])(
|
||||
"should set `options.minSpecial` (= %i) to the minimum it is less than the minimum special characters",
|
||||
(minSpecial) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
policy.specialCount = 5; // arbitrary value greater than minSpecial
|
||||
expect(minSpecial).toBeLessThan(policy.specialCount);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ObservableTracker,
|
||||
} from "../../../../../common/spec";
|
||||
import { Randomizer } from "../abstractions";
|
||||
import { Generators } from "../data";
|
||||
import {
|
||||
CredentialGeneratorConfiguration,
|
||||
GeneratedCredential,
|
||||
@@ -33,7 +34,7 @@ const SettingsKey = new UserKeyDefinition<SomeSettings>(GENERATOR_DISK, "SomeSet
|
||||
clearOn: [],
|
||||
});
|
||||
|
||||
// fake policy
|
||||
// fake policies
|
||||
const policyService = mock<PolicyService>();
|
||||
const somePolicy = new Policy({
|
||||
data: { fooPolicy: true },
|
||||
@@ -42,19 +43,43 @@ const somePolicy = new Policy({
|
||||
organizationId: "" as OrganizationId,
|
||||
enabled: true,
|
||||
});
|
||||
const passwordOverridePolicy = new Policy({
|
||||
id: "" as PolicyId,
|
||||
organizationId: "",
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
overridePasswordType: "password",
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const passphraseOverridePolicy = new Policy({
|
||||
id: "" as PolicyId,
|
||||
organizationId: "",
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
overridePasswordType: "passphrase",
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const SomeTime = new Date(1);
|
||||
const SomeCategory = "passphrase";
|
||||
const SomeAlgorithm = "passphrase";
|
||||
const SomeCategory = "password";
|
||||
const SomeNameKey = "passphraseKey";
|
||||
|
||||
// fake the configuration
|
||||
const SomeConfiguration: CredentialGeneratorConfiguration<SomeSettings, SomePolicy> = {
|
||||
id: SomeAlgorithm,
|
||||
category: SomeCategory,
|
||||
nameKey: SomeNameKey,
|
||||
onlyOnRequest: false,
|
||||
engine: {
|
||||
create: (randomizer) => {
|
||||
return {
|
||||
generate: (request, settings) => {
|
||||
const credential = request.website ? `${request.website}|${settings.foo}` : settings.foo;
|
||||
const result = new GeneratedCredential(credential, SomeCategory, SomeTime);
|
||||
const result = new GeneratedCredential(credential, SomeAlgorithm, SomeTime);
|
||||
return Promise.resolve(result);
|
||||
},
|
||||
};
|
||||
@@ -114,7 +139,7 @@ const SomeConfiguration: CredentialGeneratorConfiguration<SomeSettings, SomePoli
|
||||
// fake user information
|
||||
const SomeUser = "SomeUser" as UserId;
|
||||
const AnotherUser = "SomeOtherUser" as UserId;
|
||||
const accountService = new FakeAccountService({
|
||||
const accounts = {
|
||||
[SomeUser]: {
|
||||
name: "some user",
|
||||
email: "some.user@example.com",
|
||||
@@ -125,7 +150,8 @@ const accountService = new FakeAccountService({
|
||||
email: "some.other.user@example.com",
|
||||
emailVerified: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
const accountService = new FakeAccountService(accounts);
|
||||
|
||||
// fake state
|
||||
const stateProvider = new FakeStateProvider(accountService);
|
||||
@@ -149,7 +175,7 @@ describe("CredentialGeneratorService", () => {
|
||||
|
||||
const result = await generated.expectEmission();
|
||||
|
||||
expect(result).toEqual(new GeneratedCredential("value", SomeCategory, SomeTime));
|
||||
expect(result).toEqual(new GeneratedCredential("value", SomeAlgorithm, SomeTime));
|
||||
});
|
||||
|
||||
it("follows the active user", async () => {
|
||||
@@ -165,8 +191,8 @@ describe("CredentialGeneratorService", () => {
|
||||
generated.unsubscribe();
|
||||
|
||||
expect(generated.emissions).toEqual([
|
||||
new GeneratedCredential("some value", SomeCategory, SomeTime),
|
||||
new GeneratedCredential("another value", SomeCategory, SomeTime),
|
||||
new GeneratedCredential("some value", SomeAlgorithm, SomeTime),
|
||||
new GeneratedCredential("another value", SomeAlgorithm, SomeTime),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -182,8 +208,8 @@ describe("CredentialGeneratorService", () => {
|
||||
generated.unsubscribe();
|
||||
|
||||
expect(generated.emissions).toEqual([
|
||||
new GeneratedCredential("some value", SomeCategory, SomeTime),
|
||||
new GeneratedCredential("another value", SomeCategory, SomeTime),
|
||||
new GeneratedCredential("some value", SomeAlgorithm, SomeTime),
|
||||
new GeneratedCredential("another value", SomeAlgorithm, SomeTime),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -200,7 +226,9 @@ describe("CredentialGeneratorService", () => {
|
||||
|
||||
const result = await generated.expectEmission();
|
||||
|
||||
expect(result).toEqual(new GeneratedCredential("some website|value", SomeCategory, SomeTime));
|
||||
expect(result).toEqual(
|
||||
new GeneratedCredential("some website|value", SomeAlgorithm, SomeTime),
|
||||
);
|
||||
});
|
||||
|
||||
it("errors when `website$` errors", async () => {
|
||||
@@ -246,7 +274,7 @@ describe("CredentialGeneratorService", () => {
|
||||
|
||||
const result = await generated.expectEmission();
|
||||
|
||||
expect(result).toEqual(new GeneratedCredential("another", SomeCategory, SomeTime));
|
||||
expect(result).toEqual(new GeneratedCredential("another", SomeAlgorithm, SomeTime));
|
||||
});
|
||||
|
||||
it("emits a generation for a specific user when `user$` emits", async () => {
|
||||
@@ -261,8 +289,8 @@ describe("CredentialGeneratorService", () => {
|
||||
const result = await generated.pauseUntilReceived(2);
|
||||
|
||||
expect(result).toEqual([
|
||||
new GeneratedCredential("value", SomeCategory, SomeTime),
|
||||
new GeneratedCredential("another", SomeCategory, SomeTime),
|
||||
new GeneratedCredential("value", SomeAlgorithm, SomeTime),
|
||||
new GeneratedCredential("another", SomeAlgorithm, SomeTime),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -317,7 +345,7 @@ describe("CredentialGeneratorService", () => {
|
||||
// confirm forwarded emission
|
||||
on$.next();
|
||||
await awaitAsync();
|
||||
expect(results).toEqual([new GeneratedCredential("value", SomeCategory, SomeTime)]);
|
||||
expect(results).toEqual([new GeneratedCredential("value", SomeAlgorithm, SomeTime)]);
|
||||
|
||||
// confirm updating settings does not cause an emission
|
||||
await stateProvider.setUserState(SettingsKey, { foo: "next" }, SomeUser);
|
||||
@@ -330,8 +358,8 @@ describe("CredentialGeneratorService", () => {
|
||||
sub.unsubscribe();
|
||||
|
||||
expect(results).toEqual([
|
||||
new GeneratedCredential("value", SomeCategory, SomeTime),
|
||||
new GeneratedCredential("next", SomeCategory, SomeTime),
|
||||
new GeneratedCredential("value", SomeAlgorithm, SomeTime),
|
||||
new GeneratedCredential("next", SomeAlgorithm, SomeTime),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -370,6 +398,245 @@ describe("CredentialGeneratorService", () => {
|
||||
|
||||
expect(complete).toBeTruthy();
|
||||
});
|
||||
|
||||
// FIXME: test these when the fake state provider can delay its first emission
|
||||
it.todo("emits when settings$ become available if on$ is called before they're ready.");
|
||||
it.todo("emits when website$ become available if on$ is called before they're ready.");
|
||||
});
|
||||
|
||||
describe("algorithms", () => {
|
||||
it("outputs password generation metadata", () => {
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
|
||||
const result = generator.algorithms("password");
|
||||
|
||||
expect(result).toContain(Generators.password);
|
||||
expect(result).toContain(Generators.passphrase);
|
||||
|
||||
// this test shouldn't contain entries outside of the current category
|
||||
expect(result).not.toContain(Generators.username);
|
||||
expect(result).not.toContain(Generators.catchall);
|
||||
});
|
||||
|
||||
it("outputs username generation metadata", () => {
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
|
||||
const result = generator.algorithms("username");
|
||||
|
||||
expect(result).toContain(Generators.username);
|
||||
|
||||
// this test shouldn't contain entries outside of the current category
|
||||
expect(result).not.toContain(Generators.catchall);
|
||||
expect(result).not.toContain(Generators.password);
|
||||
});
|
||||
|
||||
it("outputs email generation metadata", () => {
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
|
||||
const result = generator.algorithms("email");
|
||||
|
||||
expect(result).toContain(Generators.catchall);
|
||||
expect(result).toContain(Generators.subaddress);
|
||||
|
||||
// this test shouldn't contain entries outside of the current category
|
||||
expect(result).not.toContain(Generators.username);
|
||||
expect(result).not.toContain(Generators.password);
|
||||
});
|
||||
|
||||
it("combines metadata across categories", () => {
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
|
||||
const result = generator.algorithms(["username", "email"]);
|
||||
|
||||
expect(result).toContain(Generators.username);
|
||||
expect(result).toContain(Generators.catchall);
|
||||
expect(result).toContain(Generators.subaddress);
|
||||
|
||||
// this test shouldn't contain entries outside of the current categories
|
||||
expect(result).not.toContain(Generators.password);
|
||||
});
|
||||
});
|
||||
|
||||
describe("algorithms$", () => {
|
||||
// these tests cannot use the observable tracker because they return
|
||||
// data that cannot be cloned
|
||||
it("returns password metadata", async () => {
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
|
||||
const result = await firstValueFrom(generator.algorithms$("password"));
|
||||
|
||||
expect(result).toContain(Generators.password);
|
||||
expect(result).toContain(Generators.passphrase);
|
||||
});
|
||||
|
||||
it("returns username metadata", async () => {
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
|
||||
const result = await firstValueFrom(generator.algorithms$("username"));
|
||||
|
||||
expect(result).toContain(Generators.username);
|
||||
});
|
||||
|
||||
it("returns email metadata", async () => {
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
|
||||
const result = await firstValueFrom(generator.algorithms$("email"));
|
||||
|
||||
expect(result).toContain(Generators.catchall);
|
||||
expect(result).toContain(Generators.subaddress);
|
||||
});
|
||||
|
||||
it("returns username and email metadata", async () => {
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
|
||||
const result = await firstValueFrom(generator.algorithms$(["username", "email"]));
|
||||
|
||||
expect(result).toContain(Generators.username);
|
||||
expect(result).toContain(Generators.catchall);
|
||||
expect(result).toContain(Generators.subaddress);
|
||||
});
|
||||
|
||||
// Subsequent tests focus on passwords and passphrases as an example of policy
|
||||
// awareness; they exercise the logic without being comprehensive
|
||||
it("enforces the active user's policy", async () => {
|
||||
const policy$ = new BehaviorSubject([passwordOverridePolicy]);
|
||||
policyService.getAll$.mockReturnValue(policy$);
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
|
||||
const result = await firstValueFrom(generator.algorithms$(["password"]));
|
||||
|
||||
expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser);
|
||||
expect(result).toContain(Generators.password);
|
||||
expect(result).not.toContain(Generators.passphrase);
|
||||
});
|
||||
|
||||
it("follows changes to the active user", async () => {
|
||||
// initialize local account service and state provider because this test is sensitive
|
||||
// to some shared data in `FakeAccountService`.
|
||||
const accountService = new FakeAccountService(accounts);
|
||||
const stateProvider = new FakeStateProvider(accountService);
|
||||
await accountService.switchAccount(SomeUser);
|
||||
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy]));
|
||||
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passphraseOverridePolicy]));
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
const results: any = [];
|
||||
const sub = generator.algorithms$("password").subscribe((r) => results.push(r));
|
||||
|
||||
await accountService.switchAccount(AnotherUser);
|
||||
await awaitAsync();
|
||||
sub.unsubscribe();
|
||||
|
||||
const [someResult, anotherResult] = results;
|
||||
|
||||
expect(policyService.getAll$).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
PolicyType.PasswordGenerator,
|
||||
SomeUser,
|
||||
);
|
||||
expect(someResult).toContain(Generators.password);
|
||||
expect(someResult).not.toContain(Generators.passphrase);
|
||||
|
||||
expect(policyService.getAll$).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
PolicyType.PasswordGenerator,
|
||||
AnotherUser,
|
||||
);
|
||||
expect(anotherResult).toContain(Generators.passphrase);
|
||||
expect(anotherResult).not.toContain(Generators.password);
|
||||
});
|
||||
|
||||
it("reads an arbitrary user's settings", async () => {
|
||||
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy]));
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||
|
||||
const result = await firstValueFrom(generator.algorithms$("password", { userId$ }));
|
||||
|
||||
expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, AnotherUser);
|
||||
expect(result).toContain(Generators.password);
|
||||
expect(result).not.toContain(Generators.passphrase);
|
||||
});
|
||||
|
||||
it("follows changes to the arbitrary user", async () => {
|
||||
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy]));
|
||||
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passphraseOverridePolicy]));
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
const results: any = [];
|
||||
const sub = generator.algorithms$("password", { userId$ }).subscribe((r) => results.push(r));
|
||||
|
||||
userId.next(AnotherUser);
|
||||
await awaitAsync();
|
||||
sub.unsubscribe();
|
||||
|
||||
const [someResult, anotherResult] = results;
|
||||
expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser);
|
||||
expect(someResult).toContain(Generators.password);
|
||||
expect(someResult).not.toContain(Generators.passphrase);
|
||||
|
||||
expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, AnotherUser);
|
||||
expect(anotherResult).toContain(Generators.passphrase);
|
||||
expect(anotherResult).not.toContain(Generators.password);
|
||||
});
|
||||
|
||||
it("errors when the arbitrary user's stream errors", async () => {
|
||||
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy]));
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
let error = null;
|
||||
|
||||
generator.algorithms$("password", { userId$ }).subscribe({
|
||||
error: (e: unknown) => {
|
||||
error = e;
|
||||
},
|
||||
});
|
||||
userId.error({ some: "error" });
|
||||
await awaitAsync();
|
||||
|
||||
expect(error).toEqual({ some: "error" });
|
||||
});
|
||||
|
||||
it("completes when the arbitrary user's stream completes", async () => {
|
||||
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy]));
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
let completed = false;
|
||||
|
||||
generator.algorithms$("password", { userId$ }).subscribe({
|
||||
complete: () => {
|
||||
completed = true;
|
||||
},
|
||||
});
|
||||
userId.complete();
|
||||
await awaitAsync();
|
||||
|
||||
expect(completed).toBeTruthy();
|
||||
});
|
||||
|
||||
it("ignores repeated arbitrary user emissions", async () => {
|
||||
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy]));
|
||||
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
let count = 0;
|
||||
|
||||
const sub = generator.algorithms$("password", { userId$ }).subscribe({
|
||||
next: () => {
|
||||
count++;
|
||||
},
|
||||
});
|
||||
await awaitAsync();
|
||||
userId.next(SomeUser);
|
||||
await awaitAsync();
|
||||
userId.next(SomeUser);
|
||||
await awaitAsync();
|
||||
sub.unsubscribe();
|
||||
|
||||
expect(count).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("settings$", () => {
|
||||
@@ -405,6 +672,11 @@ describe("CredentialGeneratorService", () => {
|
||||
});
|
||||
|
||||
it("follows changes to the active user", async () => {
|
||||
// initialize local accound service and state provider because this test is sensitive
|
||||
// to some shared data in `FakeAccountService`.
|
||||
const accountService = new FakeAccountService(accounts);
|
||||
const stateProvider = new FakeStateProvider(accountService);
|
||||
await accountService.switchAccount(SomeUser);
|
||||
const someSettings = { foo: "value" };
|
||||
const anotherSettings = { foo: "another" };
|
||||
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
concat,
|
||||
concatMap,
|
||||
distinctUntilChanged,
|
||||
endWith,
|
||||
filter,
|
||||
first,
|
||||
firstValueFrom,
|
||||
ignoreElements,
|
||||
map,
|
||||
mergeMap,
|
||||
Observable,
|
||||
race,
|
||||
share,
|
||||
skipUntil,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
withLatestFrom,
|
||||
@@ -18,6 +21,7 @@ import {
|
||||
import { Simplify } from "type-fest";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import {
|
||||
OnDependency,
|
||||
@@ -28,10 +32,21 @@ import { isDynamic } from "@bitwarden/common/tools/state/state-constraints-depen
|
||||
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||
|
||||
import { Randomizer } from "../abstractions";
|
||||
import { Generators } from "../data";
|
||||
import { availableAlgorithms } from "../policies/available-algorithms-policy";
|
||||
import { mapPolicyToConstraints } from "../rx";
|
||||
import {
|
||||
CredentialAlgorithm,
|
||||
CredentialCategories,
|
||||
CredentialCategory,
|
||||
CredentialGeneratorInfo,
|
||||
CredentialPreference,
|
||||
} from "../types";
|
||||
import { CredentialGeneratorConfiguration as Configuration } from "../types/credential-generator-configuration";
|
||||
import { GeneratorConstraints } from "../types/generator-constraints";
|
||||
|
||||
import { PREFERENCES } from "./credential-preferences";
|
||||
|
||||
type Policy$Dependencies = UserDependency;
|
||||
type Settings$Dependencies = Partial<UserDependency>;
|
||||
type Generate$Dependencies = Simplify<Partial<OnDependency> & Partial<UserDependency>> & {
|
||||
@@ -46,6 +61,8 @@ type Generate$Dependencies = Simplify<Partial<OnDependency> & Partial<UserDepend
|
||||
website$?: Observable<string>;
|
||||
};
|
||||
|
||||
type Algorithms$Dependencies = Partial<UserDependency>;
|
||||
|
||||
export class CredentialGeneratorService {
|
||||
constructor(
|
||||
private randomizer: Randomizer,
|
||||
@@ -53,6 +70,9 @@ export class CredentialGeneratorService {
|
||||
private policyService: PolicyService,
|
||||
) {}
|
||||
|
||||
// FIXME: the rxjs methods of this service can be a lot more resilient if
|
||||
// `Subjects` are introduced where sharing occurs
|
||||
|
||||
/** Generates a stream of credentials
|
||||
* @param configuration determines which generator's settings are loaded
|
||||
* @param dependencies.on$ when specified, a new credential is emitted when
|
||||
@@ -76,8 +96,24 @@ export class CredentialGeneratorService {
|
||||
const settingsComplete$ = request$.pipe(ignoreElements(), endWith(true));
|
||||
const complete$ = race(requestComplete$, settingsComplete$);
|
||||
|
||||
// if on$ triggers before settings are loaded, trigger as soon
|
||||
// as they become available.
|
||||
let readyOn$: Observable<any> = null;
|
||||
if (dependencies?.on$) {
|
||||
const NO_EMISSIONS = {};
|
||||
const ready$ = combineLatest([settings$, request$]).pipe(
|
||||
first(null, NO_EMISSIONS),
|
||||
filter((value) => value !== NO_EMISSIONS),
|
||||
share(),
|
||||
);
|
||||
readyOn$ = concat(
|
||||
dependencies.on$?.pipe(switchMap(() => ready$)),
|
||||
dependencies.on$.pipe(skipUntil(ready$)),
|
||||
);
|
||||
}
|
||||
|
||||
// generation proper
|
||||
const generate$ = (dependencies?.on$ ?? settings$).pipe(
|
||||
const generate$ = (readyOn$ ?? settings$).pipe(
|
||||
withLatestFrom(request$, settings$),
|
||||
concatMap(([, request, settings]) => engine.generate(request, settings)),
|
||||
takeUntil(complete$),
|
||||
@@ -86,6 +122,79 @@ export class CredentialGeneratorService {
|
||||
return generate$;
|
||||
}
|
||||
|
||||
/** Emits metadata concerning the provided generation algorithms
|
||||
* @param category the category or categories of interest
|
||||
* @param dependences.userId$ when provided, the algorithms are filter to only
|
||||
* those matching the provided user's policy. Otherwise, emits the algorithms
|
||||
* available to the active user.
|
||||
* @returns An observable that emits algorithm metadata.
|
||||
*/
|
||||
algorithms$(
|
||||
category: CredentialCategory,
|
||||
dependencies?: Algorithms$Dependencies,
|
||||
): Observable<CredentialGeneratorInfo[]>;
|
||||
algorithms$(
|
||||
category: CredentialCategory[],
|
||||
dependencies?: Algorithms$Dependencies,
|
||||
): Observable<CredentialGeneratorInfo[]>;
|
||||
algorithms$(
|
||||
category: CredentialCategory | CredentialCategory[],
|
||||
dependencies?: Algorithms$Dependencies,
|
||||
) {
|
||||
// any cast required here because TypeScript fails to bind `category`
|
||||
// to the union-typed overload of `algorithms`.
|
||||
const algorithms = this.algorithms(category as any);
|
||||
|
||||
// fall back to default bindings
|
||||
const userId$ = dependencies?.userId$ ?? this.stateProvider.activeUserId$;
|
||||
|
||||
// monitor completion
|
||||
const completion$ = userId$.pipe(ignoreElements(), endWith(true));
|
||||
|
||||
// apply policy
|
||||
const algorithms$ = userId$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((userId) => {
|
||||
// complete policy emissions otherwise `switchMap` holds `algorithms$` open indefinitely
|
||||
const policies$ = this.policyService.getAll$(PolicyType.PasswordGenerator, userId).pipe(
|
||||
map((p) => new Set(availableAlgorithms(p))),
|
||||
takeUntil(completion$),
|
||||
);
|
||||
return policies$;
|
||||
}),
|
||||
map((available) => {
|
||||
const filtered = algorithms.filter((c) => available.has(c.id));
|
||||
return filtered;
|
||||
}),
|
||||
);
|
||||
|
||||
return algorithms$;
|
||||
}
|
||||
|
||||
/** Lists metadata for the algorithms in a credential category
|
||||
* @param category the category or categories of interest
|
||||
* @returns A list containing the requested metadata.
|
||||
*/
|
||||
algorithms(category: CredentialCategory): CredentialGeneratorInfo[];
|
||||
algorithms(category: CredentialCategory[]): CredentialGeneratorInfo[];
|
||||
algorithms(category: CredentialCategory | CredentialCategory[]): CredentialGeneratorInfo[] {
|
||||
const categories = Array.isArray(category) ? category : [category];
|
||||
const algorithms = categories
|
||||
.flatMap((c) => CredentialCategories[c])
|
||||
.map((c) => (c === "forwarder" ? null : Generators[c]))
|
||||
.filter((info) => info !== null);
|
||||
|
||||
return algorithms;
|
||||
}
|
||||
|
||||
/** Look up the metadata for a specific generator algorithm
|
||||
* @param id identifies the algorithm
|
||||
* @returns the requested metadata, or `null` if the metadata wasn't found.
|
||||
*/
|
||||
algorithm(id: CredentialAlgorithm): CredentialGeneratorInfo {
|
||||
return (id === "forwarder" ? null : Generators[id]) ?? null;
|
||||
}
|
||||
|
||||
/** Get the settings for the provided configuration
|
||||
* @param configuration determines which generator's settings are loaded
|
||||
* @param dependencies.userId$ identifies the user to which the settings are bound.
|
||||
@@ -125,6 +234,29 @@ export class CredentialGeneratorService {
|
||||
return settings$;
|
||||
}
|
||||
|
||||
/** Get a subject bound to credential generator preferences.
|
||||
* @param dependencies.singleUserId$ identifies the user to which the preferences are bound
|
||||
* @returns a promise that resolves with the subject once `dependencies.singleUserId$`
|
||||
* becomes available.
|
||||
* @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.
|
||||
*/
|
||||
async preferences(
|
||||
dependencies: SingleUserDependency,
|
||||
): Promise<UserStateSubject<CredentialPreference>> {
|
||||
const userId = await firstValueFrom(
|
||||
dependencies.singleUserId$.pipe(filter((userId) => !!userId)),
|
||||
);
|
||||
|
||||
// FIXME: enforce policy
|
||||
const state = this.stateProvider.getUser(userId, PREFERENCES);
|
||||
const subject = new UserStateSubject(state, { ...dependencies });
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
/** Get a subject bound to a specific user's settings
|
||||
* @param configuration determines which generator's settings are loaded
|
||||
* @param dependencies.singleUserId$ identifies the user to which the settings are bound
|
||||
@@ -159,7 +291,7 @@ export class CredentialGeneratorService {
|
||||
const completion$ = dependencies.userId$.pipe(ignoreElements(), endWith(true));
|
||||
|
||||
const constraints$ = dependencies.userId$.pipe(
|
||||
mergeMap((userId) => {
|
||||
switchMap((userId) => {
|
||||
// complete policy emissions otherwise `mergeMap` holds `policies$` open indefinitely
|
||||
const policies$ = this.policyService
|
||||
.getAll$(configuration.policy.type, userId)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { DefaultCredentialPreferences } from "../data";
|
||||
|
||||
import { PREFERENCES } from "./credential-preferences";
|
||||
|
||||
describe("PREFERENCES", () => {
|
||||
describe("deserializer", () => {
|
||||
it.each([[null], [undefined]])("creates new preferences (= %p)", (value) => {
|
||||
const result = PREFERENCES.deserializer(value);
|
||||
|
||||
expect(result).toEqual(DefaultCredentialPreferences);
|
||||
});
|
||||
|
||||
it("fills missing password preferences", () => {
|
||||
const input = { ...DefaultCredentialPreferences };
|
||||
delete input.password;
|
||||
|
||||
const result = PREFERENCES.deserializer(input as any);
|
||||
|
||||
expect(result).toEqual(DefaultCredentialPreferences);
|
||||
});
|
||||
|
||||
it("fills missing email preferences", () => {
|
||||
const input = { ...DefaultCredentialPreferences };
|
||||
delete input.email;
|
||||
|
||||
const result = PREFERENCES.deserializer(input as any);
|
||||
|
||||
expect(result).toEqual(DefaultCredentialPreferences);
|
||||
});
|
||||
|
||||
it("fills missing username preferences", () => {
|
||||
const input = { ...DefaultCredentialPreferences };
|
||||
delete input.username;
|
||||
|
||||
const result = PREFERENCES.deserializer(input as any);
|
||||
|
||||
expect(result).toEqual(DefaultCredentialPreferences);
|
||||
});
|
||||
|
||||
it("converts updated fields to Dates", () => {
|
||||
const input = structuredClone(DefaultCredentialPreferences);
|
||||
input.email.updated = "1970-01-01T00:00:00.100Z" as any;
|
||||
input.password.updated = "1970-01-01T00:00:00.200Z" as any;
|
||||
input.username.updated = "1970-01-01T00:00:00.300Z" as any;
|
||||
|
||||
const result = PREFERENCES.deserializer(input as any);
|
||||
|
||||
expect(result.email.updated).toEqual(new Date(100));
|
||||
expect(result.password.updated).toEqual(new Date(200));
|
||||
expect(result.username.updated).toEqual(new Date(300));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||
|
||||
import { DefaultCredentialPreferences } from "../data";
|
||||
import { CredentialPreference } from "../types";
|
||||
|
||||
/** plaintext password generation options */
|
||||
export const PREFERENCES = new UserKeyDefinition<CredentialPreference>(
|
||||
GENERATOR_DISK,
|
||||
"credentialPreferences",
|
||||
{
|
||||
deserializer: (value) => {
|
||||
const result = (value as any) ?? {};
|
||||
|
||||
for (const key in DefaultCredentialPreferences) {
|
||||
// bind `key` to `category` to transmute the type
|
||||
const category: keyof typeof DefaultCredentialPreferences = key as any;
|
||||
|
||||
const preference = result[category] ?? { ...DefaultCredentialPreferences[category] };
|
||||
if (typeof preference.updated === "string") {
|
||||
preference.updated = new Date(preference.updated);
|
||||
}
|
||||
|
||||
result[category] = preference;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
/** Kinds of credentials that can be stored by the history service
|
||||
* password - a secret consisting of arbitrary characters used to authenticate a user
|
||||
* passphrase - a secret consisting of words used to authenticate a user
|
||||
*/
|
||||
export type CredentialCategory = "password" | "passphrase";
|
||||
@@ -2,16 +2,35 @@ import { UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||
import { Constraints } from "@bitwarden/common/tools/types";
|
||||
|
||||
import { Randomizer } from "../abstractions";
|
||||
import { PolicyConfiguration } from "../types";
|
||||
import { CredentialAlgorithm, CredentialCategory, PolicyConfiguration } from "../types";
|
||||
|
||||
import { CredentialCategory } from "./credential-category";
|
||||
import { CredentialGenerator } from "./credential-generator";
|
||||
|
||||
export type CredentialGeneratorConfiguration<Settings, Policy> = {
|
||||
/** Category describing usage of the credential generated by this configuration
|
||||
/** Credential generator metadata common across credential generators */
|
||||
export type CredentialGeneratorInfo = {
|
||||
/** Uniquely identifies the credential configuration
|
||||
*/
|
||||
id: CredentialAlgorithm;
|
||||
|
||||
/** The kind of credential generated by this configuration */
|
||||
category: CredentialCategory;
|
||||
|
||||
/** Key used to localize the credential name in the I18nService */
|
||||
nameKey: string;
|
||||
|
||||
/** Key used to localize the credential description in the I18nService */
|
||||
descriptionKey?: string;
|
||||
|
||||
/** When true, credential generation must be explicitly requested.
|
||||
* @remarks this property is useful when credential generation
|
||||
* carries side effects, such as configuring a service external
|
||||
* to Bitwarden.
|
||||
*/
|
||||
onlyOnRequest: boolean;
|
||||
};
|
||||
|
||||
/** Credential generator metadata that relies upon typed setting and policy definitions. */
|
||||
export type CredentialGeneratorConfiguration<Settings, Policy> = CredentialGeneratorInfo & {
|
||||
/** An algorithm that generates credentials when ran. */
|
||||
engine: {
|
||||
/** Factory for the generator
|
||||
@@ -28,6 +47,7 @@ export type CredentialGeneratorConfiguration<Settings, Policy> = {
|
||||
/** value used when an account's settings haven't been initialized */
|
||||
initial: Readonly<Partial<Settings>>;
|
||||
|
||||
/** Application-global constraints that apply to account settings */
|
||||
constraints: Constraints<Settings>;
|
||||
|
||||
/** storage location for account-global settings */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CredentialCategory, GeneratedCredential } from ".";
|
||||
import { CredentialAlgorithm, GeneratedCredential } from ".";
|
||||
|
||||
describe("GeneratedCredential", () => {
|
||||
describe("constructor", () => {
|
||||
@@ -34,7 +34,7 @@ describe("GeneratedCredential", () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
credential: "example",
|
||||
category: "password" as CredentialCategory,
|
||||
category: "password" as CredentialAlgorithm,
|
||||
generationDate: 100,
|
||||
});
|
||||
});
|
||||
@@ -42,7 +42,7 @@ describe("GeneratedCredential", () => {
|
||||
it("fromJSON converts Json objects into credentials", () => {
|
||||
const jsonValue = {
|
||||
credential: "example",
|
||||
category: "password" as CredentialCategory,
|
||||
category: "password" as CredentialAlgorithm,
|
||||
generationDate: 100,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { CredentialCategory } from "./credential-category";
|
||||
import { CredentialAlgorithm } from "./generator-type";
|
||||
|
||||
/** A credential generation result */
|
||||
export class GeneratedCredential {
|
||||
@@ -14,7 +14,7 @@ export class GeneratedCredential {
|
||||
*/
|
||||
constructor(
|
||||
readonly credential: string,
|
||||
readonly category: CredentialCategory,
|
||||
readonly category: CredentialAlgorithm,
|
||||
generationDate: Date | number,
|
||||
) {
|
||||
if (typeof generationDate === "number") {
|
||||
|
||||
@@ -1,7 +1,56 @@
|
||||
import { GeneratorTypes, PasswordTypes } from "../data/generator-types";
|
||||
import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "../data/generator-types";
|
||||
|
||||
/** The kind of credential being generated. */
|
||||
export type GeneratorType = (typeof GeneratorTypes)[number];
|
||||
/** A type of password that may be generated by the credential generator. */
|
||||
export type PasswordAlgorithm = (typeof PasswordAlgorithms)[number];
|
||||
|
||||
/** The kinds of passwords that can be generated. */
|
||||
export type PasswordType = (typeof PasswordTypes)[number];
|
||||
/** A type of username that may be generated by the credential generator. */
|
||||
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];
|
||||
|
||||
/** A type of credential that may be generated by the credential generator. */
|
||||
export type CredentialAlgorithm = PasswordAlgorithm | UsernameAlgorithm | EmailAlgorithm;
|
||||
|
||||
/** Compound credential types supported by the credential generator. */
|
||||
export const CredentialCategories = Object.freeze({
|
||||
/** Lists algorithms in the "password" credential category */
|
||||
password: PasswordAlgorithms as Readonly<PasswordAlgorithm[]>,
|
||||
|
||||
/** Lists algorithms in the "username" credential category */
|
||||
username: UsernameAlgorithms as Readonly<UsernameAlgorithm[]>,
|
||||
|
||||
/** Lists algorithms in the "email" credential category */
|
||||
email: EmailAlgorithms as Readonly<EmailAlgorithm[]>,
|
||||
});
|
||||
|
||||
/** Returns true when the input algorithm is a password algorithm. */
|
||||
export function isPasswordAlgorithm(
|
||||
algorithm: CredentialAlgorithm,
|
||||
): algorithm is PasswordAlgorithm {
|
||||
return PasswordAlgorithms.includes(algorithm as any);
|
||||
}
|
||||
|
||||
/** Returns true when the input algorithm is a username algorithm. */
|
||||
export function isUsernameAlgorithm(
|
||||
algorithm: CredentialAlgorithm,
|
||||
): algorithm is UsernameAlgorithm {
|
||||
return UsernameAlgorithms.includes(algorithm as any);
|
||||
}
|
||||
|
||||
/** Returns true when the input algorithm is an email algorithm. */
|
||||
export function isEmailAlgorithm(algorithm: CredentialAlgorithm): algorithm is EmailAlgorithm {
|
||||
return EmailAlgorithms.includes(algorithm as any);
|
||||
}
|
||||
|
||||
/** A type of compound credential that may be generated by the credential generator. */
|
||||
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];
|
||||
updated: Date;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CredentialAlgorithm, PasswordAlgorithm } from "./generator-type";
|
||||
|
||||
export * from "./boundary";
|
||||
export * from "./catchall-generator-options";
|
||||
export * from "./credential-category";
|
||||
export * from "./credential-generator";
|
||||
export * from "./credential-generator-configuration";
|
||||
export * from "./eff-username-generator-options";
|
||||
@@ -17,3 +18,13 @@ export * from "./password-generator-policy";
|
||||
export * from "./policy-configuration";
|
||||
export * from "./subaddress-generator-options";
|
||||
export * from "./word-options";
|
||||
|
||||
/** Provided for backwards compatibility only.
|
||||
* @deprecated Use one of the Algorithm types instead.
|
||||
*/
|
||||
export type GeneratorType = CredentialAlgorithm;
|
||||
|
||||
/** Provided for backwards compatibility only.
|
||||
* @deprecated Use one of the Algorithm types instead.
|
||||
*/
|
||||
export type PasswordType = PasswordAlgorithm;
|
||||
|
||||
Reference in New Issue
Block a user