1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-14 10:26:19 +01:00

[PM-10107] evaluate the override password type policy (#10277)

This commit is contained in:
✨ Audrey ✨ 2024-08-09 08:54:00 -04:00 committed by GitHub
parent 795c21a1c7
commit cbe7ae68cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 84 additions and 56 deletions

View File

@ -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";
} }

View File

@ -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 ||

View 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);

View File

@ -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";

View File

@ -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];

View File

@ -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,

View File

@ -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];

View File

@ -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" },
}), }),
]); ]);
}, },

View File

@ -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" });
});
}); });
}); });

View File

@ -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,
}; };
} }
} }

View File

@ -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" });
}); });
}); });

View File

@ -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,
}); });