mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-13 10:24:20 +01:00
[PM-10107] evaluate the override password type policy (#10277)
This commit is contained in:
parent
795c21a1c7
commit
cbe7ae68cc
@ -34,7 +34,6 @@ export class GeneratorComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
usernameGeneratingPromise: Promise<string>;
|
usernameGeneratingPromise: Promise<string>;
|
||||||
typeOptions: any[];
|
typeOptions: any[];
|
||||||
passTypeOptions: any[];
|
|
||||||
usernameTypeOptions: any[];
|
usernameTypeOptions: any[];
|
||||||
subaddressOptions: any[];
|
subaddressOptions: any[];
|
||||||
catchallOptions: any[];
|
catchallOptions: any[];
|
||||||
@ -48,6 +47,11 @@ export class GeneratorComponent implements OnInit, OnDestroy {
|
|||||||
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
|
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
|
||||||
usernameWebsite: string = null;
|
usernameWebsite: string = null;
|
||||||
|
|
||||||
|
get passTypeOptions() {
|
||||||
|
return this._passTypeOptions.filter((o) => !o.disabled);
|
||||||
|
}
|
||||||
|
private _passTypeOptions: { name: string; value: GeneratorType; disabled: boolean }[];
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private isInitialized$ = new BehaviorSubject(false);
|
private isInitialized$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
@ -79,9 +83,9 @@ export class GeneratorComponent implements OnInit, OnDestroy {
|
|||||||
{ name: i18nService.t("password"), value: "password" },
|
{ name: i18nService.t("password"), value: "password" },
|
||||||
{ name: i18nService.t("username"), value: "username" },
|
{ name: i18nService.t("username"), value: "username" },
|
||||||
];
|
];
|
||||||
this.passTypeOptions = [
|
this._passTypeOptions = [
|
||||||
{ name: i18nService.t("password"), value: "password" },
|
{ name: i18nService.t("password"), value: "password", disabled: false },
|
||||||
{ name: i18nService.t("passphrase"), value: "passphrase" },
|
{ name: i18nService.t("passphrase"), value: "passphrase", disabled: false },
|
||||||
];
|
];
|
||||||
this.usernameTypeOptions = [
|
this.usernameTypeOptions = [
|
||||||
{
|
{
|
||||||
@ -138,6 +142,14 @@ export class GeneratorComponent implements OnInit, OnDestroy {
|
|||||||
this.passwordOptions.type =
|
this.passwordOptions.type =
|
||||||
this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
|
this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
|
||||||
|
|
||||||
|
const overrideType = this.enforcedPasswordPolicyOptions.overridePasswordType ?? "";
|
||||||
|
const isDisabled = overrideType.length
|
||||||
|
? (value: string, policyValue: string) => value !== policyValue
|
||||||
|
: (_value: string, _policyValue: string) => false;
|
||||||
|
for (const option of this._passTypeOptions) {
|
||||||
|
option.disabled = isDisabled(option.value, overrideType);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.usernameOptions.type == null) {
|
if (this.usernameOptions.type == null) {
|
||||||
this.usernameOptions.type = "word";
|
this.usernameOptions.type = "word";
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import Domain from "../../../platform/models/domain/domain-base";
|
|||||||
*/
|
*/
|
||||||
export class PasswordGeneratorPolicyOptions extends Domain {
|
export class PasswordGeneratorPolicyOptions extends Domain {
|
||||||
/** The default kind of credential to generate */
|
/** The default kind of credential to generate */
|
||||||
defaultType: "password" | "passphrase" | "" = "";
|
overridePasswordType: "password" | "passphrase" | "" = "";
|
||||||
|
|
||||||
/** The minimum length of generated passwords.
|
/** The minimum length of generated passwords.
|
||||||
* When this is less than or equal to zero, it is ignored.
|
* When this is less than or equal to zero, it is ignored.
|
||||||
@ -70,7 +70,7 @@ export class PasswordGeneratorPolicyOptions extends Domain {
|
|||||||
*/
|
*/
|
||||||
inEffect() {
|
inEffect() {
|
||||||
return (
|
return (
|
||||||
this.defaultType ||
|
this.overridePasswordType ||
|
||||||
this.minLength > 0 ||
|
this.minLength > 0 ||
|
||||||
this.numberCount > 0 ||
|
this.numberCount > 0 ||
|
||||||
this.specialCount > 0 ||
|
this.specialCount > 0 ||
|
||||||
|
5
libs/tools/generator/core/src/data/generator-types.ts
Normal file
5
libs/tools/generator/core/src/data/generator-types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/** Types of passwords that may be configured by the password generator */
|
||||||
|
export const PasswordTypes = Object.freeze(["password", "passphrase"] as const);
|
||||||
|
|
||||||
|
/** Types of generators that may be configured by the password generator */
|
||||||
|
export const GeneratorTypes = Object.freeze([...PasswordTypes, "username"] as const);
|
@ -17,3 +17,4 @@ export * from "./forwarders";
|
|||||||
export * from "./integrations";
|
export * from "./integrations";
|
||||||
export * from "./policies";
|
export * from "./policies";
|
||||||
export * from "./username-digits";
|
export * from "./username-digits";
|
||||||
|
export * from "./generator-types";
|
||||||
|
@ -1,2 +1,7 @@
|
|||||||
|
import { GeneratorTypes, PasswordTypes } from "../data/generator-types";
|
||||||
|
|
||||||
/** The kind of credential being generated. */
|
/** The kind of credential being generated. */
|
||||||
export type GeneratorType = "password" | "passphrase" | "username";
|
export type GeneratorType = (typeof GeneratorTypes)[number];
|
||||||
|
|
||||||
|
/** The kinds of passwords that can be generated. */
|
||||||
|
export type PasswordType = (typeof PasswordTypes)[number];
|
||||||
|
@ -270,7 +270,7 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
const navigation = createNavigationGenerator(
|
const navigation = createNavigationGenerator(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
defaultType: "password",
|
overridePasswordType: "password",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const generator = new LegacyPasswordGenerationService(
|
const generator = new LegacyPasswordGenerationService(
|
||||||
@ -284,7 +284,7 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
const [, policy] = await generator.getOptions();
|
const [, policy] = await generator.getOptions();
|
||||||
|
|
||||||
expect(policy).toEqual({
|
expect(policy).toEqual({
|
||||||
defaultType: "password",
|
overridePasswordType: "password",
|
||||||
minLength: 20,
|
minLength: 20,
|
||||||
numberCount: 10,
|
numberCount: 10,
|
||||||
specialCount: 11,
|
specialCount: 11,
|
||||||
@ -402,7 +402,7 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
const navigation = createNavigationGenerator(
|
const navigation = createNavigationGenerator(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
defaultType: "password",
|
overridePasswordType: "password",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const generator = new LegacyPasswordGenerationService(
|
const generator = new LegacyPasswordGenerationService(
|
||||||
@ -416,7 +416,7 @@ describe("LegacyPasswordGenerationService", () => {
|
|||||||
const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({});
|
const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({});
|
||||||
|
|
||||||
expect(policy).toEqual({
|
expect(policy).toEqual({
|
||||||
defaultType: "password",
|
overridePasswordType: "password",
|
||||||
minLength: 20,
|
minLength: 20,
|
||||||
numberCount: 10,
|
numberCount: 10,
|
||||||
specialCount: 11,
|
specialCount: 11,
|
||||||
|
@ -248,7 +248,7 @@ export class LegacyPasswordGenerationService implements PasswordGenerationServic
|
|||||||
...options,
|
...options,
|
||||||
...navigationEvaluator.sanitize(navigationApplied),
|
...navigationEvaluator.sanitize(navigationApplied),
|
||||||
};
|
};
|
||||||
if (options.type === "password") {
|
if (navigationSanitized.type === "password") {
|
||||||
const applied = passwordEvaluator.applyPolicy(navigationSanitized);
|
const applied = passwordEvaluator.applyPolicy(navigationSanitized);
|
||||||
const sanitized = passwordEvaluator.sanitize(applied);
|
const sanitized = passwordEvaluator.sanitize(applied);
|
||||||
return [sanitized, policy];
|
return [sanitized, policy];
|
||||||
|
@ -69,7 +69,7 @@ describe("DefaultGeneratorNavigationService", () => {
|
|||||||
organizationId: "" as any,
|
organizationId: "" as any,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
data: { defaultType: "password" },
|
data: { overridePasswordType: "password" },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
@ -4,18 +4,18 @@ import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";
|
|||||||
describe("GeneratorNavigationEvaluator", () => {
|
describe("GeneratorNavigationEvaluator", () => {
|
||||||
describe("policyInEffect", () => {
|
describe("policyInEffect", () => {
|
||||||
it.each([["passphrase"], ["password"]] as const)(
|
it.each([["passphrase"], ["password"]] as const)(
|
||||||
"returns true if the policy has a defaultType (= %p)",
|
"returns true if the policy has a overridePasswordType (= %p)",
|
||||||
(defaultType) => {
|
(overridePasswordType) => {
|
||||||
const evaluator = new GeneratorNavigationEvaluator({ defaultType });
|
const evaluator = new GeneratorNavigationEvaluator({ overridePasswordType });
|
||||||
|
|
||||||
expect(evaluator.policyInEffect).toEqual(true);
|
expect(evaluator.policyInEffect).toEqual(true);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it.each([[undefined], [null], ["" as any]])(
|
it.each([[undefined], [null], ["" as any]])(
|
||||||
"returns false if the policy has a falsy defaultType (= %p)",
|
"returns false if the policy has a falsy overridePasswordType (= %p)",
|
||||||
(defaultType) => {
|
(overridePasswordType) => {
|
||||||
const evaluator = new GeneratorNavigationEvaluator({ defaultType });
|
const evaluator = new GeneratorNavigationEvaluator({ overridePasswordType });
|
||||||
|
|
||||||
expect(evaluator.policyInEffect).toEqual(false);
|
expect(evaluator.policyInEffect).toEqual(false);
|
||||||
},
|
},
|
||||||
@ -23,7 +23,7 @@ describe("GeneratorNavigationEvaluator", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("applyPolicy", () => {
|
describe("applyPolicy", () => {
|
||||||
it("returns the input options", () => {
|
it("returns the input options when a policy is not in effect", () => {
|
||||||
const evaluator = new GeneratorNavigationEvaluator(null);
|
const evaluator = new GeneratorNavigationEvaluator(null);
|
||||||
const options = { type: "password" as const };
|
const options = { type: "password" as const };
|
||||||
|
|
||||||
@ -31,19 +31,27 @@ describe("GeneratorNavigationEvaluator", () => {
|
|||||||
|
|
||||||
expect(result).toEqual(options);
|
expect(result).toEqual(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([["passphrase"], ["password"]] as const)(
|
||||||
|
"defaults options to the policy's default type (= %p) when a policy is in effect",
|
||||||
|
(overridePasswordType) => {
|
||||||
|
const evaluator = new GeneratorNavigationEvaluator({ overridePasswordType });
|
||||||
|
|
||||||
|
const result = evaluator.applyPolicy({});
|
||||||
|
|
||||||
|
expect(result).toEqual({ type: overridePasswordType });
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("sanitize", () => {
|
describe("sanitize", () => {
|
||||||
it.each([["passphrase"], ["password"]] as const)(
|
it("retains the options type when it is set", () => {
|
||||||
"defaults options to the policy's default type (= %p) when a policy is in effect",
|
const evaluator = new GeneratorNavigationEvaluator({ overridePasswordType: "passphrase" });
|
||||||
(defaultType) => {
|
|
||||||
const evaluator = new GeneratorNavigationEvaluator({ defaultType });
|
|
||||||
|
|
||||||
const result = evaluator.sanitize({});
|
const result = evaluator.sanitize({ type: "password" });
|
||||||
|
|
||||||
expect(result).toEqual({ type: defaultType });
|
expect(result).toEqual({ type: "password" });
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it("defaults options to the default generator navigation type when a policy is not in effect", () => {
|
it("defaults options to the default generator navigation type when a policy is not in effect", () => {
|
||||||
const evaluator = new GeneratorNavigationEvaluator(null);
|
const evaluator = new GeneratorNavigationEvaluator(null);
|
||||||
@ -52,13 +60,5 @@ describe("GeneratorNavigationEvaluator", () => {
|
|||||||
|
|
||||||
expect(result.type).toEqual(DefaultGeneratorNavigation.type);
|
expect(result.type).toEqual(DefaultGeneratorNavigation.type);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("retains the options type when it is set", () => {
|
|
||||||
const evaluator = new GeneratorNavigationEvaluator({ defaultType: "passphrase" });
|
|
||||||
|
|
||||||
const result = evaluator.sanitize({ type: "password" });
|
|
||||||
|
|
||||||
expect(result).toEqual({ type: "password" });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { PolicyEvaluator } from "@bitwarden/generator-core";
|
import { PasswordTypes, PolicyEvaluator } from "@bitwarden/generator-core";
|
||||||
|
|
||||||
import { DefaultGeneratorNavigation } from "./default-generator-navigation";
|
import { DefaultGeneratorNavigation } from "./default-generator-navigation";
|
||||||
import { GeneratorNavigation } from "./generator-navigation";
|
import { GeneratorNavigation } from "./generator-navigation";
|
||||||
@ -17,7 +17,7 @@ export class GeneratorNavigationEvaluator
|
|||||||
|
|
||||||
/** {@link PolicyEvaluator.policyInEffect} */
|
/** {@link PolicyEvaluator.policyInEffect} */
|
||||||
get policyInEffect(): boolean {
|
get policyInEffect(): boolean {
|
||||||
return this.policy?.defaultType ? true : false;
|
return PasswordTypes.includes(this.policy?.overridePasswordType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apply policy to the input options.
|
/** Apply policy to the input options.
|
||||||
@ -25,7 +25,13 @@ export class GeneratorNavigationEvaluator
|
|||||||
* @returns A new password generation request with policy applied.
|
* @returns A new password generation request with policy applied.
|
||||||
*/
|
*/
|
||||||
applyPolicy(options: GeneratorNavigation): GeneratorNavigation {
|
applyPolicy(options: GeneratorNavigation): GeneratorNavigation {
|
||||||
return options;
|
const result = { ...options };
|
||||||
|
|
||||||
|
if (this.policyInEffect) {
|
||||||
|
result.type = this.policy.overridePasswordType ?? result.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ensures internal options consistency.
|
/** Ensures internal options consistency.
|
||||||
@ -33,12 +39,9 @@ export class GeneratorNavigationEvaluator
|
|||||||
* @returns A passphrase generation request with cascade applied.
|
* @returns A passphrase generation request with cascade applied.
|
||||||
*/
|
*/
|
||||||
sanitize(options: GeneratorNavigation): GeneratorNavigation {
|
sanitize(options: GeneratorNavigation): GeneratorNavigation {
|
||||||
const defaultType = this.policyInEffect
|
|
||||||
? this.policy.defaultType
|
|
||||||
: DefaultGeneratorNavigation.type;
|
|
||||||
return {
|
return {
|
||||||
...options,
|
...options,
|
||||||
type: options.type ?? defaultType,
|
type: options.type ?? DefaultGeneratorNavigation.type,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,26 +38,26 @@ describe("leastPrivilege", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should take the %p from the policy", () => {
|
it("should take the %p from the policy", () => {
|
||||||
const policy = createPolicy({ defaultType: "passphrase" });
|
const policy = createPolicy({ overridePasswordType: "passphrase" });
|
||||||
|
|
||||||
const result = preferPassword({ ...DisabledGeneratorNavigationPolicy }, policy);
|
const result = preferPassword({ ...DisabledGeneratorNavigationPolicy }, policy);
|
||||||
|
|
||||||
expect(result).toEqual({ defaultType: "passphrase" });
|
expect(result).toEqual({ overridePasswordType: "passphrase" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should override passphrase with password", () => {
|
it("should override passphrase with password", () => {
|
||||||
const policy = createPolicy({ defaultType: "password" });
|
const policy = createPolicy({ overridePasswordType: "password" });
|
||||||
|
|
||||||
const result = preferPassword({ defaultType: "passphrase" }, policy);
|
const result = preferPassword({ overridePasswordType: "passphrase" }, policy);
|
||||||
|
|
||||||
expect(result).toEqual({ defaultType: "password" });
|
expect(result).toEqual({ overridePasswordType: "password" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not override password", () => {
|
it("should not override password", () => {
|
||||||
const policy = createPolicy({ defaultType: "passphrase" });
|
const policy = createPolicy({ overridePasswordType: "passphrase" });
|
||||||
|
|
||||||
const result = preferPassword({ defaultType: "password" }, policy);
|
const result = preferPassword({ overridePasswordType: "password" }, policy);
|
||||||
|
|
||||||
expect(result).toEqual({ defaultType: "password" });
|
expect(result).toEqual({ overridePasswordType: "password" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,14 +2,14 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
|||||||
// FIXME: use index.ts imports once policy abstractions and models
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
// implement ADR-0002
|
// implement ADR-0002
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { GeneratorType } from "@bitwarden/generator-core";
|
import { PasswordType } from "@bitwarden/generator-core";
|
||||||
|
|
||||||
/** Policy settings affecting password generator navigation */
|
/** Policy settings affecting password generator navigation */
|
||||||
export type GeneratorNavigationPolicy = {
|
export type GeneratorNavigationPolicy = {
|
||||||
/** The type of generator that should be shown by default when opening
|
/** The type of generator that should be shown by default when opening
|
||||||
* the password generator.
|
* the password generator.
|
||||||
*/
|
*/
|
||||||
defaultType?: GeneratorType;
|
overridePasswordType?: PasswordType;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Reduces a policy into an accumulator by preferring the password generator
|
/** Reduces a policy into an accumulator by preferring the password generator
|
||||||
@ -27,13 +27,15 @@ export function preferPassword(
|
|||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOverridable = acc.defaultType !== "password" && policy.data.defaultType;
|
const isOverridable = acc.overridePasswordType !== "password" && policy.data.overridePasswordType;
|
||||||
const result = isOverridable ? { ...acc, defaultType: policy.data.defaultType } : acc;
|
const result = isOverridable
|
||||||
|
? { ...acc, overridePasswordType: policy.data.overridePasswordType }
|
||||||
|
: acc;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The default options for password generation policy. */
|
/** The default options for password generation policy. */
|
||||||
export const DisabledGeneratorNavigationPolicy: GeneratorNavigationPolicy = Object.freeze({
|
export const DisabledGeneratorNavigationPolicy: GeneratorNavigationPolicy = Object.freeze({
|
||||||
defaultType: undefined,
|
overridePasswordType: null,
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user