1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-10-22 07:50:04 +02:00

add forwarder settings to credential generator

This commit is contained in:
✨ Audrey ✨ 2024-10-16 16:39:39 -04:00
parent d5a19bab75
commit 1a5dae51d3
No known key found for this signature in database
GPG Key ID: 0CF8B4C0D9088B97
19 changed files with 244 additions and 7 deletions

View File

@ -37,8 +37,8 @@ import { Constraints, SubjectConstraints, WithConstraints } from "../types";
import { ClassifiedFormat, isClassifiedFormat } from "./classified-format";
import { unconstrained$ } from "./identity-state-constraint";
import { isObjectKey, ObjectKey, toUserKeyDefinition } from "./object-key";
import { isDynamic } from "./state-constraints-dependency";
import { isObjectKey, ObjectKey, toUserKeyDefinition } from "./subject-key";
import { UserEncryptor } from "./user-encryptor.abstraction";
import { UserStateSubjectDependencies } from "./user-state-subject-dependencies";

View File

@ -68,6 +68,11 @@
[userId]="userId$ | async"
(onUpdated)="generate$.next()"
/>
<tools-forwarder-settings
*ngIf="!!(forwarderId$ | async)"
[forwarder]="forwarderId$ | async"
[userId]="this.userId$ | async"
/>
<tools-subaddress-settings
*ngIf="(algorithm$ | async)?.id === 'subaddress'"
[userId]="userId$ | async"

View File

@ -16,6 +16,7 @@ import {
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { IntegrationId } from "@bitwarden/common/tools/integration";
import { UserId } from "@bitwarden/common/types/guid";
import { Option } from "@bitwarden/components/src/select/option";
import {
@ -260,6 +261,11 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
// template bindings refresh immediately
this.zone.run(() => {
this.algorithm$.next(algorithm);
if (userNav === FORWARDER && forwarderNav !== NONE_SELECTED) {
this.forwarderId$.next(forwarderNav.forwarder);
} else {
this.forwarderId$.next(null);
}
});
});
@ -314,6 +320,9 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
/** Lists the credential types of the username algorithm box. */
protected forwarderOptions$ = new BehaviorSubject<Option<ForwarderNavValue>[]>([]);
/** Tracks the currently selected forwarder. */
protected forwarderId$ = new BehaviorSubject<IntegrationId>(null);
/** tracks the currently selected credential type */
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);

View File

@ -0,0 +1,15 @@
<form class="box" [formGroup]="settings" class="tw-container">
<bit-form-field *ngIf="displayDomain">
<bit-label>{{ "aliasDomain" | i18n }}</bit-label>
<input bitInput formControlName="domain" type="text" />
</bit-form-field>
<bit-form-field *ngIf="displayToken">
<bit-label>{{ "apiAccessToken" | i18n }}</bit-label>
<input bitInput formControlName="token" type="password" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
<bit-form-field *ngIf="displayBaseUrl">
<bit-label>{{ "baseUrl" | i18n }}</bit-label>
<input bitInput formControlName="baseUrl" type="text" />
</bit-form-field>
</form>

View File

@ -0,0 +1,132 @@
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 { IntegrationId } from "@bitwarden/common/tools/integration";
import { UserId } from "@bitwarden/common/types/guid";
import {
CredentialGeneratorService,
getForwarderConfiguration,
toCredentialGeneratorConfiguration,
} from "@bitwarden/generator-core";
import { completeOnAccountSwitch, toValidators } from "./util";
const Controls = Object.freeze({
domain: "domain",
token: "token",
baseUrl: "baseUrl",
});
/** Options group for forwarder integrations */
@Component({
selector: "tools-forwarder-settings",
templateUrl: "forwarder-settings.component.html",
})
export class ForwarderSettingsComponent 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;
@Input()
forwarder: IntegrationId;
/** 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<unknown>();
/** The template's control bindings */
protected settings = this.formBuilder.group({
[Controls.domain]: [""],
[Controls.token]: [""],
[Controls.baseUrl]: [""],
});
async ngOnInit() {
const singleUserId$ = this.singleUserId$();
const forwarder = getForwarderConfiguration(this.forwarder);
// type erasure necessary because the configuration properties are
// determined dynamically at runtime
// FIXME: this can be eliminated by unifying the forwarder settings types;
// see `ForwarderConfiguration<...>` for details.
const configuration = toCredentialGeneratorConfiguration<any>(forwarder);
this.displayDomain = configuration.request.includes("domain");
this.displayToken = configuration.request.includes("token");
this.displayBaseUrl = configuration.request.includes("baseUrl");
// bind settings to the UI
const settings = await this.generatorService.settings(configuration, { singleUserId$ });
settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
// skips reactive event emissions to break a subscription cycle
this.settings.patchValue(s, { emitEvent: false });
});
// bind policy to the template
this.generatorService
.policy$(configuration, { userId$: singleUserId$ })
.pipe(takeUntil(this.destroyed$))
.subscribe(({ constraints }) => {
for (const name in Controls) {
const control = this.settings.get(name);
if (configuration.request.includes(control as any)) {
control.enable({ emitEvent: false });
control.setValidators(
// the configuration's type erasure affects `toValidators` as well
toValidators(name, configuration, constraints),
);
} else {
control.disable({ emitEvent: false });
control.clearValidators();
}
}
});
// the first emission is the current value; subsequent emissions are updates
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
// now that outputs are set up, connect inputs
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
}
protected displayDomain: boolean;
protected displayToken: boolean;
protected displayBaseUrl: boolean;
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();
}
}

