1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-21 18:53:29 +00:00

[PM-5606] Add reactive generator service (#7446)

This commit is contained in:
✨ Audrey ✨
2024-01-23 14:22:52 -05:00
committed by GitHub
parent 0de72144b9
commit dbf836b573
20 changed files with 917 additions and 134 deletions

View File

@@ -1,3 +1,10 @@
// password generator "v2" interfaces
export * from "./password-generation-options";
export { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
export { PasswordGeneratorPolicy } from "./password-generator-policy";
export { PasswordGeneratorStrategy } from "./password-generator-strategy";
// legacy interfaces
export { PasswordGeneratorOptions } from "./password-generator-options";
export { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
export { PasswordGenerationService } from "./password-generation.service";

View File

@@ -0,0 +1,83 @@
import { DefaultBoundaries } from "./password-generator-options-evaluator";
/** Request format for password credential generation.
* All members of this type may be `undefined` when the user is
* generating a passphrase.
*
* @remarks The name of this type is a bit of a misnomer. This type
* it is used with the "password generator" types. The name
* `PasswordGeneratorOptions` is already in use by legacy code.
*/
export type PasswordGenerationOptions = {
/** The length of the password selected by the user */
length?: number;
/** The minimum length of the password. This defaults to 5, and increases
* to ensure `minLength` is at least as large as the sum of the other minimums.
*/
minLength?: number;
/** `true` when ambiguous characters may be included in the output.
* `false` when ambiguous characters should not be included in the output.
*/
ambiguous?: boolean;
/** `true` when uppercase ASCII characters should be included in the output
* This value defaults to `false.
*/
uppercase?: boolean;
/** The minimum number of uppercase characters to include in the output.
* The value is ignored when `uppercase` is `false`.
* The value defaults to 1 when `uppercase` is `true`.
*/
minUppercase?: number;
/** `true` when lowercase ASCII characters should be included in the output.
* This value defaults to `false`.
*/
lowercase?: boolean;
/** The minimum number of lowercase characters to include in the output.
* The value defaults to 1 when `lowercase` is `true`.
* The value defaults to 0 when `lowercase` is `false`.
*/
minLowercase?: number;
/** Whether or not to include ASCII digits in the output
* This value defaults to `true` when `minNumber` is at least 1.
* This value defaults to `false` when `minNumber` is less than 1.
*/
number?: boolean;
/** The minimum number of digits to include in the output.
* The value defaults to 1 when `number` is `true`.
* The value defaults to 0 when `number` is `false`.
*/
minNumber?: number;
/** Whether or not to include special characters in the output.
* This value defaults to `true` when `minSpecial` is at least 1.
* This value defaults to `false` when `minSpecial` is less than 1.
*/
special?: boolean;
/** The minimum number of special characters to include in the output.
* This value defaults to 1 when `special` is `true`.
* This value defaults to 0 when `special` is `false`.
*/
minSpecial?: number;
};
/** The default options for password generation. */
export const DefaultPasswordGenerationOptions: Partial<PasswordGenerationOptions> = Object.freeze({
length: 14,
minLength: DefaultBoundaries.length.min,
ambiguous: true,
uppercase: true,
lowercase: true,
number: true,
minNumber: 1,
special: true,
minSpecial: 1,
});

View File

@@ -1,17 +1,19 @@
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
/**
* include structuredClone in test environment.
* @jest-environment ../../../../shared/test.environment.ts
*/
import { PasswordGenerationOptions } from "./password-generator-options";
import {
DefaultBoundaries,
PasswordGeneratorOptionsEvaluator,
} from "./password-generator-options-evaluator";
import { DefaultBoundaries } from "./password-generator-options-evaluator";
import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy";
import { PasswordGenerationOptions, PasswordGeneratorOptionsEvaluator } from ".";
describe("Password generator options builder", () => {
const defaultOptions = Object.freeze({ minLength: 0 });
describe("constructor()", () => {
it("should set the policy object to a copy of the input policy", () => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.minLength = 10; // arbitrary change for deep equality check
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -21,7 +23,7 @@ describe("Password generator options builder", () => {
});
it("should set default boundaries when a default policy is used", () => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -35,7 +37,7 @@ describe("Password generator options builder", () => {
(minLength) => {
expect(minLength).toBeLessThan(DefaultBoundaries.length.min);
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.minLength = minLength;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -50,7 +52,7 @@ describe("Password generator options builder", () => {
expect(expectedLength).toBeGreaterThan(DefaultBoundaries.length.min);
expect(expectedLength).toBeLessThanOrEqual(DefaultBoundaries.length.max);
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.minLength = expectedLength;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -65,7 +67,7 @@ describe("Password generator options builder", () => {
(expectedLength) => {
expect(expectedLength).toBeGreaterThan(DefaultBoundaries.length.max);
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.minLength = expectedLength;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -81,7 +83,7 @@ describe("Password generator options builder", () => {
expect(expectedMinDigits).toBeGreaterThan(DefaultBoundaries.minDigits.min);
expect(expectedMinDigits).toBeLessThanOrEqual(DefaultBoundaries.minDigits.max);
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.numberCount = expectedMinDigits;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -96,7 +98,7 @@ describe("Password generator options builder", () => {
(expectedMinDigits) => {
expect(expectedMinDigits).toBeGreaterThan(DefaultBoundaries.minDigits.max);
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.numberCount = expectedMinDigits;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -116,7 +118,7 @@ describe("Password generator options builder", () => {
DefaultBoundaries.minSpecialCharacters.max,
);
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.specialCount = expectedSpecialCharacters;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -135,7 +137,7 @@ describe("Password generator options builder", () => {
DefaultBoundaries.minSpecialCharacters.max,
);
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.specialCount = expectedSpecialCharacters;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -154,7 +156,7 @@ describe("Password generator options builder", () => {
(expectedLength, numberCount, specialCount) => {
expect(expectedLength).toBeGreaterThanOrEqual(DefaultBoundaries.length.min);
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.numberCount = numberCount;
policy.specialCount = specialCount;
@@ -165,6 +167,71 @@ describe("Password generator options builder", () => {
);
});
describe("policyInEffect", () => {
it("should return false when the policy has no effect", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(builder.policyInEffect).toEqual(false);
});
it("should return true when the policy has a minlength greater than the default boundary", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.minLength = DefaultBoundaries.length.min + 1;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(builder.policyInEffect).toEqual(true);
});
it("should return true when the policy has a number count greater than the default boundary", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.numberCount = DefaultBoundaries.minDigits.min + 1;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(builder.policyInEffect).toEqual(true);
});
it("should return true when the policy has a special character count greater than the default boundary", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.specialCount = DefaultBoundaries.minSpecialCharacters.min + 1;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(builder.policyInEffect).toEqual(true);
});
it("should return true when the policy has uppercase enabled", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.useUppercase = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(builder.policyInEffect).toEqual(true);
});
it("should return true when the policy has lowercase enabled", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.useLowercase = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(builder.policyInEffect).toEqual(true);
});
it("should return true when the policy has numbers enabled", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.useNumbers = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(builder.policyInEffect).toEqual(true);
});
it("should return true when the policy has special characters enabled", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.useSpecial = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(builder.policyInEffect).toEqual(true);
});
});
describe("applyPolicy(options)", () => {
// All tests should freeze the options to ensure they are not modified
@@ -175,7 +242,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.uppercase` to '%s' when `policy.useUppercase` is false and `options.uppercase` is '%s'",
(expectedUppercase, uppercase) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.useUppercase = false;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, uppercase });
@@ -189,7 +256,7 @@ describe("Password generator options builder", () => {
it.each([false, true, undefined])(
"should set `options.uppercase` (= %s) to true when `policy.useUppercase` is true",
(uppercase) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.useUppercase = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, uppercase });
@@ -207,7 +274,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.lowercase` to '%s' when `policy.useLowercase` is false and `options.lowercase` is '%s'",
(expectedLowercase, lowercase) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.useLowercase = false;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, lowercase });
@@ -221,7 +288,7 @@ describe("Password generator options builder", () => {
it.each([false, true, undefined])(
"should set `options.lowercase` (= %s) to true when `policy.useLowercase` is true",
(lowercase) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.useLowercase = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, lowercase });
@@ -239,7 +306,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.number` to '%s' when `policy.useNumbers` is false and `options.number` is '%s'",
(expectedNumber, number) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.useNumbers = false;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, number });
@@ -253,7 +320,7 @@ describe("Password generator options builder", () => {
it.each([false, true, undefined])(
"should set `options.number` (= %s) to true when `policy.useNumbers` is true",
(number) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.useNumbers = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, number });
@@ -271,7 +338,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.special` to '%s' when `policy.useSpecial` is false and `options.special` is '%s'",
(expectedSpecial, special) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.useSpecial = false;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, special });
@@ -285,7 +352,7 @@ describe("Password generator options builder", () => {
it.each([false, true, undefined])(
"should set `options.special` (= %s) to true when `policy.useSpecial` is true",
(special) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.useSpecial = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, special });
@@ -299,7 +366,7 @@ describe("Password generator options builder", () => {
it.each([1, 2, 3, 4])(
"should set `options.length` (= %i) to the minimum it is less than the minimum length",
(length) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(length).toBeLessThan(builder.length.min);
@@ -314,7 +381,7 @@ describe("Password generator options builder", () => {
it.each([5, 10, 50, 100, 128])(
"should not change `options.length` (= %i) when it is within the boundaries",
(length) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(length).toBeGreaterThanOrEqual(builder.length.min);
expect(length).toBeLessThanOrEqual(builder.length.max);
@@ -330,7 +397,7 @@ describe("Password generator options builder", () => {
it.each([129, 500, 9000])(
"should set `options.length` (= %i) to the maximum length when it is exceeded",
(length) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(length).toBeGreaterThan(builder.length.max);
@@ -352,7 +419,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.number === %s` when `options.minNumber` (= %i) is set to a value greater than 0",
(expectedNumber, minNumber) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, minNumber });
@@ -363,7 +430,7 @@ describe("Password generator options builder", () => {
);
it("should set `options.minNumber` to the minimum value when `options.number` is true", () => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, number: true });
@@ -373,7 +440,7 @@ describe("Password generator options builder", () => {
});
it("should set `options.minNumber` to 0 when `options.number` is false", () => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, number: false });
@@ -385,7 +452,7 @@ describe("Password generator options builder", () => {
it.each([1, 2, 3, 4])(
"should set `options.minNumber` (= %i) to the minimum it is less than the minimum number",
(minNumber) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.numberCount = 5; // arbitrary value greater than minNumber
expect(minNumber).toBeLessThan(policy.numberCount);
@@ -401,7 +468,7 @@ describe("Password generator options builder", () => {
it.each([1, 3, 5, 7, 9])(
"should not change `options.minNumber` (= %i) when it is within the boundaries",
(minNumber) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(minNumber).toBeGreaterThanOrEqual(builder.minDigits.min);
expect(minNumber).toBeLessThanOrEqual(builder.minDigits.max);
@@ -417,7 +484,7 @@ describe("Password generator options builder", () => {
it.each([10, 20, 400])(
"should set `options.minNumber` (= %i) to the maximum digit boundary when it is exceeded",
(minNumber) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(minNumber).toBeGreaterThan(builder.minDigits.max);
@@ -439,7 +506,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.special === %s` when `options.minSpecial` (= %i) is set to a value greater than 0",
(expectedSpecial, minSpecial) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, minSpecial });
@@ -450,7 +517,7 @@ describe("Password generator options builder", () => {
);
it("should set `options.minSpecial` to the minimum value when `options.special` is true", () => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, special: true });
@@ -460,7 +527,7 @@ describe("Password generator options builder", () => {
});
it("should set `options.minSpecial` to 0 when `options.special` is false", () => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, special: false });
@@ -472,7 +539,7 @@ describe("Password generator options builder", () => {
it.each([1, 2, 3, 4])(
"should set `options.minSpecial` (= %i) to the minimum it is less than the minimum special characters",
(minSpecial) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
policy.specialCount = 5; // arbitrary value greater than minSpecial
expect(minSpecial).toBeLessThan(policy.specialCount);
@@ -488,7 +555,7 @@ describe("Password generator options builder", () => {
it.each([1, 3, 5, 7, 9])(
"should not change `options.minSpecial` (= %i) when it is within the boundaries",
(minSpecial) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(minSpecial).toBeGreaterThanOrEqual(builder.minSpecialCharacters.min);
expect(minSpecial).toBeLessThanOrEqual(builder.minSpecialCharacters.max);
@@ -504,7 +571,7 @@ describe("Password generator options builder", () => {
it.each([10, 20, 400])(
"should set `options.minSpecial` (= %i) to the maximum special character boundary when it is exceeded",
(minSpecial) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(minSpecial).toBeGreaterThan(builder.minSpecialCharacters.max);
@@ -517,7 +584,7 @@ describe("Password generator options builder", () => {
);
it("should preserve unknown properties", () => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
unknown: "property",
@@ -540,7 +607,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.minLowercase === %i` when `options.lowercase` is %s",
(expectedMinLowercase, lowercase) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ lowercase, ...defaultOptions });
@@ -556,7 +623,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.minUppercase === %i` when `options.uppercase` is %s",
(expectedMinUppercase, uppercase) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ uppercase, ...defaultOptions });
@@ -572,7 +639,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.minNumber === %i` when `options.number` is %s and `options.minNumber` is not set",
(expectedMinNumber, number) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ number, ...defaultOptions });
@@ -590,7 +657,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.number === %s` when `options.minNumber` is %i and `options.number` is not set",
(expectedNumber, minNumber) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ minNumber, ...defaultOptions });
@@ -606,7 +673,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.minSpecial === %i` when `options.special` is %s and `options.minSpecial` is not set",
(special, expectedMinSpecial) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ special, ...defaultOptions });
@@ -624,7 +691,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.special === %s` when `options.minSpecial` is %i and `options.special` is not set",
(minSpecial, expectedSpecial) => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ minSpecial, ...defaultOptions });
@@ -645,7 +712,7 @@ describe("Password generator options builder", () => {
const sumOfMinimums = minLowercase + minUppercase + minNumber + minSpecial;
expect(sumOfMinimums).toBeLessThan(DefaultBoundaries.length.min);
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
minLowercase,
@@ -670,7 +737,7 @@ describe("Password generator options builder", () => {
(expectedMinLength, minLowercase, minUppercase, minNumber, minSpecial) => {
expect(expectedMinLength).toBeGreaterThanOrEqual(DefaultBoundaries.length.min);
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
minLowercase,
@@ -687,7 +754,7 @@ describe("Password generator options builder", () => {
);
it("should preserve unknown properties", () => {
const policy = new PasswordGeneratorPolicyOptions();
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
unknown: "property",

View File

@@ -1,6 +1,7 @@
import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options";
import { PolicyEvaluator } from "../abstractions/policy-evaluator.abstraction";
import { PasswordGenerationOptions } from "./password-generator-options";
import { PasswordGenerationOptions } from "./password-generation-options";
import { PasswordGeneratorPolicy } from "./password-generator-policy";
function initializeBoundaries() {
const length = Object.freeze({
@@ -37,7 +38,9 @@ type Boundary = {
/** Enforces policy for password generation.
*/
export class PasswordGeneratorOptionsEvaluator {
export class PasswordGeneratorOptionsEvaluator
implements PolicyEvaluator<PasswordGeneratorPolicy, PasswordGenerationOptions>
{
// This design is not ideal, but it is a step towards a more robust password
// generator. Ideally, `sanitize` would be implemented on an options class,
// and `applyPolicy` would be implemented on a policy class, "mise en place".
@@ -62,13 +65,13 @@ export class PasswordGeneratorOptionsEvaluator {
/** Policy applied by the evaluator.
*/
readonly policy: PasswordGeneratorPolicyOptions;
readonly policy: PasswordGeneratorPolicy;
/** Instantiates the evaluator.
* @param policy The policy applied by the evaluator. When this conflicts with
* the defaults, the policy takes precedence.
*/
constructor(policy: PasswordGeneratorPolicyOptions) {
constructor(policy: PasswordGeneratorPolicy) {
function createBoundary(value: number, defaultBoundary: Boundary): Boundary {
const boundary = {
min: Math.max(defaultBoundary.min, value),
@@ -78,7 +81,7 @@ export class PasswordGeneratorOptionsEvaluator {
return boundary;
}
this.policy = policy.clone();
this.policy = structuredClone(policy);
this.minDigits = createBoundary(policy.numberCount, DefaultBoundaries.minDigits);
this.minSpecialCharacters = createBoundary(
policy.specialCount,
@@ -96,12 +99,22 @@ export class PasswordGeneratorOptionsEvaluator {
};
}
/** Apply policy to a set of options.
* @param options The options to build from. These options are not altered.
* @returns A complete password generation request with policy applied.
* @remarks This method only applies policy overrides.
* Pass the result to `sanitize` to ensure consistency.
*/
/** {@link PolicyEvaluator.policyInEffect} */
get policyInEffect(): boolean {
const policies = [
this.policy.useUppercase,
this.policy.useLowercase,
this.policy.useNumbers,
this.policy.useSpecial,
this.policy.minLength > DefaultBoundaries.length.min,
this.policy.numberCount > DefaultBoundaries.minDigits.min,
this.policy.specialCount > DefaultBoundaries.minSpecialCharacters.min,
];
return policies.includes(true);
}
/** {@link PolicyEvaluator.applyPolicy} */
applyPolicy(options: PasswordGenerationOptions): PasswordGenerationOptions {
function fitToBounds(value: number, boundaries: Boundary) {
const { min, max } = boundaries;
@@ -137,13 +150,7 @@ export class PasswordGeneratorOptionsEvaluator {
};
}
/** Ensures internal options consistency.
* @param options The options to cascade. These options are not altered.
* @returns A new password generation request with cascade applied.
* @remarks This method fills null and undefined values by looking at
* pairs of flags and values (e.g. `number` and `minNumber`). If the flag
* and value are inconsistent, the flag cascades to the value.
*/
/** {@link PolicyEvaluator.sanitize} */
sanitize(options: PasswordGenerationOptions): PasswordGenerationOptions {
function cascade(enabled: boolean, value: number): [boolean, number] {
const enabledResult = enabled ?? value > 0;

View File

@@ -1,3 +1,5 @@
import { PasswordGenerationOptions } from "./password-generation-options";
/** Request format for credential generation.
* This type includes all properties suitable for reactive data binding.
*/
@@ -12,71 +14,6 @@ export type PasswordGeneratorOptions = PasswordGenerationOptions &
type?: "password" | "passphrase";
};
/** Request format for password credential generation.
* All members of this type may be `undefined` when the user is
* generating a passphrase.
*/
export type PasswordGenerationOptions = {
/** The length of the password selected by the user */
length?: number;
/** The minimum length of the password. This defaults to 5, and increases
* to ensure `minLength` is at least as large as the sum of the other minimums.
*/
minLength?: number;
/** `true` when ambiguous characters may be included in the output.
* `false` when ambiguous characters should not be included in the output.
*/
ambiguous?: boolean;
/** `true` when uppercase ASCII characters should be included in the output
* This value defaults to `false.
*/
uppercase?: boolean;
/** The minimum number of uppercase characters to include in the output.
* The value is ignored when `uppercase` is `false`.
* The value defaults to 1 when `uppercase` is `true`.
*/
minUppercase?: number;
/** `true` when lowercase ASCII characters should be included in the output.
* This value defaults to `false`.
*/
lowercase?: boolean;
/** The minimum number of lowercase characters to include in the output.
* The value defaults to 1 when `lowercase` is `true`.
* The value defaults to 0 when `lowercase` is `false`.
*/
minLowercase?: number;
/** Whether or not to include ASCII digits in the output
* This value defaults to `true` when `minNumber` is at least 1.
* This value defaults to `false` when `minNumber` is less than 1.
*/
number?: boolean;
/** The minimum number of digits to include in the output.
* The value defaults to 1 when `number` is `true`.
* The value defaults to 0 when `number` is `false`.
*/
minNumber?: number;
/** Whether or not to include special characters in the output.
* This value defaults to `true` when `minSpecial` is at least 1.
* This value defaults to `false` when `minSpecial` is less than 1.
*/
special?: boolean;
/** The minimum number of special characters to include in the output.
* This value defaults to 1 when `special` is `true`.
* This value defaults to 0 when `special` is `false`.
*/
minSpecial?: number;
};
/** Request format for passphrase credential generation.
* The members of this type may be `undefined` when the user is
* generating a password.

View File

@@ -0,0 +1,50 @@
/** Policy options enforced during password generation. */
export type PasswordGeneratorPolicy = {
/** The minimum length of generated passwords.
* When this is less than or equal to zero, it is ignored.
* If this is less than the total number of characters required by
* the policy's other settings, then it is ignored.
*/
minLength: number;
/** When this is true, an uppercase character must be part of
* the generated password.
*/
useUppercase: boolean;
/** When this is true, a lowercase character must be part of
* the generated password.
*/
useLowercase: boolean;
/** When this is true, at least one digit must be part of the generated
* password.
*/
useNumbers: boolean;
/** The quantity of digits to include in the generated password.
* When this is less than or equal to zero, it is ignored.
*/
numberCount: number;
/** When this is true, at least one digit must be part of the generated
* password.
*/
useSpecial: boolean;
/** The quantity of special characters to include in the generated
* password. When this is less than or equal to zero, it is ignored.
*/
specialCount: number;
};
/** The default options for password generation policy. */
export const DisabledPasswordGeneratorPolicy: PasswordGeneratorPolicy = Object.freeze({
minLength: 0,
useUppercase: false,
useLowercase: false,
useNumbers: false,
numberCount: 0,
useSpecial: false,
specialCount: 0,
});

View File

@@ -0,0 +1,108 @@
/**
* include structuredClone in test environment.
* @jest-environment ../../../../shared/test.environment.ts
*/
import { mock } from "jest-mock-extended";
import { PolicyType } from "../../../admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "../../../admin-console/models/domain/policy";
import { PASSWORD_SETTINGS } from "../key-definitions";
import {
PasswordGenerationServiceAbstraction,
PasswordGeneratorOptionsEvaluator,
PasswordGeneratorStrategy,
} from ".";
describe("Password generation strategy", () => {
describe("evaluator()", () => {
it("should throw if the policy type is incorrect", () => {
const strategy = new PasswordGeneratorStrategy(null);
const policy = mock<Policy>({
type: PolicyType.DisableSend,
});
expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+"));
});
it("should map to the policy evaluator", () => {
const strategy = new PasswordGeneratorStrategy(null);
const policy = mock<Policy>({
type: PolicyType.PasswordGenerator,
data: {
minLength: 10,
useUpper: true,
useLower: true,
useNumbers: true,
minNumbers: 1,
useSpecial: true,
minSpecial: 1,
},
});
const evaluator = strategy.evaluator(policy);
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
expect(evaluator.policy).toMatchObject({
minLength: 10,
useUppercase: true,
useLowercase: true,
useNumbers: true,
numberCount: 1,
useSpecial: true,
specialCount: 1,
});
});
});
describe("disk", () => {
it("should use password settings key", () => {
const legacy = mock<PasswordGenerationServiceAbstraction>();
const strategy = new PasswordGeneratorStrategy(legacy);
expect(strategy.disk).toBe(PASSWORD_SETTINGS);
});
});
describe("policy", () => {
it("should use password generator policy", () => {
const legacy = mock<PasswordGenerationServiceAbstraction>();
const strategy = new PasswordGeneratorStrategy(legacy);
expect(strategy.policy).toBe(PolicyType.PasswordGenerator);
});
});
describe("generate()", () => {
it("should call the legacy service with the given options", async () => {
const legacy = mock<PasswordGenerationServiceAbstraction>();
const strategy = new PasswordGeneratorStrategy(legacy);
const options = {
type: "password",
minLength: 1,
useUppercase: true,
useLowercase: true,
useNumbers: true,
numberCount: 1,
useSpecial: true,
specialCount: 1,
};
await strategy.generate(options);
expect(legacy.generatePassword).toHaveBeenCalledWith(options);
});
it("should set the generation type to password", async () => {
const legacy = mock<PasswordGenerationServiceAbstraction>();
const strategy = new PasswordGeneratorStrategy(legacy);
await strategy.generate({ type: "foo" } as any);
expect(legacy.generatePassword).toHaveBeenCalledWith({ type: "password" });
});
});
});

View File

@@ -0,0 +1,60 @@
import { GeneratorStrategy } from "..";
import { PolicyType } from "../../../admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "../../../admin-console/models/domain/policy";
import { PASSWORD_SETTINGS } from "../key-definitions";
import { PasswordGenerationOptions } from "./password-generation-options";
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
import { PasswordGeneratorPolicy } from "./password-generator-policy";
const ONE_MINUTE = 60 * 1000;
/** {@link GeneratorStrategy} */
export class PasswordGeneratorStrategy
implements GeneratorStrategy<PasswordGenerationOptions, PasswordGeneratorPolicy>
{
/** instantiates the password generator strategy.
* @param legacy generates the password
*/
constructor(private legacy: PasswordGenerationServiceAbstraction) {}
/** {@link GeneratorStrategy.disk} */
get disk() {
return PASSWORD_SETTINGS;
}
/** {@link GeneratorStrategy.policy} */
get policy() {
return PolicyType.PasswordGenerator;
}
get cache_ms() {
return ONE_MINUTE;
}
/** {@link GeneratorStrategy.evaluator} */
evaluator(policy: Policy): PasswordGeneratorOptionsEvaluator {
if (policy.type !== this.policy) {
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
throw Error("Mismatched policy type. " + details);
}
return new PasswordGeneratorOptionsEvaluator({
minLength: policy.data.minLength,
useUppercase: policy.data.useUpper,
useLowercase: policy.data.useLower,
useNumbers: policy.data.useNumbers,
numberCount: policy.data.minNumbers,
useSpecial: policy.data.useSpecial,
specialCount: policy.data.minSpecial,
});
}
/** {@link GeneratorStrategy.generate} */
generate(options: PasswordGenerationOptions): Promise<string> {
return this.legacy.generatePassword({ ...options, type: "password" });
}
}