mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-16 01:21:48 +01:00
[PM-14964] revert passphrase minimum (#12019)
* revert passphrase minimum * add recommendation text to browser refresh; hide hint text when value exceeds recommendation * migrate validators to generator configuration
This commit is contained in:
parent
15418659ad
commit
3521c54672
@ -2889,8 +2889,8 @@
|
|||||||
"generateEmail": {
|
"generateEmail": {
|
||||||
"message": "Generate email"
|
"message": "Generate email"
|
||||||
},
|
},
|
||||||
"generatorBoundariesHint": {
|
"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",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"min": {
|
"min": {
|
||||||
@ -2903,6 +2903,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"passwordLengthRecommendationHint": {
|
||||||
|
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
|
||||||
|
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||||
|
"placeholders": {
|
||||||
|
"recommended": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "14"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"passphraseNumWordsRecommendationHint": {
|
||||||
|
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
|
||||||
|
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||||
|
"placeholders": {
|
||||||
|
"recommended": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"usernameType": {
|
"usernameType": {
|
||||||
"message": "Username type"
|
"message": "Username type"
|
||||||
},
|
},
|
||||||
|
@ -2444,8 +2444,8 @@
|
|||||||
"generateEmail": {
|
"generateEmail": {
|
||||||
"message": "Generate email"
|
"message": "Generate email"
|
||||||
},
|
},
|
||||||
"generatorBoundariesHint": {
|
"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",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"min": {
|
"min": {
|
||||||
@ -2458,6 +2458,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"passwordLengthRecommendationHint": {
|
||||||
|
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
|
||||||
|
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||||
|
"placeholders": {
|
||||||
|
"recommended": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "14"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"passphraseNumWordsRecommendationHint": {
|
||||||
|
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
|
||||||
|
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||||
|
"placeholders": {
|
||||||
|
"recommended": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"usernameType": {
|
"usernameType": {
|
||||||
"message": "Username type"
|
"message": "Username type"
|
||||||
},
|
},
|
||||||
|
@ -5,7 +5,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 { DefaultPassphraseBoundaries, DefaultPasswordBoundaries } from "@bitwarden/generator-core";
|
import { Generators } from "@bitwarden/generator-core";
|
||||||
|
|
||||||
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||||
|
|
||||||
@ -26,8 +26,8 @@ export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
|
|||||||
minLength: [
|
minLength: [
|
||||||
null,
|
null,
|
||||||
[
|
[
|
||||||
Validators.min(DefaultPasswordBoundaries.length.min),
|
Validators.min(Generators.password.settings.constraints.length.min),
|
||||||
Validators.max(DefaultPasswordBoundaries.length.max),
|
Validators.max(Generators.password.settings.constraints.length.max),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
useUpper: [null],
|
useUpper: [null],
|
||||||
@ -37,22 +37,22 @@ export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
|
|||||||
minNumbers: [
|
minNumbers: [
|
||||||
null,
|
null,
|
||||||
[
|
[
|
||||||
Validators.min(DefaultPasswordBoundaries.minDigits.min),
|
Validators.min(Generators.password.settings.constraints.minNumber.min),
|
||||||
Validators.max(DefaultPasswordBoundaries.minDigits.max),
|
Validators.max(Generators.password.settings.constraints.minNumber.max),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
minSpecial: [
|
minSpecial: [
|
||||||
null,
|
null,
|
||||||
[
|
[
|
||||||
Validators.min(DefaultPasswordBoundaries.minSpecialCharacters.min),
|
Validators.min(Generators.password.settings.constraints.minSpecial.min),
|
||||||
Validators.max(DefaultPasswordBoundaries.minSpecialCharacters.max),
|
Validators.max(Generators.password.settings.constraints.minSpecial.max),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
minNumberWords: [
|
minNumberWords: [
|
||||||
null,
|
null,
|
||||||
[
|
[
|
||||||
Validators.min(DefaultPassphraseBoundaries.numWords.min),
|
Validators.min(Generators.passphrase.settings.constraints.numWords.min),
|
||||||
Validators.max(DefaultPassphraseBoundaries.numWords.max),
|
Validators.max(Generators.passphrase.settings.constraints.numWords.max),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
capitalize: [null],
|
capitalize: [null],
|
||||||
|
@ -6468,8 +6468,8 @@
|
|||||||
"generateEmail": {
|
"generateEmail": {
|
||||||
"message": "Generate email"
|
"message": "Generate email"
|
||||||
},
|
},
|
||||||
"generatorBoundariesHint": {
|
"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",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"min": {
|
"min": {
|
||||||
@ -6482,6 +6482,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"passwordLengthRecommendationHint": {
|
||||||
|
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
|
||||||
|
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||||
|
"placeholders": {
|
||||||
|
"recommended": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "14"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"passphraseNumWordsRecommendationHint": {
|
||||||
|
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
|
||||||
|
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
|
||||||
|
"placeholders": {
|
||||||
|
"recommended": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"usernameType": {
|
"usernameType": {
|
||||||
"message": "Username type"
|
"message": "Username type"
|
||||||
},
|
},
|
||||||
|
@ -28,6 +28,11 @@ type NumberConstraints = {
|
|||||||
/** maximum number value. When absent, min value is unbounded. */
|
/** maximum number value. When absent, min value is unbounded. */
|
||||||
max?: number;
|
max?: number;
|
||||||
|
|
||||||
|
/** recommended value. This is the value bitwarden recommends
|
||||||
|
* to the user as an appropriate value.
|
||||||
|
*/
|
||||||
|
recommendation?: number;
|
||||||
|
|
||||||
/** requires the number be a multiple of the step value;
|
/** requires the number be a multiple of the step value;
|
||||||
* this field must be a positive number. +0 and Infinity are
|
* this field must be a positive number. +0 and Infinity are
|
||||||
* prohibited. When absent, any number is accepted.
|
* prohibited. When absent, any number is accepted.
|
||||||
|
@ -82,9 +82,24 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
|||||||
const settings = await this.generatorService.settings(Generators.passphrase, { singleUserId$ });
|
const settings = await this.generatorService.settings(Generators.passphrase, { singleUserId$ });
|
||||||
|
|
||||||
// skips reactive event emissions to break a subscription cycle
|
// skips reactive event emissions to break a subscription cycle
|
||||||
settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
|
settings.withConstraints$
|
||||||
this.settings.patchValue(s, { emitEvent: false });
|
.pipe(takeUntil(this.destroyed$))
|
||||||
});
|
.subscribe(({ state, constraints }) => {
|
||||||
|
this.settings.patchValue(state, { emitEvent: false });
|
||||||
|
|
||||||
|
let boundariesHint = this.i18nService.t(
|
||||||
|
"spinboxBoundariesHint",
|
||||||
|
constraints.numWords.min?.toString(),
|
||||||
|
constraints.numWords.max?.toString(),
|
||||||
|
);
|
||||||
|
if (state.numWords <= (constraints.numWords.recommendation ?? 0)) {
|
||||||
|
boundariesHint += this.i18nService.t(
|
||||||
|
"passphraseNumWordsRecommendationHint",
|
||||||
|
constraints.numWords.recommendation?.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||||
@ -99,13 +114,6 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
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);
|
||||||
|
|
||||||
const boundariesHint = this.i18nService.t(
|
|
||||||
"generatorBoundariesHint",
|
|
||||||
constraints.numWords.min?.toString(),
|
|
||||||
constraints.numWords.max?.toString(),
|
|
||||||
);
|
|
||||||
this.numWordsBoundariesHint.next(boundariesHint);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// now that outputs are set up, connect inputs
|
// now that outputs are set up, connect inputs
|
||||||
|
@ -112,20 +112,33 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
const settings = await this.generatorService.settings(Generators.password, { singleUserId$ });
|
const settings = await this.generatorService.settings(Generators.password, { singleUserId$ });
|
||||||
|
|
||||||
// bind settings to the UI
|
// bind settings to the UI
|
||||||
settings
|
settings.withConstraints$
|
||||||
.pipe(
|
.pipe(
|
||||||
map((settings) => {
|
map(({ state, constraints }) => {
|
||||||
// interface is "avoid" while storage is "include"
|
// interface is "avoid" while storage is "include"
|
||||||
const s: any = { ...settings };
|
const s: any = { ...state };
|
||||||
s.avoidAmbiguous = !s.ambiguous;
|
s.avoidAmbiguous = !s.ambiguous;
|
||||||
delete s.ambiguous;
|
delete s.ambiguous;
|
||||||
return s;
|
return [s, constraints] as const;
|
||||||
}),
|
}),
|
||||||
takeUntil(this.destroyed$),
|
takeUntil(this.destroyed$),
|
||||||
)
|
)
|
||||||
.subscribe((s) => {
|
.subscribe(([state, constraints]) => {
|
||||||
|
let boundariesHint = this.i18nService.t(
|
||||||
|
"spinboxBoundariesHint",
|
||||||
|
constraints.length.min?.toString(),
|
||||||
|
constraints.length.max?.toString(),
|
||||||
|
);
|
||||||
|
if (state.length <= (constraints.length.recommendation ?? 0)) {
|
||||||
|
boundariesHint += this.i18nService.t(
|
||||||
|
"passwordLengthRecommendationHint",
|
||||||
|
constraints.length.recommendation?.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.lengthBoundariesHint.next(boundariesHint);
|
||||||
|
|
||||||
// skips reactive event emissions to break a subscription cycle
|
// skips reactive event emissions to break a subscription cycle
|
||||||
this.settings.patchValue(s, { emitEvent: false });
|
this.settings.patchValue(state, { emitEvent: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
// explain policy & disable policy-overridden fields
|
// explain policy & disable policy-overridden fields
|
||||||
@ -148,13 +161,6 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
for (const [control, enabled] of toggles) {
|
for (const [control, enabled] of toggles) {
|
||||||
this.toggleEnabled(control, enabled);
|
this.toggleEnabled(control, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundariesHint = this.i18nService.t(
|
|
||||||
"generatorBoundariesHint",
|
|
||||||
constraints.length.min?.toString(),
|
|
||||||
constraints.length.max?.toString(),
|
|
||||||
);
|
|
||||||
this.lengthBoundariesHint.next(boundariesHint);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// cascade selections between checkboxes and spinboxes
|
// cascade selections between checkboxes and spinboxes
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
function initializeBoundaries() {
|
function initializeBoundaries() {
|
||||||
const numWords = Object.freeze({
|
const numWords = Object.freeze({
|
||||||
min: 6,
|
min: 3,
|
||||||
max: 20,
|
max: 20,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -71,6 +71,7 @@ const PASSPHRASE: CredentialGeneratorConfiguration<
|
|||||||
numWords: {
|
numWords: {
|
||||||
min: DefaultPassphraseBoundaries.numWords.min,
|
min: DefaultPassphraseBoundaries.numWords.min,
|
||||||
max: DefaultPassphraseBoundaries.numWords.max,
|
max: DefaultPassphraseBoundaries.numWords.max,
|
||||||
|
recommendation: DefaultPassphraseGenerationOptions.numWords,
|
||||||
},
|
},
|
||||||
wordSeparator: { maxLength: 1 },
|
wordSeparator: { maxLength: 1 },
|
||||||
},
|
},
|
||||||
@ -101,7 +102,8 @@ const PASSPHRASE: CredentialGeneratorConfiguration<
|
|||||||
}),
|
}),
|
||||||
combine: passphraseLeastPrivilege,
|
combine: passphraseLeastPrivilege,
|
||||||
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
|
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
|
||||||
toConstraints: (policy) => new PassphrasePolicyConstraints(policy),
|
toConstraints: (policy) =>
|
||||||
|
new PassphrasePolicyConstraints(policy, PASSPHRASE.settings.constraints),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -130,6 +132,7 @@ const PASSWORD: CredentialGeneratorConfiguration<
|
|||||||
length: {
|
length: {
|
||||||
min: DefaultPasswordBoundaries.length.min,
|
min: DefaultPasswordBoundaries.length.min,
|
||||||
max: DefaultPasswordBoundaries.length.max,
|
max: DefaultPasswordBoundaries.length.max,
|
||||||
|
recommendation: DefaultPasswordGenerationOptions.length,
|
||||||
},
|
},
|
||||||
minNumber: {
|
minNumber: {
|
||||||
min: DefaultPasswordBoundaries.minDigits.min,
|
min: DefaultPasswordBoundaries.minDigits.min,
|
||||||
@ -177,7 +180,8 @@ const PASSWORD: CredentialGeneratorConfiguration<
|
|||||||
}),
|
}),
|
||||||
combine: passwordLeastPrivilege,
|
combine: passwordLeastPrivilege,
|
||||||
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
|
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
|
||||||
toConstraints: (policy) => new DynamicPasswordPolicyConstraints(policy),
|
toConstraints: (policy) =>
|
||||||
|
new DynamicPasswordPolicyConstraints(policy, PASSWORD.settings.constraints),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,28 +1,36 @@
|
|||||||
import { DefaultPasswordBoundaries, DefaultPasswordGenerationOptions, Policies } from "../data";
|
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
|
||||||
|
|
||||||
|
import { Generators } from "../data";
|
||||||
|
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>;
|
||||||
|
const defaultOptions = accoutSettings.initial;
|
||||||
|
const disabledPolicy = Generators.password.policy.disabledValue;
|
||||||
|
const someConstraints = Generators.password.settings.constraints;
|
||||||
|
|
||||||
describe("DynamicPasswordPolicyConstraints", () => {
|
describe("DynamicPasswordPolicyConstraints", () => {
|
||||||
describe("constructor", () => {
|
describe("constructor", () => {
|
||||||
it("uses default boundaries when the policy is disabled", () => {
|
it("uses default boundaries when the policy is disabled", () => {
|
||||||
const { constraints } = new DynamicPasswordPolicyConstraints(Policies.Password.disabledValue);
|
const { constraints } = new DynamicPasswordPolicyConstraints(disabledPolicy, someConstraints);
|
||||||
|
|
||||||
expect(constraints.policyInEffect).toBeFalsy();
|
expect(constraints.policyInEffect).toBeFalsy();
|
||||||
expect(constraints.length).toEqual(DefaultPasswordBoundaries.length);
|
expect(constraints.length).toEqual(someConstraints.length);
|
||||||
expect(constraints.lowercase).toBeUndefined();
|
expect(constraints.lowercase).toBeUndefined();
|
||||||
expect(constraints.uppercase).toBeUndefined();
|
expect(constraints.uppercase).toBeUndefined();
|
||||||
expect(constraints.number).toBeUndefined();
|
expect(constraints.number).toBeUndefined();
|
||||||
expect(constraints.special).toBeUndefined();
|
expect(constraints.special).toBeUndefined();
|
||||||
expect(constraints.minLowercase).toBeUndefined();
|
expect(constraints.minLowercase).toBeUndefined();
|
||||||
expect(constraints.minUppercase).toBeUndefined();
|
expect(constraints.minUppercase).toBeUndefined();
|
||||||
expect(constraints.minNumber).toEqual(DefaultPasswordBoundaries.minDigits);
|
expect(constraints.minNumber).toEqual(someConstraints.minNumber);
|
||||||
expect(constraints.minSpecial).toEqual(DefaultPasswordBoundaries.minSpecialCharacters);
|
expect(constraints.minSpecial).toEqual(someConstraints.minSpecial);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("1 <= minLowercase when the policy requires lowercase", () => {
|
it("1 <= minLowercase when the policy requires lowercase", () => {
|
||||||
const policy = { ...Policies.Password.disabledValue, useLowercase: true };
|
const policy = { ...disabledPolicy, useLowercase: true };
|
||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy);
|
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);
|
||||||
@ -31,8 +39,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("1 <= minUppercase when the policy requires uppercase", () => {
|
it("1 <= minUppercase when the policy requires uppercase", () => {
|
||||||
const policy = { ...Policies.Password.disabledValue, useUppercase: true };
|
const policy = { ...disabledPolicy, useUppercase: true };
|
||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy);
|
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);
|
||||||
@ -41,8 +49,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("1 <= minNumber <= 9 when the policy requires a number", () => {
|
it("1 <= minNumber <= 9 when the policy requires a number", () => {
|
||||||
const policy = { ...Policies.Password.disabledValue, useNumbers: true };
|
const policy = { ...disabledPolicy, useNumbers: true };
|
||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy);
|
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);
|
||||||
@ -51,8 +59,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("1 <= minSpecial <= 9 when the policy requires a special character", () => {
|
it("1 <= minSpecial <= 9 when the policy requires a special character", () => {
|
||||||
const policy = { ...Policies.Password.disabledValue, useSpecial: true };
|
const policy = { ...disabledPolicy, useSpecial: true };
|
||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy);
|
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);
|
||||||
@ -61,8 +69,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("numberCount <= minNumber <= 9 when the policy requires numberCount", () => {
|
it("numberCount <= minNumber <= 9 when the policy requires numberCount", () => {
|
||||||
const policy = { ...Policies.Password.disabledValue, useNumbers: true, numberCount: 2 };
|
const policy = { ...disabledPolicy, useNumbers: true, numberCount: 2 };
|
||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy);
|
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);
|
||||||
@ -71,8 +79,8 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("specialCount <= minSpecial <= 9 when the policy requires specialCount", () => {
|
it("specialCount <= minSpecial <= 9 when the policy requires specialCount", () => {
|
||||||
const policy = { ...Policies.Password.disabledValue, useSpecial: true, specialCount: 2 };
|
const policy = { ...disabledPolicy, useSpecial: true, specialCount: 2 };
|
||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy);
|
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);
|
||||||
@ -81,16 +89,16 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("uses the policy's minimum length when the policy defines one", () => {
|
it("uses the policy's minimum length when the policy defines one", () => {
|
||||||
const policy = { ...Policies.Password.disabledValue, minLength: 10 };
|
const policy = { ...disabledPolicy, minLength: 10 };
|
||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy);
|
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||||
|
|
||||||
expect(constraints.policyInEffect).toBeTruthy();
|
expect(constraints.policyInEffect).toBeTruthy();
|
||||||
expect(constraints.length).toEqual({ min: 10, max: 128 });
|
expect(constraints.length).toEqual({ ...someConstraints.length, min: 10 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("overrides the minimum length when it is less than the sum of minimums", () => {
|
it("overrides the minimum length when it is less than the sum of minimums", () => {
|
||||||
const policy = {
|
const policy = {
|
||||||
...Policies.Password.disabledValue,
|
...disabledPolicy,
|
||||||
useUppercase: true,
|
useUppercase: true,
|
||||||
useLowercase: true,
|
useLowercase: true,
|
||||||
useNumbers: true,
|
useNumbers: true,
|
||||||
@ -98,24 +106,27 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
useSpecial: true,
|
useSpecial: true,
|
||||||
specialCount: 5,
|
specialCount: 5,
|
||||||
};
|
};
|
||||||
const { constraints } = new DynamicPasswordPolicyConstraints(policy);
|
const { constraints } = new DynamicPasswordPolicyConstraints(policy, someConstraints);
|
||||||
|
|
||||||
// lower + upper + number + special = 1 + 1 + 5 + 5 = 12
|
// lower + upper + number + special = 1 + 1 + 5 + 5 = 12
|
||||||
expect(constraints.length).toEqual({ min: 12, max: 128 });
|
expect(constraints.length).toEqual({ ...someConstraints.length, min: 12 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("calibrate", () => {
|
describe("calibrate", () => {
|
||||||
it("copies the boolean constraints into the calibration", () => {
|
it("copies the boolean constraints into the calibration", () => {
|
||||||
const dynamic = new DynamicPasswordPolicyConstraints({
|
const dynamic = new DynamicPasswordPolicyConstraints(
|
||||||
...Policies.Password.disabledValue,
|
{
|
||||||
useUppercase: true,
|
...disabledPolicy,
|
||||||
useLowercase: true,
|
useUppercase: true,
|
||||||
useNumbers: true,
|
useLowercase: true,
|
||||||
useSpecial: true,
|
useNumbers: true,
|
||||||
});
|
useSpecial: true,
|
||||||
|
},
|
||||||
|
someConstraints,
|
||||||
|
);
|
||||||
|
|
||||||
const calibrated = dynamic.calibrate(DefaultPasswordGenerationOptions);
|
const calibrated = dynamic.calibrate(defaultOptions);
|
||||||
|
|
||||||
expect(calibrated.constraints.uppercase).toEqual(dynamic.constraints.uppercase);
|
expect(calibrated.constraints.uppercase).toEqual(dynamic.constraints.uppercase);
|
||||||
expect(calibrated.constraints.lowercase).toEqual(dynamic.constraints.lowercase);
|
expect(calibrated.constraints.lowercase).toEqual(dynamic.constraints.lowercase);
|
||||||
@ -126,12 +137,15 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
it.each([[true], [false], [undefined]])(
|
it.each([[true], [false], [undefined]])(
|
||||||
"outputs at least 1 constraint when the state's lowercase flag is true and useLowercase is %p",
|
"outputs at least 1 constraint when the state's lowercase flag is true and useLowercase is %p",
|
||||||
(useLowercase) => {
|
(useLowercase) => {
|
||||||
const dynamic = new DynamicPasswordPolicyConstraints({
|
const dynamic = new DynamicPasswordPolicyConstraints(
|
||||||
...Policies.Password.disabledValue,
|
{
|
||||||
useLowercase,
|
...disabledPolicy,
|
||||||
});
|
useLowercase,
|
||||||
|
},
|
||||||
|
someConstraints,
|
||||||
|
);
|
||||||
const state = {
|
const state = {
|
||||||
...DefaultPasswordGenerationOptions,
|
...defaultOptions,
|
||||||
lowercase: true,
|
lowercase: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -142,9 +156,9 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("outputs the `minLowercase` constraint when the state's lowercase flag is true and policy is disabled", () => {
|
it("outputs the `minLowercase` constraint when the state's lowercase flag is true and policy is disabled", () => {
|
||||||
const dynamic = new DynamicPasswordPolicyConstraints(Policies.Password.disabledValue);
|
const dynamic = new DynamicPasswordPolicyConstraints(disabledPolicy, someConstraints);
|
||||||
const state = {
|
const state = {
|
||||||
...DefaultPasswordGenerationOptions,
|
...defaultOptions,
|
||||||
lowercase: true,
|
lowercase: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -154,9 +168,9 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("disables the minLowercase constraint when the state's lowercase flag is false", () => {
|
it("disables the minLowercase constraint when the state's lowercase flag is false", () => {
|
||||||
const dynamic = new DynamicPasswordPolicyConstraints(Policies.Password.disabledValue);
|
const dynamic = new DynamicPasswordPolicyConstraints(disabledPolicy, someConstraints);
|
||||||
const state = {
|
const state = {
|
||||||
...DefaultPasswordGenerationOptions,
|
...defaultOptions,
|
||||||
lowercase: false,
|
lowercase: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -168,12 +182,15 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
it.each([[true], [false], [undefined]])(
|
it.each([[true], [false], [undefined]])(
|
||||||
"outputs at least 1 constraint when the state's uppercase flag is true and useUppercase is %p",
|
"outputs at least 1 constraint when the state's uppercase flag is true and useUppercase is %p",
|
||||||
(useUppercase) => {
|
(useUppercase) => {
|
||||||
const dynamic = new DynamicPasswordPolicyConstraints({
|
const dynamic = new DynamicPasswordPolicyConstraints(
|
||||||
...Policies.Password.disabledValue,
|
{
|
||||||
useUppercase,
|
...disabledPolicy,
|
||||||
});
|
useUppercase,
|
||||||
|
},
|
||||||
|
someConstraints,
|
||||||
|
);
|
||||||
const state = {
|
const state = {
|
||||||
...DefaultPasswordGenerationOptions,
|
...defaultOptions,
|
||||||
uppercase: true,
|
uppercase: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -184,9 +201,9 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("disables the minUppercase constraint when the state's uppercase flag is false", () => {
|
it("disables the minUppercase constraint when the state's uppercase flag is false", () => {
|
||||||
const dynamic = new DynamicPasswordPolicyConstraints(Policies.Password.disabledValue);
|
const dynamic = new DynamicPasswordPolicyConstraints(disabledPolicy, someConstraints);
|
||||||
const state = {
|
const state = {
|
||||||
...DefaultPasswordGenerationOptions,
|
...defaultOptions,
|
||||||
uppercase: false,
|
uppercase: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -196,9 +213,9 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("outputs the minNumber constraint when the state's number flag is true", () => {
|
it("outputs the minNumber constraint when the state's number flag is true", () => {
|
||||||
const dynamic = new DynamicPasswordPolicyConstraints(Policies.Password.disabledValue);
|
const dynamic = new DynamicPasswordPolicyConstraints(disabledPolicy, someConstraints);
|
||||||
const state = {
|
const state = {
|
||||||
...DefaultPasswordGenerationOptions,
|
...defaultOptions,
|
||||||
number: true,
|
number: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -208,9 +225,9 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("outputs the zero constraint when the state's number flag is false", () => {
|
it("outputs the zero constraint when the state's number flag is false", () => {
|
||||||
const dynamic = new DynamicPasswordPolicyConstraints(Policies.Password.disabledValue);
|
const dynamic = new DynamicPasswordPolicyConstraints(disabledPolicy, someConstraints);
|
||||||
const state = {
|
const state = {
|
||||||
...DefaultPasswordGenerationOptions,
|
...defaultOptions,
|
||||||
number: false,
|
number: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -220,9 +237,9 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("outputs the minSpecial constraint when the state's special flag is true", () => {
|
it("outputs the minSpecial constraint when the state's special flag is true", () => {
|
||||||
const dynamic = new DynamicPasswordPolicyConstraints(Policies.Password.disabledValue);
|
const dynamic = new DynamicPasswordPolicyConstraints(disabledPolicy, someConstraints);
|
||||||
const state = {
|
const state = {
|
||||||
...DefaultPasswordGenerationOptions,
|
...defaultOptions,
|
||||||
special: true,
|
special: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -232,9 +249,9 @@ describe("DynamicPasswordPolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("outputs the zero constraint when the state's special flag is false", () => {
|
it("outputs the zero constraint when the state's special flag is false", () => {
|
||||||
const dynamic = new DynamicPasswordPolicyConstraints(Policies.Password.disabledValue);
|
const dynamic = new DynamicPasswordPolicyConstraints(disabledPolicy, someConstraints);
|
||||||
const state = {
|
const state = {
|
||||||
...DefaultPasswordGenerationOptions,
|
...defaultOptions,
|
||||||
special: false,
|
special: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
|
Constraints,
|
||||||
DynamicStateConstraints,
|
DynamicStateConstraints,
|
||||||
PolicyConstraints,
|
PolicyConstraints,
|
||||||
StateConstraints,
|
StateConstraints,
|
||||||
} from "@bitwarden/common/tools/types";
|
} from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
import { DefaultPasswordBoundaries } from "../data";
|
|
||||||
import { PasswordGeneratorPolicy, PasswordGeneratorSettings } from "../types";
|
import { PasswordGeneratorPolicy, PasswordGeneratorSettings } from "../types";
|
||||||
|
|
||||||
import { atLeast, atLeastSum, maybe, readonlyTrueWhen, AtLeastOne, Zero } from "./constraints";
|
import { atLeast, atLeastSum, maybe, readonlyTrueWhen, AtLeastOne, Zero } from "./constraints";
|
||||||
@ -18,26 +18,29 @@ export class DynamicPasswordPolicyConstraints
|
|||||||
* @param policy the password policy to enforce. This cannot be
|
* @param policy the password policy to enforce. This cannot be
|
||||||
* `null` or `undefined`.
|
* `null` or `undefined`.
|
||||||
*/
|
*/
|
||||||
constructor(policy: PasswordGeneratorPolicy) {
|
constructor(
|
||||||
|
policy: PasswordGeneratorPolicy,
|
||||||
|
readonly defaults: Constraints<PasswordGeneratorSettings>,
|
||||||
|
) {
|
||||||
const minLowercase = maybe(policy.useLowercase, AtLeastOne);
|
const minLowercase = maybe(policy.useLowercase, AtLeastOne);
|
||||||
const minUppercase = maybe(policy.useUppercase, AtLeastOne);
|
const minUppercase = maybe(policy.useUppercase, AtLeastOne);
|
||||||
|
|
||||||
const minNumber = atLeast(
|
const minNumber = atLeast(
|
||||||
policy.numberCount || (policy.useNumbers && AtLeastOne.min),
|
policy.numberCount || (policy.useNumbers && AtLeastOne.min),
|
||||||
DefaultPasswordBoundaries.minDigits,
|
defaults.minNumber,
|
||||||
);
|
);
|
||||||
|
|
||||||
const minSpecial = atLeast(
|
const minSpecial = atLeast(
|
||||||
policy.specialCount || (policy.useSpecial && AtLeastOne.min),
|
policy.specialCount || (policy.useSpecial && AtLeastOne.min),
|
||||||
DefaultPasswordBoundaries.minSpecialCharacters,
|
defaults.minSpecial,
|
||||||
);
|
);
|
||||||
|
|
||||||
const baseLength = atLeast(policy.minLength, DefaultPasswordBoundaries.length);
|
const baseLength = atLeast(policy.minLength, defaults.length);
|
||||||
const subLengths = [minLowercase, minUppercase, minNumber, minSpecial];
|
const subLengths = [minLowercase, minUppercase, minNumber, minSpecial];
|
||||||
const length = atLeastSum(baseLength, subLengths);
|
const length = atLeastSum(baseLength, subLengths);
|
||||||
|
|
||||||
this.constraints = Object.freeze({
|
this.constraints = Object.freeze({
|
||||||
policyInEffect: policyInEffect(policy),
|
policyInEffect: policyInEffect(policy, defaults),
|
||||||
lowercase: readonlyTrueWhen(policy.useLowercase),
|
lowercase: readonlyTrueWhen(policy.useLowercase),
|
||||||
uppercase: readonlyTrueWhen(policy.useUppercase),
|
uppercase: readonlyTrueWhen(policy.useUppercase),
|
||||||
number: readonlyTrueWhen(policy.useNumbers),
|
number: readonlyTrueWhen(policy.useNumbers),
|
||||||
@ -85,15 +88,18 @@ export class DynamicPasswordPolicyConstraints
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function policyInEffect(policy: PasswordGeneratorPolicy): boolean {
|
function policyInEffect(
|
||||||
|
policy: PasswordGeneratorPolicy,
|
||||||
|
defaults: Constraints<PasswordGeneratorSettings>,
|
||||||
|
): boolean {
|
||||||
const policies = [
|
const policies = [
|
||||||
policy.useUppercase,
|
policy.useUppercase,
|
||||||
policy.useLowercase,
|
policy.useLowercase,
|
||||||
policy.useNumbers,
|
policy.useNumbers,
|
||||||
policy.useSpecial,
|
policy.useSpecial,
|
||||||
policy.minLength > DefaultPasswordBoundaries.length.min,
|
policy.minLength > defaults.length.min,
|
||||||
policy.numberCount > DefaultPasswordBoundaries.minDigits.min,
|
policy.numberCount > defaults.minNumber.min,
|
||||||
policy.specialCount > DefaultPasswordBoundaries.minSpecialCharacters.min,
|
policy.specialCount > defaults.minSpecial.min,
|
||||||
];
|
];
|
||||||
|
|
||||||
return policies.includes(true);
|
return policies.includes(true);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DefaultPassphraseBoundaries, Policies } from "../data";
|
import { Generators } from "../data";
|
||||||
|
|
||||||
import { PassphrasePolicyConstraints } from "./passphrase-policy-constraints";
|
import { PassphrasePolicyConstraints } from "./passphrase-policy-constraints";
|
||||||
|
|
||||||
@ -9,54 +9,66 @@ const SomeSettings = {
|
|||||||
wordSeparator: "-",
|
wordSeparator: "-",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const disabledPolicy = Generators.passphrase.policy.disabledValue;
|
||||||
|
const someConstraints = Generators.passphrase.settings.constraints;
|
||||||
|
|
||||||
describe("PassphrasePolicyConstraints", () => {
|
describe("PassphrasePolicyConstraints", () => {
|
||||||
describe("constructor", () => {
|
describe("constructor", () => {
|
||||||
it("uses default boundaries when the policy is disabled", () => {
|
it("uses default boundaries when the policy is disabled", () => {
|
||||||
const { constraints } = new PassphrasePolicyConstraints(Policies.Passphrase.disabledValue);
|
const { constraints } = new PassphrasePolicyConstraints(disabledPolicy, someConstraints);
|
||||||
|
|
||||||
expect(constraints.policyInEffect).toBeFalsy();
|
expect(constraints.policyInEffect).toBeFalsy();
|
||||||
expect(constraints.capitalize).toBeUndefined();
|
expect(constraints.capitalize).toBeUndefined();
|
||||||
expect(constraints.includeNumber).toBeUndefined();
|
expect(constraints.includeNumber).toBeUndefined();
|
||||||
expect(constraints.numWords).toEqual(DefaultPassphraseBoundaries.numWords);
|
expect(constraints.numWords).toEqual(someConstraints.numWords);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("requires capitalization when the policy requires capitalization", () => {
|
it("requires capitalization when the policy requires capitalization", () => {
|
||||||
const { constraints } = new PassphrasePolicyConstraints({
|
const { constraints } = new PassphrasePolicyConstraints(
|
||||||
...Policies.Passphrase.disabledValue,
|
{
|
||||||
capitalize: true,
|
...disabledPolicy,
|
||||||
});
|
capitalize: true,
|
||||||
|
},
|
||||||
|
someConstraints,
|
||||||
|
);
|
||||||
|
|
||||||
expect(constraints.policyInEffect).toBeTruthy();
|
expect(constraints.policyInEffect).toBeTruthy();
|
||||||
expect(constraints.capitalize).toMatchObject({ readonly: true, requiredValue: true });
|
expect(constraints.capitalize).toMatchObject({ readonly: true, requiredValue: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("requires a number when the policy requires a number", () => {
|
it("requires a number when the policy requires a number", () => {
|
||||||
const { constraints } = new PassphrasePolicyConstraints({
|
const { constraints } = new PassphrasePolicyConstraints(
|
||||||
...Policies.Passphrase.disabledValue,
|
{
|
||||||
includeNumber: true,
|
...disabledPolicy,
|
||||||
});
|
includeNumber: true,
|
||||||
|
},
|
||||||
|
someConstraints,
|
||||||
|
);
|
||||||
|
|
||||||
expect(constraints.policyInEffect).toBeTruthy();
|
expect(constraints.policyInEffect).toBeTruthy();
|
||||||
expect(constraints.includeNumber).toMatchObject({ readonly: true, requiredValue: true });
|
expect(constraints.includeNumber).toMatchObject({ readonly: true, requiredValue: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("minNumberWords <= numWords.min when the policy requires numberCount", () => {
|
it("minNumberWords <= numWords.min when the policy requires numberCount", () => {
|
||||||
const { constraints } = new PassphrasePolicyConstraints({
|
const { constraints } = new PassphrasePolicyConstraints(
|
||||||
...Policies.Passphrase.disabledValue,
|
{
|
||||||
minNumberWords: 10,
|
...disabledPolicy,
|
||||||
});
|
minNumberWords: 10,
|
||||||
|
},
|
||||||
|
someConstraints,
|
||||||
|
);
|
||||||
|
|
||||||
expect(constraints.policyInEffect).toBeTruthy();
|
expect(constraints.policyInEffect).toBeTruthy();
|
||||||
expect(constraints.numWords).toMatchObject({
|
expect(constraints.numWords).toMatchObject({
|
||||||
min: 10,
|
min: 10,
|
||||||
max: DefaultPassphraseBoundaries.numWords.max,
|
max: someConstraints.numWords.max,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("adjust", () => {
|
describe("adjust", () => {
|
||||||
it("allows an empty word separator", () => {
|
it("allows an empty word separator", () => {
|
||||||
const policy = new PassphrasePolicyConstraints(Policies.Passphrase.disabledValue);
|
const policy = new PassphrasePolicyConstraints(disabledPolicy, someConstraints);
|
||||||
|
|
||||||
const { wordSeparator } = policy.adjust({ ...SomeSettings, wordSeparator: "" });
|
const { wordSeparator } = policy.adjust({ ...SomeSettings, wordSeparator: "" });
|
||||||
|
|
||||||
@ -64,7 +76,7 @@ describe("PassphrasePolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("takes only the first character of wordSeparator", () => {
|
it("takes only the first character of wordSeparator", () => {
|
||||||
const policy = new PassphrasePolicyConstraints(Policies.Passphrase.disabledValue);
|
const policy = new PassphrasePolicyConstraints(disabledPolicy, someConstraints);
|
||||||
|
|
||||||
const { wordSeparator } = policy.adjust({ ...SomeSettings, wordSeparator: "?." });
|
const { wordSeparator } = policy.adjust({ ...SomeSettings, wordSeparator: "?." });
|
||||||
|
|
||||||
@ -72,26 +84,32 @@ describe("PassphrasePolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[1, 6],
|
[1, someConstraints.numWords.min, 3, someConstraints.numWords.max],
|
||||||
[21, 20],
|
[21, someConstraints.numWords.min, 20, someConstraints.numWords.max],
|
||||||
])("fits numWords (=%p) within the default bounds (6 <= %p <= 20)", (value, expected) => {
|
])(
|
||||||
const policy = new PassphrasePolicyConstraints(Policies.Passphrase.disabledValue);
|
`fits numWords (=%p) within the default bounds (%p <= %p <= %p)`,
|
||||||
|
(value, _, expected, __) => {
|
||||||
|
const policy = new PassphrasePolicyConstraints(disabledPolicy, someConstraints);
|
||||||
|
|
||||||
const { numWords } = policy.adjust({ ...SomeSettings, numWords: value });
|
const { numWords } = policy.adjust({ ...SomeSettings, numWords: value });
|
||||||
|
|
||||||
expect(numWords).toEqual(expected);
|
expect(numWords).toEqual(expected);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[1, 6, 6],
|
[1, 6, 6, someConstraints.numWords.max],
|
||||||
[21, 20, 20],
|
[21, 20, 20, someConstraints.numWords.max],
|
||||||
])(
|
])(
|
||||||
"fits numWords (=%p) within the policy bounds (%p <= %p <= 20)",
|
"fits numWords (=%p) within the policy bounds (%p <= %p <= %p)",
|
||||||
(value, minNumberWords, expected) => {
|
(value, minNumberWords, expected, _) => {
|
||||||
const policy = new PassphrasePolicyConstraints({
|
const policy = new PassphrasePolicyConstraints(
|
||||||
...Policies.Passphrase.disabledValue,
|
{
|
||||||
minNumberWords,
|
...disabledPolicy,
|
||||||
});
|
minNumberWords,
|
||||||
|
},
|
||||||
|
someConstraints,
|
||||||
|
);
|
||||||
|
|
||||||
const { numWords } = policy.adjust({ ...SomeSettings, numWords: value });
|
const { numWords } = policy.adjust({ ...SomeSettings, numWords: value });
|
||||||
|
|
||||||
@ -100,10 +118,13 @@ describe("PassphrasePolicyConstraints", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("sets capitalize to true when the policy requires it", () => {
|
it("sets capitalize to true when the policy requires it", () => {
|
||||||
const policy = new PassphrasePolicyConstraints({
|
const policy = new PassphrasePolicyConstraints(
|
||||||
...Policies.Passphrase.disabledValue,
|
{
|
||||||
capitalize: true,
|
...disabledPolicy,
|
||||||
});
|
capitalize: true,
|
||||||
|
},
|
||||||
|
someConstraints,
|
||||||
|
);
|
||||||
|
|
||||||
const { capitalize } = policy.adjust({ ...SomeSettings, capitalize: false });
|
const { capitalize } = policy.adjust({ ...SomeSettings, capitalize: false });
|
||||||
|
|
||||||
@ -111,10 +132,13 @@ describe("PassphrasePolicyConstraints", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sets includeNumber to true when the policy requires it", () => {
|
it("sets includeNumber to true when the policy requires it", () => {
|
||||||
const policy = new PassphrasePolicyConstraints({
|
const policy = new PassphrasePolicyConstraints(
|
||||||
...Policies.Passphrase.disabledValue,
|
{
|
||||||
includeNumber: true,
|
...disabledPolicy,
|
||||||
});
|
includeNumber: true,
|
||||||
|
},
|
||||||
|
someConstraints,
|
||||||
|
);
|
||||||
|
|
||||||
const { includeNumber } = policy.adjust({ ...SomeSettings, capitalize: false });
|
const { includeNumber } = policy.adjust({ ...SomeSettings, capitalize: false });
|
||||||
|
|
||||||
@ -124,7 +148,7 @@ describe("PassphrasePolicyConstraints", () => {
|
|||||||
|
|
||||||
describe("fix", () => {
|
describe("fix", () => {
|
||||||
it("returns its input", () => {
|
it("returns its input", () => {
|
||||||
const policy = new PassphrasePolicyConstraints(Policies.Passphrase.disabledValue);
|
const policy = new PassphrasePolicyConstraints(disabledPolicy, someConstraints);
|
||||||
|
|
||||||
const result = policy.fix(SomeSettings);
|
const result = policy.fix(SomeSettings);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { PolicyConstraints, StateConstraints } from "@bitwarden/common/tools/types";
|
import { Constraints, PolicyConstraints, StateConstraints } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
import { DefaultPassphraseBoundaries, DefaultPassphraseGenerationOptions } from "../data";
|
import { DefaultPassphraseGenerationOptions } from "../data";
|
||||||
import { PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
|
import { PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
|
||||||
|
|
||||||
import { atLeast, enforceConstant, fitLength, fitToBounds, readonlyTrueWhen } from "./constraints";
|
import { atLeast, enforceConstant, fitLength, fitToBounds, readonlyTrueWhen } from "./constraints";
|
||||||
@ -10,13 +10,16 @@ export class PassphrasePolicyConstraints implements StateConstraints<PassphraseG
|
|||||||
* @param policy the password policy to enforce. This cannot be
|
* @param policy the password policy to enforce. This cannot be
|
||||||
* `null` or `undefined`.
|
* `null` or `undefined`.
|
||||||
*/
|
*/
|
||||||
constructor(readonly policy: PassphraseGeneratorPolicy) {
|
constructor(
|
||||||
|
readonly policy: PassphraseGeneratorPolicy,
|
||||||
|
readonly defaults: Constraints<PassphraseGenerationOptions>,
|
||||||
|
) {
|
||||||
this.constraints = {
|
this.constraints = {
|
||||||
policyInEffect: policyInEffect(policy),
|
policyInEffect: policyInEffect(policy, defaults),
|
||||||
wordSeparator: { minLength: 0, maxLength: 1 },
|
wordSeparator: { minLength: 0, maxLength: 1 },
|
||||||
capitalize: readonlyTrueWhen(policy.capitalize),
|
capitalize: readonlyTrueWhen(policy.capitalize),
|
||||||
includeNumber: readonlyTrueWhen(policy.includeNumber),
|
includeNumber: readonlyTrueWhen(policy.includeNumber),
|
||||||
numWords: atLeast(policy.minNumberWords, DefaultPassphraseBoundaries.numWords),
|
numWords: atLeast(policy.minNumberWords, defaults.numWords),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,11 +43,14 @@ export class PassphrasePolicyConstraints implements StateConstraints<PassphraseG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function policyInEffect(policy: PassphraseGeneratorPolicy): boolean {
|
function policyInEffect(
|
||||||
|
policy: PassphraseGeneratorPolicy,
|
||||||
|
defaults: Constraints<PassphraseGenerationOptions>,
|
||||||
|
): boolean {
|
||||||
const policies = [
|
const policies = [
|
||||||
policy.capitalize,
|
policy.capitalize,
|
||||||
policy.includeNumber,
|
policy.includeNumber,
|
||||||
policy.minNumberWords > DefaultPassphraseBoundaries.numWords.min,
|
policy.minNumberWords > defaults.numWords.min,
|
||||||
];
|
];
|
||||||
|
|
||||||
return policies.includes(true);
|
return policies.includes(true);
|
||||||
|
Loading…
Reference in New Issue
Block a user