View File

@ -33,6 +33,7 @@ import {
import { CatchallSettingsComponent } from "./catchall-settings.component";
import { CredentialGeneratorComponent } from "./credential-generator.component";
import { ForwarderSettingsComponent } from "./forwarder-settings.component";
import { PassphraseSettingsComponent } from "./passphrase-settings.component";
import { PasswordGeneratorComponent } from "./password-generator.component";
import { PasswordSettingsComponent } from "./password-settings.component";
@ -84,12 +85,13 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
declarations: [
CatchallSettingsComponent,
CredentialGeneratorComponent,
ForwarderSettingsComponent,
SubaddressSettingsComponent,
UsernameSettingsComponent,
PasswordGeneratorComponent,
PasswordSettingsComponent,
PassphraseSettingsComponent,
PasswordSettingsComponent,
UsernameGeneratorComponent,
UsernameSettingsComponent,
],
exports: [CredentialGeneratorComponent, PasswordGeneratorComponent, UsernameGeneratorComponent],
})

View File

@ -43,6 +43,11 @@
[userId]="this.userId$ | async"
(onUpdated)="generate$.next()"
/>
<tools-forwarder-settings
*ngIf="!!(forwarderId$ | async)"
[forwarder]="forwarderId$ | async"
[userId]="this.userId$ | async"
/>
<tools-subaddress-settings
*ngIf="(algorithm$ | async)?.id === 'subaddress'"
[userId]="this.userId$ | async"
@ -53,7 +58,6 @@
[userId]="this.userId$ | async"
(onUpdated)="generate$.next()"
/>
<!-- TODO: list forwarder settings components -->
</bit-card>
</div>
</bit-section>

View File

@ -16,6 +16,7 @@ import {
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { IntegrationId } from "@bitwarden/common/tools/integration";
import { UserId } from "@bitwarden/common/types/guid";
import { Option } from "@bitwarden/components/src/select/option";
import {
@ -201,6 +202,11 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
// template bindings refresh immediately
this.zone.run(() => {
this.algorithm$.next(algorithm);
if (userNav === FORWARDER && forwarderNav !== NONE_SELECTED) {
this.forwarderId$.next(forwarderNav.forwarder);
} else {
this.forwarderId$.next(null);
}
});
});
@ -249,6 +255,9 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
/** tracks the currently selected credential type */
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);
/** Tracks the currently selected forwarder. */
protected forwarderId$ = new BehaviorSubject<IntegrationId>(null);
/** Emits hint key for the currently selected credential type */
protected credentialTypeHint$ = new ReplaySubject<string>(1);

View File

