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

[PM-6556] reintroduce policy reduction for multi-org accounts (#8409)

This commit is contained in:
✨ Audrey ✨
2024-03-26 07:59:45 -04:00
committed by GitHub
parent da14d01062
commit d000f081da
21 changed files with 388 additions and 241 deletions

View File

@@ -0,0 +1,55 @@
import { PolicyType } from "../../../admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "../../../admin-console/models/domain/policy";
import { PolicyId } from "../../../types/guid";
import { DisabledPasswordGeneratorPolicy, leastPrivilege } from "./password-generator-policy";
function createPolicy(
data: any,
type: PolicyType = PolicyType.PasswordGenerator,
enabled: boolean = true,
) {
return new Policy({
id: "id" as PolicyId,
organizationId: "organizationId",
data,
enabled,
type,
});
}
describe("leastPrivilege", () => {
it("should return the accumulator when the policy type does not apply", () => {
const policy = createPolicy({}, PolicyType.RequireSso);
const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy);
expect(result).toEqual(DisabledPasswordGeneratorPolicy);
});
it("should return the accumulator when the policy is not enabled", () => {
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy);
expect(result).toEqual(DisabledPasswordGeneratorPolicy);
});
it.each([
["minLength", 10, "minLength"],
["useUpper", true, "useUppercase"],
["useLower", true, "useLowercase"],
["useNumbers", true, "useNumbers"],
["minNumbers", 10, "numberCount"],
["useSpecial", true, "useSpecial"],
["minSpecial", 10, "specialCount"],
])("should take the %p from the policy", (input, value, expected) => {
const policy = createPolicy({ [input]: value });
const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy);
expect(result).toEqual({ ...DisabledPasswordGeneratorPolicy, [expected]: value });
});
});

View File

@@ -1,3 +1,8 @@
import { PolicyType } from "../../../admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "../../../admin-console/models/domain/policy";
/** Policy options enforced during password generation. */
export type PasswordGeneratorPolicy = {
/** The minimum length of generated passwords.
@@ -48,3 +53,25 @@ export const DisabledPasswordGeneratorPolicy: PasswordGeneratorPolicy = Object.f
useSpecial: false,
specialCount: 0,
});
/** Reduces a policy into an accumulator by accepting the most restrictive
* values from each policy.
* @param acc the accumulator
* @param policy the policy to reduce
* @returns the most restrictive values between the policy and accumulator.
*/
export function leastPrivilege(acc: PasswordGeneratorPolicy, policy: Policy) {
if (policy.type !== PolicyType.PasswordGenerator || !policy.enabled) {
return acc;
}
return {
minLength: Math.max(acc.minLength, policy.data.minLength ?? acc.minLength),
useUppercase: policy.data.useUpper || acc.useUppercase,
useLowercase: policy.data.useLower || acc.useLowercase,
useNumbers: policy.data.useNumbers || acc.useNumbers,
numberCount: Math.max(acc.numberCount, policy.data.minNumbers ?? acc.numberCount),
useSpecial: policy.data.useSpecial || acc.useSpecial,
specialCount: Math.max(acc.specialCount, policy.data.minSpecial ?? acc.specialCount),
};
}

View File

@@ -4,6 +4,7 @@
*/
import { mock } from "jest-mock-extended";
import { of, firstValueFrom } from "rxjs";
import { PolicyType } from "../../../admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
@@ -24,17 +25,8 @@ import {
const SomeUser = "some user" as UserId;
describe("Password generation strategy", () => {
describe("evaluator()", () => {
it("should throw if the policy type is incorrect", () => {
const strategy = new PasswordGeneratorStrategy(null, null);
const policy = mock<Policy>({
type: PolicyType.DisableSend,
});
expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+"));
});
it("should map to the policy evaluator", () => {
describe("toEvaluator()", () => {
it("should map to a password policy evaluator", async () => {
const strategy = new PasswordGeneratorStrategy(null, null);
const policy = mock<Policy>({
type: PolicyType.PasswordGenerator,
@@ -49,7 +41,8 @@ describe("Password generation strategy", () => {
},
});
const evaluator = strategy.evaluator(policy);
const evaluator$ = of([policy]).pipe(strategy.toEvaluator());
const evaluator = await firstValueFrom(evaluator$);
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
expect(evaluator.policy).toMatchObject({
@@ -63,13 +56,18 @@ describe("Password generation strategy", () => {
});
});
it("should map `null` to a default policy evaluator", () => {
const strategy = new PasswordGeneratorStrategy(null, null);
const evaluator = strategy.evaluator(null);
it.each([[[]], [null], [undefined]])(
"should map `%p` to a disabled password policy evaluator",
async (policies) => {
const strategy = new PasswordGeneratorStrategy(null, null);
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy);
});
const evaluator$ = of(policies).pipe(strategy.toEvaluator());
const evaluator = await firstValueFrom(evaluator$);
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy);
},
);
});
describe("durableState", () => {

View File

@@ -1,11 +1,11 @@
import { map, pipe } from "rxjs";
import { GeneratorStrategy } from "..";
import { PolicyType } from "../../../admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "../../../admin-console/models/domain/policy";
import { StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { PASSWORD_SETTINGS } from "../key-definitions";
import { reduceCollection } from "../reduce-collection.operator";
import { PasswordGenerationOptions } from "./password-generation-options";
import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction";
@@ -13,6 +13,7 @@ import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-
import {
DisabledPasswordGeneratorPolicy,
PasswordGeneratorPolicy,
leastPrivilege,
} from "./password-generator-policy";
const ONE_MINUTE = 60 * 1000;
@@ -43,26 +44,12 @@ export class PasswordGeneratorStrategy
return ONE_MINUTE;
}
/** {@link GeneratorStrategy.evaluator} */
evaluator(policy: Policy): PasswordGeneratorOptionsEvaluator {
if (!policy) {
return new PasswordGeneratorOptionsEvaluator(DisabledPasswordGeneratorPolicy);
}
if (policy.type !== this.policy) {
const details = `Expected: ${this.policy}. Received: ${policy.type}`;
throw Error("Mismatched policy type. " + details);
}
return new PasswordGeneratorOptionsEvaluator({
minLength: policy.data.minLength,
useUppercase: policy.data.useUpper,
useLowercase: policy.data.useLower,
useNumbers: policy.data.useNumbers,
numberCount: policy.data.minNumbers,
useSpecial: policy.data.useSpecial,
specialCount: policy.data.minSpecial,
});
/** {@link GeneratorStrategy.toEvaluator} */
toEvaluator() {
return pipe(
reduceCollection(leastPrivilege, DisabledPasswordGeneratorPolicy),
map((policy) => new PasswordGeneratorOptionsEvaluator(policy)),
);
}
/** {@link GeneratorStrategy.generate} */