mirror of
https://github.com/bitwarden/browser.git
synced 2024-10-08 05:47:50 +02:00
[PM-5611] username generator panel (#11201)
* add username and email engines to generators * introduce username and email settings components * introduce generator algorithm metadata * inline generator policies * wait until settings are available during generation
This commit is contained in:
parent
f1ac1d44e3
commit
433ae13513
@ -1 +1,2 @@
|
|||||||
<tools-password-generator />
|
<!-- Note: this is all throwaway markup, so it won't follow best practices -->
|
||||||
|
<tools-username-generator />
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
import { PasswordGeneratorComponent } from "@bitwarden/generator-components";
|
import { SectionComponent } from "@bitwarden/components";
|
||||||
|
import { UsernameGeneratorComponent } from "@bitwarden/generator-components";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: "credential-generator",
|
selector: "credential-generator",
|
||||||
templateUrl: "credential-generator.component.html",
|
templateUrl: "credential-generator.component.html",
|
||||||
imports: [PasswordGeneratorComponent],
|
imports: [UsernameGeneratorComponent, SectionComponent],
|
||||||
})
|
})
|
||||||
export class CredentialGeneratorComponent {}
|
export class CredentialGeneratorComponent {}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
<form class="box" [formGroup]="settings" class="tw-container">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "domainName" | i18n }}</bit-label>
|
||||||
|
<input bitInput formControlName="catchallDomain" type="text" />
|
||||||
|
</bit-form-field>
|
||||||
|
</form>
|
@ -0,0 +1,86 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
import { BehaviorSubject, skip, Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import {
|
||||||
|
CatchallGenerationOptions,
|
||||||
|
CredentialGeneratorService,
|
||||||
|
Generators,
|
||||||
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
|
import { DependenciesModule } from "./dependencies";
|
||||||
|
import { completeOnAccountSwitch } from "./util";
|
||||||
|
|
||||||
|
/** Options group for catchall emails */
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "tools-catchall-settings",
|
||||||
|
templateUrl: "catchall-settings.component.html",
|
||||||
|
imports: [DependenciesModule],
|
||||||
|
})
|
||||||
|
export class CatchallSettingsComponent 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 component to a specific user's settings.
|
||||||
|
* When this input is not provided, the form binds to the active
|
||||||
|
* user
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
userId: UserId | null;
|
||||||
|
|
||||||
|
/** 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<CatchallGenerationOptions>();
|
||||||
|
|
||||||
|
/** The template's control bindings */
|
||||||
|
protected settings = this.formBuilder.group({
|
||||||
|
catchallDomain: [Generators.catchall.settings.initial.catchallDomain],
|
||||||
|
});
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
const singleUserId$ = this.singleUserId$();
|
||||||
|
const settings = await this.generatorService.settings(Generators.catchall, { singleUserId$ });
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ import {
|
|||||||
ItemModule,
|
ItemModule,
|
||||||
SectionComponent,
|
SectionComponent,
|
||||||
SectionHeaderComponent,
|
SectionHeaderComponent,
|
||||||
|
SelectModule,
|
||||||
ToggleGroupModule,
|
ToggleGroupModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import {
|
import {
|
||||||
@ -46,6 +47,7 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
SectionComponent,
|
SectionComponent,
|
||||||
SectionHeaderComponent,
|
SectionHeaderComponent,
|
||||||
|
SelectModule,
|
||||||
ToggleGroupModule,
|
ToggleGroupModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
export { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
export { CatchallSettingsComponent } from "./catchall-settings.component";
|
||||||
export { CredentialGeneratorHistoryComponent } from "./credential-generator-history.component";
|
export { CredentialGeneratorHistoryComponent } from "./credential-generator-history.component";
|
||||||
export { EmptyCredentialHistoryComponent } from "./empty-credential-history.component";
|
export { EmptyCredentialHistoryComponent } from "./empty-credential-history.component";
|
||||||
|
export { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
||||||
export { PasswordSettingsComponent } from "./password-settings.component";
|
export { PasswordSettingsComponent } from "./password-settings.component";
|
||||||
export { PasswordGeneratorComponent } from "./password-generator.component";
|
export { PasswordGeneratorComponent } from "./password-generator.component";
|
||||||
|
export { SubaddressSettingsComponent } from "./subaddress-settings.component";
|
||||||
|
export { UsernameGeneratorComponent } from "./username-generator.component";
|
||||||
|
export { UsernameSettingsComponent } from "./username-settings.component";
|
||||||
|
@ -39,7 +39,7 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
|||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Binds the passphrase component to a specific user's settings.
|
/** Binds the component to a specific user's settings.
|
||||||
* When this input is not provided, the form binds to the active
|
* When this input is not provided, the form binds to the active
|
||||||
* user
|
* user
|
||||||
*/
|
*/
|
||||||
@ -59,15 +59,15 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
|||||||
readonly onUpdated = new EventEmitter<PassphraseGenerationOptions>();
|
readonly onUpdated = new EventEmitter<PassphraseGenerationOptions>();
|
||||||
|
|
||||||
protected settings = this.formBuilder.group({
|
protected settings = this.formBuilder.group({
|
||||||
[Controls.numWords]: [Generators.Passphrase.settings.initial.numWords],
|
[Controls.numWords]: [Generators.passphrase.settings.initial.numWords],
|
||||||
[Controls.wordSeparator]: [Generators.Passphrase.settings.initial.wordSeparator],
|
[Controls.wordSeparator]: [Generators.passphrase.settings.initial.wordSeparator],
|
||||||
[Controls.capitalize]: [Generators.Passphrase.settings.initial.capitalize],
|
[Controls.capitalize]: [Generators.passphrase.settings.initial.capitalize],
|
||||||
[Controls.includeNumber]: [Generators.Passphrase.settings.initial.includeNumber],
|
[Controls.includeNumber]: [Generators.passphrase.settings.initial.includeNumber],
|
||||||
});
|
});
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const singleUserId$ = this.singleUserId$();
|
const singleUserId$ = this.singleUserId$();
|
||||||
const settings = await this.generatorService.settings(Generators.Passphrase, { singleUserId$ });
|
const settings = await this.generatorService.settings(Generators.passphrase, { singleUserId$ });
|
||||||
|
|
||||||
// skips reactive event emissions to break a subscription cycle
|
// skips reactive event emissions to break a subscription cycle
|
||||||
settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
|
settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
|
||||||
@ -79,16 +79,16 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// dynamic policy enforcement
|
// dynamic policy enforcement
|
||||||
this.generatorService
|
this.generatorService
|
||||||
.policy$(Generators.Passphrase, { userId$: singleUserId$ })
|
.policy$(Generators.passphrase, { userId$: singleUserId$ })
|
||||||
.pipe(takeUntil(this.destroyed$))
|
.pipe(takeUntil(this.destroyed$))
|
||||||
.subscribe(({ constraints }) => {
|
.subscribe(({ constraints }) => {
|
||||||
this.settings
|
this.settings
|
||||||
.get(Controls.numWords)
|
.get(Controls.numWords)
|
||||||
.setValidators(toValidators(Controls.numWords, Generators.Passphrase, constraints));
|
.setValidators(toValidators(Controls.numWords, Generators.passphrase, constraints));
|
||||||
|
|
||||||
this.settings
|
this.settings
|
||||||
.get(Controls.wordSeparator)
|
.get(Controls.wordSeparator)
|
||||||
.setValidators(toValidators(Controls.wordSeparator, Generators.Passphrase, constraints));
|
.setValidators(toValidators(Controls.wordSeparator, Generators.passphrase, constraints));
|
||||||
|
|
||||||
// forward word boundaries to the template (can't do it through the rx form)
|
// forward word boundaries to the template (can't do it through the rx form)
|
||||||
this.minNumWords = constraints.numWords.min;
|
this.minNumWords = constraints.numWords.min;
|
||||||
|
@ -3,8 +3,12 @@ import { BehaviorSubject, distinctUntilChanged, map, Subject, switchMap, takeUnt
|
|||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CredentialGeneratorService, Generators, GeneratorType } from "@bitwarden/generator-core";
|
import {
|
||||||
import { GeneratedCredential } from "@bitwarden/generator-history";
|
CredentialGeneratorService,
|
||||||
|
Generators,
|
||||||
|
PasswordAlgorithm,
|
||||||
|
GeneratedCredential,
|
||||||
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
import { DependenciesModule } from "./dependencies";
|
import { DependenciesModule } from "./dependencies";
|
||||||
import { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
import { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
||||||
@ -24,7 +28,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Binds the passphrase component to a specific user's settings.
|
/** Binds the component to a specific user's settings.
|
||||||
* When this input is not provided, the form binds to the active
|
* When this input is not provided, the form binds to the active
|
||||||
* user
|
* user
|
||||||
*/
|
*/
|
||||||
@ -32,7 +36,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
userId: UserId | null;
|
userId: UserId | null;
|
||||||
|
|
||||||
/** tracks the currently selected credential type */
|
/** tracks the currently selected credential type */
|
||||||
protected credentialType$ = new BehaviorSubject<GeneratorType>("password");
|
protected credentialType$ = new BehaviorSubject<PasswordAlgorithm>("password");
|
||||||
|
|
||||||
/** Emits the last generated value. */
|
/** Emits the last generated value. */
|
||||||
protected readonly value$ = new BehaviorSubject<string>("");
|
protected readonly value$ = new BehaviorSubject<string>("");
|
||||||
@ -46,7 +50,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
/** Tracks changes to the selected credential type
|
/** Tracks changes to the selected credential type
|
||||||
* @param type the new credential type
|
* @param type the new credential type
|
||||||
*/
|
*/
|
||||||
protected onCredentialTypeChanged(type: GeneratorType) {
|
protected onCredentialTypeChanged(type: PasswordAlgorithm) {
|
||||||
if (this.credentialType$.value !== type) {
|
if (this.credentialType$.value !== type) {
|
||||||
this.credentialType$.next(type);
|
this.credentialType$.next(type);
|
||||||
this.generate$.next();
|
this.generate$.next();
|
||||||
@ -85,7 +89,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private typeToGenerator$(type: GeneratorType) {
|
private typeToGenerator$(type: PasswordAlgorithm) {
|
||||||
const dependencies = {
|
const dependencies = {
|
||||||
on$: this.generate$,
|
on$: this.generate$,
|
||||||
userId$: this.userId$,
|
userId$: this.userId$,
|
||||||
@ -93,10 +97,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "password":
|
case "password":
|
||||||
return this.generatorService.generate$(Generators.Password, dependencies);
|
return this.generatorService.generate$(Generators.password, dependencies);
|
||||||
|
|
||||||
case "passphrase":
|
case "passphrase":
|
||||||
return this.generatorService.generate$(Generators.Passphrase, dependencies);
|
return this.generatorService.generate$(Generators.passphrase, dependencies);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid generator type: "${type}"`);
|
throw new Error(`Invalid generator type: "${type}"`);
|
||||||
}
|
}
|
||||||
|
@ -67,14 +67,14 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
readonly onUpdated = new EventEmitter<PasswordGenerationOptions>();
|
readonly onUpdated = new EventEmitter<PasswordGenerationOptions>();
|
||||||
|
|
||||||
protected settings = this.formBuilder.group({
|
protected settings = this.formBuilder.group({
|
||||||
[Controls.length]: [Generators.Password.settings.initial.length],
|
[Controls.length]: [Generators.password.settings.initial.length],
|
||||||
[Controls.uppercase]: [Generators.Password.settings.initial.uppercase],
|
[Controls.uppercase]: [Generators.password.settings.initial.uppercase],
|
||||||
[Controls.lowercase]: [Generators.Password.settings.initial.lowercase],
|
[Controls.lowercase]: [Generators.password.settings.initial.lowercase],
|
||||||
[Controls.number]: [Generators.Password.settings.initial.number],
|
[Controls.number]: [Generators.password.settings.initial.number],
|
||||||
[Controls.special]: [Generators.Password.settings.initial.special],
|
[Controls.special]: [Generators.password.settings.initial.special],
|
||||||
[Controls.minNumber]: [Generators.Password.settings.initial.minNumber],
|
[Controls.minNumber]: [Generators.password.settings.initial.minNumber],
|
||||||
[Controls.minSpecial]: [Generators.Password.settings.initial.minSpecial],
|
[Controls.minSpecial]: [Generators.password.settings.initial.minSpecial],
|
||||||
[Controls.avoidAmbiguous]: [!Generators.Password.settings.initial.ambiguous],
|
[Controls.avoidAmbiguous]: [!Generators.password.settings.initial.ambiguous],
|
||||||
});
|
});
|
||||||
|
|
||||||
private get numbers() {
|
private get numbers() {
|
||||||
@ -95,7 +95,7 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const singleUserId$ = this.singleUserId$();
|
const singleUserId$ = this.singleUserId$();
|
||||||
const settings = await this.generatorService.settings(Generators.Password, { singleUserId$ });
|
const settings = await this.generatorService.settings(Generators.password, { singleUserId$ });
|
||||||
|
|
||||||
// bind settings to the UI
|
// bind settings to the UI
|
||||||
settings
|
settings
|
||||||
@ -116,19 +116,19 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// bind policy to the template
|
// bind policy to the template
|
||||||
this.generatorService
|
this.generatorService
|
||||||
.policy$(Generators.Password, { userId$: singleUserId$ })
|
.policy$(Generators.password, { userId$: singleUserId$ })
|
||||||
.pipe(takeUntil(this.destroyed$))
|
.pipe(takeUntil(this.destroyed$))
|
||||||
.subscribe(({ constraints }) => {
|
.subscribe(({ constraints }) => {
|
||||||
this.settings
|
this.settings
|
||||||
.get(Controls.length)
|
.get(Controls.length)
|
||||||
.setValidators(toValidators(Controls.length, Generators.Password, constraints));
|
.setValidators(toValidators(Controls.length, Generators.password, constraints));
|
||||||
|
|
||||||
this.minNumber.setValidators(
|
this.minNumber.setValidators(
|
||||||
toValidators(Controls.minNumber, Generators.Password, constraints),
|
toValidators(Controls.minNumber, Generators.password, constraints),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.minSpecial.setValidators(
|
this.minSpecial.setValidators(
|
||||||
toValidators(Controls.minSpecial, Generators.Password, constraints),
|
toValidators(Controls.minSpecial, Generators.password, constraints),
|
||||||
);
|
);
|
||||||
|
|
||||||
// forward word boundaries to the template (can't do it through the rx form)
|
// forward word boundaries to the template (can't do it through the rx form)
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
<form class="box" [formGroup]="settings" class="tw-container">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||||
|
<input bitInput formControlName="subaddressEmail" type="text" />
|
||||||
|
</bit-form-field>
|
||||||
|
</form>
|
@ -0,0 +1,86 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
import { BehaviorSubject, skip, Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import {
|
||||||
|
CredentialGeneratorService,
|
||||||
|
Generators,
|
||||||
|
SubaddressGenerationOptions,
|
||||||
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
|
import { DependenciesModule } from "./dependencies";
|
||||||
|
import { completeOnAccountSwitch } from "./util";
|
||||||
|
|
||||||
|
/** Options group for plus-addressed emails */
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "tools-subaddress-settings",
|
||||||
|
templateUrl: "subaddress-settings.component.html",
|
||||||
|
imports: [DependenciesModule],
|
||||||
|
})
|
||||||
|
export class SubaddressSettingsComponent 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 component to a specific user's settings.
|
||||||
|
* When this input is not provided, the form binds to the active
|
||||||
|
* user
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
userId: UserId | null;
|
||||||
|
|
||||||
|
/** 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<SubaddressGenerationOptions>();
|
||||||
|
|
||||||
|
/** The template's control bindings */
|
||||||
|
protected settings = this.formBuilder.group({
|
||||||
|
subaddressEmail: [Generators.subaddress.settings.initial.subaddressEmail],
|
||||||
|
});
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
const singleUserId$ = this.singleUserId$();
|
||||||
|
const settings = await this.generatorService.settings(Generators.subaddress, { singleUserId$ });
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
<bit-card class="tw-flex tw-justify-between tw-mb-4">
|
||||||
|
<div class="tw-grow tw-flex tw-items-center">
|
||||||
|
<bit-color-password class="tw-font-mono" [password]="value$ | async"></bit-color-password>
|
||||||
|
</div>
|
||||||
|
<div class="tw-space-x-1">
|
||||||
|
<button type="button" bitIconButton="bwi-generate" buttonType="main" (click)="generate$.next()">
|
||||||
|
{{ "generatePassword" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-clone"
|
||||||
|
buttonType="main"
|
||||||
|
[appCopyClick]="value$ | async"
|
||||||
|
>
|
||||||
|
{{ "copyPassword" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</bit-card>
|
||||||
|
<bit-section>
|
||||||
|
<bit-section-header>
|
||||||
|
<h6 bitTypography="h6">{{ "options" | i18n }}</h6>
|
||||||
|
</bit-section-header>
|
||||||
|
<div class="tw-mb-4">
|
||||||
|
<bit-card>
|
||||||
|
<form class="box" [formGroup]="credential" class="tw-container">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "type" | i18n }}</bit-label>
|
||||||
|
<bit-select [items]="typeOptions$ | async" formControlName="type"> </bit-select>
|
||||||
|
<bit-hint *ngIf="!!(credentialTypeHint$ | async)">{{
|
||||||
|
credentialTypeHint$ | async
|
||||||
|
}}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
</form>
|
||||||
|
<tools-catchall-settings
|
||||||
|
*ngIf="(algorithm$ | async)?.id === 'catchall'"
|
||||||
|
[userId]="this.userId$ | async"
|
||||||
|
(onUpdated)="generate$.next()"
|
||||||
|
/>
|
||||||
|
<tools-subaddress-settings
|
||||||
|
*ngIf="(algorithm$ | async)?.id === 'subaddress'"
|
||||||
|
[userId]="this.userId$ | async"
|
||||||
|
(onUpdated)="generate$.next()"
|
||||||
|
/>
|
||||||
|
<tools-username-settings
|
||||||
|
*ngIf="(algorithm$ | async)?.id === 'username'"
|
||||||
|
[userId]="this.userId$ | async"
|
||||||
|
(onUpdated)="generate$.next()"
|
||||||
|
/>
|
||||||
|
</bit-card>
|
||||||
|
</div>
|
||||||
|
</bit-section>
|
@ -0,0 +1,238 @@
|
|||||||
|
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
map,
|
||||||
|
ReplaySubject,
|
||||||
|
Subject,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
withLatestFrom,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { Option } from "@bitwarden/components/src/select/option";
|
||||||
|
import {
|
||||||
|
CredentialAlgorithm,
|
||||||
|
CredentialGeneratorInfo,
|
||||||
|
CredentialGeneratorService,
|
||||||
|
GeneratedCredential,
|
||||||
|
Generators,
|
||||||
|
isEmailAlgorithm,
|
||||||
|
isUsernameAlgorithm,
|
||||||
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
|
import { CatchallSettingsComponent } from "./catchall-settings.component";
|
||||||
|
import { DependenciesModule } from "./dependencies";
|
||||||
|
import { SubaddressSettingsComponent } from "./subaddress-settings.component";
|
||||||
|
import { UsernameSettingsComponent } from "./username-settings.component";
|
||||||
|
import { completeOnAccountSwitch } from "./util";
|
||||||
|
|
||||||
|
/** Component that generates usernames and emails */
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "tools-username-generator",
|
||||||
|
templateUrl: "username-generator.component.html",
|
||||||
|
imports: [
|
||||||
|
DependenciesModule,
|
||||||
|
CatchallSettingsComponent,
|
||||||
|
SubaddressSettingsComponent,
|
||||||
|
UsernameSettingsComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||||
|
/** Instantiates the username generator
|
||||||
|
* @param generatorService generates credentials; stores preferences
|
||||||
|
* @param i18nService localizes generator algorithm descriptions
|
||||||
|
* @param accountService discovers the active user when one is not provided
|
||||||
|
* @param zone detects generator settings updates originating from the generator services
|
||||||
|
* @param formBuilder binds reactive form
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private generatorService: CredentialGeneratorService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private accountService: AccountService,
|
||||||
|
private zone: NgZone,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Binds the component to a specific user's settings. When this input is not provided,
|
||||||
|
* the form binds to the active user
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
userId: UserId | null;
|
||||||
|
|
||||||
|
/** Emits credentials created from a generation request. */
|
||||||
|
@Output()
|
||||||
|
readonly onGenerated = new EventEmitter<GeneratedCredential>();
|
||||||
|
|
||||||
|
/** Tracks the selected generation algorithm */
|
||||||
|
protected credential = this.formBuilder.group({
|
||||||
|
type: ["username" as CredentialAlgorithm],
|
||||||
|
});
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
if (this.userId) {
|
||||||
|
this.userId$.next(this.userId);
|
||||||
|
} else {
|
||||||
|
this.singleUserId$().pipe(takeUntil(this.destroyed)).subscribe(this.userId$);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.generatorService
|
||||||
|
.algorithms$(["email", "username"], { userId$: this.userId$ })
|
||||||
|
.pipe(
|
||||||
|
map((algorithms) => this.toOptions(algorithms)),
|
||||||
|
takeUntil(this.destroyed),
|
||||||
|
)
|
||||||
|
.subscribe(this.typeOptions$);
|
||||||
|
|
||||||
|
this.algorithm$
|
||||||
|
.pipe(
|
||||||
|
map((a) => a?.descriptionKey && this.i18nService.t(a?.descriptionKey)),
|
||||||
|
takeUntil(this.destroyed),
|
||||||
|
)
|
||||||
|
.subscribe((hint) => {
|
||||||
|
// update subjects within the angular zone so that the
|
||||||
|
// template bindings refresh immediately
|
||||||
|
this.zone.run(() => {
|
||||||
|
this.credentialTypeHint$.next(hint);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// wire up the generator
|
||||||
|
this.algorithm$
|
||||||
|
.pipe(
|
||||||
|
switchMap((algorithm) => this.typeToGenerator$(algorithm.id)),
|
||||||
|
takeUntil(this.destroyed),
|
||||||
|
)
|
||||||
|
.subscribe((generated) => {
|
||||||
|
// update subjects within the angular zone so that the
|
||||||
|
// template bindings refresh immediately
|
||||||
|
this.zone.run(() => {
|
||||||
|
this.onGenerated.next(generated);
|
||||||
|
this.value$.next(generated.credential);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// assume the last-visible generator algorithm is the user's preferred one
|
||||||
|
const preferences = await this.generatorService.preferences({ singleUserId$: this.userId$ });
|
||||||
|
this.credential.valueChanges
|
||||||
|
.pipe(withLatestFrom(preferences), takeUntil(this.destroyed))
|
||||||
|
.subscribe(([{ type }, preference]) => {
|
||||||
|
if (isEmailAlgorithm(type)) {
|
||||||
|
preference.email.algorithm = type;
|
||||||
|
preference.email.updated = new Date();
|
||||||
|
} else if (isUsernameAlgorithm(type)) {
|
||||||
|
preference.username.algorithm = type;
|
||||||
|
preference.username.updated = new Date();
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preferences.next(preference);
|
||||||
|
});
|
||||||
|
|
||||||
|
// populate the form with the user's preferences to kick off interactivity
|
||||||
|
preferences.pipe(takeUntil(this.destroyed)).subscribe(({ email, username }) => {
|
||||||
|
// this generator supports email & username; the last preference
|
||||||
|
// set by the user "wins"
|
||||||
|
const preference = email.updated > username.updated ? email.algorithm : username.algorithm;
|
||||||
|
|
||||||
|
// break subscription loop
|
||||||
|
this.credential.setValue({ type: preference }, { emitEvent: false });
|
||||||
|
|
||||||
|
const algorithm = this.generatorService.algorithm(preference);
|
||||||
|
// update subjects within the angular zone so that the
|
||||||
|
// template bindings refresh immediately
|
||||||
|
this.zone.run(() => {
|
||||||
|
this.algorithm$.next(algorithm);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// generate on load unless the generator prohibits it
|
||||||
|
this.algorithm$
|
||||||
|
.pipe(
|
||||||
|
distinctUntilChanged((prev, next) => prev.id === next.id),
|
||||||
|
filter((a) => !a.onlyOnRequest),
|
||||||
|
takeUntil(this.destroyed),
|
||||||
|
)
|
||||||
|
.subscribe(() => this.generate$.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
private typeToGenerator$(type: CredentialAlgorithm) {
|
||||||
|
const dependencies = {
|
||||||
|
on$: this.generate$,
|
||||||
|
userId$: this.userId$,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "catchall":
|
||||||
|
return this.generatorService.generate$(Generators.catchall, dependencies);
|
||||||
|
|
||||||
|
case "subaddress":
|
||||||
|
return this.generatorService.generate$(Generators.subaddress, dependencies);
|
||||||
|
|
||||||
|
case "username":
|
||||||
|
return this.generatorService.generate$(Generators.username, dependencies);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid generator type: "${type}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lists the credential types supported by the component. */
|
||||||
|
protected typeOptions$ = new BehaviorSubject<Option<CredentialAlgorithm>[]>([]);
|
||||||
|
|
||||||
|
/** tracks the currently selected credential type */
|
||||||
|
protected algorithm$ = new ReplaySubject<CredentialGeneratorInfo>(1);
|
||||||
|
|
||||||
|
/** Emits hint key for the currently selected credential type */
|
||||||
|
protected credentialTypeHint$ = new ReplaySubject<string>(1);
|
||||||
|
|
||||||
|
/** Emits the last generated value. */
|
||||||
|
protected readonly value$ = new BehaviorSubject<string>("");
|
||||||
|
|
||||||
|
/** Emits when the userId changes */
|
||||||
|
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||||
|
|
||||||
|
/** Emits when a new credential is requested */
|
||||||
|
protected readonly generate$ = new Subject<void>();
|
||||||
|
|
||||||
|
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 toOptions(algorithms: CredentialGeneratorInfo[]) {
|
||||||
|
const options: Option<CredentialAlgorithm>[] = algorithms.map((algorithm) => ({
|
||||||
|
value: algorithm.id,
|
||||||
|
label: this.i18nService.t(algorithm.nameKey),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly destroyed = new Subject<void>();
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroyed.complete();
|
||||||
|
|
||||||
|
// finalize subjects
|
||||||
|
this.generate$.complete();
|
||||||
|
this.value$.complete();
|
||||||
|
|
||||||
|
// finalize component bindings
|
||||||
|
this.onGenerated.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
<form class="box" [formGroup]="settings" class="tw-container">
|
||||||
|
<bit-form-control>
|
||||||
|
<input bitCheckbox formControlName="wordCapitalize" type="checkbox" />
|
||||||
|
<bit-label>{{ "capitalize" | i18n }}</bit-label>
|
||||||
|
</bit-form-control>
|
||||||
|
<bit-form-control>
|
||||||
|
<input bitCheckbox formControlName="wordIncludeNumber" type="checkbox" />
|
||||||
|
<bit-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||||
|
</bit-form-control>
|
||||||
|
</form>
|
@ -0,0 +1,87 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
import { BehaviorSubject, skip, Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import {
|
||||||
|
CredentialGeneratorService,
|
||||||
|
EffUsernameGenerationOptions,
|
||||||
|
Generators,
|
||||||
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
|
import { DependenciesModule } from "./dependencies";
|
||||||
|
import { completeOnAccountSwitch } from "./util";
|
||||||
|
|
||||||
|
/** Options group for usernames */
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "tools-username-settings",
|
||||||
|
templateUrl: "username-settings.component.html",
|
||||||
|
imports: [DependenciesModule],
|
||||||
|
})
|
||||||
|
export class UsernameSettingsComponent 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 component to a specific user's settings.
|
||||||
|
* When this input is not provided, the form binds to the active
|
||||||
|
* user
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
userId: UserId | null;
|
||||||
|
|
||||||
|
/** 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<EffUsernameGenerationOptions>();
|
||||||
|
|
||||||
|
/** The template's control bindings */
|
||||||
|
protected settings = this.formBuilder.group({
|
||||||
|
wordCapitalize: [Generators.username.settings.initial.wordCapitalize],
|
||||||
|
wordIncludeNumber: [Generators.username.settings.initial.wordIncludeNumber],
|
||||||
|
});
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
const singleUserId$ = this.singleUserId$();
|
||||||
|
const settings = await this.generatorService.settings(Generators.username, { singleUserId$ });
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import { CredentialPreference } from "../types";
|
||||||
|
|
||||||
|
import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "./generator-types";
|
||||||
|
|
||||||
|
export const DefaultCredentialPreferences: CredentialPreference = Object.freeze({
|
||||||
|
email: Object.freeze({
|
||||||
|
algorithm: EmailAlgorithms[0],
|
||||||
|
updated: new Date(0),
|
||||||
|
}),
|
||||||
|
password: Object.freeze({
|
||||||
|
algorithm: PasswordAlgorithms[0],
|
||||||
|
updated: new Date(0),
|
||||||
|
}),
|
||||||
|
username: Object.freeze({
|
||||||
|
algorithm: UsernameAlgorithms[0],
|
||||||
|
updated: new Date(0),
|
||||||
|
}),
|
||||||
|
});
|
@ -1,5 +1,15 @@
|
|||||||
/** Types of passwords that may be configured by the password generator */
|
/** Types of passwords that may be generated by the credential generator */
|
||||||
export const PasswordTypes = Object.freeze(["password", "passphrase"] as const);
|
export const PasswordAlgorithms = Object.freeze(["password", "passphrase"] as const);
|
||||||
|
|
||||||
/** Types of generators that may be configured by the password generator */
|
/** Types of usernames that may be generated by the credential generator */
|
||||||
export const GeneratorTypes = Object.freeze([...PasswordTypes, "username"] as const);
|
export const UsernameAlgorithms = Object.freeze(["username"] as const);
|
||||||
|
|
||||||
|
/** Types of email addresses that may be generated by the credential generator */
|
||||||
|
export const EmailAlgorithms = Object.freeze(["catchall", "forwarder", "subaddress"] as const);
|
||||||
|
|
||||||
|
/** All types of credentials that may be generated by the credential generator */
|
||||||
|
export const CredentialAlgorithms = Object.freeze([
|
||||||
|
...PasswordAlgorithms,
|
||||||
|
...UsernameAlgorithms,
|
||||||
|
...EmailAlgorithms,
|
||||||
|
] as const);
|
||||||
|
@ -1,23 +1,51 @@
|
|||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
|
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||||
|
|
||||||
import { Randomizer } from "../abstractions";
|
import { Randomizer } from "../abstractions";
|
||||||
import { PasswordRandomizer } from "../engine";
|
import { EmailRandomizer, PasswordRandomizer, UsernameRandomizer } from "../engine";
|
||||||
import { PASSPHRASE_SETTINGS, PASSWORD_SETTINGS } from "../strategies/storage";
|
|
||||||
import {
|
import {
|
||||||
|
DefaultPolicyEvaluator,
|
||||||
|
DynamicPasswordPolicyConstraints,
|
||||||
|
PassphraseGeneratorOptionsEvaluator,
|
||||||
|
passphraseLeastPrivilege,
|
||||||
|
PassphrasePolicyConstraints,
|
||||||
|
PasswordGeneratorOptionsEvaluator,
|
||||||
|
passwordLeastPrivilege,
|
||||||
|
} from "../policies";
|
||||||
|
import {
|
||||||
|
CATCHALL_SETTINGS,
|
||||||
|
EFF_USERNAME_SETTINGS,
|
||||||
|
PASSPHRASE_SETTINGS,
|
||||||
|
PASSWORD_SETTINGS,
|
||||||
|
SUBADDRESS_SETTINGS,
|
||||||
|
} from "../strategies/storage";
|
||||||
|
import {
|
||||||
|
CatchallGenerationOptions,
|
||||||
CredentialGenerator,
|
CredentialGenerator,
|
||||||
|
CredentialGeneratorConfiguration,
|
||||||
|
EffUsernameGenerationOptions,
|
||||||
|
NoPolicy,
|
||||||
PassphraseGenerationOptions,
|
PassphraseGenerationOptions,
|
||||||
PassphraseGeneratorPolicy,
|
PassphraseGeneratorPolicy,
|
||||||
PasswordGenerationOptions,
|
PasswordGenerationOptions,
|
||||||
PasswordGeneratorPolicy,
|
PasswordGeneratorPolicy,
|
||||||
|
SubaddressGenerationOptions,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { CredentialGeneratorConfiguration } from "../types/credential-generator-configuration";
|
|
||||||
|
|
||||||
|
import { DefaultCatchallOptions } from "./default-catchall-options";
|
||||||
|
import { DefaultEffUsernameOptions } from "./default-eff-username-options";
|
||||||
import { DefaultPassphraseBoundaries } from "./default-passphrase-boundaries";
|
import { DefaultPassphraseBoundaries } from "./default-passphrase-boundaries";
|
||||||
import { DefaultPassphraseGenerationOptions } from "./default-passphrase-generation-options";
|
import { DefaultPassphraseGenerationOptions } from "./default-passphrase-generation-options";
|
||||||
import { DefaultPasswordBoundaries } from "./default-password-boundaries";
|
import { DefaultPasswordBoundaries } from "./default-password-boundaries";
|
||||||
import { DefaultPasswordGenerationOptions } from "./default-password-generation-options";
|
import { DefaultPasswordGenerationOptions } from "./default-password-generation-options";
|
||||||
import { Policies } from "./policies";
|
import { DefaultSubaddressOptions } from "./default-subaddress-generator-options";
|
||||||
|
|
||||||
const PASSPHRASE = Object.freeze({
|
const PASSPHRASE = Object.freeze({
|
||||||
category: "passphrase",
|
id: "passphrase",
|
||||||
|
category: "password",
|
||||||
|
nameKey: "passphrase",
|
||||||
|
onlyOnRequest: false,
|
||||||
engine: {
|
engine: {
|
||||||
create(randomizer: Randomizer): CredentialGenerator<PassphraseGenerationOptions> {
|
create(randomizer: Randomizer): CredentialGenerator<PassphraseGenerationOptions> {
|
||||||
return new PasswordRandomizer(randomizer);
|
return new PasswordRandomizer(randomizer);
|
||||||
@ -34,14 +62,27 @@ const PASSPHRASE = Object.freeze({
|
|||||||
},
|
},
|
||||||
account: PASSPHRASE_SETTINGS,
|
account: PASSPHRASE_SETTINGS,
|
||||||
},
|
},
|
||||||
policy: Policies.Passphrase,
|
policy: {
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
disabledValue: Object.freeze({
|
||||||
|
minNumberWords: 0,
|
||||||
|
capitalize: false,
|
||||||
|
includeNumber: false,
|
||||||
|
}),
|
||||||
|
combine: passphraseLeastPrivilege,
|
||||||
|
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
|
||||||
|
toConstraints: (policy) => new PassphrasePolicyConstraints(policy),
|
||||||
|
},
|
||||||
} satisfies CredentialGeneratorConfiguration<
|
} satisfies CredentialGeneratorConfiguration<
|
||||||
PassphraseGenerationOptions,
|
PassphraseGenerationOptions,
|
||||||
PassphraseGeneratorPolicy
|
PassphraseGeneratorPolicy
|
||||||
>);
|
>);
|
||||||
|
|
||||||
const PASSWORD = Object.freeze({
|
const PASSWORD = Object.freeze({
|
||||||
|
id: "password",
|
||||||
category: "password",
|
category: "password",
|
||||||
|
nameKey: "password",
|
||||||
|
onlyOnRequest: false,
|
||||||
engine: {
|
engine: {
|
||||||
create(randomizer: Randomizer): CredentialGenerator<PasswordGenerationOptions> {
|
create(randomizer: Randomizer): CredentialGenerator<PasswordGenerationOptions> {
|
||||||
return new PasswordRandomizer(randomizer);
|
return new PasswordRandomizer(randomizer);
|
||||||
@ -65,14 +106,129 @@ const PASSWORD = Object.freeze({
|
|||||||
},
|
},
|
||||||
account: PASSWORD_SETTINGS,
|
account: PASSWORD_SETTINGS,
|
||||||
},
|
},
|
||||||
policy: Policies.Password,
|
policy: {
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
disabledValue: Object.freeze({
|
||||||
|
minLength: 0,
|
||||||
|
useUppercase: false,
|
||||||
|
useLowercase: false,
|
||||||
|
useNumbers: false,
|
||||||
|
numberCount: 0,
|
||||||
|
useSpecial: false,
|
||||||
|
specialCount: 0,
|
||||||
|
}),
|
||||||
|
combine: passwordLeastPrivilege,
|
||||||
|
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
|
||||||
|
toConstraints: (policy) => new DynamicPasswordPolicyConstraints(policy),
|
||||||
|
},
|
||||||
} satisfies CredentialGeneratorConfiguration<PasswordGenerationOptions, PasswordGeneratorPolicy>);
|
} satisfies CredentialGeneratorConfiguration<PasswordGenerationOptions, PasswordGeneratorPolicy>);
|
||||||
|
|
||||||
|
const USERNAME = Object.freeze({
|
||||||
|
id: "username",
|
||||||
|
category: "username",
|
||||||
|
nameKey: "randomWord",
|
||||||
|
onlyOnRequest: false,
|
||||||
|
engine: {
|
||||||
|
create(randomizer: Randomizer): CredentialGenerator<EffUsernameGenerationOptions> {
|
||||||
|
return new UsernameRandomizer(randomizer);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
initial: DefaultEffUsernameOptions,
|
||||||
|
constraints: {},
|
||||||
|
account: EFF_USERNAME_SETTINGS,
|
||||||
|
},
|
||||||
|
policy: {
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
disabledValue: {},
|
||||||
|
combine(_acc: NoPolicy, _policy: Policy) {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
createEvaluator(_policy: NoPolicy) {
|
||||||
|
return new DefaultPolicyEvaluator<EffUsernameGenerationOptions>();
|
||||||
|
},
|
||||||
|
toConstraints(_policy: NoPolicy) {
|
||||||
|
return new IdentityConstraint<EffUsernameGenerationOptions>();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies CredentialGeneratorConfiguration<EffUsernameGenerationOptions, NoPolicy>);
|
||||||
|
|
||||||
|
const CATCHALL = Object.freeze({
|
||||||
|
id: "catchall",
|
||||||
|
category: "email",
|
||||||
|
nameKey: "catchallEmail",
|
||||||
|
descriptionKey: "catchallEmailDesc",
|
||||||
|
onlyOnRequest: false,
|
||||||
|
engine: {
|
||||||
|
create(randomizer: Randomizer): CredentialGenerator<CatchallGenerationOptions> {
|
||||||
|
return new EmailRandomizer(randomizer);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
initial: DefaultCatchallOptions,
|
||||||
|
constraints: { catchallDomain: { minLength: 1 } },
|
||||||
|
account: CATCHALL_SETTINGS,
|
||||||
|
},
|
||||||
|
policy: {
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
disabledValue: {},
|
||||||
|
combine(_acc: NoPolicy, _policy: Policy) {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
createEvaluator(_policy: NoPolicy) {
|
||||||
|
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
|
||||||
|
},
|
||||||
|
toConstraints(_policy: NoPolicy) {
|
||||||
|
return new IdentityConstraint<CatchallGenerationOptions>();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies CredentialGeneratorConfiguration<CatchallGenerationOptions, NoPolicy>);
|
||||||
|
|
||||||
|
const SUBADDRESS = Object.freeze({
|
||||||
|
id: "subaddress",
|
||||||
|
category: "email",
|
||||||
|
nameKey: "plusAddressedEmail",
|
||||||
|
descriptionKey: "plusAddressedEmailDesc",
|
||||||
|
onlyOnRequest: false,
|
||||||
|
engine: {
|
||||||
|
create(randomizer: Randomizer): CredentialGenerator<SubaddressGenerationOptions> {
|
||||||
|
return new EmailRandomizer(randomizer);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
initial: DefaultSubaddressOptions,
|
||||||
|
constraints: {},
|
||||||
|
account: SUBADDRESS_SETTINGS,
|
||||||
|
},
|
||||||
|
policy: {
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
disabledValue: {},
|
||||||
|
combine(_acc: NoPolicy, _policy: Policy) {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
createEvaluator(_policy: NoPolicy) {
|
||||||
|
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
|
||||||
|
},
|
||||||
|
toConstraints(_policy: NoPolicy) {
|
||||||
|
return new IdentityConstraint<SubaddressGenerationOptions>();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies CredentialGeneratorConfiguration<SubaddressGenerationOptions, NoPolicy>);
|
||||||
|
|
||||||
/** Generator configurations */
|
/** Generator configurations */
|
||||||
export const Generators = Object.freeze({
|
export const Generators = Object.freeze({
|
||||||
/** Passphrase generator configuration */
|
/** Passphrase generator configuration */
|
||||||
Passphrase: PASSPHRASE,
|
passphrase: PASSPHRASE,
|
||||||
|
|
||||||
/** Password generator configuration */
|
/** Password generator configuration */
|
||||||
Password: PASSWORD,
|
password: PASSWORD,
|
||||||
|
|
||||||
|
/** Username generator configuration */
|
||||||
|
username: USERNAME,
|
||||||
|
|
||||||
|
/** Catchall email generator configuration */
|
||||||
|
catchall: CATCHALL,
|
||||||
|
|
||||||
|
/** Email subaddress generator configuration */
|
||||||
|
subaddress: SUBADDRESS,
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,7 @@ export * from "./default-eff-username-options";
|
|||||||
export * from "./default-firefox-relay-options";
|
export * from "./default-firefox-relay-options";
|
||||||
export * from "./default-passphrase-generation-options";
|
export * from "./default-passphrase-generation-options";
|
||||||
export * from "./default-password-generation-options";
|
export * from "./default-password-generation-options";
|
||||||
|
export * from "./default-credential-preferences";
|
||||||
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 "./forwarders";
|
export * from "./forwarders";
|
||||||
|
@ -1,13 +1,3 @@
|
|||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
|
||||||
|
|
||||||
import {
|
|
||||||
DynamicPasswordPolicyConstraints,
|
|
||||||
passphraseLeastPrivilege,
|
|
||||||
passwordLeastPrivilege,
|
|
||||||
PassphraseGeneratorOptionsEvaluator,
|
|
||||||
PassphrasePolicyConstraints,
|
|
||||||
PasswordGeneratorOptionsEvaluator,
|
|
||||||
} from "../policies";
|
|
||||||
import {
|
import {
|
||||||
PassphraseGenerationOptions,
|
PassphraseGenerationOptions,
|
||||||
PassphraseGeneratorPolicy,
|
PassphraseGeneratorPolicy,
|
||||||
@ -16,39 +6,18 @@ import {
|
|||||||
PolicyConfiguration,
|
PolicyConfiguration,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
const PASSPHRASE = Object.freeze({
|
import { Generators } from "./generators";
|
||||||
type: PolicyType.PasswordGenerator,
|
|
||||||
disabledValue: Object.freeze({
|
|
||||||
minNumberWords: 0,
|
|
||||||
capitalize: false,
|
|
||||||
includeNumber: false,
|
|
||||||
}),
|
|
||||||
combine: passphraseLeastPrivilege,
|
|
||||||
createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy),
|
|
||||||
toConstraints: (policy) => new PassphrasePolicyConstraints(policy),
|
|
||||||
} as PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGenerationOptions>);
|
|
||||||
|
|
||||||
const PASSWORD = Object.freeze({
|
/** Policy configurations
|
||||||
type: PolicyType.PasswordGenerator,
|
* @deprecated use Generator.*.policy instead
|
||||||
disabledValue: Object.freeze({
|
*/
|
||||||
minLength: 0,
|
|
||||||
useUppercase: false,
|
|
||||||
useLowercase: false,
|
|
||||||
useNumbers: false,
|
|
||||||
numberCount: 0,
|
|
||||||
useSpecial: false,
|
|
||||||
specialCount: 0,
|
|
||||||
}),
|
|
||||||
combine: passwordLeastPrivilege,
|
|
||||||
createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy),
|
|
||||||
toConstraints: (policy) => new DynamicPasswordPolicyConstraints(policy),
|
|
||||||
} as PolicyConfiguration<PasswordGeneratorPolicy, PasswordGenerationOptions>);
|
|
||||||
|
|
||||||
/** Policy configurations */
|
|
||||||
export const Policies = Object.freeze({
|
export const Policies = Object.freeze({
|
||||||
|
Passphrase: Generators.passphrase.policy,
|
||||||
|
Password: Generators.password.policy,
|
||||||
|
} satisfies {
|
||||||
/** Passphrase policy configuration */
|
/** Passphrase policy configuration */
|
||||||
Passphrase: PASSPHRASE,
|
Passphrase: PolicyConfiguration<PassphraseGeneratorPolicy, PassphraseGenerationOptions>;
|
||||||
|
|
||||||
/** Passphrase policy configuration */
|
/** Password policy configuration */
|
||||||
Password: PASSWORD,
|
Password: PolicyConfiguration<PasswordGeneratorPolicy, PasswordGenerationOptions>;
|
||||||
});
|
});
|
||||||
|
@ -208,4 +208,40 @@ describe("EmailRandomizer", () => {
|
|||||||
expect(randomizer.pickWord).toHaveBeenCalledWith(expectedWordList, { titleCase: false });
|
expect(randomizer.pickWord).toHaveBeenCalledWith(expectedWordList, { titleCase: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("generate", () => {
|
||||||
|
it("processes catchall generation options", async () => {
|
||||||
|
const email = new EmailRandomizer(randomizer);
|
||||||
|
|
||||||
|
const result = await email.generate(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
catchallDomain: "example.com",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.category).toEqual("catchall");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("processes subaddress generation options", async () => {
|
||||||
|
const email = new EmailRandomizer(randomizer);
|
||||||
|
|
||||||
|
const result = await email.generate(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
subaddressEmail: "foo@example.com",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.category).toEqual("subaddress");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when it cannot recognize the options type", async () => {
|
||||||
|
const email = new EmailRandomizer(randomizer);
|
||||||
|
|
||||||
|
const result = email.generate({}, {});
|
||||||
|
|
||||||
|
await expect(result).rejects.toBeInstanceOf(Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,22 @@
|
|||||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||||
|
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CatchallGenerationOptions,
|
||||||
|
CredentialGenerator,
|
||||||
|
GeneratedCredential,
|
||||||
|
SubaddressGenerationOptions,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
import { Randomizer } from "./abstractions";
|
import { Randomizer } from "./abstractions";
|
||||||
import { SUBADDRESS_PARSER } from "./data";
|
import { SUBADDRESS_PARSER } from "./data";
|
||||||
|
|
||||||
/** Generation algorithms that produce randomized email addresses */
|
/** Generation algorithms that produce randomized email addresses */
|
||||||
export class EmailRandomizer {
|
export class EmailRandomizer
|
||||||
|
implements
|
||||||
|
CredentialGenerator<CatchallGenerationOptions>,
|
||||||
|
CredentialGenerator<SubaddressGenerationOptions>
|
||||||
|
{
|
||||||
/** Instantiates the email randomizer
|
/** Instantiates the email randomizer
|
||||||
* @param random data source for random data
|
* @param random data source for random data
|
||||||
*/
|
*/
|
||||||
@ -96,4 +108,37 @@ export class EmailRandomizer {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generate(
|
||||||
|
request: GenerationRequest,
|
||||||
|
settings: CatchallGenerationOptions,
|
||||||
|
): Promise<GeneratedCredential>;
|
||||||
|
generate(
|
||||||
|
request: GenerationRequest,
|
||||||
|
settings: SubaddressGenerationOptions,
|
||||||
|
): Promise<GeneratedCredential>;
|
||||||
|
async generate(
|
||||||
|
_request: GenerationRequest,
|
||||||
|
settings: CatchallGenerationOptions | SubaddressGenerationOptions,
|
||||||
|
) {
|
||||||
|
if (isCatchallGenerationOptions(settings)) {
|
||||||
|
const email = await this.randomAsciiCatchall(settings.catchallDomain);
|
||||||
|
|
||||||
|
return new GeneratedCredential(email, "catchall", Date.now());
|
||||||
|
} else if (isSubaddressGenerationOptions(settings)) {
|
||||||
|
const email = await this.randomAsciiSubaddress(settings.subaddressEmail);
|
||||||
|
|
||||||
|
return new GeneratedCredential(email, "subaddress", Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Invalid settings received by generator.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCatchallGenerationOptions(settings: any): settings is CatchallGenerationOptions {
|
||||||
|
return "catchallDomain" in (settings ?? {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSubaddressGenerationOptions(settings: any): settings is SubaddressGenerationOptions {
|
||||||
|
return "subaddressEmail" in (settings ?? {});
|
||||||
}
|
}
|
||||||
|
@ -102,4 +102,27 @@ describe("UsernameRandomizer", () => {
|
|||||||
expect(randomizer.pickWord).toHaveBeenNthCalledWith(2, EFFLongWordList, { titleCase: false });
|
expect(randomizer.pickWord).toHaveBeenNthCalledWith(2, EFFLongWordList, { titleCase: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("generate", () => {
|
||||||
|
it("processes username generation options", async () => {
|
||||||
|
const username = new UsernameRandomizer(randomizer);
|
||||||
|
|
||||||
|
const result = await username.generate(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
wordIncludeNumber: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.category).toEqual("username");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when it cannot recognize the options type", async () => {
|
||||||
|
const username = new UsernameRandomizer(randomizer);
|
||||||
|
|
||||||
|
const result = username.generate({}, {});
|
||||||
|
|
||||||
|
await expect(result).rejects.toBeInstanceOf(Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||||
|
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
|
import { CredentialGenerator, EffUsernameGenerationOptions, GeneratedCredential } from "../types";
|
||||||
|
|
||||||
import { Randomizer } from "./abstractions";
|
import { Randomizer } from "./abstractions";
|
||||||
import { WordsRequest } from "./types";
|
import { WordsRequest } from "./types";
|
||||||
|
|
||||||
/** Generation algorithms that produce randomized usernames */
|
/** Generation algorithms that produce randomized usernames */
|
||||||
export class UsernameRandomizer {
|
export class UsernameRandomizer implements CredentialGenerator<EffUsernameGenerationOptions> {
|
||||||
/** Instantiates the username randomizer
|
/** Instantiates the username randomizer
|
||||||
* @param random data source for random data
|
* @param random data source for random data
|
||||||
*/
|
*/
|
||||||
@ -44,4 +47,21 @@ export class UsernameRandomizer {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generate(_request: GenerationRequest, settings: EffUsernameGenerationOptions) {
|
||||||
|
if (isEffUsernameGenerationOptions(settings)) {
|
||||||
|
const username = await this.randomWords({
|
||||||
|
digits: settings.wordIncludeNumber ? 1 : 0,
|
||||||
|
casing: settings.wordCapitalize ? "TitleCase" : "lowercase",
|
||||||
|
});
|
||||||
|
|
||||||
|
return new GeneratedCredential(username, "username", Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Invalid settings received by generator.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEffUsernameGenerationOptions(settings: any): settings is EffUsernameGenerationOptions {
|
||||||
|
return "wordIncludeNumber" in (settings ?? {});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
|
import { PolicyId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { CredentialAlgorithms, PasswordAlgorithms } from "../data";
|
||||||
|
|
||||||
|
import { availableAlgorithms } from "./available-algorithms-policy";
|
||||||
|
|
||||||
|
describe("availableAlgorithmsPolicy", () => {
|
||||||
|
it("returns all algorithms", () => {
|
||||||
|
const result = availableAlgorithms([]);
|
||||||
|
|
||||||
|
for (const expected of CredentialAlgorithms) {
|
||||||
|
expect(result).toContain(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([["password"], ["passphrase"]])("enforces a %p override", (override) => {
|
||||||
|
const policy = new Policy({
|
||||||
|
id: "" as PolicyId,
|
||||||
|
organizationId: "",
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
data: {
|
||||||
|
overridePasswordType: override,
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = availableAlgorithms([policy]);
|
||||||
|
|
||||||
|
expect(result).toContain(override);
|
||||||
|
|
||||||
|
for (const expected of PasswordAlgorithms.filter((a) => a !== override)) {
|
||||||
|
expect(result).not.toContain(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([["password"], ["passphrase"]])("combines %p overrides", (override) => {
|
||||||
|
const policy = new Policy({
|
||||||
|
id: "" as PolicyId,
|
||||||
|
organizationId: "",
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
data: {
|
||||||
|
overridePasswordType: override,
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = availableAlgorithms([policy, policy]);
|
||||||
|
|
||||||
|
expect(result).toContain(override);
|
||||||
|
|
||||||
|
for (const expected of PasswordAlgorithms.filter((a) => a !== override)) {
|
||||||
|
expect(result).not.toContain(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overrides passphrase policies with password policies", () => {
|
||||||
|
const password = new Policy({
|
||||||
|
id: "" as PolicyId,
|
||||||
|
organizationId: "",
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
data: {
|
||||||
|
overridePasswordType: "password",
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
const passphrase = new Policy({
|
||||||
|
id: "" as PolicyId,
|
||||||
|
organizationId: "",
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
data: {
|
||||||
|
overridePasswordType: "passphrase",
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = availableAlgorithms([password, passphrase]);
|
||||||
|
|
||||||
|
expect(result).toContain("password");
|
||||||
|
|
||||||
|
for (const expected of PasswordAlgorithms.filter((a) => a !== "password")) {
|
||||||
|
expect(result).not.toContain(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores unrelated policies", () => {
|
||||||
|
const policy = new Policy({
|
||||||
|
id: "" as PolicyId,
|
||||||
|
organizationId: "",
|
||||||
|
type: PolicyType.ActivateAutofill,
|
||||||
|
data: {
|
||||||
|
some: "policy",
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = availableAlgorithms([policy]);
|
||||||
|
|
||||||
|
for (const expected of CredentialAlgorithms) {
|
||||||
|
expect(result).toContain(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores disabled policies", () => {
|
||||||
|
const policy = new Policy({
|
||||||
|
id: "" as PolicyId,
|
||||||
|
organizationId: "",
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
data: {
|
||||||
|
some: "policy",
|
||||||
|
},
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = availableAlgorithms([policy]);
|
||||||
|
|
||||||
|
for (const expected of CredentialAlgorithms) {
|
||||||
|
expect(result).toContain(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores policies without `overridePasswordType`", () => {
|
||||||
|
const policy = new Policy({
|
||||||
|
id: "" as PolicyId,
|
||||||
|
organizationId: "",
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
data: {
|
||||||
|
some: "policy",
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = availableAlgorithms([policy]);
|
||||||
|
|
||||||
|
for (const expected of CredentialAlgorithms) {
|
||||||
|
expect(result).toContain(expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,32 @@
|
|||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
// FIXME: use index.ts imports once policy abstractions and models
|
||||||
|
// implement ADR-0002
|
||||||
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
|
import {
|
||||||
|
CredentialAlgorithm,
|
||||||
|
EmailAlgorithms,
|
||||||
|
PasswordAlgorithms,
|
||||||
|
UsernameAlgorithms,
|
||||||
|
} from "@bitwarden/generator-core";
|
||||||
|
|
||||||
|
/** Reduces policies to a set of available algorithms
|
||||||
|
* @param policies the policies to reduce
|
||||||
|
* @returns the resulting `AlgorithmAvailabilityPolicy`
|
||||||
|
*/
|
||||||
|
export function availableAlgorithms(policies: Policy[]): CredentialAlgorithm[] {
|
||||||
|
const overridePassword = policies
|
||||||
|
.filter((policy) => policy.type === PolicyType.PasswordGenerator && policy.enabled)
|
||||||
|
.reduce(
|
||||||
|
(type, policy) => (type === "password" ? type : (policy.data.overridePasswordType ?? type)),
|
||||||
|
null as CredentialAlgorithm,
|
||||||
|
);
|
||||||
|
|
||||||
|
const policy: CredentialAlgorithm[] = [...EmailAlgorithms, ...UsernameAlgorithms];
|
||||||
|
if (overridePassword) {
|
||||||
|
policy.push(overridePassword);
|
||||||
|
} else {
|
||||||
|
policy.push(...PasswordAlgorithms);
|
||||||
|
}
|
||||||
|
|
||||||
|
return policy;
|
||||||
|
}
|
@ -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({}, Policies.Passphrase.disabledValue);
|
const policy: any = 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);
|
||||||
@ -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({}, Policies.Passphrase.disabledValue);
|
const policy: any = 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({}, Policies.Passphrase.disabledValue);
|
const policy: any = 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({}, Policies.Passphrase.disabledValue);
|
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
policy.minNumberWords = minNumberWords;
|
policy.minNumberWords = minNumberWords;
|
||||||
|
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
@ -70,7 +70,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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({}, Policies.Passphrase.disabledValue);
|
const policy: any = 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({}, Policies.Passphrase.disabledValue);
|
const policy: any = 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({}, Policies.Passphrase.disabledValue);
|
const policy: any = Object.assign({}, Policies.Passphrase.disabledValue);
|
||||||
policy.includeNumber = true;
|
policy.includeNumber = true;
|
||||||
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
const builder = new PassphraseGeneratorOptionsEvaluator(policy);
|
||||||
|
|
||||||
@ -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({}, Policies.Passphrase.disabledValue);
|
const policy: any = 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 });
|
||||||
@ -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({}, Policies.Passphrase.disabledValue);
|
const policy: any = 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 });
|
||||||
|
@ -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({}, Policies.Password.disabledValue);
|
const policy: any = 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);
|
||||||
@ -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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = Object.assign({}, Policies.Password.disabledValue);
|
||||||
policy.numberCount = numberCount;
|
policy.numberCount = numberCount;
|
||||||
policy.specialCount = specialCount;
|
policy.specialCount = specialCount;
|
||||||
|
|
||||||
@ -171,7 +171,7 @@ describe("Password generator options builder", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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({}, Policies.Password.disabledValue);
|
const policy: any = 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 });
|
||||||
@ -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({}, Policies.Password.disabledValue);
|
const policy: any = 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);
|
||||||
|
|
||||||
@ -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({}, Policies.Password.disabledValue);
|
const policy: any = 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);
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
ObservableTracker,
|
ObservableTracker,
|
||||||
} from "../../../../../common/spec";
|
} from "../../../../../common/spec";
|
||||||
import { Randomizer } from "../abstractions";
|
import { Randomizer } from "../abstractions";
|
||||||
|
import { Generators } from "../data";
|
||||||
import {
|
import {
|
||||||
CredentialGeneratorConfiguration,
|
CredentialGeneratorConfiguration,
|
||||||
GeneratedCredential,
|
GeneratedCredential,
|
||||||
@ -33,7 +34,7 @@ const SettingsKey = new UserKeyDefinition<SomeSettings>(GENERATOR_DISK, "SomeSet
|
|||||||
clearOn: [],
|
clearOn: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// fake policy
|
// fake policies
|
||||||
const policyService = mock<PolicyService>();
|
const policyService = mock<PolicyService>();
|
||||||
const somePolicy = new Policy({
|
const somePolicy = new Policy({
|
||||||
data: { fooPolicy: true },
|
data: { fooPolicy: true },
|
||||||
@ -42,19 +43,43 @@ const somePolicy = new Policy({
|
|||||||
organizationId: "" as OrganizationId,
|
organizationId: "" as OrganizationId,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
const passwordOverridePolicy = new Policy({
|
||||||
|
id: "" as PolicyId,
|
||||||
|
organizationId: "",
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
data: {
|
||||||
|
overridePasswordType: "password",
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const passphraseOverridePolicy = new Policy({
|
||||||
|
id: "" as PolicyId,
|
||||||
|
organizationId: "",
|
||||||
|
type: PolicyType.PasswordGenerator,
|
||||||
|
data: {
|
||||||
|
overridePasswordType: "passphrase",
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
const SomeTime = new Date(1);
|
const SomeTime = new Date(1);
|
||||||
const SomeCategory = "passphrase";
|
const SomeAlgorithm = "passphrase";
|
||||||
|
const SomeCategory = "password";
|
||||||
|
const SomeNameKey = "passphraseKey";
|
||||||
|
|
||||||
// fake the configuration
|
// fake the configuration
|
||||||
const SomeConfiguration: CredentialGeneratorConfiguration<SomeSettings, SomePolicy> = {
|
const SomeConfiguration: CredentialGeneratorConfiguration<SomeSettings, SomePolicy> = {
|
||||||
|
id: SomeAlgorithm,
|
||||||
category: SomeCategory,
|
category: SomeCategory,
|
||||||
|
nameKey: SomeNameKey,
|
||||||
|
onlyOnRequest: false,
|
||||||
engine: {
|
engine: {
|
||||||
create: (randomizer) => {
|
create: (randomizer) => {
|
||||||
return {
|
return {
|
||||||
generate: (request, settings) => {
|
generate: (request, settings) => {
|
||||||
const credential = request.website ? `${request.website}|${settings.foo}` : settings.foo;
|
const credential = request.website ? `${request.website}|${settings.foo}` : settings.foo;
|
||||||
const result = new GeneratedCredential(credential, SomeCategory, SomeTime);
|
const result = new GeneratedCredential(credential, SomeAlgorithm, SomeTime);
|
||||||
return Promise.resolve(result);
|
return Promise.resolve(result);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -114,7 +139,7 @@ const SomeConfiguration: CredentialGeneratorConfiguration<SomeSettings, SomePoli
|
|||||||
// fake user information
|
// fake user information
|
||||||
const SomeUser = "SomeUser" as UserId;
|
const SomeUser = "SomeUser" as UserId;
|
||||||
const AnotherUser = "SomeOtherUser" as UserId;
|
const AnotherUser = "SomeOtherUser" as UserId;
|
||||||
const accountService = new FakeAccountService({
|
const accounts = {
|
||||||
[SomeUser]: {
|
[SomeUser]: {
|
||||||
name: "some user",
|
name: "some user",
|
||||||
email: "some.user@example.com",
|
email: "some.user@example.com",
|
||||||
@ -125,7 +150,8 @@ const accountService = new FakeAccountService({
|
|||||||
email: "some.other.user@example.com",
|
email: "some.other.user@example.com",
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
const accountService = new FakeAccountService(accounts);
|
||||||
|
|
||||||
// fake state
|
// fake state
|
||||||
const stateProvider = new FakeStateProvider(accountService);
|
const stateProvider = new FakeStateProvider(accountService);
|
||||||
@ -149,7 +175,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
|
|
||||||
const result = await generated.expectEmission();
|
const result = await generated.expectEmission();
|
||||||
|
|
||||||
expect(result).toEqual(new GeneratedCredential("value", SomeCategory, SomeTime));
|
expect(result).toEqual(new GeneratedCredential("value", SomeAlgorithm, SomeTime));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("follows the active user", async () => {
|
it("follows the active user", async () => {
|
||||||
@ -165,8 +191,8 @@ describe("CredentialGeneratorService", () => {
|
|||||||
generated.unsubscribe();
|
generated.unsubscribe();
|
||||||
|
|
||||||
expect(generated.emissions).toEqual([
|
expect(generated.emissions).toEqual([
|
||||||
new GeneratedCredential("some value", SomeCategory, SomeTime),
|
new GeneratedCredential("some value", SomeAlgorithm, SomeTime),
|
||||||
new GeneratedCredential("another value", SomeCategory, SomeTime),
|
new GeneratedCredential("another value", SomeAlgorithm, SomeTime),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -182,8 +208,8 @@ describe("CredentialGeneratorService", () => {
|
|||||||
generated.unsubscribe();
|
generated.unsubscribe();
|
||||||
|
|
||||||
expect(generated.emissions).toEqual([
|
expect(generated.emissions).toEqual([
|
||||||
new GeneratedCredential("some value", SomeCategory, SomeTime),
|
new GeneratedCredential("some value", SomeAlgorithm, SomeTime),
|
||||||
new GeneratedCredential("another value", SomeCategory, SomeTime),
|
new GeneratedCredential("another value", SomeAlgorithm, SomeTime),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -200,7 +226,9 @@ describe("CredentialGeneratorService", () => {
|
|||||||
|
|
||||||
const result = await generated.expectEmission();
|
const result = await generated.expectEmission();
|
||||||
|
|
||||||
expect(result).toEqual(new GeneratedCredential("some website|value", SomeCategory, SomeTime));
|
expect(result).toEqual(
|
||||||
|
new GeneratedCredential("some website|value", SomeAlgorithm, SomeTime),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("errors when `website$` errors", async () => {
|
it("errors when `website$` errors", async () => {
|
||||||
@ -246,7 +274,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
|
|
||||||
const result = await generated.expectEmission();
|
const result = await generated.expectEmission();
|
||||||
|
|
||||||
expect(result).toEqual(new GeneratedCredential("another", SomeCategory, SomeTime));
|
expect(result).toEqual(new GeneratedCredential("another", SomeAlgorithm, SomeTime));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("emits a generation for a specific user when `user$` emits", async () => {
|
it("emits a generation for a specific user when `user$` emits", async () => {
|
||||||
@ -261,8 +289,8 @@ describe("CredentialGeneratorService", () => {
|
|||||||
const result = await generated.pauseUntilReceived(2);
|
const result = await generated.pauseUntilReceived(2);
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
new GeneratedCredential("value", SomeCategory, SomeTime),
|
new GeneratedCredential("value", SomeAlgorithm, SomeTime),
|
||||||
new GeneratedCredential("another", SomeCategory, SomeTime),
|
new GeneratedCredential("another", SomeAlgorithm, SomeTime),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -317,7 +345,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
// confirm forwarded emission
|
// confirm forwarded emission
|
||||||
on$.next();
|
on$.next();
|
||||||
await awaitAsync();
|
await awaitAsync();
|
||||||
expect(results).toEqual([new GeneratedCredential("value", SomeCategory, SomeTime)]);
|
expect(results).toEqual([new GeneratedCredential("value", SomeAlgorithm, SomeTime)]);
|
||||||
|
|
||||||
// confirm updating settings does not cause an emission
|
// confirm updating settings does not cause an emission
|
||||||
await stateProvider.setUserState(SettingsKey, { foo: "next" }, SomeUser);
|
await stateProvider.setUserState(SettingsKey, { foo: "next" }, SomeUser);
|
||||||
@ -330,8 +358,8 @@ describe("CredentialGeneratorService", () => {
|
|||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
|
|
||||||
expect(results).toEqual([
|
expect(results).toEqual([
|
||||||
new GeneratedCredential("value", SomeCategory, SomeTime),
|
new GeneratedCredential("value", SomeAlgorithm, SomeTime),
|
||||||
new GeneratedCredential("next", SomeCategory, SomeTime),
|
new GeneratedCredential("next", SomeAlgorithm, SomeTime),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -370,6 +398,245 @@ describe("CredentialGeneratorService", () => {
|
|||||||
|
|
||||||
expect(complete).toBeTruthy();
|
expect(complete).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: test these when the fake state provider can delay its first emission
|
||||||
|
it.todo("emits when settings$ become available if on$ is called before they're ready.");
|
||||||
|
it.todo("emits when website$ become available if on$ is called before they're ready.");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("algorithms", () => {
|
||||||
|
it("outputs password generation metadata", () => {
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
|
const result = generator.algorithms("password");
|
||||||
|
|
||||||
|
expect(result).toContain(Generators.password);
|
||||||
|
expect(result).toContain(Generators.passphrase);
|
||||||
|
|
||||||
|
// this test shouldn't contain entries outside of the current category
|
||||||
|
expect(result).not.toContain(Generators.username);
|
||||||
|
expect(result).not.toContain(Generators.catchall);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("outputs username generation metadata", () => {
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
|
const result = generator.algorithms("username");
|
||||||
|
|
||||||
|
expect(result).toContain(Generators.username);
|
||||||
|
|
||||||
|
// this test shouldn't contain entries outside of the current category
|
||||||
|
expect(result).not.toContain(Generators.catchall);
|
||||||
|
expect(result).not.toContain(Generators.password);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("outputs email generation metadata", () => {
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
|
const result = generator.algorithms("email");
|
||||||
|
|
||||||
|
expect(result).toContain(Generators.catchall);
|
||||||
|
expect(result).toContain(Generators.subaddress);
|
||||||
|
|
||||||
|
// this test shouldn't contain entries outside of the current category
|
||||||
|
expect(result).not.toContain(Generators.username);
|
||||||
|
expect(result).not.toContain(Generators.password);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("combines metadata across categories", () => {
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
|
const result = generator.algorithms(["username", "email"]);
|
||||||
|
|
||||||
|
expect(result).toContain(Generators.username);
|
||||||
|
expect(result).toContain(Generators.catchall);
|
||||||
|
expect(result).toContain(Generators.subaddress);
|
||||||
|
|
||||||
|
// this test shouldn't contain entries outside of the current categories
|
||||||
|
expect(result).not.toContain(Generators.password);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("algorithms$", () => {
|
||||||
|
// these tests cannot use the observable tracker because they return
|
||||||
|
// data that cannot be cloned
|
||||||
|
it("returns password metadata", async () => {
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(generator.algorithms$("password"));
|
||||||
|
|
||||||
|
expect(result).toContain(Generators.password);
|
||||||
|
expect(result).toContain(Generators.passphrase);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns username metadata", async () => {
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(generator.algorithms$("username"));
|
||||||
|
|
||||||
|
expect(result).toContain(Generators.username);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns email metadata", async () => {
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(generator.algorithms$("email"));
|
||||||
|
|
||||||
|
expect(result).toContain(Generators.catchall);
|
||||||
|
expect(result).toContain(Generators.subaddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns username and email metadata", async () => {
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(generator.algorithms$(["username", "email"]));
|
||||||
|
|
||||||
|
expect(result).toContain(Generators.username);
|
||||||
|
expect(result).toContain(Generators.catchall);
|
||||||
|
expect(result).toContain(Generators.subaddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subsequent tests focus on passwords and passphrases as an example of policy
|
||||||
|
// awareness; they exercise the logic without being comprehensive
|
||||||
|
it("enforces the active user's policy", async () => {
|
||||||
|
const policy$ = new BehaviorSubject([passwordOverridePolicy]);
|
||||||
|
policyService.getAll$.mockReturnValue(policy$);
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(generator.algorithms$(["password"]));
|
||||||
|
|
||||||
|
expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser);
|
||||||
|
expect(result).toContain(Generators.password);
|
||||||
|
expect(result).not.toContain(Generators.passphrase);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("follows changes to the active user", async () => {
|
||||||
|
// initialize local account service and state provider because this test is sensitive
|
||||||
|
// to some shared data in `FakeAccountService`.
|
||||||
|
const accountService = new FakeAccountService(accounts);
|
||||||
|
const stateProvider = new FakeStateProvider(accountService);
|
||||||
|
await accountService.switchAccount(SomeUser);
|
||||||
|
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy]));
|
||||||
|
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passphraseOverridePolicy]));
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const results: any = [];
|
||||||
|
const sub = generator.algorithms$("password").subscribe((r) => results.push(r));
|
||||||
|
|
||||||
|
await accountService.switchAccount(AnotherUser);
|
||||||
|
await awaitAsync();
|
||||||
|
sub.unsubscribe();
|
||||||
|
|
||||||
|
const [someResult, anotherResult] = results;
|
||||||
|
|
||||||
|
expect(policyService.getAll$).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
PolicyType.PasswordGenerator,
|
||||||
|
SomeUser,
|
||||||
|
);
|
||||||
|
expect(someResult).toContain(Generators.password);
|
||||||
|
expect(someResult).not.toContain(Generators.passphrase);
|
||||||
|
|
||||||
|
expect(policyService.getAll$).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
PolicyType.PasswordGenerator,
|
||||||
|
AnotherUser,
|
||||||
|
);
|
||||||
|
expect(anotherResult).toContain(Generators.passphrase);
|
||||||
|
expect(anotherResult).not.toContain(Generators.password);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads an arbitrary user's settings", async () => {
|
||||||
|
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy]));
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||||
|
|
||||||
|
const result = await firstValueFrom(generator.algorithms$("password", { userId$ }));
|
||||||
|
|
||||||
|
expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, AnotherUser);
|
||||||
|
expect(result).toContain(Generators.password);
|
||||||
|
expect(result).not.toContain(Generators.passphrase);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("follows changes to the arbitrary user", async () => {
|
||||||
|
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy]));
|
||||||
|
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passphraseOverridePolicy]));
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.asObservable();
|
||||||
|
const results: any = [];
|
||||||
|
const sub = generator.algorithms$("password", { userId$ }).subscribe((r) => results.push(r));
|
||||||
|
|
||||||
|
userId.next(AnotherUser);
|
||||||
|
await awaitAsync();
|
||||||
|
sub.unsubscribe();
|
||||||
|
|
||||||
|
const [someResult, anotherResult] = results;
|
||||||
|
expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser);
|
||||||
|
expect(someResult).toContain(Generators.password);
|
||||||
|
expect(someResult).not.toContain(Generators.passphrase);
|
||||||
|
|
||||||
|
expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, AnotherUser);
|
||||||
|
expect(anotherResult).toContain(Generators.passphrase);
|
||||||
|
expect(anotherResult).not.toContain(Generators.password);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when the arbitrary user's stream errors", async () => {
|
||||||
|
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy]));
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.asObservable();
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
generator.algorithms$("password", { 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 () => {
|
||||||
|
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy]));
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.asObservable();
|
||||||
|
let completed = false;
|
||||||
|
|
||||||
|
generator.algorithms$("password", { userId$ }).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
completed = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
userId.complete();
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(completed).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores repeated arbitrary user emissions", async () => {
|
||||||
|
policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy]));
|
||||||
|
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
|
||||||
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
const userId$ = userId.asObservable();
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
const sub = generator.algorithms$("password", { userId$ }).subscribe({
|
||||||
|
next: () => {
|
||||||
|
count++;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await awaitAsync();
|
||||||
|
userId.next(SomeUser);
|
||||||
|
await awaitAsync();
|
||||||
|
userId.next(SomeUser);
|
||||||
|
await awaitAsync();
|
||||||
|
sub.unsubscribe();
|
||||||
|
|
||||||
|
expect(count).toEqual(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("settings$", () => {
|
describe("settings$", () => {
|
||||||
@ -405,6 +672,11 @@ describe("CredentialGeneratorService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("follows changes to the active user", async () => {
|
it("follows changes to the active user", async () => {
|
||||||
|
// initialize local accound service and state provider because this test is sensitive
|
||||||
|
// to some shared data in `FakeAccountService`.
|
||||||
|
const accountService = new FakeAccountService(accounts);
|
||||||
|
const stateProvider = new FakeStateProvider(accountService);
|
||||||
|
await accountService.switchAccount(SomeUser);
|
||||||
const someSettings = { foo: "value" };
|
const someSettings = { foo: "value" };
|
||||||
const anotherSettings = { foo: "another" };
|
const anotherSettings = { foo: "another" };
|
||||||
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
|
concat,
|
||||||
concatMap,
|
concatMap,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
endWith,
|
endWith,
|
||||||
filter,
|
filter,
|
||||||
|
first,
|
||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
ignoreElements,
|
ignoreElements,
|
||||||
map,
|
map,
|
||||||
mergeMap,
|
|
||||||
Observable,
|
Observable,
|
||||||
race,
|
race,
|
||||||
|
share,
|
||||||
|
skipUntil,
|
||||||
switchMap,
|
switchMap,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
@ -18,6 +21,7 @@ import {
|
|||||||
import { Simplify } from "type-fest";
|
import { Simplify } from "type-fest";
|
||||||
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
import {
|
import {
|
||||||
OnDependency,
|
OnDependency,
|
||||||
@ -28,10 +32,21 @@ import { isDynamic } from "@bitwarden/common/tools/state/state-constraints-depen
|
|||||||
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||||
|
|
||||||
import { Randomizer } from "../abstractions";
|
import { Randomizer } from "../abstractions";
|
||||||
|
import { Generators } from "../data";
|
||||||
|
import { availableAlgorithms } from "../policies/available-algorithms-policy";
|
||||||
import { mapPolicyToConstraints } from "../rx";
|
import { mapPolicyToConstraints } from "../rx";
|
||||||
|
import {
|
||||||
|
CredentialAlgorithm,
|
||||||
|
CredentialCategories,
|
||||||
|
CredentialCategory,
|
||||||
|
CredentialGeneratorInfo,
|
||||||
|
CredentialPreference,
|
||||||
|
} from "../types";
|
||||||
import { CredentialGeneratorConfiguration as Configuration } from "../types/credential-generator-configuration";
|
import { CredentialGeneratorConfiguration as Configuration } from "../types/credential-generator-configuration";
|
||||||
import { GeneratorConstraints } from "../types/generator-constraints";
|
import { GeneratorConstraints } from "../types/generator-constraints";
|
||||||
|
|
||||||
|
import { PREFERENCES } from "./credential-preferences";
|
||||||
|
|
||||||
type Policy$Dependencies = UserDependency;
|
type Policy$Dependencies = UserDependency;
|
||||||
type Settings$Dependencies = Partial<UserDependency>;
|
type Settings$Dependencies = Partial<UserDependency>;
|
||||||
type Generate$Dependencies = Simplify<Partial<OnDependency> & Partial<UserDependency>> & {
|
type Generate$Dependencies = Simplify<Partial<OnDependency> & Partial<UserDependency>> & {
|
||||||
@ -46,6 +61,8 @@ type Generate$Dependencies = Simplify<Partial<OnDependency> & Partial<UserDepend
|
|||||||
website$?: Observable<string>;
|
website$?: Observable<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Algorithms$Dependencies = Partial<UserDependency>;
|
||||||
|
|
||||||
export class CredentialGeneratorService {
|
export class CredentialGeneratorService {
|
||||||
constructor(
|
constructor(
|
||||||
private randomizer: Randomizer,
|
private randomizer: Randomizer,
|
||||||
@ -53,6 +70,9 @@ export class CredentialGeneratorService {
|
|||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
// FIXME: the rxjs methods of this service can be a lot more resilient if
|
||||||
|
// `Subjects` are introduced where sharing occurs
|
||||||
|
|
||||||
/** Generates a stream of credentials
|
/** Generates a stream of credentials
|
||||||
* @param configuration determines which generator's settings are loaded
|
* @param configuration determines which generator's settings are loaded
|
||||||
* @param dependencies.on$ when specified, a new credential is emitted when
|
* @param dependencies.on$ when specified, a new credential is emitted when
|
||||||
@ -76,8 +96,24 @@ export class CredentialGeneratorService {
|
|||||||
const settingsComplete$ = request$.pipe(ignoreElements(), endWith(true));
|
const settingsComplete$ = request$.pipe(ignoreElements(), endWith(true));
|
||||||
const complete$ = race(requestComplete$, settingsComplete$);
|
const complete$ = race(requestComplete$, settingsComplete$);
|
||||||
|
|
||||||
|
// if on$ triggers before settings are loaded, trigger as soon
|
||||||
|
// as they become available.
|
||||||
|
let readyOn$: Observable<any> = null;
|
||||||
|
if (dependencies?.on$) {
|
||||||
|
const NO_EMISSIONS = {};
|
||||||
|
const ready$ = combineLatest([settings$, request$]).pipe(
|
||||||
|
first(null, NO_EMISSIONS),
|
||||||
|
filter((value) => value !== NO_EMISSIONS),
|
||||||
|
share(),
|
||||||
|
);
|
||||||
|
readyOn$ = concat(
|
||||||
|
dependencies.on$?.pipe(switchMap(() => ready$)),
|
||||||
|
dependencies.on$.pipe(skipUntil(ready$)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// generation proper
|
// generation proper
|
||||||
const generate$ = (dependencies?.on$ ?? settings$).pipe(
|
const generate$ = (readyOn$ ?? settings$).pipe(
|
||||||
withLatestFrom(request$, settings$),
|
withLatestFrom(request$, settings$),
|
||||||
concatMap(([, request, settings]) => engine.generate(request, settings)),
|
concatMap(([, request, settings]) => engine.generate(request, settings)),
|
||||||
takeUntil(complete$),
|
takeUntil(complete$),
|
||||||
@ -86,6 +122,79 @@ export class CredentialGeneratorService {
|
|||||||
return generate$;
|
return generate$;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Emits metadata concerning the provided generation algorithms
|
||||||
|
* @param category the category or categories of interest
|
||||||
|
* @param dependences.userId$ when provided, the algorithms are filter to only
|
||||||
|
* those matching the provided user's policy. Otherwise, emits the algorithms
|
||||||
|
* available to the active user.
|
||||||
|
* @returns An observable that emits algorithm metadata.
|
||||||
|
*/
|
||||||
|
algorithms$(
|
||||||
|
category: CredentialCategory,
|
||||||
|
dependencies?: Algorithms$Dependencies,
|
||||||
|
): Observable<CredentialGeneratorInfo[]>;
|
||||||
|
algorithms$(
|
||||||
|
category: CredentialCategory[],
|
||||||
|
dependencies?: Algorithms$Dependencies,
|
||||||
|
): Observable<CredentialGeneratorInfo[]>;
|
||||||
|
algorithms$(
|
||||||
|
category: CredentialCategory | CredentialCategory[],
|
||||||
|
dependencies?: Algorithms$Dependencies,
|
||||||
|
) {
|
||||||
|
// any cast required here because TypeScript fails to bind `category`
|
||||||
|
// to the union-typed overload of `algorithms`.
|
||||||
|
const algorithms = this.algorithms(category as any);
|
||||||
|
|
||||||
|
// fall back to default bindings
|
||||||
|
const userId$ = dependencies?.userId$ ?? this.stateProvider.activeUserId$;
|
||||||
|
|
||||||
|
// monitor completion
|
||||||
|
const completion$ = userId$.pipe(ignoreElements(), endWith(true));
|
||||||
|
|
||||||
|
// apply policy
|
||||||
|
const algorithms$ = userId$.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap((userId) => {
|
||||||
|
// complete policy emissions otherwise `switchMap` holds `algorithms$` open indefinitely
|
||||||
|
const policies$ = this.policyService.getAll$(PolicyType.PasswordGenerator, userId).pipe(
|
||||||
|
map((p) => new Set(availableAlgorithms(p))),
|
||||||
|
takeUntil(completion$),
|
||||||
|
);
|
||||||
|
return policies$;
|
||||||
|
}),
|
||||||
|
map((available) => {
|
||||||
|
const filtered = algorithms.filter((c) => available.has(c.id));
|
||||||
|
return filtered;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return algorithms$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lists metadata for the algorithms in a credential category
|
||||||
|
* @param category the category or categories of interest
|
||||||
|
* @returns A list containing the requested metadata.
|
||||||
|
*/
|
||||||
|
algorithms(category: CredentialCategory): CredentialGeneratorInfo[];
|
||||||
|
algorithms(category: CredentialCategory[]): CredentialGeneratorInfo[];
|
||||||
|
algorithms(category: CredentialCategory | CredentialCategory[]): CredentialGeneratorInfo[] {
|
||||||
|
const categories = Array.isArray(category) ? category : [category];
|
||||||
|
const algorithms = categories
|
||||||
|
.flatMap((c) => CredentialCategories[c])
|
||||||
|
.map((c) => (c === "forwarder" ? null : Generators[c]))
|
||||||
|
.filter((info) => info !== null);
|
||||||
|
|
||||||
|
return algorithms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Look up the metadata for a specific generator algorithm
|
||||||
|
* @param id identifies the algorithm
|
||||||
|
* @returns the requested metadata, or `null` if the metadata wasn't found.
|
||||||
|
*/
|
||||||
|
algorithm(id: CredentialAlgorithm): CredentialGeneratorInfo {
|
||||||
|
return (id === "forwarder" ? null : Generators[id]) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Get the settings for the provided configuration
|
/** Get the settings for the provided configuration
|
||||||
* @param configuration determines which generator's settings are loaded
|
* @param configuration determines which generator's settings are loaded
|
||||||
* @param dependencies.userId$ identifies the user to which the settings are bound.
|
* @param dependencies.userId$ identifies the user to which the settings are bound.
|
||||||
@ -125,6 +234,29 @@ export class CredentialGeneratorService {
|
|||||||
return settings$;
|
return settings$;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get a subject bound to credential generator preferences.
|
||||||
|
* @param dependencies.singleUserId$ identifies the user to which the preferences are bound
|
||||||
|
* @returns a promise that resolves with the subject once `dependencies.singleUserId$`
|
||||||
|
* becomes available.
|
||||||
|
* @remarks Preferences determine which algorithms are used when generating a
|
||||||
|
* credential from a credential category (e.g. `PassX` or `Username`). Preferences
|
||||||
|
* should not be used to hold navigation history. Use @bitwarden/generator-navigation
|
||||||
|
* instead.
|
||||||
|
*/
|
||||||
|
async preferences(
|
||||||
|
dependencies: SingleUserDependency,
|
||||||
|
): Promise<UserStateSubject<CredentialPreference>> {
|
||||||
|
const userId = await firstValueFrom(
|
||||||
|
dependencies.singleUserId$.pipe(filter((userId) => !!userId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// FIXME: enforce policy
|
||||||
|
const state = this.stateProvider.getUser(userId, PREFERENCES);
|
||||||
|
const subject = new UserStateSubject(state, { ...dependencies });
|
||||||
|
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
|
||||||
/** Get a subject bound to a specific user's settings
|
/** Get a subject bound to a specific user's settings
|
||||||
* @param configuration determines which generator's settings are loaded
|
* @param configuration determines which generator's settings are loaded
|
||||||
* @param dependencies.singleUserId$ identifies the user to which the settings are bound
|
* @param dependencies.singleUserId$ identifies the user to which the settings are bound
|
||||||
@ -159,7 +291,7 @@ export class CredentialGeneratorService {
|
|||||||
const completion$ = dependencies.userId$.pipe(ignoreElements(), endWith(true));
|
const completion$ = dependencies.userId$.pipe(ignoreElements(), endWith(true));
|
||||||
|
|
||||||
const constraints$ = dependencies.userId$.pipe(
|
const constraints$ = dependencies.userId$.pipe(
|
||||||
mergeMap((userId) => {
|
switchMap((userId) => {
|
||||||
// complete policy emissions otherwise `mergeMap` holds `policies$` open indefinitely
|
// complete policy emissions otherwise `mergeMap` holds `policies$` open indefinitely
|
||||||
const policies$ = this.policyService
|
const policies$ = this.policyService
|
||||||
.getAll$(configuration.policy.type, userId)
|
.getAll$(configuration.policy.type, userId)
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import { DefaultCredentialPreferences } from "../data";
|
||||||
|
|
||||||
|
import { PREFERENCES } from "./credential-preferences";
|
||||||
|
|
||||||
|
describe("PREFERENCES", () => {
|
||||||
|
describe("deserializer", () => {
|
||||||
|
it.each([[null], [undefined]])("creates new preferences (= %p)", (value) => {
|
||||||
|
const result = PREFERENCES.deserializer(value);
|
||||||
|
|
||||||
|
expect(result).toEqual(DefaultCredentialPreferences);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fills missing password preferences", () => {
|
||||||
|
const input = { ...DefaultCredentialPreferences };
|
||||||
|
delete input.password;
|
||||||
|
|
||||||
|
const result = PREFERENCES.deserializer(input as any);
|
||||||
|
|
||||||
|
expect(result).toEqual(DefaultCredentialPreferences);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fills missing email preferences", () => {
|
||||||
|
const input = { ...DefaultCredentialPreferences };
|
||||||
|
delete input.email;
|
||||||
|
|
||||||
|
const result = PREFERENCES.deserializer(input as any);
|
||||||
|
|
||||||
|
expect(result).toEqual(DefaultCredentialPreferences);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fills missing username preferences", () => {
|
||||||
|
const input = { ...DefaultCredentialPreferences };
|
||||||
|
delete input.username;
|
||||||
|
|
||||||
|
const result = PREFERENCES.deserializer(input as any);
|
||||||
|
|
||||||
|
expect(result).toEqual(DefaultCredentialPreferences);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts updated fields to Dates", () => {
|
||||||
|
const input = structuredClone(DefaultCredentialPreferences);
|
||||||
|
input.email.updated = "1970-01-01T00:00:00.100Z" as any;
|
||||||
|
input.password.updated = "1970-01-01T00:00:00.200Z" as any;
|
||||||
|
input.username.updated = "1970-01-01T00:00:00.300Z" as any;
|
||||||
|
|
||||||
|
const result = PREFERENCES.deserializer(input as any);
|
||||||
|
|
||||||
|
expect(result.email.updated).toEqual(new Date(100));
|
||||||
|
expect(result.password.updated).toEqual(new Date(200));
|
||||||
|
expect(result.username.updated).toEqual(new Date(300));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,30 @@
|
|||||||
|
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
|
import { DefaultCredentialPreferences } from "../data";
|
||||||
|
import { CredentialPreference } from "../types";
|
||||||
|
|
||||||
|
/** plaintext password generation options */
|
||||||
|
export const PREFERENCES = new UserKeyDefinition<CredentialPreference>(
|
||||||
|
GENERATOR_DISK,
|
||||||
|
"credentialPreferences",
|
||||||
|
{
|
||||||
|
deserializer: (value) => {
|
||||||
|
const result = (value as any) ?? {};
|
||||||
|
|
||||||
|
for (const key in DefaultCredentialPreferences) {
|
||||||
|
// bind `key` to `category` to transmute the type
|
||||||
|
const category: keyof typeof DefaultCredentialPreferences = key as any;
|
||||||
|
|
||||||
|
const preference = result[category] ?? { ...DefaultCredentialPreferences[category] };
|
||||||
|
if (typeof preference.updated === "string") {
|
||||||
|
preference.updated = new Date(preference.updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
result[category] = preference;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
@ -1,5 +0,0 @@
|
|||||||
/** Kinds of credentials that can be stored by the history service
|
|
||||||
* password - a secret consisting of arbitrary characters used to authenticate a user
|
|
||||||
* passphrase - a secret consisting of words used to authenticate a user
|
|
||||||
*/
|
|
||||||
export type CredentialCategory = "password" | "passphrase";
|
|
@ -2,16 +2,35 @@ import { UserKeyDefinition } from "@bitwarden/common/platform/state";
|
|||||||
import { Constraints } from "@bitwarden/common/tools/types";
|
import { Constraints } from "@bitwarden/common/tools/types";
|
||||||
|
|
||||||
import { Randomizer } from "../abstractions";
|
import { Randomizer } from "../abstractions";
|
||||||
import { PolicyConfiguration } from "../types";
|
import { CredentialAlgorithm, CredentialCategory, PolicyConfiguration } from "../types";
|
||||||
|
|
||||||
import { CredentialCategory } from "./credential-category";
|
|
||||||
import { CredentialGenerator } from "./credential-generator";
|
import { CredentialGenerator } from "./credential-generator";
|
||||||
|
|
||||||
export type CredentialGeneratorConfiguration<Settings, Policy> = {
|
/** Credential generator metadata common across credential generators */
|
||||||
/** Category describing usage of the credential generated by this configuration
|
export type CredentialGeneratorInfo = {
|
||||||
|
/** Uniquely identifies the credential configuration
|
||||||
*/
|
*/
|
||||||
|
id: CredentialAlgorithm;
|
||||||
|
|
||||||
|
/** The kind of credential generated by this configuration */
|
||||||
category: CredentialCategory;
|
category: CredentialCategory;
|
||||||
|
|
||||||
|
/** Key used to localize the credential name in the I18nService */
|
||||||
|
nameKey: string;
|
||||||
|
|
||||||
|
/** Key used to localize the credential description in the I18nService */
|
||||||
|
descriptionKey?: string;
|
||||||
|
|
||||||
|
/** When true, credential generation must be explicitly requested.
|
||||||
|
* @remarks this property is useful when credential generation
|
||||||
|
* carries side effects, such as configuring a service external
|
||||||
|
* to Bitwarden.
|
||||||
|
*/
|
||||||
|
onlyOnRequest: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Credential generator metadata that relies upon typed setting and policy definitions. */
|
||||||
|
export type CredentialGeneratorConfiguration<Settings, Policy> = CredentialGeneratorInfo & {
|
||||||
/** An algorithm that generates credentials when ran. */
|
/** An algorithm that generates credentials when ran. */
|
||||||
engine: {
|
engine: {
|
||||||
/** Factory for the generator
|
/** Factory for the generator
|
||||||
@ -28,6 +47,7 @@ export type CredentialGeneratorConfiguration<Settings, Policy> = {
|
|||||||
/** value used when an account's settings haven't been initialized */
|
/** value used when an account's settings haven't been initialized */
|
||||||
initial: Readonly<Partial<Settings>>;
|
initial: Readonly<Partial<Settings>>;
|
||||||
|
|
||||||
|
/** Application-global constraints that apply to account settings */
|
||||||
constraints: Constraints<Settings>;
|
constraints: Constraints<Settings>;
|
||||||
|
|
||||||
/** storage location for account-global settings */
|
/** storage location for account-global settings */
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CredentialCategory, GeneratedCredential } from ".";
|
import { CredentialAlgorithm, GeneratedCredential } from ".";
|
||||||
|
|
||||||
describe("GeneratedCredential", () => {
|
describe("GeneratedCredential", () => {
|
||||||
describe("constructor", () => {
|
describe("constructor", () => {
|
||||||
@ -34,7 +34,7 @@ describe("GeneratedCredential", () => {
|
|||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
credential: "example",
|
credential: "example",
|
||||||
category: "password" as CredentialCategory,
|
category: "password" as CredentialAlgorithm,
|
||||||
generationDate: 100,
|
generationDate: 100,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -42,7 +42,7 @@ describe("GeneratedCredential", () => {
|
|||||||
it("fromJSON converts Json objects into credentials", () => {
|
it("fromJSON converts Json objects into credentials", () => {
|
||||||
const jsonValue = {
|
const jsonValue = {
|
||||||
credential: "example",
|
credential: "example",
|
||||||
category: "password" as CredentialCategory,
|
category: "password" as CredentialAlgorithm,
|
||||||
generationDate: 100,
|
generationDate: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { CredentialCategory } from "./credential-category";
|
import { CredentialAlgorithm } from "./generator-type";
|
||||||
|
|
||||||
/** A credential generation result */
|
/** A credential generation result */
|
||||||
export class GeneratedCredential {
|
export class GeneratedCredential {
|
||||||
@ -14,7 +14,7 @@ export class GeneratedCredential {
|
|||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
readonly credential: string,
|
readonly credential: string,
|
||||||
readonly category: CredentialCategory,
|
readonly category: CredentialAlgorithm,
|
||||||
generationDate: Date | number,
|
generationDate: Date | number,
|
||||||
) {
|
) {
|
||||||
if (typeof generationDate === "number") {
|
if (typeof generationDate === "number") {
|
||||||
|
@ -1,7 +1,56 @@
|
|||||||
import { GeneratorTypes, PasswordTypes } from "../data/generator-types";
|
import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "../data/generator-types";
|
||||||
|
|
||||||
/** The kind of credential being generated. */
|
/** A type of password that may be generated by the credential generator. */
|
||||||
export type GeneratorType = (typeof GeneratorTypes)[number];
|
export type PasswordAlgorithm = (typeof PasswordAlgorithms)[number];
|
||||||
|
|
||||||
/** The kinds of passwords that can be generated. */
|
/** A type of username that may be generated by the credential generator. */
|
||||||
export type PasswordType = (typeof PasswordTypes)[number];
|
export type UsernameAlgorithm = (typeof UsernameAlgorithms)[number];
|
||||||
|
|
||||||
|
/** A type of email address that may be generated by the credential generator. */
|
||||||
|
export type EmailAlgorithm = (typeof EmailAlgorithms)[number];
|
||||||
|
|
||||||
|
/** A type of credential that may be generated by the credential generator. */
|
||||||
|
export type CredentialAlgorithm = PasswordAlgorithm | UsernameAlgorithm | EmailAlgorithm;
|
||||||
|
|
||||||
|
/** Compound credential types supported by the credential generator. */
|
||||||
|
export const CredentialCategories = Object.freeze({
|
||||||
|
/** Lists algorithms in the "password" credential category */
|
||||||
|
password: PasswordAlgorithms as Readonly<PasswordAlgorithm[]>,
|
||||||
|
|
||||||
|
/** Lists algorithms in the "username" credential category */
|
||||||
|
username: UsernameAlgorithms as Readonly<UsernameAlgorithm[]>,
|
||||||
|
|
||||||
|
/** Lists algorithms in the "email" credential category */
|
||||||
|
email: EmailAlgorithms as Readonly<EmailAlgorithm[]>,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Returns true when the input algorithm is a password algorithm. */
|
||||||
|
export function isPasswordAlgorithm(
|
||||||
|
algorithm: CredentialAlgorithm,
|
||||||
|
): algorithm is PasswordAlgorithm {
|
||||||
|
return PasswordAlgorithms.includes(algorithm as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true when the input algorithm is a username algorithm. */
|
||||||
|
export function isUsernameAlgorithm(
|
||||||
|
algorithm: CredentialAlgorithm,
|
||||||
|
): algorithm is UsernameAlgorithm {
|
||||||
|
return UsernameAlgorithms.includes(algorithm as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true when the input algorithm is an email algorithm. */
|
||||||
|
export function isEmailAlgorithm(algorithm: CredentialAlgorithm): algorithm is EmailAlgorithm {
|
||||||
|
return EmailAlgorithms.includes(algorithm as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A type of compound credential that may be generated by the credential generator. */
|
||||||
|
export type CredentialCategory = keyof typeof CredentialCategories;
|
||||||
|
|
||||||
|
/** The kind of credential to generate using a compound configuration. */
|
||||||
|
// FIXME: extend the preferences to include a preferred forwarder
|
||||||
|
export type CredentialPreference = {
|
||||||
|
[Key in CredentialCategory]: {
|
||||||
|
algorithm: (typeof CredentialCategories)[Key][number];
|
||||||
|
updated: Date;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import { CredentialAlgorithm, PasswordAlgorithm } from "./generator-type";
|
||||||
|
|
||||||
export * from "./boundary";
|
export * from "./boundary";
|
||||||
export * from "./catchall-generator-options";
|
export * from "./catchall-generator-options";
|
||||||
export * from "./credential-category";
|
|
||||||
export * from "./credential-generator";
|
export * from "./credential-generator";
|
||||||
export * from "./credential-generator-configuration";
|
export * from "./credential-generator-configuration";
|
||||||
export * from "./eff-username-generator-options";
|
export * from "./eff-username-generator-options";
|
||||||
@ -17,3 +18,13 @@ export * from "./password-generator-policy";
|
|||||||
export * from "./policy-configuration";
|
export * from "./policy-configuration";
|
||||||
export * from "./subaddress-generator-options";
|
export * from "./subaddress-generator-options";
|
||||||
export * from "./word-options";
|
export * from "./word-options";
|
||||||
|
|
||||||
|
/** Provided for backwards compatibility only.
|
||||||
|
* @deprecated Use one of the Algorithm types instead.
|
||||||
|
*/
|
||||||
|
export type GeneratorType = CredentialAlgorithm;
|
||||||
|
|
||||||
|
/** Provided for backwards compatibility only.
|
||||||
|
* @deprecated Use one of the Algorithm types instead.
|
||||||
|
*/
|
||||||
|
export type PasswordType = PasswordAlgorithm;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { PasswordTypes, PolicyEvaluator } from "@bitwarden/generator-core";
|
import { PasswordAlgorithms, PolicyEvaluator } from "@bitwarden/generator-core";
|
||||||
|
|
||||||
import { DefaultGeneratorNavigation } from "./default-generator-navigation";
|
import { DefaultGeneratorNavigation } from "./default-generator-navigation";
|
||||||
import { GeneratorNavigation } from "./generator-navigation";
|
import { GeneratorNavigation } from "./generator-navigation";
|
||||||
@ -17,7 +17,7 @@ export class GeneratorNavigationEvaluator
|
|||||||
|
|
||||||
/** {@link PolicyEvaluator.policyInEffect} */
|
/** {@link PolicyEvaluator.policyInEffect} */
|
||||||
get policyInEffect(): boolean {
|
get policyInEffect(): boolean {
|
||||||
return PasswordTypes.includes(this.policy?.overridePasswordType);
|
return PasswordAlgorithms.includes(this.policy?.overridePasswordType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apply policy to the input options.
|
/** Apply policy to the input options.
|
||||||
|
Loading…
Reference in New Issue
Block a user