@ -53,6 +53,7 @@ const PASSPHRASE = Object.freeze({
category: "password",
nameKey: "passphrase",
onlyOnRequest: false,
request: [],
engine: {
create(
dependencies: GeneratorDependencyProvider,
@ -92,6 +93,7 @@ const PASSWORD = Object.freeze({
category: "password",
nameKey: "password",
onlyOnRequest: false,
request: [],
engine: {
create(
dependencies: GeneratorDependencyProvider,
@ -139,6 +141,7 @@ const USERNAME = Object.freeze({
category: "username",
nameKey: "randomWord",
onlyOnRequest: false,
request: [],
engine: {
create(
dependencies: GeneratorDependencyProvider,
@ -172,6 +175,7 @@ const CATCHALL = Object.freeze({
nameKey: "catchallEmail",
descriptionKey: "catchallEmailDesc",
onlyOnRequest: false,
request: [],
engine: {
create(
dependencies: GeneratorDependencyProvider,
@ -205,6 +209,7 @@ const SUBADDRESS = Object.freeze({
nameKey: "plusAddressedEmail",
descriptionKey: "plusAddressedEmailDesc",
onlyOnRequest: false,
request: [],
engine: {
create(
dependencies: GeneratorDependencyProvider,
@ -240,6 +245,7 @@ export function toCredentialGeneratorConfiguration<Settings extends ApiSettings
category: "email",
nameKey: configuration.name,
onlyOnRequest: true,
request: configuration.forwarder.request,
engine: {
create(dependencies: GeneratorDependencyProvider) {
// FIXME: figure out why `configuration` fails to typecheck
@ -249,7 +255,7 @@ export function toCredentialGeneratorConfiguration<Settings extends ApiSettings
},
settings: {
initial: configuration.forwarder.defaultSettings,
constraints: {},
constraints: configuration.forwarder.settingsConstraints,
account: configuration.forwarder.settings,
},
policy: {

View File

@ -1,12 +1,14 @@
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
import { IntegrationConfiguration } from "@bitwarden/common/tools/integration/integration-configuration";
import { ApiSettings } from "@bitwarden/common/tools/integration/rpc";
import { ApiSettings, SelfHostedApiSettings } from "@bitwarden/common/tools/integration/rpc";
import { IntegrationRequest } from "@bitwarden/common/tools/integration/rpc/integration-request";
import { RpcConfiguration } from "@bitwarden/common/tools/integration/rpc/rpc-definition";
import { BufferedKeyDefinition } from "@bitwarden/common/tools/state/buffered-key-definition";
import { ObjectKey } from "@bitwarden/common/tools/state/subject-key";
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
import { Constraints } from "@bitwarden/common/tools/types";
import { ForwarderContext } from "./forwarder-context";
import { EmailDomainSettings, EmailPrefixSettings } from "./settings";
/** Mixin for transmitting `getAccountId` result. */
export type AccountRequest = {
@ -25,8 +27,16 @@ export type GetAccountIdRpcDef<
Request extends IntegrationRequest = IntegrationRequest,
> = RpcConfiguration<Request, ForwarderContext<Settings>, string>;
export type ForwarderRequestFields = keyof (ApiSettings &
SelfHostedApiSettings &
EmailDomainSettings &
EmailPrefixSettings);
/** Forwarder-specific static definition */
export type ForwarderConfiguration<
// FIXME: simply forwarder settings to an object that has all
// settings properties. The runtime dynamism should be limited
// to which have values, not which have properties listed.
Settings extends ApiSettings,
Request extends IntegrationRequest = IntegrationRequest,
> = IntegrationConfiguration & {
@ -35,6 +45,11 @@ export type ForwarderConfiguration<
/** default value of all fields */
defaultSettings: Partial<Settings>;
settingsConstraints: Constraints<Settings>;
/** Well-known fields to display on the forwarder screen */
request: readonly ForwarderRequestFields[];
/** forwarder settings storage
* @deprecated use local.settings instead
*/

View File

@ -52,6 +52,12 @@ const createForwardingEmail = Object.freeze({
const forwarder = Object.freeze({
defaultSettings,
createForwardingEmail,
request: ["token", "baseUrl", "domain"],
settingsConstraints: {
token: { required: true },
domain: { required: true },
baseUrl: {},
},
local: {
settings: {
// FIXME: integration should issue keys at runtime

View File

@ -44,6 +44,10 @@ const createForwardingEmail = Object.freeze({
const forwarder = Object.freeze({
defaultSettings,
createForwardingEmail,
request: ["token"],
settingsConstraints: {
token: { required: true },
},
local: {
settings: {
// FIXME: integration should issue keys at runtime

View File

@ -110,6 +110,12 @@ const forwarder = Object.freeze({
defaultSettings,
createForwardingEmail,
getAccountId,
request: ["token", "domain", "prefix"],
settingsConstraints: {
token: { required: true },
domain: { required: true },
prefix: {},
},
local: {
settings: {
// FIXME: integration should issue keys at runtime

View File

@ -48,6 +48,10 @@ const createForwardingEmail = Object.freeze({
const forwarder = Object.freeze({
defaultSettings,
createForwardingEmail,
request: ["token"],
settingsConstraints: {
token: { required: true },
},
local: {
settings: {
// FIXME: integration should issue keys at runtime

View File

@ -50,6 +50,11 @@ const createForwardingEmail = Object.freeze({
// forwarder configuration
const forwarder = Object.freeze({
defaultSettings,
request: ["token", "domain"],
settingsConstraints: {
token: { required: true },
domain: { required: true },
},
local: {
settings: {
// FIXME: integration should issue keys at runtime

View File

@ -53,6 +53,10 @@ const createForwardingEmail = Object.freeze({
const forwarder = Object.freeze({
defaultSettings,
createForwardingEmail,
request: ["token", "baseUrl"],
settingsConstraints: {
token: { required: true },
},
local: {
settings: {
// FIXME: integration should issue keys at runtime

View File

@ -244,6 +244,7 @@ export class CredentialGeneratorService {
category: generator.category,
name: integration ? integration.name : this.i18nService.t(generator.nameKey),
onlyOnRequest: generator.onlyOnRequest,
request: generator.request,
};
if (generator.descriptionKey) {

View File

@ -40,6 +40,11 @@ export type AlgorithmInfo = {
* to Bitwarden.
*/
onlyOnRequest: boolean;
/** Well-known fields to display on the options panel or collect from the environment.
* @remarks: at present, this is only used by forwarders
*/
request: readonly string[];
};
/** Credential generator metadata common across credential generators */
@ -68,6 +73,11 @@ export type CredentialGeneratorInfo = {
* to Bitwarden.
*/
onlyOnRequest: boolean;
/** Well-known fields to display on the options panel or collect from the environment.
* @remarks: at present, this is only used by forwarders
*/
request: readonly string[];
};
/** Credential generator metadata that relies upon typed setting and policy definitions.