mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +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:
@@ -2204,6 +2204,9 @@
|
|||||||
"useThisPassword": {
|
"useThisPassword": {
|
||||||
"message": "Use this password"
|
"message": "Use this password"
|
||||||
},
|
},
|
||||||
|
"useThisPassphrase": {
|
||||||
|
"message": "Use this passphrase"
|
||||||
|
},
|
||||||
"useThisUsername": {
|
"useThisUsername": {
|
||||||
"message": "Use this username"
|
"message": "Use this username"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -351,12 +351,6 @@
|
|||||||
"other": {
|
"other": {
|
||||||
"message": "Other"
|
"message": "Other"
|
||||||
},
|
},
|
||||||
"generatePassword": {
|
|
||||||
"message": "Generate password"
|
|
||||||
},
|
|
||||||
"generatePassphrase": {
|
|
||||||
"message": "Generate passphrase"
|
|
||||||
},
|
|
||||||
"type": {
|
"type": {
|
||||||
"message": "Type"
|
"message": "Type"
|
||||||
},
|
},
|
||||||
@@ -2633,6 +2627,24 @@
|
|||||||
"usernameGenerator": {
|
"usernameGenerator": {
|
||||||
"message": "Username generator"
|
"message": "Username generator"
|
||||||
},
|
},
|
||||||
|
"generatePassword": {
|
||||||
|
"message": "Generate password"
|
||||||
|
},
|
||||||
|
"generatePassphrase": {
|
||||||
|
"message": "Generate passphrase"
|
||||||
|
},
|
||||||
|
"passwordGenerated": {
|
||||||
|
"message": "Password generated"
|
||||||
|
},
|
||||||
|
"passphraseGenerated": {
|
||||||
|
"message": "Passphrase generated"
|
||||||
|
},
|
||||||
|
"usernameGenerated": {
|
||||||
|
"message": "Username generated"
|
||||||
|
},
|
||||||
|
"emailGenerated": {
|
||||||
|
"message": "Email generated"
|
||||||
|
},
|
||||||
"spinboxBoundariesHint": {
|
"spinboxBoundariesHint": {
|
||||||
"message": "Value must be between $MIN$ and $MAX$.",
|
"message": "Value must be between $MIN$ and $MAX$.",
|
||||||
"description": "Explains spin box minimum and maximum values to the user",
|
"description": "Explains spin box minimum and maximum values to the user",
|
||||||
@@ -2686,6 +2698,15 @@
|
|||||||
"useThisEmail": {
|
"useThisEmail": {
|
||||||
"message": "Use this email"
|
"message": "Use this email"
|
||||||
},
|
},
|
||||||
|
"useThisPassword": {
|
||||||
|
"message": "Use this password"
|
||||||
|
},
|
||||||
|
"useThisPassphrase": {
|
||||||
|
"message": "Use this passphrase"
|
||||||
|
},
|
||||||
|
"useThisUsername": {
|
||||||
|
"message": "Use this username"
|
||||||
|
},
|
||||||
"random": {
|
"random": {
|
||||||
"message": "Random"
|
"message": "Random"
|
||||||
},
|
},
|
||||||
@@ -3051,12 +3072,6 @@
|
|||||||
"weakAndBreachedMasterPasswordDesc": {
|
"weakAndBreachedMasterPasswordDesc": {
|
||||||
"message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?"
|
"message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?"
|
||||||
},
|
},
|
||||||
"useThisPassword": {
|
|
||||||
"message": "Use this password"
|
|
||||||
},
|
|
||||||
"useThisUsername": {
|
|
||||||
"message": "Use this username"
|
|
||||||
},
|
|
||||||
"checkForBreaches": {
|
"checkForBreaches": {
|
||||||
"message": "Check known data breaches for this password"
|
"message": "Check known data breaches for this password"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { BehaviorSubject, map } from "rxjs";
|
|||||||
|
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { Generators } from "@bitwarden/generator-core";
|
import { BuiltIn, Profile } from "@bitwarden/generator-core";
|
||||||
|
|
||||||
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||||
|
|
||||||
@@ -26,14 +26,22 @@ export class PasswordGeneratorPolicy extends BasePolicy {
|
|||||||
export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
|
export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
|
||||||
// these properties forward the application default settings to the UI
|
// these properties forward the application default settings to the UI
|
||||||
// for HTML attribute bindings
|
// for HTML attribute bindings
|
||||||
protected readonly minLengthMin = Generators.password.settings.constraints.length.min;
|
protected readonly minLengthMin =
|
||||||
protected readonly minLengthMax = Generators.password.settings.constraints.length.max;
|
BuiltIn.password.profiles[Profile.account].constraints.default.length.min;
|
||||||
protected readonly minNumbersMin = Generators.password.settings.constraints.minNumber.min;
|
protected readonly minLengthMax =
|
||||||
protected readonly minNumbersMax = Generators.password.settings.constraints.minNumber.max;
|
BuiltIn.password.profiles[Profile.account].constraints.default.length.max;
|
||||||
protected readonly minSpecialMin = Generators.password.settings.constraints.minSpecial.min;
|
protected readonly minNumbersMin =
|
||||||
protected readonly minSpecialMax = Generators.password.settings.constraints.minSpecial.max;
|
BuiltIn.password.profiles[Profile.account].constraints.default.minNumber.min;
|
||||||
protected readonly minNumberWordsMin = Generators.passphrase.settings.constraints.numWords.min;
|
protected readonly minNumbersMax =
|
||||||
protected readonly minNumberWordsMax = Generators.passphrase.settings.constraints.numWords.max;
|
BuiltIn.password.profiles[Profile.account].constraints.default.minNumber.max;
|
||||||
|
protected readonly minSpecialMin =
|
||||||
|
BuiltIn.password.profiles[Profile.account].constraints.default.minSpecial.min;
|
||||||
|
protected readonly minSpecialMax =
|
||||||
|
BuiltIn.password.profiles[Profile.account].constraints.default.minSpecial.max;
|
||||||
|
protected readonly minNumberWordsMin =
|
||||||
|
BuiltIn.passphrase.profiles[Profile.account].constraints.default.numWords.min;
|
||||||
|
protected readonly minNumberWordsMax =
|
||||||
|
BuiltIn.passphrase.profiles[Profile.account].constraints.default.numWords.max;
|
||||||
|
|
||||||
data = this.formBuilder.group({
|
data = this.formBuilder.group({
|
||||||
overridePasswordType: [null],
|
overridePasswordType: [null],
|
||||||
|
|||||||
@@ -547,12 +547,6 @@
|
|||||||
"message": "Toggle collapse",
|
"message": "Toggle collapse",
|
||||||
"description": "Toggling an expand/collapse state."
|
"description": "Toggling an expand/collapse state."
|
||||||
},
|
},
|
||||||
"generatePassword": {
|
|
||||||
"message": "Generate password"
|
|
||||||
},
|
|
||||||
"generatePassphrase": {
|
|
||||||
"message": "Generate passphrase"
|
|
||||||
},
|
|
||||||
"checkPassword": {
|
"checkPassword": {
|
||||||
"message": "Check if password has been exposed."
|
"message": "Check if password has been exposed."
|
||||||
},
|
},
|
||||||
@@ -6825,6 +6819,24 @@
|
|||||||
"generateEmail": {
|
"generateEmail": {
|
||||||
"message": "Generate email"
|
"message": "Generate email"
|
||||||
},
|
},
|
||||||
|
"generatePassword": {
|
||||||
|
"message": "Generate password"
|
||||||
|
},
|
||||||
|
"generatePassphrase": {
|
||||||
|
"message": "Generate passphrase"
|
||||||
|
},
|
||||||
|
"passwordGenerated": {
|
||||||
|
"message": "Password generated"
|
||||||
|
},
|
||||||
|
"passphraseGenerated": {
|
||||||
|
"message": "Passphrase generated"
|
||||||
|
},
|
||||||
|
"usernameGenerated": {
|
||||||
|
"message": "Username generated"
|
||||||
|
},
|
||||||
|
"emailGenerated": {
|
||||||
|
"message": "Email generated"
|
||||||
|
},
|
||||||
"spinboxBoundariesHint": {
|
"spinboxBoundariesHint": {
|
||||||
"message": "Value must be between $MIN$ and $MAX$.",
|
"message": "Value must be between $MIN$ and $MAX$.",
|
||||||
"description": "Explains spin box minimum and maximum values to the user",
|
"description": "Explains spin box minimum and maximum values to the user",
|
||||||
@@ -6888,6 +6900,9 @@
|
|||||||
"useThisPassword": {
|
"useThisPassword": {
|
||||||
"message": "Use this password"
|
"message": "Use this password"
|
||||||
},
|
},
|
||||||
|
"useThisPassphrase": {
|
||||||
|
"message": "Use this passphrase"
|
||||||
|
},
|
||||||
"useThisUsername": {
|
"useThisUsername": {
|
||||||
"message": "Use this username"
|
"message": "Use this username"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const SomeProvider = {
|
|||||||
} as LegacyEncryptorProvider,
|
} as LegacyEncryptorProvider,
|
||||||
state: SomeStateProvider,
|
state: SomeStateProvider,
|
||||||
log: disabledSemanticLoggerProvider,
|
log: disabledSemanticLoggerProvider,
|
||||||
|
now: Date.now,
|
||||||
} as UserStateSubjectDependencyProvider;
|
} as UserStateSubjectDependencyProvider;
|
||||||
|
|
||||||
const SomeExtension: ExtensionMetadata = {
|
const SomeExtension: ExtensionMetadata = {
|
||||||
|
|||||||
24
libs/common/src/tools/log/disabled-logger.ts
Normal file
24
libs/common/src/tools/log/disabled-logger.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { deepFreeze } from "../util";
|
||||||
|
|
||||||
|
import { SemanticLogger } from "./semantic-logger.abstraction";
|
||||||
|
|
||||||
|
/** All disabled loggers emitted by this module are `===` to this logger. */
|
||||||
|
export const DISABLED_LOGGER: SemanticLogger = deepFreeze({
|
||||||
|
debug<T>(_content: Jsonify<T>, _message?: string): void {},
|
||||||
|
|
||||||
|
info<T>(_content: Jsonify<T>, _message?: string): void {},
|
||||||
|
|
||||||
|
warn<T>(_content: Jsonify<T>, _message?: string): void {},
|
||||||
|
|
||||||
|
error<T>(_content: Jsonify<T>, _message?: string): void {},
|
||||||
|
|
||||||
|
panic<T>(content: Jsonify<T>, message?: string): never {
|
||||||
|
if (typeof content === "string" && !message) {
|
||||||
|
throw new Error(content);
|
||||||
|
} else {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Jsonify } from "type-fest";
|
|
||||||
|
|
||||||
import { SemanticLogger } from "./semantic-logger.abstraction";
|
|
||||||
|
|
||||||
/** Disables semantic logs. Still panics. */
|
|
||||||
export class DisabledSemanticLogger implements SemanticLogger {
|
|
||||||
debug<T>(_content: Jsonify<T>, _message?: string): void {}
|
|
||||||
|
|
||||||
info<T>(_content: Jsonify<T>, _message?: string): void {}
|
|
||||||
|
|
||||||
warn<T>(_content: Jsonify<T>, _message?: string): void {}
|
|
||||||
|
|
||||||
error<T>(_content: Jsonify<T>, _message?: string): void {}
|
|
||||||
|
|
||||||
panic<T>(content: Jsonify<T>, message?: string): never {
|
|
||||||
if (typeof content === "string" && !message) {
|
|
||||||
throw new Error(content);
|
|
||||||
} else {
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,10 @@ import { Jsonify } from "type-fest";
|
|||||||
import { LogService } from "../../platform/abstractions/log.service";
|
import { LogService } from "../../platform/abstractions/log.service";
|
||||||
|
|
||||||
import { DefaultSemanticLogger } from "./default-semantic-logger";
|
import { DefaultSemanticLogger } from "./default-semantic-logger";
|
||||||
import { DisabledSemanticLogger } from "./disabled-semantic-logger";
|
import { DISABLED_LOGGER } from "./disabled-logger";
|
||||||
import { SemanticLogger } from "./semantic-logger.abstraction";
|
import { SemanticLogger } from "./semantic-logger.abstraction";
|
||||||
|
import { LogProvider } from "./types";
|
||||||
/** A type for injection of a log provider */
|
import { warnLoggingEnabled } from "./util";
|
||||||
export type LogProvider = <Context>(context: Jsonify<Context>) => SemanticLogger;
|
|
||||||
|
|
||||||
/** Instantiates a semantic logger that emits nothing when a message
|
/** Instantiates a semantic logger that emits nothing when a message
|
||||||
* is logged.
|
* is logged.
|
||||||
@@ -18,38 +17,72 @@ export type LogProvider = <Context>(context: Jsonify<Context>) => SemanticLogger
|
|||||||
export function disabledSemanticLoggerProvider<Context extends object>(
|
export function disabledSemanticLoggerProvider<Context extends object>(
|
||||||
_context: Jsonify<Context>,
|
_context: Jsonify<Context>,
|
||||||
): SemanticLogger {
|
): SemanticLogger {
|
||||||
return new DisabledSemanticLogger();
|
return DISABLED_LOGGER;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Instantiates a semantic logger that emits logs to the console.
|
/** Instantiates a semantic logger that emits logs to the console.
|
||||||
* @param context a static payload that is cloned when the logger
|
* @param logService writes semantic logs to the console
|
||||||
* logs a message. The `messages`, `level`, and `content` fields
|
|
||||||
* are reserved for use by loggers.
|
|
||||||
* @param settings specializes how the semantic logger functions.
|
|
||||||
* If this is omitted, the logger suppresses debug messages.
|
|
||||||
*/
|
*/
|
||||||
export function consoleSemanticLoggerProvider<Context extends object>(
|
export function consoleSemanticLoggerProvider(logService: LogService): LogProvider {
|
||||||
logger: LogService,
|
function provider<Context extends object>(context: Jsonify<Context>) {
|
||||||
context: Jsonify<Context>,
|
const logger = new DefaultSemanticLogger(logService, context);
|
||||||
): SemanticLogger {
|
|
||||||
return new DefaultSemanticLogger(logger, context);
|
warnLoggingEnabled(logService, "consoleSemanticLoggerProvider", context);
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Instantiates a semantic logger that emits logs to the console.
|
/** Instantiates a semantic logger that emits logs to the console when the
|
||||||
|
* context's `type` matches its values.
|
||||||
|
* @param logService writes semantic logs to the console
|
||||||
|
* @param types the values to match against
|
||||||
|
*/
|
||||||
|
export function enableLogForTypes(logService: LogService, types: string[]): LogProvider {
|
||||||
|
if (types.length) {
|
||||||
|
warnLoggingEnabled(logService, "enableLogForTypes", { types });
|
||||||
|
}
|
||||||
|
|
||||||
|
function provider<Context extends object>(context: Jsonify<Context>) {
|
||||||
|
const { type } = context as { type?: unknown };
|
||||||
|
if (typeof type === "string" && types.includes(type)) {
|
||||||
|
const logger = new DefaultSemanticLogger(logService, context);
|
||||||
|
|
||||||
|
warnLoggingEnabled(logService, "enableLogForTypes", {
|
||||||
|
targetType: type,
|
||||||
|
available: types,
|
||||||
|
loggerContext: context,
|
||||||
|
});
|
||||||
|
return logger;
|
||||||
|
} else {
|
||||||
|
return DISABLED_LOGGER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Instantiates a semantic logger that emits logs to the console when its enabled.
|
||||||
|
* @param enable logs are emitted when this is true
|
||||||
|
* @param logService writes semantic logs to the console
|
||||||
* @param context a static payload that is cloned when the logger
|
* @param context a static payload that is cloned when the logger
|
||||||
* logs a message. The `messages`, `level`, and `content` fields
|
* logs a message.
|
||||||
* are reserved for use by loggers.
|
*
|
||||||
* @param settings specializes how the semantic logger functions.
|
* @remarks The `message`, `level`, `provider`, and `content` fields
|
||||||
* If this is omitted, the logger suppresses debug messages.
|
* are reserved for use by the semantic logging system.
|
||||||
*/
|
*/
|
||||||
export function ifEnabledSemanticLoggerProvider<Context extends object>(
|
export function ifEnabledSemanticLoggerProvider<Context extends object>(
|
||||||
enable: boolean,
|
enable: boolean,
|
||||||
logger: LogService,
|
logService: LogService,
|
||||||
context: Jsonify<Context>,
|
context: Jsonify<Context>,
|
||||||
) {
|
) {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
return consoleSemanticLoggerProvider(logger, context);
|
const logger = new DefaultSemanticLogger(logService, context);
|
||||||
|
|
||||||
|
warnLoggingEnabled(logService, "ifEnabledSemanticLoggerProvider", context);
|
||||||
|
return logger;
|
||||||
} else {
|
} else {
|
||||||
return disabledSemanticLoggerProvider(context);
|
return DISABLED_LOGGER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
export * from "./factory";
|
export * from "./factory";
|
||||||
|
export * from "./disabled-logger";
|
||||||
|
export { LogProvider } from "./types";
|
||||||
export { SemanticLogger } from "./semantic-logger.abstraction";
|
export { SemanticLogger } from "./semantic-logger.abstraction";
|
||||||
|
|||||||
11
libs/common/src/tools/log/types.ts
Normal file
11
libs/common/src/tools/log/types.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { SemanticLogger } from "./semantic-logger.abstraction";
|
||||||
|
|
||||||
|
/** Creates a semantic logger.
|
||||||
|
* @param context all logs emitted by the logger are extended with
|
||||||
|
* these fields.
|
||||||
|
* @remarks The `message`, `level`, `provider`, and `content` fields
|
||||||
|
* are reserved for use by the semantic logging system.
|
||||||
|
*/
|
||||||
|
export type LogProvider = <Context extends object>(context: Jsonify<Context>) => SemanticLogger;
|
||||||
12
libs/common/src/tools/log/util.ts
Normal file
12
libs/common/src/tools/log/util.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { LogService } from "../../platform/abstractions/log.service";
|
||||||
|
|
||||||
|
// show our GRIT - these functions implement generalized logging
|
||||||
|
// controls and should return DISABLED_LOGGER in production.
|
||||||
|
export function warnLoggingEnabled(logService: LogService, method: string, context?: any) {
|
||||||
|
logService.warning({
|
||||||
|
method,
|
||||||
|
context,
|
||||||
|
provider: "tools/log",
|
||||||
|
message: "Semantic logging enabled. 🦟 Please report this bug if you see it 🦟",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ export class PrivateClassifier<Data> implements Classifier<Data, Record<string,
|
|||||||
}
|
}
|
||||||
const secret = picked as Jsonify<Data>;
|
const secret = picked as Jsonify<Data>;
|
||||||
|
|
||||||
return { disclosed: {}, secret };
|
return { disclosed: null, secret };
|
||||||
}
|
}
|
||||||
|
|
||||||
declassify(_disclosed: Jsonify<Record<keyof Data, never>>, secret: Jsonify<Data>) {
|
declassify(_disclosed: Jsonify<Record<keyof Data, never>>, secret: Jsonify<Data>) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export class PublicClassifier<Data> implements Classifier<Data, Data, Record<str
|
|||||||
}
|
}
|
||||||
const disclosed = picked as Jsonify<Data>;
|
const disclosed = picked as Jsonify<Data>;
|
||||||
|
|
||||||
return { disclosed, secret: "" };
|
return { disclosed, secret: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
declassify(disclosed: Jsonify<Data>, _secret: Jsonify<Record<keyof Data, never>>) {
|
declassify(disclosed: Jsonify<Data>, _secret: Jsonify<Record<keyof Data, never>>) {
|
||||||
|
|||||||
13
libs/common/src/tools/rx.rxjs.ts
Normal file
13
libs/common/src/tools/rx.rxjs.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to infer types from arguments to functions like {@link withLatestReady}.
|
||||||
|
* So that you can have `forkJoin([Observable<A>, PromiseLike<B>]): Observable<[A, B]>`
|
||||||
|
* et al.
|
||||||
|
* @remarks this type definition is derived from rxjs' {@link ObservableInputTuple}.
|
||||||
|
* The difference is it *only* works with observables, while the rx version works
|
||||||
|
* with any thing that can become an observable.
|
||||||
|
*/
|
||||||
|
export type ObservableTuple<T> = {
|
||||||
|
[K in keyof T]: Observable<T[K]>;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,13 @@ import {
|
|||||||
startWith,
|
startWith,
|
||||||
pairwise,
|
pairwise,
|
||||||
MonoTypeOperatorFunction,
|
MonoTypeOperatorFunction,
|
||||||
|
Cons,
|
||||||
|
scan,
|
||||||
|
filter,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { ObservableTuple } from "./rx.rxjs";
|
||||||
|
|
||||||
/** Returns its input. */
|
/** Returns its input. */
|
||||||
function identity(value: any): any {
|
function identity(value: any): any {
|
||||||
return value;
|
return value;
|
||||||
@@ -164,26 +169,30 @@ export function ready<T>(watch$: Observable<any> | Observable<any>[]) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withLatestReady<Source, Watch>(
|
export function withLatestReady<Source, Watch extends readonly unknown[]>(
|
||||||
watch$: Observable<Watch>,
|
...watches$: [...ObservableTuple<Watch>]
|
||||||
): OperatorFunction<Source, [Source, Watch]> {
|
): OperatorFunction<Source, Cons<Source, Watch>> {
|
||||||
return connect((source$) => {
|
return connect((source$) => {
|
||||||
// these subscriptions are safe because `source$` connects only after there
|
// these subscriptions are safe because `source$` connects only after there
|
||||||
// is an external subscriber.
|
// is an external subscriber.
|
||||||
const source = new ReplaySubject<Source>(1);
|
const source = new ReplaySubject<Source>(1);
|
||||||
source$.subscribe(source);
|
source$.subscribe(source);
|
||||||
const watch = new ReplaySubject<Watch>(1);
|
|
||||||
watch$.subscribe(watch);
|
const watches = watches$.map((w) => {
|
||||||
|
const watch$ = new ReplaySubject<unknown>(1);
|
||||||
|
w.subscribe(watch$);
|
||||||
|
return watch$;
|
||||||
|
}) as [...ObservableTuple<Watch>];
|
||||||
|
|
||||||
// `concat` is subscribed immediately after it's returned, at which point
|
// `concat` is subscribed immediately after it's returned, at which point
|
||||||
// `zip` blocks until all items in `watching$` are ready. If that occurs
|
// `zip` blocks until all items in `watches` are ready. If that occurs
|
||||||
// after `source$` is hot, then the replay subject sends the last-captured
|
// after `source$` is hot, then the replay subject sends the last-captured
|
||||||
// emission through immediately. Otherwise, `ready` waits for the next
|
// emission through immediately. Otherwise, `withLatestFrom` waits for the
|
||||||
// emission
|
// next emission
|
||||||
return concat(zip(watch).pipe(first(), ignoreElements()), source).pipe(
|
return concat(zip(watches).pipe(first(), ignoreElements()), source).pipe(
|
||||||
withLatestFrom(watch),
|
withLatestFrom(...watches),
|
||||||
takeUntil(anyComplete(source)),
|
takeUntil(anyComplete(source)),
|
||||||
);
|
) as Observable<Cons<Source, Watch>>;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,3 +247,54 @@ export function pin<T>(options?: {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** maps a value to a result and keeps a cache of the mapping
|
||||||
|
* @param mapResult - maps the stream to a result; this function must return
|
||||||
|
* a value. It must not return null or undefined.
|
||||||
|
* @param options.size - the number of entries in the cache
|
||||||
|
* @param options.key - maps the source to a cache key
|
||||||
|
* @remarks this method is useful for optimization of expensive
|
||||||
|
* `mapResult` calls. It's also useful when an interned reference type
|
||||||
|
* is needed.
|
||||||
|
*/
|
||||||
|
export function memoizedMap<Source, Result extends NonNullable<any>>(
|
||||||
|
mapResult: (source: Source) => Result,
|
||||||
|
options?: { size?: number; key?: (source: Source) => unknown },
|
||||||
|
): OperatorFunction<Source, Result> {
|
||||||
|
return pipe(
|
||||||
|
// scan's accumulator contains the cache
|
||||||
|
scan(
|
||||||
|
([cache], source) => {
|
||||||
|
const key: unknown = options?.key?.(source) ?? source;
|
||||||
|
|
||||||
|
// cache hit?
|
||||||
|
let result = cache?.get(key);
|
||||||
|
if (result) {
|
||||||
|
return [cache, result] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache miss
|
||||||
|
result = mapResult(source);
|
||||||
|
cache?.set(key, result);
|
||||||
|
|
||||||
|
// trim cache
|
||||||
|
const overage = cache.size - (options?.size ?? 1);
|
||||||
|
if (overage > 0) {
|
||||||
|
Array.from(cache?.keys() ?? [])
|
||||||
|
.slice(0, overage)
|
||||||
|
.forEach((k) => cache?.delete(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [cache, result] as const;
|
||||||
|
},
|
||||||
|
// FIXME: upgrade to a least-recently-used cache
|
||||||
|
[new Map(), null] as [Map<unknown, Result>, Source | null],
|
||||||
|
),
|
||||||
|
|
||||||
|
// encapsulate cache
|
||||||
|
map(([, result]) => result),
|
||||||
|
|
||||||
|
// preserve `NonNullable` constraint on `Result`
|
||||||
|
filter((result): result is Result => !!result),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,4 +15,9 @@ export abstract class UserStateSubjectDependencyProvider {
|
|||||||
// FIXME: remove `log` and inject the system provider into the USS instead
|
// FIXME: remove `log` and inject the system provider into the USS instead
|
||||||
/** Provides semantic logging */
|
/** Provides semantic logging */
|
||||||
abstract log: <Context extends object>(_context: Jsonify<Context>) => SemanticLogger;
|
abstract log: <Context extends object>(_context: Jsonify<Context>) => SemanticLogger;
|
||||||
|
|
||||||
|
/** Get the system time as a number of seconds since the unix epoch
|
||||||
|
* @remarks this can be turned into a date using `new Date(provider.now())`
|
||||||
|
*/
|
||||||
|
abstract now: () => number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ const SomeProvider = {
|
|||||||
} as LegacyEncryptorProvider,
|
} as LegacyEncryptorProvider,
|
||||||
state: SomeStateProvider,
|
state: SomeStateProvider,
|
||||||
log: disabledSemanticLoggerProvider,
|
log: disabledSemanticLoggerProvider,
|
||||||
|
now: () => 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
function fooMaxLength(maxLength: number): StateConstraints<TestType> {
|
function fooMaxLength(maxLength: number): StateConstraints<TestType> {
|
||||||
|
|||||||
@@ -477,7 +477,12 @@ export class UserStateSubject<
|
|||||||
* @returns the subscription
|
* @returns the subscription
|
||||||
*/
|
*/
|
||||||
subscribe(observer?: Partial<Observer<State>> | ((value: State) => void) | null): Subscription {
|
subscribe(observer?: Partial<Observer<State>> | ((value: State) => void) | null): Subscription {
|
||||||
return this.output.pipe(map((wc) => wc.state)).subscribe(observer);
|
return this.output
|
||||||
|
.pipe(
|
||||||
|
map((wc) => wc.state),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
)
|
||||||
|
.subscribe(observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// using subjects to ensure the right semantics are followed;
|
// using subjects to ensure the right semantics are followed;
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { Simplify } from "type-fest";
|
|||||||
|
|
||||||
import { IntegrationId } from "./integration";
|
import { IntegrationId } from "./integration";
|
||||||
|
|
||||||
|
/** When this is a string, it contains the i18n key. When it is an object, the `literal` member
|
||||||
|
* contains text that should not be translated.
|
||||||
|
*/
|
||||||
|
export type I18nKeyOrLiteral = string | { literal: string };
|
||||||
|
|
||||||
/** Constraints that are shared by all primitive field types */
|
/** Constraints that are shared by all primitive field types */
|
||||||
type PrimitiveConstraint = {
|
type PrimitiveConstraint = {
|
||||||
/** `true` indicates the field is required; otherwise the field is optional */
|
/** `true` indicates the field is required; otherwise the field is optional */
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { I18nKeyOrLiteral } from "./types";
|
||||||
|
|
||||||
/** Recursively freeze an object's own keys
|
/** Recursively freeze an object's own keys
|
||||||
* @param value the value to freeze
|
* @param value the value to freeze
|
||||||
* @returns `value`
|
* @returns `value`
|
||||||
@@ -10,10 +12,22 @@ export function deepFreeze<T extends object>(value: T): Readonly<T> {
|
|||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const own = value[key];
|
const own = value[key];
|
||||||
|
|
||||||
if ((own && typeof own === "object") || typeof own === "function") {
|
if (own && typeof own === "object") {
|
||||||
deepFreeze(own);
|
deepFreeze(own);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.freeze(value);
|
return Object.freeze(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Type guard that returns `true` when the value is an i18n key. */
|
||||||
|
export function isI18nKey(value: I18nKeyOrLiteral): value is string {
|
||||||
|
return typeof value === "string";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Type guard that returns `true` when the value requires no translation.
|
||||||
|
* @remarks the literal value can be accessed using the `.literal` property.
|
||||||
|
*/
|
||||||
|
export function isLiteral(value: I18nKeyOrLiteral): value is { literal: string } {
|
||||||
|
return typeof value === "object" && "literal" in value;
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import {
|
|||||||
ToastService,
|
ToastService,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { GeneratorServicesModule } from "@bitwarden/generator-components";
|
import { GeneratorServicesModule } from "@bitwarden/generator-components";
|
||||||
import { CredentialGeneratorService, GenerateRequest, Generators } from "@bitwarden/generator-core";
|
import { CredentialGeneratorService, GenerateRequest, Type } from "@bitwarden/generator-core";
|
||||||
import { ExportedVault, VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
|
import { ExportedVault, VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
|
||||||
|
|
||||||
import { EncryptedExportType } from "../enums/encrypted-export-type.enum";
|
import { EncryptedExportType } from "../enums/encrypted-export-type.enum";
|
||||||
@@ -248,7 +248,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
this.generatorService
|
this.generatorService
|
||||||
.generate$(Generators.password, { on$: this.onGenerate$, account$ })
|
.generate$({ on$: this.onGenerate$, account$ })
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe((generated) => {
|
.subscribe((generated) => {
|
||||||
this.exportForm.patchValue({
|
this.exportForm.patchValue({
|
||||||
@@ -379,7 +379,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generatePassword = async () => {
|
generatePassword = async () => {
|
||||||
this.onGenerate$.next({ source: "export" });
|
this.onGenerate$.next({ source: "export", type: Type.password });
|
||||||
};
|
};
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@@ -17,7 +15,7 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
|||||||
import {
|
import {
|
||||||
CatchallGenerationOptions,
|
CatchallGenerationOptions,
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
Generators,
|
BuiltIn,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
/** Options group for catchall emails */
|
/** Options group for catchall emails */
|
||||||
@@ -28,7 +26,6 @@ import {
|
|||||||
})
|
})
|
||||||
export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges {
|
export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges {
|
||||||
/** Instantiates the component
|
/** Instantiates the component
|
||||||
* @param accountService queries user availability
|
|
||||||
* @param generatorService settings and policy logic
|
* @param generatorService settings and policy logic
|
||||||
* @param formBuilder reactive form controls
|
* @param formBuilder reactive form controls
|
||||||
*/
|
*/
|
||||||
@@ -37,24 +34,26 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
private generatorService: CredentialGeneratorService,
|
private generatorService: CredentialGeneratorService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Binds the component to a specific user's settings.
|
/** Binds the component to a specific user's settings.\
|
||||||
|
* @remarks this is initialized to null but since it's a required input it'll
|
||||||
|
* never have that value in practice.
|
||||||
*/
|
*/
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
account: Account;
|
account!: Account;
|
||||||
|
|
||||||
private account$ = new ReplaySubject<Account>(1);
|
private account$ = new ReplaySubject<Account>(1);
|
||||||
|
|
||||||
/** Emits settings updates and completes if the settings become unavailable.
|
/** Emits settings updates and completes if the settings become unavailable.
|
||||||
* @remarks this does not emit the initial settings. If you would like
|
* @remarks this does not emit the initial settings. If you would like
|
||||||
* to receive live settings updates including the initial update,
|
* to receive live settings updates including the initial update,
|
||||||
* use `CredentialGeneratorService.settings$(...)` instead.
|
* use `CredentialGeneratorService.settings(...)` instead.
|
||||||
*/
|
*/
|
||||||
@Output()
|
@Output()
|
||||||
readonly onUpdated = new EventEmitter<CatchallGenerationOptions>();
|
readonly onUpdated = new EventEmitter<CatchallGenerationOptions>();
|
||||||
|
|
||||||
/** The template's control bindings */
|
/** The template's control bindings */
|
||||||
protected settings = this.formBuilder.group({
|
protected settings = this.formBuilder.group({
|
||||||
catchallDomain: [Generators.catchall.settings.initial.catchallDomain],
|
catchallDomain: [""],
|
||||||
});
|
});
|
||||||
|
|
||||||
async ngOnChanges(changes: SimpleChanges) {
|
async ngOnChanges(changes: SimpleChanges) {
|
||||||
@@ -64,7 +63,7 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const settings = await this.generatorService.settings(Generators.catchall, {
|
const settings = await this.generatorService.settings(BuiltIn.catchall, {
|
||||||
account$: this.account$,
|
account$: this.account$,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,7 +78,7 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
this.saveSettings
|
this.saveSettings
|
||||||
.pipe(
|
.pipe(
|
||||||
withLatestFrom(this.settings.valueChanges),
|
withLatestFrom(this.settings.valueChanges),
|
||||||
map(([, settings]) => settings),
|
map(([, settings]) => settings as CatchallGenerationOptions),
|
||||||
takeUntil(this.destroyed$),
|
takeUntil(this.destroyed$),
|
||||||
)
|
)
|
||||||
.subscribe(settings);
|
.subscribe(settings);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { BehaviorSubject, ReplaySubject, Subject, map, switchMap, takeUntil, tap
|
|||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import {
|
import {
|
||||||
SemanticLogger,
|
SemanticLogger,
|
||||||
@@ -19,10 +20,11 @@ import {
|
|||||||
ItemModule,
|
ItemModule,
|
||||||
NoItemsModule,
|
NoItemsModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { CredentialGeneratorService } from "@bitwarden/generator-core";
|
import { AlgorithmsByType, CredentialGeneratorService } from "@bitwarden/generator-core";
|
||||||
import { GeneratedCredential, GeneratorHistoryService } from "@bitwarden/generator-history";
|
import { GeneratedCredential, GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||||
|
|
||||||
import { GeneratorModule } from "./generator.module";
|
import { GeneratorModule } from "./generator.module";
|
||||||
|
import { translate } from "./util";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -45,6 +47,7 @@ export class CredentialGeneratorHistoryComponent implements OnChanges, OnInit, O
|
|||||||
constructor(
|
constructor(
|
||||||
private generatorService: CredentialGeneratorService,
|
private generatorService: CredentialGeneratorService,
|
||||||
private history: GeneratorHistoryService,
|
private history: GeneratorHistoryService,
|
||||||
|
private i18nService: I18nService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -94,13 +97,19 @@ export class CredentialGeneratorHistoryComponent implements OnChanges, OnInit, O
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getCopyText(credential: GeneratedCredential) {
|
protected getCopyText(credential: GeneratedCredential) {
|
||||||
const info = this.generatorService.algorithm(credential.category);
|
// there isn't a way way to look up category metadata so
|
||||||
return info.copy;
|
// bodge it by looking up algorithm metadata
|
||||||
|
const [id] = AlgorithmsByType[credential.category];
|
||||||
|
const info = this.generatorService.algorithm(id);
|
||||||
|
return translate(info.i18nKeys.copyCredential, this.i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getGeneratedValueText(credential: GeneratedCredential) {
|
protected getGeneratedValueText(credential: GeneratedCredential) {
|
||||||
const info = this.generatorService.algorithm(credential.category);
|
// there isn't a way way to look up category metadata so
|
||||||
return info.credentialType;
|
// bodge it by looking up algorithm metadata
|
||||||
|
const [id] = AlgorithmsByType[credential.category];
|
||||||
|
const info = this.generatorService.algorithm(id);
|
||||||
|
return translate(info.i18nKeys.credentialType, this.i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
|||||||
@@ -42,13 +42,13 @@
|
|||||||
</bit-card>
|
</bit-card>
|
||||||
<tools-password-settings
|
<tools-password-settings
|
||||||
class="tw-mt-6"
|
class="tw-mt-6"
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'password'"
|
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.password"
|
||||||
[account]="account$ | async"
|
[account]="account$ | async"
|
||||||
(onUpdated)="generate('password settings')"
|
(onUpdated)="generate('password settings')"
|
||||||
/>
|
/>
|
||||||
<tools-passphrase-settings
|
<tools-passphrase-settings
|
||||||
class="tw-mt-6"
|
class="tw-mt-6"
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'passphrase'"
|
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.passphrase"
|
||||||
[account]="account$ | async"
|
[account]="account$ | async"
|
||||||
(onUpdated)="generate('passphrase settings')"
|
(onUpdated)="generate('passphrase settings')"
|
||||||
/>
|
/>
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</form>
|
</form>
|
||||||
<tools-catchall-settings
|
<tools-catchall-settings
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'catchall'"
|
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.catchall"
|
||||||
[account]="account$ | async"
|
[account]="account$ | async"
|
||||||
(onUpdated)="generate('catchall settings')"
|
(onUpdated)="generate('catchall settings')"
|
||||||
/>
|
/>
|
||||||
@@ -94,12 +94,12 @@
|
|||||||
[forwarder]="forwarderId$ | async"
|
[forwarder]="forwarderId$ | async"
|
||||||
/>
|
/>
|
||||||
<tools-subaddress-settings
|
<tools-subaddress-settings
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'subaddress'"
|
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.plusAddress"
|
||||||
[account]="account$ | async"
|
[account]="account$ | async"
|
||||||
(onUpdated)="generate('subaddress settings')"
|
(onUpdated)="generate('subaddress settings')"
|
||||||
/>
|
/>
|
||||||
<tools-username-settings
|
<tools-username-settings
|
||||||
*ngIf="(showAlgorithm$ | async)?.id === 'username'"
|
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.username"
|
||||||
[account]="account$ | async"
|
[account]="account$ | async"
|
||||||
(onUpdated)="generate('username settings')"
|
(onUpdated)="generate('username settings')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
@@ -24,15 +22,15 @@ import {
|
|||||||
map,
|
map,
|
||||||
ReplaySubject,
|
ReplaySubject,
|
||||||
Subject,
|
Subject,
|
||||||
switchMap,
|
|
||||||
takeUntil,
|
takeUntil,
|
||||||
|
tap,
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||||
import {
|
import {
|
||||||
SemanticLogger,
|
SemanticLogger,
|
||||||
disabledSemanticLoggerProvider,
|
disabledSemanticLoggerProvider,
|
||||||
@@ -41,23 +39,25 @@ import {
|
|||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { ToastService, Option } from "@bitwarden/components";
|
import { ToastService, Option } from "@bitwarden/components";
|
||||||
import {
|
import {
|
||||||
AlgorithmInfo,
|
CredentialType,
|
||||||
CredentialAlgorithm,
|
|
||||||
CredentialCategory,
|
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
GenerateRequest,
|
GenerateRequest,
|
||||||
GeneratedCredential,
|
GeneratedCredential,
|
||||||
Generators,
|
isForwarderExtensionId,
|
||||||
getForwarderConfiguration,
|
|
||||||
isEmailAlgorithm,
|
|
||||||
isForwarderIntegration,
|
|
||||||
isPasswordAlgorithm,
|
|
||||||
isSameAlgorithm,
|
isSameAlgorithm,
|
||||||
|
isEmailAlgorithm,
|
||||||
isUsernameAlgorithm,
|
isUsernameAlgorithm,
|
||||||
toCredentialGeneratorConfiguration,
|
isPasswordAlgorithm,
|
||||||
|
CredentialAlgorithm,
|
||||||
|
AlgorithmMetadata,
|
||||||
|
Algorithm,
|
||||||
|
AlgorithmsByType,
|
||||||
|
Type,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||||
|
|
||||||
|
import { translate } from "./util";
|
||||||
|
|
||||||
// constants used to identify navigation selections that are not
|
// constants used to identify navigation selections that are not
|
||||||
// generator algorithms
|
// generator algorithms
|
||||||
const IDENTIFIER = "identifier";
|
const IDENTIFIER = "identifier";
|
||||||
@@ -84,11 +84,14 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
private ariaLive: LiveAnnouncer,
|
private ariaLive: LiveAnnouncer,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/** exports algorithm symbols to the template */
|
||||||
|
protected readonly Algorithm = Algorithm;
|
||||||
|
|
||||||
/** Binds the component to a specific user's settings. When this input is not provided,
|
/** Binds the component to a specific user's settings. When this input is not provided,
|
||||||
* the form binds to the active user
|
* the form binds to the active user
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
account: Account | null;
|
account: Account | null = null;
|
||||||
|
|
||||||
/** Send structured debug logs from the credential generator component
|
/** Send structured debug logs from the credential generator component
|
||||||
* to the debugger console.
|
* to the debugger console.
|
||||||
@@ -127,7 +130,7 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
@Output()
|
@Output()
|
||||||
readonly onGenerated = new EventEmitter<GeneratedCredential>();
|
readonly onGenerated = new EventEmitter<GeneratedCredential>();
|
||||||
|
|
||||||
protected root$ = new BehaviorSubject<{ nav: string }>({
|
protected root$ = new BehaviorSubject<{ nav: string | null }>({
|
||||||
nav: null,
|
nav: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,11 +144,11 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected username = this.formBuilder.group({
|
protected username = this.formBuilder.group({
|
||||||
nav: [null as string],
|
nav: [null as string | null],
|
||||||
});
|
});
|
||||||
|
|
||||||
protected forwarder = this.formBuilder.group({
|
protected forwarder = this.formBuilder.group({
|
||||||
nav: [null as string],
|
nav: [null as string | null],
|
||||||
});
|
});
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -154,33 +157,52 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!this.account) {
|
if (!this.account) {
|
||||||
this.account = await firstValueFrom(this.accountService.activeAccount$);
|
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
this.log.info(
|
if (!account) {
|
||||||
{ userId: this.account.id },
|
this.log.panic("active account cannot be `null`.");
|
||||||
"account not specified; using active account settings",
|
}
|
||||||
);
|
|
||||||
this.account$.next(this.account);
|
this.log.info({ userId: account.id }, "account not specified; using active account settings");
|
||||||
|
this.account$.next(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.generatorService
|
combineLatest([
|
||||||
.algorithms$(["email", "username"], { account$: this.account$ })
|
this.generatorService.algorithms$("email", { account$: this.account$ }),
|
||||||
|
this.generatorService.algorithms$("username", { account$: this.account$ }),
|
||||||
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
|
map((algorithms) => algorithms.flat()),
|
||||||
map((algorithms) => {
|
map((algorithms) => {
|
||||||
const usernames = algorithms.filter((a) => !isForwarderIntegration(a.id));
|
// construct options for username and email algorithms; replace forwarder
|
||||||
|
// entry with a virtual entry for drill-down
|
||||||
|
const usernames = algorithms.filter((a) => !isForwarderExtensionId(a.id));
|
||||||
|
usernames.sort((a, b) => a.weight - b.weight);
|
||||||
const usernameOptions = this.toOptions(usernames);
|
const usernameOptions = this.toOptions(usernames);
|
||||||
usernameOptions.push({ value: FORWARDER, label: this.i18nService.t("forwardedEmail") });
|
usernameOptions.splice(-1, 0, {
|
||||||
|
value: FORWARDER,
|
||||||
|
label: this.i18nService.t("forwardedEmail"),
|
||||||
|
});
|
||||||
|
|
||||||
const forwarders = algorithms.filter((a) => isForwarderIntegration(a.id));
|
// construct options for forwarder algorithms; they get their own selection box
|
||||||
|
const forwarders = algorithms.filter((a) => isForwarderExtensionId(a.id));
|
||||||
|
forwarders.sort((a, b) => a.weight - b.weight);
|
||||||
const forwarderOptions = this.toOptions(forwarders);
|
const forwarderOptions = this.toOptions(forwarders);
|
||||||
forwarderOptions.unshift({ value: NONE_SELECTED, label: this.i18nService.t("select") });
|
forwarderOptions.unshift({ value: NONE_SELECTED, label: this.i18nService.t("select") });
|
||||||
|
|
||||||
return [usernameOptions, forwarderOptions] as const;
|
return [usernameOptions, forwarderOptions] as const;
|
||||||
}),
|
}),
|
||||||
|
tap((algorithms) =>
|
||||||
|
this.log.debug({ algorithms: algorithms as object }, "algorithms loaded"),
|
||||||
|
),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(([usernames, forwarders]) => {
|
.subscribe(([usernames, forwarders]) => {
|
||||||
this.usernameOptions$.next(usernames);
|
// update subjects within the angular zone so that the
|
||||||
this.forwarderOptions$.next(forwarders);
|
// template bindings refresh immediately
|
||||||
|
this.zone.run(() => {
|
||||||
|
this.usernameOptions$.next(usernames);
|
||||||
|
this.forwarderOptions$.next(forwarders);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.generatorService
|
this.generatorService
|
||||||
@@ -195,9 +217,15 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
)
|
)
|
||||||
.subscribe(this.rootOptions$);
|
.subscribe(this.rootOptions$);
|
||||||
|
|
||||||
this.algorithm$
|
this.maybeAlgorithm$
|
||||||
.pipe(
|
.pipe(
|
||||||
map((a) => a?.description),
|
map((a) => {
|
||||||
|
if (a?.i18nKeys?.description) {
|
||||||
|
return translate(a.i18nKeys.description, this.i18nService);
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe((hint) => {
|
.subscribe((hint) => {
|
||||||
@@ -208,9 +236,9 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.algorithm$
|
this.maybeAlgorithm$
|
||||||
.pipe(
|
.pipe(
|
||||||
map((a) => a?.category),
|
map((a) => a?.type),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
@@ -223,10 +251,12 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
});
|
});
|
||||||
|
|
||||||
// wire up the generator
|
// wire up the generator
|
||||||
this.algorithm$
|
this.generatorService
|
||||||
|
.generate$({
|
||||||
|
on$: this.generate$,
|
||||||
|
account$: this.account$,
|
||||||
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
|
||||||
switchMap((algorithm) => this.typeToGenerator$(algorithm.id)),
|
|
||||||
catchError((error: unknown, generator) => {
|
catchError((error: unknown, generator) => {
|
||||||
if (typeof error === "string") {
|
if (typeof error === "string") {
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
@@ -241,11 +271,14 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
// continue with origin stream
|
// continue with origin stream
|
||||||
return generator;
|
return generator;
|
||||||
}),
|
}),
|
||||||
withLatestFrom(this.account$, this.algorithm$),
|
withLatestFrom(this.account$, this.maybeAlgorithm$),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(([generated, account, algorithm]) => {
|
.subscribe(([generated, account, algorithm]) => {
|
||||||
this.log.debug({ source: generated.source }, "credential generated");
|
this.log.debug(
|
||||||
|
{ source: generated.source ?? null, algorithm: algorithm?.id ?? null },
|
||||||
|
"credential generated",
|
||||||
|
);
|
||||||
|
|
||||||
this.generatorHistoryService
|
this.generatorHistoryService
|
||||||
.track(account.id, generated.credential, generated.category, generated.generationDate)
|
.track(account.id, generated.credential, generated.category, generated.generationDate)
|
||||||
@@ -256,8 +289,8 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
// update subjects within the angular zone so that the
|
// update subjects within the angular zone so that the
|
||||||
// template bindings refresh immediately
|
// template bindings refresh immediately
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
if (generated.source === this.USER_REQUEST) {
|
if (algorithm && generated.source === this.USER_REQUEST) {
|
||||||
this.announce(algorithm.onGeneratedMessage);
|
this.announce(translate(algorithm.i18nKeys.credentialGenerated, this.i18nService));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.generatedCredential$.next(generated);
|
this.generatedCredential$.next(generated);
|
||||||
@@ -274,36 +307,46 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
|
|
||||||
this.root$
|
this.root$
|
||||||
.pipe(
|
.pipe(
|
||||||
map(
|
map((root): CascadeValue => {
|
||||||
(root): CascadeValue =>
|
if (root.nav === IDENTIFIER) {
|
||||||
root.nav === IDENTIFIER
|
return { nav: root.nav };
|
||||||
? { nav: root.nav }
|
} else if (root.nav) {
|
||||||
: { nav: root.nav, algorithm: JSON.parse(root.nav) },
|
return { nav: root.nav, algorithm: JSON.parse(root.nav) };
|
||||||
),
|
} else {
|
||||||
|
return { nav: IDENTIFIER };
|
||||||
|
}
|
||||||
|
}),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(activeRoot$);
|
.subscribe(activeRoot$);
|
||||||
|
|
||||||
this.username.valueChanges
|
this.username.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
map(
|
map((username): CascadeValue => {
|
||||||
(username): CascadeValue =>
|
if (username.nav === FORWARDER) {
|
||||||
username.nav === FORWARDER
|
return { nav: username.nav };
|
||||||
? { nav: username.nav }
|
} else if (username.nav) {
|
||||||
: { nav: username.nav, algorithm: JSON.parse(username.nav) },
|
return { nav: username.nav, algorithm: JSON.parse(username.nav) };
|
||||||
),
|
} else {
|
||||||
|
const [algorithm] = AlgorithmsByType[Type.username];
|
||||||
|
return { nav: JSON.stringify(algorithm), algorithm };
|
||||||
|
}
|
||||||
|
}),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(activeIdentifier$);
|
.subscribe(activeIdentifier$);
|
||||||
|
|
||||||
this.forwarder.valueChanges
|
this.forwarder.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
map(
|
map((forwarder): CascadeValue => {
|
||||||
(forwarder): CascadeValue =>
|
if (forwarder.nav === NONE_SELECTED) {
|
||||||
forwarder.nav === NONE_SELECTED
|
return { nav: forwarder.nav };
|
||||||
? { nav: forwarder.nav }
|
} else if (forwarder.nav) {
|
||||||
: { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) },
|
return { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) };
|
||||||
),
|
} else {
|
||||||
|
return { nav: NONE_SELECTED };
|
||||||
|
}
|
||||||
|
}),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(activeForwarder$);
|
.subscribe(activeForwarder$);
|
||||||
@@ -314,7 +357,7 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
map(([root, username, forwarder]) => {
|
map(([root, username, forwarder]) => {
|
||||||
const showForwarder = !root.algorithm && !username.algorithm;
|
const showForwarder = !root.algorithm && !username.algorithm;
|
||||||
const forwarderId =
|
const forwarderId =
|
||||||
showForwarder && isForwarderIntegration(forwarder.algorithm)
|
showForwarder && forwarder.algorithm && isForwarderExtensionId(forwarder.algorithm)
|
||||||
? forwarder.algorithm.forwarder
|
? forwarder.algorithm.forwarder
|
||||||
: null;
|
: null;
|
||||||
return [showForwarder, forwarderId] as const;
|
return [showForwarder, forwarderId] as const;
|
||||||
@@ -344,47 +387,51 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)),
|
distinctUntilChanged((prev, next) => {
|
||||||
|
if (prev === null || next === null) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return isSameAlgorithm(prev.id, next.id);
|
||||||
|
}
|
||||||
|
}),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe((algorithm) => {
|
.subscribe((algorithm) => {
|
||||||
this.log.debug(algorithm, "algorithm selected");
|
this.log.debug({ algorithm: algorithm?.id ?? null }, "algorithm selected");
|
||||||
|
|
||||||
// update subjects within the angular zone so that the
|
// update subjects within the angular zone so that the
|
||||||
// template bindings refresh immediately
|
// template bindings refresh immediately
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
this.algorithm$.next(algorithm);
|
this.maybeAlgorithm$.next(algorithm);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// assume the last-selected generator algorithm is the user's preferred one
|
// assume the last-selected generator algorithm is the user's preferred one
|
||||||
const preferences = await this.generatorService.preferences({ account$: this.account$ });
|
const preferences = await this.generatorService.preferences({ account$: this.account$ });
|
||||||
this.algorithm$
|
this.algorithm$
|
||||||
.pipe(
|
.pipe(withLatestFrom(preferences), takeUntil(this.destroyed))
|
||||||
filter((algorithm) => !!algorithm),
|
|
||||||
withLatestFrom(preferences),
|
|
||||||
takeUntil(this.destroyed),
|
|
||||||
)
|
|
||||||
.subscribe(([algorithm, preference]) => {
|
.subscribe(([algorithm, preference]) => {
|
||||||
function setPreference(category: CredentialCategory, log: SemanticLogger) {
|
function setPreference(type: CredentialType) {
|
||||||
const p = preference[category];
|
const p = preference[type];
|
||||||
p.algorithm = algorithm.id;
|
p.algorithm = algorithm.id;
|
||||||
p.updated = new Date();
|
p.updated = new Date();
|
||||||
|
|
||||||
log.info({ algorithm, category }, "algorithm preferences updated");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// `is*Algorithm` decides `algorithm`'s type, which flows into `setPreference`
|
// `is*Algorithm` decides `algorithm`'s type, which flows into `setPreference`
|
||||||
if (isEmailAlgorithm(algorithm.id)) {
|
if (isEmailAlgorithm(algorithm.id)) {
|
||||||
setPreference("email", this.log);
|
setPreference("email");
|
||||||
} else if (isUsernameAlgorithm(algorithm.id)) {
|
} else if (isUsernameAlgorithm(algorithm.id)) {
|
||||||
setPreference("username", this.log);
|
setPreference("username");
|
||||||
} else if (isPasswordAlgorithm(algorithm.id)) {
|
} else if (isPasswordAlgorithm(algorithm.id)) {
|
||||||
setPreference("password", this.log);
|
setPreference("password");
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.log.info(
|
||||||
|
{ algorithm: algorithm.id, type: algorithm.type },
|
||||||
|
"algorithm preferences updated",
|
||||||
|
);
|
||||||
preferences.next(preference);
|
preferences.next(preference);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -392,10 +439,12 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
preferences
|
preferences
|
||||||
.pipe(
|
.pipe(
|
||||||
map(({ email, username, password }) => {
|
map(({ email, username, password }) => {
|
||||||
const forwarderPref = isForwarderIntegration(email.algorithm) ? email : null;
|
|
||||||
const usernamePref = email.updated > username.updated ? email : username;
|
const usernamePref = email.updated > username.updated ? email : username;
|
||||||
|
const forwarderPref = isForwarderExtensionId(usernamePref.algorithm)
|
||||||
|
? usernamePref
|
||||||
|
: null;
|
||||||
|
|
||||||
// inject drilldown flags
|
// inject drill-down flags
|
||||||
const forwarderNav = !forwarderPref
|
const forwarderNav = !forwarderPref
|
||||||
? NONE_SELECTED
|
? NONE_SELECTED
|
||||||
: JSON.stringify(forwarderPref.algorithm);
|
: JSON.stringify(forwarderPref.algorithm);
|
||||||
@@ -411,14 +460,14 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
selection: { nav: rootNav },
|
selection: { nav: rootNav },
|
||||||
active: {
|
active: {
|
||||||
nav: rootNav,
|
nav: rootNav,
|
||||||
algorithm: rootNav === IDENTIFIER ? null : password.algorithm,
|
algorithm: rootNav === IDENTIFIER ? undefined : password.algorithm,
|
||||||
} as CascadeValue,
|
} as CascadeValue,
|
||||||
},
|
},
|
||||||
username: {
|
username: {
|
||||||
selection: { nav: userNav },
|
selection: { nav: userNav },
|
||||||
active: {
|
active: {
|
||||||
nav: userNav,
|
nav: userNav,
|
||||||
algorithm: forwarderPref ? null : usernamePref.algorithm,
|
algorithm: forwarderPref ? undefined : usernamePref.algorithm,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
forwarder: {
|
forwarder: {
|
||||||
@@ -435,6 +484,15 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(({ root, username, forwarder }) => {
|
.subscribe(({ root, username, forwarder }) => {
|
||||||
|
this.log.debug(
|
||||||
|
{
|
||||||
|
root: root.selection,
|
||||||
|
username: username.selection,
|
||||||
|
forwarder: forwarder.selection,
|
||||||
|
},
|
||||||
|
"navigation updated",
|
||||||
|
);
|
||||||
|
|
||||||
// update navigation; break subscription loop
|
// update navigation; break subscription loop
|
||||||
this.onRootChanged(root.selection);
|
this.onRootChanged(root.selection);
|
||||||
this.username.setValue(username.selection, { emitEvent: false });
|
this.username.setValue(username.selection, { emitEvent: false });
|
||||||
@@ -448,16 +506,16 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
|
|
||||||
// automatically regenerate when the algorithm switches if the algorithm
|
// automatically regenerate when the algorithm switches if the algorithm
|
||||||
// allows it; otherwise set a placeholder
|
// allows it; otherwise set a placeholder
|
||||||
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
this.maybeAlgorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
if (!a || a.onlyOnRequest) {
|
if (a?.capabilities?.autogenerate) {
|
||||||
this.log.debug("autogeneration disabled; clearing generated credential");
|
|
||||||
this.generatedCredential$.next(null);
|
|
||||||
} else {
|
|
||||||
this.log.debug("autogeneration enabled");
|
this.log.debug("autogeneration enabled");
|
||||||
this.generate("autogenerate").catch((e: unknown) => {
|
this.generate("autogenerate").catch((e: unknown) => {
|
||||||
this.log.error(e as object, "a failure occurred during autogeneration");
|
this.log.error(e as object, "a failure occurred during autogeneration");
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.log.debug("autogeneration disabled; clearing generated credential");
|
||||||
|
this.generatedCredential$.next(undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -469,41 +527,6 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
|
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
private typeToGenerator$(algorithm: CredentialAlgorithm) {
|
|
||||||
const dependencies = {
|
|
||||||
on$: this.generate$,
|
|
||||||
account$: this.account$,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.log.debug({ algorithm }, "constructing generation stream");
|
|
||||||
|
|
||||||
switch (algorithm) {
|
|
||||||
case "catchall":
|
|
||||||
return this.generatorService.generate$(Generators.catchall, dependencies);
|
|
||||||
|
|
||||||
case "subaddress":
|
|
||||||
return this.generatorService.generate$(Generators.subaddress, dependencies);
|
|
||||||
|
|
||||||
case "username":
|
|
||||||
return this.generatorService.generate$(Generators.username, dependencies);
|
|
||||||
|
|
||||||
case "password":
|
|
||||||
return this.generatorService.generate$(Generators.password, dependencies);
|
|
||||||
|
|
||||||
case "passphrase":
|
|
||||||
return this.generatorService.generate$(Generators.passphrase, dependencies);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isForwarderIntegration(algorithm)) {
|
|
||||||
const forwarder = getForwarderConfiguration(algorithm.forwarder);
|
|
||||||
const configuration = toCredentialGeneratorConfiguration(forwarder);
|
|
||||||
const generator = this.generatorService.generate$(configuration, dependencies);
|
|
||||||
return generator;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log.panic({ algorithm }, `Invalid generator type: "${algorithm}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Lists the top-level credential types supported by the component.
|
/** Lists the top-level credential types supported by the component.
|
||||||
* @remarks This is string-typed because angular doesn't support
|
* @remarks This is string-typed because angular doesn't support
|
||||||
* structural equality for objects, which prevents `CredentialAlgorithm`
|
* structural equality for objects, which prevents `CredentialAlgorithm`
|
||||||
@@ -519,15 +542,20 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
protected forwarderOptions$ = new BehaviorSubject<Option<string>[]>([]);
|
protected forwarderOptions$ = new BehaviorSubject<Option<string>[]>([]);
|
||||||
|
|
||||||
/** Tracks the currently selected forwarder. */
|
/** Tracks the currently selected forwarder. */
|
||||||
protected forwarderId$ = new BehaviorSubject<IntegrationId>(null);
|
protected forwarderId$ = new BehaviorSubject<VendorId | null>(null);
|
||||||
|
|
||||||
/** Tracks forwarder control visibility */
|
/** Tracks forwarder control visibility */
|
||||||
protected showForwarder$ = new BehaviorSubject<boolean>(false);
|
protected showForwarder$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
/** tracks the currently selected credential type */
|
/** tracks the currently selected credential type */
|
||||||
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);
|
protected maybeAlgorithm$ = new ReplaySubject<AlgorithmMetadata | null>(1);
|
||||||
|
|
||||||
protected showAlgorithm$ = this.algorithm$.pipe(
|
/** tracks the last valid algorithm selection */
|
||||||
|
protected algorithm$ = this.maybeAlgorithm$.pipe(
|
||||||
|
filter((algorithm): algorithm is AlgorithmMetadata => !!algorithm),
|
||||||
|
);
|
||||||
|
|
||||||
|
protected showAlgorithm$ = this.maybeAlgorithm$.pipe(
|
||||||
combineLatestWith(this.showForwarder$),
|
combineLatestWith(this.showForwarder$),
|
||||||
map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)),
|
map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)),
|
||||||
);
|
);
|
||||||
@@ -536,33 +564,32 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
* Emits the copy button aria-label respective of the selected credential type
|
* Emits the copy button aria-label respective of the selected credential type
|
||||||
*/
|
*/
|
||||||
protected credentialTypeCopyLabel$ = this.algorithm$.pipe(
|
protected credentialTypeCopyLabel$ = this.algorithm$.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
map(({ i18nKeys: { copyCredential } }) => translate(copyCredential, this.i18nService)),
|
||||||
map(({ copy }) => copy),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the generate button aria-label respective of the selected credential type
|
* Emits the generate button aria-label respective of the selected credential type
|
||||||
*/
|
*/
|
||||||
protected credentialTypeGenerateLabel$ = this.algorithm$.pipe(
|
protected credentialTypeGenerateLabel$ = this.algorithm$.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
map(({ i18nKeys: { generateCredential } }) => translate(generateCredential, this.i18nService)),
|
||||||
map(({ generate }) => generate),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the copy credential toast respective of the selected credential type
|
* Emits the copy credential toast respective of the selected credential type
|
||||||
*/
|
*/
|
||||||
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
map(({ i18nKeys: { credentialType } }) => translate(credentialType, this.i18nService)),
|
||||||
map(({ credentialType }) => credentialType),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Emits hint key for the currently selected credential type */
|
/** Emits hint key for the currently selected credential type */
|
||||||
protected credentialTypeHint$ = new ReplaySubject<string>(1);
|
protected credentialTypeHint$ = new ReplaySubject<string | undefined>(1);
|
||||||
|
|
||||||
/** tracks the currently selected credential category */
|
/** tracks the currently selected credential category */
|
||||||
protected category$ = new ReplaySubject<string>(1);
|
protected category$ = new ReplaySubject<string | undefined>(1);
|
||||||
|
|
||||||
private readonly generatedCredential$ = new BehaviorSubject<GeneratedCredential>(null);
|
private readonly generatedCredential$ = new BehaviorSubject<GeneratedCredential | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
/** Emits the last generated value. */
|
/** Emits the last generated value. */
|
||||||
protected readonly value$ = this.generatedCredential$.pipe(
|
protected readonly value$ = this.generatedCredential$.pipe(
|
||||||
@@ -580,15 +607,20 @@ export class CredentialGeneratorComponent implements OnInit, OnChanges, OnDestro
|
|||||||
* origin in the debugger.
|
* origin in the debugger.
|
||||||
*/
|
*/
|
||||||
protected async generate(source: string) {
|
protected async generate(source: string) {
|
||||||
const request = { source, website: this.website };
|
const algorithm = await firstValueFrom(this.algorithm$);
|
||||||
|
const request: GenerateRequest = { source, algorithm: algorithm.id };
|
||||||
|
if (this.website) {
|
||||||
|
request.website = this.website;
|
||||||
|
}
|
||||||
|
|
||||||
this.log.debug(request, "generation requested");
|
this.log.debug(request, "generation requested");
|
||||||
this.generate$.next(request);
|
this.generate$.next(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toOptions(algorithms: AlgorithmInfo[]) {
|
private toOptions(algorithms: AlgorithmMetadata[]) {
|
||||||
const options: Option<string>[] = algorithms.map((algorithm) => ({
|
const options: Option<string>[] = algorithms.map((algorithm) => ({
|
||||||
value: JSON.stringify(algorithm.id),
|
value: JSON.stringify(algorithm.id),
|
||||||
label: algorithm.name,
|
label: translate(algorithm.i18nKeys.name, this.i18nService),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@@ -14,13 +12,11 @@ import { FormBuilder } from "@angular/forms";
|
|||||||
import { map, ReplaySubject, skip, Subject, switchAll, takeUntil, withLatestFrom } from "rxjs";
|
import { map, ReplaySubject, skip, Subject, switchAll, takeUntil, withLatestFrom } from "rxjs";
|
||||||
|
|
||||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||||
import {
|
import {
|
||||||
CredentialGeneratorConfiguration,
|
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
getForwarderConfiguration,
|
ForwarderOptions,
|
||||||
NoPolicy,
|
GeneratorMetadata,
|
||||||
toCredentialGeneratorConfiguration,
|
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
const Controls = Object.freeze({
|
const Controls = Object.freeze({
|
||||||
@@ -37,7 +33,6 @@ const Controls = Object.freeze({
|
|||||||
})
|
})
|
||||||
export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
/** Instantiates the component
|
/** Instantiates the component
|
||||||
* @param accountService queries user availability
|
|
||||||
* @param generatorService settings and policy logic
|
* @param generatorService settings and policy logic
|
||||||
* @param formBuilder reactive form controls
|
* @param formBuilder reactive form controls
|
||||||
*/
|
*/
|
||||||
@@ -47,14 +42,16 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Binds the component to a specific user's settings.
|
/** Binds the component to a specific user's settings.
|
||||||
|
* @remarks this is initialized to null but since it's a required input it'll
|
||||||
|
* never have that value in practice.
|
||||||
*/
|
*/
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
account: Account;
|
account: Account = null!;
|
||||||
|
|
||||||
protected account$ = new ReplaySubject<Account>(1);
|
protected account$ = new ReplaySubject<Account>(1);
|
||||||
|
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
forwarder: IntegrationId;
|
forwarder: VendorId = null!;
|
||||||
|
|
||||||
/** Emits settings updates and completes if the settings become unavailable.
|
/** Emits settings updates and completes if the settings become unavailable.
|
||||||
* @remarks this does not emit the initial settings. If you would like
|
* @remarks this does not emit the initial settings. If you would like
|
||||||
@@ -71,24 +68,19 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
[Controls.baseUrl]: [""],
|
[Controls.baseUrl]: [""],
|
||||||
});
|
});
|
||||||
|
|
||||||
private forwarderId$ = new ReplaySubject<IntegrationId>(1);
|
private vendor = new ReplaySubject<VendorId>(1);
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const forwarder$ = new ReplaySubject<CredentialGeneratorConfiguration<any, NoPolicy>>(1);
|
const forwarder$ = new ReplaySubject<GeneratorMetadata<ForwarderOptions>>(1);
|
||||||
this.forwarderId$
|
this.vendor
|
||||||
.pipe(
|
.pipe(
|
||||||
map((id) => getForwarderConfiguration(id)),
|
map((vendor) => this.generatorService.forwarder(vendor)),
|
||||||
// type erasure necessary because the configuration properties are
|
|
||||||
// determined dynamically at runtime
|
|
||||||
// FIXME: this can be eliminated by unifying the forwarder settings types;
|
|
||||||
// see `ForwarderConfiguration<...>` for details.
|
|
||||||
map((forwarder) => toCredentialGeneratorConfiguration<any>(forwarder)),
|
|
||||||
takeUntil(this.destroyed$),
|
takeUntil(this.destroyed$),
|
||||||
)
|
)
|
||||||
.subscribe((forwarder) => {
|
.subscribe((forwarder) => {
|
||||||
this.displayDomain = forwarder.request.includes("domain");
|
this.displayDomain = forwarder.capabilities.fields.includes("domain");
|
||||||
this.displayToken = forwarder.request.includes("token");
|
this.displayToken = forwarder.capabilities.fields.includes("token");
|
||||||
this.displayBaseUrl = forwarder.request.includes("baseUrl");
|
this.displayBaseUrl = forwarder.capabilities.fields.includes("baseUrl");
|
||||||
|
|
||||||
forwarder$.next(forwarder);
|
forwarder$.next(forwarder);
|
||||||
});
|
});
|
||||||
@@ -107,10 +99,10 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
forwarder$.pipe(takeUntil(this.destroyed$)).subscribe((forwarder) => {
|
forwarder$.pipe(takeUntil(this.destroyed$)).subscribe((forwarder) => {
|
||||||
for (const name in Controls) {
|
for (const name in Controls) {
|
||||||
const control = this.settings.get(name);
|
const control = this.settings.get(name);
|
||||||
if (forwarder.request.includes(name as any)) {
|
if (forwarder.capabilities.fields.includes(name)) {
|
||||||
control.enable({ emitEvent: false });
|
control?.enable({ emitEvent: false });
|
||||||
} else {
|
} else {
|
||||||
control.disable({ emitEvent: false });
|
control?.disable({ emitEvent: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -128,7 +120,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
this.saveSettings
|
this.saveSettings
|
||||||
.pipe(withLatestFrom(this.settings.valueChanges, settings$), takeUntil(this.destroyed$))
|
.pipe(withLatestFrom(this.settings.valueChanges, settings$), takeUntil(this.destroyed$))
|
||||||
.subscribe(([, value, settings]) => {
|
.subscribe(([, value, settings]) => {
|
||||||
settings.next(value);
|
settings.next(value as ForwarderOptions);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +132,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
async ngOnChanges(changes: SimpleChanges) {
|
async ngOnChanges(changes: SimpleChanges) {
|
||||||
this.refresh$.complete();
|
this.refresh$.complete();
|
||||||
if ("forwarder" in changes) {
|
if ("forwarder" in changes) {
|
||||||
this.forwarderId$.next(this.forwarder);
|
this.vendor.next(this.forwarder);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("account" in changes) {
|
if ("account" in changes) {
|
||||||
@@ -148,9 +140,9 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected displayDomain: boolean;
|
protected displayDomain: boolean = false;
|
||||||
protected displayToken: boolean;
|
protected displayToken: boolean = false;
|
||||||
protected displayBaseUrl: boolean;
|
protected displayBaseUrl: boolean = false;
|
||||||
|
|
||||||
private readonly refresh$ = new Subject<void>();
|
private readonly refresh$ = new Subject<void>();
|
||||||
|
|
||||||
|
|||||||
@@ -7,19 +7,40 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider";
|
import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider";
|
||||||
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
|
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
|
||||||
import { disabledSemanticLoggerProvider } from "@bitwarden/common/tools/log";
|
import { Site } from "@bitwarden/common/tools/extension";
|
||||||
|
import { ExtensionRegistry } from "@bitwarden/common/tools/extension/extension-registry.abstraction";
|
||||||
|
import { ExtensionService } from "@bitwarden/common/tools/extension/extension.service";
|
||||||
|
import { DefaultFields, DefaultSites, Extension } from "@bitwarden/common/tools/extension/metadata";
|
||||||
|
import { RuntimeExtensionRegistry } from "@bitwarden/common/tools/extension/runtime-extension-registry";
|
||||||
|
import { VendorExtensions, Vendors } from "@bitwarden/common/tools/extension/vendor";
|
||||||
|
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
|
||||||
|
import {
|
||||||
|
LogProvider,
|
||||||
|
disabledSemanticLoggerProvider,
|
||||||
|
enableLogForTypes,
|
||||||
|
} from "@bitwarden/common/tools/log";
|
||||||
|
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||||
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
||||||
import {
|
import {
|
||||||
|
BuiltIn,
|
||||||
createRandomizer,
|
createRandomizer,
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
Randomizer,
|
Randomizer,
|
||||||
|
providers,
|
||||||
|
DefaultCredentialGeneratorService,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
export const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
export const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
||||||
|
const GENERATOR_SERVICE_PROVIDER = new SafeInjectionToken<providers.CredentialGeneratorProviders>(
|
||||||
|
"CredentialGeneratorProviders",
|
||||||
|
);
|
||||||
|
const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>("SystemServices");
|
||||||
|
|
||||||
/** Shared module containing generator component dependencies */
|
/** Shared module containing generator component dependencies */
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -35,6 +56,116 @@ export const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
|||||||
useClass: KeyServiceLegacyEncryptorProvider,
|
useClass: KeyServiceLegacyEncryptorProvider,
|
||||||
deps: [EncryptService, KeyService],
|
deps: [EncryptService, KeyService],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: ExtensionRegistry,
|
||||||
|
useFactory: () => {
|
||||||
|
const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields);
|
||||||
|
|
||||||
|
registry.registerSite(Extension[Site.forwarder]);
|
||||||
|
for (const vendor of Vendors) {
|
||||||
|
registry.registerVendor(vendor);
|
||||||
|
}
|
||||||
|
for (const extension of VendorExtensions) {
|
||||||
|
registry.registerExtension(extension);
|
||||||
|
}
|
||||||
|
registry.setPermission({ all: true }, "default");
|
||||||
|
|
||||||
|
return registry;
|
||||||
|
},
|
||||||
|
deps: [],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SYSTEM_SERVICE_PROVIDER,
|
||||||
|
useFactory: (
|
||||||
|
encryptor: LegacyEncryptorProvider,
|
||||||
|
state: StateProvider,
|
||||||
|
policy: PolicyService,
|
||||||
|
registry: ExtensionRegistry,
|
||||||
|
logger: LogService,
|
||||||
|
environment: PlatformUtilsService,
|
||||||
|
) => {
|
||||||
|
let log: LogProvider;
|
||||||
|
if (environment.isDev()) {
|
||||||
|
log = enableLogForTypes(logger, []);
|
||||||
|
} else {
|
||||||
|
log = disabledSemanticLoggerProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = new ExtensionService(registry, {
|
||||||
|
encryptor,
|
||||||
|
state,
|
||||||
|
log,
|
||||||
|
now: Date.now,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
policy,
|
||||||
|
extension,
|
||||||
|
log,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
deps: [
|
||||||
|
LegacyEncryptorProvider,
|
||||||
|
StateProvider,
|
||||||
|
PolicyService,
|
||||||
|
ExtensionRegistry,
|
||||||
|
LogService,
|
||||||
|
PlatformUtilsService,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: GENERATOR_SERVICE_PROVIDER,
|
||||||
|
useFactory: (
|
||||||
|
system: SystemServiceProvider,
|
||||||
|
random: Randomizer,
|
||||||
|
encryptor: LegacyEncryptorProvider,
|
||||||
|
state: StateProvider,
|
||||||
|
i18n: I18nService,
|
||||||
|
api: ApiService,
|
||||||
|
) => {
|
||||||
|
const userStateDeps = {
|
||||||
|
encryptor,
|
||||||
|
state,
|
||||||
|
log: system.log,
|
||||||
|
now: Date.now,
|
||||||
|
} satisfies UserStateSubjectDependencyProvider;
|
||||||
|
|
||||||
|
const metadata = new providers.GeneratorMetadataProvider(
|
||||||
|
userStateDeps,
|
||||||
|
system,
|
||||||
|
Object.values(BuiltIn),
|
||||||
|
);
|
||||||
|
const profile = new providers.GeneratorProfileProvider(userStateDeps, system.policy);
|
||||||
|
|
||||||
|
const generator: providers.GeneratorDependencyProvider = {
|
||||||
|
randomizer: random,
|
||||||
|
client: new RestClient(api, i18n),
|
||||||
|
i18nService: i18n,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userState: UserStateSubjectDependencyProvider = {
|
||||||
|
encryptor,
|
||||||
|
state,
|
||||||
|
log: system.log,
|
||||||
|
now: Date.now,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
userState,
|
||||||
|
generator,
|
||||||
|
profile,
|
||||||
|
metadata,
|
||||||
|
} satisfies providers.CredentialGeneratorProviders;
|
||||||
|
},
|
||||||
|
deps: [
|
||||||
|
SYSTEM_SERVICE_PROVIDER,
|
||||||
|
RANDOMIZER,
|
||||||
|
LegacyEncryptorProvider,
|
||||||
|
StateProvider,
|
||||||
|
I18nService,
|
||||||
|
ApiService,
|
||||||
|
],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: UserStateSubjectDependencyProvider,
|
provide: UserStateSubjectDependencyProvider,
|
||||||
useFactory: (encryptor: LegacyEncryptorProvider, state: StateProvider) =>
|
useFactory: (encryptor: LegacyEncryptorProvider, state: StateProvider) =>
|
||||||
@@ -42,19 +173,14 @@ export const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
|||||||
encryptor,
|
encryptor,
|
||||||
state,
|
state,
|
||||||
log: disabledSemanticLoggerProvider,
|
log: disabledSemanticLoggerProvider,
|
||||||
|
now: Date.now,
|
||||||
}),
|
}),
|
||||||
deps: [LegacyEncryptorProvider, StateProvider],
|
deps: [LegacyEncryptorProvider, StateProvider],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: CredentialGeneratorService,
|
provide: CredentialGeneratorService,
|
||||||
useClass: CredentialGeneratorService,
|
useClass: DefaultCredentialGeneratorService,
|
||||||
deps: [
|
deps: [GENERATOR_SERVICE_PROVIDER, SYSTEM_SERVICE_PROVIDER],
|
||||||
RANDOMIZER,
|
|
||||||
PolicyService,
|
|
||||||
ApiService,
|
|
||||||
I18nService,
|
|
||||||
UserStateSubjectDependencyProvider,
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
import {
|
import {
|
||||||
OnInit,
|
OnInit,
|
||||||
@@ -12,14 +10,20 @@ import {
|
|||||||
OnChanges,
|
OnChanges,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { skip, takeUntil, Subject, map, withLatestFrom, ReplaySubject } from "rxjs";
|
import { skip, takeUntil, Subject, map, withLatestFrom, ReplaySubject, tap } from "rxjs";
|
||||||
|
|
||||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import {
|
||||||
|
SemanticLogger,
|
||||||
|
disabledSemanticLoggerProvider,
|
||||||
|
ifEnabledSemanticLoggerProvider,
|
||||||
|
} from "@bitwarden/common/tools/log";
|
||||||
import {
|
import {
|
||||||
Generators,
|
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
PassphraseGenerationOptions,
|
PassphraseGenerationOptions,
|
||||||
|
BuiltIn,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
const Controls = Object.freeze({
|
const Controls = Object.freeze({
|
||||||
@@ -45,12 +49,26 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private generatorService: CredentialGeneratorService,
|
private generatorService: CredentialGeneratorService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
|
private logService: LogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/** Send structured debug logs from the credential generator component
|
||||||
|
* to the debugger console.
|
||||||
|
*
|
||||||
|
* @warning this may reveal sensitive information in plaintext.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
debug: boolean = false;
|
||||||
|
|
||||||
|
// this `log` initializer is overridden in `ngOnInit`
|
||||||
|
private log: SemanticLogger = disabledSemanticLoggerProvider({});
|
||||||
|
|
||||||
/** Binds the component to a specific user's settings.
|
/** Binds the component to a specific user's settings.
|
||||||
|
* @remarks this is initialized to null but since it's a required input it'll
|
||||||
|
* never have that value in practice.
|
||||||
*/
|
*/
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
account: Account;
|
account: Account = null!;
|
||||||
|
|
||||||
protected account$ = new ReplaySubject<Account>(1);
|
protected account$ = new ReplaySubject<Account>(1);
|
||||||
|
|
||||||
@@ -70,53 +88,66 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
/** Emits settings updates and completes if the settings become unavailable.
|
/** Emits settings updates and completes if the settings become unavailable.
|
||||||
* @remarks this does not emit the initial settings. If you would like
|
* @remarks this does not emit the initial settings. If you would like
|
||||||
* to receive live settings updates including the initial update,
|
* to receive live settings updates including the initial update,
|
||||||
* use `CredentialGeneratorService.settings$(...)` instead.
|
* use {@link CredentialGeneratorService.settings} instead.
|
||||||
*/
|
*/
|
||||||
@Output()
|
@Output()
|
||||||
readonly onUpdated = new EventEmitter<PassphraseGenerationOptions>();
|
readonly onUpdated = new EventEmitter<PassphraseGenerationOptions>();
|
||||||
|
|
||||||
protected settings = this.formBuilder.group({
|
protected settings = this.formBuilder.group({
|
||||||
[Controls.numWords]: [Generators.passphrase.settings.initial.numWords],
|
[Controls.numWords]: [0],
|
||||||
[Controls.wordSeparator]: [Generators.passphrase.settings.initial.wordSeparator],
|
[Controls.wordSeparator]: [""],
|
||||||
[Controls.capitalize]: [Generators.passphrase.settings.initial.capitalize],
|
[Controls.capitalize]: [false],
|
||||||
[Controls.includeNumber]: [Generators.passphrase.settings.initial.includeNumber],
|
[Controls.includeNumber]: [false],
|
||||||
});
|
});
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const settings = await this.generatorService.settings(Generators.passphrase, {
|
this.log = ifEnabledSemanticLoggerProvider(this.debug, this.logService, {
|
||||||
|
type: "PassphraseSettingsComponent",
|
||||||
|
});
|
||||||
|
|
||||||
|
const settings = await this.generatorService.settings(BuiltIn.passphrase, {
|
||||||
account$: this.account$,
|
account$: this.account$,
|
||||||
});
|
});
|
||||||
|
|
||||||
// skips reactive event emissions to break a subscription cycle
|
// skips reactive event emissions to break a subscription cycle
|
||||||
settings.withConstraints$
|
settings.withConstraints$
|
||||||
.pipe(takeUntil(this.destroyed$))
|
.pipe(
|
||||||
|
tap((content) => this.log.debug(content, "passphrase settings loaded with constraints")),
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
)
|
||||||
.subscribe(({ state, constraints }) => {
|
.subscribe(({ state, constraints }) => {
|
||||||
this.settings.patchValue(state, { emitEvent: false });
|
this.settings.patchValue(state, { emitEvent: false });
|
||||||
|
|
||||||
let boundariesHint = this.i18nService.t(
|
let boundariesHint = this.i18nService.t(
|
||||||
"spinboxBoundariesHint",
|
"spinboxBoundariesHint",
|
||||||
constraints.numWords.min?.toString(),
|
constraints.numWords?.min?.toString(),
|
||||||
constraints.numWords.max?.toString(),
|
constraints.numWords?.max?.toString(),
|
||||||
);
|
);
|
||||||
if (state.numWords <= (constraints.numWords.recommendation ?? 0)) {
|
if ((state.numWords ?? 0) <= (constraints.numWords?.recommendation ?? 0)) {
|
||||||
boundariesHint += this.i18nService.t(
|
boundariesHint += this.i18nService.t(
|
||||||
"passphraseNumWordsRecommendationHint",
|
"passphraseNumWordsRecommendationHint",
|
||||||
constraints.numWords.recommendation?.toString(),
|
constraints.numWords?.recommendation?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.numWordsBoundariesHint.next(boundariesHint);
|
this.numWordsBoundariesHint.next(boundariesHint);
|
||||||
});
|
});
|
||||||
|
|
||||||
// the first emission is the current value; subsequent emissions are updates
|
// the first emission is the current value; subsequent emissions are updates
|
||||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
settings
|
||||||
|
.pipe(
|
||||||
|
skip(1),
|
||||||
|
tap((settings) => this.log.debug(settings, "passphrase settings onUpdate event")),
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
)
|
||||||
|
.subscribe(this.onUpdated);
|
||||||
|
|
||||||
// explain policy & disable policy-overridden fields
|
// explain policy & disable policy-overridden fields
|
||||||
this.generatorService
|
this.generatorService
|
||||||
.policy$(Generators.passphrase, { account$: this.account$ })
|
.policy$(BuiltIn.passphrase, { account$: this.account$ })
|
||||||
.pipe(takeUntil(this.destroyed$))
|
.pipe(takeUntil(this.destroyed$))
|
||||||
.subscribe(({ constraints }) => {
|
.subscribe(({ constraints }) => {
|
||||||
this.wordSeparatorMaxLength = constraints.wordSeparator.maxLength;
|
this.wordSeparatorMaxLength = constraints.wordSeparator?.maxLength ?? 0;
|
||||||
this.policyInEffect = constraints.policyInEffect;
|
this.policyInEffect = constraints.policyInEffect ?? false;
|
||||||
|
|
||||||
this.toggleEnabled(Controls.capitalize, !constraints.capitalize?.readonly);
|
this.toggleEnabled(Controls.capitalize, !constraints.capitalize?.readonly);
|
||||||
this.toggleEnabled(Controls.includeNumber, !constraints.includeNumber?.readonly);
|
this.toggleEnabled(Controls.includeNumber, !constraints.includeNumber?.readonly);
|
||||||
@@ -126,22 +157,25 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
this.saveSettings
|
this.saveSettings
|
||||||
.pipe(
|
.pipe(
|
||||||
withLatestFrom(this.settings.valueChanges),
|
withLatestFrom(this.settings.valueChanges),
|
||||||
map(([, settings]) => settings),
|
tap(([source, form]) =>
|
||||||
|
this.log.debug({ source, form }, "save passphrase settings request"),
|
||||||
|
),
|
||||||
|
map(([, settings]) => settings as PassphraseGenerationOptions),
|
||||||
takeUntil(this.destroyed$),
|
takeUntil(this.destroyed$),
|
||||||
)
|
)
|
||||||
.subscribe(settings);
|
.subscribe(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** attribute binding for wordSeparator[maxlength] */
|
/** attribute binding for wordSeparator[maxlength] */
|
||||||
protected wordSeparatorMaxLength: number;
|
protected wordSeparatorMaxLength: number = 0;
|
||||||
|
|
||||||
private saveSettings = new Subject<string>();
|
private saveSettings = new Subject<string>();
|
||||||
save(site: string = "component api call") {
|
save(source: string = "component api call") {
|
||||||
this.saveSettings.next(site);
|
this.saveSettings.next(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** display binding for enterprise policy notice */
|
/** display binding for enterprise policy notice */
|
||||||
protected policyInEffect: boolean;
|
protected policyInEffect: boolean = false;
|
||||||
|
|
||||||
private numWordsBoundariesHint = new ReplaySubject<string>(1);
|
private numWordsBoundariesHint = new ReplaySubject<string>(1);
|
||||||
|
|
||||||
@@ -150,9 +184,9 @@ export class PassphraseSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
|
|
||||||
private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) {
|
private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
this.settings.get(setting).enable({ emitEvent: false });
|
this.settings.get(setting)?.enable({ emitEvent: false });
|
||||||
} else {
|
} else {
|
||||||
this.settings.get(setting).disable({ emitEvent: false });
|
this.settings.get(setting)?.disable({ emitEvent: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
class="tw-mb-4"
|
class="tw-mb-4"
|
||||||
[selected]="credentialType$ | async"
|
[selected]="credentialType$ | async"
|
||||||
(selectedChange)="onCredentialTypeChanged($event)"
|
(selectedChange)="onCredentialTypeChanged($event)"
|
||||||
|
*ngIf="showCredentialTypes$ | async"
|
||||||
attr.aria-label="{{ 'type' | i18n }}"
|
attr.aria-label="{{ 'type' | i18n }}"
|
||||||
>
|
>
|
||||||
<bit-toggle *ngFor="let option of passwordOptions$ | async" [value]="option.value">
|
<bit-toggle *ngFor="let option of passwordOptions$ | async" [value]="option.value">
|
||||||
@@ -38,14 +39,14 @@
|
|||||||
</bit-card>
|
</bit-card>
|
||||||
<tools-password-settings
|
<tools-password-settings
|
||||||
class="tw-mt-6"
|
class="tw-mt-6"
|
||||||
*ngIf="(algorithm$ | async)?.id === 'password'"
|
*ngIf="(algorithm$ | async)?.id === Algorithm.password"
|
||||||
[account]="account$ | async"
|
[account]="account$ | async"
|
||||||
[disableMargin]="disableMargin"
|
[disableMargin]="disableMargin"
|
||||||
(onUpdated)="generate('password settings')"
|
(onUpdated)="generate('password settings')"
|
||||||
/>
|
/>
|
||||||
<tools-passphrase-settings
|
<tools-passphrase-settings
|
||||||
class="tw-mt-6"
|
class="tw-mt-6"
|
||||||
*ngIf="(algorithm$ | async)?.id === 'passphrase'"
|
*ngIf="(algorithm$ | async)?.id === Algorithm.passphrase"
|
||||||
[account]="account$ | async"
|
[account]="account$ | async"
|
||||||
(onUpdated)="generate('passphrase settings')"
|
(onUpdated)="generate('passphrase settings')"
|
||||||
[disableMargin]="disableMargin"
|
[disableMargin]="disableMargin"
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
import {
|
import {
|
||||||
@@ -22,12 +20,12 @@ import {
|
|||||||
map,
|
map,
|
||||||
ReplaySubject,
|
ReplaySubject,
|
||||||
Subject,
|
Subject,
|
||||||
switchMap,
|
|
||||||
takeUntil,
|
takeUntil,
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import {
|
import {
|
||||||
SemanticLogger,
|
SemanticLogger,
|
||||||
@@ -38,17 +36,22 @@ import { UserId } from "@bitwarden/common/types/guid";
|
|||||||
import { ToastService, Option } from "@bitwarden/components";
|
import { ToastService, Option } from "@bitwarden/components";
|
||||||
import {
|
import {
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
Generators,
|
|
||||||
GeneratedCredential,
|
GeneratedCredential,
|
||||||
|
AlgorithmInfo,
|
||||||
|
GenerateRequest,
|
||||||
|
isSameAlgorithm,
|
||||||
CredentialAlgorithm,
|
CredentialAlgorithm,
|
||||||
isPasswordAlgorithm,
|
isPasswordAlgorithm,
|
||||||
AlgorithmInfo,
|
Algorithm,
|
||||||
isSameAlgorithm,
|
AlgorithmMetadata,
|
||||||
GenerateRequest,
|
Type,
|
||||||
CredentialCategories,
|
GeneratorProfile,
|
||||||
|
Profile,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||||
|
|
||||||
|
import { toAlgorithmInfo, translate } from "./util";
|
||||||
|
|
||||||
/** Options group for passwords */
|
/** Options group for passwords */
|
||||||
@Component({
|
@Component({
|
||||||
selector: "tools-password-generator",
|
selector: "tools-password-generator",
|
||||||
@@ -60,17 +63,21 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
private generatorService: CredentialGeneratorService,
|
private generatorService: CredentialGeneratorService,
|
||||||
private generatorHistoryService: GeneratorHistoryService,
|
private generatorHistoryService: GeneratorHistoryService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
|
private i18nService: I18nService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private ariaLive: LiveAnnouncer,
|
private ariaLive: LiveAnnouncer,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/** exports algorithm symbols to the template */
|
||||||
|
protected readonly Algorithm = Algorithm;
|
||||||
|
|
||||||
/** Binds the component to a specific user's settings. When this input is not provided,
|
/** Binds the component to a specific user's settings. When this input is not provided,
|
||||||
* the form binds to the active user
|
* the form binds to the active user
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
account: Account | null;
|
account: Account | null = null;
|
||||||
|
|
||||||
protected account$ = new ReplaySubject<Account>(1);
|
protected account$ = new ReplaySubject<Account>(1);
|
||||||
|
|
||||||
@@ -87,7 +94,11 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
|
|
||||||
async ngOnChanges(changes: SimpleChanges) {
|
async ngOnChanges(changes: SimpleChanges) {
|
||||||
const account = changes?.account;
|
const account = changes?.account;
|
||||||
if (account?.previousValue?.id !== account?.currentValue?.id) {
|
if (
|
||||||
|
account &&
|
||||||
|
account.currentValue.id &&
|
||||||
|
account.previousValue.id !== account.currentValue.id
|
||||||
|
) {
|
||||||
this.log.debug(
|
this.log.debug(
|
||||||
{
|
{
|
||||||
previousUserId: account?.previousValue?.id as UserId,
|
previousUserId: account?.previousValue?.id as UserId,
|
||||||
@@ -95,15 +106,19 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
},
|
},
|
||||||
"account input change detected",
|
"account input change detected",
|
||||||
);
|
);
|
||||||
this.account$.next(this.account);
|
this.account$.next(account.currentValue.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
profile: GeneratorProfile = Profile.account;
|
||||||
|
|
||||||
/** Removes bottom margin, passed to downstream components */
|
/** Removes bottom margin, passed to downstream components */
|
||||||
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
|
@Input({ transform: coerceBooleanProperty })
|
||||||
|
disableMargin = false;
|
||||||
|
|
||||||
/** tracks the currently selected credential type */
|
/** tracks the currently selected credential type */
|
||||||
protected credentialType$ = new BehaviorSubject<CredentialAlgorithm>(null);
|
protected credentialType$ = new BehaviorSubject<CredentialAlgorithm | null>(null);
|
||||||
|
|
||||||
/** Emits the last generated value. */
|
/** Emits the last generated value. */
|
||||||
protected readonly value$ = new BehaviorSubject<string>("");
|
protected readonly value$ = new BehaviorSubject<string>("");
|
||||||
@@ -119,9 +134,11 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
* origin in the debugger.
|
* origin in the debugger.
|
||||||
*/
|
*/
|
||||||
protected async generate(source: string) {
|
protected async generate(source: string) {
|
||||||
this.log.debug({ source }, "generation requested");
|
const algorithm = await firstValueFrom(this.algorithm$);
|
||||||
|
const request: GenerateRequest = { source, algorithm: algorithm.id, profile: this.profile };
|
||||||
|
|
||||||
this.generate$.next({ source });
|
this.log.debug(request, "generation requested");
|
||||||
|
this.generate$.next(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tracks changes to the selected credential type
|
/** Tracks changes to the selected credential type
|
||||||
@@ -146,16 +163,17 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.log = ifEnabledSemanticLoggerProvider(this.debug, this.logService, {
|
this.log = ifEnabledSemanticLoggerProvider(this.debug, this.logService, {
|
||||||
type: "UsernameGeneratorComponent",
|
type: "PasswordGeneratorComponent",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.account) {
|
if (!this.account) {
|
||||||
this.account = await firstValueFrom(this.accountService.activeAccount$);
|
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
this.log.info(
|
if (!account) {
|
||||||
{ userId: this.account.id },
|
this.log.panic("active account cannot be `null`.");
|
||||||
"account not specified; using active account settings",
|
}
|
||||||
);
|
|
||||||
this.account$.next(this.account);
|
this.log.info({ userId: account.id }, "account not specified; using active account settings");
|
||||||
|
this.account$.next(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.generatorService
|
this.generatorService
|
||||||
@@ -167,10 +185,9 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
.subscribe(this.passwordOptions$);
|
.subscribe(this.passwordOptions$);
|
||||||
|
|
||||||
// wire up the generator
|
// wire up the generator
|
||||||
this.algorithm$
|
this.generatorService
|
||||||
|
.generate$({ on$: this.generate$, account$: this.account$ })
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
|
||||||
switchMap((algorithm) => this.typeToGenerator$(algorithm.id)),
|
|
||||||
catchError((error: unknown, generator) => {
|
catchError((error: unknown, generator) => {
|
||||||
if (typeof error === "string") {
|
if (typeof error === "string") {
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
@@ -189,7 +206,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(([generated, account, algorithm]) => {
|
.subscribe(([generated, account, algorithm]) => {
|
||||||
this.log.debug({ source: generated.source }, "credential generated");
|
this.log.debug({ source: generated.source ?? null }, "credential generated");
|
||||||
|
|
||||||
this.generatorHistoryService
|
this.generatorHistoryService
|
||||||
.track(account.id, generated.credential, generated.category, generated.generationDate)
|
.track(account.id, generated.credential, generated.category, generated.generationDate)
|
||||||
@@ -201,7 +218,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
// template bindings refresh immediately
|
// template bindings refresh immediately
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
if (generated.source === this.USER_REQUEST) {
|
if (generated.source === this.USER_REQUEST) {
|
||||||
this.announce(algorithm.onGeneratedMessage);
|
this.announce(translate(algorithm.i18nKeys.credentialGenerated, this.i18nService));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onGenerated.next(generated);
|
this.onGenerated.next(generated);
|
||||||
@@ -219,10 +236,7 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
)
|
)
|
||||||
.subscribe(([algorithm, preference]) => {
|
.subscribe(([algorithm, preference]) => {
|
||||||
if (isPasswordAlgorithm(algorithm)) {
|
if (isPasswordAlgorithm(algorithm)) {
|
||||||
this.log.info(
|
this.log.info({ algorithm, type: Type.password }, "algorithm preferences updated");
|
||||||
{ algorithm, category: CredentialCategories.password },
|
|
||||||
"algorithm preferences updated",
|
|
||||||
);
|
|
||||||
preference.password.algorithm = algorithm;
|
preference.password.algorithm = algorithm;
|
||||||
preference.password.updated = new Date();
|
preference.password.updated = new Date();
|
||||||
} else {
|
} else {
|
||||||
@@ -236,11 +250,17 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
preferences
|
preferences
|
||||||
.pipe(
|
.pipe(
|
||||||
map(({ password }) => this.generatorService.algorithm(password.algorithm)),
|
map(({ password }) => this.generatorService.algorithm(password.algorithm)),
|
||||||
distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)),
|
distinctUntilChanged((prev, next) => {
|
||||||
|
if (prev === null || next === null) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return isSameAlgorithm(prev.id, next.id);
|
||||||
|
}
|
||||||
|
}),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe((algorithm) => {
|
.subscribe((algorithm) => {
|
||||||
this.log.debug(algorithm, "algorithm selected");
|
this.log.debug({ algorithm: algorithm.id }, "algorithm selected");
|
||||||
|
|
||||||
// update navigation
|
// update navigation
|
||||||
this.onCredentialTypeChanged(algorithm.id);
|
this.onCredentialTypeChanged(algorithm.id);
|
||||||
@@ -248,22 +268,22 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
// update subjects within the angular zone so that the
|
// update subjects within the angular zone so that the
|
||||||
// template bindings refresh immediately
|
// template bindings refresh immediately
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
this.algorithm$.next(algorithm);
|
this.maybeAlgorithm$.next(algorithm);
|
||||||
this.onAlgorithm.next(algorithm);
|
this.onAlgorithm.next(toAlgorithmInfo(algorithm, this.i18nService));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// generate on load unless the generator prohibits it
|
// generate on load unless the generator prohibits it
|
||||||
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
this.maybeAlgorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
if (!a || a.onlyOnRequest) {
|
if (a?.capabilities?.autogenerate) {
|
||||||
this.log.debug("autogeneration disabled; clearing generated credential");
|
|
||||||
this.value$.next("-");
|
|
||||||
} else {
|
|
||||||
this.log.debug("autogeneration enabled");
|
this.log.debug("autogeneration enabled");
|
||||||
this.generate("autogenerate").catch((e: unknown) => {
|
this.generate("autogenerate").catch((e: unknown) => {
|
||||||
this.log.error(e as object, "a failure occurred during autogeneration");
|
this.log.error(e as object, "a failure occurred during autogeneration");
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.log.debug("autogeneration disabled; clearing generated credential");
|
||||||
|
this.value$.next("-");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -275,59 +295,45 @@ export class PasswordGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
|
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
private typeToGenerator$(algorithm: CredentialAlgorithm) {
|
|
||||||
const dependencies = {
|
|
||||||
on$: this.generate$,
|
|
||||||
account$: this.account$,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.log.debug({ algorithm }, "constructing generation stream");
|
|
||||||
|
|
||||||
switch (algorithm) {
|
|
||||||
case "password":
|
|
||||||
return this.generatorService.generate$(Generators.password, dependencies);
|
|
||||||
|
|
||||||
case "passphrase":
|
|
||||||
return this.generatorService.generate$(Generators.passphrase, dependencies);
|
|
||||||
default:
|
|
||||||
this.log.panic({ algorithm }, `Invalid generator type: "${algorithm}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Lists the credential types supported by the component. */
|
/** Lists the credential types supported by the component. */
|
||||||
protected passwordOptions$ = new BehaviorSubject<Option<CredentialAlgorithm>[]>([]);
|
protected passwordOptions$ = new BehaviorSubject<Option<CredentialAlgorithm>[]>([]);
|
||||||
|
|
||||||
|
/** Determines when the password/passphrase selector is visible. */
|
||||||
|
protected showCredentialTypes$ = this.passwordOptions$.pipe(map((options) => options.length > 1));
|
||||||
|
|
||||||
/** tracks the currently selected credential type */
|
/** tracks the currently selected credential type */
|
||||||
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);
|
protected maybeAlgorithm$ = new ReplaySubject<AlgorithmMetadata>(1);
|
||||||
|
|
||||||
|
/** tracks the last valid algorithm selection */
|
||||||
|
protected algorithm$ = this.maybeAlgorithm$.pipe(
|
||||||
|
filter((algorithm): algorithm is AlgorithmMetadata => !!algorithm),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the copy button aria-label respective of the selected credential type
|
* Emits the copy button aria-label respective of the selected credential type
|
||||||
*/
|
*/
|
||||||
protected credentialTypeCopyLabel$ = this.algorithm$.pipe(
|
protected credentialTypeCopyLabel$ = this.algorithm$.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
map(({ i18nKeys: { copyCredential } }) => translate(copyCredential, this.i18nService)),
|
||||||
map(({ copy }) => copy),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the generate button aria-label respective of the selected credential type
|
* Emits the generate button aria-label respective of the selected credential type
|
||||||
*/
|
*/
|
||||||
protected credentialTypeGenerateLabel$ = this.algorithm$.pipe(
|
protected credentialTypeGenerateLabel$ = this.algorithm$.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
map(({ i18nKeys: { generateCredential } }) => translate(generateCredential, this.i18nService)),
|
||||||
map(({ generate }) => generate),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the copy credential toast respective of the selected credential type
|
* Emits the copy credential toast respective of the selected credential type
|
||||||
*/
|
*/
|
||||||
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
map(({ i18nKeys: { credentialType } }) => translate(credentialType, this.i18nService)),
|
||||||
map(({ credentialType }) => credentialType),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
private toOptions(algorithms: AlgorithmInfo[]) {
|
private toOptions(algorithms: AlgorithmMetadata[]) {
|
||||||
const options: Option<CredentialAlgorithm>[] = algorithms.map((algorithm) => ({
|
const options: Option<CredentialAlgorithm>[] = algorithms.map((algorithm) => ({
|
||||||
value: algorithm.id,
|
value: algorithm.id,
|
||||||
label: algorithm.name,
|
label: translate(algorithm.i18nKeys.name, this.i18nService),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
import {
|
import {
|
||||||
OnInit,
|
OnInit,
|
||||||
@@ -17,11 +15,13 @@ import { takeUntil, Subject, map, filter, tap, skip, ReplaySubject, withLatestFr
|
|||||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import {
|
import {
|
||||||
Generators,
|
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
PasswordGenerationOptions,
|
PasswordGenerationOptions,
|
||||||
|
BuiltIn,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
|
import { hasRangeOfValues } from "./util";
|
||||||
|
|
||||||
const Controls = Object.freeze({
|
const Controls = Object.freeze({
|
||||||
length: "length",
|
length: "length",
|
||||||
uppercase: "uppercase",
|
uppercase: "uppercase",
|
||||||
@@ -52,9 +52,11 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Binds the component to a specific user's settings.
|
/** Binds the component to a specific user's settings.
|
||||||
|
* @remarks this is initialized to null but since it's a required input it'll
|
||||||
|
* never have that value in practice.
|
||||||
*/
|
*/
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
account: Account;
|
account: Account = null!;
|
||||||
|
|
||||||
protected account$ = new ReplaySubject<Account>(1);
|
protected account$ = new ReplaySubject<Account>(1);
|
||||||
|
|
||||||
@@ -78,40 +80,40 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
/** Emits settings updates and completes if the settings become unavailable.
|
/** Emits settings updates and completes if the settings become unavailable.
|
||||||
* @remarks this does not emit the initial settings. If you would like
|
* @remarks this does not emit the initial settings. If you would like
|
||||||
* to receive live settings updates including the initial update,
|
* to receive live settings updates including the initial update,
|
||||||
* use `CredentialGeneratorService.settings$(...)` instead.
|
* use `CredentialGeneratorService.settings(...)` instead.
|
||||||
*/
|
*/
|
||||||
@Output()
|
@Output()
|
||||||
readonly onUpdated = new EventEmitter<PasswordGenerationOptions>();
|
readonly onUpdated = new EventEmitter<PasswordGenerationOptions>();
|
||||||
|
|
||||||
protected settings = this.formBuilder.group({
|
protected settings = this.formBuilder.group({
|
||||||
[Controls.length]: [Generators.password.settings.initial.length],
|
[Controls.length]: [0],
|
||||||
[Controls.uppercase]: [Generators.password.settings.initial.uppercase],
|
[Controls.uppercase]: [false],
|
||||||
[Controls.lowercase]: [Generators.password.settings.initial.lowercase],
|
[Controls.lowercase]: [false],
|
||||||
[Controls.number]: [Generators.password.settings.initial.number],
|
[Controls.number]: [false],
|
||||||
[Controls.special]: [Generators.password.settings.initial.special],
|
[Controls.special]: [false],
|
||||||
[Controls.minNumber]: [Generators.password.settings.initial.minNumber],
|
[Controls.minNumber]: [0],
|
||||||
[Controls.minSpecial]: [Generators.password.settings.initial.minSpecial],
|
[Controls.minSpecial]: [0],
|
||||||
[Controls.avoidAmbiguous]: [!Generators.password.settings.initial.ambiguous],
|
[Controls.avoidAmbiguous]: [false],
|
||||||
});
|
});
|
||||||
|
|
||||||
private get numbers() {
|
private get numbers() {
|
||||||
return this.settings.get(Controls.number);
|
return this.settings.get(Controls.number)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get special() {
|
private get special() {
|
||||||
return this.settings.get(Controls.special);
|
return this.settings.get(Controls.special)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get minNumber() {
|
private get minNumber() {
|
||||||
return this.settings.get(Controls.minNumber);
|
return this.settings.get(Controls.minNumber)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get minSpecial() {
|
private get minSpecial() {
|
||||||
return this.settings.get(Controls.minSpecial);
|
return this.settings.get(Controls.minSpecial)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const settings = await this.generatorService.settings(Generators.password, {
|
const settings = await this.generatorService.settings(BuiltIn.password, {
|
||||||
account$: this.account$,
|
account$: this.account$,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,13 +132,13 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
.subscribe(([state, constraints]) => {
|
.subscribe(([state, constraints]) => {
|
||||||
let boundariesHint = this.i18nService.t(
|
let boundariesHint = this.i18nService.t(
|
||||||
"spinboxBoundariesHint",
|
"spinboxBoundariesHint",
|
||||||
constraints.length.min?.toString(),
|
constraints.length?.min?.toString(),
|
||||||
constraints.length.max?.toString(),
|
constraints.length?.max?.toString(),
|
||||||
);
|
);
|
||||||
if (state.length <= (constraints.length.recommendation ?? 0)) {
|
if (state.length <= (constraints.length?.recommendation ?? 0)) {
|
||||||
boundariesHint += this.i18nService.t(
|
boundariesHint += this.i18nService.t(
|
||||||
"passwordLengthRecommendationHint",
|
"passwordLengthRecommendationHint",
|
||||||
constraints.length.recommendation?.toString(),
|
constraints.length?.recommendation?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.lengthBoundariesHint.next(boundariesHint);
|
this.lengthBoundariesHint.next(boundariesHint);
|
||||||
@@ -147,19 +149,25 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
// explain policy & disable policy-overridden fields
|
// explain policy & disable policy-overridden fields
|
||||||
this.generatorService
|
this.generatorService
|
||||||
.policy$(Generators.password, { account$: this.account$ })
|
.policy$(BuiltIn.password, { account$: this.account$ })
|
||||||
.pipe(takeUntil(this.destroyed$))
|
.pipe(takeUntil(this.destroyed$))
|
||||||
.subscribe(({ constraints }) => {
|
.subscribe(({ constraints }) => {
|
||||||
this.policyInEffect = constraints.policyInEffect;
|
this.policyInEffect = constraints.policyInEffect ?? false;
|
||||||
|
|
||||||
const toggles = [
|
const toggles = [
|
||||||
[Controls.length, constraints.length.min < constraints.length.max],
|
[Controls.length, hasRangeOfValues(constraints.length?.min, constraints.length?.max)],
|
||||||
[Controls.uppercase, !constraints.uppercase?.readonly],
|
[Controls.uppercase, !constraints.uppercase?.readonly],
|
||||||
[Controls.lowercase, !constraints.lowercase?.readonly],
|
[Controls.lowercase, !constraints.lowercase?.readonly],
|
||||||
[Controls.number, !constraints.number?.readonly],
|
[Controls.number, !constraints.number?.readonly],
|
||||||
[Controls.special, !constraints.special?.readonly],
|
[Controls.special, !constraints.special?.readonly],
|
||||||
[Controls.minNumber, constraints.minNumber.min < constraints.minNumber.max],
|
[
|
||||||
[Controls.minSpecial, constraints.minSpecial.min < constraints.minSpecial.max],
|
Controls.minNumber,
|
||||||
|
hasRangeOfValues(constraints.minNumber?.min, constraints.minNumber?.max),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Controls.minSpecial,
|
||||||
|
hasRangeOfValues(constraints.minSpecial?.min, constraints.minSpecial?.max),
|
||||||
|
],
|
||||||
] as [keyof typeof Controls, boolean][];
|
] as [keyof typeof Controls, boolean][];
|
||||||
|
|
||||||
for (const [control, enabled] of toggles) {
|
for (const [control, enabled] of toggles) {
|
||||||
@@ -172,7 +180,7 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
let lastMinNumber = 1;
|
let lastMinNumber = 1;
|
||||||
this.numbers.valueChanges
|
this.numbers.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((checked) => !(checked && this.minNumber.value > 0)),
|
filter((checked) => !(checked && (this.minNumber.value ?? 0) > 0)),
|
||||||
map((checked) => (checked ? lastMinNumber : 0)),
|
map((checked) => (checked ? lastMinNumber : 0)),
|
||||||
takeUntil(this.destroyed$),
|
takeUntil(this.destroyed$),
|
||||||
)
|
)
|
||||||
@@ -180,8 +188,11 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
this.minNumber.valueChanges
|
this.minNumber.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
map((value) => [value, value > 0] as const),
|
map((value) => [value, (value ?? 0) > 0] as const),
|
||||||
tap(([value, checkNumbers]) => (lastMinNumber = checkNumbers ? value : lastMinNumber)),
|
tap(
|
||||||
|
([value, checkNumbers]) =>
|
||||||
|
(lastMinNumber = checkNumbers && value ? value : lastMinNumber),
|
||||||
|
),
|
||||||
takeUntil(this.destroyed$),
|
takeUntil(this.destroyed$),
|
||||||
)
|
)
|
||||||
.subscribe(([, checkNumbers]) => this.numbers.setValue(checkNumbers, { emitEvent: false }));
|
.subscribe(([, checkNumbers]) => this.numbers.setValue(checkNumbers, { emitEvent: false }));
|
||||||
@@ -189,7 +200,7 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
let lastMinSpecial = 1;
|
let lastMinSpecial = 1;
|
||||||
this.special.valueChanges
|
this.special.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((checked) => !(checked && this.minSpecial.value > 0)),
|
filter((checked) => !(checked && (this.minSpecial.value ?? 0) > 0)),
|
||||||
map((checked) => (checked ? lastMinSpecial : 0)),
|
map((checked) => (checked ? lastMinSpecial : 0)),
|
||||||
takeUntil(this.destroyed$),
|
takeUntil(this.destroyed$),
|
||||||
)
|
)
|
||||||
@@ -197,8 +208,11 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
this.minSpecial.valueChanges
|
this.minSpecial.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
map((value) => [value, value > 0] as const),
|
map((value) => [value, (value ?? 0) > 0] as const),
|
||||||
tap(([value, checkSpecial]) => (lastMinSpecial = checkSpecial ? value : lastMinSpecial)),
|
tap(
|
||||||
|
([value, checkSpecial]) =>
|
||||||
|
(lastMinSpecial = checkSpecial && value ? value : lastMinSpecial),
|
||||||
|
),
|
||||||
takeUntil(this.destroyed$),
|
takeUntil(this.destroyed$),
|
||||||
)
|
)
|
||||||
.subscribe(([, checkSpecial]) => this.special.setValue(checkSpecial, { emitEvent: false }));
|
.subscribe(([, checkSpecial]) => this.special.setValue(checkSpecial, { emitEvent: false }));
|
||||||
@@ -230,7 +244,7 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** display binding for enterprise policy notice */
|
/** display binding for enterprise policy notice */
|
||||||
protected policyInEffect: boolean;
|
protected policyInEffect: boolean = false;
|
||||||
|
|
||||||
private lengthBoundariesHint = new ReplaySubject<string>(1);
|
private lengthBoundariesHint = new ReplaySubject<string>(1);
|
||||||
|
|
||||||
@@ -239,9 +253,9 @@ export class PasswordSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) {
|
private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
this.settings.get(setting).enable({ emitEvent: false });
|
this.settings.get(setting)?.enable({ emitEvent: false });
|
||||||
} else {
|
} else {
|
||||||
this.settings.get(setting).disable({ emitEvent: false });
|
this.settings.get(setting)?.disable({ emitEvent: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@@ -13,10 +11,10 @@ import {
|
|||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { map, ReplaySubject, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
import { map, ReplaySubject, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||||
|
|
||||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import {
|
import {
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
Generators,
|
BuiltIn,
|
||||||
SubaddressGenerationOptions,
|
SubaddressGenerationOptions,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
@@ -28,20 +26,20 @@ import {
|
|||||||
})
|
})
|
||||||
export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
/** Instantiates the component
|
/** Instantiates the component
|
||||||
* @param accountService queries user availability
|
|
||||||
* @param generatorService settings and policy logic
|
* @param generatorService settings and policy logic
|
||||||
* @param formBuilder reactive form controls
|
* @param formBuilder reactive form controls
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private generatorService: CredentialGeneratorService,
|
private generatorService: CredentialGeneratorService,
|
||||||
private accountService: AccountService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Binds the component to a specific user's settings.
|
/** Binds the component to a specific user's settings.
|
||||||
|
* @remarks this is initialized to null but since it's a required input it'll
|
||||||
|
* never have that value in practice.
|
||||||
*/
|
*/
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
account: Account;
|
account: Account = null!;
|
||||||
|
|
||||||
protected account$ = new ReplaySubject<Account>(1);
|
protected account$ = new ReplaySubject<Account>(1);
|
||||||
|
|
||||||
@@ -54,18 +52,18 @@ export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
/** Emits settings updates and completes if the settings become unavailable.
|
/** Emits settings updates and completes if the settings become unavailable.
|
||||||
* @remarks this does not emit the initial settings. If you would like
|
* @remarks this does not emit the initial settings. If you would like
|
||||||
* to receive live settings updates including the initial update,
|
* to receive live settings updates including the initial update,
|
||||||
* use `CredentialGeneratorService.settings$(...)` instead.
|
* use `CredentialGeneratorService.settings(...)` instead.
|
||||||
*/
|
*/
|
||||||
@Output()
|
@Output()
|
||||||
readonly onUpdated = new EventEmitter<SubaddressGenerationOptions>();
|
readonly onUpdated = new EventEmitter<SubaddressGenerationOptions>();
|
||||||
|
|
||||||
/** The template's control bindings */
|
/** The template's control bindings */
|
||||||
protected settings = this.formBuilder.group({
|
protected settings = this.formBuilder.group({
|
||||||
subaddressEmail: [Generators.subaddress.settings.initial.subaddressEmail],
|
subaddressEmail: [""],
|
||||||
});
|
});
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const settings = await this.generatorService.settings(Generators.subaddress, {
|
const settings = await this.generatorService.settings(BuiltIn.plusAddress, {
|
||||||
account$: this.account$,
|
account$: this.account$,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,7 +77,7 @@ export class SubaddressSettingsComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
this.saveSettings
|
this.saveSettings
|
||||||
.pipe(
|
.pipe(
|
||||||
withLatestFrom(this.settings.valueChanges),
|
withLatestFrom(this.settings.valueChanges),
|
||||||
map(([, settings]) => settings),
|
map(([, settings]) => settings as SubaddressGenerationOptions),
|
||||||
takeUntil(this.destroyed$),
|
takeUntil(this.destroyed$),
|
||||||
)
|
)
|
||||||
.subscribe(settings);
|
.subscribe(settings);
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</form>
|
</form>
|
||||||
<tools-catchall-settings
|
<tools-catchall-settings
|
||||||
*ngIf="(algorithm$ | async)?.id === 'catchall'"
|
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.catchall"
|
||||||
[account]="account$ | async"
|
[account]="account$ | async"
|
||||||
(onUpdated)="generate('catchall settings')"
|
(onUpdated)="generate('catchall settings')"
|
||||||
/>
|
/>
|
||||||
@@ -69,12 +69,12 @@
|
|||||||
[account]="account$ | async"
|
[account]="account$ | async"
|
||||||
/>
|
/>
|
||||||
<tools-subaddress-settings
|
<tools-subaddress-settings
|
||||||
*ngIf="(algorithm$ | async)?.id === 'subaddress'"
|
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.plusAddress"
|
||||||
[account]="account$ | async"
|
[account]="account$ | async"
|
||||||
(onUpdated)="generate('subaddress settings')"
|
(onUpdated)="generate('subaddress settings')"
|
||||||
/>
|
/>
|
||||||
<tools-username-settings
|
<tools-username-settings
|
||||||
*ngIf="(algorithm$ | async)?.id === 'username'"
|
*ngIf="(showAlgorithm$ | async)?.id === Algorithm.username"
|
||||||
[account]="account$ | async"
|
[account]="account$ | async"
|
||||||
(onUpdated)="generate('username settings')"
|
(onUpdated)="generate('username settings')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
import {
|
import {
|
||||||
@@ -25,15 +23,15 @@ import {
|
|||||||
map,
|
map,
|
||||||
ReplaySubject,
|
ReplaySubject,
|
||||||
Subject,
|
Subject,
|
||||||
switchMap,
|
|
||||||
takeUntil,
|
takeUntil,
|
||||||
|
tap,
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { IntegrationId } from "@bitwarden/common/tools/integration";
|
import { VendorId } from "@bitwarden/common/tools/extension";
|
||||||
import {
|
import {
|
||||||
SemanticLogger,
|
SemanticLogger,
|
||||||
disabledSemanticLoggerProvider,
|
disabledSemanticLoggerProvider,
|
||||||
@@ -43,21 +41,23 @@ import { UserId } from "@bitwarden/common/types/guid";
|
|||||||
import { ToastService, Option } from "@bitwarden/components";
|
import { ToastService, Option } from "@bitwarden/components";
|
||||||
import {
|
import {
|
||||||
AlgorithmInfo,
|
AlgorithmInfo,
|
||||||
CredentialAlgorithm,
|
|
||||||
CredentialCategories,
|
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
GenerateRequest,
|
GenerateRequest,
|
||||||
GeneratedCredential,
|
GeneratedCredential,
|
||||||
Generators,
|
isForwarderExtensionId,
|
||||||
getForwarderConfiguration,
|
|
||||||
isEmailAlgorithm,
|
isEmailAlgorithm,
|
||||||
isForwarderIntegration,
|
|
||||||
isSameAlgorithm,
|
|
||||||
isUsernameAlgorithm,
|
isUsernameAlgorithm,
|
||||||
toCredentialGeneratorConfiguration,
|
isSameAlgorithm,
|
||||||
|
CredentialAlgorithm,
|
||||||
|
AlgorithmMetadata,
|
||||||
|
AlgorithmsByType,
|
||||||
|
Type,
|
||||||
|
Algorithm,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||||
|
|
||||||
|
import { toAlgorithmInfo, translate } from "./util";
|
||||||
|
|
||||||
// constants used to identify navigation selections that are not
|
// constants used to identify navigation selections that are not
|
||||||
// generator algorithms
|
// generator algorithms
|
||||||
const FORWARDER = "forwarder";
|
const FORWARDER = "forwarder";
|
||||||
@@ -89,11 +89,14 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
private ariaLive: LiveAnnouncer,
|
private ariaLive: LiveAnnouncer,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/** exports algorithm symbols to the template */
|
||||||
|
protected readonly Algorithm = Algorithm;
|
||||||
|
|
||||||
/** Binds the component to a specific user's settings. When this input is not provided,
|
/** Binds the component to a specific user's settings. When this input is not provided,
|
||||||
* the form binds to the active user
|
* the form binds to the active user
|
||||||
*/
|
*/
|
||||||
@Input()
|
@Input()
|
||||||
account: Account | null;
|
account: Account | null = null;
|
||||||
|
|
||||||
protected account$ = new ReplaySubject<Account>(1);
|
protected account$ = new ReplaySubject<Account>(1);
|
||||||
|
|
||||||
@@ -110,7 +113,11 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
|
|
||||||
async ngOnChanges(changes: SimpleChanges) {
|
async ngOnChanges(changes: SimpleChanges) {
|
||||||
const account = changes?.account;
|
const account = changes?.account;
|
||||||
if (account?.previousValue?.id !== account?.currentValue?.id) {
|
if (
|
||||||
|
account &&
|
||||||
|
account.currentValue.id &&
|
||||||
|
account.previousValue.id !== account.currentValue.id
|
||||||
|
) {
|
||||||
this.log.debug(
|
this.log.debug(
|
||||||
{
|
{
|
||||||
previousUserId: account?.previousValue?.id as UserId,
|
previousUserId: account?.previousValue?.id as UserId,
|
||||||
@@ -118,7 +125,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
},
|
},
|
||||||
"account input change detected",
|
"account input change detected",
|
||||||
);
|
);
|
||||||
this.account$.next(this.account);
|
this.account$.next(account.currentValue.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,18 +141,18 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
|
|
||||||
/** emits algorithm info when the selected algorithm changes */
|
/** emits algorithm info when the selected algorithm changes */
|
||||||
@Output()
|
@Output()
|
||||||
readonly onAlgorithm = new EventEmitter<AlgorithmInfo>();
|
readonly onAlgorithm = new EventEmitter<AlgorithmInfo | null>();
|
||||||
|
|
||||||
/** Removes bottom margin from internal elements */
|
/** Removes bottom margin from internal elements */
|
||||||
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
|
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
|
||||||
|
|
||||||
/** Tracks the selected generation algorithm */
|
/** Tracks the selected generation algorithm */
|
||||||
protected username = this.formBuilder.group({
|
protected username = this.formBuilder.group({
|
||||||
nav: [null as string],
|
nav: [null as string | null],
|
||||||
});
|
});
|
||||||
|
|
||||||
protected forwarder = this.formBuilder.group({
|
protected forwarder = this.formBuilder.group({
|
||||||
nav: [null as string],
|
nav: [null as string | null],
|
||||||
});
|
});
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -154,38 +161,63 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!this.account) {
|
if (!this.account) {
|
||||||
this.account = await firstValueFrom(this.accountService.activeAccount$);
|
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
this.log.info(
|
if (!account) {
|
||||||
{ userId: this.account.id },
|
this.log.panic("active account cannot be `null`.");
|
||||||
"account not specified; using active account settings",
|
}
|
||||||
);
|
|
||||||
this.account$.next(this.account);
|
this.log.info({ userId: account.id }, "account not specified; using active account settings");
|
||||||
|
this.account$.next(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.generatorService
|
combineLatest([
|
||||||
.algorithms$(["email", "username"], { account$: this.account$ })
|
this.generatorService.algorithms$("email", { account$: this.account$ }),
|
||||||
|
this.generatorService.algorithms$("username", { account$: this.account$ }),
|
||||||
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
|
map((algorithms) => algorithms.flat()),
|
||||||
map((algorithms) => {
|
map((algorithms) => {
|
||||||
const usernames = algorithms.filter((a) => !isForwarderIntegration(a.id));
|
// construct options for username and email algorithms; replace forwarder
|
||||||
|
// entry with a virtual entry for drill-down
|
||||||
|
const usernames = algorithms.filter((a) => !isForwarderExtensionId(a.id));
|
||||||
|
usernames.sort((a, b) => a.weight - b.weight);
|
||||||
const usernameOptions = this.toOptions(usernames);
|
const usernameOptions = this.toOptions(usernames);
|
||||||
usernameOptions.push({ value: FORWARDER, label: this.i18nService.t("forwardedEmail") });
|
usernameOptions.splice(-1, 0, {
|
||||||
|
value: FORWARDER,
|
||||||
|
label: this.i18nService.t("forwardedEmail"),
|
||||||
|
});
|
||||||
|
|
||||||
const forwarders = algorithms.filter((a) => isForwarderIntegration(a.id));
|
// construct options for forwarder algorithms; they get their own selection box
|
||||||
|
const forwarders = algorithms.filter((a) => isForwarderExtensionId(a.id));
|
||||||
|
forwarders.sort((a, b) => a.weight - b.weight);
|
||||||
const forwarderOptions = this.toOptions(forwarders);
|
const forwarderOptions = this.toOptions(forwarders);
|
||||||
forwarderOptions.unshift({ value: NONE_SELECTED, label: this.i18nService.t("select") });
|
forwarderOptions.unshift({ value: NONE_SELECTED, label: this.i18nService.t("select") });
|
||||||
|
|
||||||
return [usernameOptions, forwarderOptions] as const;
|
return [usernameOptions, forwarderOptions] as const;
|
||||||
}),
|
}),
|
||||||
|
tap((algorithms) =>
|
||||||
|
this.log.debug({ algorithms: algorithms as object }, "algorithms loaded"),
|
||||||
|
),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(([usernames, forwarders]) => {
|
.subscribe(([usernames, forwarders]) => {
|
||||||
this.typeOptions$.next(usernames);
|
// update subjects within the angular zone so that the
|
||||||
this.forwarderOptions$.next(forwarders);
|
// template bindings refresh immediately
|
||||||
|
this.zone.run(() => {
|
||||||
|
this.typeOptions$.next(usernames);
|
||||||
|
this.forwarderOptions$.next(forwarders);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.algorithm$
|
this.maybeAlgorithm$
|
||||||
.pipe(
|
.pipe(
|
||||||
map((a) => a?.description),
|
map((a) => {
|
||||||
|
if (a?.i18nKeys?.description) {
|
||||||
|
return translate(a.i18nKeys.description, this.i18nService);
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe((hint) => {
|
.subscribe((hint) => {
|
||||||
@@ -197,10 +229,12 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
});
|
});
|
||||||
|
|
||||||
// wire up the generator
|
// wire up the generator
|
||||||
this.algorithm$
|
this.generatorService
|
||||||
|
.generate$({
|
||||||
|
on$: this.generate$,
|
||||||
|
account$: this.account$,
|
||||||
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
|
||||||
switchMap((algorithm) => this.typeToGenerator$(algorithm.id)),
|
|
||||||
catchError((error: unknown, generator) => {
|
catchError((error: unknown, generator) => {
|
||||||
if (typeof error === "string") {
|
if (typeof error === "string") {
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
@@ -215,11 +249,14 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
// continue with origin stream
|
// continue with origin stream
|
||||||
return generator;
|
return generator;
|
||||||
}),
|
}),
|
||||||
withLatestFrom(this.account$, this.algorithm$),
|
withLatestFrom(this.account$, this.maybeAlgorithm$),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(([generated, account, algorithm]) => {
|
.subscribe(([generated, account, algorithm]) => {
|
||||||
this.log.debug({ source: generated.source }, "credential generated");
|
this.log.debug(
|
||||||
|
{ source: generated.source ?? null, algorithm: algorithm?.id ?? null },
|
||||||
|
"credential generated",
|
||||||
|
);
|
||||||
|
|
||||||
this.generatorHistoryService
|
this.generatorHistoryService
|
||||||
.track(account.id, generated.credential, generated.category, generated.generationDate)
|
.track(account.id, generated.credential, generated.category, generated.generationDate)
|
||||||
@@ -230,12 +267,12 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
// update subjects within the angular zone so that the
|
// update subjects within the angular zone so that the
|
||||||
// template bindings refresh immediately
|
// template bindings refresh immediately
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
if (generated.source === this.USER_REQUEST) {
|
if (algorithm && generated.source === this.USER_REQUEST) {
|
||||||
this.announce(algorithm.onGeneratedMessage);
|
this.announce(translate(algorithm.i18nKeys.credentialGenerated, this.i18nService));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.generatedCredential$.next(generated);
|
||||||
this.onGenerated.next(generated);
|
this.onGenerated.next(generated);
|
||||||
this.value$.next(generated.credential);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -248,24 +285,31 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
|
|
||||||
this.username.valueChanges
|
this.username.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
map(
|
map((username): CascadeValue => {
|
||||||
(username): CascadeValue =>
|
if (username.nav === FORWARDER) {
|
||||||
username.nav === FORWARDER
|
return { nav: username.nav };
|
||||||
? { nav: username.nav }
|
} else if (username.nav) {
|
||||||
: { nav: username.nav, algorithm: JSON.parse(username.nav) },
|
return { nav: username.nav, algorithm: JSON.parse(username.nav) };
|
||||||
),
|
} else {
|
||||||
|
const [algorithm] = AlgorithmsByType[Type.username];
|
||||||
|
return { nav: JSON.stringify(algorithm), algorithm };
|
||||||
|
}
|
||||||
|
}),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(activeIdentifier$);
|
.subscribe(activeIdentifier$);
|
||||||
|
|
||||||
this.forwarder.valueChanges
|
this.forwarder.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
map(
|
map((forwarder): CascadeValue => {
|
||||||
(forwarder): CascadeValue =>
|
if (forwarder.nav === NONE_SELECTED) {
|
||||||
forwarder.nav === NONE_SELECTED
|
return { nav: forwarder.nav };
|
||||||
? { nav: forwarder.nav }
|
} else if (forwarder.nav) {
|
||||||
: { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) },
|
return { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) };
|
||||||
),
|
} else {
|
||||||
|
return { nav: NONE_SELECTED };
|
||||||
|
}
|
||||||
|
}),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(activeForwarder$);
|
.subscribe(activeForwarder$);
|
||||||
@@ -276,7 +320,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
map(([username, forwarder]) => {
|
map(([username, forwarder]) => {
|
||||||
const showForwarder = !username.algorithm;
|
const showForwarder = !username.algorithm;
|
||||||
const forwarderId =
|
const forwarderId =
|
||||||
showForwarder && isForwarderIntegration(forwarder.algorithm)
|
showForwarder && forwarder.algorithm && isForwarderExtensionId(forwarder.algorithm)
|
||||||
? forwarder.algorithm.forwarder
|
? forwarder.algorithm.forwarder
|
||||||
: null;
|
: null;
|
||||||
return [showForwarder, forwarderId] as const;
|
return [showForwarder, forwarderId] as const;
|
||||||
@@ -306,57 +350,61 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)),
|
distinctUntilChanged((prev, next) => {
|
||||||
|
if (prev === null || next === null) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return isSameAlgorithm(prev.id, next.id);
|
||||||
|
}
|
||||||
|
}),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe((algorithm) => {
|
.subscribe((algorithm) => {
|
||||||
this.log.debug(algorithm, "algorithm selected");
|
this.log.debug({ algorithm: algorithm?.id ?? null }, "algorithm selected");
|
||||||
|
|
||||||
// update subjects within the angular zone so that the
|
// update subjects within the angular zone so that the
|
||||||
// template bindings refresh immediately
|
// template bindings refresh immediately
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
this.algorithm$.next(algorithm);
|
this.maybeAlgorithm$.next(algorithm);
|
||||||
this.onAlgorithm.next(algorithm);
|
if (algorithm) {
|
||||||
|
this.onAlgorithm.next(toAlgorithmInfo(algorithm, this.i18nService));
|
||||||
|
} else {
|
||||||
|
this.onAlgorithm.next(null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// assume the last-visible generator algorithm is the user's preferred one
|
// assume the last-visible generator algorithm is the user's preferred one
|
||||||
const preferences = await this.generatorService.preferences({ account$: this.account$ });
|
const preferences = await this.generatorService.preferences({ account$: this.account$ });
|
||||||
this.algorithm$
|
this.algorithm$
|
||||||
.pipe(
|
.pipe(withLatestFrom(preferences), takeUntil(this.destroyed))
|
||||||
filter((algorithm) => !!algorithm),
|
|
||||||
withLatestFrom(preferences),
|
|
||||||
takeUntil(this.destroyed),
|
|
||||||
)
|
|
||||||
.subscribe(([algorithm, preference]) => {
|
.subscribe(([algorithm, preference]) => {
|
||||||
if (isEmailAlgorithm(algorithm.id)) {
|
if (isEmailAlgorithm(algorithm.id)) {
|
||||||
this.log.info(
|
|
||||||
{ algorithm, category: CredentialCategories.email },
|
|
||||||
"algorithm preferences updated",
|
|
||||||
);
|
|
||||||
preference.email.algorithm = algorithm.id;
|
preference.email.algorithm = algorithm.id;
|
||||||
preference.email.updated = new Date();
|
preference.email.updated = new Date();
|
||||||
} else if (isUsernameAlgorithm(algorithm.id)) {
|
} else if (isUsernameAlgorithm(algorithm.id)) {
|
||||||
this.log.info(
|
|
||||||
{ algorithm, category: CredentialCategories.username },
|
|
||||||
"algorithm preferences updated",
|
|
||||||
);
|
|
||||||
preference.username.algorithm = algorithm.id;
|
preference.username.algorithm = algorithm.id;
|
||||||
preference.username.updated = new Date();
|
preference.username.updated = new Date();
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.log.info(
|
||||||
|
{ algorithm: algorithm.id, type: algorithm.type },
|
||||||
|
"algorithm preferences updated",
|
||||||
|
);
|
||||||
preferences.next(preference);
|
preferences.next(preference);
|
||||||
});
|
});
|
||||||
|
|
||||||
preferences
|
preferences
|
||||||
.pipe(
|
.pipe(
|
||||||
map(({ email, username }) => {
|
map(({ email, username }) => {
|
||||||
const forwarderPref = isForwarderIntegration(email.algorithm) ? email : null;
|
|
||||||
const usernamePref = email.updated > username.updated ? email : username;
|
const usernamePref = email.updated > username.updated ? email : username;
|
||||||
|
const forwarderPref = isForwarderExtensionId(usernamePref.algorithm)
|
||||||
|
? usernamePref
|
||||||
|
: null;
|
||||||
|
|
||||||
// inject drilldown flags
|
// inject drill-down flags
|
||||||
const forwarderNav = !forwarderPref
|
const forwarderNav = !forwarderPref
|
||||||
? NONE_SELECTED
|
? NONE_SELECTED
|
||||||
: JSON.stringify(forwarderPref.algorithm);
|
: JSON.stringify(forwarderPref.algorithm);
|
||||||
@@ -368,7 +416,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
selection: { nav: userNav },
|
selection: { nav: userNav },
|
||||||
active: {
|
active: {
|
||||||
nav: userNav,
|
nav: userNav,
|
||||||
algorithm: forwarderPref ? null : usernamePref.algorithm,
|
algorithm: forwarderPref ? undefined : usernamePref.algorithm,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
forwarder: {
|
forwarder: {
|
||||||
@@ -385,6 +433,14 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(({ username, forwarder }) => {
|
.subscribe(({ username, forwarder }) => {
|
||||||
|
this.log.debug(
|
||||||
|
{
|
||||||
|
username: username.selection,
|
||||||
|
forwarder: forwarder.selection,
|
||||||
|
},
|
||||||
|
"navigation updated",
|
||||||
|
);
|
||||||
|
|
||||||
// update navigation; break subscription loop
|
// update navigation; break subscription loop
|
||||||
this.username.setValue(username.selection, { emitEvent: false });
|
this.username.setValue(username.selection, { emitEvent: false });
|
||||||
this.forwarder.setValue(forwarder.selection, { emitEvent: false });
|
this.forwarder.setValue(forwarder.selection, { emitEvent: false });
|
||||||
@@ -396,17 +452,16 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
|
|
||||||
// automatically regenerate when the algorithm switches if the algorithm
|
// automatically regenerate when the algorithm switches if the algorithm
|
||||||
// allows it; otherwise set a placeholder
|
// allows it; otherwise set a placeholder
|
||||||
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
this.maybeAlgorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
if (!a || a.onlyOnRequest) {
|
if (a?.capabilities?.autogenerate) {
|
||||||
this.log.debug("autogeneration disabled; clearing generated credential");
|
|
||||||
this.value$.next("-");
|
|
||||||
} else {
|
|
||||||
this.log.debug("autogeneration enabled");
|
this.log.debug("autogeneration enabled");
|
||||||
|
|
||||||
this.generate("autogenerate").catch((e: unknown) => {
|
this.generate("autogenerate").catch((e: unknown) => {
|
||||||
this.log.error(e as object, "a failure occurred during autogeneration");
|
this.log.error(e as object, "a failure occurred during autogeneration");
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.log.debug("autogeneration disabled; clearing generated credential");
|
||||||
|
this.generatedCredential$.next(undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -414,34 +469,6 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
this.log.debug("component initialized");
|
this.log.debug("component initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
private typeToGenerator$(algorithm: CredentialAlgorithm) {
|
|
||||||
const dependencies = {
|
|
||||||
on$: this.generate$,
|
|
||||||
account$: this.account$,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.log.debug({ algorithm }, "constructing generation stream");
|
|
||||||
|
|
||||||
switch (algorithm) {
|
|
||||||
case "catchall":
|
|
||||||
return this.generatorService.generate$(Generators.catchall, dependencies);
|
|
||||||
|
|
||||||
case "subaddress":
|
|
||||||
return this.generatorService.generate$(Generators.subaddress, dependencies);
|
|
||||||
|
|
||||||
case "username":
|
|
||||||
return this.generatorService.generate$(Generators.username, dependencies);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isForwarderIntegration(algorithm)) {
|
|
||||||
const forwarder = getForwarderConfiguration(algorithm.forwarder);
|
|
||||||
const configuration = toCredentialGeneratorConfiguration(forwarder);
|
|
||||||
return this.generatorService.generate$(configuration, dependencies);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log.panic({ algorithm }, `Invalid generator type: "${algorithm}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private announce(message: string) {
|
private announce(message: string) {
|
||||||
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
|
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
|
||||||
}
|
}
|
||||||
@@ -450,7 +477,7 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
protected typeOptions$ = new BehaviorSubject<Option<string>[]>([]);
|
protected typeOptions$ = new BehaviorSubject<Option<string>[]>([]);
|
||||||
|
|
||||||
/** Tracks the currently selected forwarder. */
|
/** Tracks the currently selected forwarder. */
|
||||||
protected forwarderId$ = new BehaviorSubject<IntegrationId>(null);
|
protected forwarderId$ = new BehaviorSubject<VendorId | null>(null);
|
||||||
|
|
||||||
/** Lists the credential types supported by the component. */
|
/** Lists the credential types supported by the component. */
|
||||||
protected forwarderOptions$ = new BehaviorSubject<Option<string>[]>([]);
|
protected forwarderOptions$ = new BehaviorSubject<Option<string>[]>([]);
|
||||||
@@ -458,19 +485,30 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
/** Tracks forwarder control visibility */
|
/** Tracks forwarder control visibility */
|
||||||
protected showForwarder$ = new BehaviorSubject<boolean>(false);
|
protected showForwarder$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
/** tracks the currently selected credential type */
|
/** tracks the currently selected algorithm; emits `null` when no algorithm selected */
|
||||||
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);
|
protected maybeAlgorithm$ = new ReplaySubject<AlgorithmMetadata | null>(1);
|
||||||
|
|
||||||
|
/** tracks the last valid algorithm selection */
|
||||||
|
protected algorithm$ = this.maybeAlgorithm$.pipe(
|
||||||
|
filter((algorithm): algorithm is AlgorithmMetadata => !!algorithm),
|
||||||
|
);
|
||||||
|
|
||||||
/** Emits hint key for the currently selected credential type */
|
/** Emits hint key for the currently selected credential type */
|
||||||
protected credentialTypeHint$ = new ReplaySubject<string>(1);
|
protected credentialTypeHint$ = new ReplaySubject<string>(1);
|
||||||
|
|
||||||
|
private readonly generatedCredential$ = new BehaviorSubject<GeneratedCredential | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
/** Emits the last generated value. */
|
/** Emits the last generated value. */
|
||||||
protected readonly value$ = new BehaviorSubject<string>("");
|
protected readonly value$ = this.generatedCredential$.pipe(
|
||||||
|
map((generated) => generated?.credential ?? "-"),
|
||||||
|
);
|
||||||
|
|
||||||
/** Emits when a new credential is requested */
|
/** Emits when a new credential is requested */
|
||||||
private readonly generate$ = new Subject<GenerateRequest>();
|
private readonly generate$ = new Subject<GenerateRequest>();
|
||||||
|
|
||||||
protected showAlgorithm$ = this.algorithm$.pipe(
|
protected showAlgorithm$ = this.maybeAlgorithm$.pipe(
|
||||||
combineLatestWith(this.showForwarder$),
|
combineLatestWith(this.showForwarder$),
|
||||||
map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)),
|
map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)),
|
||||||
);
|
);
|
||||||
@@ -479,24 +517,21 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
* Emits the copy button aria-label respective of the selected credential type
|
* Emits the copy button aria-label respective of the selected credential type
|
||||||
*/
|
*/
|
||||||
protected credentialTypeCopyLabel$ = this.algorithm$.pipe(
|
protected credentialTypeCopyLabel$ = this.algorithm$.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
map(({ i18nKeys: { copyCredential } }) => translate(copyCredential, this.i18nService)),
|
||||||
map(({ copy }) => copy),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the generate button aria-label respective of the selected credential type
|
* Emits the generate button aria-label respective of the selected credential type
|
||||||
*/
|
*/
|
||||||
protected credentialTypeGenerateLabel$ = this.algorithm$.pipe(
|
protected credentialTypeGenerateLabel$ = this.algorithm$.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
map(({ i18nKeys: { generateCredential } }) => translate(generateCredential, this.i18nService)),
|
||||||
map(({ generate }) => generate),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits the copy credential toast respective of the selected credential type
|
* Emits the copy credential toast respective of the selected credential type
|
||||||
*/
|
*/
|
||||||
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
map(({ i18nKeys: { credentialType } }) => translate(credentialType, this.i18nService)),
|
||||||
map(({ credentialType }) => credentialType),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Identifies generator requests that were requested by the user */
|
/** Identifies generator requests that were requested by the user */
|
||||||
@@ -507,15 +542,20 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
* origin in the debugger.
|
* origin in the debugger.
|
||||||
*/
|
*/
|
||||||
protected async generate(source: string) {
|
protected async generate(source: string) {
|
||||||
const request = { source, website: this.website };
|
const algorithm = await firstValueFrom(this.algorithm$);
|
||||||
|
const request: GenerateRequest = { source, algorithm: algorithm.id };
|
||||||
|
if (this.website) {
|
||||||
|
request.website = this.website;
|
||||||
|
}
|
||||||
|
|
||||||
this.log.debug(request, "generation requested");
|
this.log.debug(request, "generation requested");
|
||||||
this.generate$.next(request);
|
this.generate$.next(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toOptions(algorithms: AlgorithmInfo[]) {
|
private toOptions(algorithms: AlgorithmMetadata[]) {
|
||||||
const options: Option<string>[] = algorithms.map((algorithm) => ({
|
const options: Option<string>[] = algorithms.map((algorithm) => ({
|
||||||
value: JSON.stringify(algorithm.id),
|
value: JSON.stringify(algorithm.id),
|
||||||
label: algorithm.name,
|
label: translate(algorithm.i18nKeys.name, this.i18nService),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
@@ -528,9 +568,11 @@ export class UsernameGeneratorComponent implements OnInit, OnChanges, OnDestroy
|
|||||||
|
|
||||||
// finalize subjects
|
// finalize subjects
|
||||||
this.generate$.complete();
|
this.generate$.complete();
|
||||||
this.value$.complete();
|
this.generatedCredential$.complete();
|
||||||
|
|
||||||
// finalize component bindings
|
// finalize component bindings
|
||||||
this.onGenerated.complete();
|
this.onGenerated.complete();
|
||||||
|
|
||||||
|
this.log.debug("component destroyed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@@ -17,7 +15,7 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
|||||||
import {
|
import {
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
EffUsernameGenerationOptions,
|
EffUsernameGenerationOptions,
|
||||||
Generators,
|
BuiltIn,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
/** Options group for usernames */
|
/** Options group for usernames */
|
||||||
@@ -28,7 +26,6 @@ import {
|
|||||||
})
|
})
|
||||||
export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
/** Instantiates the component
|
/** Instantiates the component
|
||||||
* @param accountService queries user availability
|
|
||||||
* @param generatorService settings and policy logic
|
* @param generatorService settings and policy logic
|
||||||
* @param formBuilder reactive form controls
|
* @param formBuilder reactive form controls
|
||||||
*/
|
*/
|
||||||
@@ -38,9 +35,11 @@ export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Binds the component to a specific user's settings.
|
/** Binds the component to a specific user's settings.
|
||||||
|
* @remarks this is initialized to null but since it's a required input it'll
|
||||||
|
* never have that value in practice.
|
||||||
*/
|
*/
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
account: Account;
|
account: Account = null!;
|
||||||
|
|
||||||
protected account$ = new ReplaySubject<Account>(1);
|
protected account$ = new ReplaySubject<Account>(1);
|
||||||
|
|
||||||
@@ -53,19 +52,19 @@ export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
/** Emits settings updates and completes if the settings become unavailable.
|
/** Emits settings updates and completes if the settings become unavailable.
|
||||||
* @remarks this does not emit the initial settings. If you would like
|
* @remarks this does not emit the initial settings. If you would like
|
||||||
* to receive live settings updates including the initial update,
|
* to receive live settings updates including the initial update,
|
||||||
* use `CredentialGeneratorService.settings$(...)` instead.
|
* use `CredentialGeneratorService.settings(...)` instead.
|
||||||
*/
|
*/
|
||||||
@Output()
|
@Output()
|
||||||
readonly onUpdated = new EventEmitter<EffUsernameGenerationOptions>();
|
readonly onUpdated = new EventEmitter<EffUsernameGenerationOptions>();
|
||||||
|
|
||||||
/** The template's control bindings */
|
/** The template's control bindings */
|
||||||
protected settings = this.formBuilder.group({
|
protected settings = this.formBuilder.group({
|
||||||
wordCapitalize: [Generators.username.settings.initial.wordCapitalize],
|
wordCapitalize: [false],
|
||||||
wordIncludeNumber: [Generators.username.settings.initial.wordIncludeNumber],
|
wordIncludeNumber: [false],
|
||||||
});
|
});
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const settings = await this.generatorService.settings(Generators.username, {
|
const settings = await this.generatorService.settings(BuiltIn.effWordList, {
|
||||||
account$: this.account$,
|
account$: this.account$,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,7 +78,7 @@ export class UsernameSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.saveSettings
|
this.saveSettings
|
||||||
.pipe(
|
.pipe(
|
||||||
withLatestFrom(this.settings.valueChanges),
|
withLatestFrom(this.settings.valueChanges),
|
||||||
map(([, settings]) => settings),
|
map(([, settings]) => settings as EffUsernameGenerationOptions),
|
||||||
takeUntil(this.destroyed$),
|
takeUntil(this.destroyed$),
|
||||||
)
|
)
|
||||||
.subscribe(settings);
|
.subscribe(settings);
|
||||||
|
|||||||
@@ -1,71 +1,46 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
// @ts-strict-ignore
|
import { I18nKeyOrLiteral } from "@bitwarden/common/tools/types";
|
||||||
import { ValidatorFn, Validators } from "@angular/forms";
|
import { isI18nKey } from "@bitwarden/common/tools/util";
|
||||||
import { distinctUntilChanged, map, pairwise, pipe, skipWhile, startWith, takeWhile } from "rxjs";
|
import { AlgorithmInfo, AlgorithmMetadata } from "@bitwarden/generator-core";
|
||||||
|
|
||||||
import { AnyConstraint, Constraints } from "@bitwarden/common/tools/types";
|
/** Adapts {@link AlgorithmMetadata} to legacy {@link AlgorithmInfo} structure. */
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
export function toAlgorithmInfo(metadata: AlgorithmMetadata, i18n: I18nService) {
|
||||||
import { CredentialGeneratorConfiguration } from "@bitwarden/generator-core";
|
const info: AlgorithmInfo = {
|
||||||
|
id: metadata.id,
|
||||||
|
type: metadata.type,
|
||||||
|
name: translate(metadata.i18nKeys.name, i18n),
|
||||||
|
generate: translate(metadata.i18nKeys.generateCredential, i18n),
|
||||||
|
onGeneratedMessage: translate(metadata.i18nKeys.credentialGenerated, i18n),
|
||||||
|
credentialType: translate(metadata.i18nKeys.credentialType, i18n),
|
||||||
|
copy: translate(metadata.i18nKeys.copyCredential, i18n),
|
||||||
|
useGeneratedValue: translate(metadata.i18nKeys.useCredential, i18n),
|
||||||
|
onlyOnRequest: !metadata.capabilities.autogenerate,
|
||||||
|
request: metadata.capabilities.fields,
|
||||||
|
};
|
||||||
|
|
||||||
export function completeOnAccountSwitch() {
|
if (metadata.i18nKeys.description) {
|
||||||
return pipe(
|
info.description = translate(metadata.i18nKeys.description, i18n);
|
||||||
map(({ id }: { id: UserId | null }) => id),
|
}
|
||||||
skipWhile((id) => !id),
|
|
||||||
startWith(null as UserId),
|
return info;
|
||||||
pairwise(),
|
|
||||||
takeWhile(([prev, next]) => (prev ?? next) === next),
|
|
||||||
map(([_, id]) => id),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toValidators<Policy, Settings>(
|
/** Translates an internationalization key
|
||||||
target: keyof Settings,
|
* @param key the key to translate
|
||||||
configuration: CredentialGeneratorConfiguration<Settings, Policy>,
|
* @param i18n the service providing translations
|
||||||
policy?: Constraints<Settings>,
|
* @returns the translated key; if the key is a literal the literal
|
||||||
) {
|
* is returned instead.
|
||||||
const validators: Array<ValidatorFn> = [];
|
*/
|
||||||
|
export function translate(key: I18nKeyOrLiteral, i18n: I18nService) {
|
||||||
// widen the types to avoid typecheck issues
|
return isI18nKey(key) ? i18n.t(key) : key.literal;
|
||||||
const config: AnyConstraint = configuration.settings.constraints[target];
|
|
||||||
const runtime: AnyConstraint = policy[target];
|
|
||||||
|
|
||||||
const required = getConstraint("required", config, runtime) ?? false;
|
|
||||||
if (required) {
|
|
||||||
validators.push(Validators.required);
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxLength = getConstraint("maxLength", config, runtime);
|
|
||||||
if (maxLength !== undefined) {
|
|
||||||
validators.push(Validators.maxLength(maxLength));
|
|
||||||
}
|
|
||||||
|
|
||||||
const minLength = getConstraint("minLength", config, runtime);
|
|
||||||
if (minLength !== undefined) {
|
|
||||||
validators.push(Validators.minLength(config.minLength));
|
|
||||||
}
|
|
||||||
|
|
||||||
const min = getConstraint("min", config, runtime);
|
|
||||||
if (min !== undefined) {
|
|
||||||
validators.push(Validators.min(min));
|
|
||||||
}
|
|
||||||
|
|
||||||
const max = getConstraint("max", config, runtime);
|
|
||||||
if (max !== undefined) {
|
|
||||||
validators.push(Validators.max(max));
|
|
||||||
}
|
|
||||||
|
|
||||||
return validators;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConstraint<Key extends keyof AnyConstraint>(
|
/** Returns true when min < max
|
||||||
key: Key,
|
* @param min the minimum value to check; when this is nullish it becomes 0.
|
||||||
config: AnyConstraint,
|
* @param max the maximum value to check; when this is nullish it becomes +Infinity.
|
||||||
policy?: AnyConstraint,
|
*/
|
||||||
) {
|
export function hasRangeOfValues(min?: number, max?: number) {
|
||||||
if (policy && key in policy) {
|
const minimum = min ?? 0;
|
||||||
return policy[key] ?? config[key];
|
const maximum = max ?? Number.POSITIVE_INFINITY;
|
||||||
} else if (config && key in config) {
|
return minimum < maximum;
|
||||||
return config[key];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
/** Generates credentials used for user authentication
|
||||||
* @typeParam Options the credential generation configuration
|
* @typeParam Options the credential generation configuration
|
||||||
* @typeParam Policy the policy enforced by the generator
|
* @typeParam Policy the policy enforced by the generator
|
||||||
|
* @deprecated Use {@link CredentialGeneratorService} instead.
|
||||||
*/
|
*/
|
||||||
export abstract class GeneratorService<Options, Policy> {
|
export abstract class GeneratorService<Options, Policy> {
|
||||||
/** An observable monitoring the options saved to disk.
|
/** 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 { GeneratorService } from "./generator.service.abstraction";
|
||||||
export { GeneratorStrategy } from "./generator-strategy.abstraction";
|
export { GeneratorStrategy } from "./generator-strategy.abstraction";
|
||||||
export { PolicyEvaluator } from "./policy-evaluator.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,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-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-passphrase-boundaries";
|
||||||
export * from "./default-password-boundaries";
|
export * from "./default-password-boundaries";
|
||||||
export * from "./default-eff-username-options";
|
export * from "./default-eff-username-options";
|
||||||
export * from "./default-firefox-relay-options";
|
|
||||||
export * from "./default-passphrase-generation-options";
|
export * from "./default-passphrase-generation-options";
|
||||||
export * from "./default-password-generation-options";
|
export * from "./default-password-generation-options";
|
||||||
export * from "./default-credential-preferences";
|
|
||||||
export * from "./default-subaddress-generator-options";
|
export * from "./default-subaddress-generator-options";
|
||||||
export * from "./default-simple-login-options";
|
|
||||||
export * from "./forwarders";
|
|
||||||
export * from "./integrations";
|
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 { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||||
|
|
||||||
|
import { Algorithm, Type } from "../metadata";
|
||||||
|
|
||||||
import { Randomizer } from "./abstractions";
|
import { Randomizer } from "./abstractions";
|
||||||
import { EmailRandomizer } from "./email-randomizer";
|
import { EmailRandomizer } from "./email-randomizer";
|
||||||
|
|
||||||
@@ -41,7 +43,8 @@ describe("EmailRandomizer", () => {
|
|||||||
async (email) => {
|
async (email) => {
|
||||||
const emailRandomizer = new EmailRandomizer(randomizer);
|
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("");
|
expect(result).toEqual("");
|
||||||
},
|
},
|
||||||
@@ -100,7 +103,8 @@ describe("EmailRandomizer", () => {
|
|||||||
it.each([[null], [undefined], [""]])("returns null if the domain is %p", async (domain) => {
|
it.each([[null], [undefined], [""]])("returns null if the domain is %p", async (domain) => {
|
||||||
const emailRandomizer = new EmailRandomizer(randomizer);
|
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();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -150,7 +154,8 @@ describe("EmailRandomizer", () => {
|
|||||||
it.each([[null], [undefined], [""]])("returns null if the domain is %p", async (domain) => {
|
it.each([[null], [undefined], [""]])("returns null if the domain is %p", async (domain) => {
|
||||||
const emailRandomizer = new EmailRandomizer(randomizer);
|
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();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -214,32 +219,32 @@ describe("EmailRandomizer", () => {
|
|||||||
const email = new EmailRandomizer(randomizer);
|
const email = new EmailRandomizer(randomizer);
|
||||||
|
|
||||||
const result = await email.generate(
|
const result = await email.generate(
|
||||||
{},
|
{ algorithm: Algorithm.catchall },
|
||||||
{
|
{
|
||||||
catchallDomain: "example.com",
|
catchallDomain: "example.com",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.category).toEqual("catchall");
|
expect(result.category).toEqual(Type.email);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("processes subaddress generation options", async () => {
|
it("processes subaddress generation options", async () => {
|
||||||
const email = new EmailRandomizer(randomizer);
|
const email = new EmailRandomizer(randomizer);
|
||||||
|
|
||||||
const result = await email.generate(
|
const result = await email.generate(
|
||||||
{},
|
{ algorithm: Algorithm.plusAddress },
|
||||||
{
|
{
|
||||||
subaddressEmail: "foo@example.com",
|
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 () => {
|
it("throws when it cannot recognize the options type", async () => {
|
||||||
const email = new EmailRandomizer(randomizer);
|
const email = new EmailRandomizer(randomizer);
|
||||||
|
|
||||||
const result = email.generate({}, {});
|
const result = email.generate({ algorithm: Algorithm.password }, {});
|
||||||
|
|
||||||
await expect(result).rejects.toBeInstanceOf(Error);
|
await expect(result).rejects.toBeInstanceOf(Error);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||||
|
|
||||||
|
import { Type } from "../metadata";
|
||||||
import {
|
import {
|
||||||
CatchallGenerationOptions,
|
CatchallGenerationOptions,
|
||||||
CredentialGenerator,
|
CredentialGenerator,
|
||||||
@@ -128,7 +129,7 @@ export class EmailRandomizer
|
|||||||
|
|
||||||
return new GeneratedCredential(
|
return new GeneratedCredential(
|
||||||
email,
|
email,
|
||||||
"catchall",
|
Type.email,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
request.source,
|
request.source,
|
||||||
request.website,
|
request.website,
|
||||||
@@ -138,7 +139,7 @@ export class EmailRandomizer
|
|||||||
|
|
||||||
return new GeneratedCredential(
|
return new GeneratedCredential(
|
||||||
email,
|
email,
|
||||||
"subaddress",
|
Type.email,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
request.source,
|
request.source,
|
||||||
request.website,
|
request.website,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "@bitwarden/common/tools/integration/rpc";
|
} from "@bitwarden/common/tools/integration/rpc";
|
||||||
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
|
import { Type } from "../metadata";
|
||||||
import { CredentialGenerator, GeneratedCredential } from "../types";
|
import { CredentialGenerator, GeneratedCredential } from "../types";
|
||||||
|
|
||||||
import { AccountRequest, ForwarderConfiguration } from "./forwarder-configuration";
|
import { AccountRequest, ForwarderConfiguration } from "./forwarder-configuration";
|
||||||
@@ -40,9 +41,8 @@ export class Forwarder implements CredentialGenerator<ApiSettings> {
|
|||||||
|
|
||||||
const create = this.createForwardingAddress(this.configuration, settings);
|
const create = this.createForwardingAddress(this.configuration, settings);
|
||||||
const result = await this.client.fetchJson(create, requestOptions);
|
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>(
|
private createContext<Settings>(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended";
|
|||||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||||
|
|
||||||
import { Randomizer } from "../abstractions";
|
import { Randomizer } from "../abstractions";
|
||||||
|
import { Algorithm, Type } from "../metadata";
|
||||||
|
|
||||||
import { Ascii } from "./data";
|
import { Ascii } from "./data";
|
||||||
import { PasswordRandomizer } from "./password-randomizer";
|
import { PasswordRandomizer } from "./password-randomizer";
|
||||||
@@ -341,32 +342,32 @@ describe("PasswordRandomizer", () => {
|
|||||||
const password = new PasswordRandomizer(randomizer);
|
const password = new PasswordRandomizer(randomizer);
|
||||||
|
|
||||||
const result = await password.generate(
|
const result = await password.generate(
|
||||||
{},
|
{ algorithm: Algorithm.password },
|
||||||
{
|
{
|
||||||
length: 10,
|
length: 10,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.category).toEqual("password");
|
expect(result.category).toEqual(Type.password);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("processes passphrase generation options", async () => {
|
it("processes passphrase generation options", async () => {
|
||||||
const password = new PasswordRandomizer(randomizer);
|
const password = new PasswordRandomizer(randomizer);
|
||||||
|
|
||||||
const result = await password.generate(
|
const result = await password.generate(
|
||||||
{},
|
{ algorithm: Algorithm.passphrase },
|
||||||
{
|
{
|
||||||
numWords: 10,
|
numWords: 10,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.category).toEqual("passphrase");
|
expect(result.category).toEqual(Type.password);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when it cannot recognize the options type", async () => {
|
it("throws when it cannot recognize the options type", async () => {
|
||||||
const password = new PasswordRandomizer(randomizer);
|
const password = new PasswordRandomizer(randomizer);
|
||||||
|
|
||||||
const result = password.generate({}, {});
|
const result = password.generate({ algorithm: Algorithm.username }, {});
|
||||||
|
|
||||||
await expect(result).rejects.toBeInstanceOf(Error);
|
await expect(result).rejects.toBeInstanceOf(Error);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||||
|
|
||||||
|
import { Type } from "../metadata";
|
||||||
import {
|
import {
|
||||||
CredentialGenerator,
|
CredentialGenerator,
|
||||||
GenerateRequest,
|
GenerateRequest,
|
||||||
@@ -86,7 +87,7 @@ export class PasswordRandomizer
|
|||||||
|
|
||||||
return new GeneratedCredential(
|
return new GeneratedCredential(
|
||||||
password,
|
password,
|
||||||
"password",
|
Type.password,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
request.source,
|
request.source,
|
||||||
request.website,
|
request.website,
|
||||||
@@ -97,7 +98,7 @@ export class PasswordRandomizer
|
|||||||
|
|
||||||
return new GeneratedCredential(
|
return new GeneratedCredential(
|
||||||
passphrase,
|
passphrase,
|
||||||
"passphrase",
|
Type.password,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
request.source,
|
request.source,
|
||||||
request.website,
|
request.website,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { mock } from "jest-mock-extended";
|
|||||||
|
|
||||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||||
|
|
||||||
|
import { Algorithm, Type } from "../metadata";
|
||||||
|
|
||||||
import { Randomizer } from "./abstractions";
|
import { Randomizer } from "./abstractions";
|
||||||
import { UsernameRandomizer } from "./username-randomizer";
|
import { UsernameRandomizer } from "./username-randomizer";
|
||||||
|
|
||||||
@@ -108,19 +110,19 @@ describe("UsernameRandomizer", () => {
|
|||||||
const username = new UsernameRandomizer(randomizer);
|
const username = new UsernameRandomizer(randomizer);
|
||||||
|
|
||||||
const result = await username.generate(
|
const result = await username.generate(
|
||||||
{},
|
{ algorithm: Algorithm.username },
|
||||||
{
|
{
|
||||||
wordIncludeNumber: true,
|
wordIncludeNumber: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.category).toEqual("username");
|
expect(result.category).toEqual(Type.username);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when it cannot recognize the options type", async () => {
|
it("throws when it cannot recognize the options type", async () => {
|
||||||
const username = new UsernameRandomizer(randomizer);
|
const username = new UsernameRandomizer(randomizer);
|
||||||
|
|
||||||
const result = username.generate({}, {});
|
const result = username.generate({ algorithm: Algorithm.passphrase }, {});
|
||||||
|
|
||||||
await expect(result).rejects.toBeInstanceOf(Error);
|
await expect(result).rejects.toBeInstanceOf(Error);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,13 +3,34 @@ export * from "./abstractions";
|
|||||||
export * from "./data";
|
export * from "./data";
|
||||||
export { createRandomizer } from "./factories";
|
export { createRandomizer } from "./factories";
|
||||||
export * from "./types";
|
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
|
// These internal interfacess are exposed for use by other generator modules
|
||||||
// They are unstable and may change arbitrarily
|
// They are unstable and may change arbitrarily
|
||||||
export * as engine from "./engine";
|
export * as engine from "./engine";
|
||||||
export * as integration from "./integration";
|
export * as integration from "./integration";
|
||||||
export * as policies from "./policies";
|
export * as policies from "./policies";
|
||||||
|
export * as providers from "./providers";
|
||||||
export * as rx from "./rx";
|
export * as rx from "./rx";
|
||||||
export * as services from "./services";
|
export * as services from "./services";
|
||||||
export * as strategies from "./strategies";
|
export * as strategies from "./strategies";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
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 { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import {
|
import {
|
||||||
ApiSettings,
|
ApiSettings,
|
||||||
@@ -101,7 +102,7 @@ const forwarder = Object.freeze({
|
|||||||
|
|
||||||
export const AddyIo = Object.freeze({
|
export const AddyIo = Object.freeze({
|
||||||
// integration
|
// integration
|
||||||
id: "anonaddy" as IntegrationId & VendorId,
|
id: Vendor.addyio as IntegrationId & VendorId,
|
||||||
name: "Addy.io",
|
name: "Addy.io",
|
||||||
extends: ["forwarder"],
|
extends: ["forwarder"],
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
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 { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||||
@@ -90,7 +91,7 @@ const forwarder = Object.freeze({
|
|||||||
|
|
||||||
// integration-wide configuration
|
// integration-wide configuration
|
||||||
export const DuckDuckGo = Object.freeze({
|
export const DuckDuckGo = Object.freeze({
|
||||||
id: "duckduckgo" as IntegrationId & VendorId,
|
id: Vendor.duckduckgo as IntegrationId & VendorId,
|
||||||
name: "DuckDuckGo",
|
name: "DuckDuckGo",
|
||||||
baseUrl: "https://quack.duckduckgo.com/api",
|
baseUrl: "https://quack.duckduckgo.com/api",
|
||||||
selfHost: "never",
|
selfHost: "never",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
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 { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||||
@@ -160,7 +161,7 @@ const forwarder = Object.freeze({
|
|||||||
|
|
||||||
// integration-wide configuration
|
// integration-wide configuration
|
||||||
export const Fastmail = Object.freeze({
|
export const Fastmail = Object.freeze({
|
||||||
id: "fastmail" as IntegrationId & VendorId,
|
id: Vendor.fastmail as IntegrationId & VendorId,
|
||||||
name: "Fastmail",
|
name: "Fastmail",
|
||||||
baseUrl: "https://api.fastmail.com",
|
baseUrl: "https://api.fastmail.com",
|
||||||
selfHost: "maybe",
|
selfHost: "maybe",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
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 { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||||
@@ -98,7 +99,7 @@ const forwarder = Object.freeze({
|
|||||||
|
|
||||||
// integration-wide configuration
|
// integration-wide configuration
|
||||||
export const FirefoxRelay = Object.freeze({
|
export const FirefoxRelay = Object.freeze({
|
||||||
id: "firefoxrelay" as IntegrationId & VendorId,
|
id: Vendor.mozilla as IntegrationId & VendorId,
|
||||||
name: "Firefox Relay",
|
name: "Firefox Relay",
|
||||||
baseUrl: "https://relay.firefox.com/api",
|
baseUrl: "https://relay.firefox.com/api",
|
||||||
selfHost: "never",
|
selfHost: "never",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
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 { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
|
||||||
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier";
|
||||||
@@ -102,7 +103,7 @@ const forwarder = Object.freeze({
|
|||||||
|
|
||||||
export const ForwardEmail = Object.freeze({
|
export const ForwardEmail = Object.freeze({
|
||||||
// integration metadata
|
// integration metadata
|
||||||
id: "forwardemail" as IntegrationId & VendorId,
|
id: Vendor.forwardemail as IntegrationId & VendorId,
|
||||||
name: "Forward Email",
|
name: "Forward Email",
|
||||||
extends: ["forwarder"],
|
extends: ["forwarder"],
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
import { VendorId } from "@bitwarden/common/tools/extension";
|
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 { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration";
|
||||||
import {
|
import {
|
||||||
ApiSettings,
|
ApiSettings,
|
||||||
@@ -104,7 +105,7 @@ const forwarder = Object.freeze({
|
|||||||
|
|
||||||
// integration-wide configuration
|
// integration-wide configuration
|
||||||
export const SimpleLogin = Object.freeze({
|
export const SimpleLogin = Object.freeze({
|
||||||
id: "simplelogin" as IntegrationId & VendorId,
|
id: Vendor.simplelogin as IntegrationId & VendorId,
|
||||||
name: "SimpleLogin",
|
name: "SimpleLogin",
|
||||||
selfHost: "maybe",
|
selfHost: "maybe",
|
||||||
extends: ["forwarder"],
|
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 */
|
/** Credential generator metadata common across credential generators */
|
||||||
export type AlgorithmMetadata = {
|
export type AlgorithmMetadata = {
|
||||||
@@ -14,7 +14,7 @@ export type AlgorithmMetadata = {
|
|||||||
id: CredentialAlgorithm;
|
id: CredentialAlgorithm;
|
||||||
|
|
||||||
/** The kind of credential generated by this configuration */
|
/** The kind of credential generated by this configuration */
|
||||||
category: CredentialType;
|
type: CredentialType;
|
||||||
|
|
||||||
/** Used to order credential algorithms for display purposes.
|
/** Used to order credential algorithms for display purposes.
|
||||||
* Items with lesser weights appear before entries with greater
|
* Items with lesser weights appear before entries with greater
|
||||||
@@ -23,6 +23,10 @@ export type AlgorithmMetadata = {
|
|||||||
weight: number;
|
weight: number;
|
||||||
|
|
||||||
/** Localization keys */
|
/** 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: {
|
i18nKeys: {
|
||||||
/** descriptive name of the algorithm */
|
/** descriptive name of the algorithm */
|
||||||
name: I18nKeyOrLiteral;
|
name: I18nKeyOrLiteral;
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { mock } from "jest-mock-extended";
|
|||||||
|
|
||||||
import { EmailRandomizer } from "../../engine";
|
import { EmailRandomizer } from "../../engine";
|
||||||
import { CatchallConstraints } from "../../policies/catchall-constraints";
|
import { CatchallConstraints } from "../../policies/catchall-constraints";
|
||||||
import { CatchallGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
import { GeneratorDependencyProvider } from "../../providers";
|
||||||
|
import { CatchallGenerationOptions } from "../../types";
|
||||||
import { Profile } from "../data";
|
import { Profile } from "../data";
|
||||||
import { CoreProfileMetadata } from "../profile-metadata";
|
import { CoreProfileMetadata } from "../profile-metadata";
|
||||||
import { isCoreProfile } from "../util";
|
import { isCoreProfile } from "../util";
|
||||||
|
|||||||
@@ -4,17 +4,14 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
|
|||||||
|
|
||||||
import { EmailRandomizer } from "../../engine";
|
import { EmailRandomizer } from "../../engine";
|
||||||
import { CatchallConstraints } from "../../policies/catchall-constraints";
|
import { CatchallConstraints } from "../../policies/catchall-constraints";
|
||||||
import {
|
import { GeneratorDependencyProvider } from "../../providers";
|
||||||
CatchallGenerationOptions,
|
import { CatchallGenerationOptions, CredentialGenerator } from "../../types";
|
||||||
CredentialGenerator,
|
|
||||||
GeneratorDependencyProvider,
|
|
||||||
} from "../../types";
|
|
||||||
import { Algorithm, Type, Profile } from "../data";
|
import { Algorithm, Type, Profile } from "../data";
|
||||||
import { GeneratorMetadata } from "../generator-metadata";
|
import { GeneratorMetadata } from "../generator-metadata";
|
||||||
|
|
||||||
const catchall: GeneratorMetadata<CatchallGenerationOptions> = deepFreeze({
|
const catchall: GeneratorMetadata<CatchallGenerationOptions> = deepFreeze({
|
||||||
id: Algorithm.catchall,
|
id: Algorithm.catchall,
|
||||||
category: Type.email,
|
type: Type.email,
|
||||||
weight: 210,
|
weight: 210,
|
||||||
i18nKeys: {
|
i18nKeys: {
|
||||||
name: "catchallEmail",
|
name: "catchallEmail",
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
import { ExtensionMetadata, ExtensionStorageKey } from "@bitwarden/common/tools/extension/type";
|
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 { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||||
|
|
||||||
import { getForwarderConfiguration } from "../../data";
|
import { getForwarderConfiguration } from "../../data";
|
||||||
import { EmailDomainSettings, EmailPrefixSettings } from "../../engine";
|
|
||||||
import { Forwarder } from "../../engine/forwarder";
|
import { Forwarder } from "../../engine/forwarder";
|
||||||
import { GeneratorDependencyProvider } from "../../types";
|
import { GeneratorDependencyProvider } from "../../providers";
|
||||||
|
import { ForwarderOptions } from "../../types";
|
||||||
import { Profile, Type } from "../data";
|
import { Profile, Type } from "../data";
|
||||||
import { GeneratorMetadata } from "../generator-metadata";
|
import { GeneratorMetadata } from "../generator-metadata";
|
||||||
import { ForwarderProfileMetadata } from "../profile-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
|
// update the extension metadata
|
||||||
export function toForwarderMetadata(
|
export function toForwarderMetadata(
|
||||||
extension: ExtensionMetadata,
|
extension: ExtensionMetadata,
|
||||||
@@ -28,7 +23,7 @@ export function toForwarderMetadata(
|
|||||||
|
|
||||||
const generator: GeneratorMetadata<ForwarderOptions> = {
|
const generator: GeneratorMetadata<ForwarderOptions> = {
|
||||||
id: { forwarder: extension.product.vendor.id },
|
id: { forwarder: extension.product.vendor.id },
|
||||||
category: Type.email,
|
type: Type.email,
|
||||||
weight: 300,
|
weight: 300,
|
||||||
i18nKeys: {
|
i18nKeys: {
|
||||||
name,
|
name,
|
||||||
@@ -56,6 +51,12 @@ export function toForwarderMetadata(
|
|||||||
storage: {
|
storage: {
|
||||||
key: "forwarder",
|
key: "forwarder",
|
||||||
frame: 512,
|
frame: 512,
|
||||||
|
initial: {
|
||||||
|
token: "",
|
||||||
|
baseUrl: "",
|
||||||
|
domain: "",
|
||||||
|
prefix: "",
|
||||||
|
},
|
||||||
options: {
|
options: {
|
||||||
deserializer: (value) => value,
|
deserializer: (value) => value,
|
||||||
clearOn: ["logout"],
|
clearOn: ["logout"],
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { mock } from "jest-mock-extended";
|
|||||||
|
|
||||||
import { EmailRandomizer } from "../../engine";
|
import { EmailRandomizer } from "../../engine";
|
||||||
import { SubaddressConstraints } from "../../policies/subaddress-constraints";
|
import { SubaddressConstraints } from "../../policies/subaddress-constraints";
|
||||||
import { SubaddressGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
import { GeneratorDependencyProvider } from "../../providers";
|
||||||
|
import { SubaddressGenerationOptions } from "../../types";
|
||||||
import { Profile } from "../data";
|
import { Profile } from "../data";
|
||||||
import { CoreProfileMetadata } from "../profile-metadata";
|
import { CoreProfileMetadata } from "../profile-metadata";
|
||||||
import { isCoreProfile } from "../util";
|
import { isCoreProfile } from "../util";
|
||||||
|
|||||||
@@ -4,17 +4,14 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
|
|||||||
|
|
||||||
import { EmailRandomizer } from "../../engine";
|
import { EmailRandomizer } from "../../engine";
|
||||||
import { SubaddressConstraints } from "../../policies/subaddress-constraints";
|
import { SubaddressConstraints } from "../../policies/subaddress-constraints";
|
||||||
import {
|
import { GeneratorDependencyProvider } from "../../providers";
|
||||||
CredentialGenerator,
|
import { CredentialGenerator, SubaddressGenerationOptions } from "../../types";
|
||||||
GeneratorDependencyProvider,
|
|
||||||
SubaddressGenerationOptions,
|
|
||||||
} from "../../types";
|
|
||||||
import { Algorithm, Profile, Type } from "../data";
|
import { Algorithm, Profile, Type } from "../data";
|
||||||
import { GeneratorMetadata } from "../generator-metadata";
|
import { GeneratorMetadata } from "../generator-metadata";
|
||||||
|
|
||||||
const plusAddress: GeneratorMetadata<SubaddressGenerationOptions> = deepFreeze({
|
const plusAddress: GeneratorMetadata<SubaddressGenerationOptions> = deepFreeze({
|
||||||
id: Algorithm.plusAddress,
|
id: Algorithm.plusAddress,
|
||||||
category: Type.email,
|
type: Type.email,
|
||||||
weight: 200,
|
weight: 200,
|
||||||
i18nKeys: {
|
i18nKeys: {
|
||||||
name: "plusAddressedEmail",
|
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 { AlgorithmMetadata } from "./algorithm-metadata";
|
||||||
import { Profile } from "./data";
|
import { Profile } from "./data";
|
||||||
|
|||||||
@@ -3,7 +3,32 @@ import {
|
|||||||
AlgorithmsByType as AlgorithmsByTypeData,
|
AlgorithmsByType as AlgorithmsByTypeData,
|
||||||
Type as TypeData,
|
Type as TypeData,
|
||||||
} from "./data";
|
} 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 { 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
|
// `CredentialAlgorithm` is defined in terms of `ABT`; supplying
|
||||||
// type information in the barrel file breaks a circular dependency.
|
// type information in the barrel file breaks a circular dependency.
|
||||||
@@ -12,14 +37,29 @@ export const AlgorithmsByType: Record<
|
|||||||
CredentialType,
|
CredentialType,
|
||||||
ReadonlyArray<CredentialAlgorithm>
|
ReadonlyArray<CredentialAlgorithm>
|
||||||
> = AlgorithmsByTypeData;
|
> = 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(
|
export const Algorithms: ReadonlyArray<CredentialAlgorithm> = Object.freeze(
|
||||||
Object.values(AlgorithmData),
|
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 const Types: ReadonlyArray<CredentialType> = Object.freeze(Object.values(TypeData));
|
||||||
|
|
||||||
export { Profile, Type, Algorithm } from "./data";
|
export { Profile, Type, Algorithm } from "./data";
|
||||||
export { toForwarderMetadata } from "./email/forwarder";
|
export { toForwarderMetadata } from "./email/forwarder";
|
||||||
|
export { AlgorithmMetadata } from "./algorithm-metadata";
|
||||||
export { GeneratorMetadata } from "./generator-metadata";
|
export { GeneratorMetadata } from "./generator-metadata";
|
||||||
export { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "./profile-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";
|
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 { PasswordRandomizer } from "../../engine";
|
||||||
import { PassphrasePolicyConstraints } from "../../policies";
|
import { PassphrasePolicyConstraints } from "../../policies";
|
||||||
import { PassphraseGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
import { GeneratorDependencyProvider } from "../../providers";
|
||||||
|
import { PassphraseGenerationOptions } from "../../types";
|
||||||
import { Profile } from "../data";
|
import { Profile } from "../data";
|
||||||
import { CoreProfileMetadata } from "../profile-metadata";
|
import { CoreProfileMetadata } from "../profile-metadata";
|
||||||
import { isCoreProfile } from "../util";
|
import { isCoreProfile } from "../util";
|
||||||
|
|||||||
@@ -5,17 +5,14 @@ import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
|
|||||||
|
|
||||||
import { PasswordRandomizer } from "../../engine";
|
import { PasswordRandomizer } from "../../engine";
|
||||||
import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies";
|
import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies";
|
||||||
import {
|
import { GeneratorDependencyProvider } from "../../providers";
|
||||||
CredentialGenerator,
|
import { CredentialGenerator, PassphraseGenerationOptions } from "../../types";
|
||||||
GeneratorDependencyProvider,
|
|
||||||
PassphraseGenerationOptions,
|
|
||||||
} from "../../types";
|
|
||||||
import { Algorithm, Profile, Type } from "../data";
|
import { Algorithm, Profile, Type } from "../data";
|
||||||
import { GeneratorMetadata } from "../generator-metadata";
|
import { GeneratorMetadata } from "../generator-metadata";
|
||||||
|
|
||||||
const passphrase: GeneratorMetadata<PassphraseGenerationOptions> = {
|
const passphrase: GeneratorMetadata<PassphraseGenerationOptions> = {
|
||||||
id: Algorithm.passphrase,
|
id: Algorithm.passphrase,
|
||||||
category: Type.password,
|
type: Type.password,
|
||||||
weight: 110,
|
weight: 110,
|
||||||
i18nKeys: {
|
i18nKeys: {
|
||||||
name: "passphrase",
|
name: "passphrase",
|
||||||
@@ -26,7 +23,7 @@ const passphrase: GeneratorMetadata<PassphraseGenerationOptions> = {
|
|||||||
useCredential: "useThisPassphrase",
|
useCredential: "useThisPassphrase",
|
||||||
},
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
autogenerate: false,
|
autogenerate: true,
|
||||||
fields: [],
|
fields: [],
|
||||||
},
|
},
|
||||||
engine: {
|
engine: {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
|||||||
|
|
||||||
import { PasswordRandomizer } from "../../engine";
|
import { PasswordRandomizer } from "../../engine";
|
||||||
import { DynamicPasswordPolicyConstraints } from "../../policies";
|
import { DynamicPasswordPolicyConstraints } from "../../policies";
|
||||||
import { PasswordGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
import { GeneratorDependencyProvider } from "../../providers";
|
||||||
|
import { PasswordGenerationOptions } from "../../types";
|
||||||
import { Profile } from "../data";
|
import { Profile } from "../data";
|
||||||
import { CoreProfileMetadata } from "../profile-metadata";
|
import { CoreProfileMetadata } from "../profile-metadata";
|
||||||
import { isCoreProfile } from "../util";
|
import { isCoreProfile } from "../util";
|
||||||
|
|||||||
@@ -5,17 +5,14 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
|
|||||||
|
|
||||||
import { PasswordRandomizer } from "../../engine";
|
import { PasswordRandomizer } from "../../engine";
|
||||||
import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies";
|
import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies";
|
||||||
import {
|
import { GeneratorDependencyProvider } from "../../providers";
|
||||||
CredentialGenerator,
|
import { CredentialGenerator, PasswordGeneratorSettings } from "../../types";
|
||||||
GeneratorDependencyProvider,
|
|
||||||
PasswordGeneratorSettings,
|
|
||||||
} from "../../types";
|
|
||||||
import { Algorithm, Profile, Type } from "../data";
|
import { Algorithm, Profile, Type } from "../data";
|
||||||
import { GeneratorMetadata } from "../generator-metadata";
|
import { GeneratorMetadata } from "../generator-metadata";
|
||||||
|
|
||||||
const password: GeneratorMetadata<PasswordGeneratorSettings> = deepFreeze({
|
const password: GeneratorMetadata<PasswordGeneratorSettings> = deepFreeze({
|
||||||
id: Algorithm.password,
|
id: Algorithm.password,
|
||||||
category: Type.password,
|
type: Type.password,
|
||||||
weight: 100,
|
weight: 100,
|
||||||
i18nKeys: {
|
i18nKeys: {
|
||||||
name: "password",
|
name: "password",
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { mock } from "jest-mock-extended";
|
|||||||
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||||
|
|
||||||
import { UsernameRandomizer } from "../../engine";
|
import { UsernameRandomizer } from "../../engine";
|
||||||
import { EffUsernameGenerationOptions, GeneratorDependencyProvider } from "../../types";
|
import { GeneratorDependencyProvider } from "../../providers";
|
||||||
|
import { EffUsernameGenerationOptions } from "../../types";
|
||||||
import { Profile } from "../data";
|
import { Profile } from "../data";
|
||||||
import { CoreProfileMetadata } from "../profile-metadata";
|
import { CoreProfileMetadata } from "../profile-metadata";
|
||||||
import { isCoreProfile } from "../util";
|
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 { deepFreeze } from "@bitwarden/common/tools/util";
|
||||||
|
|
||||||
import { UsernameRandomizer } from "../../engine";
|
import { UsernameRandomizer } from "../../engine";
|
||||||
import {
|
import { GeneratorDependencyProvider } from "../../providers";
|
||||||
CredentialGenerator,
|
import { CredentialGenerator, EffUsernameGenerationOptions } from "../../types";
|
||||||
EffUsernameGenerationOptions,
|
|
||||||
GeneratorDependencyProvider,
|
|
||||||
} from "../../types";
|
|
||||||
import { Algorithm, Profile, Type } from "../data";
|
import { Algorithm, Profile, Type } from "../data";
|
||||||
import { GeneratorMetadata } from "../generator-metadata";
|
import { GeneratorMetadata } from "../generator-metadata";
|
||||||
|
|
||||||
const effWordList: GeneratorMetadata<EffUsernameGenerationOptions> = deepFreeze({
|
const effWordList: GeneratorMetadata<EffUsernameGenerationOptions> = deepFreeze({
|
||||||
id: Algorithm.username,
|
id: Algorithm.username,
|
||||||
category: Type.username,
|
type: Type.username,
|
||||||
weight: 400,
|
weight: 400,
|
||||||
i18nKeys: {
|
i18nKeys: {
|
||||||
name: "randomWord",
|
name: "randomWord",
|
||||||
|
|||||||
@@ -12,23 +12,23 @@ import {
|
|||||||
|
|
||||||
/** Returns true when the input algorithm is a password algorithm. */
|
/** Returns true when the input algorithm is a password algorithm. */
|
||||||
export function isPasswordAlgorithm(
|
export function isPasswordAlgorithm(
|
||||||
algorithm: CredentialAlgorithm,
|
algorithm: CredentialAlgorithm | null,
|
||||||
): algorithm is PasswordAlgorithm {
|
): algorithm is PasswordAlgorithm {
|
||||||
return AlgorithmsByType.password.includes(algorithm as any);
|
return AlgorithmsByType.password.includes(algorithm as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns true when the input algorithm is a username algorithm. */
|
/** Returns true when the input algorithm is a username algorithm. */
|
||||||
export function isUsernameAlgorithm(
|
export function isUsernameAlgorithm(
|
||||||
algorithm: CredentialAlgorithm,
|
algorithm: CredentialAlgorithm | null,
|
||||||
): algorithm is UsernameAlgorithm {
|
): algorithm is UsernameAlgorithm {
|
||||||
return AlgorithmsByType.username.includes(algorithm as any);
|
return AlgorithmsByType.username.includes(algorithm as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns true when the input algorithm is a forwarder integration. */
|
/** Returns true when the input algorithm is a forwarder integration. */
|
||||||
export function isForwarderExtensionId(
|
export function isForwarderExtensionId(
|
||||||
algorithm: CredentialAlgorithm,
|
algorithm: CredentialAlgorithm | null,
|
||||||
): algorithm is ForwarderExtensionId {
|
): 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`.
|
/** 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 { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { PolicyId } from "@bitwarden/common/types/guid";
|
import { PolicyId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { CredentialAlgorithms, PasswordAlgorithms } from "../data";
|
import { Algorithm, Algorithms, AlgorithmsByType } from "../metadata";
|
||||||
|
|
||||||
import { availableAlgorithms } from "./available-algorithms-policy";
|
import { availableAlgorithms } from "./available-algorithms-policy";
|
||||||
|
|
||||||
describe("availableAlgorithmsPolicy", () => {
|
describe("availableAlgorithms_vNextPolicy", () => {
|
||||||
it("returns all algorithms", () => {
|
it("returns all algorithms", () => {
|
||||||
const result = availableAlgorithms([]);
|
const result = availableAlgorithms([]);
|
||||||
|
|
||||||
for (const expected of CredentialAlgorithms) {
|
for (const expected of Algorithms) {
|
||||||
expect(result).toContain(expected);
|
expect(result).toContain(expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -30,7 +30,7 @@ describe("availableAlgorithmsPolicy", () => {
|
|||||||
|
|
||||||
expect(result).toContain(override);
|
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);
|
expect(result).not.toContain(expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -50,7 +50,7 @@ describe("availableAlgorithmsPolicy", () => {
|
|||||||
|
|
||||||
expect(result).toContain(override);
|
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);
|
expect(result).not.toContain(expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -79,7 +79,7 @@ describe("availableAlgorithmsPolicy", () => {
|
|||||||
|
|
||||||
expect(result).toContain("password");
|
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);
|
expect(result).not.toContain(expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -97,7 +97,7 @@ describe("availableAlgorithmsPolicy", () => {
|
|||||||
|
|
||||||
const result = availableAlgorithms([policy]);
|
const result = availableAlgorithms([policy]);
|
||||||
|
|
||||||
for (const expected of CredentialAlgorithms) {
|
for (const expected of Algorithms) {
|
||||||
expect(result).toContain(expected);
|
expect(result).toContain(expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -115,7 +115,7 @@ describe("availableAlgorithmsPolicy", () => {
|
|||||||
|
|
||||||
const result = availableAlgorithms([policy]);
|
const result = availableAlgorithms([policy]);
|
||||||
|
|
||||||
for (const expected of CredentialAlgorithms) {
|
for (const expected of Algorithms) {
|
||||||
expect(result).toContain(expected);
|
expect(result).toContain(expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -133,7 +133,7 @@ describe("availableAlgorithmsPolicy", () => {
|
|||||||
|
|
||||||
const result = availableAlgorithms([policy]);
|
const result = availableAlgorithms([policy]);
|
||||||
|
|
||||||
for (const expected of CredentialAlgorithms) {
|
for (const expected of Algorithms) {
|
||||||
expect(result).toContain(expected);
|
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";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
|
|
||||||
import {
|
import { AlgorithmsByType, CredentialAlgorithm, Type } from "../metadata";
|
||||||
CredentialAlgorithm as LegacyAlgorithm,
|
|
||||||
EmailAlgorithms,
|
|
||||||
PasswordAlgorithms,
|
|
||||||
UsernameAlgorithms,
|
|
||||||
} from "..";
|
|
||||||
import { CredentialAlgorithm } from "../metadata";
|
|
||||||
|
|
||||||
/** Reduces policies to a set of available algorithms
|
/** Reduces policies to a set of available algorithms
|
||||||
* @param policies the policies to reduce
|
* @param policies the policies to reduce
|
||||||
* @returns the resulting `AlgorithmAvailabilityPolicy`
|
* @returns the resulting `AlgorithmAvailabilityPolicy`
|
||||||
*/
|
*/
|
||||||
export function availableAlgorithms(policies: Policy[]): LegacyAlgorithm[] {
|
export function availableAlgorithms(policies: Policy[]): CredentialAlgorithm[] {
|
||||||
const overridePassword = policies
|
const overridePassword = policies
|
||||||
.filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled)
|
.filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled)
|
||||||
.reduce(
|
.reduce(
|
||||||
(type, policy) => (type === "password" ? type : (policy.data.overridePasswordType ?? type)),
|
(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) {
|
if (overridePassword) {
|
||||||
policy.push(overridePassword);
|
policy.push(overridePassword);
|
||||||
} else {
|
} else {
|
||||||
policy.push(...PasswordAlgorithms);
|
policy.push(...AlgorithmsByType[Type.password]);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return policy;
|
return policy;
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
|
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
|
||||||
|
|
||||||
import { Generators } from "../data";
|
import { BuiltIn, Profile } from "../metadata";
|
||||||
import { PasswordGeneratorSettings } from "../types";
|
import { PasswordGeneratorSettings } from "../types";
|
||||||
|
|
||||||
import { AtLeastOne, Zero } from "./constraints";
|
import { AtLeastOne, Zero } from "./constraints";
|
||||||
import { DynamicPasswordPolicyConstraints } from "./dynamic-password-policy-constraints";
|
import { DynamicPasswordPolicyConstraints } from "./dynamic-password-policy-constraints";
|
||||||
|
|
||||||
const accoutSettings = Generators.password.settings.account as ObjectKey<PasswordGeneratorSettings>;
|
// non-null assertions used because these are always-defined constants
|
||||||
const defaultOptions = accoutSettings.initial;
|
const accoutSettings = BuiltIn.password.profiles[Profile.account]!
|
||||||
const disabledPolicy = Generators.password.policy.disabledValue;
|
.storage as ObjectKey<PasswordGeneratorSettings>;
|
||||||
const someConstraints = Generators.password.settings.constraints;
|
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("DynamicPasswordPolicyConstraints", () => {
|
||||||
describe("constructor", () => {
|
describe("constructor", () => {
|
||||||
@@ -33,8 +43,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||||
|
|
||||||
expect(constraints.policyInEffect).toBeTruthy();
|
expect(constraints.policyInEffect).toBeTruthy();
|
||||||
expect(constraints.lowercase.readonly).toEqual(true);
|
expect(constraints.lowercase?.readonly).toEqual(true);
|
||||||
expect(constraints.lowercase.requiredValue).toEqual(true);
|
expect(constraints.lowercase?.requiredValue).toEqual(true);
|
||||||
expect(constraints.minLowercase).toEqual({ min: 1 });
|
expect(constraints.minLowercase).toEqual({ min: 1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,8 +53,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||||
|
|
||||||
expect(constraints.policyInEffect).toBeTruthy();
|
expect(constraints.policyInEffect).toBeTruthy();
|
||||||
expect(constraints.uppercase.readonly).toEqual(true);
|
expect(constraints.uppercase?.readonly).toEqual(true);
|
||||||
expect(constraints.uppercase.requiredValue).toEqual(true);
|
expect(constraints.uppercase?.requiredValue).toEqual(true);
|
||||||
expect(constraints.minUppercase).toEqual({ min: 1 });
|
expect(constraints.minUppercase).toEqual({ min: 1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,8 +63,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||||
|
|
||||||
expect(constraints.policyInEffect).toBeTruthy();
|
expect(constraints.policyInEffect).toBeTruthy();
|
||||||
expect(constraints.number.readonly).toEqual(true);
|
expect(constraints.number?.readonly).toEqual(true);
|
||||||
expect(constraints.number.requiredValue).toEqual(true);
|
expect(constraints.number?.requiredValue).toEqual(true);
|
||||||
expect(constraints.minNumber).toEqual({ min: 1, max: 9 });
|
expect(constraints.minNumber).toEqual({ min: 1, max: 9 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,8 +73,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||||
|
|
||||||
expect(constraints.policyInEffect).toBeTruthy();
|
expect(constraints.policyInEffect).toBeTruthy();
|
||||||
expect(constraints.special.readonly).toEqual(true);
|
expect(constraints.special?.readonly).toEqual(true);
|
||||||
expect(constraints.special.requiredValue).toEqual(true);
|
expect(constraints.special?.requiredValue).toEqual(true);
|
||||||
expect(constraints.minSpecial).toEqual({ min: 1, max: 9 });
|
expect(constraints.minSpecial).toEqual({ min: 1, max: 9 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,8 +83,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||||
|
|
||||||
expect(constraints.policyInEffect).toBeTruthy();
|
expect(constraints.policyInEffect).toBeTruthy();
|
||||||
expect(constraints.number.readonly).toEqual(true);
|
expect(constraints.number?.readonly).toEqual(true);
|
||||||
expect(constraints.number.requiredValue).toEqual(true);
|
expect(constraints.number?.requiredValue).toEqual(true);
|
||||||
expect(constraints.minNumber).toEqual({ min: 2, max: 9 });
|
expect(constraints.minNumber).toEqual({ min: 2, max: 9 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,8 +93,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||||
|
|
||||||
expect(constraints.policyInEffect).toBeTruthy();
|
expect(constraints.policyInEffect).toBeTruthy();
|
||||||
expect(constraints.special.readonly).toEqual(true);
|
expect(constraints.special?.readonly).toEqual(true);
|
||||||
expect(constraints.special.requiredValue).toEqual(true);
|
expect(constraints.special?.requiredValue).toEqual(true);
|
||||||
expect(constraints.minSpecial).toEqual({ min: 2, max: 9 });
|
expect(constraints.minSpecial).toEqual({ min: 2, max: 9 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,7 +150,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
const dynamic = new DynamicPasswordPolicyConstraints(
|
const dynamic = new DynamicPasswordPolicyConstraints(
|
||||||
{
|
{
|
||||||
...disabledPolicy,
|
...disabledPolicy,
|
||||||
useLowercase,
|
// the `undefined` case is testing behavior when the type system is bypassed
|
||||||
|
useLowercase: useLowercase!,
|
||||||
},
|
},
|
||||||
someConstraints,
|
someConstraints,
|
||||||
);
|
);
|
||||||
@@ -185,7 +196,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
const dynamic = new DynamicPasswordPolicyConstraints(
|
const dynamic = new DynamicPasswordPolicyConstraints(
|
||||||
{
|
{
|
||||||
...disabledPolicy,
|
...disabledPolicy,
|
||||||
useUppercase,
|
// the `undefined` case is testing behavior when the type system is bypassed
|
||||||
|
useUppercase: useUppercase!,
|
||||||
},
|
},
|
||||||
someConstraints,
|
someConstraints,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ export { PassphrasePolicyConstraints } from "./passphrase-policy-constraints";
|
|||||||
export { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
export { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
||||||
export { passphraseLeastPrivilege } from "./passphrase-least-privilege";
|
export { passphraseLeastPrivilege } from "./passphrase-least-privilege";
|
||||||
export { passwordLeastPrivilege } from "./password-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 { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { PassphraseGenerationOptions } from "../types";
|
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 { 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()", () => {
|
describe("constructor()", () => {
|
||||||
it("should set the policy object to a copy of the input policy", () => {
|
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
|
policy.minNumberWords = 10; // arbitrary change for deep equality check
|
||||||
|
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
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", () => {
|
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);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
expect(builder.numWords).toEqual(DefaultPassphraseBoundaries.numWords);
|
expect(builder.numWords).toEqual(DefaultPassphraseBoundaries.numWords);
|
||||||
@@ -25,7 +45,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([1, 2])(
|
it.each([1, 2])(
|
||||||
"should use the default word boundaries when they are greater than `policy.minNumberWords` (= %i)",
|
"should use the default word boundaries when they are greater than `policy.minNumberWords` (= %i)",
|
||||||
(minNumberWords) => {
|
(minNumberWords) => {
|
||||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
const policy: any = Object.assign({}, Passphrase.disabledValue);
|
||||||
policy.minNumberWords = minNumberWords;
|
policy.minNumberWords = minNumberWords;
|
||||||
|
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
@@ -37,7 +57,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([8, 12, 18])(
|
it.each([8, 12, 18])(
|
||||||
"should use `policy.minNumberWords` (= %i) when it is greater than the default minimum words",
|
"should use `policy.minNumberWords` (= %i) when it is greater than the default minimum words",
|
||||||
(minNumberWords) => {
|
(minNumberWords) => {
|
||||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
const policy: any = Object.assign({}, Passphrase.disabledValue);
|
||||||
policy.minNumberWords = minNumberWords;
|
policy.minNumberWords = minNumberWords;
|
||||||
|
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
@@ -50,7 +70,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([150, 300, 9000])(
|
it.each([150, 300, 9000])(
|
||||||
"should use `policy.minNumberWords` (= %i) when it is greater than the default boundaries",
|
"should use `policy.minNumberWords` (= %i) when it is greater than the default boundaries",
|
||||||
(minNumberWords) => {
|
(minNumberWords) => {
|
||||||
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
const policy: any = Object.assign({}, Passphrase.disabledValue);
|
||||||
policy.minNumberWords = minNumberWords;
|
policy.minNumberWords = minNumberWords;
|
||||||
|
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
@@ -63,14 +83,14 @@ describe("Password generator options builder", () => {
|
|||||||
|
|
||||||
describe("policyInEffect", () => {
|
describe("policyInEffect", () => {
|
||||||
it("should return false when the policy has no effect", () => {
|
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);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
expect(builder.policyInEffect).toEqual(false);
|
expect(builder.policyInEffect).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has a numWords greater than the default boundary", () => {
|
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;
|
policy.minNumberWords = DefaultPassphraseBoundaries.numWords.min + 1;
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@@ -78,7 +98,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has capitalize enabled", () => {
|
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;
|
policy.capitalize = true;
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@@ -86,7 +106,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has includeNumber enabled", () => {
|
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;
|
policy.includeNumber = true;
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
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
|
// 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", () => {
|
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 builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({});
|
const options = Object.freeze({});
|
||||||
|
|
||||||
@@ -108,7 +128,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set `capitalize` to `true` when the policy overrides it", () => {
|
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;
|
policy.capitalize = true;
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ capitalize: false });
|
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", () => {
|
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 builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({});
|
const options = Object.freeze({});
|
||||||
|
|
||||||
@@ -129,7 +149,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set `includeNumber` to true when the policy overrides it", () => {
|
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;
|
policy.includeNumber = true;
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ includeNumber: false });
|
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", () => {
|
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 builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({});
|
const options = Object.freeze({});
|
||||||
|
|
||||||
@@ -154,7 +174,7 @@ describe("Password generator options builder", () => {
|
|||||||
(numWords) => {
|
(numWords) => {
|
||||||
expect(numWords).toBeLessThan(DefaultPassphraseBoundaries.numWords.min);
|
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 builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ numWords });
|
const options = Object.freeze({ numWords });
|
||||||
|
|
||||||
@@ -170,7 +190,7 @@ describe("Password generator options builder", () => {
|
|||||||
expect(numWords).toBeGreaterThanOrEqual(DefaultPassphraseBoundaries.numWords.min);
|
expect(numWords).toBeGreaterThanOrEqual(DefaultPassphraseBoundaries.numWords.min);
|
||||||
expect(numWords).toBeLessThanOrEqual(DefaultPassphraseBoundaries.numWords.max);
|
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 builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ numWords });
|
const options = Object.freeze({ numWords });
|
||||||
|
|
||||||
@@ -185,7 +205,7 @@ describe("Password generator options builder", () => {
|
|||||||
(numWords) => {
|
(numWords) => {
|
||||||
expect(numWords).toBeGreaterThan(DefaultPassphraseBoundaries.numWords.max);
|
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 builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ numWords });
|
const options = Object.freeze({ numWords });
|
||||||
|
|
||||||
@@ -196,7 +216,7 @@ describe("Password generator options builder", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("should preserve unknown properties", () => {
|
it("should preserve unknown properties", () => {
|
||||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({
|
const options = Object.freeze({
|
||||||
unknown: "property",
|
unknown: "property",
|
||||||
@@ -214,7 +234,7 @@ describe("Password generator options builder", () => {
|
|||||||
// All tests should freeze the options to ensure they are not modified
|
// All tests should freeze the options to ensure they are not modified
|
||||||
|
|
||||||
it("should return the input options without altering them", () => {
|
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 builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ wordSeparator: "%" });
|
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", () => {
|
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 builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({});
|
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", () => {
|
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 builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ wordSeparator: "" });
|
const options = Object.freeze({ wordSeparator: "" });
|
||||||
|
|
||||||
@@ -244,7 +264,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should preserve unknown properties", () => {
|
it("should preserve unknown properties", () => {
|
||||||
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
const policy = Object.assign({}, Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({
|
const options = Object.freeze({
|
||||||
unknown: "property",
|
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 { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { PolicyId } from "@bitwarden/common/types/guid";
|
import { PolicyId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { Policies } from "../data";
|
|
||||||
|
|
||||||
import { passphraseLeastPrivilege } from "./passphrase-least-privilege";
|
import { passphraseLeastPrivilege } from "./passphrase-least-privilege";
|
||||||
|
|
||||||
function createPolicy(
|
function createPolicy(
|
||||||
@@ -22,21 +20,27 @@ function createPolicy(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disabledValue = Object.freeze({
|
||||||
|
minNumberWords: 0,
|
||||||
|
capitalize: false,
|
||||||
|
includeNumber: false,
|
||||||
|
});
|
||||||
|
|
||||||
describe("passphraseLeastPrivilege", () => {
|
describe("passphraseLeastPrivilege", () => {
|
||||||
it("should return the accumulator when the policy type does not apply", () => {
|
it("should return the accumulator when the policy type does not apply", () => {
|
||||||
const policy = createPolicy({}, PolicyType.RequireSso);
|
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", () => {
|
it("should return the accumulator when the policy is not enabled", () => {
|
||||||
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
|
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([
|
it.each([
|
||||||
@@ -46,8 +50,8 @@ describe("passphraseLeastPrivilege", () => {
|
|||||||
])("should take the %p from the policy", (input, value) => {
|
])("should take the %p from the policy", (input, value) => {
|
||||||
const policy = createPolicy({ [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";
|
import { PassphrasePolicyConstraints } from "./passphrase-policy-constraints";
|
||||||
|
|
||||||
@@ -9,8 +9,12 @@ const SomeSettings = {
|
|||||||
wordSeparator: "-",
|
wordSeparator: "-",
|
||||||
};
|
};
|
||||||
|
|
||||||
const disabledPolicy = Generators.passphrase.policy.disabledValue;
|
const disabledPolicy = {
|
||||||
const someConstraints = Generators.passphrase.settings.constraints;
|
minNumberWords: 0,
|
||||||
|
capitalize: false,
|
||||||
|
includeNumber: false,
|
||||||
|
};
|
||||||
|
const someConstraints = BuiltIn.passphrase.profiles[Profile.account]!.constraints.default;
|
||||||
|
|
||||||
describe("PassphrasePolicyConstraints", () => {
|
describe("PassphrasePolicyConstraints", () => {
|
||||||
describe("constructor", () => {
|
describe("constructor", () => {
|
||||||
@@ -61,7 +65,7 @@ describe("PassphrasePolicyConstraints", () => {
|
|||||||
expect(constraints.policyInEffect).toBeTruthy();
|
expect(constraints.policyInEffect).toBeTruthy();
|
||||||
expect(constraints.numWords).toMatchObject({
|
expect(constraints.numWords).toMatchObject({
|
||||||
min: 10,
|
min: 10,
|
||||||
max: someConstraints.numWords.max,
|
max: someConstraints.numWords?.max,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -84,8 +88,8 @@ describe("PassphrasePolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[1, someConstraints.numWords.min, 3, someConstraints.numWords.max],
|
[1, someConstraints.numWords?.min, 3, someConstraints.numWords?.max],
|
||||||
[21, someConstraints.numWords.min, 20, someConstraints.numWords.max],
|
[21, someConstraints.numWords?.min, 20, someConstraints.numWords?.max],
|
||||||
])(
|
])(
|
||||||
`fits numWords (=%p) within the default bounds (%p <= %p <= %p)`,
|
`fits numWords (=%p) within the default bounds (%p <= %p <= %p)`,
|
||||||
(value, _, expected, __) => {
|
(value, _, expected, __) => {
|
||||||
@@ -98,8 +102,8 @@ describe("PassphrasePolicyConstraints", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[1, 6, 6, someConstraints.numWords.max],
|
[1, 6, 6, someConstraints.numWords?.max],
|
||||||
[21, 20, 20, someConstraints.numWords.max],
|
[21, 20, 20, someConstraints.numWords?.max],
|
||||||
])(
|
])(
|
||||||
"fits numWords (=%p) within the policy bounds (%p <= %p <= %p)",
|
"fits numWords (=%p) within the policy bounds (%p <= %p <= %p)",
|
||||||
(value, minNumberWords, expected, _) => {
|
(value, minNumberWords, expected, _) => {
|
||||||
|
|||||||
@@ -1,14 +1,34 @@
|
|||||||
import { DefaultPasswordBoundaries, Policies } from "../data";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { PasswordGenerationOptions } from "../types";
|
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 { 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", () => {
|
describe("Password generator options builder", () => {
|
||||||
const defaultOptions = Object.freeze({ minLength: 0 });
|
const defaultOptions = Object.freeze({ minLength: 0 });
|
||||||
|
|
||||||
describe("constructor()", () => {
|
describe("constructor()", () => {
|
||||||
it("should set the policy object to a copy of the input policy", () => {
|
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
|
policy.minLength = 10; // arbitrary change for deep equality check
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
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", () => {
|
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);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@@ -32,7 +52,7 @@ describe("Password generator options builder", () => {
|
|||||||
(minLength) => {
|
(minLength) => {
|
||||||
expect(minLength).toBeLessThan(DefaultPasswordBoundaries.length.min);
|
expect(minLength).toBeLessThan(DefaultPasswordBoundaries.length.min);
|
||||||
|
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.minLength = minLength;
|
policy.minLength = minLength;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@@ -47,7 +67,7 @@ describe("Password generator options builder", () => {
|
|||||||
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.min);
|
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.min);
|
||||||
expect(expectedLength).toBeLessThanOrEqual(DefaultPasswordBoundaries.length.max);
|
expect(expectedLength).toBeLessThanOrEqual(DefaultPasswordBoundaries.length.max);
|
||||||
|
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.minLength = expectedLength;
|
policy.minLength = expectedLength;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@@ -62,7 +82,7 @@ describe("Password generator options builder", () => {
|
|||||||
(expectedLength) => {
|
(expectedLength) => {
|
||||||
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.max);
|
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.max);
|
||||||
|
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.minLength = expectedLength;
|
policy.minLength = expectedLength;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@@ -78,7 +98,7 @@ describe("Password generator options builder", () => {
|
|||||||
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.min);
|
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.min);
|
||||||
expect(expectedMinDigits).toBeLessThanOrEqual(DefaultPasswordBoundaries.minDigits.max);
|
expect(expectedMinDigits).toBeLessThanOrEqual(DefaultPasswordBoundaries.minDigits.max);
|
||||||
|
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.numberCount = expectedMinDigits;
|
policy.numberCount = expectedMinDigits;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@@ -93,7 +113,7 @@ describe("Password generator options builder", () => {
|
|||||||
(expectedMinDigits) => {
|
(expectedMinDigits) => {
|
||||||
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.max);
|
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.max);
|
||||||
|
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.numberCount = expectedMinDigits;
|
policy.numberCount = expectedMinDigits;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@@ -113,7 +133,7 @@ describe("Password generator options builder", () => {
|
|||||||
DefaultPasswordBoundaries.minSpecialCharacters.max,
|
DefaultPasswordBoundaries.minSpecialCharacters.max,
|
||||||
);
|
);
|
||||||
|
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.specialCount = expectedSpecialCharacters;
|
policy.specialCount = expectedSpecialCharacters;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@@ -132,7 +152,7 @@ describe("Password generator options builder", () => {
|
|||||||
DefaultPasswordBoundaries.minSpecialCharacters.max,
|
DefaultPasswordBoundaries.minSpecialCharacters.max,
|
||||||
);
|
);
|
||||||
|
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.specialCount = expectedSpecialCharacters;
|
policy.specialCount = expectedSpecialCharacters;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@@ -151,7 +171,7 @@ describe("Password generator options builder", () => {
|
|||||||
(expectedLength, numberCount, specialCount) => {
|
(expectedLength, numberCount, specialCount) => {
|
||||||
expect(expectedLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
|
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.numberCount = numberCount;
|
||||||
policy.specialCount = specialCount;
|
policy.specialCount = specialCount;
|
||||||
|
|
||||||
@@ -164,14 +184,14 @@ describe("Password generator options builder", () => {
|
|||||||
|
|
||||||
describe("policyInEffect", () => {
|
describe("policyInEffect", () => {
|
||||||
it("should return false when the policy has no effect", () => {
|
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);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
expect(builder.policyInEffect).toEqual(false);
|
expect(builder.policyInEffect).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has a minlength greater than the default boundary", () => {
|
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;
|
policy.minLength = DefaultPasswordBoundaries.length.min + 1;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
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", () => {
|
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;
|
policy.numberCount = DefaultPasswordBoundaries.minDigits.min + 1;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
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", () => {
|
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;
|
policy.specialCount = DefaultPasswordBoundaries.minSpecialCharacters.min + 1;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@@ -195,7 +215,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has uppercase enabled", () => {
|
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;
|
policy.useUppercase = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@@ -203,7 +223,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has lowercase enabled", () => {
|
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;
|
policy.useLowercase = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@@ -211,7 +231,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has numbers enabled", () => {
|
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;
|
policy.useNumbers = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
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", () => {
|
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;
|
policy.useSpecial = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
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'",
|
"should set `options.uppercase` to '%s' when `policy.useUppercase` is false and `options.uppercase` is '%s'",
|
||||||
(expectedUppercase, uppercase) => {
|
(expectedUppercase, uppercase) => {
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.useUppercase = false;
|
policy.useUppercase = false;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, uppercase });
|
const options = Object.freeze({ ...defaultOptions, uppercase });
|
||||||
@@ -251,7 +271,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([false, true, undefined])(
|
it.each([false, true, undefined])(
|
||||||
"should set `options.uppercase` (= %s) to true when `policy.useUppercase` is true",
|
"should set `options.uppercase` (= %s) to true when `policy.useUppercase` is true",
|
||||||
(uppercase) => {
|
(uppercase) => {
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.useUppercase = true;
|
policy.useUppercase = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, uppercase });
|
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'",
|
"should set `options.lowercase` to '%s' when `policy.useLowercase` is false and `options.lowercase` is '%s'",
|
||||||
(expectedLowercase, lowercase) => {
|
(expectedLowercase, lowercase) => {
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.useLowercase = false;
|
policy.useLowercase = false;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, lowercase });
|
const options = Object.freeze({ ...defaultOptions, lowercase });
|
||||||
@@ -283,7 +303,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([false, true, undefined])(
|
it.each([false, true, undefined])(
|
||||||
"should set `options.lowercase` (= %s) to true when `policy.useLowercase` is true",
|
"should set `options.lowercase` (= %s) to true when `policy.useLowercase` is true",
|
||||||
(lowercase) => {
|
(lowercase) => {
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.useLowercase = true;
|
policy.useLowercase = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, lowercase });
|
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'",
|
"should set `options.number` to '%s' when `policy.useNumbers` is false and `options.number` is '%s'",
|
||||||
(expectedNumber, number) => {
|
(expectedNumber, number) => {
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.useNumbers = false;
|
policy.useNumbers = false;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, number });
|
const options = Object.freeze({ ...defaultOptions, number });
|
||||||
@@ -315,7 +335,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([false, true, undefined])(
|
it.each([false, true, undefined])(
|
||||||
"should set `options.number` (= %s) to true when `policy.useNumbers` is true",
|
"should set `options.number` (= %s) to true when `policy.useNumbers` is true",
|
||||||
(number) => {
|
(number) => {
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.useNumbers = true;
|
policy.useNumbers = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, number });
|
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'",
|
"should set `options.special` to '%s' when `policy.useSpecial` is false and `options.special` is '%s'",
|
||||||
(expectedSpecial, special) => {
|
(expectedSpecial, special) => {
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.useSpecial = false;
|
policy.useSpecial = false;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, special });
|
const options = Object.freeze({ ...defaultOptions, special });
|
||||||
@@ -347,7 +367,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([false, true, undefined])(
|
it.each([false, true, undefined])(
|
||||||
"should set `options.special` (= %s) to true when `policy.useSpecial` is true",
|
"should set `options.special` (= %s) to true when `policy.useSpecial` is true",
|
||||||
(special) => {
|
(special) => {
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.useSpecial = true;
|
policy.useSpecial = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, special });
|
const options = Object.freeze({ ...defaultOptions, special });
|
||||||
@@ -361,7 +381,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([1, 2, 3, 4])(
|
it.each([1, 2, 3, 4])(
|
||||||
"should set `options.length` (= %i) to the minimum it is less than the minimum length",
|
"should set `options.length` (= %i) to the minimum it is less than the minimum length",
|
||||||
(length) => {
|
(length) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(length).toBeLessThan(builder.length.min);
|
expect(length).toBeLessThan(builder.length.min);
|
||||||
|
|
||||||
@@ -376,7 +396,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([5, 10, 50, 100, 128])(
|
it.each([5, 10, 50, 100, 128])(
|
||||||
"should not change `options.length` (= %i) when it is within the boundaries",
|
"should not change `options.length` (= %i) when it is within the boundaries",
|
||||||
(length) => {
|
(length) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(length).toBeGreaterThanOrEqual(builder.length.min);
|
expect(length).toBeGreaterThanOrEqual(builder.length.min);
|
||||||
expect(length).toBeLessThanOrEqual(builder.length.max);
|
expect(length).toBeLessThanOrEqual(builder.length.max);
|
||||||
@@ -392,7 +412,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([129, 500, 9000])(
|
it.each([129, 500, 9000])(
|
||||||
"should set `options.length` (= %i) to the maximum length when it is exceeded",
|
"should set `options.length` (= %i) to the maximum length when it is exceeded",
|
||||||
(length) => {
|
(length) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(length).toBeGreaterThan(builder.length.max);
|
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",
|
"should set `options.number === %s` when `options.minNumber` (= %i) is set to a value greater than 0",
|
||||||
(expectedNumber, minNumber) => {
|
(expectedNumber, minNumber) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, minNumber });
|
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", () => {
|
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 builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, number: true });
|
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", () => {
|
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 builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, number: false });
|
const options = Object.freeze({ ...defaultOptions, number: false });
|
||||||
|
|
||||||
@@ -447,7 +467,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([1, 2, 3, 4])(
|
it.each([1, 2, 3, 4])(
|
||||||
"should set `options.minNumber` (= %i) to the minimum it is less than the minimum number",
|
"should set `options.minNumber` (= %i) to the minimum it is less than the minimum number",
|
||||||
(minNumber) => {
|
(minNumber) => {
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.numberCount = 5; // arbitrary value greater than minNumber
|
policy.numberCount = 5; // arbitrary value greater than minNumber
|
||||||
expect(minNumber).toBeLessThan(policy.numberCount);
|
expect(minNumber).toBeLessThan(policy.numberCount);
|
||||||
|
|
||||||
@@ -463,7 +483,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([1, 3, 5, 7, 9])(
|
it.each([1, 3, 5, 7, 9])(
|
||||||
"should not change `options.minNumber` (= %i) when it is within the boundaries",
|
"should not change `options.minNumber` (= %i) when it is within the boundaries",
|
||||||
(minNumber) => {
|
(minNumber) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(minNumber).toBeGreaterThanOrEqual(builder.minDigits.min);
|
expect(minNumber).toBeGreaterThanOrEqual(builder.minDigits.min);
|
||||||
expect(minNumber).toBeLessThanOrEqual(builder.minDigits.max);
|
expect(minNumber).toBeLessThanOrEqual(builder.minDigits.max);
|
||||||
@@ -479,7 +499,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([10, 20, 400])(
|
it.each([10, 20, 400])(
|
||||||
"should set `options.minNumber` (= %i) to the maximum digit boundary when it is exceeded",
|
"should set `options.minNumber` (= %i) to the maximum digit boundary when it is exceeded",
|
||||||
(minNumber) => {
|
(minNumber) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(minNumber).toBeGreaterThan(builder.minDigits.max);
|
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",
|
"should set `options.special === %s` when `options.minSpecial` (= %i) is set to a value greater than 0",
|
||||||
(expectedSpecial, minSpecial) => {
|
(expectedSpecial, minSpecial) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, minSpecial });
|
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", () => {
|
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 builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, special: true });
|
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", () => {
|
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 builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, special: false });
|
const options = Object.freeze({ ...defaultOptions, special: false });
|
||||||
|
|
||||||
@@ -534,7 +554,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([1, 2, 3, 4])(
|
it.each([1, 2, 3, 4])(
|
||||||
"should set `options.minSpecial` (= %i) to the minimum it is less than the minimum special characters",
|
"should set `options.minSpecial` (= %i) to the minimum it is less than the minimum special characters",
|
||||||
(minSpecial) => {
|
(minSpecial) => {
|
||||||
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Password.disabledValue);
|
||||||
policy.specialCount = 5; // arbitrary value greater than minSpecial
|
policy.specialCount = 5; // arbitrary value greater than minSpecial
|
||||||
expect(minSpecial).toBeLessThan(policy.specialCount);
|
expect(minSpecial).toBeLessThan(policy.specialCount);
|
||||||
|
|
||||||
@@ -550,7 +570,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([1, 3, 5, 7, 9])(
|
it.each([1, 3, 5, 7, 9])(
|
||||||
"should not change `options.minSpecial` (= %i) when it is within the boundaries",
|
"should not change `options.minSpecial` (= %i) when it is within the boundaries",
|
||||||
(minSpecial) => {
|
(minSpecial) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(minSpecial).toBeGreaterThanOrEqual(builder.minSpecialCharacters.min);
|
expect(minSpecial).toBeGreaterThanOrEqual(builder.minSpecialCharacters.min);
|
||||||
expect(minSpecial).toBeLessThanOrEqual(builder.minSpecialCharacters.max);
|
expect(minSpecial).toBeLessThanOrEqual(builder.minSpecialCharacters.max);
|
||||||
@@ -566,7 +586,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([10, 20, 400])(
|
it.each([10, 20, 400])(
|
||||||
"should set `options.minSpecial` (= %i) to the maximum special character boundary when it is exceeded",
|
"should set `options.minSpecial` (= %i) to the maximum special character boundary when it is exceeded",
|
||||||
(minSpecial) => {
|
(minSpecial) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(minSpecial).toBeGreaterThan(builder.minSpecialCharacters.max);
|
expect(minSpecial).toBeGreaterThan(builder.minSpecialCharacters.max);
|
||||||
|
|
||||||
@@ -579,7 +599,7 @@ describe("Password generator options builder", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("should preserve unknown properties", () => {
|
it("should preserve unknown properties", () => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({
|
const options = Object.freeze({
|
||||||
unknown: "property",
|
unknown: "property",
|
||||||
@@ -602,7 +622,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should output `options.minLowercase === %i` when `options.lowercase` is %s",
|
"should output `options.minLowercase === %i` when `options.lowercase` is %s",
|
||||||
(expectedMinLowercase, lowercase) => {
|
(expectedMinLowercase, lowercase) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ lowercase, ...defaultOptions });
|
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",
|
"should output `options.minUppercase === %i` when `options.uppercase` is %s",
|
||||||
(expectedMinUppercase, uppercase) => {
|
(expectedMinUppercase, uppercase) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ uppercase, ...defaultOptions });
|
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",
|
"should output `options.minNumber === %i` when `options.number` is %s and `options.minNumber` is not set",
|
||||||
(expectedMinNumber, number) => {
|
(expectedMinNumber, number) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ number, ...defaultOptions });
|
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",
|
"should output `options.number === %s` when `options.minNumber` is %i and `options.number` is not set",
|
||||||
(expectedNumber, minNumber) => {
|
(expectedNumber, minNumber) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ minNumber, ...defaultOptions });
|
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",
|
"should output `options.minSpecial === %i` when `options.special` is %s and `options.minSpecial` is not set",
|
||||||
(special, expectedMinSpecial) => {
|
(special, expectedMinSpecial) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ special, ...defaultOptions });
|
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",
|
"should output `options.special === %s` when `options.minSpecial` is %i and `options.special` is not set",
|
||||||
(minSpecial, expectedSpecial) => {
|
(minSpecial, expectedSpecial) => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ minSpecial, ...defaultOptions });
|
const options = Object.freeze({ minSpecial, ...defaultOptions });
|
||||||
|
|
||||||
@@ -707,7 +727,7 @@ describe("Password generator options builder", () => {
|
|||||||
const sumOfMinimums = minLowercase + minUppercase + minNumber + minSpecial;
|
const sumOfMinimums = minLowercase + minUppercase + minNumber + minSpecial;
|
||||||
expect(sumOfMinimums).toBeLessThan(DefaultPasswordBoundaries.length.min);
|
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 builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({
|
const options = Object.freeze({
|
||||||
minLowercase,
|
minLowercase,
|
||||||
@@ -732,7 +752,7 @@ describe("Password generator options builder", () => {
|
|||||||
(expectedMinLength, minLowercase, minUppercase, minNumber, minSpecial) => {
|
(expectedMinLength, minLowercase, minUppercase, minNumber, minSpecial) => {
|
||||||
expect(expectedMinLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
|
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 builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({
|
const options = Object.freeze({
|
||||||
minLowercase,
|
minLowercase,
|
||||||
@@ -749,7 +769,7 @@ describe("Password generator options builder", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("should preserve unknown properties", () => {
|
it("should preserve unknown properties", () => {
|
||||||
const policy = Object.assign({}, Policies.Password.disabledValue);
|
const policy = Object.assign({}, Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({
|
const options = Object.freeze({
|
||||||
unknown: "property",
|
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 { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { PolicyId } from "@bitwarden/common/types/guid";
|
import { PolicyId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { Policies } from "../data";
|
|
||||||
|
|
||||||
import { passwordLeastPrivilege } from "./password-least-privilege";
|
import { passwordLeastPrivilege } from "./password-least-privilege";
|
||||||
|
|
||||||
function createPolicy(
|
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", () => {
|
describe("passwordLeastPrivilege", () => {
|
||||||
it("should return the accumulator when the policy type does not apply", () => {
|
it("should return the accumulator when the policy type does not apply", () => {
|
||||||
const policy = createPolicy({}, PolicyType.RequireSso);
|
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", () => {
|
it("should return the accumulator when the policy is not enabled", () => {
|
||||||
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
|
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([
|
it.each([
|
||||||
@@ -50,8 +58,8 @@ describe("passwordLeastPrivilege", () => {
|
|||||||
])("should take the %p from the policy", (input, value, expected) => {
|
])("should take the %p from the policy", (input, value, expected) => {
|
||||||
const policy = createPolicy({ [input]: value });
|
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,
|
} as LegacyEncryptorProvider,
|
||||||
state: SomeStateProvider,
|
state: SomeStateProvider,
|
||||||
log: disabledSemanticLoggerProvider,
|
log: disabledSemanticLoggerProvider,
|
||||||
|
now: Date.now,
|
||||||
} as UserStateSubjectDependencyProvider;
|
} as UserStateSubjectDependencyProvider;
|
||||||
|
|
||||||
const SomeSiteId: SiteId = Site.forwarder;
|
const SomeSiteId: SiteId = Site.forwarder;
|
||||||
@@ -415,14 +416,14 @@ describe("GeneratorMetadataProvider", () => {
|
|||||||
await expect(firstValueFrom(result)).resolves.toEqual(plusAddress.id);
|
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([]));
|
SomePolicyService.policiesByType$.mockReturnValue(new BehaviorSubject([]));
|
||||||
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []);
|
||||||
const result = new ReplaySubject<CredentialAlgorithm | undefined>(1);
|
const result = new ReplaySubject<CredentialAlgorithm | undefined>(1);
|
||||||
|
|
||||||
provider.preference$(Type.email, { account$: SomeAccount$ }).subscribe(result);
|
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 {
|
import { Observable, distinctUntilChanged, map, shareReplay, switchMap, takeUntil } from "rxjs";
|
||||||
Observable,
|
|
||||||
combineLatestWith,
|
|
||||||
distinctUntilChanged,
|
|
||||||
map,
|
|
||||||
shareReplay,
|
|
||||||
switchMap,
|
|
||||||
takeUntil,
|
|
||||||
} from "rxjs";
|
|
||||||
|
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
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 { ExtensionSite } from "@bitwarden/common/tools/extension";
|
||||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
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 { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||||
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider";
|
||||||
|
|
||||||
@@ -29,7 +21,7 @@ import {
|
|||||||
Algorithms,
|
Algorithms,
|
||||||
Types,
|
Types,
|
||||||
} from "../metadata";
|
} from "../metadata";
|
||||||
import { availableAlgorithms_vNext } from "../policies/available-algorithms-policy";
|
import { AvailableAlgorithmsConstraint, availableAlgorithms } from "../policies";
|
||||||
import { CredentialPreference } from "../types";
|
import { CredentialPreference } from "../types";
|
||||||
import {
|
import {
|
||||||
AlgorithmRequest,
|
AlgorithmRequest,
|
||||||
@@ -148,8 +140,15 @@ export class GeneratorMetadataProvider {
|
|||||||
const policies$ = this.application.policy
|
const policies$ = this.application.policy
|
||||||
.policiesByType$(PolicyType.PasswordGenerator, id)
|
.policiesByType$(PolicyType.PasswordGenerator, id)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((p) => availableAlgorithms_vNext(p).filter((a) => this._metadata.has(a))),
|
map((p) =>
|
||||||
map((p) => new Set(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
|
// complete policy emissions otherwise `switchMap` holds `available$` open indefinitely
|
||||||
takeUntil(anyComplete(id$)),
|
takeUntil(anyComplete(id$)),
|
||||||
);
|
);
|
||||||
@@ -211,24 +210,7 @@ export class GeneratorMetadataProvider {
|
|||||||
const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||||
|
|
||||||
const algorithm$ = this.preferences({ account$ }).pipe(
|
const algorithm$ = this.preferences({ account$ }).pipe(
|
||||||
combineLatestWith(this.isAvailable$({ account$ })),
|
map((preferences) => preferences[type].algorithm),
|
||||||
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;
|
|
||||||
}),
|
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -246,8 +228,16 @@ export class GeneratorMetadataProvider {
|
|||||||
preferences(
|
preferences(
|
||||||
dependencies: BoundDependency<"account", Account>,
|
dependencies: BoundDependency<"account", Account>,
|
||||||
): UserStateSubject<CredentialPreference> {
|
): UserStateSubject<CredentialPreference> {
|
||||||
// FIXME: enforce policy
|
const account$ = dependencies.account$.pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||||
const subject = new UserStateSubject(PREFERENCES, this.system, dependencies);
|
|
||||||
|
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;
|
return subject;
|
||||||
}
|
}
|
||||||
@@ -67,6 +67,7 @@ const dependencyProvider: UserStateSubjectDependencyProvider = {
|
|||||||
encryptor: encryptorProvider,
|
encryptor: encryptorProvider,
|
||||||
state: stateProvider,
|
state: stateProvider,
|
||||||
log: disabledSemanticLoggerProvider,
|
log: disabledSemanticLoggerProvider,
|
||||||
|
now: Date.now,
|
||||||
};
|
};
|
||||||
|
|
||||||
// settings storage location
|
// settings storage location
|
||||||
@@ -19,6 +19,7 @@ import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/stat
|
|||||||
|
|
||||||
import { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "../metadata";
|
import { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "../metadata";
|
||||||
import { GeneratorConstraints } from "../types/generator-constraints";
|
import { GeneratorConstraints } from "../types/generator-constraints";
|
||||||
|
import { equivalent } from "../util";
|
||||||
|
|
||||||
/** Surfaces contextual information to credential generators */
|
/** Surfaces contextual information to credential generators */
|
||||||
export class GeneratorProfileProvider {
|
export class GeneratorProfileProvider {
|
||||||
@@ -99,7 +100,10 @@ export class GeneratorProfileProvider {
|
|||||||
|
|
||||||
const constraints$ = policies$.pipe(
|
const constraints$ = policies$.pipe(
|
||||||
map((policies) => profile.constraints.create(policies, context)),
|
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$;
|
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. */
|
/** Constructs a method that maps a policy to the default (no-op) policy. */
|
||||||
export function newDefaultEvaluator<Target>() {
|
export function newDefaultEvaluator<Target>() {
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user