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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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: "",
|
||||
});
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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 | "";
|
||||
};
|
||||
6
libs/tools/generator/extensions/src/navigation/index.ts
Normal file
6
libs/tools/generator/extensions/src/navigation/index.ts
Normal 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";
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"],
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user