1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-5609] passphrase settings component & services (#10535)

This commit is contained in:
✨ Audrey ✨
2024-08-28 09:26:17 -04:00
committed by GitHub
parent 819c312ce2
commit 8c4b8d71ea
34 changed files with 1147 additions and 144 deletions

View File

@@ -1,8 +0,0 @@
import { PassphraseGeneratorPolicy } from "../types";
/** The default options for password generation policy. */
export const DisabledPassphraseGeneratorPolicy: PassphraseGeneratorPolicy = Object.freeze({
minNumberWords: 0,
capitalize: false,
includeNumber: false,
});

View File

@@ -1,12 +0,0 @@
import { PasswordGeneratorPolicy } from "../types";
/** 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,31 @@
import { PASSPHRASE_SETTINGS } from "../strategies/storage";
import { PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
import { CredentialGeneratorConfiguration } from "../types/credential-generator-configuration";
import { DefaultPassphraseBoundaries } from "./default-passphrase-boundaries";
import { DefaultPassphraseGenerationOptions } from "./default-passphrase-generation-options";
import { Policies } from "./policies";
const PASSPHRASE = Object.freeze({
settings: {
initial: DefaultPassphraseGenerationOptions,
constraints: {
numWords: {
min: DefaultPassphraseBoundaries.numWords.min,
max: DefaultPassphraseBoundaries.numWords.max,
},
wordSeparator: { maxLength: 1 },
},
account: PASSPHRASE_SETTINGS,
},
policy: Policies.Passphrase,
} satisfies CredentialGeneratorConfiguration<
PassphraseGenerationOptions,
PassphraseGeneratorPolicy
>);
/** Generator configurations */
export const Generators = Object.freeze({
/** Passphrase generator configuration */
Passphrase: PASSPHRASE,
});

View File

@@ -1,3 +1,4 @@
export * from "./generators";
export * from "./default-addy-io-options";
export * from "./default-catchall-options";
export * from "./default-duck-duck-go-options";
@@ -11,8 +12,6 @@ export * from "./default-passphrase-generation-options";
export * from "./default-password-generation-options";
export * from "./default-subaddress-generator-options";
export * from "./default-simple-login-options";
export * from "./disabled-passphrase-generator-policy";
export * from "./disabled-password-generator-policy";
export * from "./forwarders";
export * from "./integrations";
export * from "./policies";

View File

@@ -1,23 +1,45 @@
import { DisabledPassphraseGeneratorPolicy, DisabledPasswordGeneratorPolicy } from "../data";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import {
passphraseLeastPrivilege,
passwordLeastPrivilege,
PassphraseGeneratorOptionsEvaluator,
PasswordGeneratorOptionsEvaluator,
} from "../policies";
import { PassphraseGeneratorPolicy, PasswordGeneratorPolicy, PolicyConfiguration } from "../types";
import {
PassphraseGenerationOptions,
PassphraseGeneratorPolicy,
PasswordGenerationOptions,
PasswordGeneratorPolicy,
PolicyConfiguration,
} from "../types";
const PASSPHRASE = Object.freeze({
disabledValue: DisabledPassphraseGeneratorPolicy,
type: PolicyType.PasswordGenerator,
disabledValue: Object.freeze({
minNumberWords: 0,
capitalize: false,
includeNumber: false,
}),
combine: passphraseLeastPrivilege,
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
} as PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGeneratorOptionsEvaluator>);
createEvaluatorV2: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
} as PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGenerationOptions>);
const PASSWORD = Object.freeze({
disabledValue: DisabledPasswordGeneratorPolicy,
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),
} as PolicyConfiguration<PasswordGeneratorPolicy, PasswordGeneratorOptionsEvaluator>);
} as PolicyConfiguration<PasswordGeneratorPolicy, PasswordGenerationOptions>);
/** Policy configurations */
export const Policies = Object.freeze({

View File

@@ -1,10 +1,15 @@
// The root module interface has API stability guarantees
export * from "./abstractions";
export * from "./data";
export { createRandomizer } from "./factories";
export * from "./types";
export { CredentialGeneratorService } from "./services";
// These internal interfacess are exposed for use by other generator modules
// They are unstable and may change arbitrarily
export * as engine from "./engine";
export * as integration from "./integration";
export * as policies from "./policies";
export * as rx from "./rx";
export * as services from "./services";
export * as strategies from "./strategies";
export * from "./types";

View File

@@ -1,4 +1,4 @@
import { DisabledPassphraseGeneratorPolicy, DefaultPassphraseBoundaries } from "../data";
import { Policies, DefaultPassphraseBoundaries } from "../data";
import { PassphraseGenerationOptions } from "../types";
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
@@ -6,7 +6,7 @@ import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-opti
describe("Password generator options builder", () => {
describe("constructor()", () => {
it("should set the policy object to a copy of the input policy", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
policy.minNumberWords = 10; // arbitrary change for deep equality check
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -16,7 +16,7 @@ describe("Password generator options builder", () => {
});
it("should set default boundaries when a default policy is used", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
expect(builder.numWords).toEqual(DefaultPassphraseBoundaries.numWords);
@@ -25,7 +25,7 @@ describe("Password generator options builder", () => {
it.each([1, 2])(
"should use the default word boundaries when they are greater than `policy.minNumberWords` (= %i)",
(minNumberWords) => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
policy.minNumberWords = minNumberWords;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -37,7 +37,7 @@ describe("Password generator options builder", () => {
it.each([8, 12, 18])(
"should use `policy.minNumberWords` (= %i) when it is greater than the default minimum words",
(minNumberWords) => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
policy.minNumberWords = minNumberWords;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -50,7 +50,7 @@ describe("Password generator options builder", () => {
it.each([150, 300, 9000])(
"should use `policy.minNumberWords` (= %i) when it is greater than the default boundaries",
(minNumberWords) => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
policy.minNumberWords = minNumberWords;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -63,14 +63,14 @@ describe("Password generator options builder", () => {
describe("policyInEffect", () => {
it("should return false when the policy has no effect", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
expect(builder.policyInEffect).toEqual(false);
});
it("should return true when the policy has a numWords greater than the default boundary", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
policy.minNumberWords = DefaultPassphraseBoundaries.numWords.min + 1;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -78,7 +78,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has capitalize enabled", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
policy.capitalize = true;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -86,7 +86,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has includeNumber enabled", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
policy.includeNumber = true;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
@@ -98,7 +98,7 @@ describe("Password generator options builder", () => {
// All tests should freeze the options to ensure they are not modified
it("should set `capitalize` to `false` when the policy does not override it", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({});
@@ -108,7 +108,7 @@ describe("Password generator options builder", () => {
});
it("should set `capitalize` to `true` when the policy overrides it", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
policy.capitalize = true;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ capitalize: false });
@@ -119,7 +119,7 @@ describe("Password generator options builder", () => {
});
it("should set `includeNumber` to false when the policy does not override it", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({});
@@ -129,7 +129,7 @@ describe("Password generator options builder", () => {
});
it("should set `includeNumber` to true when the policy overrides it", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
policy.includeNumber = true;
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ includeNumber: false });
@@ -140,7 +140,7 @@ describe("Password generator options builder", () => {
});
it("should set `numWords` to the minimum value when it isn't supplied", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({});
@@ -154,7 +154,7 @@ describe("Password generator options builder", () => {
(numWords) => {
expect(numWords).toBeLessThan(DefaultPassphraseBoundaries.numWords.min);
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ numWords });
@@ -170,7 +170,7 @@ describe("Password generator options builder", () => {
expect(numWords).toBeGreaterThanOrEqual(DefaultPassphraseBoundaries.numWords.min);
expect(numWords).toBeLessThanOrEqual(DefaultPassphraseBoundaries.numWords.max);
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ numWords });
@@ -185,7 +185,7 @@ describe("Password generator options builder", () => {
(numWords) => {
expect(numWords).toBeGreaterThan(DefaultPassphraseBoundaries.numWords.max);
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ numWords });
@@ -196,7 +196,7 @@ describe("Password generator options builder", () => {
);
it("should preserve unknown properties", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
unknown: "property",
@@ -214,7 +214,7 @@ describe("Password generator options builder", () => {
// All tests should freeze the options to ensure they are not modified
it("should return the input options without altering them", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ wordSeparator: "%" });
@@ -224,7 +224,7 @@ describe("Password generator options builder", () => {
});
it("should set `wordSeparator` to '-' when it isn't supplied and there is no policy override", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({});
@@ -234,7 +234,7 @@ describe("Password generator options builder", () => {
});
it("should leave `wordSeparator` as the empty string '' when it is the empty string", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ wordSeparator: "" });
@@ -244,7 +244,7 @@ describe("Password generator options builder", () => {
});
it("should preserve unknown properties", () => {
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
unknown: "property",

View File

@@ -1,3 +1,5 @@
import { Constraints } from "@bitwarden/common/tools/types";
import { PolicyEvaluator } from "../abstractions";
import { DefaultPassphraseGenerationOptions, DefaultPassphraseBoundaries } from "../data";
import { Boundary, PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
@@ -5,7 +7,9 @@ import { Boundary, PassphraseGenerationOptions, PassphraseGeneratorPolicy } from
/** Enforces policy for passphrase generation options.
*/
export class PassphraseGeneratorOptionsEvaluator
implements PolicyEvaluator<PassphraseGeneratorPolicy, PassphraseGenerationOptions>
implements
PolicyEvaluator<PassphraseGeneratorPolicy, PassphraseGenerationOptions>,
Constraints<PassphraseGenerationOptions>
{
// This design is not ideal, but it is a step towards a more robust passphrase
// generator. Ideally, `sanitize` would be implemented on an options class,

View File

@@ -4,7 +4,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PolicyId } from "@bitwarden/common/types/guid";
import { DisabledPassphraseGeneratorPolicy } from "../data";
import { Policies } from "../data";
import { passphraseLeastPrivilege } from "./passphrase-least-privilege";
@@ -26,17 +26,17 @@ describe("passphraseLeastPrivilege", () => {
it("should return the accumulator when the policy type does not apply", () => {
const policy = createPolicy({}, PolicyType.RequireSso);
const result = passphraseLeastPrivilege(DisabledPassphraseGeneratorPolicy, policy);
const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy);
expect(result).toEqual(DisabledPassphraseGeneratorPolicy);
expect(result).toEqual(Policies.Passphrase.disabledValue);
});
it("should return the accumulator when the policy is not enabled", () => {
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
const result = passphraseLeastPrivilege(DisabledPassphraseGeneratorPolicy, policy);
const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy);
expect(result).toEqual(DisabledPassphraseGeneratorPolicy);
expect(result).toEqual(Policies.Passphrase.disabledValue);
});
it.each([
@@ -46,8 +46,8 @@ describe("passphraseLeastPrivilege", () => {
])("should take the %p from the policy", (input, value) => {
const policy = createPolicy({ [input]: value });
const result = passphraseLeastPrivilege(DisabledPassphraseGeneratorPolicy, policy);
const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy);
expect(result).toEqual({ ...DisabledPassphraseGeneratorPolicy, [input]: value });
expect(result).toEqual({ ...Policies.Passphrase.disabledValue, [input]: value });
});
});

View File

@@ -1,4 +1,4 @@
import { DefaultPasswordBoundaries, DisabledPasswordGeneratorPolicy } from "../data";
import { DefaultPasswordBoundaries, Policies } from "../data";
import { PasswordGenerationOptions } from "../types";
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
@@ -8,7 +8,7 @@ describe("Password generator options builder", () => {
describe("constructor()", () => {
it("should set the policy object to a copy of the input policy", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.minLength = 10; // arbitrary change for deep equality check
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -18,7 +18,7 @@ describe("Password generator options builder", () => {
});
it("should set default boundaries when a default policy is used", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -32,7 +32,7 @@ describe("Password generator options builder", () => {
(minLength) => {
expect(minLength).toBeLessThan(DefaultPasswordBoundaries.length.min);
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.minLength = minLength;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -47,7 +47,7 @@ describe("Password generator options builder", () => {
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.min);
expect(expectedLength).toBeLessThanOrEqual(DefaultPasswordBoundaries.length.max);
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.minLength = expectedLength;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -62,7 +62,7 @@ describe("Password generator options builder", () => {
(expectedLength) => {
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.max);
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.minLength = expectedLength;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -78,7 +78,7 @@ describe("Password generator options builder", () => {
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.min);
expect(expectedMinDigits).toBeLessThanOrEqual(DefaultPasswordBoundaries.minDigits.max);
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.numberCount = expectedMinDigits;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -93,7 +93,7 @@ describe("Password generator options builder", () => {
(expectedMinDigits) => {
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.max);
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.numberCount = expectedMinDigits;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -113,7 +113,7 @@ describe("Password generator options builder", () => {
DefaultPasswordBoundaries.minSpecialCharacters.max,
);
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.specialCount = expectedSpecialCharacters;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -132,7 +132,7 @@ describe("Password generator options builder", () => {
DefaultPasswordBoundaries.minSpecialCharacters.max,
);
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.specialCount = expectedSpecialCharacters;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -151,7 +151,7 @@ describe("Password generator options builder", () => {
(expectedLength, numberCount, specialCount) => {
expect(expectedLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.numberCount = numberCount;
policy.specialCount = specialCount;
@@ -164,14 +164,14 @@ describe("Password generator options builder", () => {
describe("policyInEffect", () => {
it("should return false when the policy has no effect", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(builder.policyInEffect).toEqual(false);
});
it("should return true when the policy has a minlength greater than the default boundary", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.minLength = DefaultPasswordBoundaries.length.min + 1;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -179,7 +179,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has a number count greater than the default boundary", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.numberCount = DefaultPasswordBoundaries.minDigits.min + 1;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -187,7 +187,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has a special character count greater than the default boundary", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.specialCount = DefaultPasswordBoundaries.minSpecialCharacters.min + 1;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -195,7 +195,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has uppercase enabled", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.useUppercase = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -203,7 +203,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has lowercase enabled", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.useLowercase = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -211,7 +211,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has numbers enabled", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.useNumbers = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -219,7 +219,7 @@ describe("Password generator options builder", () => {
});
it("should return true when the policy has special characters enabled", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.useSpecial = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
@@ -237,7 +237,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.uppercase` to '%s' when `policy.useUppercase` is false and `options.uppercase` is '%s'",
(expectedUppercase, uppercase) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.useUppercase = false;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, uppercase });
@@ -251,7 +251,7 @@ describe("Password generator options builder", () => {
it.each([false, true, undefined])(
"should set `options.uppercase` (= %s) to true when `policy.useUppercase` is true",
(uppercase) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.useUppercase = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, uppercase });
@@ -269,7 +269,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.lowercase` to '%s' when `policy.useLowercase` is false and `options.lowercase` is '%s'",
(expectedLowercase, lowercase) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.useLowercase = false;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, lowercase });
@@ -283,7 +283,7 @@ describe("Password generator options builder", () => {
it.each([false, true, undefined])(
"should set `options.lowercase` (= %s) to true when `policy.useLowercase` is true",
(lowercase) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.useLowercase = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, lowercase });
@@ -301,7 +301,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.number` to '%s' when `policy.useNumbers` is false and `options.number` is '%s'",
(expectedNumber, number) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.useNumbers = false;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, number });
@@ -315,7 +315,7 @@ describe("Password generator options builder", () => {
it.each([false, true, undefined])(
"should set `options.number` (= %s) to true when `policy.useNumbers` is true",
(number) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.useNumbers = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, number });
@@ -333,7 +333,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.special` to '%s' when `policy.useSpecial` is false and `options.special` is '%s'",
(expectedSpecial, special) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.useSpecial = false;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, special });
@@ -347,7 +347,7 @@ describe("Password generator options builder", () => {
it.each([false, true, undefined])(
"should set `options.special` (= %s) to true when `policy.useSpecial` is true",
(special) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.useSpecial = true;
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, special });
@@ -361,7 +361,7 @@ describe("Password generator options builder", () => {
it.each([1, 2, 3, 4])(
"should set `options.length` (= %i) to the minimum it is less than the minimum length",
(length) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(length).toBeLessThan(builder.length.min);
@@ -376,7 +376,7 @@ describe("Password generator options builder", () => {
it.each([5, 10, 50, 100, 128])(
"should not change `options.length` (= %i) when it is within the boundaries",
(length) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(length).toBeGreaterThanOrEqual(builder.length.min);
expect(length).toBeLessThanOrEqual(builder.length.max);
@@ -392,7 +392,7 @@ describe("Password generator options builder", () => {
it.each([129, 500, 9000])(
"should set `options.length` (= %i) to the maximum length when it is exceeded",
(length) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(length).toBeGreaterThan(builder.length.max);
@@ -414,7 +414,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.number === %s` when `options.minNumber` (= %i) is set to a value greater than 0",
(expectedNumber, minNumber) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, minNumber });
@@ -425,7 +425,7 @@ describe("Password generator options builder", () => {
);
it("should set `options.minNumber` to the minimum value when `options.number` is true", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, number: true });
@@ -435,7 +435,7 @@ describe("Password generator options builder", () => {
});
it("should set `options.minNumber` to 0 when `options.number` is false", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, number: false });
@@ -447,7 +447,7 @@ describe("Password generator options builder", () => {
it.each([1, 2, 3, 4])(
"should set `options.minNumber` (= %i) to the minimum it is less than the minimum number",
(minNumber) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.numberCount = 5; // arbitrary value greater than minNumber
expect(minNumber).toBeLessThan(policy.numberCount);
@@ -463,7 +463,7 @@ describe("Password generator options builder", () => {
it.each([1, 3, 5, 7, 9])(
"should not change `options.minNumber` (= %i) when it is within the boundaries",
(minNumber) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(minNumber).toBeGreaterThanOrEqual(builder.minDigits.min);
expect(minNumber).toBeLessThanOrEqual(builder.minDigits.max);
@@ -479,7 +479,7 @@ describe("Password generator options builder", () => {
it.each([10, 20, 400])(
"should set `options.minNumber` (= %i) to the maximum digit boundary when it is exceeded",
(minNumber) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(minNumber).toBeGreaterThan(builder.minDigits.max);
@@ -501,7 +501,7 @@ describe("Password generator options builder", () => {
])(
"should set `options.special === %s` when `options.minSpecial` (= %i) is set to a value greater than 0",
(expectedSpecial, minSpecial) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, minSpecial });
@@ -512,7 +512,7 @@ describe("Password generator options builder", () => {
);
it("should set `options.minSpecial` to the minimum value when `options.special` is true", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, special: true });
@@ -522,7 +522,7 @@ describe("Password generator options builder", () => {
});
it("should set `options.minSpecial` to 0 when `options.special` is false", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ ...defaultOptions, special: false });
@@ -534,7 +534,7 @@ describe("Password generator options builder", () => {
it.each([1, 2, 3, 4])(
"should set `options.minSpecial` (= %i) to the minimum it is less than the minimum special characters",
(minSpecial) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
policy.specialCount = 5; // arbitrary value greater than minSpecial
expect(minSpecial).toBeLessThan(policy.specialCount);
@@ -550,7 +550,7 @@ describe("Password generator options builder", () => {
it.each([1, 3, 5, 7, 9])(
"should not change `options.minSpecial` (= %i) when it is within the boundaries",
(minSpecial) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(minSpecial).toBeGreaterThanOrEqual(builder.minSpecialCharacters.min);
expect(minSpecial).toBeLessThanOrEqual(builder.minSpecialCharacters.max);
@@ -566,7 +566,7 @@ describe("Password generator options builder", () => {
it.each([10, 20, 400])(
"should set `options.minSpecial` (= %i) to the maximum special character boundary when it is exceeded",
(minSpecial) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
expect(minSpecial).toBeGreaterThan(builder.minSpecialCharacters.max);
@@ -579,7 +579,7 @@ describe("Password generator options builder", () => {
);
it("should preserve unknown properties", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
unknown: "property",
@@ -602,7 +602,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.minLowercase === %i` when `options.lowercase` is %s",
(expectedMinLowercase, lowercase) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ lowercase, ...defaultOptions });
@@ -618,7 +618,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.minUppercase === %i` when `options.uppercase` is %s",
(expectedMinUppercase, uppercase) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ uppercase, ...defaultOptions });
@@ -634,7 +634,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.minNumber === %i` when `options.number` is %s and `options.minNumber` is not set",
(expectedMinNumber, number) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ number, ...defaultOptions });
@@ -652,7 +652,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.number === %s` when `options.minNumber` is %i and `options.number` is not set",
(expectedNumber, minNumber) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ minNumber, ...defaultOptions });
@@ -668,7 +668,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.minSpecial === %i` when `options.special` is %s and `options.minSpecial` is not set",
(special, expectedMinSpecial) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ special, ...defaultOptions });
@@ -686,7 +686,7 @@ describe("Password generator options builder", () => {
])(
"should output `options.special === %s` when `options.minSpecial` is %i and `options.special` is not set",
(minSpecial, expectedSpecial) => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({ minSpecial, ...defaultOptions });
@@ -707,7 +707,7 @@ describe("Password generator options builder", () => {
const sumOfMinimums = minLowercase + minUppercase + minNumber + minSpecial;
expect(sumOfMinimums).toBeLessThan(DefaultPasswordBoundaries.length.min);
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
minLowercase,
@@ -732,7 +732,7 @@ describe("Password generator options builder", () => {
(expectedMinLength, minLowercase, minUppercase, minNumber, minSpecial) => {
expect(expectedMinLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
minLowercase,
@@ -749,7 +749,7 @@ describe("Password generator options builder", () => {
);
it("should preserve unknown properties", () => {
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
const policy = Object.assign({}, Policies.Password.disabledValue);
const builder = new PasswordGeneratorOptionsEvaluator(policy);
const options = Object.freeze({
unknown: "property",

View File

@@ -4,7 +4,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PolicyId } from "@bitwarden/common/types/guid";
import { DisabledPasswordGeneratorPolicy } from "../data";
import { Policies } from "../data";
import { passwordLeastPrivilege } from "./password-least-privilege";
@@ -26,17 +26,17 @@ describe("passwordLeastPrivilege", () => {
it("should return the accumulator when the policy type does not apply", () => {
const policy = createPolicy({}, PolicyType.RequireSso);
const result = passwordLeastPrivilege(DisabledPasswordGeneratorPolicy, policy);
const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy);
expect(result).toEqual(DisabledPasswordGeneratorPolicy);
expect(result).toEqual(Policies.Password.disabledValue);
});
it("should return the accumulator when the policy is not enabled", () => {
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
const result = passwordLeastPrivilege(DisabledPasswordGeneratorPolicy, policy);
const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy);
expect(result).toEqual(DisabledPasswordGeneratorPolicy);
expect(result).toEqual(Policies.Password.disabledValue);
});
it.each([
@@ -50,8 +50,8 @@ describe("passwordLeastPrivilege", () => {
])("should take the %p from the policy", (input, value, expected) => {
const policy = createPolicy({ [input]: value });
const result = passwordLeastPrivilege(DisabledPasswordGeneratorPolicy, policy);
const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy);
expect(result).toEqual({ ...DisabledPasswordGeneratorPolicy, [expected]: value });
expect(result).toEqual({ ...Policies.Password.disabledValue, [expected]: value });
});
});

View File

@@ -18,6 +18,19 @@ export function mapPolicyToEvaluator<Policy, Evaluator>(
);
}
/** Maps an administrative console policy to a policy evaluator using the provided configuration.
* @param configuration the configuration that constructs the evaluator.
*/
export function mapPolicyToEvaluatorV2<Policy, Evaluator>(
configuration: PolicyConfiguration<Policy, Evaluator>,
) {
return pipe(
reduceCollection(configuration.combine, configuration.disabledValue),
distinctIfShallowMatch(),
map(configuration.createEvaluatorV2),
);
}
/** Constructs a method that maps a policy to the default (no-op) policy. */
export function newDefaultEvaluator<Target>() {
return () => {

View File

@@ -0,0 +1,377 @@
import { mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
import { Constraints } from "@bitwarden/common/tools/types";
import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
import { FakeStateProvider, FakeAccountService, awaitAsync } from "../../../../../common/spec";
import { PolicyEvaluator } from "../abstractions";
import { CredentialGeneratorConfiguration } from "../types";
import { CredentialGeneratorService } from "./credential-generator.service";
// arbitrary settings types
type SomeSettings = { foo: string };
type SomePolicy = { fooPolicy: boolean };
// settings storage location
const SettingsKey = new UserKeyDefinition<SomeSettings>(GENERATOR_DISK, "SomeSettings", {
deserializer: (value) => value,
clearOn: [],
});
// fake policy
const policyService = mock<PolicyService>();
const somePolicy = new Policy({
data: { fooPolicy: true },
type: PolicyType.PasswordGenerator,
id: "" as PolicyId,
organizationId: "" as OrganizationId,
enabled: true,
});
// fake the configuration
const SomeConfiguration: CredentialGeneratorConfiguration<SomeSettings, SomePolicy> = {
settings: {
initial: { foo: "initial" },
constraints: { foo: {} },
account: SettingsKey,
},
policy: {
type: PolicyType.PasswordGenerator,
disabledValue: {
fooPolicy: false,
},
combine: (acc, policy) => {
return { fooPolicy: acc.fooPolicy || policy.data.fooPolicy };
},
createEvaluator: () => {
throw new Error("this should never be called");
},
createEvaluatorV2: (policy) => {
return {
foo: {},
policy,
policyInEffect: policy.fooPolicy,
applyPolicy: (settings) => {
return policy.fooPolicy ? { foo: `apply(${settings.foo})` } : settings;
},
sanitize: (settings) => {
return policy.fooPolicy ? { foo: `sanitize(${settings.foo})` } : settings;
},
} as PolicyEvaluator<SomePolicy, SomeSettings> & Constraints<SomeSettings>;
},
},
};
// fake user information
const SomeUser = "SomeUser" as UserId;
const AnotherUser = "SomeOtherUser" as UserId;
const accountService = new FakeAccountService({
[SomeUser]: {
name: "some user",
email: "some.user@example.com",
emailVerified: true,
},
[AnotherUser]: {
name: "some other user",
email: "some.other.user@example.com",
emailVerified: true,
},
});
// fake state
const stateProvider = new FakeStateProvider(accountService);
describe("CredentialGeneratorService", () => {
beforeEach(async () => {
await accountService.switchAccount(SomeUser);
policyService.getAll$.mockImplementation(() => new BehaviorSubject([]).asObservable());
jest.clearAllMocks();
});
describe("settings$", () => {
it("defaults to the configuration's initial settings if settings aren't found", async () => {
await stateProvider.setUserState(SettingsKey, null, SomeUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
expect(result).toEqual(SomeConfiguration.settings.initial);
});
it("reads from the active user's configuration-defined storage", async () => {
const settings = { foo: "value" };
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
expect(result).toEqual(settings);
});
it("applies policy to the loaded settings", async () => {
const settings = { foo: "value" };
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
const policy$ = new BehaviorSubject([somePolicy]);
policyService.getAll$.mockReturnValue(policy$);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
expect(result).toEqual({ foo: "sanitize(apply(value))" });
});
it("follows changes to the active user", async () => {
const someSettings = { foo: "value" };
const anotherSettings = { foo: "another" };
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const results: any = [];
const sub = generator.settings$(SomeConfiguration).subscribe((r) => results.push(r));
await accountService.switchAccount(AnotherUser);
await awaitAsync();
sub.unsubscribe();
const [someResult, anotherResult] = results;
expect(someResult).toEqual(someSettings);
expect(anotherResult).toEqual(anotherSettings);
});
it("reads an arbitrary user's settings", async () => {
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
const anotherSettings = { foo: "another" };
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
const result = await firstValueFrom(generator.settings$(SomeConfiguration, { userId$ }));
expect(result).toEqual(anotherSettings);
});
it("follows changes to the arbitrary user", async () => {
const someSettings = { foo: "value" };
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
const anotherSettings = { foo: "another" };
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
const results: any = [];
const sub = generator
.settings$(SomeConfiguration, { userId$ })
.subscribe((r) => results.push(r));
userId.next(AnotherUser);
await awaitAsync();
sub.unsubscribe();
const [someResult, anotherResult] = results;
expect(someResult).toEqual(someSettings);
expect(anotherResult).toEqual(anotherSettings);
});
it("errors when the arbitrary user's stream errors", async () => {
await stateProvider.setUserState(SettingsKey, null, SomeUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
let error = null;
generator.settings$(SomeConfiguration, { userId$ }).subscribe({
error: (e: unknown) => {
error = e;
},
});
userId.error({ some: "error" });
await awaitAsync();
expect(error).toEqual({ some: "error" });
});
it("completes when the arbitrary user's stream completes", async () => {
await stateProvider.setUserState(SettingsKey, null, SomeUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
let completed = false;
generator.settings$(SomeConfiguration, { userId$ }).subscribe({
complete: () => {
completed = true;
},
});
userId.complete();
await awaitAsync();
expect(completed).toBeTruthy();
});
it("ignores repeated arbitrary user emissions", async () => {
await stateProvider.setUserState(SettingsKey, null, SomeUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
let count = 0;
const sub = generator.settings$(SomeConfiguration, { userId$ }).subscribe({
next: () => {
count++;
},
});
await awaitAsync();
userId.next(SomeUser);
await awaitAsync();
userId.next(SomeUser);
await awaitAsync();
sub.unsubscribe();
expect(count).toEqual(1);
});
});
describe("settings", () => {
it("writes to the user's state", async () => {
const singleUserId$ = new BehaviorSubject(SomeUser).asObservable();
const generator = new CredentialGeneratorService(stateProvider, policyService);
const subject = await generator.settings(SomeConfiguration, { singleUserId$ });
subject.next({ foo: "next value" });
await awaitAsync();
const result = await firstValueFrom(stateProvider.getUserState$(SettingsKey, SomeUser));
expect(result).toEqual({ foo: "next value" });
});
it("waits for the user to become available", async () => {
const singleUserId = new BehaviorSubject(null);
const singleUserId$ = singleUserId.asObservable();
const generator = new CredentialGeneratorService(stateProvider, policyService);
let completed = false;
const promise = generator.settings(SomeConfiguration, { singleUserId$ }).then((settings) => {
completed = true;
return settings;
});
await awaitAsync();
expect(completed).toBeFalsy();
singleUserId.next(SomeUser);
const result = await promise;
expect(result.userId).toEqual(SomeUser);
});
});
describe("policy$", () => {
it("creates a disabled policy evaluator when there is no policy", async () => {
const generator = new CredentialGeneratorService(stateProvider, policyService);
const userId$ = new BehaviorSubject(SomeUser).asObservable();
const result = await firstValueFrom(generator.policy$(SomeConfiguration, { userId$ }));
expect(result.policy).toEqual(SomeConfiguration.policy.disabledValue);
expect(result.policyInEffect).toBeFalsy();
});
it("creates an active policy evaluator when there is a policy", async () => {
const generator = new CredentialGeneratorService(stateProvider, policyService);
const userId$ = new BehaviorSubject(SomeUser).asObservable();
const policy$ = new BehaviorSubject([somePolicy]);
policyService.getAll$.mockReturnValue(policy$);
const result = await firstValueFrom(generator.policy$(SomeConfiguration, { userId$ }));
expect(result.policy).toEqual({ fooPolicy: true });
expect(result.policyInEffect).toBeTruthy();
});
it("follows policy emissions", async () => {
const generator = new CredentialGeneratorService(stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
const somePolicySubject = new BehaviorSubject([somePolicy]);
policyService.getAll$.mockReturnValueOnce(somePolicySubject.asObservable());
const emissions: any = [];
const sub = generator
.policy$(SomeConfiguration, { userId$ })
.subscribe((policy) => emissions.push(policy));
// swap the active policy for an inactive policy
somePolicySubject.next([]);
await awaitAsync();
sub.unsubscribe();
const [someResult, anotherResult] = emissions;
expect(someResult.policy).toEqual({ fooPolicy: true });
expect(someResult.policyInEffect).toBeTruthy();
expect(anotherResult.policy).toEqual(SomeConfiguration.policy.disabledValue);
expect(anotherResult.policyInEffect).toBeFalsy();
});
it("follows user emissions", async () => {
const generator = new CredentialGeneratorService(stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
const somePolicy$ = new BehaviorSubject([somePolicy]).asObservable();
const anotherPolicy$ = new BehaviorSubject([]).asObservable();
policyService.getAll$.mockReturnValueOnce(somePolicy$).mockReturnValueOnce(anotherPolicy$);
const emissions: any = [];
const sub = generator
.policy$(SomeConfiguration, { userId$ })
.subscribe((policy) => emissions.push(policy));
// swapping the user invokes the return for `anotherPolicy$`
userId.next(AnotherUser);
await awaitAsync();
sub.unsubscribe();
const [someResult, anotherResult] = emissions;
expect(someResult.policy).toEqual({ fooPolicy: true });
expect(someResult.policyInEffect).toBeTruthy();
expect(anotherResult.policy).toEqual(SomeConfiguration.policy.disabledValue);
expect(anotherResult.policyInEffect).toBeFalsy();
});
it("errors when the user errors", async () => {
const generator = new CredentialGeneratorService(stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
const expectedError = { some: "error" };
let actualError: any = null;
generator.policy$(SomeConfiguration, { userId$ }).subscribe({
error: (e: unknown) => {
actualError = e;
},
});
userId.error(expectedError);
await awaitAsync();
expect(actualError).toEqual(expectedError);
});
it("completes when the user completes", async () => {
const generator = new CredentialGeneratorService(stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
let completed = false;
generator.policy$(SomeConfiguration, { userId$ }).subscribe({
complete: () => {
completed = true;
},
});
userId.complete();
await awaitAsync();
expect(completed).toBeTruthy();
});
});
});

View File

@@ -0,0 +1,128 @@
import {
combineLatest,
distinctUntilChanged,
endWith,
filter,
firstValueFrom,
ignoreElements,
map,
mergeMap,
Observable,
switchMap,
takeUntil,
} from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { StateProvider } from "@bitwarden/common/platform/state";
import { SingleUserDependency, UserDependency } from "@bitwarden/common/tools/dependencies";
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
import { Constraints } from "@bitwarden/common/tools/types";
import { PolicyEvaluator } from "../abstractions";
import { mapPolicyToEvaluatorV2 } from "../rx";
import { CredentialGeneratorConfiguration as Configuration } from "../types/credential-generator-configuration";
type Policy$Dependencies = UserDependency;
type Settings$Dependencies = Partial<UserDependency>;
// FIXME: once the modernization is complete, switch the type parameters
// in `PolicyEvaluator<P, S>` and bake-in the constraints type.
type Evaluator<Settings, Policy> = PolicyEvaluator<Policy, Settings> & Constraints<Settings>;
export class CredentialGeneratorService {
constructor(
private stateProvider: StateProvider,
private policyService: PolicyService,
) {}
/** Get the settings for the provided configuration
* @param configuration determines which generator's settings are loaded
* @param dependencies.userId$ identifies the user to which the settings are bound.
* If this parameter is not provided, the observable follows the active user and
* may not complete.
* @returns an observable that emits settings
* @remarks the observable enforces policies on the settings
*/
settings$<Settings, Policy>(
configuration: Configuration<Settings, Policy>,
dependencies?: Settings$Dependencies,
) {
const userId$ = dependencies?.userId$ ?? this.stateProvider.activeUserId$;
const completion$ = userId$.pipe(ignoreElements(), endWith(true));
const state$ = userId$.pipe(
filter((userId) => !!userId),
distinctUntilChanged(),
switchMap((userId) => {
const state$ = this.stateProvider
.getUserState$(configuration.settings.account, userId)
.pipe(takeUntil(completion$));
return state$;
}),
map((settings) => settings ?? structuredClone(configuration.settings.initial)),
);
const settings$ = combineLatest([state$, this.policy$(configuration, { userId$ })]).pipe(
map(([settings, policy]) => {
// FIXME: create `onLoadApply` that wraps these operations
const applied = policy.applyPolicy(settings);
const sanitized = policy.sanitize(applied);
return sanitized;
}),
);
return settings$;
}
/** Get a subject bound to a specific user's settings
* @param configuration determines which generator's settings are loaded
* @param dependencies.singleUserId$ identifies the user to which the settings are bound
* @returns a promise that resolves with the subject once
* `dependencies.singleUserId$` becomes available.
* @remarks the subject enforces policy for the settings
*/
async settings<Settings, Policy>(
configuration: Configuration<Settings, Policy>,
dependencies: SingleUserDependency,
) {
const userId = await firstValueFrom(
dependencies.singleUserId$.pipe(filter((userId) => !!userId)),
);
const state = this.stateProvider.getUser(userId, configuration.settings.account);
// FIXME: apply policy to the settings - this should happen *within* the subject.
// Note that policies could be evaluated when the settings are saved or when they
// are loaded. The existing subject presently could only apply settings on save
// (by wiring the policy in as a dependency and applying with "nextState"), and
// even that has a limitation since arbitrary dependencies do not trigger state
// emissions.
const subject = new UserStateSubject(state, dependencies);
return subject;
}
/** Get the policy for the provided configuration
* @param dependencies.userId$ determines which user's policy is loaded
* @returns an observable that emits the policy once `dependencies.userId$`
* and the policy become available.
*/
policy$<Settings, Policy>(
configuration: Configuration<Settings, Policy>,
dependencies: Policy$Dependencies,
): Observable<Evaluator<Settings, Policy>> {
const completion$ = dependencies.userId$.pipe(ignoreElements(), endWith(true));
const policy$ = dependencies.userId$.pipe(
mergeMap((userId) => {
// complete policy emissions otherwise `mergeMap` holds `policy$` open indefinitely
const policies$ = this.policyService
.getAll$(configuration.policy.type, userId)
.pipe(takeUntil(completion$));
return policies$;
}),
mapPolicyToEvaluatorV2(configuration.policy),
);
return policy$;
}
}

View File

@@ -1 +1,2 @@
export { DefaultGeneratorService } from "./default-generator.service";
export { CredentialGeneratorService } from "./credential-generator.service";

View File

@@ -8,7 +8,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { DefaultPassphraseGenerationOptions, DisabledPassphraseGeneratorPolicy } from "../data";
import { DefaultPassphraseGenerationOptions, Policies } from "../data";
import { PasswordRandomizer } from "../engine";
import { PassphraseGeneratorOptionsEvaluator } from "../policies";
@@ -50,7 +50,7 @@ describe("Password generation strategy", () => {
const evaluator = await firstValueFrom(evaluator$);
expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator);
expect(evaluator.policy).toMatchObject(DisabledPassphraseGeneratorPolicy);
expect(evaluator.policy).toMatchObject(Policies.Passphrase.disabledValue);
},
);
});

View File

@@ -8,7 +8,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { DefaultPasswordGenerationOptions, DisabledPasswordGeneratorPolicy } from "../data";
import { DefaultPasswordGenerationOptions, Policies } from "../data";
import { PasswordRandomizer } from "../engine";
import { PasswordGeneratorOptionsEvaluator } from "../policies";
@@ -58,7 +58,7 @@ describe("Password generation strategy", () => {
const evaluator = await firstValueFrom(evaluator$);
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy);
expect(evaluator.policy).toMatchObject(Policies.Password.disabledValue);
},
);
});

View File

@@ -0,0 +1,19 @@
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
import { Constraints } from "@bitwarden/common/tools/types";
import { PolicyConfiguration } from "../types";
export type CredentialGeneratorConfiguration<Settings, Policy> = {
settings: {
/** value used when an account's settings haven't been initialized */
initial: Readonly<Partial<Settings>>;
constraints: Constraints<Settings>;
/** storage location for account-global settings */
account: UserKeyDefinition<Settings>;
};
/** defines how to construct policy for this settings instance */
policy: PolicyConfiguration<Policy, Settings>;
};

View File

@@ -1,5 +1,6 @@
export * from "./boundary";
export * from "./catchall-generator-options";
export * from "./credential-generator-configuration";
export * from "./eff-username-generator-options";
export * from "./forwarder-options";
export * from "./generator-options";

View File

@@ -1,7 +1,13 @@
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy as AdminPolicy } from "@bitwarden/common/admin-console/models/domain/policy";
import { Constraints } from "@bitwarden/common/tools/types";
import { PolicyEvaluator } from "../abstractions";
/** Determines how to construct a password generator policy */
export type PolicyConfiguration<Policy, Evaluator> = {
export type PolicyConfiguration<Policy, Settings> = {
type: PolicyType;
/** The value of the policy when it is not in effect. */
disabledValue: Policy;
@@ -12,5 +18,12 @@ export type PolicyConfiguration<Policy, Evaluator> = {
/** Converts policy service data into an actionable policy.
*/
createEvaluator: (policy: Policy) => Evaluator;
createEvaluator: (policy: Policy) => PolicyEvaluator<Policy, Settings>;
/** Converts policy service data into an actionable policy.
* @remarks this version includes constraints needed for the reactive forms;
* it was introduced so that the constraints can be incrementally introduced
* as the new UI is built.
*/
createEvaluatorV2?: (policy: Policy) => PolicyEvaluator<Policy, Settings> & Constraints<Settings>;
};