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:
parent
d5a19bab75
commit
1a5dae51d3
@ -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";
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
@ -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],
|
||||
})
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user