mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01:00
[PM-5609] passphrase settings component & services (#10535)
This commit is contained in:
parent
819c312ce2
commit
8c4b8d71ea
@ -9,6 +9,7 @@ import {
|
|||||||
unauthGuardFn,
|
unauthGuardFn,
|
||||||
} from "@bitwarden/angular/auth/guards";
|
} from "@bitwarden/angular/auth/guards";
|
||||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||||
|
import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap";
|
||||||
import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap";
|
import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap";
|
||||||
import {
|
import {
|
||||||
AnonLayoutWrapperComponent,
|
AnonLayoutWrapperComponent,
|
||||||
@ -51,6 +52,7 @@ import { PremiumV2Component } from "../billing/popup/settings/premium-v2.compone
|
|||||||
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
||||||
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
||||||
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
|
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
|
||||||
|
import { CredentialGeneratorComponent } from "../tools/popup/generator/credential-generator.component";
|
||||||
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
||||||
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
|
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
|
||||||
import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component";
|
import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component";
|
||||||
@ -472,12 +474,11 @@ const routes: Routes = [
|
|||||||
canDeactivate: [clearVaultStateGuard],
|
canDeactivate: [clearVaultStateGuard],
|
||||||
data: { state: "tabs_vault" },
|
data: { state: "tabs_vault" },
|
||||||
}),
|
}),
|
||||||
{
|
...generatorSwap(GeneratorComponent, CredentialGeneratorComponent, {
|
||||||
path: "generator",
|
path: "generator",
|
||||||
component: GeneratorComponent,
|
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { state: "tabs_generator" },
|
data: { state: "tabs_generator" },
|
||||||
},
|
}),
|
||||||
...extensionRefreshSwap(SettingsComponent, SettingsV2Component, {
|
...extensionRefreshSwap(SettingsComponent, SettingsV2Component, {
|
||||||
path: "settings",
|
path: "settings",
|
||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<bit-passphrase-settings />
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
import { PassphraseSettingsComponent } from "@bitwarden/generator-components";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "credential-generator",
|
||||||
|
templateUrl: "credential-generator.component.html",
|
||||||
|
imports: [PassphraseSettingsComponent],
|
||||||
|
})
|
||||||
|
export class CredentialGeneratorComponent {}
|
@ -32,6 +32,7 @@ module.exports = {
|
|||||||
"<rootDir>/libs/components/jest.config.js",
|
"<rootDir>/libs/components/jest.config.js",
|
||||||
"<rootDir>/libs/tools/export/vault-export/vault-export-core/jest.config.js",
|
"<rootDir>/libs/tools/export/vault-export/vault-export-core/jest.config.js",
|
||||||
"<rootDir>/libs/tools/generator/core/jest.config.js",
|
"<rootDir>/libs/tools/generator/core/jest.config.js",
|
||||||
|
"<rootDir>/libs/tools/generator/components/jest.config.js",
|
||||||
"<rootDir>/libs/tools/generator/extensions/history/jest.config.js",
|
"<rootDir>/libs/tools/generator/extensions/history/jest.config.js",
|
||||||
"<rootDir>/libs/tools/generator/extensions/legacy/jest.config.js",
|
"<rootDir>/libs/tools/generator/extensions/legacy/jest.config.js",
|
||||||
"<rootDir>/libs/tools/generator/extensions/navigation/jest.config.js",
|
"<rootDir>/libs/tools/generator/extensions/navigation/jest.config.js",
|
||||||
|
32
libs/angular/src/tools/generator/generator-swap.ts
Normal file
32
libs/angular/src/tools/generator/generator-swap.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Type, inject } from "@angular/core";
|
||||||
|
import { Route, Routes } from "@angular/router";
|
||||||
|
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
|
||||||
|
import { componentRouteSwap } from "../../utils/component-route-swap";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to swap between two components based on the GeneratorToolsModernization feature flag.
|
||||||
|
* @param defaultComponent - The current non-refreshed component to render.
|
||||||
|
* @param refreshedComponent - The new refreshed component to render.
|
||||||
|
* @param options - The shared route options to apply to the default component, and to the alt component if altOptions is not provided.
|
||||||
|
* @param altOptions - The alt route options to apply to the alt component.
|
||||||
|
*/
|
||||||
|
export function generatorSwap(
|
||||||
|
defaultComponent: Type<any>,
|
||||||
|
refreshedComponent: Type<any>,
|
||||||
|
options: Route,
|
||||||
|
altOptions?: Route,
|
||||||
|
): Routes {
|
||||||
|
return componentRouteSwap(
|
||||||
|
defaultComponent,
|
||||||
|
refreshedComponent,
|
||||||
|
async () => {
|
||||||
|
const configService = inject(ConfigService);
|
||||||
|
return configService.getFeatureFlag(FeatureFlag.GeneratorToolsModernization);
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
altOptions,
|
||||||
|
);
|
||||||
|
}
|
@ -257,8 +257,8 @@ describe("UserStateSubject", () => {
|
|||||||
|
|
||||||
let actual: TestType = null;
|
let actual: TestType = null;
|
||||||
subject.subscribe({
|
subject.subscribe({
|
||||||
error: (value) => {
|
error: (value: unknown) => {
|
||||||
actual = value;
|
actual = value as any;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
subject.error(expected);
|
subject.error(expected);
|
||||||
@ -275,8 +275,8 @@ describe("UserStateSubject", () => {
|
|||||||
|
|
||||||
let actual: TestType = null;
|
let actual: TestType = null;
|
||||||
subject.subscribe({
|
subject.subscribe({
|
||||||
error: (value) => {
|
error: (value: unknown) => {
|
||||||
actual = value;
|
actual = value as any;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
subject.error("expectedError");
|
subject.error("expectedError");
|
||||||
@ -415,8 +415,8 @@ describe("UserStateSubject", () => {
|
|||||||
|
|
||||||
let error = false;
|
let error = false;
|
||||||
subject.subscribe({
|
subject.subscribe({
|
||||||
error: (e) => {
|
error: (e: unknown) => {
|
||||||
error = e;
|
error = e as any;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
singleUserId$.next(errorUserId);
|
singleUserId$.next(errorUserId);
|
||||||
@ -434,8 +434,8 @@ describe("UserStateSubject", () => {
|
|||||||
|
|
||||||
let actual = false;
|
let actual = false;
|
||||||
subject.subscribe({
|
subject.subscribe({
|
||||||
error: (e) => {
|
error: (e: unknown) => {
|
||||||
actual = e;
|
actual = e as any;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
singleUserId$.error(expected);
|
singleUserId$.error(expected);
|
||||||
@ -454,8 +454,8 @@ describe("UserStateSubject", () => {
|
|||||||
|
|
||||||
let actual = false;
|
let actual = false;
|
||||||
subject.subscribe({
|
subject.subscribe({
|
||||||
error: (e) => {
|
error: (e: unknown) => {
|
||||||
actual = e;
|
actual = e as any;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
when$.error(expected);
|
when$.error(expected);
|
||||||
@ -464,4 +464,14 @@ describe("UserStateSubject", () => {
|
|||||||
expect(actual).toEqual(expected);
|
expect(actual).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("userId", () => {
|
||||||
|
it("returns the userId to which the subject is bound", () => {
|
||||||
|
const state = new FakeSingleUserState<TestType>(SomeUser, { foo: "init" });
|
||||||
|
const singleUserId$ = new Subject<UserId>();
|
||||||
|
const subject = new UserStateSubject(state, { singleUserId$ });
|
||||||
|
|
||||||
|
expect(subject.userId).toEqual(SomeUser);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,6 +15,8 @@ import {
|
|||||||
ignoreElements,
|
ignoreElements,
|
||||||
endWith,
|
endWith,
|
||||||
startWith,
|
startWith,
|
||||||
|
Observable,
|
||||||
|
Subscription,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import { Simplify } from "type-fest";
|
import { Simplify } from "type-fest";
|
||||||
|
|
||||||
@ -59,7 +61,10 @@ export type UserStateSubjectDependencies<State, Dependency> = Simplify<
|
|||||||
* @template State the state stored by the subject
|
* @template State the state stored by the subject
|
||||||
* @template Dependencies use-specific dependencies provided by the user.
|
* @template Dependencies use-specific dependencies provided by the user.
|
||||||
*/
|
*/
|
||||||
export class UserStateSubject<State, Dependencies = null> implements SubjectLike<State> {
|
export class UserStateSubject<State, Dependencies = null>
|
||||||
|
extends Observable<State>
|
||||||
|
implements SubjectLike<State>
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* Instantiates the user state subject
|
* Instantiates the user state subject
|
||||||
* @param state the backing store of the subject
|
* @param state the backing store of the subject
|
||||||
@ -76,6 +81,8 @@ export class UserStateSubject<State, Dependencies = null> implements SubjectLike
|
|||||||
private state: SingleUserState<State>,
|
private state: SingleUserState<State>,
|
||||||
private dependencies: UserStateSubjectDependencies<State, Dependencies>,
|
private dependencies: UserStateSubjectDependencies<State, Dependencies>,
|
||||||
) {
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
// normalize dependencies
|
// normalize dependencies
|
||||||
const when$ = (this.dependencies.when$ ?? new BehaviorSubject(true)).pipe(
|
const when$ = (this.dependencies.when$ ?? new BehaviorSubject(true)).pipe(
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
@ -114,6 +121,12 @@ export class UserStateSubject<State, Dependencies = null> implements SubjectLike
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The userId to which the subject is bound.
|
||||||
|
*/
|
||||||
|
get userId() {
|
||||||
|
return this.state.userId;
|
||||||
|
}
|
||||||
|
|
||||||
next(value: State) {
|
next(value: State) {
|
||||||
this.input?.next(value);
|
this.input?.next(value);
|
||||||
}
|
}
|
||||||
@ -130,7 +143,7 @@ export class UserStateSubject<State, Dependencies = null> implements SubjectLike
|
|||||||
* @param observer listening for events
|
* @param observer listening for events
|
||||||
* @returns the subscription
|
* @returns the subscription
|
||||||
*/
|
*/
|
||||||
subscribe(observer: Partial<Observer<State>> | ((value: State) => void)): Unsubscribable {
|
subscribe(observer?: Partial<Observer<State>> | ((value: State) => void) | null): Subscription {
|
||||||
return this.output.subscribe(observer);
|
return this.output.subscribe(observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
48
libs/common/src/tools/types.ts
Normal file
48
libs/common/src/tools/types.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Simplify } from "type-fest";
|
||||||
|
|
||||||
|
/** Constraints that are shared by all primitive field types */
|
||||||
|
type PrimitiveConstraint = {
|
||||||
|
/** presence indicates the field is required */
|
||||||
|
required?: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Constraints that are shared by string fields */
|
||||||
|
type StringConstraints = {
|
||||||
|
/** minimum string length. When absent, min length is 0. */
|
||||||
|
minLength?: number;
|
||||||
|
|
||||||
|
/** maximum string length. When absent, max length is unbounded. */
|
||||||
|
maxLength?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Constraints that are shared by number fields */
|
||||||
|
type NumberConstraints = {
|
||||||
|
/** minimum number value. When absent, min value is unbounded. */
|
||||||
|
min?: number;
|
||||||
|
|
||||||
|
/** maximum number value. When absent, min value is unbounded. */
|
||||||
|
max?: number;
|
||||||
|
|
||||||
|
/** presence indicates the field only accepts integer values */
|
||||||
|
integer?: true;
|
||||||
|
|
||||||
|
/** requires the number be a multiple of the step value */
|
||||||
|
step?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Utility type that transforms keys of T into their supported
|
||||||
|
* validators.
|
||||||
|
*/
|
||||||
|
export type Constraints<T> = {
|
||||||
|
[Key in keyof T]: Simplify<
|
||||||
|
PrimitiveConstraint &
|
||||||
|
(T[Key] extends string
|
||||||
|
? StringConstraints
|
||||||
|
: T[Key] extends number
|
||||||
|
? NumberConstraints
|
||||||
|
: never)
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** utility type for methods that evaluate constraints generically. */
|
||||||
|
export type AnyConstraint = PrimitiveConstraint & StringConstraints & NumberConstraints;
|
48
libs/tools/generator/components/src/dependencies.ts
Normal file
48
libs/tools/generator/components/src/dependencies.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { ReactiveFormsModule } from "@angular/forms";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||||
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
import {
|
||||||
|
CardComponent,
|
||||||
|
CheckboxModule,
|
||||||
|
ColorPasswordModule,
|
||||||
|
FormFieldModule,
|
||||||
|
InputModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
import { CredentialGeneratorService } from "@bitwarden/generator-core";
|
||||||
|
|
||||||
|
/** Shared module containing generator component dependencies */
|
||||||
|
@NgModule({
|
||||||
|
imports: [SectionComponent, SectionHeaderComponent, CardComponent],
|
||||||
|
exports: [
|
||||||
|
JslibModule,
|
||||||
|
JslibServicesModule,
|
||||||
|
FormFieldModule,
|
||||||
|
CommonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ColorPasswordModule,
|
||||||
|
InputModule,
|
||||||
|
CheckboxModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
CardComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
safeProvider({
|
||||||
|
provide: CredentialGeneratorService,
|
||||||
|
useClass: CredentialGeneratorService,
|
||||||
|
deps: [StateProvider, PolicyService],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
declarations: [],
|
||||||
|
})
|
||||||
|
export class DependenciesModule {
|
||||||
|
constructor() {}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
@ -0,0 +1,38 @@
|
|||||||
|
<bit-section>
|
||||||
|
<bit-section-header *ngIf="showHeader">
|
||||||
|
<h6 bitTypography="h6">{{ "options" | i18n }}</h6>
|
||||||
|
</bit-section-header>
|
||||||
|
<form class="box" [formGroup]="settings" class="tw-container">
|
||||||
|
<div class="tw-mb-4">
|
||||||
|
<bit-card>
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "numWords" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
formControlName="numWords"
|
||||||
|
id="num-words"
|
||||||
|
type="number"
|
||||||
|
[min]="minNumWords"
|
||||||
|
[max]="maxNumWords"
|
||||||
|
/>
|
||||||
|
</bit-form-field>
|
||||||
|
</bit-card>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<bit-card>
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "wordSeparator" | i18n }}</bit-label>
|
||||||
|
<input bitInput formControlName="wordSeparator" id="word-separator" type="text" />
|
||||||
|
</bit-form-field>
|
||||||
|
<bit-form-control>
|
||||||
|
<input bitCheckbox formControlName="capitalize" id="capitalize" type="checkbox" />
|
||||||
|
<bit-label>{{ "capitalize" | i18n }}</bit-label>
|
||||||
|
</bit-form-control>
|
||||||
|
<bit-form-control>
|
||||||
|
<input bitCheckbox formControlName="includeNumber" id="include-number" type="checkbox" />
|
||||||
|
<bit-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||||
|
</bit-form-control>
|
||||||
|
</bit-card>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</bit-section>
|
@ -0,0 +1,139 @@
|
|||||||
|
import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core";
|
||||||
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
import { BehaviorSubject, skip, takeUntil, Subject } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import {
|
||||||
|
Generators,
|
||||||
|
CredentialGeneratorService,
|
||||||
|
PassphraseGenerationOptions,
|
||||||
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
|
import { DependenciesModule } from "./dependencies";
|
||||||
|
import { completeOnAccountSwitch, toValidators } from "./util";
|
||||||
|
|
||||||
|
const Controls = Object.freeze({
|
||||||
|
numWords: "numWords",
|
||||||
|
includeNumber: "includeNumber",
|
||||||
|
capitalize: "capitalize",
|
||||||
|
wordSeparator: "wordSeparator",
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Options group for passphrases */
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "bit-passphrase-settings",
|
||||||
|
templateUrl: "passphrase-settings.component.html",
|
||||||
|
imports: [DependenciesModule],
|
||||||
|
})
|
||||||
|
export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
||||||
|
/** Instantiates the component
|
||||||
|
* @param accountService queries user availability
|
||||||
|
* @param generatorService settings and policy logic
|
||||||
|
* @param formBuilder reactive form controls
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private generatorService: CredentialGeneratorService,
|
||||||
|
private accountService: AccountService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Binds the passphrase component to a specific user's settings.
|
||||||
|
* When this input is not provided, the form binds to the active
|
||||||
|
* user
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
userId: UserId | null;
|
||||||
|
|
||||||
|
/** When `true`, an options header is displayed by the component. Otherwise, the header is hidden. */
|
||||||
|
@Input()
|
||||||
|
showHeader: boolean = true;
|
||||||
|
|
||||||
|
/** Emits settings updates and completes if the settings become unavailable.
|
||||||
|
* @remarks this does not emit the initial settings. If you would like
|
||||||
|
* to receive live settings updates including the initial update,
|
||||||
|
* use `CredentialGeneratorService.settings$(...)` instead.
|
||||||
|
*/
|
||||||
|
@Output()
|
||||||
|
readonly onUpdated = new EventEmitter<PassphraseGenerationOptions>();
|
||||||
|
|
||||||
|
protected settings = this.formBuilder.group({
|
||||||
|
[Controls.numWords]: [Generators.Passphrase.settings.initial.numWords],
|
||||||
|
[Controls.wordSeparator]: [Generators.Passphrase.settings.initial.wordSeparator],
|
||||||
|
[Controls.capitalize]: [Generators.Passphrase.settings.initial.capitalize],
|
||||||
|
[Controls.includeNumber]: [Generators.Passphrase.settings.initial.includeNumber],
|
||||||
|
});
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
const singleUserId$ = this.singleUserId$();
|
||||||
|
const settings = await this.generatorService.settings(Generators.Passphrase, { singleUserId$ });
|
||||||
|
|
||||||
|
// skips reactive event emissions to break a subscription cycle
|
||||||
|
settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
|
||||||
|
this.settings.patchValue(s, { emitEvent: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
// the first emission is the current value; subsequent emissions are updates
|
||||||
|
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||||
|
|
||||||
|
// dynamic policy enforcement
|
||||||
|
this.generatorService
|
||||||
|
.policy$(Generators.Passphrase, { userId$: singleUserId$ })
|
||||||
|
.pipe(takeUntil(this.destroyed$))
|
||||||
|
.subscribe((policy) => {
|
||||||
|
this.settings
|
||||||
|
.get(Controls.numWords)
|
||||||
|
.setValidators(toValidators(Controls.numWords, Generators.Passphrase, policy));
|
||||||
|
|
||||||
|
this.settings
|
||||||
|
.get(Controls.wordSeparator)
|
||||||
|
.setValidators(toValidators(Controls.wordSeparator, Generators.Passphrase, policy));
|
||||||
|
|
||||||
|
// forward word boundaries to the template (can't do it through the rx form)
|
||||||
|
// FIXME: move the boundary logic fully into the policy evaluator
|
||||||
|
this.minNumWords =
|
||||||
|
policy.numWords?.min ?? Generators.Passphrase.settings.constraints.numWords.min;
|
||||||
|
this.maxNumWords =
|
||||||
|
policy.numWords?.max ?? Generators.Passphrase.settings.constraints.numWords.max;
|
||||||
|
|
||||||
|
this.toggleEnabled(Controls.capitalize, !policy.policy.capitalize);
|
||||||
|
this.toggleEnabled(Controls.includeNumber, !policy.policy.includeNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
// now that outputs are set up, connect inputs
|
||||||
|
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** attribute binding for numWords[min] */
|
||||||
|
protected minNumWords: number;
|
||||||
|
|
||||||
|
/** attribute binding for numWords[max] */
|
||||||
|
protected maxNumWords: number;
|
||||||
|
|
||||||
|
private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
this.settings.get(setting).enable();
|
||||||
|
} else {
|
||||||
|
this.settings.get(setting).disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private singleUserId$() {
|
||||||
|
// FIXME: this branch should probably scan for the user and make sure
|
||||||
|
// the account is unlocked
|
||||||
|
if (this.userId) {
|
||||||
|
return new BehaviorSubject(this.userId as UserId).asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.accountService.activeAccount$.pipe(
|
||||||
|
completeOnAccountSwitch(),
|
||||||
|
takeUntil(this.destroyed$),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly destroyed$ = new Subject<void>();
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroyed$.complete();
|
||||||
|
}
|
||||||
|
}
|
68
libs/tools/generator/components/src/util.ts
Normal file
68
libs/tools/generator/components/src/util.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { ValidatorFn, Validators } from "@angular/forms";
|
||||||
|
import { map, pairwise, pipe, skipWhile, startWith, takeWhile } from "rxjs";
|
||||||
|
|
||||||
|
import { AnyConstraint, Constraints } from "@bitwarden/common/tools/types";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CredentialGeneratorConfiguration } from "@bitwarden/generator-core";
|
||||||
|
|
||||||
|
export function completeOnAccountSwitch() {
|
||||||
|
return pipe(
|
||||||
|
map(({ id }: { id: UserId | null }) => id),
|
||||||
|
skipWhile((id) => !id),
|
||||||
|
startWith(null as UserId),
|
||||||
|
pairwise(),
|
||||||
|
takeWhile(([prev, next]) => (prev ?? next) === next),
|
||||||
|
map(([_, id]) => id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toValidators<Policy, Settings>(
|
||||||
|
target: keyof Settings,
|
||||||
|
configuration: CredentialGeneratorConfiguration<Settings, Policy>,
|
||||||
|
policy?: Constraints<Settings>,
|
||||||
|
) {
|
||||||
|
const validators: Array<ValidatorFn> = [];
|
||||||
|
|
||||||
|
// widen the types to avoid typecheck issues
|
||||||
|
const config: AnyConstraint = configuration.settings.constraints[target];
|
||||||
|
const runtime: AnyConstraint = policy[target];
|
||||||
|
|
||||||
|
const required = getConstraint("required", config, runtime) ?? false;
|
||||||
|
if (required) {
|
||||||
|
validators.push(Validators.required);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxLength = getConstraint("maxLength", config, runtime);
|
||||||
|
if (maxLength !== undefined) {
|
||||||
|
validators.push(Validators.maxLength(maxLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
const minLength = getConstraint("minLength", config, runtime);
|
||||||
|
if (minLength !== undefined) {
|
||||||
|
validators.push(Validators.minLength(config.minLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = getConstraint("min", config, runtime);
|
||||||
|
if (min !== undefined) {
|
||||||
|
validators.push(Validators.min(min));
|
||||||
|
}
|
||||||
|
|
||||||
|
const max = getConstraint("max", config, runtime);
|
||||||
|
if (max === undefined) {
|
||||||
|
validators.push(Validators.max(max));
|
||||||
|
}
|
||||||
|
|
||||||
|
return validators;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConstraint<Key extends keyof AnyConstraint>(
|
||||||
|
key: Key,
|
||||||
|
config: AnyConstraint,
|
||||||
|
policy?: AnyConstraint,
|
||||||
|
) {
|
||||||
|
if (policy && key in policy) {
|
||||||
|
return policy[key] ?? config[key];
|
||||||
|
} else if (key in config) {
|
||||||
|
return config[key];
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
import { PassphraseGeneratorPolicy } from "../types";
|
|
||||||
|
|
||||||
/** The default options for password generation policy. */
|
|
||||||
export const DisabledPassphraseGeneratorPolicy: PassphraseGeneratorPolicy = Object.freeze({
|
|
||||||
minNumberWords: 0,
|
|
||||||
capitalize: false,
|
|
||||||
includeNumber: false,
|
|
||||||
});
|
|
@ -1,12 +0,0 @@
|
|||||||
import { PasswordGeneratorPolicy } from "../types";
|
|
||||||
|
|
||||||
/** The default options for password generation policy. */
|
|
||||||
export const DisabledPasswordGeneratorPolicy: PasswordGeneratorPolicy = Object.freeze({
|
|
||||||
minLength: 0,
|
|
||||||
useUppercase: false,
|
|
||||||
useLowercase: false,
|
|
||||||
useNumbers: false,
|
|
||||||
numberCount: 0,
|
|
||||||
useSpecial: false,
|
|
||||||
specialCount: 0,
|
|
||||||
});
|
|
31
libs/tools/generator/core/src/data/generators.ts
Normal file
31
libs/tools/generator/core/src/data/generators.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { PASSPHRASE_SETTINGS } from "../strategies/storage";
|
||||||
|
import { PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
|
||||||
|
import { CredentialGeneratorConfiguration } from "../types/credential-generator-configuration";
|
||||||
|
|
||||||
|
import { DefaultPassphraseBoundaries } from "./default-passphrase-boundaries";
|
||||||
|
import { DefaultPassphraseGenerationOptions } from "./default-passphrase-generation-options";
|
||||||
|
import { Policies } from "./policies";
|
||||||
|
|
||||||
|
const PASSPHRASE = Object.freeze({
|
||||||
|
settings: {
|
||||||
|
initial: DefaultPassphraseGenerationOptions,
|
||||||
|
constraints: {
|
||||||
|
numWords: {
|
||||||
|
min: DefaultPassphraseBoundaries.numWords.min,
|
||||||
|
max: DefaultPassphraseBoundaries.numWords.max,
|
||||||
|
},
|
||||||
|
wordSeparator: { maxLength: 1 },
|
||||||
|
},
|
||||||
|
account: PASSPHRASE_SETTINGS,
|
||||||
|
},
|
||||||
|
policy: Policies.Passphrase,
|
||||||
|
} satisfies CredentialGeneratorConfiguration<
|
||||||
|
PassphraseGenerationOptions,
|
||||||
|
PassphraseGeneratorPolicy
|
||||||
|
>);
|
||||||
|
|
||||||
|
/** Generator configurations */
|
||||||
|
export const Generators = Object.freeze({
|
||||||
|
/** Passphrase generator configuration */
|
||||||
|
Passphrase: PASSPHRASE,
|
||||||
|
});
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./generators";
|
||||||
export * from "./default-addy-io-options";
|
export * from "./default-addy-io-options";
|
||||||
export * from "./default-catchall-options";
|
export * from "./default-catchall-options";
|
||||||
export * from "./default-duck-duck-go-options";
|
export * from "./default-duck-duck-go-options";
|
||||||
@ -11,8 +12,6 @@ export * from "./default-passphrase-generation-options";
|
|||||||
export * from "./default-password-generation-options";
|
export * from "./default-password-generation-options";
|
||||||
export * from "./default-subaddress-generator-options";
|
export * from "./default-subaddress-generator-options";
|
||||||
export * from "./default-simple-login-options";
|
export * from "./default-simple-login-options";
|
||||||
export * from "./disabled-passphrase-generator-policy";
|
|
||||||
export * from "./disabled-password-generator-policy";
|
|
||||||
export * from "./forwarders";
|
export * from "./forwarders";
|
||||||
export * from "./integrations";
|
export * from "./integrations";
|
||||||
export * from "./policies";
|
export * from "./policies";
|
||||||
|
@ -1,23 +1,45 @@
|
|||||||
import { DisabledPassphraseGeneratorPolicy, DisabledPasswordGeneratorPolicy } from "../data";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
passphraseLeastPrivilege,
|
passphraseLeastPrivilege,
|
||||||
passwordLeastPrivilege,
|
passwordLeastPrivilege,
|
||||||
PassphraseGeneratorOptionsEvaluator,
|
PassphraseGeneratorOptionsEvaluator,
|
||||||
PasswordGeneratorOptionsEvaluator,
|
PasswordGeneratorOptionsEvaluator,
|
||||||
} from "../policies";
|
} from "../policies";
|
||||||
import { PassphraseGeneratorPolicy, PasswordGeneratorPolicy, PolicyConfiguration } from "../types";
|
import {
|
||||||
|
PassphraseGenerationOptions,
|
||||||
|
PassphraseGeneratorPolicy,
|
||||||
|
PasswordGenerationOptions,
|
||||||
|
PasswordGeneratorPolicy,
|
||||||
|
PolicyConfiguration,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
const PASSPHRASE = Object.freeze({
|
const PASSPHRASE = Object.freeze({
|
||||||
disabledValue: DisabledPassphraseGeneratorPolicy,
|
type: PolicyType.PasswordGenerator,
|
||||||
|
disabledValue: Object.freeze({
|
||||||
|
minNumberWords: 0,
|
||||||
|
capitalize: false,
|
||||||
|
includeNumber: false,
|
||||||
|
}),
|
||||||
combine: passphraseLeastPrivilege,
|
combine: passphraseLeastPrivilege,
|
||||||
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
|
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
|
||||||
} as PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGeneratorOptionsEvaluator>);
|
createEvaluatorV2: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
|
||||||
|
} as PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGenerationOptions>);
|
||||||
|
|
||||||
const PASSWORD = Object.freeze({
|
const PASSWORD = Object.freeze({
|
||||||
disabledValue: DisabledPasswordGeneratorPolicy,
|
type: PolicyType.PasswordGenerator,
|
||||||
|
disabledValue: Object.freeze({
|
||||||
|
minLength: 0,
|
||||||
|
useUppercase: false,
|
||||||
|
useLowercase: false,
|
||||||
|
useNumbers: false,
|
||||||
|
numberCount: 0,
|
||||||
|
useSpecial: false,
|
||||||
|
specialCount: 0,
|
||||||
|
}),
|
||||||
combine: passwordLeastPrivilege,
|
combine: passwordLeastPrivilege,
|
||||||
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
|
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
|
||||||
} as PolicyConfiguration<PasswordGeneratorPolicy, PasswordGeneratorOptionsEvaluator>);
|
} as PolicyConfiguration<PasswordGeneratorPolicy, PasswordGenerationOptions>);
|
||||||
|
|
||||||
/** Policy configurations */
|
/** Policy configurations */
|
||||||
export const Policies = Object.freeze({
|
export const Policies = Object.freeze({
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
|
// The root module interface has API stability guarantees
|
||||||
export * from "./abstractions";
|
export * from "./abstractions";
|
||||||
export * from "./data";
|
export * from "./data";
|
||||||
export { createRandomizer } from "./factories";
|
export { createRandomizer } from "./factories";
|
||||||
|
export * from "./types";
|
||||||
|
export { CredentialGeneratorService } from "./services";
|
||||||
|
|
||||||
|
// These internal interfacess are exposed for use by other generator modules
|
||||||
|
// They are unstable and may change arbitrarily
|
||||||
export * as engine from "./engine";
|
export * as engine from "./engine";
|
||||||
export * as integration from "./integration";
|
export * as integration from "./integration";
|
||||||
export * as policies from "./policies";
|
export * as policies from "./policies";
|
||||||
export * as rx from "./rx";
|
export * as rx from "./rx";
|
||||||
export * as services from "./services";
|
export * as services from "./services";
|
||||||
export * as strategies from "./strategies";
|
export * as strategies from "./strategies";
|
||||||
export * from "./types";
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DisabledPassphraseGeneratorPolicy, DefaultPassphraseBoundaries } from "../data";
|
import { Policies, DefaultPassphraseBoundaries } from "../data";
|
||||||
import { PassphraseGenerationOptions } from "../types";
|
import { PassphraseGenerationOptions } from "../types";
|
||||||
|
|
||||||
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator";
|
||||||
@ -6,7 +6,7 @@ import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-opti
|
|||||||
describe("Password generator options builder", () => {
|
describe("Password generator options builder", () => {
|
||||||
describe("constructor()", () => {
|
describe("constructor()", () => {
|
||||||
it("should set the policy object to a copy of the input policy", () => {
|
it("should set the policy object to a copy of the input policy", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
policy.minNumberWords = 10; // arbitrary change for deep equality check
|
policy.minNumberWords = 10; // arbitrary change for deep equality check
|
||||||
|
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
@ -16,7 +16,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set default boundaries when a default policy is used", () => {
|
it("should set default boundaries when a default policy is used", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
expect(builder.numWords).toEqual(DefaultPassphraseBoundaries.numWords);
|
expect(builder.numWords).toEqual(DefaultPassphraseBoundaries.numWords);
|
||||||
@ -25,7 +25,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([1, 2])(
|
it.each([1, 2])(
|
||||||
"should use the default word boundaries when they are greater than `policy.minNumberWords` (= %i)",
|
"should use the default word boundaries when they are greater than `policy.minNumberWords` (= %i)",
|
||||||
(minNumberWords) => {
|
(minNumberWords) => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
policy.minNumberWords = minNumberWords;
|
policy.minNumberWords = minNumberWords;
|
||||||
|
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
@ -37,7 +37,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([8, 12, 18])(
|
it.each([8, 12, 18])(
|
||||||
"should use `policy.minNumberWords` (= %i) when it is greater than the default minimum words",
|
"should use `policy.minNumberWords` (= %i) when it is greater than the default minimum words",
|
||||||
(minNumberWords) => {
|
(minNumberWords) => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
policy.minNumberWords = minNumberWords;
|
policy.minNumberWords = minNumberWords;
|
||||||
|
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
@ -50,7 +50,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([150, 300, 9000])(
|
it.each([150, 300, 9000])(
|
||||||
"should use `policy.minNumberWords` (= %i) when it is greater than the default boundaries",
|
"should use `policy.minNumberWords` (= %i) when it is greater than the default boundaries",
|
||||||
(minNumberWords) => {
|
(minNumberWords) => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
policy.minNumberWords = minNumberWords;
|
policy.minNumberWords = minNumberWords;
|
||||||
|
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
@ -63,14 +63,14 @@ describe("Password generator options builder", () => {
|
|||||||
|
|
||||||
describe("policyInEffect", () => {
|
describe("policyInEffect", () => {
|
||||||
it("should return false when the policy has no effect", () => {
|
it("should return false when the policy has no effect", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
expect(builder.policyInEffect).toEqual(false);
|
expect(builder.policyInEffect).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has a numWords greater than the default boundary", () => {
|
it("should return true when the policy has a numWords greater than the default boundary", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
policy.minNumberWords = DefaultPassphraseBoundaries.numWords.min + 1;
|
policy.minNumberWords = DefaultPassphraseBoundaries.numWords.min + 1;
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has capitalize enabled", () => {
|
it("should return true when the policy has capitalize enabled", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
policy.capitalize = true;
|
policy.capitalize = true;
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has includeNumber enabled", () => {
|
it("should return true when the policy has includeNumber enabled", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
policy.includeNumber = true;
|
policy.includeNumber = true;
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ describe("Password generator options builder", () => {
|
|||||||
// All tests should freeze the options to ensure they are not modified
|
// All tests should freeze the options to ensure they are not modified
|
||||||
|
|
||||||
it("should set `capitalize` to `false` when the policy does not override it", () => {
|
it("should set `capitalize` to `false` when the policy does not override it", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({});
|
const options = Object.freeze({});
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set `capitalize` to `true` when the policy overrides it", () => {
|
it("should set `capitalize` to `true` when the policy overrides it", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
policy.capitalize = true;
|
policy.capitalize = true;
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ capitalize: false });
|
const options = Object.freeze({ capitalize: false });
|
||||||
@ -119,7 +119,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set `includeNumber` to false when the policy does not override it", () => {
|
it("should set `includeNumber` to false when the policy does not override it", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({});
|
const options = Object.freeze({});
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set `includeNumber` to true when the policy overrides it", () => {
|
it("should set `includeNumber` to true when the policy overrides it", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
policy.includeNumber = true;
|
policy.includeNumber = true;
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ includeNumber: false });
|
const options = Object.freeze({ includeNumber: false });
|
||||||
@ -140,7 +140,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set `numWords` to the minimum value when it isn't supplied", () => {
|
it("should set `numWords` to the minimum value when it isn't supplied", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({});
|
const options = Object.freeze({});
|
||||||
|
|
||||||
@ -154,7 +154,7 @@ describe("Password generator options builder", () => {
|
|||||||
(numWords) => {
|
(numWords) => {
|
||||||
expect(numWords).toBeLessThan(DefaultPassphraseBoundaries.numWords.min);
|
expect(numWords).toBeLessThan(DefaultPassphraseBoundaries.numWords.min);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ numWords });
|
const options = Object.freeze({ numWords });
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ describe("Password generator options builder", () => {
|
|||||||
expect(numWords).toBeGreaterThanOrEqual(DefaultPassphraseBoundaries.numWords.min);
|
expect(numWords).toBeGreaterThanOrEqual(DefaultPassphraseBoundaries.numWords.min);
|
||||||
expect(numWords).toBeLessThanOrEqual(DefaultPassphraseBoundaries.numWords.max);
|
expect(numWords).toBeLessThanOrEqual(DefaultPassphraseBoundaries.numWords.max);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ numWords });
|
const options = Object.freeze({ numWords });
|
||||||
|
|
||||||
@ -185,7 +185,7 @@ describe("Password generator options builder", () => {
|
|||||||
(numWords) => {
|
(numWords) => {
|
||||||
expect(numWords).toBeGreaterThan(DefaultPassphraseBoundaries.numWords.max);
|
expect(numWords).toBeGreaterThan(DefaultPassphraseBoundaries.numWords.max);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ numWords });
|
const options = Object.freeze({ numWords });
|
||||||
|
|
||||||
@ -196,7 +196,7 @@ describe("Password generator options builder", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("should preserve unknown properties", () => {
|
it("should preserve unknown properties", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({
|
const options = Object.freeze({
|
||||||
unknown: "property",
|
unknown: "property",
|
||||||
@ -214,7 +214,7 @@ describe("Password generator options builder", () => {
|
|||||||
// All tests should freeze the options to ensure they are not modified
|
// All tests should freeze the options to ensure they are not modified
|
||||||
|
|
||||||
it("should return the input options without altering them", () => {
|
it("should return the input options without altering them", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ wordSeparator: "%" });
|
const options = Object.freeze({ wordSeparator: "%" });
|
||||||
|
|
||||||
@ -224,7 +224,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set `wordSeparator` to '-' when it isn't supplied and there is no policy override", () => {
|
it("should set `wordSeparator` to '-' when it isn't supplied and there is no policy override", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({});
|
const options = Object.freeze({});
|
||||||
|
|
||||||
@ -234,7 +234,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should leave `wordSeparator` as the empty string '' when it is the empty string", () => {
|
it("should leave `wordSeparator` as the empty string '' when it is the empty string", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ wordSeparator: "" });
|
const options = Object.freeze({ wordSeparator: "" });
|
||||||
|
|
||||||
@ -244,7 +244,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should preserve unknown properties", () => {
|
it("should preserve unknown properties", () => {
|
||||||
const policy = Object.assign({}, DisabledPassphraseGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({
|
const options = Object.freeze({
|
||||||
unknown: "property",
|
unknown: "property",
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Constraints } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
import { PolicyEvaluator } from "../abstractions";
|
import { PolicyEvaluator } from "../abstractions";
|
||||||
import { DefaultPassphraseGenerationOptions, DefaultPassphraseBoundaries } from "../data";
|
import { DefaultPassphraseGenerationOptions, DefaultPassphraseBoundaries } from "../data";
|
||||||
import { Boundary, PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
|
import { Boundary, PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
|
||||||
@ -5,7 +7,9 @@ import { Boundary, PassphraseGenerationOptions, PassphraseGeneratorPolicy } from
|
|||||||
/** Enforces policy for passphrase generation options.
|
/** Enforces policy for passphrase generation options.
|
||||||
*/
|
*/
|
||||||
export class PassphraseGeneratorOptionsEvaluator
|
export class PassphraseGeneratorOptionsEvaluator
|
||||||
implements PolicyEvaluator<PassphraseGeneratorPolicy, PassphraseGenerationOptions>
|
implements
|
||||||
|
PolicyEvaluator<PassphraseGeneratorPolicy, PassphraseGenerationOptions>,
|
||||||
|
Constraints<PassphraseGenerationOptions>
|
||||||
{
|
{
|
||||||
// This design is not ideal, but it is a step towards a more robust passphrase
|
// This design is not ideal, but it is a step towards a more robust passphrase
|
||||||
// generator. Ideally, `sanitize` would be implemented on an options class,
|
// generator. Ideally, `sanitize` would be implemented on an options class,
|
||||||
|
@ -4,7 +4,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
|||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { PolicyId } from "@bitwarden/common/types/guid";
|
import { PolicyId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { DisabledPassphraseGeneratorPolicy } from "../data";
|
import { Policies } from "../data";
|
||||||
|
|
||||||
import { passphraseLeastPrivilege } from "./passphrase-least-privilege";
|
import { passphraseLeastPrivilege } from "./passphrase-least-privilege";
|
||||||
|
|
||||||
@ -26,17 +26,17 @@ describe("passphraseLeastPrivilege", () => {
|
|||||||
it("should return the accumulator when the policy type does not apply", () => {
|
it("should return the accumulator when the policy type does not apply", () => {
|
||||||
const policy = createPolicy({}, PolicyType.RequireSso);
|
const policy = createPolicy({}, PolicyType.RequireSso);
|
||||||
|
|
||||||
const result = passphraseLeastPrivilege(DisabledPassphraseGeneratorPolicy, policy);
|
const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy);
|
||||||
|
|
||||||
expect(result).toEqual(DisabledPassphraseGeneratorPolicy);
|
expect(result).toEqual(Policies.Passphrase.disabledValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return the accumulator when the policy is not enabled", () => {
|
it("should return the accumulator when the policy is not enabled", () => {
|
||||||
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
|
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
|
||||||
|
|
||||||
const result = passphraseLeastPrivilege(DisabledPassphraseGeneratorPolicy, policy);
|
const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy);
|
||||||
|
|
||||||
expect(result).toEqual(DisabledPassphraseGeneratorPolicy);
|
expect(result).toEqual(Policies.Passphrase.disabledValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
@ -46,8 +46,8 @@ describe("passphraseLeastPrivilege", () => {
|
|||||||
])("should take the %p from the policy", (input, value) => {
|
])("should take the %p from the policy", (input, value) => {
|
||||||
const policy = createPolicy({ [input]: value });
|
const policy = createPolicy({ [input]: value });
|
||||||
|
|
||||||
const result = passphraseLeastPrivilege(DisabledPassphraseGeneratorPolicy, policy);
|
const result = passphraseLeastPrivilege(Policies.Passphrase.disabledValue, policy);
|
||||||
|
|
||||||
expect(result).toEqual({ ...DisabledPassphraseGeneratorPolicy, [input]: value });
|
expect(result).toEqual({ ...Policies.Passphrase.disabledValue, [input]: value });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DefaultPasswordBoundaries, DisabledPasswordGeneratorPolicy } from "../data";
|
import { DefaultPasswordBoundaries, Policies } from "../data";
|
||||||
import { PasswordGenerationOptions } from "../types";
|
import { PasswordGenerationOptions } from "../types";
|
||||||
|
|
||||||
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator";
|
||||||
@ -8,7 +8,7 @@ describe("Password generator options builder", () => {
|
|||||||
|
|
||||||
describe("constructor()", () => {
|
describe("constructor()", () => {
|
||||||
it("should set the policy object to a copy of the input policy", () => {
|
it("should set the policy object to a copy of the input policy", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.minLength = 10; // arbitrary change for deep equality check
|
policy.minLength = 10; // arbitrary change for deep equality check
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@ -18,7 +18,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set default boundaries when a default policy is used", () => {
|
it("should set default boundaries when a default policy is used", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ describe("Password generator options builder", () => {
|
|||||||
(minLength) => {
|
(minLength) => {
|
||||||
expect(minLength).toBeLessThan(DefaultPasswordBoundaries.length.min);
|
expect(minLength).toBeLessThan(DefaultPasswordBoundaries.length.min);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.minLength = minLength;
|
policy.minLength = minLength;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@ -47,7 +47,7 @@ describe("Password generator options builder", () => {
|
|||||||
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.min);
|
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.min);
|
||||||
expect(expectedLength).toBeLessThanOrEqual(DefaultPasswordBoundaries.length.max);
|
expect(expectedLength).toBeLessThanOrEqual(DefaultPasswordBoundaries.length.max);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.minLength = expectedLength;
|
policy.minLength = expectedLength;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@ -62,7 +62,7 @@ describe("Password generator options builder", () => {
|
|||||||
(expectedLength) => {
|
(expectedLength) => {
|
||||||
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.max);
|
expect(expectedLength).toBeGreaterThan(DefaultPasswordBoundaries.length.max);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.minLength = expectedLength;
|
policy.minLength = expectedLength;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@ -78,7 +78,7 @@ describe("Password generator options builder", () => {
|
|||||||
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.min);
|
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.min);
|
||||||
expect(expectedMinDigits).toBeLessThanOrEqual(DefaultPasswordBoundaries.minDigits.max);
|
expect(expectedMinDigits).toBeLessThanOrEqual(DefaultPasswordBoundaries.minDigits.max);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.numberCount = expectedMinDigits;
|
policy.numberCount = expectedMinDigits;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@ -93,7 +93,7 @@ describe("Password generator options builder", () => {
|
|||||||
(expectedMinDigits) => {
|
(expectedMinDigits) => {
|
||||||
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.max);
|
expect(expectedMinDigits).toBeGreaterThan(DefaultPasswordBoundaries.minDigits.max);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.numberCount = expectedMinDigits;
|
policy.numberCount = expectedMinDigits;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@ -113,7 +113,7 @@ describe("Password generator options builder", () => {
|
|||||||
DefaultPasswordBoundaries.minSpecialCharacters.max,
|
DefaultPasswordBoundaries.minSpecialCharacters.max,
|
||||||
);
|
);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.specialCount = expectedSpecialCharacters;
|
policy.specialCount = expectedSpecialCharacters;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@ -132,7 +132,7 @@ describe("Password generator options builder", () => {
|
|||||||
DefaultPasswordBoundaries.minSpecialCharacters.max,
|
DefaultPasswordBoundaries.minSpecialCharacters.max,
|
||||||
);
|
);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.specialCount = expectedSpecialCharacters;
|
policy.specialCount = expectedSpecialCharacters;
|
||||||
|
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
@ -151,7 +151,7 @@ describe("Password generator options builder", () => {
|
|||||||
(expectedLength, numberCount, specialCount) => {
|
(expectedLength, numberCount, specialCount) => {
|
||||||
expect(expectedLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
|
expect(expectedLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.numberCount = numberCount;
|
policy.numberCount = numberCount;
|
||||||
policy.specialCount = specialCount;
|
policy.specialCount = specialCount;
|
||||||
|
|
||||||
@ -164,14 +164,14 @@ describe("Password generator options builder", () => {
|
|||||||
|
|
||||||
describe("policyInEffect", () => {
|
describe("policyInEffect", () => {
|
||||||
it("should return false when the policy has no effect", () => {
|
it("should return false when the policy has no effect", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
expect(builder.policyInEffect).toEqual(false);
|
expect(builder.policyInEffect).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has a minlength greater than the default boundary", () => {
|
it("should return true when the policy has a minlength greater than the default boundary", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.minLength = DefaultPasswordBoundaries.length.min + 1;
|
policy.minLength = DefaultPasswordBoundaries.length.min + 1;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@ -179,7 +179,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has a number count greater than the default boundary", () => {
|
it("should return true when the policy has a number count greater than the default boundary", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.numberCount = DefaultPasswordBoundaries.minDigits.min + 1;
|
policy.numberCount = DefaultPasswordBoundaries.minDigits.min + 1;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@ -187,7 +187,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has a special character count greater than the default boundary", () => {
|
it("should return true when the policy has a special character count greater than the default boundary", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.specialCount = DefaultPasswordBoundaries.minSpecialCharacters.min + 1;
|
policy.specialCount = DefaultPasswordBoundaries.minSpecialCharacters.min + 1;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@ -195,7 +195,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has uppercase enabled", () => {
|
it("should return true when the policy has uppercase enabled", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.useUppercase = true;
|
policy.useUppercase = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@ -203,7 +203,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has lowercase enabled", () => {
|
it("should return true when the policy has lowercase enabled", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.useLowercase = true;
|
policy.useLowercase = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@ -211,7 +211,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has numbers enabled", () => {
|
it("should return true when the policy has numbers enabled", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.useNumbers = true;
|
policy.useNumbers = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@ -219,7 +219,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when the policy has special characters enabled", () => {
|
it("should return true when the policy has special characters enabled", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.useSpecial = true;
|
policy.useSpecial = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@ -237,7 +237,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should set `options.uppercase` to '%s' when `policy.useUppercase` is false and `options.uppercase` is '%s'",
|
"should set `options.uppercase` to '%s' when `policy.useUppercase` is false and `options.uppercase` is '%s'",
|
||||||
(expectedUppercase, uppercase) => {
|
(expectedUppercase, uppercase) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.useUppercase = false;
|
policy.useUppercase = false;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, uppercase });
|
const options = Object.freeze({ ...defaultOptions, uppercase });
|
||||||
@ -251,7 +251,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([false, true, undefined])(
|
it.each([false, true, undefined])(
|
||||||
"should set `options.uppercase` (= %s) to true when `policy.useUppercase` is true",
|
"should set `options.uppercase` (= %s) to true when `policy.useUppercase` is true",
|
||||||
(uppercase) => {
|
(uppercase) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.useUppercase = true;
|
policy.useUppercase = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, uppercase });
|
const options = Object.freeze({ ...defaultOptions, uppercase });
|
||||||
@ -269,7 +269,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should set `options.lowercase` to '%s' when `policy.useLowercase` is false and `options.lowercase` is '%s'",
|
"should set `options.lowercase` to '%s' when `policy.useLowercase` is false and `options.lowercase` is '%s'",
|
||||||
(expectedLowercase, lowercase) => {
|
(expectedLowercase, lowercase) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.useLowercase = false;
|
policy.useLowercase = false;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, lowercase });
|
const options = Object.freeze({ ...defaultOptions, lowercase });
|
||||||
@ -283,7 +283,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([false, true, undefined])(
|
it.each([false, true, undefined])(
|
||||||
"should set `options.lowercase` (= %s) to true when `policy.useLowercase` is true",
|
"should set `options.lowercase` (= %s) to true when `policy.useLowercase` is true",
|
||||||
(lowercase) => {
|
(lowercase) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.useLowercase = true;
|
policy.useLowercase = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, lowercase });
|
const options = Object.freeze({ ...defaultOptions, lowercase });
|
||||||
@ -301,7 +301,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should set `options.number` to '%s' when `policy.useNumbers` is false and `options.number` is '%s'",
|
"should set `options.number` to '%s' when `policy.useNumbers` is false and `options.number` is '%s'",
|
||||||
(expectedNumber, number) => {
|
(expectedNumber, number) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.useNumbers = false;
|
policy.useNumbers = false;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, number });
|
const options = Object.freeze({ ...defaultOptions, number });
|
||||||
@ -315,7 +315,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([false, true, undefined])(
|
it.each([false, true, undefined])(
|
||||||
"should set `options.number` (= %s) to true when `policy.useNumbers` is true",
|
"should set `options.number` (= %s) to true when `policy.useNumbers` is true",
|
||||||
(number) => {
|
(number) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.useNumbers = true;
|
policy.useNumbers = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, number });
|
const options = Object.freeze({ ...defaultOptions, number });
|
||||||
@ -333,7 +333,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should set `options.special` to '%s' when `policy.useSpecial` is false and `options.special` is '%s'",
|
"should set `options.special` to '%s' when `policy.useSpecial` is false and `options.special` is '%s'",
|
||||||
(expectedSpecial, special) => {
|
(expectedSpecial, special) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.useSpecial = false;
|
policy.useSpecial = false;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, special });
|
const options = Object.freeze({ ...defaultOptions, special });
|
||||||
@ -347,7 +347,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([false, true, undefined])(
|
it.each([false, true, undefined])(
|
||||||
"should set `options.special` (= %s) to true when `policy.useSpecial` is true",
|
"should set `options.special` (= %s) to true when `policy.useSpecial` is true",
|
||||||
(special) => {
|
(special) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.useSpecial = true;
|
policy.useSpecial = true;
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, special });
|
const options = Object.freeze({ ...defaultOptions, special });
|
||||||
@ -361,7 +361,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([1, 2, 3, 4])(
|
it.each([1, 2, 3, 4])(
|
||||||
"should set `options.length` (= %i) to the minimum it is less than the minimum length",
|
"should set `options.length` (= %i) to the minimum it is less than the minimum length",
|
||||||
(length) => {
|
(length) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(length).toBeLessThan(builder.length.min);
|
expect(length).toBeLessThan(builder.length.min);
|
||||||
|
|
||||||
@ -376,7 +376,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([5, 10, 50, 100, 128])(
|
it.each([5, 10, 50, 100, 128])(
|
||||||
"should not change `options.length` (= %i) when it is within the boundaries",
|
"should not change `options.length` (= %i) when it is within the boundaries",
|
||||||
(length) => {
|
(length) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(length).toBeGreaterThanOrEqual(builder.length.min);
|
expect(length).toBeGreaterThanOrEqual(builder.length.min);
|
||||||
expect(length).toBeLessThanOrEqual(builder.length.max);
|
expect(length).toBeLessThanOrEqual(builder.length.max);
|
||||||
@ -392,7 +392,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([129, 500, 9000])(
|
it.each([129, 500, 9000])(
|
||||||
"should set `options.length` (= %i) to the maximum length when it is exceeded",
|
"should set `options.length` (= %i) to the maximum length when it is exceeded",
|
||||||
(length) => {
|
(length) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(length).toBeGreaterThan(builder.length.max);
|
expect(length).toBeGreaterThan(builder.length.max);
|
||||||
|
|
||||||
@ -414,7 +414,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should set `options.number === %s` when `options.minNumber` (= %i) is set to a value greater than 0",
|
"should set `options.number === %s` when `options.minNumber` (= %i) is set to a value greater than 0",
|
||||||
(expectedNumber, minNumber) => {
|
(expectedNumber, minNumber) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, minNumber });
|
const options = Object.freeze({ ...defaultOptions, minNumber });
|
||||||
|
|
||||||
@ -425,7 +425,7 @@ describe("Password generator options builder", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("should set `options.minNumber` to the minimum value when `options.number` is true", () => {
|
it("should set `options.minNumber` to the minimum value when `options.number` is true", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, number: true });
|
const options = Object.freeze({ ...defaultOptions, number: true });
|
||||||
|
|
||||||
@ -435,7 +435,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set `options.minNumber` to 0 when `options.number` is false", () => {
|
it("should set `options.minNumber` to 0 when `options.number` is false", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, number: false });
|
const options = Object.freeze({ ...defaultOptions, number: false });
|
||||||
|
|
||||||
@ -447,7 +447,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([1, 2, 3, 4])(
|
it.each([1, 2, 3, 4])(
|
||||||
"should set `options.minNumber` (= %i) to the minimum it is less than the minimum number",
|
"should set `options.minNumber` (= %i) to the minimum it is less than the minimum number",
|
||||||
(minNumber) => {
|
(minNumber) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.numberCount = 5; // arbitrary value greater than minNumber
|
policy.numberCount = 5; // arbitrary value greater than minNumber
|
||||||
expect(minNumber).toBeLessThan(policy.numberCount);
|
expect(minNumber).toBeLessThan(policy.numberCount);
|
||||||
|
|
||||||
@ -463,7 +463,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([1, 3, 5, 7, 9])(
|
it.each([1, 3, 5, 7, 9])(
|
||||||
"should not change `options.minNumber` (= %i) when it is within the boundaries",
|
"should not change `options.minNumber` (= %i) when it is within the boundaries",
|
||||||
(minNumber) => {
|
(minNumber) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(minNumber).toBeGreaterThanOrEqual(builder.minDigits.min);
|
expect(minNumber).toBeGreaterThanOrEqual(builder.minDigits.min);
|
||||||
expect(minNumber).toBeLessThanOrEqual(builder.minDigits.max);
|
expect(minNumber).toBeLessThanOrEqual(builder.minDigits.max);
|
||||||
@ -479,7 +479,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([10, 20, 400])(
|
it.each([10, 20, 400])(
|
||||||
"should set `options.minNumber` (= %i) to the maximum digit boundary when it is exceeded",
|
"should set `options.minNumber` (= %i) to the maximum digit boundary when it is exceeded",
|
||||||
(minNumber) => {
|
(minNumber) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(minNumber).toBeGreaterThan(builder.minDigits.max);
|
expect(minNumber).toBeGreaterThan(builder.minDigits.max);
|
||||||
|
|
||||||
@ -501,7 +501,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should set `options.special === %s` when `options.minSpecial` (= %i) is set to a value greater than 0",
|
"should set `options.special === %s` when `options.minSpecial` (= %i) is set to a value greater than 0",
|
||||||
(expectedSpecial, minSpecial) => {
|
(expectedSpecial, minSpecial) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, minSpecial });
|
const options = Object.freeze({ ...defaultOptions, minSpecial });
|
||||||
|
|
||||||
@ -512,7 +512,7 @@ describe("Password generator options builder", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("should set `options.minSpecial` to the minimum value when `options.special` is true", () => {
|
it("should set `options.minSpecial` to the minimum value when `options.special` is true", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, special: true });
|
const options = Object.freeze({ ...defaultOptions, special: true });
|
||||||
|
|
||||||
@ -522,7 +522,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should set `options.minSpecial` to 0 when `options.special` is false", () => {
|
it("should set `options.minSpecial` to 0 when `options.special` is false", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ ...defaultOptions, special: false });
|
const options = Object.freeze({ ...defaultOptions, special: false });
|
||||||
|
|
||||||
@ -534,7 +534,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([1, 2, 3, 4])(
|
it.each([1, 2, 3, 4])(
|
||||||
"should set `options.minSpecial` (= %i) to the minimum it is less than the minimum special characters",
|
"should set `options.minSpecial` (= %i) to the minimum it is less than the minimum special characters",
|
||||||
(minSpecial) => {
|
(minSpecial) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.specialCount = 5; // arbitrary value greater than minSpecial
|
policy.specialCount = 5; // arbitrary value greater than minSpecial
|
||||||
expect(minSpecial).toBeLessThan(policy.specialCount);
|
expect(minSpecial).toBeLessThan(policy.specialCount);
|
||||||
|
|
||||||
@ -550,7 +550,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([1, 3, 5, 7, 9])(
|
it.each([1, 3, 5, 7, 9])(
|
||||||
"should not change `options.minSpecial` (= %i) when it is within the boundaries",
|
"should not change `options.minSpecial` (= %i) when it is within the boundaries",
|
||||||
(minSpecial) => {
|
(minSpecial) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(minSpecial).toBeGreaterThanOrEqual(builder.minSpecialCharacters.min);
|
expect(minSpecial).toBeGreaterThanOrEqual(builder.minSpecialCharacters.min);
|
||||||
expect(minSpecial).toBeLessThanOrEqual(builder.minSpecialCharacters.max);
|
expect(minSpecial).toBeLessThanOrEqual(builder.minSpecialCharacters.max);
|
||||||
@ -566,7 +566,7 @@ describe("Password generator options builder", () => {
|
|||||||
it.each([10, 20, 400])(
|
it.each([10, 20, 400])(
|
||||||
"should set `options.minSpecial` (= %i) to the maximum special character boundary when it is exceeded",
|
"should set `options.minSpecial` (= %i) to the maximum special character boundary when it is exceeded",
|
||||||
(minSpecial) => {
|
(minSpecial) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
expect(minSpecial).toBeGreaterThan(builder.minSpecialCharacters.max);
|
expect(minSpecial).toBeGreaterThan(builder.minSpecialCharacters.max);
|
||||||
|
|
||||||
@ -579,7 +579,7 @@ describe("Password generator options builder", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("should preserve unknown properties", () => {
|
it("should preserve unknown properties", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({
|
const options = Object.freeze({
|
||||||
unknown: "property",
|
unknown: "property",
|
||||||
@ -602,7 +602,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should output `options.minLowercase === %i` when `options.lowercase` is %s",
|
"should output `options.minLowercase === %i` when `options.lowercase` is %s",
|
||||||
(expectedMinLowercase, lowercase) => {
|
(expectedMinLowercase, lowercase) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ lowercase, ...defaultOptions });
|
const options = Object.freeze({ lowercase, ...defaultOptions });
|
||||||
|
|
||||||
@ -618,7 +618,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should output `options.minUppercase === %i` when `options.uppercase` is %s",
|
"should output `options.minUppercase === %i` when `options.uppercase` is %s",
|
||||||
(expectedMinUppercase, uppercase) => {
|
(expectedMinUppercase, uppercase) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ uppercase, ...defaultOptions });
|
const options = Object.freeze({ uppercase, ...defaultOptions });
|
||||||
|
|
||||||
@ -634,7 +634,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should output `options.minNumber === %i` when `options.number` is %s and `options.minNumber` is not set",
|
"should output `options.minNumber === %i` when `options.number` is %s and `options.minNumber` is not set",
|
||||||
(expectedMinNumber, number) => {
|
(expectedMinNumber, number) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ number, ...defaultOptions });
|
const options = Object.freeze({ number, ...defaultOptions });
|
||||||
|
|
||||||
@ -652,7 +652,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should output `options.number === %s` when `options.minNumber` is %i and `options.number` is not set",
|
"should output `options.number === %s` when `options.minNumber` is %i and `options.number` is not set",
|
||||||
(expectedNumber, minNumber) => {
|
(expectedNumber, minNumber) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ minNumber, ...defaultOptions });
|
const options = Object.freeze({ minNumber, ...defaultOptions });
|
||||||
|
|
||||||
@ -668,7 +668,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should output `options.minSpecial === %i` when `options.special` is %s and `options.minSpecial` is not set",
|
"should output `options.minSpecial === %i` when `options.special` is %s and `options.minSpecial` is not set",
|
||||||
(special, expectedMinSpecial) => {
|
(special, expectedMinSpecial) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ special, ...defaultOptions });
|
const options = Object.freeze({ special, ...defaultOptions });
|
||||||
|
|
||||||
@ -686,7 +686,7 @@ describe("Password generator options builder", () => {
|
|||||||
])(
|
])(
|
||||||
"should output `options.special === %s` when `options.minSpecial` is %i and `options.special` is not set",
|
"should output `options.special === %s` when `options.minSpecial` is %i and `options.special` is not set",
|
||||||
(minSpecial, expectedSpecial) => {
|
(minSpecial, expectedSpecial) => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({ minSpecial, ...defaultOptions });
|
const options = Object.freeze({ minSpecial, ...defaultOptions });
|
||||||
|
|
||||||
@ -707,7 +707,7 @@ describe("Password generator options builder", () => {
|
|||||||
const sumOfMinimums = minLowercase + minUppercase + minNumber + minSpecial;
|
const sumOfMinimums = minLowercase + minUppercase + minNumber + minSpecial;
|
||||||
expect(sumOfMinimums).toBeLessThan(DefaultPasswordBoundaries.length.min);
|
expect(sumOfMinimums).toBeLessThan(DefaultPasswordBoundaries.length.min);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({
|
const options = Object.freeze({
|
||||||
minLowercase,
|
minLowercase,
|
||||||
@ -732,7 +732,7 @@ describe("Password generator options builder", () => {
|
|||||||
(expectedMinLength, minLowercase, minUppercase, minNumber, minSpecial) => {
|
(expectedMinLength, minLowercase, minUppercase, minNumber, minSpecial) => {
|
||||||
expect(expectedMinLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
|
expect(expectedMinLength).toBeGreaterThanOrEqual(DefaultPasswordBoundaries.length.min);
|
||||||
|
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({
|
const options = Object.freeze({
|
||||||
minLowercase,
|
minLowercase,
|
||||||
@ -749,7 +749,7 @@ describe("Password generator options builder", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("should preserve unknown properties", () => {
|
it("should preserve unknown properties", () => {
|
||||||
const policy = Object.assign({}, DisabledPasswordGeneratorPolicy);
|
const policy = Object.assign({}, Policies.Password.disabledValue);
|
||||||
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
const builder = new PasswordGeneratorOptionsEvaluator(policy);
|
||||||
const options = Object.freeze({
|
const options = Object.freeze({
|
||||||
unknown: "property",
|
unknown: "property",
|
||||||
|
@ -4,7 +4,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
|||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { PolicyId } from "@bitwarden/common/types/guid";
|
import { PolicyId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { DisabledPasswordGeneratorPolicy } from "../data";
|
import { Policies } from "../data";
|
||||||
|
|
||||||
import { passwordLeastPrivilege } from "./password-least-privilege";
|
import { passwordLeastPrivilege } from "./password-least-privilege";
|
||||||
|
|
||||||
@ -26,17 +26,17 @@ describe("passwordLeastPrivilege", () => {
|
|||||||
it("should return the accumulator when the policy type does not apply", () => {
|
it("should return the accumulator when the policy type does not apply", () => {
|
||||||
const policy = createPolicy({}, PolicyType.RequireSso);
|
const policy = createPolicy({}, PolicyType.RequireSso);
|
||||||
|
|
||||||
const result = passwordLeastPrivilege(DisabledPasswordGeneratorPolicy, policy);
|
const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy);
|
||||||
|
|
||||||
expect(result).toEqual(DisabledPasswordGeneratorPolicy);
|
expect(result).toEqual(Policies.Password.disabledValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return the accumulator when the policy is not enabled", () => {
|
it("should return the accumulator when the policy is not enabled", () => {
|
||||||
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
|
const policy = createPolicy({}, PolicyType.PasswordGenerator, false);
|
||||||
|
|
||||||
const result = passwordLeastPrivilege(DisabledPasswordGeneratorPolicy, policy);
|
const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy);
|
||||||
|
|
||||||
expect(result).toEqual(DisabledPasswordGeneratorPolicy);
|
expect(result).toEqual(Policies.Password.disabledValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
@ -50,8 +50,8 @@ describe("passwordLeastPrivilege", () => {
|
|||||||
])("should take the %p from the policy", (input, value, expected) => {
|
])("should take the %p from the policy", (input, value, expected) => {
|
||||||
const policy = createPolicy({ [input]: value });
|
const policy = createPolicy({ [input]: value });
|
||||||
|
|
||||||
const result = passwordLeastPrivilege(DisabledPasswordGeneratorPolicy, policy);
|
const result = passwordLeastPrivilege(Policies.Password.disabledValue, policy);
|
||||||
|
|
||||||
expect(result).toEqual({ ...DisabledPasswordGeneratorPolicy, [expected]: value });
|
expect(result).toEqual({ ...Policies.Password.disabledValue, [expected]: value });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,19 @@ export function mapPolicyToEvaluator<Policy, Evaluator>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Maps an administrative console policy to a policy evaluator using the provided configuration.
|
||||||
|
* @param configuration the configuration that constructs the evaluator.
|
||||||
|
*/
|
||||||
|
export function mapPolicyToEvaluatorV2<Policy, Evaluator>(
|
||||||
|
configuration: PolicyConfiguration<Policy, Evaluator>,
|
||||||
|
) {
|
||||||
|
return pipe(
|
||||||
|
reduceCollection(configuration.combine, configuration.disabledValue),
|
||||||
|
distinctIfShallowMatch(),
|
||||||
|
map(configuration.createEvaluatorV2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Constructs a method that maps a policy to the default (no-op) policy. */
|
/** Constructs a method that maps a policy to the default (no-op) policy. */
|
||||||
export function newDefaultEvaluator<Target>() {
|
export function newDefaultEvaluator<Target>() {
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -0,0 +1,377 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
|
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||||
|
import { Constraints } from "@bitwarden/common/tools/types";
|
||||||
|
import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { FakeStateProvider, FakeAccountService, awaitAsync } from "../../../../../common/spec";
|
||||||
|
import { PolicyEvaluator } from "../abstractions";
|
||||||
|
import { CredentialGeneratorConfiguration } from "../types";
|
||||||
|
|
||||||
|
import { CredentialGeneratorService } from "./credential-generator.service";
|
||||||
|
|
||||||
|
// arbitrary settings types
|
||||||
|
type SomeSettings = { foo: string };
|
||||||
|
type SomePolicy = { fooPolicy: boolean };
|
||||||
|
|
||||||
|
// settings storage location
|
||||||
|
const SettingsKey = new UserKeyDefinition<SomeSettings>(GENERATOR_DISK, "SomeSettings", {
|
||||||
|
deserializer: (value) => value,
|
||||||
|
clearOn: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// fake policy
|
||||||
|
const policyService = mock<PolicyService>();
|
||||||
|
const somePolicy = new Policy({
|
||||||
|
data: { fooPolicy: true },
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
id: "" as PolicyId,
|
||||||
|
organizationId: "" as OrganizationId,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// fake the configuration
|
||||||
|
const SomeConfiguration: CredentialGeneratorConfiguration<SomeSettings, SomePolicy> = {
|
||||||
|
settings: {
|
||||||
|
initial: { foo: "initial" },
|
||||||
|
constraints: { foo: {} },
|
||||||
|
account: SettingsKey,
|
||||||
|
},
|
||||||
|
policy: {
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
disabledValue: {
|
||||||
|
fooPolicy: false,
|
||||||
|
},
|
||||||
|
combine: (acc, policy) => {
|
||||||
|
return { fooPolicy: acc.fooPolicy || policy.data.fooPolicy };
|
||||||
|
},
|
||||||
|
createEvaluator: () => {
|
||||||
|
throw new Error("this should never be called");
|
||||||
|
},
|
||||||
|
createEvaluatorV2: (policy) => {
|
||||||
|
return {
|
||||||
|
foo: {},
|
||||||
|
policy,
|
||||||
|
policyInEffect: policy.fooPolicy,
|
||||||
|
applyPolicy: (settings) => {
|
||||||
|
return policy.fooPolicy ? { foo: `apply(${settings.foo})` } : settings;
|
||||||
|
},
|
||||||
|
sanitize: (settings) => {
|
||||||
|
return policy.fooPolicy ? { foo: `sanitize(${settings.foo})` } : settings;
|
||||||
|
},
|
||||||
|
} as PolicyEvaluator<SomePolicy, SomeSettings> & Constraints<SomeSettings>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// fake user information
|
||||||
|
const SomeUser = "SomeUser" as UserId;
|
||||||
|
const AnotherUser = "SomeOtherUser" as UserId;
|
||||||
|
const accountService = new FakeAccountService({
|
||||||
|
[SomeUser]: {
|
||||||
|
name: "some user",
|
||||||
|
email: "some.user@example.com",
|
||||||
|
emailVerified: true,
|
||||||
|
},
|
||||||
|
[AnotherUser]: {
|
||||||
|
name: "some other user",
|
||||||
|
email: "some.other.user@example.com",
|
||||||
|
emailVerified: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// fake state
|
||||||
|
const stateProvider = new FakeStateProvider(accountService);
|
||||||
|
|
||||||
|
describe("CredentialGeneratorService", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await accountService.switchAccount(SomeUser);
|
||||||
|
policyService.getAll$.mockImplementation(() => new BehaviorSubject([]).asObservable());
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("settings$", () => {
|
||||||
|
it("defaults to the configuration's initial settings if settings aren't found", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||||
|
|
||||||
|
expect(result).toEqual(SomeConfiguration.settings.initial);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads from the active user's configuration-defined storage", async () => {
|
||||||
|
const settings = { foo: "value" };
|
||||||
|
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||||
|
|
||||||
|
expect(result).toEqual(settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies policy to the loaded settings", async () => {
|
||||||
|
const settings = { foo: "value" };
|
||||||
|
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
||||||
|
const policy$ = new BehaviorSubject([somePolicy]);
|
||||||
|
policyService.getAll$.mockReturnValue(policy$);
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||||
|
|
||||||
|
expect(result).toEqual({ foo: "sanitize(apply(value))" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("follows changes to the active user", async () => {
|
||||||
|
const someSettings = { foo: "value" };
|
||||||
|
const anotherSettings = { foo: "another" };
|
||||||
|
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
||||||
|
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const results: any = [];
|
||||||
|
const sub = generator.settings$(SomeConfiguration).subscribe((r) => results.push(r));
|
||||||
|
|
||||||
|
await accountService.switchAccount(AnotherUser);
|
||||||
|
await awaitAsync();
|
||||||
|
sub.unsubscribe();
|
||||||
|
|
||||||
|
const [someResult, anotherResult] = results;
|
||||||
|
expect(someResult).toEqual(someSettings);
|
||||||
|
expect(anotherResult).toEqual(anotherSettings);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads an arbitrary user's settings", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
||||||
|
const anotherSettings = { foo: "another" };
|
||||||
|
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||||
|
|
||||||
|
const result = await firstValueFrom(generator.settings$(SomeConfiguration, { userId$ }));
|
||||||
|
|
||||||
|
expect(result).toEqual(anotherSettings);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("follows changes to the arbitrary user", async () => {
|
||||||
|
const someSettings = { foo: "value" };
|
||||||
|
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
||||||
|
const anotherSettings = { foo: "another" };
|
||||||
|
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.asObservable();
|
||||||
|
const results: any = [];
|
||||||
|
const sub = generator
|
||||||
|
.settings$(SomeConfiguration, { userId$ })
|
||||||
|
.subscribe((r) => results.push(r));
|
||||||
|
|
||||||
|
userId.next(AnotherUser);
|
||||||
|
await awaitAsync();
|
||||||
|
sub.unsubscribe();
|
||||||
|
|
||||||
|
const [someResult, anotherResult] = results;
|
||||||
|
expect(someResult).toEqual(someSettings);
|
||||||
|
expect(anotherResult).toEqual(anotherSettings);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when the arbitrary user's stream errors", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.asObservable();
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
generator.settings$(SomeConfiguration, { userId$ }).subscribe({
|
||||||
|
error: (e: unknown) => {
|
||||||
|
error = e;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
userId.error({ some: "error" });
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(error).toEqual({ some: "error" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when the arbitrary user's stream completes", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.asObservable();
|
||||||
|
let completed = false;
|
||||||
|
|
||||||
|
generator.settings$(SomeConfiguration, { userId$ }).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
completed = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
userId.complete();
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(completed).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores repeated arbitrary user emissions", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.asObservable();
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
const sub = generator.settings$(SomeConfiguration, { userId$ }).subscribe({
|
||||||
|
next: () => {
|
||||||
|
count++;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await awaitAsync();
|
||||||
|
userId.next(SomeUser);
|
||||||
|
await awaitAsync();
|
||||||
|
userId.next(SomeUser);
|
||||||
|
await awaitAsync();
|
||||||
|
sub.unsubscribe();
|
||||||
|
|
||||||
|
expect(count).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("settings", () => {
|
||||||
|
it("writes to the user's state", async () => {
|
||||||
|
const singleUserId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const subject = await generator.settings(SomeConfiguration, { singleUserId$ });
|
||||||
|
|
||||||
|
subject.next({ foo: "next value" });
|
||||||
|
await awaitAsync();
|
||||||
|
const result = await firstValueFrom(stateProvider.getUserState$(SettingsKey, SomeUser));
|
||||||
|
|
||||||
|
expect(result).toEqual({ foo: "next value" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("waits for the user to become available", async () => {
|
||||||
|
const singleUserId = new BehaviorSubject(null);
|
||||||
|
const singleUserId$ = singleUserId.asObservable();
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
|
||||||
|
let completed = false;
|
||||||
|
const promise = generator.settings(SomeConfiguration, { singleUserId$ }).then((settings) => {
|
||||||
|
completed = true;
|
||||||
|
return settings;
|
||||||
|
});
|
||||||
|
await awaitAsync();
|
||||||
|
expect(completed).toBeFalsy();
|
||||||
|
singleUserId.next(SomeUser);
|
||||||
|
const result = await promise;
|
||||||
|
|
||||||
|
expect(result.userId).toEqual(SomeUser);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("policy$", () => {
|
||||||
|
it("creates a disabled policy evaluator when there is no policy", async () => {
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||||
|
|
||||||
|
const result = await firstValueFrom(generator.policy$(SomeConfiguration, { userId$ }));
|
||||||
|
|
||||||
|
expect(result.policy).toEqual(SomeConfiguration.policy.disabledValue);
|
||||||
|
expect(result.policyInEffect).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates an active policy evaluator when there is a policy", async () => {
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||||
|
const policy$ = new BehaviorSubject([somePolicy]);
|
||||||
|
policyService.getAll$.mockReturnValue(policy$);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(generator.policy$(SomeConfiguration, { userId$ }));
|
||||||
|
|
||||||
|
expect(result.policy).toEqual({ fooPolicy: true });
|
||||||
|
expect(result.policyInEffect).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("follows policy emissions", async () => {
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.asObservable();
|
||||||
|
const somePolicySubject = new BehaviorSubject([somePolicy]);
|
||||||
|
policyService.getAll$.mockReturnValueOnce(somePolicySubject.asObservable());
|
||||||
|
const emissions: any = [];
|
||||||
|
const sub = generator
|
||||||
|
.policy$(SomeConfiguration, { userId$ })
|
||||||
|
.subscribe((policy) => emissions.push(policy));
|
||||||
|
|
||||||
|
// swap the active policy for an inactive policy
|
||||||
|
somePolicySubject.next([]);
|
||||||
|
await awaitAsync();
|
||||||
|
sub.unsubscribe();
|
||||||
|
const [someResult, anotherResult] = emissions;
|
||||||
|
|
||||||
|
expect(someResult.policy).toEqual({ fooPolicy: true });
|
||||||
|
expect(someResult.policyInEffect).toBeTruthy();
|
||||||
|
expect(anotherResult.policy).toEqual(SomeConfiguration.policy.disabledValue);
|
||||||
|
expect(anotherResult.policyInEffect).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("follows user emissions", async () => {
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.asObservable();
|
||||||
|
const somePolicy$ = new BehaviorSubject([somePolicy]).asObservable();
|
||||||
|
const anotherPolicy$ = new BehaviorSubject([]).asObservable();
|
||||||
|
policyService.getAll$.mockReturnValueOnce(somePolicy$).mockReturnValueOnce(anotherPolicy$);
|
||||||
|
const emissions: any = [];
|
||||||
|
const sub = generator
|
||||||
|
.policy$(SomeConfiguration, { userId$ })
|
||||||
|
.subscribe((policy) => emissions.push(policy));
|
||||||
|
|
||||||
|
// swapping the user invokes the return for `anotherPolicy$`
|
||||||
|
userId.next(AnotherUser);
|
||||||
|
await awaitAsync();
|
||||||
|
sub.unsubscribe();
|
||||||
|
const [someResult, anotherResult] = emissions;
|
||||||
|
|
||||||
|
expect(someResult.policy).toEqual({ fooPolicy: true });
|
||||||
|
expect(someResult.policyInEffect).toBeTruthy();
|
||||||
|
expect(anotherResult.policy).toEqual(SomeConfiguration.policy.disabledValue);
|
||||||
|
expect(anotherResult.policyInEffect).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when the user errors", async () => {
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.asObservable();
|
||||||
|
const expectedError = { some: "error" };
|
||||||
|
|
||||||
|
let actualError: any = null;
|
||||||
|
generator.policy$(SomeConfiguration, { userId$ }).subscribe({
|
||||||
|
error: (e: unknown) => {
|
||||||
|
actualError = e;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
userId.error(expectedError);
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(actualError).toEqual(expectedError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when the user completes", async () => {
|
||||||
|
const generator = new CredentialGeneratorService(stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.asObservable();
|
||||||
|
|
||||||
|
let completed = false;
|
||||||
|
generator.policy$(SomeConfiguration, { userId$ }).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
completed = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
userId.complete();
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(completed).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,128 @@
|
|||||||
|
import {
|
||||||
|
combineLatest,
|
||||||
|
distinctUntilChanged,
|
||||||
|
endWith,
|
||||||
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
|
ignoreElements,
|
||||||
|
map,
|
||||||
|
mergeMap,
|
||||||
|
Observable,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
import { SingleUserDependency, UserDependency } from "@bitwarden/common/tools/dependencies";
|
||||||
|
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||||
|
import { Constraints } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
|
import { PolicyEvaluator } from "../abstractions";
|
||||||
|
import { mapPolicyToEvaluatorV2 } from "../rx";
|
||||||
|
import { CredentialGeneratorConfiguration as Configuration } from "../types/credential-generator-configuration";
|
||||||
|
|
||||||
|
type Policy$Dependencies = UserDependency;
|
||||||
|
type Settings$Dependencies = Partial<UserDependency>;
|
||||||
|
// FIXME: once the modernization is complete, switch the type parameters
|
||||||
|
// in `PolicyEvaluator<P, S>` and bake-in the constraints type.
|
||||||
|
type Evaluator<Settings, Policy> = PolicyEvaluator<Policy, Settings> & Constraints<Settings>;
|
||||||
|
|
||||||
|
export class CredentialGeneratorService {
|
||||||
|
constructor(
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
private policyService: PolicyService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Get the settings for the provided configuration
|
||||||
|
* @param configuration determines which generator's settings are loaded
|
||||||
|
* @param dependencies.userId$ identifies the user to which the settings are bound.
|
||||||
|
* If this parameter is not provided, the observable follows the active user and
|
||||||
|
* may not complete.
|
||||||
|
* @returns an observable that emits settings
|
||||||
|
* @remarks the observable enforces policies on the settings
|
||||||
|
*/
|
||||||
|
settings$<Settings, Policy>(
|
||||||
|
configuration: Configuration<Settings, Policy>,
|
||||||
|
dependencies?: Settings$Dependencies,
|
||||||
|
) {
|
||||||
|
const userId$ = dependencies?.userId$ ?? this.stateProvider.activeUserId$;
|
||||||
|
const completion$ = userId$.pipe(ignoreElements(), endWith(true));
|
||||||
|
|
||||||
|
const state$ = userId$.pipe(
|
||||||
|
filter((userId) => !!userId),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap((userId) => {
|
||||||
|
const state$ = this.stateProvider
|
||||||
|
.getUserState$(configuration.settings.account, userId)
|
||||||
|
.pipe(takeUntil(completion$));
|
||||||
|
|
||||||
|
return state$;
|
||||||
|
}),
|
||||||
|
map((settings) => settings ?? structuredClone(configuration.settings.initial)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const settings$ = combineLatest([state$, this.policy$(configuration, { userId$ })]).pipe(
|
||||||
|
map(([settings, policy]) => {
|
||||||
|
// FIXME: create `onLoadApply` that wraps these operations
|
||||||
|
const applied = policy.applyPolicy(settings);
|
||||||
|
const sanitized = policy.sanitize(applied);
|
||||||
|
return sanitized;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return settings$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a subject bound to a specific user's settings
|
||||||
|
* @param configuration determines which generator's settings are loaded
|
||||||
|
* @param dependencies.singleUserId$ identifies the user to which the settings are bound
|
||||||
|
* @returns a promise that resolves with the subject once
|
||||||
|
* `dependencies.singleUserId$` becomes available.
|
||||||
|
* @remarks the subject enforces policy for the settings
|
||||||
|
*/
|
||||||
|
async settings<Settings, Policy>(
|
||||||
|
configuration: Configuration<Settings, Policy>,
|
||||||
|
dependencies: SingleUserDependency,
|
||||||
|
) {
|
||||||
|
const userId = await firstValueFrom(
|
||||||
|
dependencies.singleUserId$.pipe(filter((userId) => !!userId)),
|
||||||
|
);
|
||||||
|
const state = this.stateProvider.getUser(userId, configuration.settings.account);
|
||||||
|
|
||||||
|
// FIXME: apply policy to the settings - this should happen *within* the subject.
|
||||||
|
// Note that policies could be evaluated when the settings are saved or when they
|
||||||
|
// are loaded. The existing subject presently could only apply settings on save
|
||||||
|
// (by wiring the policy in as a dependency and applying with "nextState"), and
|
||||||
|
// even that has a limitation since arbitrary dependencies do not trigger state
|
||||||
|
// emissions.
|
||||||
|
const subject = new UserStateSubject(state, dependencies);
|
||||||
|
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the policy for the provided configuration
|
||||||
|
* @param dependencies.userId$ determines which user's policy is loaded
|
||||||
|
* @returns an observable that emits the policy once `dependencies.userId$`
|
||||||
|
* and the policy become available.
|
||||||
|
*/
|
||||||
|
policy$<Settings, Policy>(
|
||||||
|
configuration: Configuration<Settings, Policy>,
|
||||||
|
dependencies: Policy$Dependencies,
|
||||||
|
): Observable<Evaluator<Settings, Policy>> {
|
||||||
|
const completion$ = dependencies.userId$.pipe(ignoreElements(), endWith(true));
|
||||||
|
|
||||||
|
const policy$ = dependencies.userId$.pipe(
|
||||||
|
mergeMap((userId) => {
|
||||||
|
// complete policy emissions otherwise `mergeMap` holds `policy$` open indefinitely
|
||||||
|
const policies$ = this.policyService
|
||||||
|
.getAll$(configuration.policy.type, userId)
|
||||||
|
.pipe(takeUntil(completion$));
|
||||||
|
return policies$;
|
||||||
|
}),
|
||||||
|
mapPolicyToEvaluatorV2(configuration.policy),
|
||||||
|
);
|
||||||
|
|
||||||
|
return policy$;
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
export { DefaultGeneratorService } from "./default-generator.service";
|
export { DefaultGeneratorService } from "./default-generator.service";
|
||||||
|
export { CredentialGeneratorService } from "./credential-generator.service";
|
||||||
|
@ -8,7 +8,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
|||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { DefaultPassphraseGenerationOptions, DisabledPassphraseGeneratorPolicy } from "../data";
|
import { DefaultPassphraseGenerationOptions, Policies } from "../data";
|
||||||
import { PasswordRandomizer } from "../engine";
|
import { PasswordRandomizer } from "../engine";
|
||||||
import { PassphraseGeneratorOptionsEvaluator } from "../policies";
|
import { PassphraseGeneratorOptionsEvaluator } from "../policies";
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ describe("Password generation strategy", () => {
|
|||||||
const evaluator = await firstValueFrom(evaluator$);
|
const evaluator = await firstValueFrom(evaluator$);
|
||||||
|
|
||||||
expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator);
|
expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator);
|
||||||
expect(evaluator.policy).toMatchObject(DisabledPassphraseGeneratorPolicy);
|
expect(evaluator.policy).toMatchObject(Policies.Passphrase.disabledValue);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
|||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { DefaultPasswordGenerationOptions, DisabledPasswordGeneratorPolicy } from "../data";
|
import { DefaultPasswordGenerationOptions, Policies } from "../data";
|
||||||
import { PasswordRandomizer } from "../engine";
|
import { PasswordRandomizer } from "../engine";
|
||||||
import { PasswordGeneratorOptionsEvaluator } from "../policies";
|
import { PasswordGeneratorOptionsEvaluator } from "../policies";
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ describe("Password generation strategy", () => {
|
|||||||
const evaluator = await firstValueFrom(evaluator$);
|
const evaluator = await firstValueFrom(evaluator$);
|
||||||
|
|
||||||
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
|
expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator);
|
||||||
expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy);
|
expect(evaluator.policy).toMatchObject(Policies.Password.disabledValue);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||||
|
import { Constraints } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
|
import { PolicyConfiguration } from "../types";
|
||||||
|
|
||||||
|
export type CredentialGeneratorConfiguration<Settings, Policy> = {
|
||||||
|
settings: {
|
||||||
|
/** value used when an account's settings haven't been initialized */
|
||||||
|
initial: Readonly<Partial<Settings>>;
|
||||||
|
|
||||||
|
constraints: Constraints<Settings>;
|
||||||
|
|
||||||
|
/** storage location for account-global settings */
|
||||||
|
account: UserKeyDefinition<Settings>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** defines how to construct policy for this settings instance */
|
||||||
|
policy: PolicyConfiguration<Policy, Settings>;
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./boundary";
|
export * from "./boundary";
|
||||||
export * from "./catchall-generator-options";
|
export * from "./catchall-generator-options";
|
||||||
|
export * from "./credential-generator-configuration";
|
||||||
export * from "./eff-username-generator-options";
|
export * from "./eff-username-generator-options";
|
||||||
export * from "./forwarder-options";
|
export * from "./forwarder-options";
|
||||||
export * from "./generator-options";
|
export * from "./generator-options";
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Policy as AdminPolicy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy as AdminPolicy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
|
import { Constraints } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
|
import { PolicyEvaluator } from "../abstractions";
|
||||||
|
|
||||||
/** Determines how to construct a password generator policy */
|
/** Determines how to construct a password generator policy */
|
||||||
export type PolicyConfiguration<Policy, Evaluator> = {
|
export type PolicyConfiguration<Policy, Settings> = {
|
||||||
|
type: PolicyType;
|
||||||
|
|
||||||
/** The value of the policy when it is not in effect. */
|
/** The value of the policy when it is not in effect. */
|
||||||
disabledValue: Policy;
|
disabledValue: Policy;
|
||||||
|
|
||||||
@ -12,5 +18,12 @@ export type PolicyConfiguration<Policy, Evaluator> = {
|
|||||||
|
|
||||||
/** Converts policy service data into an actionable policy.
|
/** Converts policy service data into an actionable policy.
|
||||||
*/
|
*/
|
||||||
createEvaluator: (policy: Policy) => Evaluator;
|
createEvaluator: (policy: Policy) => PolicyEvaluator<Policy, Settings>;
|
||||||
|
|
||||||
|
/** Converts policy service data into an actionable policy.
|
||||||
|
* @remarks this version includes constraints needed for the reactive forms;
|
||||||
|
* it was introduced so that the constraints can be incrementally introduced
|
||||||
|
* as the new UI is built.
|
||||||
|
*/
|
||||||
|
createEvaluatorV2?: (policy: Policy) => PolicyEvaluator<Policy, Settings> & Constraints<Settings>;
|
||||||
};
|
};
|
||||||
|
@ -7,8 +7,7 @@ import {
|
|||||||
GeneratorService,
|
GeneratorService,
|
||||||
DefaultPassphraseGenerationOptions,
|
DefaultPassphraseGenerationOptions,
|
||||||
DefaultPasswordGenerationOptions,
|
DefaultPasswordGenerationOptions,
|
||||||
DisabledPassphraseGeneratorPolicy,
|
Policies,
|
||||||
DisabledPasswordGeneratorPolicy,
|
|
||||||
PassphraseGenerationOptions,
|
PassphraseGenerationOptions,
|
||||||
PassphraseGeneratorPolicy,
|
PassphraseGeneratorPolicy,
|
||||||
PasswordGenerationOptions,
|
PasswordGenerationOptions,
|
||||||
@ -39,7 +38,7 @@ const PasswordGeneratorOptionsEvaluator = policies.PasswordGeneratorOptionsEvalu
|
|||||||
|
|
||||||
function createPassphraseGenerator(
|
function createPassphraseGenerator(
|
||||||
options: PassphraseGenerationOptions = {},
|
options: PassphraseGenerationOptions = {},
|
||||||
policy: PassphraseGeneratorPolicy = DisabledPassphraseGeneratorPolicy,
|
policy: PassphraseGeneratorPolicy = Policies.Passphrase.disabledValue,
|
||||||
) {
|
) {
|
||||||
let savedOptions = options;
|
let savedOptions = options;
|
||||||
const generator = mock<GeneratorService<PassphraseGenerationOptions, PassphraseGeneratorPolicy>>({
|
const generator = mock<GeneratorService<PassphraseGenerationOptions, PassphraseGeneratorPolicy>>({
|
||||||
@ -64,7 +63,7 @@ function createPassphraseGenerator(
|
|||||||
|
|
||||||
function createPasswordGenerator(
|
function createPasswordGenerator(
|
||||||
options: PasswordGenerationOptions = {},
|
options: PasswordGenerationOptions = {},
|
||||||
policy: PasswordGeneratorPolicy = DisabledPasswordGeneratorPolicy,
|
policy: PasswordGeneratorPolicy = Policies.Password.disabledValue,
|
||||||
) {
|
) {
|
||||||
let savedOptions = options;
|
let savedOptions = options;
|
||||||
const generator = mock<GeneratorService<PasswordGenerationOptions, PasswordGeneratorPolicy>>({
|
const generator = mock<GeneratorService<PasswordGenerationOptions, PasswordGeneratorPolicy>>({
|
||||||
|
Loading…
Reference in New Issue
Block a user