1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-7289] implement generator libraries (#9549)

This is a copy of the files. The source in `@bitwarden/common` will be deleted once
all of the applications have been ported to the library.
This commit is contained in:
✨ Audrey ✨
2024-06-11 16:06:37 -04:00
committed by GitHub
parent fe82dbe2b9
commit 882a432ca6
130 changed files with 9335 additions and 46 deletions

View File

@@ -0,0 +1,98 @@
import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { UserId } from "@bitwarden/common/types/guid";
import { FakeStateProvider, mockAccountServiceWith } from "../../../../../common/spec";
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);
});
});
});

View File

@@ -0,0 +1,73 @@
import { BehaviorSubject, Observable, firstValueFrom, map } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { StateProvider } from "@bitwarden/common/platform/state";
import { distinctIfShallowMatch, reduceCollection } from "@bitwarden/common/tools/rx";
import { UserId } from "@bitwarden/common/types/guid";
import { DefaultGeneratorNavigation } from "./default-generator-navigation";
import { GeneratorNavigation } from "./generator-navigation";
import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";
import { DisabledGeneratorNavigationPolicy, preferPassword } from "./generator-navigation-policy";
import { GeneratorNavigationService } from "./generator-navigation.service.abstraction";
import { GENERATOR_SETTINGS } from "./key-definitions";
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),
distinctIfShallowMatch(),
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);
}
}

View File

@@ -0,0 +1,8 @@
import { GeneratorNavigation } from "./generator-navigation";
/** The default options for password generation. */
export const DefaultGeneratorNavigation: Partial<GeneratorNavigation> = Object.freeze({
type: "password",
username: "word",
forwarder: "",
});

View File

@@ -0,0 +1,64 @@
import { DefaultGeneratorNavigation } from "./default-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" });
});
});
});

View File

@@ -0,0 +1,44 @@
import { PolicyEvaluator } from "@bitwarden/generator-core";
import { DefaultGeneratorNavigation } from "./default-generator-navigation";
import { 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,
};
}
}

View File

@@ -0,0 +1,63 @@
import { PolicyType } from "@bitwarden/common/admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { PolicyId } from "@bitwarden/common/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" });
});
});

View File

@@ -0,0 +1,39 @@
import { PolicyType } from "@bitwarden/common/admin-console/enums";
// FIXME: use index.ts imports once policy abstractions and models
// implement ADR-0002
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { GeneratorType } from "@bitwarden/generator-core";
/** 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,
});

View File

@@ -0,0 +1,42 @@
import { Observable } from "rxjs";
import { UserId } from "@bitwarden/common/types/guid";
import { PolicyEvaluator } from "@bitwarden/generator-core";
import { GeneratorNavigation } from "./generator-navigation";
import { GeneratorNavigationPolicy } from "./generator-navigation-policy";
/** Loads and stores generator navigational data
*/
export abstract class GeneratorNavigationService {
/** 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>;
/** Gets the default options. */
defaults$: (userId: UserId) => Observable<GeneratorNavigation>;
/** 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,
) => Observable<PolicyEvaluator<GeneratorNavigationPolicy, GeneratorNavigation>>;
/** 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
*/
enforcePolicy: (userId: UserId, options: GeneratorNavigation) => Promise<GeneratorNavigation>;
/** 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
*/
saveOptions: (userId: UserId, options: GeneratorNavigation) => Promise<void>;
}

View File

@@ -0,0 +1,16 @@
import { GeneratorType, ForwarderId, UsernameGeneratorType } from "@bitwarden/generator-core";
/** 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 | "";
};

View File

@@ -0,0 +1,6 @@
export { DefaultGeneratorNavigation } from "./default-generator-navigation";
export { DefaultGeneratorNavigationService } from "./default-generator-navigation.service";
export { GeneratorNavigation } from "./generator-navigation";
export { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator";
export { GeneratorNavigationService } from "./generator-navigation.service.abstraction";
export { GeneratorNavigationPolicy } from "./generator-navigation-policy";

View File

@@ -0,0 +1,11 @@
import { GENERATOR_SETTINGS } from "./key-definitions";
describe("Key definitions", () => {
describe("GENERATOR_SETTINGS", () => {
it("should pass through deserialization", () => {
const value = {};
const result = GENERATOR_SETTINGS.deserializer(value);
expect(result).toBe(value);
});
});
});

View File

@@ -0,0 +1,13 @@
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
import { GeneratorNavigation } from "./generator-navigation";
/** plaintext password generation options */
export const GENERATOR_SETTINGS = new UserKeyDefinition<GeneratorNavigation>(
GENERATOR_DISK,
"generatorSettings",
{
deserializer: (value) => value,
clearOn: ["logout"],
},
);