From 414bdde232bb6b184b3851bc52fa1213a5850f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Wed, 6 Nov 2024 11:54:29 -0500 Subject: [PATCH] [PM-13876] replace angular validation with html constraints validation (#11816) * rough-in passphrase validation failure handling * trigger valid change from settings * fix `max` constraint enforcement * add taps for generator validation monitoring/debugging * HTML constraints validation rises like a phoenix * remove min/max boundaries to fix chrome display issue * bind settings components as view children of options components * remove defunct `okSettings$` * extend validationless generator to passwords * extend validationless generator to catchall emails * extend validationless generator to forwarder emails * extend validationless generator to subaddress emails * extend validationless generator to usernames * fix observable cycle * disable generate button when no algorithm is selected * prevent duplicate algorithm emissions * add constraints that assign email address defaults --- libs/common/src/tools/state/object-key.ts | 1 + .../src/tools/state/user-state-subject.ts | 7 +- .../src/catchall-settings.component.html | 7 +- .../src/catchall-settings.component.ts | 22 ++- .../src/credential-generator.component.html | 8 + .../src/credential-generator.component.ts | 10 +- .../src/forwarder-settings.component.html | 18 +- .../src/forwarder-settings.component.ts | 53 ++---- .../components/src/generator.module.ts | 2 + .../src/passphrase-settings.component.html | 33 +++- .../src/passphrase-settings.component.ts | 42 +++-- .../src/password-generator.component.html | 4 + .../src/password-generator.component.ts | 53 +++--- .../src/password-settings.component.html | 46 ++++- .../src/password-settings.component.ts | 43 +++-- .../src/subaddress-settings.component.html | 7 +- .../src/subaddress-settings.component.ts | 34 ++-- .../src/username-generator.component.html | 6 + .../src/username-generator.component.ts | 5 +- .../src/username-settings.component.html | 14 +- .../src/username-settings.component.ts | 16 +- libs/tools/generator/components/src/util.ts | 2 +- .../generator/core/src/data/generators.ts | 175 +++++++++++------- .../core/src/policies/catchall-constraints.ts | 45 +++++ .../src/policies/subaddress-constraints.ts | 34 ++++ libs/tools/generator/core/src/rx.ts | 3 +- .../credential-generator.service.spec.ts | 45 +++++ .../services/credential-generator.service.ts | 27 ++- .../core/src/types/policy-configuration.ts | 6 +- .../send-ui/src/send-form/send-form.module.ts | 2 + 30 files changed, 552 insertions(+), 218 deletions(-) create mode 100644 libs/tools/generator/core/src/policies/catchall-constraints.ts create mode 100644 libs/tools/generator/core/src/policies/subaddress-constraints.ts diff --git a/libs/common/src/tools/state/object-key.ts b/libs/common/src/tools/state/object-key.ts index 88365d5cbd..0593186ec4 100644 --- a/libs/common/src/tools/state/object-key.ts +++ b/libs/common/src/tools/state/object-key.ts @@ -22,6 +22,7 @@ export type ObjectKey> classifier: Classifier; format: "plain" | "classified"; options: UserKeyDefinitionOptions; + initial?: State; }; export function isObjectKey(key: any): key is ObjectKey { diff --git a/libs/common/src/tools/state/user-state-subject.ts b/libs/common/src/tools/state/user-state-subject.ts index 89f19ac3c7..845ab25c80 100644 --- a/libs/common/src/tools/state/user-state-subject.ts +++ b/libs/common/src/tools/state/user-state-subject.ts @@ -254,17 +254,18 @@ export class UserStateSubject< withConstraints, map(([loadedState, constraints]) => { // bypass nulls - if (!loadedState) { + if (!loadedState && !this.objectKey?.initial) { return { constraints: {} as Constraints, state: null, } satisfies Constrained; } + const unconstrained = loadedState ?? structuredClone(this.objectKey.initial); const calibration = isDynamic(constraints) - ? constraints.calibrate(loadedState) + ? constraints.calibrate(unconstrained) : constraints; - const adjusted = calibration.adjust(loadedState); + const adjusted = calibration.adjust(unconstrained); return { constraints: calibration.constraints, diff --git a/libs/tools/generator/components/src/catchall-settings.component.html b/libs/tools/generator/components/src/catchall-settings.component.html index 0b2a9e69ef..61037c91a7 100644 --- a/libs/tools/generator/components/src/catchall-settings.component.html +++ b/libs/tools/generator/components/src/catchall-settings.component.html @@ -1,6 +1,11 @@
{{ "domainName" | i18n }} - +
diff --git a/libs/tools/generator/components/src/catchall-settings.component.ts b/libs/tools/generator/components/src/catchall-settings.component.ts index 55ddc1f810..74fb37d233 100644 --- a/libs/tools/generator/components/src/catchall-settings.component.ts +++ b/libs/tools/generator/components/src/catchall-settings.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { BehaviorSubject, skip, Subject, takeUntil } from "rxjs"; +import { BehaviorSubject, map, skip, Subject, takeUntil, withLatestFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserId } from "@bitwarden/common/types/guid"; @@ -12,6 +12,11 @@ import { import { completeOnAccountSwitch } from "./util"; +/** Splits an email into a username, subaddress, and domain named group. + * Subaddress is optional. + */ +export const DOMAIN_PARSER = new RegExp("[^@]+@(?.+)"); + /** Options group for catchall emails */ @Component({ selector: "tools-catchall-settings", @@ -60,7 +65,19 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy { // 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); + // now that outputs are set up, connect inputs + this.saveSettings + .pipe( + withLatestFrom(this.settings.valueChanges), + map(([, settings]) => settings), + takeUntil(this.destroyed$), + ) + .subscribe(settings); + } + + private saveSettings = new Subject(); + save(site: string = "component api call") { + this.saveSettings.next(site); } private singleUserId$() { @@ -78,6 +95,7 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy { private readonly destroyed$ = new Subject(); ngOnDestroy(): void { + this.destroyed$.next(); this.destroyed$.complete(); } } diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index f580b75f1b..0182bd1c20 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -22,6 +22,7 @@ buttonType="main" (click)="generate('user request')" [appA11yTitle]="credentialTypeGenerateLabel$ | async" + [disabled]="!(algorithm$ | async)" > {{ credentialTypeGenerateLabel$ | async }} @@ -33,16 +34,19 @@ [appA11yTitle]="credentialTypeCopyLabel$ | async" [appCopyClick]="value$ | async" [valueLabel]="credentialTypeLabel$ | async" + [disabled]="!(algorithm$ | async)" > (); const activeIdentifier$ = new Subject(); @@ -385,7 +384,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { if (!a || a.onlyOnRequest) { this.value$.next("-"); } else { - this.generate("autogenerate"); + this.generate("autogenerate").catch((e: unknown) => this.logService.error(e)); } }); }); @@ -495,7 +494,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { * @param requestor a label used to trace generation request * origin in the debugger. */ - protected generate(requestor: string) { + protected async generate(requestor: string) { this.generate$.next(requestor); } @@ -510,6 +509,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { private readonly destroyed = new Subject(); ngOnDestroy() { + this.destroyed.next(); this.destroyed.complete(); // finalize subjects diff --git a/libs/tools/generator/components/src/forwarder-settings.component.html b/libs/tools/generator/components/src/forwarder-settings.component.html index 64566fa956..0e15c2e89a 100644 --- a/libs/tools/generator/components/src/forwarder-settings.component.html +++ b/libs/tools/generator/components/src/forwarder-settings.component.html @@ -1,16 +1,28 @@
{{ "forwarderDomainName" | i18n }} - + {{ "forwarderDomainNameHint" | i18n }} {{ "apiKey" | i18n }} - + {{ "selfHostBaseUrl" | i18n }} - +
diff --git a/libs/tools/generator/components/src/forwarder-settings.component.ts b/libs/tools/generator/components/src/forwarder-settings.component.ts index 67e93c611e..f1caf91ade 100644 --- a/libs/tools/generator/components/src/forwarder-settings.component.ts +++ b/libs/tools/generator/components/src/forwarder-settings.component.ts @@ -17,7 +17,6 @@ import { skip, Subject, switchAll, - switchMap, takeUntil, withLatestFrom, } from "rxjs"; @@ -33,7 +32,7 @@ import { toCredentialGeneratorConfiguration, } from "@bitwarden/generator-core"; -import { completeOnAccountSwitch, toValidators } from "./util"; +import { completeOnAccountSwitch } from "./util"; const Controls = Object.freeze({ domain: "domain", @@ -117,35 +116,17 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy this.settings.patchValue(settings as any, { emitEvent: false }); }); - // bind policy to the reactive form - forwarder$ - .pipe( - switchMap((forwarder) => { - const constraints$ = this.generatorService - .policy$(forwarder, { userId$: singleUserId$ }) - .pipe(map(({ constraints }) => [constraints, forwarder] as const)); - - return constraints$; - }), - takeUntil(this.destroyed$), - ) - .subscribe(([constraints, forwarder]) => { - for (const name in Controls) { - const control = this.settings.get(name); - if (forwarder.request.includes(name as any)) { - control.enable({ emitEvent: false }); - control.setValidators( - // the configuration's type erasure affects `toValidators` as well - toValidators(name, forwarder, constraints), - ); - } else { - control.disable({ emitEvent: false }); - control.clearValidators(); - } + // enable requested forwarder inputs + forwarder$.pipe(takeUntil(this.destroyed$)).subscribe((forwarder) => { + for (const name in Controls) { + const control = this.settings.get(name); + if (forwarder.request.includes(name as any)) { + control.enable({ emitEvent: false }); + } else { + control.disable({ emitEvent: false }); } - - this.settings.updateValueAndValidity({ emitEvent: false }); - }); + } + }); // the first emission is the current value; subsequent emissions are updates settings$$ @@ -157,13 +138,18 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy .subscribe(this.onUpdated); // now that outputs are set up, connect inputs - this.settings.valueChanges - .pipe(withLatestFrom(settings$$), takeUntil(this.destroyed$)) - .subscribe(([value, settings]) => { + this.saveSettings + .pipe(withLatestFrom(this.settings.valueChanges, settings$$), takeUntil(this.destroyed$)) + .subscribe(([, value, settings]) => { settings.next(value); }); } + private saveSettings = new Subject(); + save(site: string = "component api call") { + this.saveSettings.next(site); + } + ngOnChanges(changes: SimpleChanges): void { this.refresh$.complete(); if ("forwarder" in changes) { @@ -192,6 +178,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy private readonly destroyed$ = new Subject(); ngOnDestroy(): void { + this.destroyed$.next(); this.destroyed$.complete(); } } diff --git a/libs/tools/generator/components/src/generator.module.ts b/libs/tools/generator/components/src/generator.module.ts index 2d1cedca40..e73d687d7d 100644 --- a/libs/tools/generator/components/src/generator.module.ts +++ b/libs/tools/generator/components/src/generator.module.ts @@ -7,6 +7,7 @@ import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateProvider } from "@bitwarden/common/platform/state"; @@ -79,6 +80,7 @@ const RANDOMIZER = new SafeInjectionToken("Randomizer"); I18nService, EncryptService, KeyService, + AccountService, ], }), ], diff --git a/libs/tools/generator/components/src/passphrase-settings.component.html b/libs/tools/generator/components/src/passphrase-settings.component.html index d089de7a07..4e073f3424 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.html +++ b/libs/tools/generator/components/src/passphrase-settings.component.html @@ -7,7 +7,13 @@ {{ "numWords" | i18n }} - + {{ numWordsBoundariesHint$ | async }} @@ -16,14 +22,33 @@ {{ "wordSeparator" | i18n }} - + - + {{ "capitalize" | i18n }} - + {{ "includeNumber" | i18n }}

{{ "generatorPolicyInEffect" | i18n }}

diff --git a/libs/tools/generator/components/src/passphrase-settings.component.ts b/libs/tools/generator/components/src/passphrase-settings.component.ts index d65e897f4e..f2f1749cb6 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.ts +++ b/libs/tools/generator/components/src/passphrase-settings.component.ts @@ -1,7 +1,15 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { BehaviorSubject, skip, takeUntil, Subject, ReplaySubject } from "rxjs"; +import { + BehaviorSubject, + skip, + takeUntil, + Subject, + map, + withLatestFrom, + ReplaySubject, +} from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -12,7 +20,7 @@ import { PassphraseGenerationOptions, } from "@bitwarden/generator-core"; -import { completeOnAccountSwitch, toValidators } from "./util"; +import { completeOnAccountSwitch } from "./util"; const Controls = Object.freeze({ numWords: "numWords", @@ -81,21 +89,12 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { // the first emission is the current value; subsequent emissions are updates settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated); - // dynamic policy enforcement + // explain policy & disable policy-overridden fields this.generatorService .policy$(Generators.passphrase, { userId$: singleUserId$ }) .pipe(takeUntil(this.destroyed$)) .subscribe(({ constraints }) => { - this.settings - .get(Controls.numWords) - .setValidators(toValidators(Controls.numWords, Generators.passphrase, constraints)); - - this.settings - .get(Controls.wordSeparator) - .setValidators(toValidators(Controls.wordSeparator, Generators.passphrase, constraints)); - - this.settings.updateValueAndValidity({ emitEvent: false }); - + this.wordSeparatorMaxLength = constraints.wordSeparator.maxLength; this.policyInEffect = constraints.policyInEffect; this.toggleEnabled(Controls.capitalize, !constraints.capitalize?.readonly); @@ -110,7 +109,21 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { }); // now that outputs are set up, connect inputs - this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings); + this.saveSettings + .pipe( + withLatestFrom(this.settings.valueChanges), + map(([, settings]) => settings), + takeUntil(this.destroyed$), + ) + .subscribe(settings); + } + + /** attribute binding for wordSeparator[maxlength] */ + protected wordSeparatorMaxLength: number; + + private saveSettings = new Subject(); + save(site: string = "component api call") { + this.saveSettings.next(site); } /** display binding for enterprise policy notice */ @@ -144,6 +157,7 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { private readonly destroyed$ = new Subject(); ngOnDestroy(): void { + this.destroyed$.next(); this.destroyed$.complete(); } } diff --git a/libs/tools/generator/components/src/password-generator.component.html b/libs/tools/generator/components/src/password-generator.component.html index 6726df3085..81e18ed02a 100644 --- a/libs/tools/generator/components/src/password-generator.component.html +++ b/libs/tools/generator/components/src/password-generator.component.html @@ -20,6 +20,7 @@ buttonType="main" (click)="generate('user request')" [appA11yTitle]="credentialTypeGenerateLabel$ | async" + [disabled]="!(algorithm$ | async)" > {{ credentialTypeGenerateLabel$ | async }} @@ -31,10 +32,12 @@ [appA11yTitle]="credentialTypeCopyLabel$ | async" [appCopyClick]="value$ | async" [valueLabel]="credentialTypeLabel$ | async" + [disabled]="!(algorithm$ | async)" >
(null); + protected credentialType$ = new BehaviorSubject(null); /** Emits the last generated value. */ protected readonly value$ = new BehaviorSubject(""); @@ -72,14 +72,14 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { * @param requestor a label used to trace generation request * origin in the debugger. */ - protected generate(requestor: string) { + protected async generate(requestor: string) { this.generate$.next(requestor); } /** Tracks changes to the selected credential type * @param type the new credential type */ - protected onCredentialTypeChanged(type: PasswordAlgorithm) { + protected onCredentialTypeChanged(type: CredentialAlgorithm) { // break subscription cycle if (this.credentialType$.value !== type) { this.zone.run(() => { @@ -169,29 +169,34 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { preferences.next(preference); }); - // populate the form with the user's preferences to kick off interactivity - preferences.pipe(takeUntil(this.destroyed)).subscribe(({ password }) => { - // update navigation - this.onCredentialTypeChanged(password.algorithm); - - // load algorithm metadata - const algorithm = this.generatorService.algorithm(password.algorithm); - - // 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$ + // update active algorithm + preferences .pipe( - distinctUntilChanged((prev, next) => prev.id === next.id), - filter((a) => !a.onlyOnRequest), + map(({ password }) => this.generatorService.algorithm(password.algorithm)), + distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)), takeUntil(this.destroyed), ) - .subscribe(() => this.generate("autogenerate")); + .subscribe((algorithm) => { + // update navigation + this.onCredentialTypeChanged(algorithm.id); + + // 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(takeUntil(this.destroyed)).subscribe((a) => { + this.zone.run(() => { + if (!a || a.onlyOnRequest) { + this.value$.next("-"); + } else { + this.generate("autogenerate").catch((e: unknown) => this.logService.error(e)); + } + }); + }); } private typeToGenerator$(type: CredentialAlgorithm) { diff --git a/libs/tools/generator/components/src/password-settings.component.html b/libs/tools/generator/components/src/password-settings.component.html index aa12a3247c..9f8e00921f 100644 --- a/libs/tools/generator/components/src/password-settings.component.html +++ b/libs/tools/generator/components/src/password-settings.component.html @@ -7,7 +7,7 @@ {{ "length" | i18n }} - + {{ lengthBoundariesHint$ | async }} @@ -21,7 +21,12 @@ attr.aria-description="{{ 'uppercaseDescription' | i18n }}" title="{{ 'uppercaseDescription' | i18n }}" > - + {{ "uppercaseLabel" | i18n }} - + {{ "lowercaseLabel" | i18n }} - + {{ "numbersLabel" | i18n }} - + {{ "specialCharactersLabel" | i18n }}
{{ "minNumbers" | i18n }} - + {{ "minSpecial" | i18n }} - +
- + {{ "avoidAmbiguous" | i18n }}

{{ "generatorPolicyInEffect" | i18n }}

diff --git a/libs/tools/generator/components/src/password-settings.component.ts b/libs/tools/generator/components/src/password-settings.component.ts index 6e9d106b71..677a3417b9 100644 --- a/libs/tools/generator/components/src/password-settings.component.ts +++ b/libs/tools/generator/components/src/password-settings.component.ts @@ -1,7 +1,17 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { BehaviorSubject, takeUntil, Subject, map, filter, tap, skip, ReplaySubject } from "rxjs"; +import { + BehaviorSubject, + takeUntil, + Subject, + map, + filter, + tap, + skip, + ReplaySubject, + withLatestFrom, +} from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -12,7 +22,7 @@ import { PasswordGenerationOptions, } from "@bitwarden/generator-core"; -import { completeOnAccountSwitch, toValidators } from "./util"; +import { completeOnAccountSwitch } from "./util"; const Controls = Object.freeze({ length: "length", @@ -118,23 +128,11 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { this.settings.patchValue(s, { emitEvent: false }); }); - // bind policy to the template + // explain policy & disable policy-overridden fields this.generatorService .policy$(Generators.password, { userId$: singleUserId$ }) .pipe(takeUntil(this.destroyed$)) .subscribe(({ constraints }) => { - this.settings - .get(Controls.length) - .setValidators(toValidators(Controls.length, Generators.password, constraints)); - - this.minNumber.setValidators( - toValidators(Controls.minNumber, Generators.password, constraints), - ); - - this.minSpecial.setValidators( - toValidators(Controls.minSpecial, Generators.password, constraints), - ); - this.policyInEffect = constraints.policyInEffect; const toggles = [ @@ -153,8 +151,8 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { const boundariesHint = this.i18nService.t( "generatorBoundariesHint", - constraints.length.min, - constraints.length.max, + constraints.length.min?.toString(), + constraints.length.max?.toString(), ); this.lengthBoundariesHint.next(boundariesHint); }); @@ -201,9 +199,10 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated); // now that outputs are set up, connect inputs - this.settings.valueChanges + this.saveSettings .pipe( - map((settings) => { + withLatestFrom(this.settings.valueChanges), + map(([, settings]) => { // interface is "avoid" while storage is "include" const s: any = { ...settings }; s.ambiguous = s.avoidAmbiguous; @@ -215,6 +214,11 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { .subscribe(settings); } + private saveSettings = new Subject(); + save(site: string = "component api call") { + this.saveSettings.next(site); + } + /** display binding for enterprise policy notice */ protected policyInEffect: boolean; @@ -246,6 +250,7 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { private readonly destroyed$ = new Subject(); ngOnDestroy(): void { + this.destroyed$.next(); this.destroyed$.complete(); } } diff --git a/libs/tools/generator/components/src/subaddress-settings.component.html b/libs/tools/generator/components/src/subaddress-settings.component.html index 16f3aea28b..1dfb5e3460 100644 --- a/libs/tools/generator/components/src/subaddress-settings.component.html +++ b/libs/tools/generator/components/src/subaddress-settings.component.html @@ -1,6 +1,11 @@
{{ "email" | i18n }} - +
diff --git a/libs/tools/generator/components/src/subaddress-settings.component.ts b/libs/tools/generator/components/src/subaddress-settings.component.ts index bd6ca899db..5a310c8def 100644 --- a/libs/tools/generator/components/src/subaddress-settings.component.ts +++ b/libs/tools/generator/components/src/subaddress-settings.component.ts @@ -53,28 +53,25 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy { const singleUserId$ = this.singleUserId$(); const settings = await this.generatorService.settings(Generators.subaddress, { singleUserId$ }); - settings - .pipe( - withLatestFrom(this.accountService.activeAccount$), - map(([settings, activeAccount]) => { - // if the subaddress isn't specified, copy it from - // the user's settings - if ((settings.subaddressEmail ?? "").length < 1) { - settings.subaddressEmail = activeAccount.email; - } - - return settings; - }), - takeUntil(this.destroyed$), - ) - .subscribe((s) => { - this.settings.patchValue(s, { emitEvent: false }); - }); + 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); + this.saveSettings + .pipe( + withLatestFrom(this.settings.valueChanges), + map(([, settings]) => settings), + takeUntil(this.destroyed$), + ) + .subscribe(settings); + } + + private saveSettings = new Subject(); + save(site: string = "component api call") { + this.saveSettings.next(site); } private singleUserId$() { @@ -92,6 +89,7 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy { private readonly destroyed$ = new Subject(); ngOnDestroy(): void { + this.destroyed$.next(); this.destroyed$.complete(); } } diff --git a/libs/tools/generator/components/src/username-generator.component.html b/libs/tools/generator/components/src/username-generator.component.html index 36aaae57ce..f96374e063 100644 --- a/libs/tools/generator/components/src/username-generator.component.html +++ b/libs/tools/generator/components/src/username-generator.component.html @@ -9,6 +9,7 @@ buttonType="main" (click)="generate('user request')" [appA11yTitle]="credentialTypeGenerateLabel$ | async" + [disabled]="!(algorithm$ | async)" > {{ credentialTypeGenerateLabel$ | async }} @@ -20,6 +21,7 @@ [appA11yTitle]="credentialTypeCopyLabel$ | async" [appCopyClick]="value$ | async" [valueLabel]="credentialTypeLabel$ | async" + [disabled]="!(algorithm$ | async)" > {{ credentialTypeCopyLabel$ | async }} @@ -57,21 +59,25 @@ this.logService.error(e)); } }); }); @@ -414,7 +414,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { * @param requestor a label used to trace generation request * origin in the debugger. */ - protected generate(requestor: string) { + protected async generate(requestor: string) { this.generate$.next(requestor); } @@ -429,6 +429,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { private readonly destroyed = new Subject(); ngOnDestroy() { + this.destroyed.next(); this.destroyed.complete(); // finalize subjects diff --git a/libs/tools/generator/components/src/username-settings.component.html b/libs/tools/generator/components/src/username-settings.component.html index 4a4f8cd9fe..649cd052e7 100644 --- a/libs/tools/generator/components/src/username-settings.component.html +++ b/libs/tools/generator/components/src/username-settings.component.html @@ -1,10 +1,20 @@
- + {{ "capitalize" | i18n }} - + {{ "includeNumber" | i18n }}
diff --git a/libs/tools/generator/components/src/username-settings.component.ts b/libs/tools/generator/components/src/username-settings.component.ts index 8237b8674c..05a46feaaa 100644 --- a/libs/tools/generator/components/src/username-settings.component.ts +++ b/libs/tools/generator/components/src/username-settings.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { BehaviorSubject, skip, Subject, takeUntil } from "rxjs"; +import { BehaviorSubject, map, skip, Subject, takeUntil, withLatestFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserId } from "@bitwarden/common/types/guid"; @@ -61,7 +61,18 @@ export class UsernameSettingsComponent implements OnInit, OnDestroy { // 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); + this.saveSettings + .pipe( + withLatestFrom(this.settings.valueChanges), + map(([, settings]) => settings), + takeUntil(this.destroyed$), + ) + .subscribe(settings); + } + + private saveSettings = new Subject(); + save(site: string = "component api call") { + this.saveSettings.next(site); } private singleUserId$() { @@ -79,6 +90,7 @@ export class UsernameSettingsComponent implements OnInit, OnDestroy { private readonly destroyed$ = new Subject(); ngOnDestroy(): void { + this.destroyed$.next(); this.destroyed$.complete(); } } diff --git a/libs/tools/generator/components/src/util.ts b/libs/tools/generator/components/src/util.ts index d6cd4e6fba..7977f77459 100644 --- a/libs/tools/generator/components/src/util.ts +++ b/libs/tools/generator/components/src/util.ts @@ -49,7 +49,7 @@ export function toValidators( } const max = getConstraint("max", config, runtime); - if (max === undefined) { + if (max !== undefined) { validators.push(Validators.max(max)); } diff --git a/libs/tools/generator/core/src/data/generators.ts b/libs/tools/generator/core/src/data/generators.ts index 6090fe789c..6ddea595ec 100644 --- a/libs/tools/generator/core/src/data/generators.ts +++ b/libs/tools/generator/core/src/data/generators.ts @@ -1,7 +1,10 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; import { ApiSettings } from "@bitwarden/common/tools/integration/rpc"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; import { EmailRandomizer, @@ -19,12 +22,12 @@ import { PasswordGeneratorOptionsEvaluator, passwordLeastPrivilege, } from "../policies"; +import { CatchallConstraints } from "../policies/catchall-constraints"; +import { SubaddressConstraints } from "../policies/subaddress-constraints"; import { - CATCHALL_SETTINGS, EFF_USERNAME_SETTINGS, PASSPHRASE_SETTINGS, PASSWORD_SETTINGS, - SUBADDRESS_SETTINGS, } from "../strategies/storage"; import { CatchallGenerationOptions, @@ -178,79 +181,115 @@ const USERNAME = Object.freeze({ }, } satisfies CredentialGeneratorConfiguration); -const CATCHALL = Object.freeze({ - id: "catchall", - category: "email", - nameKey: "catchallEmail", - descriptionKey: "catchallEmailDesc", - generateKey: "generateEmail", - generatedValueKey: "email", - copyKey: "copyEmail", - onlyOnRequest: false, - request: [], - engine: { - create( - dependencies: GeneratorDependencyProvider, - ): CredentialGenerator { - return new EmailRandomizer(dependencies.randomizer); +const CATCHALL: CredentialGeneratorConfiguration = + Object.freeze({ + id: "catchall", + category: "email", + nameKey: "catchallEmail", + descriptionKey: "catchallEmailDesc", + generateKey: "generateEmail", + generatedValueKey: "email", + copyKey: "copyEmail", + onlyOnRequest: false, + request: [], + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new EmailRandomizer(dependencies.randomizer); + }, }, - }, - settings: { - initial: DefaultCatchallOptions, - constraints: { catchallDomain: { minLength: 1 } }, - account: CATCHALL_SETTINGS, - }, - policy: { - type: PolicyType.PasswordGenerator, - disabledValue: {}, - combine(_acc: NoPolicy, _policy: Policy) { - return {}; + settings: { + initial: DefaultCatchallOptions, + constraints: { catchallDomain: { minLength: 1 } }, + account: { + key: "catchallGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "catchallType", + "catchallDomain", + ]), + state: GENERATOR_DISK, + initial: { + catchallType: "random", + catchallDomain: "", + }, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + } satisfies ObjectKey, }, - createEvaluator(_policy: NoPolicy) { - return new DefaultPolicyEvaluator(); + policy: { + type: PolicyType.PasswordGenerator, + disabledValue: {}, + combine(_acc: NoPolicy, _policy: Policy) { + return {}; + }, + createEvaluator(_policy: NoPolicy) { + return new DefaultPolicyEvaluator(); + }, + toConstraints(_policy: NoPolicy, email: string) { + return new CatchallConstraints(email); + }, }, - toConstraints(_policy: NoPolicy) { - return new IdentityConstraint(); - }, - }, -} satisfies CredentialGeneratorConfiguration); + }); -const SUBADDRESS = Object.freeze({ - id: "subaddress", - category: "email", - nameKey: "plusAddressedEmail", - descriptionKey: "plusAddressedEmailDesc", - generateKey: "generateEmail", - generatedValueKey: "email", - copyKey: "copyEmail", - onlyOnRequest: false, - request: [], - engine: { - create( - dependencies: GeneratorDependencyProvider, - ): CredentialGenerator { - return new EmailRandomizer(dependencies.randomizer); +const SUBADDRESS: CredentialGeneratorConfiguration = + Object.freeze({ + id: "subaddress", + category: "email", + nameKey: "plusAddressedEmail", + descriptionKey: "plusAddressedEmailDesc", + generateKey: "generateEmail", + generatedValueKey: "email", + copyKey: "copyEmail", + onlyOnRequest: false, + request: [], + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new EmailRandomizer(dependencies.randomizer); + }, }, - }, - settings: { - initial: DefaultSubaddressOptions, - constraints: {}, - account: SUBADDRESS_SETTINGS, - }, - policy: { - type: PolicyType.PasswordGenerator, - disabledValue: {}, - combine(_acc: NoPolicy, _policy: Policy) { - return {}; + settings: { + initial: DefaultSubaddressOptions, + constraints: {}, + account: { + key: "subaddressGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "subaddressType", + "subaddressEmail", + ]), + state: GENERATOR_DISK, + initial: { + subaddressType: "random", + subaddressEmail: "", + }, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + } satisfies ObjectKey, }, - createEvaluator(_policy: NoPolicy) { - return new DefaultPolicyEvaluator(); + policy: { + type: PolicyType.PasswordGenerator, + disabledValue: {}, + combine(_acc: NoPolicy, _policy: Policy) { + return {}; + }, + createEvaluator(_policy: NoPolicy) { + return new DefaultPolicyEvaluator(); + }, + toConstraints(_policy: NoPolicy, email: string) { + return new SubaddressConstraints(email); + }, }, - toConstraints(_policy: NoPolicy) { - return new IdentityConstraint(); - }, - }, -} satisfies CredentialGeneratorConfiguration); + }); export function toCredentialGeneratorConfiguration( configuration: ForwarderConfiguration, diff --git a/libs/tools/generator/core/src/policies/catchall-constraints.ts b/libs/tools/generator/core/src/policies/catchall-constraints.ts new file mode 100644 index 0000000000..37f62f874c --- /dev/null +++ b/libs/tools/generator/core/src/policies/catchall-constraints.ts @@ -0,0 +1,45 @@ +import { Constraints, StateConstraints } from "@bitwarden/common/tools/types"; + +import { CatchallGenerationOptions } from "../types"; + +/** Parses the domain part of an email address + */ +const DOMAIN_PARSER = new RegExp("[^@]+@(?.+)"); + +/** A constraint that sets the catchall domain using a fixed email address */ +export class CatchallConstraints implements StateConstraints { + /** Creates a catchall constraints + * @param email - the email address containing the domain. + */ + constructor(email: string) { + if (!email) { + this.domain = ""; + return; + } + + const parsed = DOMAIN_PARSER.exec(email); + if (parsed && parsed.groups?.domain) { + this.domain = parsed.groups.domain; + } + } + private domain: string; + + constraints: Readonly> = {}; + + adjust(state: CatchallGenerationOptions) { + const currentDomain = (state.catchallDomain ?? "").trim(); + + if (currentDomain !== "") { + return state; + } + + const options = { ...state }; + options.catchallDomain = this.domain; + + return options; + } + + fix(state: CatchallGenerationOptions) { + return state; + } +} diff --git a/libs/tools/generator/core/src/policies/subaddress-constraints.ts b/libs/tools/generator/core/src/policies/subaddress-constraints.ts new file mode 100644 index 0000000000..db05f712cf --- /dev/null +++ b/libs/tools/generator/core/src/policies/subaddress-constraints.ts @@ -0,0 +1,34 @@ +import { Constraints, StateConstraints } from "@bitwarden/common/tools/types"; + +import { SubaddressGenerationOptions } from "../types"; + +/** A constraint that sets the subaddress email using a fixed email address */ +export class SubaddressConstraints implements StateConstraints { + /** Creates a catchall constraints + * @param email - the email address containing the domain. + */ + constructor(readonly email: string) { + if (!email) { + this.email = ""; + } + } + + constraints: Readonly> = {}; + + adjust(state: SubaddressGenerationOptions) { + const currentDomain = (state.subaddressEmail ?? "").trim(); + + if (currentDomain !== "") { + return state; + } + + const options = { ...state }; + options.subaddressEmail = this.email; + + return options; + } + + fix(state: SubaddressGenerationOptions) { + return state; + } +} diff --git a/libs/tools/generator/core/src/rx.ts b/libs/tools/generator/core/src/rx.ts index 070d34d37d..44d23ef1c5 100644 --- a/libs/tools/generator/core/src/rx.ts +++ b/libs/tools/generator/core/src/rx.ts @@ -23,11 +23,12 @@ export function mapPolicyToEvaluator( */ export function mapPolicyToConstraints( configuration: PolicyConfiguration, + email: string, ) { return pipe( reduceCollection(configuration.combine, configuration.disabledValue), distinctIfShallowMatch(), - map(configuration.toConstraints), + map((policy) => configuration.toConstraints(policy, email)), ); } diff --git a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts index 225745e5f9..bd26642157 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts @@ -202,6 +202,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); @@ -223,6 +224,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); @@ -248,6 +250,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); @@ -276,6 +279,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const website$ = new BehaviorSubject("some website"); const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { website$ })); @@ -297,6 +301,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const website$ = new BehaviorSubject("some website"); let error = null; @@ -322,6 +327,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const website$ = new BehaviorSubject("some website"); let completed = false; @@ -348,6 +354,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId$ = new BehaviorSubject(AnotherUser).asObservable(); const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ })); @@ -368,6 +375,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.pipe(filter((u) => !!u)); @@ -392,6 +400,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId$ = new BehaviorSubject(SomeUser); let error = null; @@ -417,6 +426,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId$ = new BehaviorSubject(SomeUser); let completed = false; @@ -443,6 +453,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const on$ = new Subject(); const results: any[] = []; @@ -485,6 +496,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const on$ = new Subject(); let error: any = null; @@ -511,6 +523,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const on$ = new Subject(); let complete = false; @@ -542,6 +555,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const result = generator.algorithms("password"); @@ -563,6 +577,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const result = generator.algorithms("username"); @@ -583,6 +598,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const result = generator.algorithms("email"); @@ -604,6 +620,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const result = generator.algorithms(["username", "email"]); @@ -629,6 +646,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const result = await firstValueFrom(generator.algorithms$("password")); @@ -646,6 +664,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const result = await firstValueFrom(generator.algorithms$("username")); @@ -662,6 +681,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const result = await firstValueFrom(generator.algorithms$("email")); @@ -679,6 +699,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const result = await firstValueFrom(generator.algorithms$(["username", "email"])); @@ -701,6 +722,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const result = await firstValueFrom(generator.algorithms$(["password"])); @@ -726,6 +748,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const results: any = []; const sub = generator.algorithms$("password").subscribe((r) => results.push(r)); @@ -763,6 +786,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId$ = new BehaviorSubject(AnotherUser).asObservable(); @@ -784,6 +808,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); @@ -814,6 +839,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); @@ -840,6 +866,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); @@ -866,6 +893,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); @@ -898,6 +926,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const result = await firstValueFrom(generator.settings$(SomeConfiguration)); @@ -916,6 +945,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const result = await firstValueFrom(generator.settings$(SomeConfiguration)); @@ -936,6 +966,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const result = await firstValueFrom(generator.settings$(SomeConfiguration)); @@ -961,6 +992,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const results: any = []; const sub = generator.settings$(SomeConfiguration).subscribe((r) => results.push(r)); @@ -986,6 +1018,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId$ = new BehaviorSubject(AnotherUser).asObservable(); @@ -1007,6 +1040,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); @@ -1034,6 +1068,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); @@ -1060,6 +1095,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); @@ -1086,6 +1122,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); @@ -1118,6 +1155,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const subject = await generator.settings(SomeConfiguration, { singleUserId$ }); @@ -1139,6 +1177,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); let completed = false; @@ -1165,6 +1204,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId$ = new BehaviorSubject(SomeUser).asObservable(); @@ -1182,6 +1222,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId$ = new BehaviorSubject(SomeUser).asObservable(); const policy$ = new BehaviorSubject([somePolicy]); @@ -1201,6 +1242,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); @@ -1230,6 +1272,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); @@ -1260,6 +1303,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); @@ -1286,6 +1330,7 @@ describe("CredentialGeneratorService", () => { i18nService, encryptService, keyService, + accountService, ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); diff --git a/libs/tools/generator/core/src/services/credential-generator.service.ts b/libs/tools/generator/core/src/services/credential-generator.service.ts index 04413ba2c0..8c971b0d61 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.ts @@ -23,6 +23,7 @@ import { Simplify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateProvider } from "@bitwarden/common/platform/state"; @@ -98,6 +99,7 @@ export class CredentialGeneratorService { private readonly i18nService: I18nService, private readonly encryptService: EncryptService, private readonly keyService: KeyService, + private readonly accountService: AccountService, ) {} private getDependencyProvider(): GeneratorDependencyProvider { @@ -380,17 +382,30 @@ export class CredentialGeneratorService { configuration: Configuration, dependencies: Policy$Dependencies, ): Observable> { - const completion$ = dependencies.userId$.pipe(ignoreElements(), endWith(true)); + const email$ = dependencies.userId$.pipe( + distinctUntilChanged(), + withLatestFrom(this.accountService.accounts$), + filter((accounts) => !!accounts), + map(([userId, accounts]) => { + if (userId in accounts) { + return { userId, email: accounts[userId].email }; + } - const constraints$ = dependencies.userId$.pipe( - switchMap((userId) => { - // complete policy emissions otherwise `mergeMap` holds `policies$` open indefinitely + return { userId, email: null }; + }), + ); + + const constraints$ = email$.pipe( + switchMap(({ userId, email }) => { + // complete policy emissions otherwise `switchMap` holds `policies$` open indefinitely const policies$ = this.policyService .getAll$(configuration.policy.type, userId) - .pipe(takeUntil(completion$)); + .pipe( + mapPolicyToConstraints(configuration.policy, email), + takeUntil(anyComplete(email$)), + ); return policies$; }), - mapPolicyToConstraints(configuration.policy), ); return constraints$; diff --git a/libs/tools/generator/core/src/types/policy-configuration.ts b/libs/tools/generator/core/src/types/policy-configuration.ts index 2b01a04b92..07ded88660 100644 --- a/libs/tools/generator/core/src/types/policy-configuration.ts +++ b/libs/tools/generator/core/src/types/policy-configuration.ts @@ -24,9 +24,13 @@ export type PolicyConfiguration = { createEvaluator: (policy: Policy) => PolicyEvaluator; /** Converts policy service data into actionable policy constraints. + * + * @param policy - the policy to map into policy constraints. + * @param email - the default email to extend. + * * @remarks this version includes constraints needed for the reactive forms; * it was introduced so that the constraints can be incrementally introduced * as the new UI is built. */ - toConstraints: (policy: Policy) => GeneratorConstraints; + toConstraints: (policy: Policy, email: string) => GeneratorConstraints; }; diff --git a/libs/tools/send/send-ui/src/send-form/send-form.module.ts b/libs/tools/send/send-ui/src/send-form/send-form.module.ts index 67f1f910cc..ec51c2c0e3 100644 --- a/libs/tools/send/send-ui/src/send-form/send-form.module.ts +++ b/libs/tools/send/send-ui/src/send-form/send-form.module.ts @@ -4,6 +4,7 @@ import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateProvider } from "@bitwarden/common/platform/state"; @@ -43,6 +44,7 @@ const RANDOMIZER = new SafeInjectionToken("Randomizer"); I18nService, EncryptService, KeyService, + AccountService, ], }), ],