mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
[PM-6818] legacy generator service adapter (#8582)
* introduce legacy generators * introduce generator navigation service * Introduce default options. These accept a userId so that they can be policy-defined * replace `GeneratorOptions` with backwards compatible `GeneratorNavigation`
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* include structuredClone in test environment.
|
||||
* @jest-environment ../../../../shared/test.environment.ts
|
||||
*/
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
||||
import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
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 { UserId } from "../../../types/guid";
|
||||
import { GENERATOR_SETTINGS } from "../key-definitions";
|
||||
|
||||
import {
|
||||
GeneratorNavigationEvaluator,
|
||||
DefaultGeneratorNavigationService,
|
||||
DefaultGeneratorNavigation,
|
||||
} from "./";
|
||||
|
||||
const SomeUser = "some user" as UserId;
|
||||
|
||||
describe("DefaultGeneratorNavigationService", () => {
|
||||
describe("options$", () => {
|
||||
it("emits options", async () => {
|
||||
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
|
||||
const settings = { type: "password" as const };
|
||||
await stateProvider.setUserState(GENERATOR_SETTINGS, settings, SomeUser);
|
||||
const navigation = new DefaultGeneratorNavigationService(stateProvider, null);
|
||||
|
||||
const result = await firstValueFrom(navigation.options$(SomeUser));
|
||||
|
||||
expect(result).toEqual(settings);
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaults$", () => {
|
||||
it("emits default options", async () => {
|
||||
const navigation = new DefaultGeneratorNavigationService(null, null);
|
||||
|
||||
const result = await firstValueFrom(navigation.defaults$(SomeUser));
|
||||
|
||||
expect(result).toEqual(DefaultGeneratorNavigation);
|
||||
});
|
||||
});
|
||||
|
||||
describe("evaluator$", () => {
|
||||
it("emits a GeneratorNavigationEvaluator", async () => {
|
||||
const policyService = mock<PolicyService>({
|
||||
getAll$() {
|
||||
return of([]);
|
||||
},
|
||||
});
|
||||
const navigation = new DefaultGeneratorNavigationService(null, policyService);
|
||||
|
||||
const result = await firstValueFrom(navigation.evaluator$(SomeUser));
|
||||
|
||||
expect(result).toBeInstanceOf(GeneratorNavigationEvaluator);
|
||||
});
|
||||
});
|
||||
|
||||
describe("enforcePolicy", () => {
|
||||
it("applies policy", async () => {
|
||||
const policyService = mock<PolicyService>({
|
||||
getAll$(_type: PolicyType, _user: UserId) {
|
||||
return of([
|
||||
new Policy({
|
||||
id: "" as any,
|
||||
organizationId: "" as any,
|
||||
enabled: true,
|
||||
type: PolicyType.PasswordGenerator,
|
||||
data: { defaultType: "password" },
|
||||
}),
|
||||
]);
|
||||
},
|
||||
});
|
||||
const navigation = new DefaultGeneratorNavigationService(null, policyService);
|
||||
const options = {};
|
||||
|
||||
const result = await navigation.enforcePolicy(SomeUser, options);
|
||||
|
||||
expect(result).toMatchObject({ type: "password" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveOptions", () => {
|
||||
it("updates options$", async () => {
|
||||
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));
|
||||
const navigation = new DefaultGeneratorNavigationService(stateProvider, null);
|
||||
const settings = { type: "password" as const };
|
||||
|
||||
await navigation.saveOptions(SomeUser, settings);
|
||||
const result = await firstValueFrom(navigation.options$(SomeUser));
|
||||
|
||||
expect(result).toEqual(settings);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { BehaviorSubject, Observable, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "../../../admin-console/enums";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { GeneratorNavigationService } from "../abstractions/generator-navigation.service.abstraction";
|
||||
import { GENERATOR_SETTINGS } from "../key-definitions";
|
||||
import { reduceCollection } from "../reduce-collection.operator";
|
||||
|
||||
import { DefaultGeneratorNavigation, GeneratorNavigation } from "./generator-navigation";
|
||||
import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";
|
||||
import { DisabledGeneratorNavigationPolicy, preferPassword } from "./generator-navigation-policy";
|
||||
|
||||
export class DefaultGeneratorNavigationService implements GeneratorNavigationService {
|
||||
/** instantiates the password generator strategy.
|
||||
* @param stateProvider provides durable state
|
||||
* @param policy provides the policy to enforce
|
||||
*/
|
||||
constructor(
|
||||
private readonly stateProvider: StateProvider,
|
||||
private readonly policy: PolicyService,
|
||||
) {}
|
||||
|
||||
/** An observable monitoring the options saved to disk.
|
||||
* The observable updates when the options are saved.
|
||||
* @param userId: Identifies the user making the request
|
||||
*/
|
||||
options$(userId: UserId): Observable<GeneratorNavigation> {
|
||||
return this.stateProvider.getUserState$(GENERATOR_SETTINGS, userId);
|
||||
}
|
||||
|
||||
/** Gets the default options. */
|
||||
defaults$(userId: UserId): Observable<GeneratorNavigation> {
|
||||
return new BehaviorSubject({ ...DefaultGeneratorNavigation });
|
||||
}
|
||||
|
||||
/** An observable monitoring the options used to enforce policy.
|
||||
* The observable updates when the policy changes.
|
||||
* @param userId: Identifies the user making the request
|
||||
*/
|
||||
evaluator$(userId: UserId) {
|
||||
const evaluator$ = this.policy.getAll$(PolicyType.PasswordGenerator, userId).pipe(
|
||||
reduceCollection(preferPassword, DisabledGeneratorNavigationPolicy),
|
||||
map((policy) => new GeneratorNavigationEvaluator(policy)),
|
||||
);
|
||||
|
||||
return evaluator$;
|
||||
}
|
||||
|
||||
/** Enforces the policy on the given options
|
||||
* @param userId: Identifies the user making the request
|
||||
* @param options the options to enforce the policy on
|
||||
* @returns a new instance of the options with the policy enforced
|
||||
*/
|
||||
async enforcePolicy(userId: UserId, options: GeneratorNavigation) {
|
||||
const evaluator = await firstValueFrom(this.evaluator$(userId));
|
||||
const applied = evaluator.applyPolicy(options);
|
||||
const sanitized = evaluator.sanitize(applied);
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/** Saves the navigation options to disk.
|
||||
* @param userId: Identifies the user making the request
|
||||
* @param options the options to save
|
||||
* @returns a promise that resolves when the options are saved
|
||||
*/
|
||||
async saveOptions(userId: UserId, options: GeneratorNavigation): Promise<void> {
|
||||
await this.stateProvider.setUserState(GENERATOR_SETTINGS, options, userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { DefaultGeneratorNavigation } from "./generator-navigation";
|
||||
import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";
|
||||
|
||||
describe("GeneratorNavigationEvaluator", () => {
|
||||
describe("policyInEffect", () => {
|
||||
it.each([["passphrase"], ["password"]] as const)(
|
||||
"returns true if the policy has a defaultType (= %p)",
|
||||
(defaultType) => {
|
||||
const evaluator = new GeneratorNavigationEvaluator({ defaultType });
|
||||
|
||||
expect(evaluator.policyInEffect).toEqual(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([[undefined], [null], ["" as any]])(
|
||||
"returns false if the policy has a falsy defaultType (= %p)",
|
||||
(defaultType) => {
|
||||
const evaluator = new GeneratorNavigationEvaluator({ defaultType });
|
||||
|
||||
expect(evaluator.policyInEffect).toEqual(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("applyPolicy", () => {
|
||||
it("returns the input options", () => {
|
||||
const evaluator = new GeneratorNavigationEvaluator(null);
|
||||
const options = { type: "password" as const };
|
||||
|
||||
const result = evaluator.applyPolicy(options);
|
||||
|
||||
expect(result).toEqual(options);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitize", () => {
|
||||
it.each([["passphrase"], ["password"]] as const)(
|
||||
"defaults options to the policy's default type (= %p) when a policy is in effect",
|
||||
(defaultType) => {
|
||||
const evaluator = new GeneratorNavigationEvaluator({ defaultType });
|
||||
|
||||
const result = evaluator.sanitize({});
|
||||
|
||||
expect(result).toEqual({ type: defaultType });
|
||||
},
|
||||
);
|
||||
|
||||
it("defaults options to the default generator navigation type when a policy is not in effect", () => {
|
||||
const evaluator = new GeneratorNavigationEvaluator(null);
|
||||
|
||||
const result = evaluator.sanitize({});
|
||||
|
||||
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" });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { PolicyEvaluator } from "../abstractions";
|
||||
|
||||
import { DefaultGeneratorNavigation, GeneratorNavigation } from "./generator-navigation";
|
||||
import { GeneratorNavigationPolicy } from "./generator-navigation-policy";
|
||||
|
||||
/** Enforces policy for generator navigation options.
|
||||
*/
|
||||
export class GeneratorNavigationEvaluator
|
||||
implements PolicyEvaluator<GeneratorNavigationPolicy, GeneratorNavigation>
|
||||
{
|
||||
/** Instantiates the evaluator.
|
||||
* @param policy The policy applied by the evaluator. When this conflicts with
|
||||
* the defaults, the policy takes precedence.
|
||||
*/
|
||||
constructor(readonly policy: GeneratorNavigationPolicy) {}
|
||||
|
||||
/** {@link PolicyEvaluator.policyInEffect} */
|
||||
get policyInEffect(): boolean {
|
||||
return this.policy?.defaultType ? true : false;
|
||||
}
|
||||
|
||||
/** Apply policy to the input options.
|
||||
* @param options The options to build from. These options are not altered.
|
||||
* @returns A new password generation request with policy applied.
|
||||
*/
|
||||
applyPolicy(options: GeneratorNavigation): GeneratorNavigation {
|
||||
return options;
|
||||
}
|
||||
|
||||
/** Ensures internal options consistency.
|
||||
* @param options The options to cascade. These options are not altered.
|
||||
* @returns A passphrase generation request with cascade applied.
|
||||
*/
|
||||
sanitize(options: GeneratorNavigation): GeneratorNavigation {
|
||||
const defaultType = this.policyInEffect
|
||||
? this.policy.defaultType
|
||||
: DefaultGeneratorNavigation.type;
|
||||
return {
|
||||
...options,
|
||||
type: options.type ?? defaultType,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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 { DisabledGeneratorNavigationPolicy, preferPassword } from "./generator-navigation-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 = preferPassword(DisabledGeneratorNavigationPolicy, policy);
|
||||
|
||||
expect(result).toEqual(DisabledGeneratorNavigationPolicy);
|
||||
});
|
||||
|
||||
it("should return the accumulator when the policy is not enabled", () => {
|
||||
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
|
||||
|
||||
const result = preferPassword(DisabledGeneratorNavigationPolicy, policy);
|
||||
|
||||
expect(result).toEqual(DisabledGeneratorNavigationPolicy);
|
||||
});
|
||||
|
||||
it("should take the %p from the policy", () => {
|
||||
const policy = createPolicy({ defaultType: "passphrase" });
|
||||
|
||||
const result = preferPassword({ ...DisabledGeneratorNavigationPolicy }, policy);
|
||||
|
||||
expect(result).toEqual({ defaultType: "passphrase" });
|
||||
});
|
||||
|
||||
it("should override passphrase with password", () => {
|
||||
const policy = createPolicy({ defaultType: "password" });
|
||||
|
||||
const result = preferPassword({ defaultType: "passphrase" }, policy);
|
||||
|
||||
expect(result).toEqual({ defaultType: "password" });
|
||||
});
|
||||
|
||||
it("should not override password", () => {
|
||||
const policy = createPolicy({ defaultType: "passphrase" });
|
||||
|
||||
const result = preferPassword({ defaultType: "password" }, policy);
|
||||
|
||||
expect(result).toEqual({ defaultType: "password" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
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 { GeneratorType } from "../generator-type";
|
||||
|
||||
/** Policy settings affecting password generator navigation */
|
||||
export type GeneratorNavigationPolicy = {
|
||||
/** The type of generator that should be shown by default when opening
|
||||
* the password generator.
|
||||
*/
|
||||
defaultType?: GeneratorType;
|
||||
};
|
||||
|
||||
/** Reduces a policy into an accumulator by preferring the password generator
|
||||
* type to other generator types.
|
||||
* @param acc the accumulator
|
||||
* @param policy the policy to reduce
|
||||
* @returns the resulting `GeneratorNavigationPolicy`
|
||||
*/
|
||||
export function preferPassword(
|
||||
acc: GeneratorNavigationPolicy,
|
||||
policy: Policy,
|
||||
): GeneratorNavigationPolicy {
|
||||
const isEnabled = policy.type === PolicyType.PasswordGenerator && policy.enabled;
|
||||
if (!isEnabled) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const isOverridable = acc.defaultType !== "password" && policy.data.defaultType;
|
||||
const result = isOverridable ? { ...acc, defaultType: policy.data.defaultType } : acc;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The default options for password generation policy. */
|
||||
export const DisabledGeneratorNavigationPolicy: GeneratorNavigationPolicy = Object.freeze({
|
||||
defaultType: undefined,
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { GeneratorType } from "../generator-type";
|
||||
import { ForwarderId } from "../username/options";
|
||||
import { UsernameGeneratorType } from "../username/options/generator-options";
|
||||
|
||||
/** Stores credential generator UI state. */
|
||||
|
||||
export type GeneratorNavigation = {
|
||||
/** The kind of credential being generated.
|
||||
* @remarks The legacy generator only supports "password" and "passphrase".
|
||||
* The componentized generator supports all values.
|
||||
*/
|
||||
type?: GeneratorType;
|
||||
|
||||
/** When `type === "username"`, this stores the username algorithm. */
|
||||
username?: UsernameGeneratorType;
|
||||
|
||||
/** When `username === "forwarded"`, this stores the forwarder implementation. */
|
||||
forwarder?: ForwarderId | "";
|
||||
};
|
||||
/** The default options for password generation. */
|
||||
|
||||
export const DefaultGeneratorNavigation: Partial<GeneratorNavigation> = Object.freeze({
|
||||
type: "password",
|
||||
username: "word",
|
||||
forwarder: "",
|
||||
});
|
||||
3
libs/common/src/tools/generator/navigation/index.ts
Normal file
3
libs/common/src/tools/generator/navigation/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";
|
||||
export { DefaultGeneratorNavigationService } from "./default-generator-navigation.service";
|
||||
export { GeneratorNavigation, DefaultGeneratorNavigation } from "./generator-navigation";
|
||||
Reference in New Issue
Block a user