mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-16793] port credential generator service to providers (#14071)
* introduce extension service * deprecate legacy forwarder types * eliminate repeat algorithm emissions * extend logging to preference management * align forwarder ids with vendor ids * fix duplicate policy emissions; debugging required logger enhancements ----- Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BoundDependency, OnDependency } from "@bitwarden/common/tools/dependencies";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||
|
||||
import {
|
||||
CredentialAlgorithm,
|
||||
GeneratorMetadata,
|
||||
GeneratorProfile,
|
||||
CredentialType,
|
||||
} from "../metadata";
|
||||
import { AlgorithmMetadata } from "../metadata/algorithm-metadata";
|
||||
import {
|
||||
CredentialPreference,
|
||||
ForwarderOptions,
|
||||
GeneratedCredential,
|
||||
GenerateRequest,
|
||||
} from "../types";
|
||||
import { GeneratorConstraints } from "../types/generator-constraints";
|
||||
|
||||
/** Generates credentials used in identity and/or authentication flows.
|
||||
*/
|
||||
export abstract class CredentialGeneratorService {
|
||||
/** Generates a stream of credentials
|
||||
* @param dependencies.on$ Required. A new credential is emitted when this emits.
|
||||
*/
|
||||
abstract generate$: (
|
||||
dependencies: OnDependency<GenerateRequest> & BoundDependency<"account", Account>,
|
||||
) => Observable<GeneratedCredential>;
|
||||
|
||||
/** Emits metadata for the set of algorithms available to a user.
|
||||
* @param type the set of algorithms
|
||||
* @param dependencies.account$ algorithms are filtered to only
|
||||
* those matching the provided account's policy.
|
||||
* @returns An observable that emits algorithm metadata.
|
||||
*/
|
||||
abstract algorithms$: (
|
||||
type: CredentialType,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
) => Observable<AlgorithmMetadata[]>;
|
||||
|
||||
/** Lists metadata for a set of algorithms.
|
||||
* @param type the type or types of algorithms
|
||||
* @returns A list containing the requested metadata.
|
||||
* @remarks this is a raw data interface. To apply rules such as algorithm availability,
|
||||
* use {@link algorithms$} instead.
|
||||
*/
|
||||
abstract algorithms: (type: CredentialType | CredentialType[]) => AlgorithmMetadata[];
|
||||
|
||||
/** 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.
|
||||
*/
|
||||
abstract algorithm: (id: CredentialAlgorithm) => AlgorithmMetadata;
|
||||
|
||||
/** Look up the forwarder metadata for a vendor.
|
||||
* @param id identifies the vendor proving the forwarder
|
||||
*/
|
||||
abstract forwarder: (id: VendorId) => GeneratorMetadata<ForwarderOptions>;
|
||||
|
||||
/** Get a subject bound to credential generator preferences.
|
||||
* @param dependencies.account$ identifies the account to which the preferences are bound
|
||||
* @returns a subject bound to the user's preferences
|
||||
* @remarks Preferences determine which algorithms are used when generating a
|
||||
* credential from a credential category (e.g. `PassX` or `Username`). Preferences
|
||||
* should not be used to hold navigation history. Use {@link @bitwarden/generator-navigation}
|
||||
* instead.
|
||||
*/
|
||||
abstract preferences: (
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
) => UserStateSubject<CredentialPreference>;
|
||||
|
||||
/** Get a subject bound to a specific user's settings. the subject enforces policy for the
|
||||
* settings by automatically updating incorrect values to those allowed by policy.
|
||||
* @param metadata determines which generator's settings are loaded
|
||||
* @param dependencies.account$ identifies the account to which the settings are bound
|
||||
* @param profile identifies the generator profile to load; when this is not specified
|
||||
* the user's account profile is loaded.
|
||||
* @returns a subject bound to the requested user's generator settings
|
||||
* @remarks Generator metadata can be looked up using {@link BuiltIn} and {@link forwarder}.
|
||||
*/
|
||||
abstract settings: <Settings extends object>(
|
||||
metadata: Readonly<GeneratorMetadata<Settings>>,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
profile?: GeneratorProfile,
|
||||
) => UserStateSubject<Settings>;
|
||||
|
||||
/** Get the policy constraints for the provided configuration
|
||||
* @param metadata determines which generator's policy is loaded
|
||||
* @param dependencies.account$ determines which user's policy is loaded
|
||||
* @param profile identifies the generator profile to load; when this is not specified
|
||||
* the user's account profile is loaded.
|
||||
* @returns an observable that emits the policy once `dependencies.account$`
|
||||
* and the policy become available.
|
||||
* @remarks Generator metadata can be looked up using {@link BuiltIn} and {@link forwarder}.
|
||||
*/
|
||||
abstract policy$: <Settings>(
|
||||
metadata: Readonly<GeneratorMetadata<Settings>>,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
profile?: GeneratorProfile,
|
||||
) => Observable<GeneratorConstraints<Settings>>;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
||||
/** Generates credentials used for user authentication
|
||||
* @typeParam Options the credential generation configuration
|
||||
* @typeParam Policy the policy enforced by the generator
|
||||
* @deprecated Use {@link CredentialGeneratorService} instead.
|
||||
*/
|
||||
export abstract class GeneratorService<Options, Policy> {
|
||||
/** An observable monitoring the options saved to disk.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { CredentialGeneratorService } from "./credential-generator-service.abstraction";
|
||||
export { GeneratorService } from "./generator.service.abstraction";
|
||||
export { GeneratorStrategy } from "./generator-strategy.abstraction";
|
||||
export { PolicyEvaluator } from "./policy-evaluator.abstraction";
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { EmailDomainOptions, SelfHostedApiOptions } from "../types";
|
||||
|
||||
export const DefaultAddyIoOptions: SelfHostedApiOptions & EmailDomainOptions = Object.freeze({
|
||||
website: null,
|
||||
baseUrl: "https://app.addy.io",
|
||||
token: "",
|
||||
domain: "",
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
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,6 +0,0 @@
|
||||
import { ApiOptions } from "../types";
|
||||
|
||||
export const DefaultDuckDuckGoOptions: ApiOptions = Object.freeze({
|
||||
website: null,
|
||||
token: "",
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import { ApiOptions, EmailPrefixOptions } from "../types";
|
||||
|
||||
export const DefaultFastmailOptions: ApiOptions & EmailPrefixOptions = Object.freeze({
|
||||
website: "",
|
||||
domain: "",
|
||||
prefix: "",
|
||||
token: "",
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { ApiOptions } from "../types";
|
||||
|
||||
export const DefaultFirefoxRelayOptions: ApiOptions = Object.freeze({
|
||||
website: null,
|
||||
token: "",
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { ApiOptions, EmailDomainOptions } from "../types";
|
||||
|
||||
export const DefaultForwardEmailOptions: ApiOptions & EmailDomainOptions = Object.freeze({
|
||||
website: null,
|
||||
token: "",
|
||||
domain: "",
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { SelfHostedApiOptions } from "../types";
|
||||
|
||||
export const DefaultSimpleLoginOptions: SelfHostedApiOptions = Object.freeze({
|
||||
website: null,
|
||||
baseUrl: "https://app.simplelogin.io",
|
||||
token: "",
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ForwarderMetadata } from "../types";
|
||||
|
||||
/** Metadata about an email forwarding service.
|
||||
* @remarks This is used to populate the forwarder selection list
|
||||
* and to identify forwarding services in error messages.
|
||||
*/
|
||||
export const Forwarders = Object.freeze({
|
||||
/** For https://addy.io/ */
|
||||
AddyIo: Object.freeze({
|
||||
id: "anonaddy",
|
||||
name: "Addy.io",
|
||||
validForSelfHosted: true,
|
||||
} as ForwarderMetadata),
|
||||
|
||||
/** For https://duckduckgo.com/email/ */
|
||||
DuckDuckGo: Object.freeze({
|
||||
id: "duckduckgo",
|
||||
name: "DuckDuckGo",
|
||||
validForSelfHosted: false,
|
||||
} as ForwarderMetadata),
|
||||
|
||||
/** For https://www.fastmail.com. */
|
||||
Fastmail: Object.freeze({
|
||||
id: "fastmail",
|
||||
name: "Fastmail",
|
||||
validForSelfHosted: true,
|
||||
} as ForwarderMetadata),
|
||||
|
||||
/** For https://relay.firefox.com/ */
|
||||
FirefoxRelay: Object.freeze({
|
||||
id: "firefoxrelay",
|
||||
name: "Firefox Relay",
|
||||
validForSelfHosted: false,
|
||||
} as ForwarderMetadata),
|
||||
|
||||
/** For https://forwardemail.net/ */
|
||||
ForwardEmail: Object.freeze({
|
||||
id: "forwardemail",
|
||||
name: "Forward Email",
|
||||
validForSelfHosted: true,
|
||||
} as ForwarderMetadata),
|
||||
|
||||
/** For https://simplelogin.io/ */
|
||||
SimpleLogin: Object.freeze({
|
||||
id: "simplelogin",
|
||||
name: "SimpleLogin",
|
||||
validForSelfHosted: true,
|
||||
} as ForwarderMetadata),
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
/** Types of passwords that may be generated by the credential generator */
|
||||
export const PasswordAlgorithms = Object.freeze(["password", "passphrase"] 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", "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,422 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
|
||||
import { ApiSettings } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
|
||||
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
|
||||
|
||||
import {
|
||||
EmailRandomizer,
|
||||
ForwarderConfiguration,
|
||||
PasswordRandomizer,
|
||||
UsernameRandomizer,
|
||||
} from "../engine";
|
||||
import { Forwarder } from "../engine/forwarder";
|
||||
import {
|
||||
DefaultPolicyEvaluator,
|
||||
DynamicPasswordPolicyConstraints,
|
||||
PassphraseGeneratorOptionsEvaluator,
|
||||
passphraseLeastPrivilege,
|
||||
PassphrasePolicyConstraints,
|
||||
PasswordGeneratorOptionsEvaluator,
|
||||
passwordLeastPrivilege,
|
||||
} from "../policies";
|
||||
import { CatchallConstraints } from "../policies/catchall-constraints";
|
||||
import { SubaddressConstraints } from "../policies/subaddress-constraints";
|
||||
import {
|
||||
CatchallGenerationOptions,
|
||||
CredentialGenerator,
|
||||
CredentialGeneratorConfiguration,
|
||||
EffUsernameGenerationOptions,
|
||||
GeneratorDependencyProvider,
|
||||
NoPolicy,
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGeneratorPolicy,
|
||||
PasswordGenerationOptions,
|
||||
PasswordGeneratorPolicy,
|
||||
SubaddressGenerationOptions,
|
||||
} from "../types";
|
||||
|
||||
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 { DefaultSubaddressOptions } from "./default-subaddress-generator-options";
|
||||
|
||||
const PASSPHRASE: CredentialGeneratorConfiguration<
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGeneratorPolicy
|
||||
> = Object.freeze({
|
||||
id: "passphrase",
|
||||
category: "password",
|
||||
nameKey: "passphrase",
|
||||
generateKey: "generatePassphrase",
|
||||
onGeneratedMessageKey: "passphraseGenerated",
|
||||
credentialTypeKey: "passphrase",
|
||||
copyKey: "copyPassphrase",
|
||||
useGeneratedValueKey: "useThisPassword",
|
||||
onlyOnRequest: false,
|
||||
request: [],
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<PassphraseGenerationOptions> {
|
||||
return new PasswordRandomizer(dependencies.randomizer);
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: DefaultPassphraseGenerationOptions,
|
||||
constraints: {
|
||||
numWords: {
|
||||
min: DefaultPassphraseBoundaries.numWords.min,
|
||||
max: DefaultPassphraseBoundaries.numWords.max,
|
||||
recommendation: DefaultPassphraseGenerationOptions.numWords,
|
||||
},
|
||||
wordSeparator: { maxLength: 1 },
|
||||
},
|
||||
account: {
|
||||
key: "passphraseGeneratorSettings",
|
||||
target: "object",
|
||||
format: "plain",
|
||||
classifier: new PublicClassifier<PassphraseGenerationOptions>([
|
||||
"numWords",
|
||||
"wordSeparator",
|
||||
"capitalize",
|
||||
"includeNumber",
|
||||
]),
|
||||
state: GENERATOR_DISK,
|
||||
initial: DefaultPassphraseGenerationOptions,
|
||||
options: {
|
||||
deserializer: (value) => value,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
} satisfies ObjectKey<PassphraseGenerationOptions>,
|
||||
},
|
||||
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, PASSPHRASE.settings.constraints),
|
||||
},
|
||||
});
|
||||
|
||||
const PASSWORD: CredentialGeneratorConfiguration<
|
||||
PasswordGenerationOptions,
|
||||
PasswordGeneratorPolicy
|
||||
> = Object.freeze({
|
||||
id: "password",
|
||||
category: "password",
|
||||
nameKey: "password",
|
||||
generateKey: "generatePassword",
|
||||
onGeneratedMessageKey: "passwordGenerated",
|
||||
credentialTypeKey: "password",
|
||||
copyKey: "copyPassword",
|
||||
useGeneratedValueKey: "useThisPassword",
|
||||
onlyOnRequest: false,
|
||||
request: [],
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<PasswordGenerationOptions> {
|
||||
return new PasswordRandomizer(dependencies.randomizer);
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: DefaultPasswordGenerationOptions,
|
||||
constraints: {
|
||||
length: {
|
||||
min: DefaultPasswordBoundaries.length.min,
|
||||
max: DefaultPasswordBoundaries.length.max,
|
||||
recommendation: DefaultPasswordGenerationOptions.length,
|
||||
},
|
||||
minNumber: {
|
||||
min: DefaultPasswordBoundaries.minDigits.min,
|
||||
max: DefaultPasswordBoundaries.minDigits.max,
|
||||
},
|
||||
minSpecial: {
|
||||
min: DefaultPasswordBoundaries.minSpecialCharacters.min,
|
||||
max: DefaultPasswordBoundaries.minSpecialCharacters.max,
|
||||
},
|
||||
},
|
||||
account: {
|
||||
key: "passwordGeneratorSettings",
|
||||
target: "object",
|
||||
format: "plain",
|
||||
classifier: new PublicClassifier<PasswordGenerationOptions>([
|
||||
"length",
|
||||
"ambiguous",
|
||||
"uppercase",
|
||||
"minUppercase",
|
||||
"lowercase",
|
||||
"minLowercase",
|
||||
"number",
|
||||
"minNumber",
|
||||
"special",
|
||||
"minSpecial",
|
||||
]),
|
||||
state: GENERATOR_DISK,
|
||||
initial: DefaultPasswordGenerationOptions,
|
||||
options: {
|
||||
deserializer: (value) => value,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
} satisfies ObjectKey<PasswordGenerationOptions>,
|
||||
},
|
||||
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, PASSWORD.settings.constraints),
|
||||
},
|
||||
});
|
||||
|
||||
const USERNAME: CredentialGeneratorConfiguration<EffUsernameGenerationOptions, NoPolicy> =
|
||||
Object.freeze({
|
||||
id: "username",
|
||||
category: "username",
|
||||
nameKey: "randomWord",
|
||||
generateKey: "generateUsername",
|
||||
onGeneratedMessageKey: "usernameGenerated",
|
||||
credentialTypeKey: "username",
|
||||
copyKey: "copyUsername",
|
||||
useGeneratedValueKey: "useThisUsername",
|
||||
onlyOnRequest: false,
|
||||
request: [],
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<EffUsernameGenerationOptions> {
|
||||
return new UsernameRandomizer(dependencies.randomizer);
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: DefaultEffUsernameOptions,
|
||||
constraints: {},
|
||||
account: {
|
||||
key: "effUsernameGeneratorSettings",
|
||||
target: "object",
|
||||
format: "plain",
|
||||
classifier: new PublicClassifier<EffUsernameGenerationOptions>([
|
||||
"wordCapitalize",
|
||||
"wordIncludeNumber",
|
||||
]),
|
||||
state: GENERATOR_DISK,
|
||||
initial: DefaultEffUsernameOptions,
|
||||
options: {
|
||||
deserializer: (value) => value,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
} satisfies ObjectKey<EffUsernameGenerationOptions>,
|
||||
},
|
||||
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>();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const CATCHALL: CredentialGeneratorConfiguration<CatchallGenerationOptions, NoPolicy> =
|
||||
Object.freeze({
|
||||
id: "catchall",
|
||||
category: "email",
|
||||
nameKey: "catchallEmail",
|
||||
descriptionKey: "catchallEmailDesc",
|
||||
generateKey: "generateEmail",
|
||||
onGeneratedMessageKey: "emailGenerated",
|
||||
credentialTypeKey: "email",
|
||||
copyKey: "copyEmail",
|
||||
useGeneratedValueKey: "useThisEmail",
|
||||
onlyOnRequest: false,
|
||||
request: [],
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<CatchallGenerationOptions> {
|
||||
return new EmailRandomizer(dependencies.randomizer);
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: DefaultCatchallOptions,
|
||||
constraints: { catchallDomain: { minLength: 1 } },
|
||||
account: {
|
||||
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"],
|
||||
},
|
||||
} satisfies ObjectKey<CatchallGenerationOptions>,
|
||||
},
|
||||
policy: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {},
|
||||
combine(_acc: NoPolicy, _policy: Policy) {
|
||||
return {};
|
||||
},
|
||||
createEvaluator(_policy: NoPolicy) {
|
||||
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
|
||||
},
|
||||
toConstraints(_policy: NoPolicy, email: string) {
|
||||
return new CatchallConstraints(email);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const SUBADDRESS: CredentialGeneratorConfiguration<SubaddressGenerationOptions, NoPolicy> =
|
||||
Object.freeze({
|
||||
id: "subaddress",
|
||||
category: "email",
|
||||
nameKey: "plusAddressedEmail",
|
||||
descriptionKey: "plusAddressedEmailDesc",
|
||||
generateKey: "generateEmail",
|
||||
onGeneratedMessageKey: "emailGenerated",
|
||||
credentialTypeKey: "email",
|
||||
copyKey: "copyEmail",
|
||||
useGeneratedValueKey: "useThisEmail",
|
||||
onlyOnRequest: false,
|
||||
request: [],
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<SubaddressGenerationOptions> {
|
||||
return new EmailRandomizer(dependencies.randomizer);
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: DefaultSubaddressOptions,
|
||||
constraints: {},
|
||||
account: {
|
||||
key: "subaddressGeneratorSettings",
|
||||
target: "object",
|
||||
format: "plain",
|
||||
classifier: new PublicClassifier<SubaddressGenerationOptions>([
|
||||
"subaddressType",
|
||||
"subaddressEmail",
|
||||
]),
|
||||
state: GENERATOR_DISK,
|
||||
initial: {
|
||||
subaddressType: "random",
|
||||
subaddressEmail: "",
|
||||
},
|
||||
options: {
|
||||
deserializer: (value) => value,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
} satisfies ObjectKey<SubaddressGenerationOptions>,
|
||||
},
|
||||
policy: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {},
|
||||
combine(_acc: NoPolicy, _policy: Policy) {
|
||||
return {};
|
||||
},
|
||||
createEvaluator(_policy: NoPolicy) {
|
||||
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
|
||||
},
|
||||
toConstraints(_policy: NoPolicy, email: string) {
|
||||
return new SubaddressConstraints(email);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function toCredentialGeneratorConfiguration<Settings extends ApiSettings = ApiSettings>(
|
||||
configuration: ForwarderConfiguration<Settings>,
|
||||
) {
|
||||
const forwarder = Object.freeze({
|
||||
id: { forwarder: configuration.id },
|
||||
category: "email",
|
||||
nameKey: configuration.name,
|
||||
descriptionKey: "forwardedEmailDesc",
|
||||
generateKey: "generateEmail",
|
||||
onGeneratedMessageKey: "emailGenerated",
|
||||
credentialTypeKey: "email",
|
||||
copyKey: "copyEmail",
|
||||
useGeneratedValueKey: "useThisEmail",
|
||||
onlyOnRequest: true,
|
||||
request: configuration.forwarder.request,
|
||||
engine: {
|
||||
create(dependencies: GeneratorDependencyProvider) {
|
||||
// FIXME: figure out why `configuration` fails to typecheck
|
||||
const config: any = configuration;
|
||||
return new Forwarder(config, dependencies.client, dependencies.i18nService);
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: configuration.forwarder.defaultSettings,
|
||||
constraints: configuration.forwarder.settingsConstraints,
|
||||
account: configuration.forwarder.local.settings,
|
||||
},
|
||||
policy: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {},
|
||||
combine(_acc: NoPolicy, _policy: Policy) {
|
||||
return {};
|
||||
},
|
||||
createEvaluator(_policy: NoPolicy) {
|
||||
return new DefaultPolicyEvaluator<Settings>();
|
||||
},
|
||||
toConstraints(_policy: NoPolicy) {
|
||||
return new IdentityConstraint<Settings>();
|
||||
},
|
||||
},
|
||||
} satisfies CredentialGeneratorConfiguration<Settings, NoPolicy>);
|
||||
|
||||
return forwarder;
|
||||
}
|
||||
|
||||
/** Generator configurations */
|
||||
export const Generators = Object.freeze({
|
||||
/** Passphrase generator configuration */
|
||||
passphrase: PASSPHRASE,
|
||||
|
||||
/** Password generator configuration */
|
||||
password: PASSWORD,
|
||||
|
||||
/** Username generator configuration */
|
||||
username: USERNAME,
|
||||
|
||||
/** Catchall email generator configuration */
|
||||
catchall: CATCHALL,
|
||||
|
||||
/** Email subaddress generator configuration */
|
||||
subaddress: SUBADDRESS,
|
||||
});
|
||||
@@ -1,20 +1,8 @@
|
||||
export * from "./generators";
|
||||
export * from "./default-addy-io-options";
|
||||
export * from "./default-catchall-options";
|
||||
export * from "./default-duck-duck-go-options";
|
||||
export * from "./default-fastmail-options";
|
||||
export * from "./default-forward-email-options";
|
||||
export * from "./default-passphrase-boundaries";
|
||||
export * from "./default-password-boundaries";
|
||||
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";
|
||||
export * from "./integrations";
|
||||
export * from "./policies";
|
||||
export * from "./username-digits";
|
||||
export * from "./generator-types";
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import {
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGeneratorPolicy,
|
||||
PasswordGenerationOptions,
|
||||
PasswordGeneratorPolicy,
|
||||
PolicyConfiguration,
|
||||
} from "../types";
|
||||
|
||||
import { Generators } from "./generators";
|
||||
|
||||
/** 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: PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGenerationOptions>;
|
||||
|
||||
/** Password policy configuration */
|
||||
Password: PolicyConfiguration<PasswordGeneratorPolicy, PasswordGenerationOptions>;
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
export const UsernameDigits = Object.freeze({
|
||||
enabled: 4,
|
||||
disabled: 0,
|
||||
});
|
||||
@@ -2,6 +2,8 @@ import { mock } from "jest-mock-extended";
|
||||
|
||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||
|
||||
import { Algorithm, Type } from "../metadata";
|
||||
|
||||
import { Randomizer } from "./abstractions";
|
||||
import { EmailRandomizer } from "./email-randomizer";
|
||||
|
||||
@@ -41,7 +43,8 @@ describe("EmailRandomizer", () => {
|
||||
async (email) => {
|
||||
const emailRandomizer = new EmailRandomizer(randomizer);
|
||||
|
||||
const result = await emailRandomizer.randomAsciiSubaddress(email);
|
||||
// this tests what happens when the type system is subverted
|
||||
const result = await emailRandomizer.randomAsciiSubaddress(email!);
|
||||
|
||||
expect(result).toEqual("");
|
||||
},
|
||||
@@ -100,7 +103,8 @@ describe("EmailRandomizer", () => {
|
||||
it.each([[null], [undefined], [""]])("returns null if the domain is %p", async (domain) => {
|
||||
const emailRandomizer = new EmailRandomizer(randomizer);
|
||||
|
||||
const result = await emailRandomizer.randomAsciiCatchall(domain);
|
||||
// this tests what happens when the type system is subverted
|
||||
const result = await emailRandomizer.randomAsciiCatchall(domain!);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
@@ -150,7 +154,8 @@ describe("EmailRandomizer", () => {
|
||||
it.each([[null], [undefined], [""]])("returns null if the domain is %p", async (domain) => {
|
||||
const emailRandomizer = new EmailRandomizer(randomizer);
|
||||
|
||||
const result = await emailRandomizer.randomWordsCatchall(domain);
|
||||
// this tests what happens when the type system is subverted
|
||||
const result = await emailRandomizer.randomWordsCatchall(domain!);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
@@ -214,32 +219,32 @@ describe("EmailRandomizer", () => {
|
||||
const email = new EmailRandomizer(randomizer);
|
||||
|
||||
const result = await email.generate(
|
||||
{},
|
||||
{ algorithm: Algorithm.catchall },
|
||||
{
|
||||
catchallDomain: "example.com",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.category).toEqual("catchall");
|
||||
expect(result.category).toEqual(Type.email);
|
||||
});
|
||||
|
||||
it("processes subaddress generation options", async () => {
|
||||
const email = new EmailRandomizer(randomizer);
|
||||
|
||||
const result = await email.generate(
|
||||
{},
|
||||
{ algorithm: Algorithm.plusAddress },
|
||||
{
|
||||
subaddressEmail: "foo@example.com",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.category).toEqual("subaddress");
|
||||
expect(result.category).toEqual(Type.email);
|
||||
});
|
||||
|
||||
it("throws when it cannot recognize the options type", async () => {
|
||||
const email = new EmailRandomizer(randomizer);
|
||||
|
||||
const result = email.generate({}, {});
|
||||
const result = email.generate({ algorithm: Algorithm.password }, {});
|
||||
|
||||
await expect(result).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||
|
||||
import { Type } from "../metadata";
|
||||
import {
|
||||
CatchallGenerationOptions,
|
||||
CredentialGenerator,
|
||||
@@ -128,7 +129,7 @@ export class EmailRandomizer
|
||||
|
||||
return new GeneratedCredential(
|
||||
email,
|
||||
"catchall",
|
||||
Type.email,
|
||||
Date.now(),
|
||||
request.source,
|
||||
request.website,
|
||||
@@ -138,7 +139,7 @@ export class EmailRandomizer
|
||||
|
||||
return new GeneratedCredential(
|
||||
email,
|
||||
"subaddress",
|
||||
Type.email,
|
||||
Date.now(),
|
||||
request.source,
|
||||
request.website,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@bitwarden/common/tools/integration/rpc";
|
||||
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
||||
|
||||
import { Type } from "../metadata";
|
||||
import { CredentialGenerator, GeneratedCredential } from "../types";
|
||||
|
||||
import { AccountRequest, ForwarderConfiguration } from "./forwarder-configuration";
|
||||
@@ -40,9 +41,8 @@ export class Forwarder implements CredentialGenerator<ApiSettings> {
|
||||
|
||||
const create = this.createForwardingAddress(this.configuration, settings);
|
||||
const result = await this.client.fetchJson(create, requestOptions);
|
||||
const id = { forwarder: this.configuration.id };
|
||||
|
||||
return new GeneratedCredential(result, id, Date.now());
|
||||
return new GeneratedCredential(result, Type.email, Date.now());
|
||||
}
|
||||
|
||||
private createContext<Settings>(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended";
|
||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||
|
||||
import { Randomizer } from "../abstractions";
|
||||
import { Algorithm, Type } from "../metadata";
|
||||
|
||||
import { Ascii } from "./data";
|
||||
import { PasswordRandomizer } from "./password-randomizer";
|
||||
@@ -341,32 +342,32 @@ describe("PasswordRandomizer", () => {
|
||||
const password = new PasswordRandomizer(randomizer);
|
||||
|
||||
const result = await password.generate(
|
||||
{},
|
||||
{ algorithm: Algorithm.password },
|
||||
{
|
||||
length: 10,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.category).toEqual("password");
|
||||
expect(result.category).toEqual(Type.password);
|
||||
});
|
||||
|
||||
it("processes passphrase generation options", async () => {
|
||||
const password = new PasswordRandomizer(randomizer);
|
||||
|
||||
const result = await password.generate(
|
||||
{},
|
||||
{ algorithm: Algorithm.passphrase },
|
||||
{
|
||||
numWords: 10,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.category).toEqual("passphrase");
|
||||
expect(result.category).toEqual(Type.password);
|
||||
});
|
||||
|
||||
it("throws when it cannot recognize the options type", async () => {
|
||||
const password = new PasswordRandomizer(randomizer);
|
||||
|
||||
const result = password.generate({}, {});
|
||||
const result = password.generate({ algorithm: Algorithm.username }, {});
|
||||
|
||||
await expect(result).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||
|
||||
import { Type } from "../metadata";
|
||||
import {
|
||||
CredentialGenerator,
|
||||
GenerateRequest,
|
||||
@@ -86,7 +87,7 @@ export class PasswordRandomizer
|
||||
|
||||
return new GeneratedCredential(
|
||||
password,
|
||||
"password",
|
||||
Type.password,
|
||||
Date.now(),
|
||||
request.source,
|
||||
request.website,
|
||||
@@ -97,7 +98,7 @@ export class PasswordRandomizer
|
||||
|
||||
return new GeneratedCredential(
|
||||
passphrase,
|
||||
"passphrase",
|
||||
Type.password,
|
||||
Date.now(),
|
||||
request.source,
|
||||
request.website,
|
||||
|
||||
@@ -2,6 +2,8 @@ import { mock } from "jest-mock-extended";
|
||||
|
||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||
|
||||
import { Algorithm, Type } from "../metadata";
|
||||
|
||||
import { Randomizer } from "./abstractions";
|
||||
import { UsernameRandomizer } from "./username-randomizer";
|
||||
|
||||
@@ -108,19 +110,19 @@ describe("UsernameRandomizer", () => {
|
||||
const username = new UsernameRandomizer(randomizer);
|
||||
|
||||
const result = await username.generate(
|
||||
{},
|
||||
{ algorithm: Algorithm.username },
|
||||
{
|
||||
wordIncludeNumber: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.category).toEqual("username");
|
||||
expect(result.category).toEqual(Type.username);
|
||||
});
|
||||
|
||||
it("throws when it cannot recognize the options type", async () => {
|
||||
const username = new UsernameRandomizer(randomizer);
|
||||
|
||||
const result = username.generate({}, {});
|
||||
const result = username.generate({ algorithm: Algorithm.passphrase }, {});
|
||||
|
||||
await expect(result).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
@@ -3,13 +3,34 @@ export * from "./abstractions";
|
||||
export * from "./data";
|
||||
export { createRandomizer } from "./factories";
|
||||
export * from "./types";
|
||||
export { CredentialGeneratorService } from "./services";
|
||||
export { DefaultCredentialGeneratorService } from "./services";
|
||||
export {
|
||||
CredentialType,
|
||||
CredentialAlgorithm,
|
||||
PasswordAlgorithm,
|
||||
Algorithm,
|
||||
BuiltIn,
|
||||
Type,
|
||||
Profile,
|
||||
GeneratorMetadata,
|
||||
GeneratorProfile,
|
||||
AlgorithmMetadata,
|
||||
AlgorithmsByType,
|
||||
} from "./metadata";
|
||||
export {
|
||||
isForwarderExtensionId,
|
||||
isEmailAlgorithm,
|
||||
isUsernameAlgorithm,
|
||||
isPasswordAlgorithm,
|
||||
isSameAlgorithm,
|
||||
} from "./metadata/util";
|
||||
|
||||
// These internal interfacess are exposed for use by other generator modules
|
||||
// They are unstable and may change arbitrarily
|
||||
export * as engine from "./engine";
|
||||
export * as integration from "./integration";
|
||||
export * as policies from "./policies";
|
||||
export * as providers from "./providers";
|
||||
export * as rx from "./rx";
|
||||
export * as services from "./services";
|
||||
export * as strategies from "./strategies";
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { Vendor } from "@bitwarden/common/tools/extension/vendor/data";
|
||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import {
|
||||
ApiSettings,
|
||||
@@ -101,7 +102,7 @@ const forwarder = Object.freeze({
|
||||
|
||||
export const AddyIo = Object.freeze({
|
||||
// integration
|
||||
id: "anonaddy" as IntegrationId & VendorId,
|
||||
id: Vendor.addyio as IntegrationId & VendorId,
|
||||
name: "Addy.io",
|
||||
extends: ["forwarder"],
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { Vendor } from "@bitwarden/common/tools/extension/vendor/data";
|
||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||
@@ -90,7 +91,7 @@ const forwarder = Object.freeze({
|
||||
|
||||
// integration-wide configuration
|
||||
export const DuckDuckGo = Object.freeze({
|
||||
id: "duckduckgo" as IntegrationId & VendorId,
|
||||
id: Vendor.duckduckgo as IntegrationId & VendorId,
|
||||
name: "DuckDuckGo",
|
||||
baseUrl: "https://quack.duckduckgo.com/api",
|
||||
selfHost: "never",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { Vendor } from "@bitwarden/common/tools/extension/vendor/data";
|
||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||
@@ -160,7 +161,7 @@ const forwarder = Object.freeze({
|
||||
|
||||
// integration-wide configuration
|
||||
export const Fastmail = Object.freeze({
|
||||
id: "fastmail" as IntegrationId & VendorId,
|
||||
id: Vendor.fastmail as IntegrationId & VendorId,
|
||||
name: "Fastmail",
|
||||
baseUrl: "https://api.fastmail.com",
|
||||
selfHost: "maybe",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { Vendor } from "@bitwarden/common/tools/extension/vendor/data";
|
||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||
@@ -98,7 +99,7 @@ const forwarder = Object.freeze({
|
||||
|
||||
// integration-wide configuration
|
||||
export const FirefoxRelay = Object.freeze({
|
||||
id: "firefoxrelay" as IntegrationId & VendorId,
|
||||
id: Vendor.mozilla as IntegrationId & VendorId,
|
||||
name: "Firefox Relay",
|
||||
baseUrl: "https://relay.firefox.com/api",
|
||||
selfHost: "never",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { Vendor } from "@bitwarden/common/tools/extension/vendor/data";
|
||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||
@@ -102,7 +103,7 @@ const forwarder = Object.freeze({
|
||||
|
||||
export const ForwardEmail = Object.freeze({
|
||||
// integration metadata
|
||||
id: "forwardemail" as IntegrationId & VendorId,
|
||||
id: Vendor.forwardemail as IntegrationId & VendorId,
|
||||
name: "Forward Email",
|
||||
extends: ["forwarder"],
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { Vendor } from "@bitwarden/common/tools/extension/vendor/data";
|
||||
import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
import {
|
||||
ApiSettings,
|
||||
@@ -104,7 +105,7 @@ const forwarder = Object.freeze({
|
||||
|
||||
// integration-wide configuration
|
||||
export const SimpleLogin = Object.freeze({
|
||||
id: "simplelogin" as IntegrationId & VendorId,
|
||||
id: Vendor.simplelogin as IntegrationId & VendorId,
|
||||
name: "SimpleLogin",
|
||||
selfHost: "maybe",
|
||||
extends: ["forwarder"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CredentialAlgorithm, CredentialType } from "./type";
|
||||
import { I18nKeyOrLiteral } from "@bitwarden/common/tools/types";
|
||||
|
||||
type I18nKeyOrLiteral = string | { literal: string };
|
||||
import { CredentialAlgorithm, CredentialType } from "./type";
|
||||
|
||||
/** Credential generator metadata common across credential generators */
|
||||
export type AlgorithmMetadata = {
|
||||
@@ -14,7 +14,7 @@ export type AlgorithmMetadata = {
|
||||
id: CredentialAlgorithm;
|
||||
|
||||
/** The kind of credential generated by this configuration */
|
||||
category: CredentialType;
|
||||
type: CredentialType;
|
||||
|
||||
/** Used to order credential algorithms for display purposes.
|
||||
* Items with lesser weights appear before entries with greater
|
||||
@@ -23,6 +23,10 @@ export type AlgorithmMetadata = {
|
||||
weight: number;
|
||||
|
||||
/** Localization keys */
|
||||
// FIXME: in practice, keys like `credentialGenerated` all align
|
||||
// with credential types and contain duplicate keys. Extract
|
||||
// them into a "credential type metadata" type and integrate
|
||||
// that type with the algorithm metadata instead.
|
||||
i18nKeys: {
|
||||
/** descriptive name of the algorithm */
|
||||
name: I18nKeyOrLiteral;
|
||||
|
||||
@@ -2,7 +2,8 @@ import { mock } from "jest-mock-extended";
|
||||
|
||||
import { EmailRandomizer } from "../../engine";
|
||||
import { CatchallConstraints } from "../../policies/catchall-constraints";
|
||||
import { CatchallGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
||||
import { GeneratorDependencyProvider } from "../../providers";
|
||||
import { CatchallGenerationOptions } from "../../types";
|
||||
import { Profile } from "../data";
|
||||
import { CoreProfileMetadata } from "../profile-metadata";
|
||||
import { isCoreProfile } from "../util";
|
||||
|
||||
@@ -4,17 +4,14 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
|
||||
import { EmailRandomizer } from "../../engine";
|
||||
import { CatchallConstraints } from "../../policies/catchall-constraints";
|
||||
import {
|
||||
CatchallGenerationOptions,
|
||||
CredentialGenerator,
|
||||
GeneratorDependencyProvider,
|
||||
} from "../../types";
|
||||
import { GeneratorDependencyProvider } from "../../providers";
|
||||
import { CatchallGenerationOptions, CredentialGenerator } from "../../types";
|
||||
import { Algorithm, Type, Profile } from "../data";
|
||||
import { GeneratorMetadata } from "../generator-metadata";
|
||||
|
||||
const catchall: GeneratorMetadata<CatchallGenerationOptions> = deepFreeze({
|
||||
id: Algorithm.catchall,
|
||||
category: Type.email,
|
||||
type: Type.email,
|
||||
weight: 210,
|
||||
i18nKeys: {
|
||||
name: "catchallEmail",
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { ExtensionMetadata, ExtensionStorageKey } from "@bitwarden/common/tools/extension/type";
|
||||
import { SelfHostedApiSettings } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||
|
||||
import { getForwarderConfiguration } from "../../data";
|
||||
import { EmailDomainSettings, EmailPrefixSettings } from "../../engine";
|
||||
import { Forwarder } from "../../engine/forwarder";
|
||||
import { GeneratorDependencyProvider } from "../../types";
|
||||
import { GeneratorDependencyProvider } from "../../providers";
|
||||
import { ForwarderOptions } from "../../types";
|
||||
import { Profile, Type } from "../data";
|
||||
import { GeneratorMetadata } from "../generator-metadata";
|
||||
import { ForwarderProfileMetadata } from "../profile-metadata";
|
||||
|
||||
// These options are used by all forwarders; each forwarder uses a different set,
|
||||
// as defined by `GeneratorMetadata<T>.capabilities.fields`.
|
||||
type ForwarderOptions = Partial<EmailDomainSettings & EmailPrefixSettings & SelfHostedApiSettings>;
|
||||
|
||||
// update the extension metadata
|
||||
export function toForwarderMetadata(
|
||||
extension: ExtensionMetadata,
|
||||
@@ -28,7 +23,7 @@ export function toForwarderMetadata(
|
||||
|
||||
const generator: GeneratorMetadata<ForwarderOptions> = {
|
||||
id: { forwarder: extension.product.vendor.id },
|
||||
category: Type.email,
|
||||
type: Type.email,
|
||||
weight: 300,
|
||||
i18nKeys: {
|
||||
name,
|
||||
@@ -56,6 +51,12 @@ export function toForwarderMetadata(
|
||||
storage: {
|
||||
key: "forwarder",
|
||||
frame: 512,
|
||||
initial: {
|
||||
token: "",
|
||||
baseUrl: "",
|
||||
domain: "",
|
||||
prefix: "",
|
||||
},
|
||||
options: {
|
||||
deserializer: (value) => value,
|
||||
clearOn: ["logout"],
|
||||
|
||||
@@ -2,7 +2,8 @@ import { mock } from "jest-mock-extended";
|
||||
|
||||
import { EmailRandomizer } from "../../engine";
|
||||
import { SubaddressConstraints } from "../../policies/subaddress-constraints";
|
||||
import { SubaddressGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
||||
import { GeneratorDependencyProvider } from "../../providers";
|
||||
import { SubaddressGenerationOptions } from "../../types";
|
||||
import { Profile } from "../data";
|
||||
import { CoreProfileMetadata } from "../profile-metadata";
|
||||
import { isCoreProfile } from "../util";
|
||||
|
||||
@@ -4,17 +4,14 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
|
||||
import { EmailRandomizer } from "../../engine";
|
||||
import { SubaddressConstraints } from "../../policies/subaddress-constraints";
|
||||
import {
|
||||
CredentialGenerator,
|
||||
GeneratorDependencyProvider,
|
||||
SubaddressGenerationOptions,
|
||||
} from "../../types";
|
||||
import { GeneratorDependencyProvider } from "../../providers";
|
||||
import { CredentialGenerator, 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,
|
||||
type: Type.email,
|
||||
weight: 200,
|
||||
i18nKeys: {
|
||||
name: "plusAddressedEmail",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CredentialGenerator, GeneratorDependencyProvider } from "../types";
|
||||
import { GeneratorDependencyProvider } from "../providers";
|
||||
import { CredentialGenerator } from "../types";
|
||||
|
||||
import { AlgorithmMetadata } from "./algorithm-metadata";
|
||||
import { Profile } from "./data";
|
||||
|
||||
@@ -3,7 +3,32 @@ import {
|
||||
AlgorithmsByType as AlgorithmsByTypeData,
|
||||
Type as TypeData,
|
||||
} from "./data";
|
||||
import catchall from "./email/catchall";
|
||||
import plusAddress from "./email/plus-address";
|
||||
import passphrase from "./password/eff-word-list";
|
||||
import password from "./password/random-password";
|
||||
import { CredentialType, CredentialAlgorithm } from "./type";
|
||||
import effWordList from "./username/eff-word-list";
|
||||
|
||||
/** Credential generators hosted natively by the credential generator system.
|
||||
* These are supplemented by generators from the {@link ExtensionService}.
|
||||
*/
|
||||
export const BuiltIn = Object.freeze({
|
||||
/** Catchall email address generator */
|
||||
catchall,
|
||||
|
||||
/** plus-addressed email address generator */
|
||||
plusAddress,
|
||||
|
||||
/** passphrase generator using the EFF word list */
|
||||
passphrase,
|
||||
|
||||
/** password generator */
|
||||
password,
|
||||
|
||||
/** username generator using the EFF word list */
|
||||
effWordList,
|
||||
});
|
||||
|
||||
// `CredentialAlgorithm` is defined in terms of `ABT`; supplying
|
||||
// type information in the barrel file breaks a circular dependency.
|
||||
@@ -12,14 +37,29 @@ export const AlgorithmsByType: Record<
|
||||
CredentialType,
|
||||
ReadonlyArray<CredentialAlgorithm>
|
||||
> = AlgorithmsByTypeData;
|
||||
|
||||
/** A list of all built-in algorithm identifiers
|
||||
* @remarks this is useful when you need to filter invalid values
|
||||
*/
|
||||
export const Algorithms: ReadonlyArray<CredentialAlgorithm> = Object.freeze(
|
||||
Object.values(AlgorithmData),
|
||||
);
|
||||
|
||||
/** A list of all built-in algorithm types
|
||||
* @remarks this is useful when you need to filter invalid values
|
||||
*/
|
||||
export const Types: ReadonlyArray<CredentialType> = Object.freeze(Object.values(TypeData));
|
||||
|
||||
export { Profile, Type, Algorithm } from "./data";
|
||||
export { toForwarderMetadata } from "./email/forwarder";
|
||||
export { AlgorithmMetadata } from "./algorithm-metadata";
|
||||
export { GeneratorMetadata } from "./generator-metadata";
|
||||
export { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "./profile-metadata";
|
||||
export { GeneratorProfile, CredentialAlgorithm, CredentialType } from "./type";
|
||||
export {
|
||||
GeneratorProfile,
|
||||
CredentialAlgorithm,
|
||||
PasswordAlgorithm,
|
||||
CredentialType,
|
||||
ForwarderExtensionId,
|
||||
} from "./type";
|
||||
export { isForwarderProfile, toVendorId, isForwarderExtensionId } from "./util";
|
||||
|
||||
@@ -5,7 +5,8 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
|
||||
import { PasswordRandomizer } from "../../engine";
|
||||
import { PassphrasePolicyConstraints } from "../../policies";
|
||||
import { PassphraseGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
||||
import { GeneratorDependencyProvider } from "../../providers";
|
||||
import { PassphraseGenerationOptions } from "../../types";
|
||||
import { Profile } from "../data";
|
||||
import { CoreProfileMetadata } from "../profile-metadata";
|
||||
import { isCoreProfile } from "../util";
|
||||
|
||||
@@ -5,17 +5,14 @@ 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 { GeneratorDependencyProvider } from "../../providers";
|
||||
import { CredentialGenerator, PassphraseGenerationOptions } from "../../types";
|
||||
import { Algorithm, Profile, Type } from "../data";
|
||||
import { GeneratorMetadata } from "../generator-metadata";
|
||||
|
||||
const passphrase: GeneratorMetadata<PassphraseGenerationOptions> = {
|
||||
id: Algorithm.passphrase,
|
||||
category: Type.password,
|
||||
type: Type.password,
|
||||
weight: 110,
|
||||
i18nKeys: {
|
||||
name: "passphrase",
|
||||
@@ -26,7 +23,7 @@ const passphrase: GeneratorMetadata<PassphraseGenerationOptions> = {
|
||||
useCredential: "useThisPassphrase",
|
||||
},
|
||||
capabilities: {
|
||||
autogenerate: false,
|
||||
autogenerate: true,
|
||||
fields: [],
|
||||
},
|
||||
engine: {
|
||||
|
||||
@@ -5,7 +5,8 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
|
||||
import { PasswordRandomizer } from "../../engine";
|
||||
import { DynamicPasswordPolicyConstraints } from "../../policies";
|
||||
import { PasswordGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
||||
import { GeneratorDependencyProvider } from "../../providers";
|
||||
import { PasswordGenerationOptions } from "../../types";
|
||||
import { Profile } from "../data";
|
||||
import { CoreProfileMetadata } from "../profile-metadata";
|
||||
import { isCoreProfile } from "../util";
|
||||
|
||||
@@ -5,17 +5,14 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
|
||||
import { PasswordRandomizer } from "../../engine";
|
||||
import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies";
|
||||
import {
|
||||
CredentialGenerator,
|
||||
GeneratorDependencyProvider,
|
||||
PasswordGeneratorSettings,
|
||||
} from "../../types";
|
||||
import { GeneratorDependencyProvider } from "../../providers";
|
||||
import { CredentialGenerator, 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,
|
||||
type: Type.password,
|
||||
weight: 100,
|
||||
i18nKeys: {
|
||||
name: "password",
|
||||
|
||||
@@ -3,7 +3,8 @@ 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 { GeneratorDependencyProvider } from "../../providers";
|
||||
import { EffUsernameGenerationOptions } from "../../types";
|
||||
import { Profile } from "../data";
|
||||
import { CoreProfileMetadata } from "../profile-metadata";
|
||||
import { isCoreProfile } from "../util";
|
||||
|
||||
@@ -4,17 +4,14 @@ import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state
|
||||
import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
|
||||
import { UsernameRandomizer } from "../../engine";
|
||||
import {
|
||||
CredentialGenerator,
|
||||
EffUsernameGenerationOptions,
|
||||
GeneratorDependencyProvider,
|
||||
} from "../../types";
|
||||
import { GeneratorDependencyProvider } from "../../providers";
|
||||
import { CredentialGenerator, EffUsernameGenerationOptions } from "../../types";
|
||||
import { Algorithm, Profile, Type } from "../data";
|
||||
import { GeneratorMetadata } from "../generator-metadata";
|
||||
|
||||
const effWordList: GeneratorMetadata<EffUsernameGenerationOptions> = deepFreeze({
|
||||
id: Algorithm.username,
|
||||
category: Type.username,
|
||||
type: Type.username,
|
||||
weight: 400,
|
||||
i18nKeys: {
|
||||
name: "randomWord",
|
||||
|
||||
@@ -12,23 +12,23 @@ import {
|
||||
|
||||
/** Returns true when the input algorithm is a password algorithm. */
|
||||
export function isPasswordAlgorithm(
|
||||
algorithm: CredentialAlgorithm,
|
||||
algorithm: CredentialAlgorithm | null,
|
||||
): 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: CredentialAlgorithm | null,
|
||||
): 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: CredentialAlgorithm | null,
|
||||
): algorithm is ForwarderExtensionId {
|
||||
return algorithm && typeof algorithm === "object" && "forwarder" in algorithm;
|
||||
return !!(algorithm && typeof algorithm === "object" && "forwarder" in algorithm);
|
||||
}
|
||||
|
||||
/** Extract a `VendorId` from a `CredentialAlgorithm`.
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
||||
import { Constraints, StateConstraints } from "@bitwarden/common/tools/types";
|
||||
|
||||
import { CredentialAlgorithm, CredentialType } from "../metadata";
|
||||
import { CredentialPreference } from "../types";
|
||||
import { TypeRequest } from "../types/metadata-request";
|
||||
|
||||
export class AvailableAlgorithmsConstraint implements StateConstraints<CredentialPreference> {
|
||||
/** Well-known constraints of `State` */
|
||||
readonly constraints: Readonly<Constraints<CredentialPreference>> = {};
|
||||
|
||||
/** Creates a password policy constraints
|
||||
* @param algorithms loads the algorithms for an algorithm type
|
||||
* @param isAvailable returns `true` when `algorithm` is enabled by policy
|
||||
* @param system provides logging facilities
|
||||
*/
|
||||
constructor(
|
||||
readonly algorithms: (request: TypeRequest) => CredentialAlgorithm[],
|
||||
readonly isAvailable: (algorithm: CredentialAlgorithm) => boolean,
|
||||
readonly system: UserStateSubjectDependencyProvider,
|
||||
) {
|
||||
this.log = system.log({ type: "AvailableAlgorithmsConstraint" });
|
||||
}
|
||||
private readonly log: SemanticLogger;
|
||||
|
||||
adjust(preferences: CredentialPreference): CredentialPreference {
|
||||
const result: any = {};
|
||||
|
||||
const types = Object.keys(preferences) as CredentialType[];
|
||||
for (const t of types) {
|
||||
result[t] = this.adjustPreference(t, preferences[t]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private adjustPreference(type: CredentialType, preference: { algorithm: CredentialAlgorithm }) {
|
||||
if (this.isAvailable(preference.algorithm)) {
|
||||
this.log.debug({ preference, type }, "using preferred algorithm");
|
||||
|
||||
return preference;
|
||||
}
|
||||
|
||||
// choose a default - this algorithm is arbitrary, but stable.
|
||||
const algorithms = type ? this.algorithms({ type: type }) : [];
|
||||
const defaultAlgorithm = algorithms.find(this.isAvailable) ?? null;
|
||||
|
||||
// adjust the preference
|
||||
let adjustedPreference;
|
||||
if (defaultAlgorithm) {
|
||||
adjustedPreference = {
|
||||
...preference,
|
||||
algorithm: defaultAlgorithm,
|
||||
updated: this.system.now(),
|
||||
};
|
||||
this.log.debug(
|
||||
{ preference, defaultAlgorithm, type },
|
||||
"preference not available; defaulting the algorithm",
|
||||
);
|
||||
} else {
|
||||
// FIXME: hard-code a fallback in category metadata
|
||||
this.log.warn(
|
||||
{ preference, type },
|
||||
"preference not available and default algorithm not found; continuing with preference",
|
||||
);
|
||||
adjustedPreference = preference;
|
||||
}
|
||||
|
||||
return adjustedPreference;
|
||||
}
|
||||
|
||||
fix(preferences: CredentialPreference): CredentialPreference {
|
||||
return preferences;
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,15 @@ 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 { Algorithm, Algorithms, AlgorithmsByType } from "../metadata";
|
||||
|
||||
import { availableAlgorithms } from "./available-algorithms-policy";
|
||||
|
||||
describe("availableAlgorithmsPolicy", () => {
|
||||
describe("availableAlgorithms_vNextPolicy", () => {
|
||||
it("returns all algorithms", () => {
|
||||
const result = availableAlgorithms([]);
|
||||
|
||||
for (const expected of CredentialAlgorithms) {
|
||||
for (const expected of Algorithms) {
|
||||
expect(result).toContain(expected);
|
||||
}
|
||||
});
|
||||
@@ -30,7 +30,7 @@ describe("availableAlgorithmsPolicy", () => {
|
||||
|
||||
expect(result).toContain(override);
|
||||
|
||||
for (const expected of PasswordAlgorithms.filter((a) => a !== override)) {
|
||||
for (const expected of AlgorithmsByType[Algorithm.password].filter((a) => a !== override)) {
|
||||
expect(result).not.toContain(expected);
|
||||
}
|
||||
});
|
||||
@@ -50,7 +50,7 @@ describe("availableAlgorithmsPolicy", () => {
|
||||
|
||||
expect(result).toContain(override);
|
||||
|
||||
for (const expected of PasswordAlgorithms.filter((a) => a !== override)) {
|
||||
for (const expected of AlgorithmsByType[Algorithm.password].filter((a) => a !== override)) {
|
||||
expect(result).not.toContain(expected);
|
||||
}
|
||||
});
|
||||
@@ -79,7 +79,7 @@ describe("availableAlgorithmsPolicy", () => {
|
||||
|
||||
expect(result).toContain("password");
|
||||
|
||||
for (const expected of PasswordAlgorithms.filter((a) => a !== "password")) {
|
||||
for (const expected of AlgorithmsByType[Algorithm.password].filter((a) => a !== "password")) {
|
||||
expect(result).not.toContain(expected);
|
||||
}
|
||||
});
|
||||
@@ -97,7 +97,7 @@ describe("availableAlgorithmsPolicy", () => {
|
||||
|
||||
const result = availableAlgorithms([policy]);
|
||||
|
||||
for (const expected of CredentialAlgorithms) {
|
||||
for (const expected of Algorithms) {
|
||||
expect(result).toContain(expected);
|
||||
}
|
||||
});
|
||||
@@ -115,7 +115,7 @@ describe("availableAlgorithmsPolicy", () => {
|
||||
|
||||
const result = availableAlgorithms([policy]);
|
||||
|
||||
for (const expected of CredentialAlgorithms) {
|
||||
for (const expected of Algorithms) {
|
||||
expect(result).toContain(expected);
|
||||
}
|
||||
});
|
||||
@@ -133,7 +133,7 @@ describe("availableAlgorithmsPolicy", () => {
|
||||
|
||||
const result = availableAlgorithms([policy]);
|
||||
|
||||
for (const expected of CredentialAlgorithms) {
|
||||
for (const expected of Algorithms) {
|
||||
expect(result).toContain(expected);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,57 +1,30 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
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 as LegacyAlgorithm,
|
||||
EmailAlgorithms,
|
||||
PasswordAlgorithms,
|
||||
UsernameAlgorithms,
|
||||
} from "..";
|
||||
import { CredentialAlgorithm } from "../metadata";
|
||||
import { AlgorithmsByType, CredentialAlgorithm, Type } from "../metadata";
|
||||
|
||||
/** Reduces policies to a set of available algorithms
|
||||
* @param policies the policies to reduce
|
||||
* @returns the resulting `AlgorithmAvailabilityPolicy`
|
||||
*/
|
||||
export function availableAlgorithms(policies: Policy[]): LegacyAlgorithm[] {
|
||||
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 LegacyAlgorithm,
|
||||
null as CredentialAlgorithm | null,
|
||||
);
|
||||
|
||||
const policy: LegacyAlgorithm[] = [...EmailAlgorithms, ...UsernameAlgorithms];
|
||||
const policy: CredentialAlgorithm[] = [
|
||||
...AlgorithmsByType[Type.email],
|
||||
...AlgorithmsByType[Type.username],
|
||||
];
|
||||
if (overridePassword) {
|
||||
policy.push(overridePassword);
|
||||
} else {
|
||||
policy.push(...PasswordAlgorithms);
|
||||
}
|
||||
|
||||
return policy;
|
||||
}
|
||||
|
||||
/** Reduces policies to a set of available algorithms
|
||||
* @param policies the policies to reduce
|
||||
* @returns the resulting `AlgorithmAvailabilityPolicy`
|
||||
*/
|
||||
export function availableAlgorithms_vNext(policies: Policy[]): CredentialAlgorithm[] {
|
||||
const overridePassword = policies
|
||||
.filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled)
|
||||
.reduce(
|
||||
(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);
|
||||
policy.push(...AlgorithmsByType[Type.password]);
|
||||
}
|
||||
|
||||
return policy;
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
|
||||
|
||||
import { Generators } from "../data";
|
||||
import { BuiltIn, Profile } from "../metadata";
|
||||
import { PasswordGeneratorSettings } from "../types";
|
||||
|
||||
import { AtLeastOne, Zero } from "./constraints";
|
||||
import { DynamicPasswordPolicyConstraints } from "./dynamic-password-policy-constraints";
|
||||
|
||||
const accoutSettings = Generators.password.settings.account as ObjectKey<PasswordGeneratorSettings>;
|
||||
const defaultOptions = accoutSettings.initial;
|
||||
const disabledPolicy = Generators.password.policy.disabledValue;
|
||||
const someConstraints = Generators.password.settings.constraints;
|
||||
// non-null assertions used because these are always-defined constants
|
||||
const accoutSettings = BuiltIn.password.profiles[Profile.account]!
|
||||
.storage as ObjectKey<PasswordGeneratorSettings>;
|
||||
const defaultOptions = accoutSettings.initial!;
|
||||
const disabledPolicy = {
|
||||
minLength: 0,
|
||||
useUppercase: false,
|
||||
useLowercase: false,
|
||||
useNumbers: false,
|
||||
numberCount: 0,
|
||||
useSpecial: false,
|
||||
specialCount: 0,
|
||||
};
|
||||
const someConstraints = BuiltIn.password.profiles[Profile.account]!.constraints.default;
|
||||
|
||||
describe("DynamicPasswordPolicyConstraints", () => {
|
||||
describe("constructor", () => {
|
||||
@@ -33,8 +43,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||
|
||||
expect(constraints.policyInEffect).toBeTruthy();
|
||||
expect(constraints.lowercase.readonly).toEqual(true);
|
||||
expect(constraints.lowercase.requiredValue).toEqual(true);
|
||||
expect(constraints.lowercase?.readonly).toEqual(true);
|
||||
expect(constraints.lowercase?.requiredValue).toEqual(true);
|
||||
expect(constraints.minLowercase).toEqual({ min: 1 });
|
||||
});
|
||||
|
||||
@@ -43,8 +53,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||
|
||||
expect(constraints.policyInEffect).toBeTruthy();
|
||||
expect(constraints.uppercase.readonly).toEqual(true);
|
||||
expect(constraints.uppercase.requiredValue).toEqual(true);
|
||||
expect(constraints.uppercase?.readonly).toEqual(true);
|
||||
expect(constraints.uppercase?.requiredValue).toEqual(true);
|
||||
expect(constraints.minUppercase).toEqual({ min: 1 });
|
||||
});
|
||||
|
||||
@@ -53,8 +63,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||
|
||||
expect(constraints.policyInEffect).toBeTruthy();
|
||||
expect(constraints.number.readonly).toEqual(true);
|
||||
expect(constraints.number.requiredValue).toEqual(true);
|
||||
expect(constraints.number?.readonly).toEqual(true);
|
||||
expect(constraints.number?.requiredValue).toEqual(true);
|
||||
expect(constraints.minNumber).toEqual({ min: 1, max: 9 });
|
||||
});
|
||||
|
||||
@@ -63,8 +73,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||
|
||||
expect(constraints.policyInEffect).toBeTruthy();
|
||||
expect(constraints.special.readonly).toEqual(true);
|
||||
expect(constraints.special.requiredValue).toEqual(true);
|
||||
expect(constraints.special?.readonly).toEqual(true);
|
||||
expect(constraints.special?.requiredValue).toEqual(true);
|
||||
expect(constraints.minSpecial).toEqual({ min: 1, max: 9 });
|
||||
});
|
||||
|
||||
@@ -73,8 +83,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||
|
||||
expect(constraints.policyInEffect).toBeTruthy();
|
||||
expect(constraints.number.readonly).toEqual(true);
|
||||
expect(constraints.number.requiredValue).toEqual(true);
|
||||
expect(constraints.number?.readonly).toEqual(true);
|
||||
expect(constraints.number?.requiredValue).toEqual(true);
|
||||
expect(constraints.minNumber).toEqual({ min: 2, max: 9 });
|
||||
});
|
||||
|
||||
@@ -83,8 +93,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||
|
||||
expect(constraints.policyInEffect).toBeTruthy();
|
||||
expect(constraints.special.readonly).toEqual(true);
|
||||
expect(constraints.special.requiredValue).toEqual(true);
|
||||
expect(constraints.special?.readonly).toEqual(true);
|
||||
expect(constraints.special?.requiredValue).toEqual(true);
|
||||
expect(constraints.minSpecial).toEqual({ min: 2, max: 9 });
|
||||
});
|
||||
|
||||
@@ -140,7 +150,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
||||
const dynamic = new DynamicPasswordPolicyConstraints(
|
||||
{
|
||||
...disabledPolicy,
|
||||
useLowercase,
|
||||
// the `undefined` case is testing behavior when the type system is bypassed
|
||||
useLowercase: useLowercase!,
|
||||
},
|
||||
someConstraints,
|
||||
);
|
||||
@@ -185,7 +196,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
||||
const dynamic = new DynamicPasswordPolicyConstraints(
|
||||
{
|
||||
...disabledPolicy,
|
||||
useUppercase,
|
||||
// the `undefined` case is testing behavior when the type system is bypassed
|
||||
useUppercase: useUppercase!,
|
||||
},
|
||||
someConstraints,
|
||||
);
|
||||
|
||||
@@ -5,3 +5,5 @@ export { PassphrasePolicyConstraints } from "./passphrase-policy-constraints";
|
||||
export { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
||||
export { passphraseLeastPrivilege } from "./passphrase-least-privilege";
|
||||
export { passwordLeastPrivilege } from "./password-least-privilege";
|
||||
export { AvailableAlgorithmsConstraint } from "./available-algorithms-constraint";
|
||||
export { availableAlgorithms } from "./available-algorithms-policy";
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
import { Policies, DefaultPassphraseBoundaries } from "../data";
|
||||
import { PassphraseGenerationOptions } from "../types";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
|
||||
import { DefaultPassphraseBoundaries } from "../data";
|
||||
import {
|
||||
PassphraseGenerationOptions,
|
||||
PassphraseGeneratorPolicy,
|
||||
PolicyConfiguration,
|
||||
} from "../types";
|
||||
|
||||
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
||||
import { passphraseLeastPrivilege } from "./passphrase-least-privilege";
|
||||
|
||||
describe("Password generator options builder", () => {
|
||||
const Passphrase: PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGenerationOptions> =
|
||||
deepFreeze({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {
|
||||
minNumberWords: 0,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
},
|
||||
combine: passphraseLeastPrivilege,
|
||||
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
|
||||
});
|
||||
|
||||
describe("Passphrase generator options builder", () => {
|
||||
describe("constructor()", () => {
|
||||
it("should set the policy object to a copy of the input policy", () => {
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Passphrase.disabledValue);
|
||||
policy.minNumberWords = 10; // arbitrary change for deep equality check
|
||||
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
@@ -16,7 +36,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should set default boundaries when a default policy is used", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
|
||||
expect(builder.numWords).toEqual(DefaultPassphraseBoundaries.numWords);
|
||||
@@ -25,7 +45,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: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Passphrase.disabledValue);
|
||||
policy.minNumberWords = minNumberWords;
|
||||
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
@@ -37,7 +57,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: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Passphrase.disabledValue);
|
||||
policy.minNumberWords = minNumberWords;
|
||||
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
@@ -50,7 +70,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: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Passphrase.disabledValue);
|
||||
policy.minNumberWords = minNumberWords;
|
||||
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
@@ -63,14 +83,14 @@ describe("Password generator options builder", () => {
|
||||
|
||||
describe("policyInEffect", () => {
|
||||
it("should return false when the policy has no effect", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
|
||||
expect(builder.policyInEffect).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true when the policy has a numWords greater than the default boundary", () => {
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Passphrase.disabledValue);
|
||||
policy.minNumberWords = DefaultPassphraseBoundaries.numWords.min + 1;
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -78,7 +98,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has capitalize enabled", () => {
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Passphrase.disabledValue);
|
||||
policy.capitalize = true;
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -86,7 +106,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has includeNumber enabled", () => {
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Passphrase.disabledValue);
|
||||
policy.includeNumber = true;
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -98,7 +118,7 @@ describe("Password generator options builder", () => {
|
||||
// All tests should freeze the options to ensure they are not modified
|
||||
|
||||
it("should set `capitalize` to `false` when the policy does not override it", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({});
|
||||
|
||||
@@ -108,7 +128,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should set `capitalize` to `true` when the policy overrides it", () => {
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Passphrase.disabledValue);
|
||||
policy.capitalize = true;
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ capitalize: false });
|
||||
@@ -119,7 +139,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should set `includeNumber` to false when the policy does not override it", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({});
|
||||
|
||||
@@ -129,7 +149,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should set `includeNumber` to true when the policy overrides it", () => {
|
||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy: any = Object.assign({}, Passphrase.disabledValue);
|
||||
policy.includeNumber = true;
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ includeNumber: false });
|
||||
@@ -140,7 +160,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should set `numWords` to the minimum value when it isn't supplied", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({});
|
||||
|
||||
@@ -154,7 +174,7 @@ describe("Password generator options builder", () => {
|
||||
(numWords) => {
|
||||
expect(numWords).toBeLessThan(DefaultPassphraseBoundaries.numWords.min);
|
||||
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ numWords });
|
||||
|
||||
@@ -170,7 +190,7 @@ describe("Password generator options builder", () => {
|
||||
expect(numWords).toBeGreaterThanOrEqual(DefaultPassphraseBoundaries.numWords.min);
|
||||
expect(numWords).toBeLessThanOrEqual(DefaultPassphraseBoundaries.numWords.max);
|
||||
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ numWords });
|
||||
|
||||
@@ -185,7 +205,7 @@ describe("Password generator options builder", () => {
|
||||
(numWords) => {
|
||||
expect(numWords).toBeGreaterThan(DefaultPassphraseBoundaries.numWords.max);
|
||||
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ numWords });
|
||||
|
||||
@@ -196,7 +216,7 @@ describe("Password generator options builder", () => {
|
||||
);
|
||||
|
||||
it("should preserve unknown properties", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({
|
||||
unknown: "property",
|
||||
@@ -214,7 +234,7 @@ describe("Password generator options builder", () => {
|
||||
// All tests should freeze the options to ensure they are not modified
|
||||
|
||||
it("should return the input options without altering them", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ wordSeparator: "%" });
|
||||
|
||||
@@ -224,7 +244,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should set `wordSeparator` to '-' when it isn't supplied and there is no policy override", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({});
|
||||
|
||||
@@ -234,7 +254,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should leave `wordSeparator` as the empty string '' when it is the empty string", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ wordSeparator: "" });
|
||||
|
||||
@@ -244,7 +264,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should preserve unknown properties", () => {
|
||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({
|
||||
unknown: "property",
|
||||
|
||||
@@ -4,8 +4,6 @@ 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 { Policies } from "../data";
|
||||
|
||||
import { passphraseLeastPrivilege } from "./passphrase-least-privilege";
|
||||
|
||||
function createPolicy(
|
||||
@@ -22,21 +20,27 @@ function createPolicy(
|
||||
});
|
||||
}
|
||||
|
||||
const disabledValue = Object.freeze({
|
||||
minNumberWords: 0,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
});
|
||||
|
||||
describe("passphraseLeastPrivilege", () => {
|
||||
it("should return the accumulator when the policy type does not apply", () => {
|
||||
const policy = createPolicy({}, PolicyType.RequireSso);
|
||||
|
||||
const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy);
|
||||
const result = passphraseLeastPrivilege(disabledValue, policy);
|
||||
|
||||
expect(result).toEqual(Policies.Passphrase.disabledValue);
|
||||
expect(result).toEqual(disabledValue);
|
||||
});
|
||||
|
||||
it("should return the accumulator when the policy is not enabled", () => {
|
||||
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
|
||||
|
||||
const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy);
|
||||
const result = passphraseLeastPrivilege(disabledValue, policy);
|
||||
|
||||
expect(result).toEqual(Policies.Passphrase.disabledValue);
|
||||
expect(result).toEqual(disabledValue);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -46,8 +50,8 @@ describe("passphraseLeastPrivilege", () => {
|
||||
])("should take the %p from the policy", (input, value) => {
|
||||
const policy = createPolicy({ [input]: value });
|
||||
|
||||
const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy);
|
||||
const result = passphraseLeastPrivilege(disabledValue, policy);
|
||||
|
||||
expect(result).toEqual({ ...Policies.Passphrase.disabledValue, [input]: value });
|
||||
expect(result).toEqual({ ...disabledValue, [input]: value });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Generators } from "../data";
|
||||
import { BuiltIn, Profile } from "../metadata";
|
||||
|
||||
import { PassphrasePolicyConstraints } from "./passphrase-policy-constraints";
|
||||
|
||||
@@ -9,8 +9,12 @@ const SomeSettings = {
|
||||
wordSeparator: "-",
|
||||
};
|
||||
|
||||
const disabledPolicy = Generators.passphrase.policy.disabledValue;
|
||||
const someConstraints = Generators.passphrase.settings.constraints;
|
||||
const disabledPolicy = {
|
||||
minNumberWords: 0,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
};
|
||||
const someConstraints = BuiltIn.passphrase.profiles[Profile.account]!.constraints.default;
|
||||
|
||||
describe("PassphrasePolicyConstraints", () => {
|
||||
describe("constructor", () => {
|
||||
@@ -61,7 +65,7 @@ describe("PassphrasePolicyConstraints", () => {
|
||||
expect(constraints.policyInEffect).toBeTruthy();
|
||||
expect(constraints.numWords).toMatchObject({
|
||||
min: 10,
|
||||
max: someConstraints.numWords.max,
|
||||
max: someConstraints.numWords?.max,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -84,8 +88,8 @@ describe("PassphrasePolicyConstraints", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
[1, someConstraints.numWords.min, 3, someConstraints.numWords.max],
|
||||
[21, someConstraints.numWords.min, 20, someConstraints.numWords.max],
|
||||
[1, someConstraints.numWords?.min, 3, someConstraints.numWords?.max],
|
||||
[21, someConstraints.numWords?.min, 20, someConstraints.numWords?.max],
|
||||
])(
|
||||
`fits numWords (=%p) within the default bounds (%p <= %p <= %p)`,
|
||||
(value, _, expected, __) => {
|
||||
@@ -98,8 +102,8 @@ describe("PassphrasePolicyConstraints", () => {
|
||||
);
|
||||
|
||||
it.each([
|
||||
[1, 6, 6, someConstraints.numWords.max],
|
||||
[21, 20, 20, someConstraints.numWords.max],
|
||||
[1, 6, 6, someConstraints.numWords?.max],
|
||||
[21, 20, 20, someConstraints.numWords?.max],
|
||||
])(
|
||||
"fits numWords (=%p) within the policy bounds (%p <= %p <= %p)",
|
||||
(value, minNumberWords, expected, _) => {
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
import { DefaultPasswordBoundaries, Policies } from "../data";
|
||||
import { PasswordGenerationOptions } from "../types";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
|
||||
import { DefaultPasswordBoundaries } from "../data";
|
||||
import { PasswordGenerationOptions, PasswordGeneratorPolicy, PolicyConfiguration } from "../types";
|
||||
|
||||
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
||||
import { passwordLeastPrivilege } from "./password-least-privilege";
|
||||
|
||||
const Password: PolicyConfiguration<PasswordGeneratorPolicy, PasswordGenerationOptions> =
|
||||
deepFreeze({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {
|
||||
minLength: 0,
|
||||
useUppercase: false,
|
||||
useLowercase: false,
|
||||
useNumbers: false,
|
||||
numberCount: 0,
|
||||
useSpecial: false,
|
||||
specialCount: 0,
|
||||
},
|
||||
combine: passwordLeastPrivilege,
|
||||
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
|
||||
});
|
||||
|
||||
describe("Password generator options builder", () => {
|
||||
const defaultOptions = Object.freeze({ minLength: 0 });
|
||||
|
||||
describe("constructor()", () => {
|
||||
it("should set the policy object to a copy of the input policy", () => {
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.minLength = 10; // arbitrary change for deep equality check
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -18,7 +38,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should set default boundaries when a default policy is used", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -32,7 +52,7 @@ describe("Password generator options builder", () => {
|
||||
(minLength) => {
|
||||
expect(minLength).toBeLessThan(DefaultPasswordBoundaries.length.min);
|
||||
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.minLength = minLength;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -47,7 +67,7 @@ describe("Password generator options builder", () => {
|
||||
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.min);
|
||||
expect(expectedLength).toBeLessThanOrEqual(DefaultPasswordBoundaries.length.max);
|
||||
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.minLength = expectedLength;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -62,7 +82,7 @@ describe("Password generator options builder", () => {
|
||||
(expectedLength) => {
|
||||
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.max);
|
||||
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.minLength = expectedLength;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -78,7 +98,7 @@ describe("Password generator options builder", () => {
|
||||
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.min);
|
||||
expect(expectedMinDigits).toBeLessThanOrEqual(DefaultPasswordBoundaries.minDigits.max);
|
||||
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.numberCount = expectedMinDigits;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -93,7 +113,7 @@ describe("Password generator options builder", () => {
|
||||
(expectedMinDigits) => {
|
||||
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.max);
|
||||
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.numberCount = expectedMinDigits;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -113,7 +133,7 @@ describe("Password generator options builder", () => {
|
||||
DefaultPasswordBoundaries.minSpecialCharacters.max,
|
||||
);
|
||||
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.specialCount = expectedSpecialCharacters;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -132,7 +152,7 @@ describe("Password generator options builder", () => {
|
||||
DefaultPasswordBoundaries.minSpecialCharacters.max,
|
||||
);
|
||||
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.specialCount = expectedSpecialCharacters;
|
||||
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
@@ -151,7 +171,7 @@ describe("Password generator options builder", () => {
|
||||
(expectedLength, numberCount, specialCount) => {
|
||||
expect(expectedLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
|
||||
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.numberCount = numberCount;
|
||||
policy.specialCount = specialCount;
|
||||
|
||||
@@ -164,14 +184,14 @@ describe("Password generator options builder", () => {
|
||||
|
||||
describe("policyInEffect", () => {
|
||||
it("should return false when the policy has no effect", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
expect(builder.policyInEffect).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true when the policy has a minlength greater than the default boundary", () => {
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.minLength = DefaultPasswordBoundaries.length.min + 1;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -179,7 +199,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has a number count greater than the default boundary", () => {
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.numberCount = DefaultPasswordBoundaries.minDigits.min + 1;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -187,7 +207,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: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.specialCount = DefaultPasswordBoundaries.minSpecialCharacters.min + 1;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -195,7 +215,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has uppercase enabled", () => {
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.useUppercase = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -203,7 +223,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has lowercase enabled", () => {
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.useLowercase = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -211,7 +231,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has numbers enabled", () => {
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.useNumbers = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -219,7 +239,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should return true when the policy has special characters enabled", () => {
|
||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.useSpecial = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
|
||||
@@ -237,7 +257,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: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.useUppercase = false;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, uppercase });
|
||||
@@ -251,7 +271,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: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.useUppercase = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, uppercase });
|
||||
@@ -269,7 +289,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: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.useLowercase = false;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, lowercase });
|
||||
@@ -283,7 +303,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: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.useLowercase = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, lowercase });
|
||||
@@ -301,7 +321,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: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.useNumbers = false;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, number });
|
||||
@@ -315,7 +335,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: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.useNumbers = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, number });
|
||||
@@ -333,7 +353,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: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.useSpecial = false;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, special });
|
||||
@@ -347,7 +367,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: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.useSpecial = true;
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, special });
|
||||
@@ -361,7 +381,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([1, 2, 3, 4])(
|
||||
"should set `options.length` (= %i) to the minimum it is less than the minimum length",
|
||||
(length) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
expect(length).toBeLessThan(builder.length.min);
|
||||
|
||||
@@ -376,7 +396,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([5, 10, 50, 100, 128])(
|
||||
"should not change `options.length` (= %i) when it is within the boundaries",
|
||||
(length) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
expect(length).toBeGreaterThanOrEqual(builder.length.min);
|
||||
expect(length).toBeLessThanOrEqual(builder.length.max);
|
||||
@@ -392,7 +412,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([129, 500, 9000])(
|
||||
"should set `options.length` (= %i) to the maximum length when it is exceeded",
|
||||
(length) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
expect(length).toBeGreaterThan(builder.length.max);
|
||||
|
||||
@@ -414,7 +434,7 @@ describe("Password generator options builder", () => {
|
||||
])(
|
||||
"should set `options.number === %s` when `options.minNumber` (= %i) is set to a value greater than 0",
|
||||
(expectedNumber, minNumber) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, minNumber });
|
||||
|
||||
@@ -425,7 +445,7 @@ describe("Password generator options builder", () => {
|
||||
);
|
||||
|
||||
it("should set `options.minNumber` to the minimum value when `options.number` is true", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, number: true });
|
||||
|
||||
@@ -435,7 +455,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should set `options.minNumber` to 0 when `options.number` is false", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, number: false });
|
||||
|
||||
@@ -447,7 +467,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: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.numberCount = 5; // arbitrary value greater than minNumber
|
||||
expect(minNumber).toBeLessThan(policy.numberCount);
|
||||
|
||||
@@ -463,7 +483,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([1, 3, 5, 7, 9])(
|
||||
"should not change `options.minNumber` (= %i) when it is within the boundaries",
|
||||
(minNumber) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
expect(minNumber).toBeGreaterThanOrEqual(builder.minDigits.min);
|
||||
expect(minNumber).toBeLessThanOrEqual(builder.minDigits.max);
|
||||
@@ -479,7 +499,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([10, 20, 400])(
|
||||
"should set `options.minNumber` (= %i) to the maximum digit boundary when it is exceeded",
|
||||
(minNumber) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
expect(minNumber).toBeGreaterThan(builder.minDigits.max);
|
||||
|
||||
@@ -501,7 +521,7 @@ describe("Password generator options builder", () => {
|
||||
])(
|
||||
"should set `options.special === %s` when `options.minSpecial` (= %i) is set to a value greater than 0",
|
||||
(expectedSpecial, minSpecial) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, minSpecial });
|
||||
|
||||
@@ -512,7 +532,7 @@ describe("Password generator options builder", () => {
|
||||
);
|
||||
|
||||
it("should set `options.minSpecial` to the minimum value when `options.special` is true", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, special: true });
|
||||
|
||||
@@ -522,7 +542,7 @@ describe("Password generator options builder", () => {
|
||||
});
|
||||
|
||||
it("should set `options.minSpecial` to 0 when `options.special` is false", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ ...defaultOptions, special: false });
|
||||
|
||||
@@ -534,7 +554,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: any = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy: any = Object.assign({}, Password.disabledValue);
|
||||
policy.specialCount = 5; // arbitrary value greater than minSpecial
|
||||
expect(minSpecial).toBeLessThan(policy.specialCount);
|
||||
|
||||
@@ -550,7 +570,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([1, 3, 5, 7, 9])(
|
||||
"should not change `options.minSpecial` (= %i) when it is within the boundaries",
|
||||
(minSpecial) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
expect(minSpecial).toBeGreaterThanOrEqual(builder.minSpecialCharacters.min);
|
||||
expect(minSpecial).toBeLessThanOrEqual(builder.minSpecialCharacters.max);
|
||||
@@ -566,7 +586,7 @@ describe("Password generator options builder", () => {
|
||||
it.each([10, 20, 400])(
|
||||
"should set `options.minSpecial` (= %i) to the maximum special character boundary when it is exceeded",
|
||||
(minSpecial) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
expect(minSpecial).toBeGreaterThan(builder.minSpecialCharacters.max);
|
||||
|
||||
@@ -579,7 +599,7 @@ describe("Password generator options builder", () => {
|
||||
);
|
||||
|
||||
it("should preserve unknown properties", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({
|
||||
unknown: "property",
|
||||
@@ -602,7 +622,7 @@ describe("Password generator options builder", () => {
|
||||
])(
|
||||
"should output `options.minLowercase === %i` when `options.lowercase` is %s",
|
||||
(expectedMinLowercase, lowercase) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ lowercase, ...defaultOptions });
|
||||
|
||||
@@ -618,7 +638,7 @@ describe("Password generator options builder", () => {
|
||||
])(
|
||||
"should output `options.minUppercase === %i` when `options.uppercase` is %s",
|
||||
(expectedMinUppercase, uppercase) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ uppercase, ...defaultOptions });
|
||||
|
||||
@@ -634,7 +654,7 @@ describe("Password generator options builder", () => {
|
||||
])(
|
||||
"should output `options.minNumber === %i` when `options.number` is %s and `options.minNumber` is not set",
|
||||
(expectedMinNumber, number) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ number, ...defaultOptions });
|
||||
|
||||
@@ -652,7 +672,7 @@ describe("Password generator options builder", () => {
|
||||
])(
|
||||
"should output `options.number === %s` when `options.minNumber` is %i and `options.number` is not set",
|
||||
(expectedNumber, minNumber) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ minNumber, ...defaultOptions });
|
||||
|
||||
@@ -668,7 +688,7 @@ describe("Password generator options builder", () => {
|
||||
])(
|
||||
"should output `options.minSpecial === %i` when `options.special` is %s and `options.minSpecial` is not set",
|
||||
(special, expectedMinSpecial) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ special, ...defaultOptions });
|
||||
|
||||
@@ -686,7 +706,7 @@ describe("Password generator options builder", () => {
|
||||
])(
|
||||
"should output `options.special === %s` when `options.minSpecial` is %i and `options.special` is not set",
|
||||
(minSpecial, expectedSpecial) => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({ minSpecial, ...defaultOptions });
|
||||
|
||||
@@ -707,7 +727,7 @@ describe("Password generator options builder", () => {
|
||||
const sumOfMinimums = minLowercase + minUppercase + minNumber + minSpecial;
|
||||
expect(sumOfMinimums).toBeLessThan(DefaultPasswordBoundaries.length.min);
|
||||
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({
|
||||
minLowercase,
|
||||
@@ -732,7 +752,7 @@ describe("Password generator options builder", () => {
|
||||
(expectedMinLength, minLowercase, minUppercase, minNumber, minSpecial) => {
|
||||
expect(expectedMinLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
|
||||
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({
|
||||
minLowercase,
|
||||
@@ -749,7 +769,7 @@ describe("Password generator options builder", () => {
|
||||
);
|
||||
|
||||
it("should preserve unknown properties", () => {
|
||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||
const policy = Object.assign({}, Password.disabledValue);
|
||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||
const options = Object.freeze({
|
||||
unknown: "property",
|
||||
|
||||
@@ -4,8 +4,6 @@ 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 { Policies } from "../data";
|
||||
|
||||
import { passwordLeastPrivilege } from "./password-least-privilege";
|
||||
|
||||
function createPolicy(
|
||||
@@ -22,21 +20,31 @@ function createPolicy(
|
||||
});
|
||||
}
|
||||
|
||||
const disabledValue = Object.freeze({
|
||||
minLength: 0,
|
||||
useUppercase: false,
|
||||
useLowercase: false,
|
||||
useNumbers: false,
|
||||
numberCount: 0,
|
||||
useSpecial: false,
|
||||
specialCount: 0,
|
||||
});
|
||||
|
||||
describe("passwordLeastPrivilege", () => {
|
||||
it("should return the accumulator when the policy type does not apply", () => {
|
||||
const policy = createPolicy({}, PolicyType.RequireSso);
|
||||
|
||||
const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy);
|
||||
const result = passwordLeastPrivilege(disabledValue, policy);
|
||||
|
||||
expect(result).toEqual(Policies.Password.disabledValue);
|
||||
expect(result).toEqual(disabledValue);
|
||||
});
|
||||
|
||||
it("should return the accumulator when the policy is not enabled", () => {
|
||||
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
|
||||
|
||||
const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy);
|
||||
const result = passwordLeastPrivilege(disabledValue, policy);
|
||||
|
||||
expect(result).toEqual(Policies.Password.disabledValue);
|
||||
expect(result).toEqual(disabledValue);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -50,8 +58,8 @@ describe("passwordLeastPrivilege", () => {
|
||||
])("should take the %p from the policy", (input, value, expected) => {
|
||||
const policy = createPolicy({ [input]: value });
|
||||
|
||||
const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy);
|
||||
const result = passwordLeastPrivilege(disabledValue, policy);
|
||||
|
||||
expect(result).toEqual({ ...Policies.Password.disabledValue, [expected]: value });
|
||||
expect(result).toEqual({ ...disabledValue, [expected]: value });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
||||
|
||||
import { GeneratorDependencyProvider } from "./generator-dependency-provider";
|
||||
import { GeneratorMetadataProvider } from "./generator-metadata-provider";
|
||||
import { GeneratorProfileProvider } from "./generator-profile-provider";
|
||||
|
||||
// FIXME: find a better way to manage common dependencies than smashing them all
|
||||
// together into a mega-type.
|
||||
export type CredentialGeneratorProviders = {
|
||||
readonly userState: UserStateSubjectDependencyProvider;
|
||||
readonly generator: GeneratorDependencyProvider;
|
||||
readonly profile: GeneratorProfileProvider;
|
||||
readonly metadata: GeneratorMetadataProvider;
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
import { AlgorithmsByType, Type } from "../metadata";
|
||||
import { CredentialPreference } from "../types";
|
||||
|
||||
import { PREFERENCES } from "./credential-preferences";
|
||||
|
||||
const SomeCredentialPreferences: CredentialPreference = Object.freeze({
|
||||
email: Object.freeze({
|
||||
algorithm: AlgorithmsByType[Type.email][0],
|
||||
updated: new Date(0),
|
||||
}),
|
||||
password: Object.freeze({
|
||||
algorithm: AlgorithmsByType[Type.password][0],
|
||||
updated: new Date(0),
|
||||
}),
|
||||
username: Object.freeze({
|
||||
algorithm: AlgorithmsByType[Type.username][0],
|
||||
updated: new Date(0),
|
||||
}),
|
||||
});
|
||||
|
||||
describe("PREFERENCES", () => {
|
||||
describe("deserializer", () => {
|
||||
it.each([[null], [undefined]])("creates new preferences (= %p)", (value) => {
|
||||
// this case tests what happens when the type system is bypassed
|
||||
const result = PREFERENCES.deserializer(value!);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
email: {
|
||||
algorithm: AlgorithmsByType[Type.email][0],
|
||||
},
|
||||
password: {
|
||||
algorithm: AlgorithmsByType[Type.password][0],
|
||||
},
|
||||
username: {
|
||||
algorithm: AlgorithmsByType[Type.username][0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("fills missing password preferences", () => {
|
||||
const input: any = structuredClone(SomeCredentialPreferences);
|
||||
delete input.password;
|
||||
|
||||
const result = PREFERENCES.deserializer(input);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
password: {
|
||||
algorithm: AlgorithmsByType[Type.password][0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("fills missing email preferences", () => {
|
||||
const input: any = structuredClone(SomeCredentialPreferences);
|
||||
delete input.email;
|
||||
|
||||
const result = PREFERENCES.deserializer(input);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
email: {
|
||||
algorithm: AlgorithmsByType[Type.email][0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("fills missing username preferences", () => {
|
||||
const input: any = structuredClone(SomeCredentialPreferences);
|
||||
delete input.username;
|
||||
|
||||
const result = PREFERENCES.deserializer(input);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
username: {
|
||||
algorithm: AlgorithmsByType[Type.username][0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("converts string fields to Dates", () => {
|
||||
const input: any = structuredClone(SomeCredentialPreferences);
|
||||
input.email.updated = "1970-01-01T00:00:00.100Z";
|
||||
input.password.updated = "1970-01-01T00:00:00.200Z";
|
||||
input.username.updated = "1970-01-01T00:00:00.300Z";
|
||||
|
||||
const result = PREFERENCES.deserializer(input);
|
||||
|
||||
expect(result?.email.updated).toEqual(new Date(100));
|
||||
expect(result?.password.updated).toEqual(new Date(200));
|
||||
expect(result?.username.updated).toEqual(new Date(300));
|
||||
});
|
||||
|
||||
it("converts number fields to Dates", () => {
|
||||
const input: any = structuredClone(SomeCredentialPreferences);
|
||||
input.email.updated = 100;
|
||||
input.password.updated = 200;
|
||||
input.username.updated = 300;
|
||||
|
||||
const result = PREFERENCES.deserializer(input);
|
||||
|
||||
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,28 @@
|
||||
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||
|
||||
import { AlgorithmsByType, CredentialType } from "../metadata";
|
||||
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 AlgorithmsByType) {
|
||||
const type = key as CredentialType;
|
||||
if (result[type]) {
|
||||
result[type].updated = new Date(result[type].updated);
|
||||
} else {
|
||||
const [algorithm] = AlgorithmsByType[type];
|
||||
result[type] = { algorithm, updated: new Date() };
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
|
||||
|
||||
import { Randomizer } from "../abstractions";
|
||||
|
||||
export type GeneratorDependencyProvider = {
|
||||
randomizer: Randomizer;
|
||||
client: RestClient;
|
||||
// FIXME: introduce `I18nKeyOrLiteral` into forwarder
|
||||
// structures and remove this dependency
|
||||
i18nService: I18nService;
|
||||
};
|
||||
@@ -75,6 +75,7 @@ const SystemProvider = {
|
||||
} as LegacyEncryptorProvider,
|
||||
state: SomeStateProvider,
|
||||
log: disabledSemanticLoggerProvider,
|
||||
now: Date.now,
|
||||
} as UserStateSubjectDependencyProvider;
|
||||
|
||||
const SomeSiteId: SiteId = Site.forwarder;
|
||||
@@ -415,14 +416,14 @@ describe("GeneratorMetadataProvider", () => {
|
||||
await expect(firstValueFrom(result)).resolves.toEqual(plusAddress.id);
|
||||
});
|
||||
|
||||
it("emits undefined when the user's preference is unavailable and there is no metadata", async () => {
|
||||
it("emits the original preference when the user's preference is unavailable and there is no metadata", async () => {
|
||||
SomePolicyService.policiesByType$.mockReturnValue(new BehaviorSubject([]));
|
||||
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||
const result = new ReplaySubject<CredentialAlgorithm | undefined>(1);
|
||||
|
||||
provider.preference$(Type.email, { account$: SomeAccount$ }).subscribe(result);
|
||||
|
||||
await expect(firstValueFrom(result)).resolves.toBeUndefined();
|
||||
await expect(firstValueFrom(result)).resolves.toEqual(preferences[Type.email].algorithm);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
Observable,
|
||||
combineLatestWith,
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
import { Observable, distinctUntilChanged, map, shareReplay, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -14,7 +6,7 @@ import { BoundDependency } from "@bitwarden/common/tools/dependencies";
|
||||
import { ExtensionSite } from "@bitwarden/common/tools/extension";
|
||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import { anyComplete, pin } from "@bitwarden/common/tools/rx";
|
||||
import { anyComplete, memoizedMap, pin } from "@bitwarden/common/tools/rx";
|
||||
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
||||
|
||||
@@ -29,7 +21,7 @@ import {
|
||||
Algorithms,
|
||||
Types,
|
||||
} from "../metadata";
|
||||
import { availableAlgorithms_vNext } from "../policies/available-algorithms-policy";
|
||||
import { AvailableAlgorithmsConstraint, availableAlgorithms } from "../policies";
|
||||
import { CredentialPreference } from "../types";
|
||||
import {
|
||||
AlgorithmRequest,
|
||||
@@ -148,8 +140,15 @@ export class GeneratorMetadataProvider {
|
||||
const policies$ = this.application.policy
|
||||
.policiesByType$(PolicyType.PasswordGenerator, id)
|
||||
.pipe(
|
||||
map((p) => availableAlgorithms_vNext(p).filter((a) => this._metadata.has(a))),
|
||||
map((p) => new Set(p)),
|
||||
map((p) =>
|
||||
availableAlgorithms(p)
|
||||
.filter((a) => this._metadata.has(a))
|
||||
.sort(),
|
||||
),
|
||||
// interning the set transformation lets `distinctUntilChanged()` eliminate
|
||||
// repeating policy emissions using reference equality
|
||||
memoizedMap((a) => new Set(a), { key: (a) => a.join(":") }),
|
||||
distinctUntilChanged(),
|
||||
// complete policy emissions otherwise `switchMap` holds `available$` open indefinitely
|
||||
takeUntil(anyComplete(id$)),
|
||||
);
|
||||
@@ -211,24 +210,7 @@ export class GeneratorMetadataProvider {
|
||||
const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
|
||||
const algorithm$ = this.preferences({ account$ }).pipe(
|
||||
combineLatestWith(this.isAvailable$({ account$ })),
|
||||
map(([preferences, isAvailable]) => {
|
||||
const algorithm: CredentialAlgorithm = preferences[type].algorithm;
|
||||
if (isAvailable(algorithm)) {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
const algorithms = type ? this.algorithms({ type: type }) : [];
|
||||
// `?? null` because logging types must be `Jsonify<T>`
|
||||
const defaultAlgorithm = algorithms.find(isAvailable) ?? null;
|
||||
this.log.debug(
|
||||
{ algorithm, defaultAlgorithm, credentialType: type },
|
||||
"preference not available; defaulting the generator algorithm",
|
||||
);
|
||||
|
||||
// `?? undefined` so that interface is ADR-14 compliant
|
||||
return defaultAlgorithm ?? undefined;
|
||||
}),
|
||||
map((preferences) => preferences[type].algorithm),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
@@ -246,8 +228,16 @@ export class GeneratorMetadataProvider {
|
||||
preferences(
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): UserStateSubject<CredentialPreference> {
|
||||
// FIXME: enforce policy
|
||||
const subject = new UserStateSubject(PREFERENCES, this.system, dependencies);
|
||||
const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
|
||||
const constraints$ = this.isAvailable$({ account$ }).pipe(
|
||||
map(
|
||||
(isAvailable) =>
|
||||
new AvailableAlgorithmsConstraint(this.algorithms.bind(this), isAvailable, this.system),
|
||||
),
|
||||
);
|
||||
|
||||
const subject = new UserStateSubject(PREFERENCES, this.system, { account$, constraints$ });
|
||||
|
||||
return subject;
|
||||
}
|
||||
@@ -67,6 +67,7 @@ const dependencyProvider: UserStateSubjectDependencyProvider = {
|
||||
encryptor: encryptorProvider,
|
||||
state: stateProvider,
|
||||
log: disabledSemanticLoggerProvider,
|
||||
now: Date.now,
|
||||
};
|
||||
|
||||
// settings storage location
|
||||
@@ -19,6 +19,7 @@ import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/stat
|
||||
|
||||
import { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "../metadata";
|
||||
import { GeneratorConstraints } from "../types/generator-constraints";
|
||||
import { equivalent } from "../util";
|
||||
|
||||
/** Surfaces contextual information to credential generators */
|
||||
export class GeneratorProfileProvider {
|
||||
@@ -99,7 +100,10 @@ export class GeneratorProfileProvider {
|
||||
|
||||
const constraints$ = policies$.pipe(
|
||||
map((policies) => profile.constraints.create(policies, context)),
|
||||
tap(() => this.log.debug("constraints created")),
|
||||
distinctUntilChanged((previous, next) => {
|
||||
return equivalent(previous, next);
|
||||
}),
|
||||
tap((constraints) => this.log.debug(constraints as object, "constraints updated")),
|
||||
);
|
||||
|
||||
return constraints$;
|
||||
4
libs/tools/generator/core/src/providers/index.ts
Normal file
4
libs/tools/generator/core/src/providers/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { CredentialGeneratorProviders } from "./credential-generator-providers";
|
||||
export { GeneratorMetadataProvider } from "./generator-metadata-provider";
|
||||
export { GeneratorProfileProvider } from "./generator-profile-provider";
|
||||
export { GeneratorDependencyProvider } from "./generator-dependency-provider";
|
||||
@@ -18,20 +18,6 @@ export function mapPolicyToEvaluator<Policy, Evaluator>(
|
||||
);
|
||||
}
|
||||
|
||||
/** Maps an administrative console policy to constraints using the provided configuration.
|
||||
* @param configuration the configuration that constructs the constraints.
|
||||
*/
|
||||
export function mapPolicyToConstraints<Policy, Evaluator>(
|
||||
configuration: PolicyConfiguration<Policy, Evaluator>,
|
||||
email: string,
|
||||
) {
|
||||
return pipe(
|
||||
reduceCollection(configuration.combine, configuration.disabledValue),
|
||||
distinctIfShallowMatch(),
|
||||
map((policy) => configuration.toConstraints(policy, email)),
|
||||
);
|
||||
}
|
||||
|
||||
/** Constructs a method that maps a policy to the default (no-op) policy. */
|
||||
export function newDefaultEvaluator<Target>() {
|
||||
return () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,296 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { concatMap, distinctUntilChanged, map, Observable, switchMap, takeUntil } from "rxjs";
|
||||
import { Simplify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { BoundDependency, OnDependency } from "@bitwarden/common/tools/dependencies";
|
||||
import { IntegrationMetadata } from "@bitwarden/common/tools/integration";
|
||||
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { anyComplete, withLatestReady } from "@bitwarden/common/tools/rx";
|
||||
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
||||
|
||||
import { Randomizer } from "../abstractions";
|
||||
import {
|
||||
Generators,
|
||||
getForwarderConfiguration,
|
||||
Integrations,
|
||||
toCredentialGeneratorConfiguration,
|
||||
} from "../data";
|
||||
import { availableAlgorithms } from "../policies/available-algorithms-policy";
|
||||
import { mapPolicyToConstraints } from "../rx";
|
||||
import {
|
||||
CredentialAlgorithm,
|
||||
CredentialCategories,
|
||||
CredentialCategory,
|
||||
AlgorithmInfo,
|
||||
CredentialPreference,
|
||||
isForwarderIntegration,
|
||||
ForwarderIntegration,
|
||||
GenerateRequest,
|
||||
} from "../types";
|
||||
import {
|
||||
CredentialGeneratorConfiguration as Configuration,
|
||||
CredentialGeneratorInfo,
|
||||
GeneratorDependencyProvider,
|
||||
} from "../types/credential-generator-configuration";
|
||||
import { GeneratorConstraints } from "../types/generator-constraints";
|
||||
|
||||
import { PREFERENCES } from "./credential-preferences";
|
||||
|
||||
type Generate$Dependencies = Simplify<
|
||||
OnDependency<GenerateRequest> & BoundDependency<"account", Account>
|
||||
>;
|
||||
|
||||
export class CredentialGeneratorService {
|
||||
constructor(
|
||||
private readonly randomizer: Randomizer,
|
||||
private readonly policyService: PolicyService,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly providers: UserStateSubjectDependencyProvider,
|
||||
) {}
|
||||
|
||||
private getDependencyProvider(): GeneratorDependencyProvider {
|
||||
return {
|
||||
client: new RestClient(this.apiService, this.i18nService),
|
||||
i18nService: this.i18nService,
|
||||
randomizer: this.randomizer,
|
||||
};
|
||||
}
|
||||
|
||||
// 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$ Required. A new credential is emitted when this emits.
|
||||
*/
|
||||
generate$<Settings extends object, Policy>(
|
||||
configuration: Readonly<Configuration<Settings, Policy>>,
|
||||
dependencies: Generate$Dependencies,
|
||||
) {
|
||||
const engine = configuration.engine.create(this.getDependencyProvider());
|
||||
const settings$ = this.settings$(configuration, dependencies);
|
||||
|
||||
// generation proper
|
||||
const generate$ = dependencies.on$.pipe(
|
||||
withLatestReady(settings$),
|
||||
concatMap(([request, settings]) => engine.generate(request, settings)),
|
||||
takeUntil(anyComplete([settings$])),
|
||||
);
|
||||
|
||||
return generate$;
|
||||
}
|
||||
|
||||
/** Emits metadata concerning the provided generation algorithms
|
||||
* @param category the category or categories of interest
|
||||
* @param dependences.account$ algorithms are filtered to only
|
||||
* those matching the provided account's policy.
|
||||
* @returns An observable that emits algorithm metadata.
|
||||
*/
|
||||
algorithms$(
|
||||
category: CredentialCategory,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): Observable<AlgorithmInfo[]>;
|
||||
algorithms$(
|
||||
category: CredentialCategory[],
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): Observable<AlgorithmInfo[]>;
|
||||
algorithms$(
|
||||
category: CredentialCategory | CredentialCategory[],
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
) {
|
||||
// any cast required here because TypeScript fails to bind `category`
|
||||
// to the union-typed overload of `algorithms`.
|
||||
const algorithms = this.algorithms(category as any);
|
||||
|
||||
// apply policy
|
||||
const algorithms$ = dependencies.account$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((account) => {
|
||||
const policies$ = this.policyService
|
||||
.policiesByType$(PolicyType.PasswordGenerator, account.id)
|
||||
.pipe(
|
||||
map((p) => new Set(availableAlgorithms(p))),
|
||||
// complete policy emissions otherwise `switchMap` holds `algorithms$` open indefinitely
|
||||
takeUntil(anyComplete(dependencies.account$)),
|
||||
);
|
||||
return policies$;
|
||||
}),
|
||||
map((available) => {
|
||||
const filtered = algorithms.filter(
|
||||
(c) => isForwarderIntegration(c.id) || 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): AlgorithmInfo[];
|
||||
algorithms(category: CredentialCategory[]): AlgorithmInfo[];
|
||||
algorithms(category: CredentialCategory | CredentialCategory[]): AlgorithmInfo[] {
|
||||
const categories: CredentialCategory[] = Array.isArray(category) ? category : [category];
|
||||
|
||||
const algorithms = categories
|
||||
.flatMap((c) => CredentialCategories[c] as CredentialAlgorithm[])
|
||||
.map((id) => this.algorithm(id))
|
||||
.filter((info) => info !== null);
|
||||
|
||||
const forwarders = Object.keys(Integrations)
|
||||
.map((key: keyof typeof Integrations) => {
|
||||
const forwarder: ForwarderIntegration = { forwarder: Integrations[key].id };
|
||||
return this.algorithm(forwarder);
|
||||
})
|
||||
.filter((forwarder) => categories.includes(forwarder.category));
|
||||
|
||||
return algorithms.concat(forwarders);
|
||||
}
|
||||
|
||||
/** 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): AlgorithmInfo {
|
||||
let generator: CredentialGeneratorInfo = null;
|
||||
let integration: IntegrationMetadata = null;
|
||||
|
||||
if (isForwarderIntegration(id)) {
|
||||
const forwarderConfig = getForwarderConfiguration(id.forwarder);
|
||||
integration = forwarderConfig;
|
||||
|
||||
if (forwarderConfig) {
|
||||
generator = toCredentialGeneratorConfiguration(forwarderConfig);
|
||||
}
|
||||
} else {
|
||||
generator = Generators[id];
|
||||
}
|
||||
|
||||
if (!generator) {
|
||||
throw new Error(`Invalid credential algorithm: ${JSON.stringify(id)}`);
|
||||
}
|
||||
|
||||
const info: AlgorithmInfo = {
|
||||
id: generator.id,
|
||||
category: generator.category,
|
||||
name: integration ? integration.name : this.i18nService.t(generator.nameKey),
|
||||
generate: this.i18nService.t(generator.generateKey),
|
||||
onGeneratedMessage: this.i18nService.t(generator.onGeneratedMessageKey),
|
||||
credentialType: this.i18nService.t(generator.credentialTypeKey),
|
||||
copy: this.i18nService.t(generator.copyKey),
|
||||
useGeneratedValue: this.i18nService.t(generator.useGeneratedValueKey),
|
||||
onlyOnRequest: generator.onlyOnRequest,
|
||||
request: generator.request,
|
||||
};
|
||||
|
||||
if (generator.descriptionKey) {
|
||||
info.description = this.i18nService.t(generator.descriptionKey);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/** Get the settings for the provided configuration
|
||||
* @param configuration determines which generator's settings are loaded
|
||||
* @param dependencies.account$ identifies the account to which the settings are bound.
|
||||
* @returns an observable that emits settings
|
||||
* @remarks the observable enforces policies on the settings
|
||||
*/
|
||||
settings$<Settings extends object, Policy>(
|
||||
configuration: Configuration<Settings, Policy>,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
) {
|
||||
const constraints$ = this.policy$(configuration, dependencies);
|
||||
|
||||
const settings = new UserStateSubject(configuration.settings.account, this.providers, {
|
||||
constraints$,
|
||||
account$: dependencies.account$,
|
||||
});
|
||||
|
||||
const settings$ = settings.pipe(
|
||||
map((settings) => settings ?? structuredClone(configuration.settings.initial)),
|
||||
);
|
||||
|
||||
return settings$;
|
||||
}
|
||||
|
||||
/** Get a subject bound to credential generator preferences.
|
||||
* @param dependencies.account$ identifies the account to which the preferences are bound
|
||||
* @returns a subject bound to the user's preferences
|
||||
* @remarks Preferences determine which algorithms are used when generating a
|
||||
* credential from a credential category (e.g. `PassX` or `Username`). Preferences
|
||||
* should not be used to hold navigation history. Use @bitwarden/generator-navigation
|
||||
* instead.
|
||||
*/
|
||||
preferences(
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): UserStateSubject<CredentialPreference> {
|
||||
// FIXME: enforce policy
|
||||
const subject = new UserStateSubject(PREFERENCES, this.providers, dependencies);
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
/** Get a subject bound to a specific user's settings
|
||||
* @param configuration determines which generator's settings are loaded
|
||||
* @param dependencies.account$ identifies the account to which the settings are bound
|
||||
* @returns a subject bound to the requested user's generator settings
|
||||
* @remarks the subject enforces policy for the settings
|
||||
*/
|
||||
settings<Settings extends object, Policy>(
|
||||
configuration: Readonly<Configuration<Settings, Policy>>,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
) {
|
||||
const constraints$ = this.policy$(configuration, dependencies);
|
||||
|
||||
const subject = new UserStateSubject(configuration.settings.account, this.providers, {
|
||||
constraints$,
|
||||
account$: dependencies.account$,
|
||||
});
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
/** Get the policy constraints for the provided configuration
|
||||
* @param dependencies.account$ determines which user's policy is loaded
|
||||
* @returns an observable that emits the policy once `dependencies.account$`
|
||||
* and the policy become available.
|
||||
*/
|
||||
policy$<Settings, Policy>(
|
||||
configuration: Configuration<Settings, Policy>,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
): Observable<GeneratorConstraints<Settings>> {
|
||||
const constraints$ = dependencies.account$.pipe(
|
||||
map((account) => {
|
||||
if (account.emailVerified) {
|
||||
return { userId: account.id, email: account.email };
|
||||
}
|
||||
|
||||
return { userId: account.id, email: null };
|
||||
}),
|
||||
switchMap(({ userId, email }) => {
|
||||
// complete policy emissions otherwise `switchMap` holds `policies$` open indefinitely
|
||||
const policies$ = this.policyService
|
||||
.policiesByType$(configuration.policy.type, userId)
|
||||
.pipe(
|
||||
mapPolicyToConstraints(configuration.policy, email),
|
||||
takeUntil(anyComplete(dependencies.account$)),
|
||||
);
|
||||
return policies$;
|
||||
}),
|
||||
);
|
||||
|
||||
return constraints$;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
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"],
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,356 @@
|
||||
import { BehaviorSubject, Subject, firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { Site, VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { Bitwarden } from "@bitwarden/common/tools/extension/vendor/bitwarden";
|
||||
import { Vendor } from "@bitwarden/common/tools/extension/vendor/data";
|
||||
import { SemanticLogger, ifEnabledSemanticLoggerProvider } from "@bitwarden/common/tools/log";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { awaitAsync } from "../../../../../common/spec";
|
||||
import {
|
||||
Algorithm,
|
||||
CredentialAlgorithm,
|
||||
CredentialType,
|
||||
ForwarderExtensionId,
|
||||
GeneratorMetadata,
|
||||
Profile,
|
||||
Type,
|
||||
} from "../metadata";
|
||||
import { CredentialGeneratorProviders } from "../providers";
|
||||
import { GenerateRequest, GeneratedCredential } from "../types";
|
||||
|
||||
import { DefaultCredentialGeneratorService } from "./default-credential-generator.service";
|
||||
|
||||
// Custom type for jest.fn() mocks to preserve their type
|
||||
type JestMockFunction<T extends (...args: any) => any> = jest.Mock<ReturnType<T>, Parameters<T>>;
|
||||
|
||||
// two-level partial that preserves jest.fn() mock types
|
||||
type MockTwoLevelPartial<T> = {
|
||||
[K in keyof T]?: T[K] extends object
|
||||
? {
|
||||
[P in keyof T[K]]?: T[K][P] extends (...args: any) => any
|
||||
? JestMockFunction<T[K][P]>
|
||||
: T[K][P];
|
||||
}
|
||||
: T[K];
|
||||
};
|
||||
|
||||
describe("DefaultCredentialGeneratorService", () => {
|
||||
let service: DefaultCredentialGeneratorService;
|
||||
let providers: MockTwoLevelPartial<CredentialGeneratorProviders>;
|
||||
let system: any;
|
||||
let log: SemanticLogger;
|
||||
let mockExtension: { settings: jest.Mock };
|
||||
let account: Account;
|
||||
let createService: (overrides?: any) => DefaultCredentialGeneratorService;
|
||||
|
||||
beforeEach(() => {
|
||||
log = ifEnabledSemanticLoggerProvider(false, new ConsoleLogService(true), {
|
||||
from: "DefaultCredentialGeneratorService tests",
|
||||
});
|
||||
|
||||
mockExtension = { settings: jest.fn() };
|
||||
|
||||
// Use a hard-coded value for mockAccount
|
||||
account = {
|
||||
id: "test-account-id" as UserId,
|
||||
emailVerified: true,
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
};
|
||||
|
||||
system = {
|
||||
log: jest.fn().mockReturnValue(log),
|
||||
extension: mockExtension,
|
||||
};
|
||||
|
||||
providers = {
|
||||
metadata: {
|
||||
metadata: jest.fn(),
|
||||
preference$: jest.fn(),
|
||||
algorithms$: jest.fn(),
|
||||
algorithms: jest.fn(),
|
||||
preferences: jest.fn(),
|
||||
},
|
||||
profile: {
|
||||
settings: jest.fn(),
|
||||
constraints$: jest.fn(),
|
||||
},
|
||||
generator: {},
|
||||
};
|
||||
|
||||
// Creating the service instance with a cast to the expected type
|
||||
createService = (overrides = {}) => {
|
||||
// Force cast the incomplete providers to the required type
|
||||
// similar to how the overrides are applied
|
||||
const providersCast = providers as unknown as CredentialGeneratorProviders;
|
||||
|
||||
const instance = new DefaultCredentialGeneratorService(providersCast, system);
|
||||
Object.assign(instance, overrides);
|
||||
return instance;
|
||||
};
|
||||
|
||||
service = createService();
|
||||
});
|
||||
|
||||
describe("generate$", () => {
|
||||
it("should generate credentials when provided a specific algorithm", async () => {
|
||||
const mockEngine = {
|
||||
generate: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
of(
|
||||
new GeneratedCredential("generatedPassword", Type.password, Date.now(), "unit test"),
|
||||
),
|
||||
),
|
||||
};
|
||||
const mockMetadata = {
|
||||
id: Algorithm.password,
|
||||
engine: { create: jest.fn().mockReturnValue(mockEngine) },
|
||||
} as unknown as GeneratorMetadata<any>;
|
||||
const mockSettings = new BehaviorSubject({ length: 12 });
|
||||
providers.metadata!.metadata = jest.fn().mockReturnValue(mockMetadata);
|
||||
service = createService({
|
||||
settings: () => mockSettings as any,
|
||||
});
|
||||
const on$ = new Subject<GenerateRequest>();
|
||||
const account$ = new BehaviorSubject(account);
|
||||
const result$ = new BehaviorSubject<GeneratedCredential | null>(null);
|
||||
|
||||
service.generate$({ on$, account$ }).subscribe(result$);
|
||||
on$.next({ algorithm: Algorithm.password });
|
||||
await awaitAsync();
|
||||
|
||||
expect(result$.value?.credential).toEqual("generatedPassword");
|
||||
expect(providers.metadata!.metadata).toHaveBeenCalledWith(Algorithm.password);
|
||||
expect(mockMetadata.engine.create).toHaveBeenCalled();
|
||||
expect(mockEngine.generate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should determine preferred algorithm from credential type and generate credentials", async () => {
|
||||
const mockEngine = {
|
||||
generate: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
of(new GeneratedCredential("generatedPassword", "password", Date.now(), "unit test")),
|
||||
),
|
||||
};
|
||||
const mockMetadata = {
|
||||
id: "testAlgorithm",
|
||||
engine: { create: jest.fn().mockReturnValue(mockEngine) },
|
||||
} as unknown as GeneratorMetadata<any>;
|
||||
const mockSettings = new BehaviorSubject({ length: 12 });
|
||||
|
||||
providers.metadata!.preference$ = jest
|
||||
.fn()
|
||||
.mockReturnValue(of("testAlgorithm" as CredentialAlgorithm));
|
||||
providers.metadata!.metadata = jest.fn().mockReturnValue(mockMetadata);
|
||||
service = createService({
|
||||
settings: () => mockSettings as any,
|
||||
});
|
||||
|
||||
const on$ = new Subject<GenerateRequest>();
|
||||
const account$ = new BehaviorSubject(account);
|
||||
const result$ = new BehaviorSubject<GeneratedCredential | null>(null);
|
||||
|
||||
service.generate$({ on$, account$ }).subscribe(result$);
|
||||
on$.next({ type: Type.password });
|
||||
await awaitAsync();
|
||||
|
||||
expect(result$.value?.credential).toBe("generatedPassword");
|
||||
expect(result$.value?.category).toBe(Type.password);
|
||||
expect(providers.metadata!.metadata).toHaveBeenCalledWith("testAlgorithm");
|
||||
});
|
||||
});
|
||||
|
||||
describe("algorithms$", () => {
|
||||
it("should retrieve and map available algorithms for a credential type", async () => {
|
||||
const mockAlgorithms = [Algorithm.password, Algorithm.passphrase] as CredentialAlgorithm[];
|
||||
const mockMetadata1 = { id: Algorithm.password } as GeneratorMetadata<any>;
|
||||
const mockMetadata2 = { id: Algorithm.passphrase } as GeneratorMetadata<any>;
|
||||
|
||||
providers.metadata!.algorithms$ = jest.fn().mockReturnValue(of(mockAlgorithms));
|
||||
providers.metadata!.metadata = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(mockMetadata1)
|
||||
.mockReturnValueOnce(mockMetadata2);
|
||||
|
||||
const result = await firstValueFrom(
|
||||
service.algorithms$("password" as CredentialType, { account$: of(account) }),
|
||||
);
|
||||
|
||||
expect(result).toEqual([mockMetadata1, mockMetadata2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("algorithms", () => {
|
||||
it("should list algorithm metadata for a single credential type", () => {
|
||||
providers.metadata!.algorithms = jest
|
||||
.fn()
|
||||
.mockReturnValue([Algorithm.password, Algorithm.passphrase] as CredentialAlgorithm[]);
|
||||
service = createService({
|
||||
algorithm: (id: CredentialAlgorithm) => ({ id }) as GeneratorMetadata<any>,
|
||||
});
|
||||
|
||||
const result = service.algorithms("password" as CredentialType);
|
||||
|
||||
expect(result).toEqual([{ id: Algorithm.password }, { id: Algorithm.passphrase }]);
|
||||
expect(providers.metadata!.algorithms).toHaveBeenCalledWith({ type: "password" });
|
||||
});
|
||||
|
||||
it("should list combined algorithm metadata for multiple credential types", () => {
|
||||
providers.metadata!.algorithms = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce([Algorithm.password] as CredentialAlgorithm[])
|
||||
.mockReturnValueOnce([Algorithm.username] as CredentialAlgorithm[]);
|
||||
|
||||
service = createService({
|
||||
algorithm: (id: CredentialAlgorithm) => ({ id }) as GeneratorMetadata<any>,
|
||||
});
|
||||
|
||||
const result = service.algorithms(["password", "username"] as CredentialType[]);
|
||||
|
||||
expect(result).toEqual([{ id: Algorithm.password }, { id: Algorithm.username }]);
|
||||
expect(providers.metadata!.algorithms).toHaveBeenCalledWith({ type: "password" });
|
||||
expect(providers.metadata!.algorithms).toHaveBeenCalledWith({ type: "username" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("algorithm", () => {
|
||||
it("should retrieve metadata for a specific generator algorithm", () => {
|
||||
const mockMetadata = { id: Algorithm.password } as GeneratorMetadata<any>;
|
||||
providers.metadata!.metadata = jest.fn().mockReturnValue(mockMetadata);
|
||||
|
||||
const result = service.algorithm(Algorithm.password);
|
||||
|
||||
expect(result).toBe(mockMetadata);
|
||||
expect(providers.metadata!.metadata).toHaveBeenCalledWith(Algorithm.password);
|
||||
});
|
||||
|
||||
it("should log a panic when algorithm ID is invalid", () => {
|
||||
providers.metadata!.metadata = jest.fn().mockReturnValue(null);
|
||||
|
||||
expect(() => service.algorithm("invalidAlgo" as CredentialAlgorithm)).toThrow(
|
||||
"invalid credential algorithm",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("forwarder", () => {
|
||||
it("should retrieve forwarder metadata for a specific vendor", () => {
|
||||
const vendorId = Vendor.bitwarden;
|
||||
const forwarderExtensionId: ForwarderExtensionId = { forwarder: vendorId };
|
||||
const mockMetadata = {
|
||||
id: forwarderExtensionId,
|
||||
type: "email" as CredentialType,
|
||||
} as GeneratorMetadata<any>;
|
||||
|
||||
providers.metadata!.metadata = jest.fn().mockReturnValue(mockMetadata);
|
||||
|
||||
const result = service.forwarder(vendorId);
|
||||
|
||||
expect(result).toBe(mockMetadata);
|
||||
expect(providers.metadata!.metadata).toHaveBeenCalledWith(forwarderExtensionId);
|
||||
});
|
||||
|
||||
it("should log a panic when vendor ID is invalid", () => {
|
||||
const invalidVendorId = "invalid-vendor" as VendorId;
|
||||
providers.metadata!.metadata = jest.fn().mockReturnValue(null);
|
||||
|
||||
expect(() => service.forwarder(invalidVendorId)).toThrow("invalid vendor");
|
||||
});
|
||||
});
|
||||
|
||||
describe("preferences", () => {
|
||||
it("should retrieve credential preferences bound to the user's account", () => {
|
||||
const mockPreferences = { defaultType: "password" };
|
||||
providers.metadata!.preferences = jest.fn().mockReturnValue(mockPreferences);
|
||||
|
||||
const result = service.preferences({ account$: of(account) });
|
||||
|
||||
expect(result).toBe(mockPreferences);
|
||||
});
|
||||
});
|
||||
|
||||
describe("settings", () => {
|
||||
it("should load user settings for account-bound profiles", () => {
|
||||
const mockSettings = { value: { length: 12 } };
|
||||
const mockMetadata = {
|
||||
id: "test",
|
||||
profiles: {
|
||||
[Profile.account]: { id: "accountProfile" },
|
||||
},
|
||||
} as unknown as GeneratorMetadata<any>;
|
||||
|
||||
providers.profile!.settings = jest.fn().mockReturnValue(mockSettings);
|
||||
|
||||
const result = service.settings(mockMetadata, { account$: of(account) });
|
||||
|
||||
expect(result).toBe(mockSettings);
|
||||
});
|
||||
|
||||
it("should load user settings for extension-bound profiles", () => {
|
||||
const mockSettings = new BehaviorSubject({ value: { length: 12 } });
|
||||
const vendorId = Vendor.bitwarden;
|
||||
const forwarderProfile = {
|
||||
id: { forwarder: Bitwarden.id },
|
||||
site: Site.forwarder,
|
||||
type: "extension",
|
||||
};
|
||||
const mockMetadata = {
|
||||
id: { forwarder: vendorId } as ForwarderExtensionId,
|
||||
profiles: {
|
||||
[Profile.account]: forwarderProfile,
|
||||
},
|
||||
} as unknown as GeneratorMetadata<any>;
|
||||
|
||||
mockExtension.settings.mockReturnValue(mockSettings);
|
||||
|
||||
const result = service.settings(mockMetadata, { account$: of(account) });
|
||||
|
||||
expect(result).toBe(mockSettings);
|
||||
});
|
||||
|
||||
it("should log a panic when profile metadata is not found", () => {
|
||||
const mockMetadata = {
|
||||
id: "test",
|
||||
profiles: {},
|
||||
} as unknown as GeneratorMetadata<any>;
|
||||
|
||||
expect(() => service.settings(mockMetadata, { account$: of(account) })).toThrow(
|
||||
"failed to load settings; profile metadata not found",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("policy$", () => {
|
||||
it("should retrieve policy constraints for a specific profile", async () => {
|
||||
const mockConstraints = { minLength: 8 };
|
||||
const mockMetadata = {
|
||||
id: "test",
|
||||
profiles: {
|
||||
[Profile.account]: { id: "accountProfile" },
|
||||
},
|
||||
} as unknown as GeneratorMetadata<any>;
|
||||
|
||||
providers.profile!.constraints$ = jest.fn().mockReturnValue(of(mockConstraints));
|
||||
|
||||
const result = await firstValueFrom(service.policy$(mockMetadata, { account$: of(account) }));
|
||||
|
||||
expect(result).toEqual(mockConstraints);
|
||||
});
|
||||
|
||||
it("should log a panic when profile metadata is not found for policy retrieval", () => {
|
||||
const mockMetadata = {
|
||||
id: "test",
|
||||
profiles: {},
|
||||
} as unknown as GeneratorMetadata<any>;
|
||||
|
||||
expect(() => service.policy$(mockMetadata, { account$: of(account) })).toThrow(
|
||||
"failed to load policy; profile metadata not found",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
import {
|
||||
ReplaySubject,
|
||||
concatMap,
|
||||
filter,
|
||||
first,
|
||||
map,
|
||||
of,
|
||||
share,
|
||||
shareReplay,
|
||||
switchAll,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
timer,
|
||||
zip,
|
||||
} from "rxjs";
|
||||
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BoundDependency, OnDependency } from "@bitwarden/common/tools/dependencies";
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import { anyComplete, memoizedMap } from "@bitwarden/common/tools/rx";
|
||||
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||
|
||||
import { CredentialGeneratorService } from "../abstractions";
|
||||
import {
|
||||
CredentialAlgorithm,
|
||||
Profile,
|
||||
GeneratorMetadata,
|
||||
GeneratorProfile,
|
||||
isForwarderProfile,
|
||||
toVendorId,
|
||||
CredentialType,
|
||||
} from "../metadata";
|
||||
import { CredentialGeneratorProviders } from "../providers";
|
||||
import { GenerateRequest } from "../types";
|
||||
import { isAlgorithmRequest, isTypeRequest } from "../types/metadata-request";
|
||||
|
||||
const ALGORITHM_CACHE_SIZE = 10;
|
||||
const THREE_MINUTES = 3 * 60 * 1000;
|
||||
|
||||
export class DefaultCredentialGeneratorService implements CredentialGeneratorService {
|
||||
/** Instantiate the `DefaultCredentialGeneratorService`.
|
||||
* @param provide application services required by the credential generator.
|
||||
* @param system low-level services required by the credential generator.
|
||||
*/
|
||||
constructor(
|
||||
private readonly provide: CredentialGeneratorProviders,
|
||||
private readonly system: SystemServiceProvider,
|
||||
) {
|
||||
this.log = system.log({ type: "DefaultCredentialGeneratorService" });
|
||||
}
|
||||
|
||||
private readonly log: SemanticLogger;
|
||||
|
||||
generate$(dependencies: OnDependency<GenerateRequest> & BoundDependency<"account", Account>) {
|
||||
const request$ = dependencies.on$.pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
const account$ = dependencies.account$.pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
|
||||
// load algorithm metadata
|
||||
const metadata$ = request$.pipe(
|
||||
switchMap((request) => {
|
||||
if (isAlgorithmRequest(request)) {
|
||||
return of(request.algorithm);
|
||||
} else if (isTypeRequest(request)) {
|
||||
return this.provide.metadata.preference$(request.type, { account$ }).pipe(first());
|
||||
} else {
|
||||
this.log.panic(request, "algorithm or category required");
|
||||
}
|
||||
}),
|
||||
filter((algorithm): algorithm is CredentialAlgorithm => !!algorithm),
|
||||
memoizedMap((algorithm) => this.provide.metadata.metadata(algorithm), {
|
||||
size: ALGORITHM_CACHE_SIZE,
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
// load the active profile's settings
|
||||
const settings$ = zip(request$, metadata$).pipe(
|
||||
map(
|
||||
([request, metadata]) =>
|
||||
[{ ...request, profile: request.profile ?? Profile.account }, metadata] as const,
|
||||
),
|
||||
memoizedMap(
|
||||
([request, metadata]) => {
|
||||
const [profile, algorithm] = [request.profile, metadata.id];
|
||||
|
||||
// settings$ stays hot and buffers the most recent value in the cache
|
||||
// for the next `request`
|
||||
const settings$ = this.settings(metadata, { account$ }, profile).pipe(
|
||||
tap(() => this.log.debug({ algorithm, profile }, "settings update received")),
|
||||
share({
|
||||
connector: () => new ReplaySubject<object>(1, THREE_MINUTES),
|
||||
resetOnRefCountZero: () => timer(THREE_MINUTES),
|
||||
}),
|
||||
tap({
|
||||
subscribe: () => this.log.debug({ algorithm, profile }, "settings hot"),
|
||||
complete: () => this.log.debug({ algorithm, profile }, "settings cold"),
|
||||
}),
|
||||
first(),
|
||||
);
|
||||
|
||||
this.log.debug({ algorithm, profile }, "settings cached");
|
||||
return settings$;
|
||||
},
|
||||
{ key: ([request, metadata]) => `${metadata.id}:${request.profile}` },
|
||||
),
|
||||
switchAll(),
|
||||
);
|
||||
|
||||
// load the algorithm's engine
|
||||
const engine$ = metadata$.pipe(
|
||||
memoizedMap(
|
||||
(metadata) => {
|
||||
const engine = metadata.engine.create(this.provide.generator);
|
||||
|
||||
this.log.debug({ algorithm: metadata.id }, "engine cached");
|
||||
return engine;
|
||||
},
|
||||
{ size: ALGORITHM_CACHE_SIZE },
|
||||
),
|
||||
);
|
||||
|
||||
// generation proper
|
||||
const generate$ = zip([request$, settings$, engine$]).pipe(
|
||||
tap(([request]) => this.log.debug(request, "generating credential")),
|
||||
concatMap(([request, settings, engine]) => engine.generate(request, settings)),
|
||||
takeUntil(anyComplete([settings$])),
|
||||
);
|
||||
|
||||
return generate$;
|
||||
}
|
||||
|
||||
algorithms$(type: CredentialType, dependencies: BoundDependency<"account", Account>) {
|
||||
return this.provide.metadata
|
||||
.algorithms$({ type }, dependencies)
|
||||
.pipe(map((algorithms) => algorithms.map((a) => this.algorithm(a))));
|
||||
}
|
||||
|
||||
algorithms(type: CredentialType | CredentialType[]) {
|
||||
const types: CredentialType[] = Array.isArray(type) ? type : [type];
|
||||
const algorithms = types
|
||||
.flatMap((type) => this.provide.metadata.algorithms({ type }))
|
||||
.map((algorithm) => this.algorithm(algorithm));
|
||||
return algorithms;
|
||||
}
|
||||
|
||||
algorithm(id: CredentialAlgorithm) {
|
||||
const metadata = this.provide.metadata.metadata(id);
|
||||
if (!metadata) {
|
||||
this.log.panic({ algorithm: id }, "invalid credential algorithm");
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
forwarder(id: VendorId) {
|
||||
const metadata = this.provide.metadata.metadata({ forwarder: id });
|
||||
if (!metadata) {
|
||||
this.log.panic({ algorithm: id }, "invalid vendor");
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
preferences(dependencies: BoundDependency<"account", Account>) {
|
||||
return this.provide.metadata.preferences(dependencies);
|
||||
}
|
||||
|
||||
settings<Settings extends object>(
|
||||
metadata: Readonly<GeneratorMetadata<Settings>>,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
profile: GeneratorProfile = Profile.account,
|
||||
) {
|
||||
const activeProfile = metadata.profiles[profile];
|
||||
if (!activeProfile) {
|
||||
this.log.panic(
|
||||
{ algorithm: metadata.id, profile },
|
||||
"failed to load settings; profile metadata not found",
|
||||
);
|
||||
}
|
||||
|
||||
let settings: UserStateSubject<Settings>;
|
||||
if (isForwarderProfile(activeProfile)) {
|
||||
const vendor = toVendorId(metadata.id);
|
||||
if (!vendor) {
|
||||
this.log.panic(
|
||||
{ algorithm: metadata.id, profile },
|
||||
"failed to load extension profile; vendor not specified",
|
||||
);
|
||||
}
|
||||
|
||||
this.log.info({ profile, vendor, site: activeProfile.site }, "loading extension profile");
|
||||
settings = this.system.extension.settings(activeProfile, vendor, dependencies);
|
||||
} else {
|
||||
this.log.info({ profile, algorithm: metadata.id }, "loading generator profile");
|
||||
settings = this.provide.profile.settings(activeProfile, dependencies);
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
policy$<Settings>(
|
||||
metadata: Readonly<GeneratorMetadata<Settings>>,
|
||||
dependencies: BoundDependency<"account", Account>,
|
||||
profile: GeneratorProfile = Profile.account,
|
||||
) {
|
||||
const activeProfile = metadata.profiles[profile];
|
||||
if (!activeProfile) {
|
||||
this.log.panic(
|
||||
{ algorithm: metadata.id, profile },
|
||||
"failed to load policy; profile metadata not found",
|
||||
);
|
||||
}
|
||||
|
||||
return this.provide.profile.constraints$(activeProfile, dependencies);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import { DefaultGeneratorService } from "./default-generator.service";
|
||||
function mockPolicyService(config?: { state?: BehaviorSubject<Policy[]> }) {
|
||||
const service = mock<PolicyService>();
|
||||
|
||||
const stateValue = config?.state ?? new BehaviorSubject<Policy[]>([null]);
|
||||
const stateValue = config?.state ?? new BehaviorSubject<Policy[]>([]);
|
||||
service.policiesByType$.mockReturnValue(stateValue);
|
||||
|
||||
return service;
|
||||
@@ -119,22 +119,22 @@ describe("Password generator service", () => {
|
||||
|
||||
it("should update the evaluator when the password generator policy changes", async () => {
|
||||
// set up dependencies
|
||||
const state = new BehaviorSubject<Policy[]>([null]);
|
||||
const state = new BehaviorSubject<Policy[]>([]);
|
||||
const policy = mockPolicyService({ state });
|
||||
const strategy = mockGeneratorStrategy();
|
||||
const service = new DefaultGeneratorService(strategy, policy);
|
||||
|
||||
// model responses for the observable update. The map is called multiple times,
|
||||
// and the array shift ensures reference equality is maintained.
|
||||
const firstEvaluator = mock<PolicyEvaluator<any, any>>();
|
||||
const secondEvaluator = mock<PolicyEvaluator<any, any>>();
|
||||
const firstEvaluator: PolicyEvaluator<any, any> = mock<PolicyEvaluator<any, any>>();
|
||||
const secondEvaluator: PolicyEvaluator<any, any> = mock<PolicyEvaluator<any, any>>();
|
||||
const evaluators = [firstEvaluator, secondEvaluator];
|
||||
strategy.toEvaluator.mockReturnValueOnce(pipe(map(() => evaluators.shift())));
|
||||
strategy.toEvaluator.mockReturnValueOnce(pipe(map(() => evaluators.shift()!)));
|
||||
|
||||
// act
|
||||
const evaluator$ = service.evaluator$(SomeUser);
|
||||
const firstResult = await firstValueFrom(evaluator$);
|
||||
state.next([null]);
|
||||
state.next([]);
|
||||
const secondResult = await firstValueFrom(evaluator$);
|
||||
|
||||
// assert
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { DefaultGeneratorService } from "./default-generator.service";
|
||||
export { CredentialGeneratorService } from "./credential-generator.service";
|
||||
export { DefaultCredentialGeneratorService } from "./default-credential-generator.service";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
import { DefaultEffUsernameOptions, UsernameDigits } from "../data";
|
||||
import { DefaultEffUsernameOptions } from "../data";
|
||||
import { UsernameRandomizer } from "../engine";
|
||||
import { newDefaultEvaluator } from "../rx";
|
||||
import { EffUsernameGenerationOptions, NoPolicy } from "../types";
|
||||
@@ -10,6 +10,11 @@ import { observe$PerUserId, sharedStateByUserId } from "../util";
|
||||
|
||||
import { EFF_USERNAME_SETTINGS } from "./storage";
|
||||
|
||||
const UsernameDigits = Object.freeze({
|
||||
enabled: 4,
|
||||
disabled: 0,
|
||||
});
|
||||
|
||||
/** Strategy for creating usernames from the EFF wordlist */
|
||||
export class EffUsernameGeneratorStrategy
|
||||
implements GeneratorStrategy<EffUsernameGenerationOptions, NoPolicy>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DefaultPassphraseGenerationOptions, Policies } from "../data";
|
||||
import { DefaultPassphraseGenerationOptions } from "../data";
|
||||
import { PasswordRandomizer } from "../engine";
|
||||
import { PassphraseGeneratorOptionsEvaluator } from "../policies";
|
||||
|
||||
@@ -20,7 +20,7 @@ const SomeUser = "some user" as UserId;
|
||||
describe("Passphrase generation strategy", () => {
|
||||
describe("toEvaluator()", () => {
|
||||
it("should map to the policy evaluator", async () => {
|
||||
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||
const strategy = new PassphraseGeneratorStrategy(null!, null!);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
@@ -44,13 +44,18 @@ describe("Passphrase generation strategy", () => {
|
||||
it.each([[[]], [null], [undefined]])(
|
||||
"should map `%p` to a disabled password policy evaluator",
|
||||
async (policies) => {
|
||||
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||
const strategy = new PassphraseGeneratorStrategy(null!, null!);
|
||||
|
||||
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||
// this case tests when the type system is subverted
|
||||
const evaluator$ = of(policies!).pipe(strategy.toEvaluator());
|
||||
const evaluator = await firstValueFrom(evaluator$);
|
||||
|
||||
expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator);
|
||||
expect(evaluator.policy).toMatchObject(Policies.Passphrase.disabledValue);
|
||||
expect(evaluator.policy).toMatchObject({
|
||||
minNumberWords: 0,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -58,7 +63,7 @@ describe("Passphrase generation strategy", () => {
|
||||
describe("durableState", () => {
|
||||
it("should use password settings key", () => {
|
||||
const provider = mock<StateProvider>();
|
||||
const strategy = new PassphraseGeneratorStrategy(null, provider);
|
||||
const strategy = new PassphraseGeneratorStrategy(null!, provider);
|
||||
|
||||
strategy.durableState(SomeUser);
|
||||
|
||||
@@ -68,7 +73,7 @@ describe("Passphrase generation strategy", () => {
|
||||
|
||||
describe("defaults$", () => {
|
||||
it("should return the default subaddress options", async () => {
|
||||
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||
const strategy = new PassphraseGeneratorStrategy(null!, null!);
|
||||
|
||||
const result = await firstValueFrom(strategy.defaults$(SomeUser));
|
||||
|
||||
@@ -78,7 +83,7 @@ describe("Passphrase generation strategy", () => {
|
||||
|
||||
describe("policy", () => {
|
||||
it("should use password generator policy", () => {
|
||||
const strategy = new PassphraseGeneratorStrategy(null, null);
|
||||
const strategy = new PassphraseGeneratorStrategy(null!, null!);
|
||||
|
||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||
});
|
||||
@@ -95,7 +100,7 @@ describe("Passphrase generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should map options", async () => {
|
||||
const strategy = new PassphraseGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PassphraseGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
numWords: 6,
|
||||
@@ -114,7 +119,7 @@ describe("Passphrase generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should default numWords", async () => {
|
||||
const strategy = new PassphraseGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PassphraseGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
capitalize: true,
|
||||
@@ -132,7 +137,7 @@ describe("Passphrase generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should default capitalize", async () => {
|
||||
const strategy = new PassphraseGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PassphraseGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
numWords: 6,
|
||||
@@ -150,7 +155,7 @@ describe("Passphrase generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should default includeNumber", async () => {
|
||||
const strategy = new PassphraseGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PassphraseGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
numWords: 6,
|
||||
@@ -168,7 +173,7 @@ describe("Passphrase generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should default wordSeparator", async () => {
|
||||
const strategy = new PassphraseGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PassphraseGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
numWords: 6,
|
||||
|
||||
@@ -4,8 +4,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
import { DefaultPassphraseGenerationOptions, Policies } from "../data";
|
||||
import { DefaultPassphraseGenerationOptions } from "../data";
|
||||
import { PasswordRandomizer } from "../engine";
|
||||
import { PassphraseGeneratorOptionsEvaluator, passphraseLeastPrivilege } from "../policies";
|
||||
import { mapPolicyToEvaluator } from "../rx";
|
||||
import { PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
|
||||
import { observe$PerUserId, optionsToEffWordListRequest, sharedStateByUserId } from "../util";
|
||||
@@ -30,7 +31,16 @@ export class PassphraseGeneratorStrategy
|
||||
defaults$ = observe$PerUserId(() => DefaultPassphraseGenerationOptions);
|
||||
readonly policy = PolicyType.PasswordGenerator;
|
||||
toEvaluator() {
|
||||
return mapPolicyToEvaluator(Policies.Passphrase);
|
||||
return mapPolicyToEvaluator({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: Object.freeze({
|
||||
minNumberWords: 0,
|
||||
capitalize: false,
|
||||
includeNumber: false,
|
||||
}),
|
||||
combine: passphraseLeastPrivilege,
|
||||
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
|
||||
});
|
||||
}
|
||||
|
||||
// algorithm
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DefaultPasswordGenerationOptions, Policies } from "../data";
|
||||
import { DefaultPasswordGenerationOptions } from "../data";
|
||||
import { PasswordRandomizer } from "../engine";
|
||||
import { PasswordGeneratorOptionsEvaluator } from "../policies";
|
||||
|
||||
@@ -20,7 +20,7 @@ const SomeUser = "some user" as UserId;
|
||||
describe("Password generation strategy", () => {
|
||||
describe("toEvaluator()", () => {
|
||||
it("should map to a password policy evaluator", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||
const strategy = new PasswordGeneratorStrategy(null!, null!);
|
||||
const policy = mock<Policy>({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: {
|
||||
@@ -52,13 +52,22 @@ describe("Password generation strategy", () => {
|
||||
it.each([[[]], [null], [undefined]])(
|
||||
"should map `%p` to a disabled password policy evaluator",
|
||||
async (policies) => {
|
||||
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||
const strategy = new PasswordGeneratorStrategy(null!, null!);
|
||||
|
||||
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
|
||||
// this case tests when the type system is subverted
|
||||
const evaluator$ = of(policies!).pipe(strategy.toEvaluator());
|
||||
const evaluator = await firstValueFrom(evaluator$);
|
||||
|
||||
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
|
||||
expect(evaluator.policy).toMatchObject(Policies.Password.disabledValue);
|
||||
expect(evaluator.policy).toMatchObject({
|
||||
minLength: 0,
|
||||
useUppercase: false,
|
||||
useLowercase: false,
|
||||
useNumbers: false,
|
||||
numberCount: 0,
|
||||
useSpecial: false,
|
||||
specialCount: 0,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -66,7 +75,7 @@ describe("Password generation strategy", () => {
|
||||
describe("durableState", () => {
|
||||
it("should use password settings key", () => {
|
||||
const provider = mock<StateProvider>();
|
||||
const strategy = new PasswordGeneratorStrategy(null, provider);
|
||||
const strategy = new PasswordGeneratorStrategy(null!, provider);
|
||||
|
||||
strategy.durableState(SomeUser);
|
||||
|
||||
@@ -76,7 +85,7 @@ describe("Password generation strategy", () => {
|
||||
|
||||
describe("defaults$", () => {
|
||||
it("should return the default subaddress options", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||
const strategy = new PasswordGeneratorStrategy(null!, null!);
|
||||
|
||||
const result = await firstValueFrom(strategy.defaults$(SomeUser));
|
||||
|
||||
@@ -86,7 +95,7 @@ describe("Password generation strategy", () => {
|
||||
|
||||
describe("policy", () => {
|
||||
it("should use password generator policy", () => {
|
||||
const strategy = new PasswordGeneratorStrategy(null, null);
|
||||
const strategy = new PasswordGeneratorStrategy(null!, null!);
|
||||
|
||||
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
|
||||
});
|
||||
@@ -103,7 +112,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should map options", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 20,
|
||||
@@ -130,7 +139,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should disable uppercase", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 3,
|
||||
@@ -157,7 +166,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should disable lowercase", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 3,
|
||||
@@ -184,7 +193,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should disable digits", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 3,
|
||||
@@ -211,7 +220,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should disable special", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 3,
|
||||
@@ -238,7 +247,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should override length with minimums", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 20,
|
||||
@@ -265,7 +274,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should default uppercase", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 2,
|
||||
@@ -291,7 +300,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should default lowercase", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 0,
|
||||
@@ -317,7 +326,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should default number", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 0,
|
||||
@@ -343,7 +352,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should default special", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 0,
|
||||
@@ -369,7 +378,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should default minUppercase", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 0,
|
||||
@@ -395,7 +404,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should default minLowercase", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 0,
|
||||
@@ -421,7 +430,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should default minNumber", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 0,
|
||||
@@ -447,7 +456,7 @@ describe("Password generation strategy", () => {
|
||||
});
|
||||
|
||||
it("should default minSpecial", async () => {
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null);
|
||||
const strategy = new PasswordGeneratorStrategy(randomizer, null!);
|
||||
|
||||
const result = await strategy.generate({
|
||||
length: 0,
|
||||
|
||||
@@ -2,8 +2,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
|
||||
import { GeneratorStrategy } from "../abstractions";
|
||||
import { Policies, DefaultPasswordGenerationOptions } from "../data";
|
||||
import { DefaultPasswordGenerationOptions } from "../data";
|
||||
import { PasswordRandomizer } from "../engine";
|
||||
import { PasswordGeneratorOptionsEvaluator, passwordLeastPrivilege } from "../policies";
|
||||
import { mapPolicyToEvaluator } from "../rx";
|
||||
import { PasswordGenerationOptions, PasswordGeneratorPolicy } from "../types";
|
||||
import { observe$PerUserId, optionsToRandomAsciiRequest, sharedStateByUserId } from "../util";
|
||||
@@ -27,7 +28,20 @@ export class PasswordGeneratorStrategy
|
||||
defaults$ = observe$PerUserId(() => DefaultPasswordGenerationOptions);
|
||||
readonly policy = PolicyType.PasswordGenerator;
|
||||
toEvaluator() {
|
||||
return mapPolicyToEvaluator(Policies.Password);
|
||||
return mapPolicyToEvaluator({
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {
|
||||
minLength: 0,
|
||||
useUppercase: false,
|
||||
useLowercase: false,
|
||||
useNumbers: false,
|
||||
numberCount: 0,
|
||||
useSpecial: false,
|
||||
specialCount: 0,
|
||||
},
|
||||
combine: passwordLeastPrivilege,
|
||||
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
|
||||
});
|
||||
}
|
||||
|
||||
// algorithm
|
||||
|
||||
50
libs/tools/generator/core/src/types/algorithm-info.ts
Normal file
50
libs/tools/generator/core/src/types/algorithm-info.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { CredentialAlgorithm, CredentialType } from "../metadata";
|
||||
|
||||
// FIXME: deprecate or delete `AlgorithmInfo` once a better translation
|
||||
// strategy is identified.
|
||||
export type AlgorithmInfo = {
|
||||
/** Uniquely identifies the credential configuration
|
||||
* @example
|
||||
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
|
||||
* // to pattern test whether the credential describes a forwarder algorithm
|
||||
* const meta : CredentialGeneratorInfo = // ...
|
||||
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {};
|
||||
*/
|
||||
id: CredentialAlgorithm;
|
||||
|
||||
/** The kind of credential generated by this configuration */
|
||||
type: CredentialType;
|
||||
|
||||
/** Localized algorithm name */
|
||||
name: string;
|
||||
|
||||
/* Localized generate button label */
|
||||
generate: string;
|
||||
|
||||
/** Localized "credential generated" informational message */
|
||||
onGeneratedMessage: string;
|
||||
|
||||
/* Localized copy button label */
|
||||
copy: string;
|
||||
|
||||
/* Localized dialog button label */
|
||||
useGeneratedValue: string;
|
||||
|
||||
/* Localized generated value label */
|
||||
credentialType: string;
|
||||
|
||||
/** Localized algorithm description */
|
||||
description?: 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;
|
||||
|
||||
/** Well-known fields to display on the options panel or collect from the environment.
|
||||
* @remarks: at present, this is only used by forwarders
|
||||
*/
|
||||
request: readonly string[];
|
||||
};
|
||||
@@ -1,153 +0,0 @@
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
|
||||
import { Constraints } from "@bitwarden/common/tools/types";
|
||||
|
||||
import { Randomizer } from "../abstractions";
|
||||
import { CredentialAlgorithm, CredentialCategory, PolicyConfiguration } from "../types";
|
||||
|
||||
import { CredentialGenerator } from "./credential-generator";
|
||||
|
||||
export type GeneratorDependencyProvider = {
|
||||
randomizer: Randomizer;
|
||||
client: RestClient;
|
||||
i18nService: I18nService;
|
||||
};
|
||||
|
||||
export type AlgorithmInfo = {
|
||||
/** Uniquely identifies the credential configuration
|
||||
* @example
|
||||
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
|
||||
* // to pattern test whether the credential describes a forwarder algorithm
|
||||
* const meta : CredentialGeneratorInfo = // ...
|
||||
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {};
|
||||
*/
|
||||
id: CredentialAlgorithm;
|
||||
|
||||
/** The kind of credential generated by this configuration */
|
||||
category: CredentialCategory;
|
||||
|
||||
/** Localized algorithm name */
|
||||
name: string;
|
||||
|
||||
/* Localized generate button label */
|
||||
generate: string;
|
||||
|
||||
/** Localized "credential generated" informational message */
|
||||
onGeneratedMessage: string;
|
||||
|
||||
/* Localized copy button label */
|
||||
copy: string;
|
||||
|
||||
/* Localized dialog button label */
|
||||
useGeneratedValue: string;
|
||||
|
||||
/* Localized generated value label */
|
||||
credentialType: string;
|
||||
|
||||
/** Localized algorithm description */
|
||||
description?: 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;
|
||||
|
||||
/** Well-known fields to display on the options panel or collect from the environment.
|
||||
* @remarks: at present, this is only used by forwarders
|
||||
*/
|
||||
request: readonly string[];
|
||||
};
|
||||
|
||||
/** Credential generator metadata common across credential generators */
|
||||
export type CredentialGeneratorInfo = {
|
||||
/** Uniquely identifies the credential configuration
|
||||
* @example
|
||||
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
|
||||
* // to pattern test whether the credential describes a forwarder algorithm
|
||||
* const meta : CredentialGeneratorInfo = // ...
|
||||
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {};
|
||||
*/
|
||||
id: CredentialAlgorithm;
|
||||
|
||||
/** The kind of credential generated by this configuration */
|
||||
category: CredentialCategory;
|
||||
|
||||
/** Localization key for the credential name */
|
||||
nameKey: string;
|
||||
|
||||
/** Localization key for the credential description*/
|
||||
descriptionKey?: string;
|
||||
|
||||
/** Localization key for the generate command label */
|
||||
generateKey: string;
|
||||
|
||||
/** Localization key for the copy button label */
|
||||
copyKey: string;
|
||||
|
||||
/** Localization key for the "credential generated" informational message */
|
||||
onGeneratedMessageKey: string;
|
||||
|
||||
/** Localized "use generated credential" button label */
|
||||
useGeneratedValueKey: string;
|
||||
|
||||
/** Localization key for describing the kind of credential generated
|
||||
* by this generator.
|
||||
*/
|
||||
credentialTypeKey: 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;
|
||||
|
||||
/** Well-known fields to display on the options panel or collect from the environment.
|
||||
* @remarks: at present, this is only used by forwarders
|
||||
*/
|
||||
request: readonly string[];
|
||||
};
|
||||
|
||||
/** Credential generator metadata that relies upon typed setting and policy definitions.
|
||||
* @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 CredentialGeneratorConfiguration<Settings, Policy> = CredentialGeneratorInfo & {
|
||||
/** An algorithm that generates credentials when ran. */
|
||||
engine: {
|
||||
/** Factory for the generator
|
||||
*/
|
||||
// FIXME: note that this erases the engine's type so that credentials are
|
||||
// generated uniformly. This property needs to be maintained for
|
||||
// the credential generator, but engine configurations should return
|
||||
// the underlying type. `create` may be able to do double-duty w/ an
|
||||
// engine definition if `CredentialGenerator` can be made covariant.
|
||||
create: (randomizer: GeneratorDependencyProvider) => CredentialGenerator<Settings>;
|
||||
};
|
||||
/** Defines the stored parameters for credential generation */
|
||||
settings: {
|
||||
/** value used when an account's settings haven't been initialized
|
||||
* @deprecated use `ObjectKey.initial` for your desired storage property instead
|
||||
*/
|
||||
initial: Readonly<Partial<Settings>>;
|
||||
|
||||
/** Application-global constraints that apply to account settings */
|
||||
constraints: Constraints<Settings>;
|
||||
|
||||
/** storage location for account-global settings */
|
||||
account: UserKeyDefinition<Settings> | ObjectKey<Settings>;
|
||||
|
||||
/** storage location for *plaintext* settings imports */
|
||||
import?: UserKeyDefinition<Settings> | ObjectKey<Settings, Record<string, never>, Settings>;
|
||||
};
|
||||
|
||||
/** defines how to construct policy for this settings instance */
|
||||
policy: PolicyConfiguration<Policy, Settings>;
|
||||
};
|
||||
10
libs/tools/generator/core/src/types/credential-preference.ts
Normal file
10
libs/tools/generator/core/src/types/credential-preference.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { CredentialAlgorithm, CredentialType } from "../metadata";
|
||||
|
||||
/** The kind of credential to generate using a compound configuration. */
|
||||
// FIXME: extend the preferences to include a preferred forwarder
|
||||
export type CredentialPreference = {
|
||||
[Key in CredentialType]: {
|
||||
algorithm: CredentialAlgorithm;
|
||||
updated: Date;
|
||||
};
|
||||
};
|
||||
@@ -7,32 +7,65 @@ import {
|
||||
|
||||
import { EmailDomainSettings, EmailPrefixSettings } from "../engine";
|
||||
|
||||
// FIXME: this type alias is in place for legacy support purposes;
|
||||
// when replacing the forwarder implementation, eliminate `ForwarderId` and
|
||||
// `IntegrationId`. The proper type is `VendorId`.
|
||||
/** Identifiers for email forwarding services.
|
||||
* @remarks These are used to select forwarder-specific options.
|
||||
* The must be kept in sync with the forwarder implementations.
|
||||
*/
|
||||
export type ForwarderId = IntegrationId;
|
||||
|
||||
/** Metadata format for email forwarding services. */
|
||||
export type ForwarderMetadata = {
|
||||
/** The unique identifier for the forwarder. */
|
||||
id: ForwarderId;
|
||||
|
||||
/** The name of the service the forwarder queries. */
|
||||
name: string;
|
||||
|
||||
/** Whether the forwarder is valid for self-hosted instances of Bitwarden. */
|
||||
validForSelfHosted: boolean;
|
||||
};
|
||||
|
||||
/** Options common to all forwarder APIs */
|
||||
/** Options common to all forwarder APIs
|
||||
* @deprecated use {@link ForwarderOptions} instead.
|
||||
*/
|
||||
export type ApiOptions = ApiSettings & IntegrationRequest;
|
||||
|
||||
/** Api configuration for forwarders that support self-hosted installations. */
|
||||
/** Api configuration for forwarders that support self-hosted installations.
|
||||
* @deprecated use {@link ForwarderOptions} instead.
|
||||
*/
|
||||
export type SelfHostedApiOptions = SelfHostedApiSettings & IntegrationRequest;
|
||||
|
||||
/** Api configuration for forwarders that support custom domains. */
|
||||
/** Api configuration for forwarders that support custom domains.
|
||||
* @deprecated use {@link ForwarderOptions} instead.
|
||||
*/
|
||||
export type EmailDomainOptions = EmailDomainSettings;
|
||||
|
||||
/** Api configuration for forwarders that support custom email parts. */
|
||||
/** Api configuration for forwarders that support custom email parts.
|
||||
* @deprecated use {@link ForwarderOptions} instead.
|
||||
*/
|
||||
export type EmailPrefixOptions = EmailDomainSettings & EmailPrefixSettings;
|
||||
|
||||
/** These options are used by all forwarders; each forwarder uses a different set,
|
||||
* as defined by `GeneratorMetadata<T>.capabilities.fields`.
|
||||
*/
|
||||
export type ForwarderOptions = Partial<
|
||||
{
|
||||
/** bearer token that authenticates bitwarden to the forwarder.
|
||||
* This is required to issue an API request.
|
||||
*/
|
||||
token: string;
|
||||
|
||||
/** The base URL of the forwarder's API.
|
||||
* When this is undefined or empty, the forwarder's default production API is used.
|
||||
*/
|
||||
baseUrl: string;
|
||||
|
||||
/** The domain part of the generated email address.
|
||||
* @remarks The domain should be authorized by the forwarder before
|
||||
* submitting a request through bitwarden.
|
||||
* @example If the domain is `domain.io` and the generated username
|
||||
* is `jd`, then the generated email address will be `jd@domain.io`
|
||||
*/
|
||||
domain: string;
|
||||
|
||||
/** A prefix joined to the generated email address' username.
|
||||
* @example If the prefix is `foo`, the generated username is `bar`,
|
||||
* and the domain is `domain.io`, then the generated email address is
|
||||
* `foobar@domain.io`.
|
||||
*/
|
||||
prefix: string;
|
||||
} & EmailDomainSettings &
|
||||
EmailPrefixSettings &
|
||||
SelfHostedApiSettings
|
||||
>;
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { RequireExactlyOne } from "type-fest";
|
||||
|
||||
import { CredentialType, GeneratorProfile, CredentialAlgorithm } from "../metadata";
|
||||
|
||||
/** Contextual information about the application state when a generator is invoked.
|
||||
*/
|
||||
export type GenerateRequest = {
|
||||
export type GenerateRequest = RequireExactlyOne<
|
||||
{ type: CredentialType; algorithm: CredentialAlgorithm },
|
||||
"type" | "algorithm"
|
||||
> & {
|
||||
profile?: GeneratorProfile;
|
||||
|
||||
/** Traces the origin of the generation request. This parameter is
|
||||
* copied to the generated credential.
|
||||
*
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
import { CredentialAlgorithm, GeneratedCredential } from ".";
|
||||
import { Type } from "../metadata";
|
||||
|
||||
import { GeneratedCredential } from "./generated-credential";
|
||||
|
||||
describe("GeneratedCredential", () => {
|
||||
describe("constructor", () => {
|
||||
it("assigns credential", () => {
|
||||
const result = new GeneratedCredential("example", "passphrase", new Date(100));
|
||||
const result = new GeneratedCredential("example", Type.password, new Date(100));
|
||||
|
||||
expect(result.credential).toEqual("example");
|
||||
});
|
||||
|
||||
it("assigns category", () => {
|
||||
const result = new GeneratedCredential("example", "passphrase", new Date(100));
|
||||
const result = new GeneratedCredential("example", Type.password, new Date(100));
|
||||
|
||||
expect(result.category).toEqual("passphrase");
|
||||
expect(result.category).toEqual(Type.password);
|
||||
});
|
||||
|
||||
it("passes through date parameters", () => {
|
||||
const result = new GeneratedCredential("example", "password", new Date(100));
|
||||
const result = new GeneratedCredential("example", Type.password, new Date(100));
|
||||
|
||||
expect(result.generationDate).toEqual(new Date(100));
|
||||
});
|
||||
|
||||
it("converts numeric dates to Dates", () => {
|
||||
const result = new GeneratedCredential("example", "password", 100);
|
||||
const result = new GeneratedCredential("example", Type.password, 100);
|
||||
|
||||
expect(result.generationDate).toEqual(new Date(100));
|
||||
});
|
||||
});
|
||||
|
||||
it("toJSON converts from a credential into a JSON object", () => {
|
||||
const credential = new GeneratedCredential("example", "password", new Date(100));
|
||||
const credential = new GeneratedCredential("example", Type.password, new Date(100));
|
||||
|
||||
const result = credential.toJSON();
|
||||
|
||||
expect(result).toEqual({
|
||||
credential: "example",
|
||||
category: "password" as CredentialAlgorithm,
|
||||
category: Type.password,
|
||||
generationDate: 100,
|
||||
});
|
||||
});
|
||||
@@ -42,7 +44,7 @@ describe("GeneratedCredential", () => {
|
||||
it("fromJSON converts Json objects into credentials", () => {
|
||||
const jsonValue = {
|
||||
credential: "example",
|
||||
category: "password" as CredentialAlgorithm,
|
||||
category: Type.password,
|
||||
generationDate: 100,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { CredentialAlgorithm } from "./generator-type";
|
||||
import { CredentialType } from "../metadata";
|
||||
|
||||
/** A credential generation result */
|
||||
export class GeneratedCredential {
|
||||
/**
|
||||
* Instantiates a generated credential
|
||||
* @param credential The value of the generated credential (e.g. a password)
|
||||
* @param category The kind of credential
|
||||
* @param category The type of credential
|
||||
* @param generationDate The date that the credential was generated.
|
||||
* Numeric values should are interpreted using {@link Date.valueOf}
|
||||
* semantics.
|
||||
@@ -16,7 +16,9 @@ export class GeneratedCredential {
|
||||
*/
|
||||
constructor(
|
||||
readonly credential: string,
|
||||
readonly category: CredentialAlgorithm,
|
||||
// FIXME: create a way to migrate the data stored in `category` to a new `type`
|
||||
// field. The hard part: This requires the migration occur post-decryption.
|
||||
readonly category: CredentialType,
|
||||
generationDate: Date | number,
|
||||
readonly source?: string,
|
||||
readonly website?: string,
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
||||
|
||||
import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "../data/generator-types";
|
||||
import { AlgorithmsByType, CredentialType } from "../metadata";
|
||||
|
||||
/** A type of password that may be generated by the credential generator. */
|
||||
export type PasswordAlgorithm = (typeof PasswordAlgorithms)[number];
|
||||
|
||||
/** 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];
|
||||
|
||||
export type ForwarderIntegration = { forwarder: IntegrationId & VendorId };
|
||||
|
||||
/** Returns true when the input algorithm is a forwarder integration. */
|
||||
export function isForwarderIntegration(
|
||||
algorithm: CredentialAlgorithm,
|
||||
): algorithm is ForwarderIntegration {
|
||||
return algorithm && typeof algorithm === "object" && "forwarder" in algorithm;
|
||||
}
|
||||
|
||||
export function isSameAlgorithm(lhs: CredentialAlgorithm, rhs: CredentialAlgorithm) {
|
||||
if (lhs === rhs) {
|
||||
return true;
|
||||
} else if (isForwarderIntegration(lhs) && isForwarderIntegration(rhs)) {
|
||||
return lhs.forwarder === rhs.forwarder;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** A type of credential that may be generated by the credential generator. */
|
||||
export type CredentialAlgorithm =
|
||||
| PasswordAlgorithm
|
||||
| UsernameAlgorithm
|
||||
| EmailAlgorithm
|
||||
| ForwarderIntegration;
|
||||
|
||||
/** 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 | ForwarderIntegration)[]>,
|
||||
});
|
||||
|
||||
/** 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) || isForwarderIntegration(algorithm);
|
||||
}
|
||||
|
||||
/** 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 CredentialType & CredentialCategory]: {
|
||||
algorithm: CredentialAlgorithm & (typeof AlgorithmsByType)[Key][number];
|
||||
updated: Date;
|
||||
};
|
||||
};
|
||||
@@ -1,16 +1,14 @@
|
||||
import { EmailAlgorithm, PasswordAlgorithm, UsernameAlgorithm } from "./generator-type";
|
||||
|
||||
export * from "./boundary";
|
||||
export * from "./catchall-generator-options";
|
||||
export * from "./credential-generator";
|
||||
export * from "./credential-generator-configuration";
|
||||
export * from "./algorithm-info";
|
||||
export * from "./eff-username-generator-options";
|
||||
export * from "./forwarder-options";
|
||||
export * from "./generate-request";
|
||||
export * from "./generator-constraints";
|
||||
export * from "./generated-credential";
|
||||
export * from "./generator-options";
|
||||
export * from "./generator-type";
|
||||
export * from "./credential-preference";
|
||||
export * from "./no-policy";
|
||||
export * from "./passphrase-generation-options";
|
||||
export * from "./passphrase-generator-policy";
|
||||
@@ -19,13 +17,3 @@ 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 = PasswordAlgorithm | UsernameAlgorithm | EmailAlgorithm;
|
||||
|
||||
/** Provided for backwards compatibility only.
|
||||
* @deprecated Use one of the Algorithm types instead.
|
||||
*/
|
||||
export type PasswordType = PasswordAlgorithm;
|
||||
|
||||
@@ -3,8 +3,6 @@ import { Policy as AdminPolicy } from "@bitwarden/common/admin-console/models/do
|
||||
|
||||
import { PolicyEvaluator } from "../abstractions";
|
||||
|
||||
import { GeneratorConstraints } from "./generator-constraints";
|
||||
|
||||
/** Determines how to construct a password generator policy */
|
||||
export type PolicyConfiguration<Policy, Settings> = {
|
||||
type: PolicyType;
|
||||
@@ -22,15 +20,4 @@ export type PolicyConfiguration<Policy, Settings> = {
|
||||
* Use `toConstraints` instead.
|
||||
*/
|
||||
createEvaluator: (policy: Policy) => PolicyEvaluator<Policy, Settings>;
|
||||
|
||||
/** Converts policy service data into actionable policy constraints.
|
||||
*
|
||||
* @param policy - the policy to map into policy constraints.
|
||||
* @param email - the default email to extend.
|
||||
*
|
||||
* @remarks this version includes constraints needed for the reactive forms;
|
||||
* it was introduced so that the constraints can be incrementally introduced
|
||||
* as the new UI is built.
|
||||
*/
|
||||
toConstraints: (policy: Policy, email: string) => GeneratorConstraints<Settings>;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DefaultPassphraseGenerationOptions } from "./data";
|
||||
import { optionsToEffWordListRequest, optionsToRandomAsciiRequest, sum } from "./util";
|
||||
import { GeneratorConstraints, PassphraseGenerationOptions } from "./types";
|
||||
import { optionsToEffWordListRequest, optionsToRandomAsciiRequest, sum, equivalent } from "./util";
|
||||
|
||||
describe("sum", () => {
|
||||
it("returns 0 when the list is empty", () => {
|
||||
@@ -424,3 +425,90 @@ describe("optionsToEffWordListRequest", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("equivalent", () => {
|
||||
// constructs a partial constraints object; only the properties compared
|
||||
// by `equivalent` are included.
|
||||
function createConstraints(
|
||||
policyInEffect: boolean,
|
||||
numWordsMin?: number,
|
||||
capitalize?: boolean,
|
||||
): GeneratorConstraints<PassphraseGenerationOptions> {
|
||||
return {
|
||||
constraints: {
|
||||
policyInEffect,
|
||||
numWords: numWordsMin !== undefined ? { min: numWordsMin } : undefined,
|
||||
capitalize: capitalize !== undefined ? { requiredValue: capitalize } : undefined,
|
||||
},
|
||||
} as unknown as GeneratorConstraints<PassphraseGenerationOptions>;
|
||||
}
|
||||
|
||||
it("should return true for identical constraints", () => {
|
||||
const lhs = createConstraints(false, 3, true);
|
||||
const rhs = createConstraints(false, 3, true);
|
||||
|
||||
expect(equivalent(lhs, rhs)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when policy effects differ", () => {
|
||||
const lhs = createConstraints(true, 3);
|
||||
const rhs = createConstraints(false, 3);
|
||||
|
||||
expect(equivalent(lhs, rhs)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when constraint values differ", () => {
|
||||
const lhs = createConstraints(false, 3);
|
||||
const rhs = createConstraints(false, 4);
|
||||
|
||||
expect(equivalent(lhs, rhs)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when one has additional constraints", () => {
|
||||
const lhs = createConstraints(false, 3, true);
|
||||
const rhs = createConstraints(false, 3);
|
||||
|
||||
expect(equivalent(lhs, rhs)).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle undefined constraints", () => {
|
||||
const lhs = createConstraints(false);
|
||||
const rhs = createConstraints(false);
|
||||
|
||||
expect(equivalent(lhs, rhs)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle empty constraint objects", () => {
|
||||
const lhs = {
|
||||
constraints: {
|
||||
policyInEffect: false,
|
||||
numWords: {},
|
||||
},
|
||||
} as unknown as GeneratorConstraints<PassphraseGenerationOptions>;
|
||||
const rhs = {
|
||||
constraints: {
|
||||
policyInEffect: false,
|
||||
numWords: {},
|
||||
},
|
||||
} as unknown as GeneratorConstraints<PassphraseGenerationOptions>;
|
||||
|
||||
expect(equivalent(lhs, rhs)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when inner constraint properties differ", () => {
|
||||
const lhs = {
|
||||
constraints: {
|
||||
policyInEffect: false,
|
||||
numWords: { min: 3, max: 5 },
|
||||
},
|
||||
} as any;
|
||||
const rhs = {
|
||||
constraints: {
|
||||
policyInEffect: false,
|
||||
numWords: { min: 3, max: 6 },
|
||||
},
|
||||
} as unknown as GeneratorConstraints<PassphraseGenerationOptions>;
|
||||
|
||||
expect(equivalent(lhs, rhs)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,11 @@ import {
|
||||
DefaultPassphraseGenerationOptions,
|
||||
DefaultPasswordGenerationOptions,
|
||||
} from "./data";
|
||||
import { PassphraseGenerationOptions, PasswordGenerationOptions } from "./types";
|
||||
import {
|
||||
PassphraseGenerationOptions,
|
||||
PasswordGenerationOptions,
|
||||
GeneratorConstraints,
|
||||
} from "./types";
|
||||
|
||||
/** construct a method that outputs a copy of `defaultValue` as an observable. */
|
||||
export function observe$PerUserId<Value>(
|
||||
@@ -135,3 +139,30 @@ export function optionsToEffWordListRequest(options: PassphraseGenerationOptions
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
export function equivalent<T>(lhs: GeneratorConstraints<T>, rhs: GeneratorConstraints<T>): boolean {
|
||||
if (lhs.constraints.policyInEffect !== rhs.constraints.policyInEffect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// safe because `Constraints<T>` shares keys with `T`
|
||||
const keys = Object.keys(lhs.constraints) as (keyof T)[];
|
||||
|
||||
// use `for` loop so that `equivalent` can return as soon as the constraints
|
||||
// differ. Using `array.xyz` would evaluate the whole key list eagerly
|
||||
for (const k of keys) {
|
||||
if (!(k in rhs.constraints)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lhsConstraints: any = lhs.constraints[k] ?? {};
|
||||
const rhsConstraints: any = rhs.constraints[k] ?? {};
|
||||
|
||||
const innerKeys = Object.keys(lhsConstraints);
|
||||
if (innerKeys.some((k) => lhsConstraints[k] !== rhsConstraints[k])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,6 @@
|
||||
"@bitwarden/key-management": ["../../../key-management/src"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"../extensions/src/history/generator-history.abstraction.ts",
|
||||
"../extensions/src/navigation/generator-navigation.service.abstraction.ts"
|
||||
],
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user