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

forwarder lookup and generation support

This commit is contained in:
✨ Audrey ✨ 2024-10-10 13:23:35 -04:00
parent 0791191ce0
commit 3aebbe9a64
No known key found for this signature in database
GPG Key ID: 0CF8B4C0D9088B97
7 changed files with 117 additions and 57 deletions

View File

@ -1,7 +1,13 @@
import { Opaque } from "type-fest";
export const IntegrationIds = [
"anonaddy",
"duckduckgo",
"fastmail",
"firefoxrelay",
"forwardemail",
"simplelogin",
] as const;
/** Identifies a vendor integrated into bitwarden */
export type IntegrationId = Opaque<
"anonaddy" | "duckduckgo" | "fastmail" | "firefoxrelay" | "forwardemail" | "simplelogin",
"IntegrationId"
>;
export type IntegrationId = Opaque<(typeof IntegrationIds)[number], "IntegrationId">;

View File

@ -25,10 +25,13 @@ import {
CredentialGeneratorService,
GeneratedCredential,
Generators,
getForwarderConfiguration,
isEmailAlgorithm,
isForwarderIntegration,
isPasswordAlgorithm,
isUsernameAlgorithm,
PasswordAlgorithm,
toCredentialGeneratorConfiguration,
} from "@bitwarden/generator-core";
/** root category that drills into username and email categories */
@ -247,10 +250,15 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
case "passphrase":
return this.generatorService.generate$(Generators.passphrase, dependencies);
default:
throw new Error(`Invalid generator type: "${type}"`);
}
if (isForwarderIntegration(type)) {
const forwarder = getForwarderConfiguration(type.forwarder);
const configuration = toCredentialGeneratorConfiguration(forwarder);
return this.generatorService.generate$(configuration, dependencies);
}
throw new Error(`Invalid generator type: "${type}"`);
}
/** Lists the credential types of the username algorithm box. */

View File

@ -22,9 +22,11 @@ import {
CredentialGeneratorService,
GeneratedCredential,
Generators,
getForwarderConfiguration,
isEmailAlgorithm,
isForwarderIntegration,
isUsernameAlgorithm,
toCredentialGeneratorConfiguration,
} from "@bitwarden/generator-core";
/** Component that generates usernames and emails */
@ -79,10 +81,13 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
this.generatorService
.algorithms$(["email", "username"], { userId$: this.userId$ })
.pipe(
map((algorithms) => [
this.toOptions(algorithms.filter(a => !isForwarderIntegration(a.id))),
this.toOptions(algorithms.filter(a => isForwarderIntegration(a.id)))
] as const),
map(
(algorithms) =>
[
this.toOptions(algorithms.filter((a) => !isForwarderIntegration(a.id))),
this.toOptions(algorithms.filter((a) => isForwarderIntegration(a.id))),
] as const,
),
takeUntil(this.destroyed),
)
.subscribe(([type, forwarder]) => {
@ -182,10 +187,15 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
case "username":
return this.generatorService.generate$(Generators.username, dependencies);
default:
throw new Error(`Invalid generator type: "${type}"`);
}
if (isForwarderIntegration(type)) {
const forwarder = getForwarderConfiguration(type.forwarder);
const configuration = toCredentialGeneratorConfiguration(forwarder);
return this.generatorService.generate$(configuration, dependencies);
}
throw new Error(`Invalid generator type: "${type}"`);
}
/** Lists the credential types supported by the component. */

View File

@ -3,7 +3,12 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { ApiSettings } from "@bitwarden/common/tools/integration/rpc";
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
import { EmailRandomizer, PasswordRandomizer, UsernameRandomizer } from "../engine";
import {
EmailRandomizer,
ForwarderConfiguration,
PasswordRandomizer,
UsernameRandomizer,
} from "../engine";
import { Forwarder } from "../engine/forwarder";
import {
DefaultPolicyEvaluator,
@ -26,7 +31,6 @@ import {
CredentialGenerator,
CredentialGeneratorConfiguration,
EffUsernameGenerationOptions,
ForwarderIntegration,
GeneratorDependencyProvider,
NoPolicy,
PassphraseGenerationOptions,
@ -43,8 +47,6 @@ import { DefaultPassphraseGenerationOptions } from "./default-passphrase-generat
import { DefaultPasswordBoundaries } from "./default-password-boundaries";
import { DefaultPasswordGenerationOptions } from "./default-password-generation-options";
import { DefaultSubaddressOptions } from "./default-subaddress-generator-options";
import { getForwarderConfiguration } from "./integrations";
const PASSPHRASE = Object.freeze({
id: "passphrase",
@ -52,7 +54,9 @@ const PASSPHRASE = Object.freeze({
nameKey: "passphrase",
onlyOnRequest: false,
engine: {
create(dependencies: GeneratorDependencyProvider): CredentialGenerator<PassphraseGenerationOptions> {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<PassphraseGenerationOptions> {
return new PasswordRandomizer(dependencies.randomizer);
},
},
@ -89,7 +93,9 @@ const PASSWORD = Object.freeze({
nameKey: "password",
onlyOnRequest: false,
engine: {
create(dependencies: GeneratorDependencyProvider): CredentialGenerator<PasswordGenerationOptions> {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<PasswordGenerationOptions> {
return new PasswordRandomizer(dependencies.randomizer);
},
},
@ -134,7 +140,9 @@ const USERNAME = Object.freeze({
nameKey: "randomWord",
onlyOnRequest: false,
engine: {
create(dependencies: GeneratorDependencyProvider): CredentialGenerator<EffUsernameGenerationOptions> {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<EffUsernameGenerationOptions> {
return new UsernameRandomizer(dependencies.randomizer);
},
},
@ -165,7 +173,9 @@ const CATCHALL = Object.freeze({
descriptionKey: "catchallEmailDesc",
onlyOnRequest: false,
engine: {
create(dependencies: GeneratorDependencyProvider): CredentialGenerator<CatchallGenerationOptions> {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<CatchallGenerationOptions> {
return new EmailRandomizer(dependencies.randomizer);
},
},
@ -196,7 +206,9 @@ const SUBADDRESS = Object.freeze({
descriptionKey: "plusAddressedEmailDesc",
onlyOnRequest: false,
engine: {
create(dependencies: GeneratorDependencyProvider): CredentialGenerator<SubaddressGenerationOptions> {
create(
dependencies: GeneratorDependencyProvider,
): CredentialGenerator<SubaddressGenerationOptions> {
return new EmailRandomizer(dependencies.randomizer);
},
},
@ -220,13 +232,9 @@ const SUBADDRESS = Object.freeze({
},
} satisfies CredentialGeneratorConfiguration<SubaddressGenerationOptions, NoPolicy>);
export function toCredentialGeneratorConfiguration<Settings extends ApiSettings = ApiSettings>(id :ForwarderIntegration) {
// TODO: eliminate the type erasure
const configuration = getForwarderConfiguration(id.forwarder) as any;
if(!configuration) {
throw new Error(`Invalid forwarder id: ${id.forwarder}`);
}
export function toCredentialGeneratorConfiguration<Settings extends ApiSettings = ApiSettings>(
configuration: ForwarderConfiguration<Settings>,
) {
const forwarder = Object.freeze({
id: { forwarder: configuration.id },
category: "email",
@ -234,13 +242,15 @@ export function toCredentialGeneratorConfiguration<Settings extends ApiSettings
onlyOnRequest: true,
engine: {
create(dependencies: GeneratorDependencyProvider) {
return new Forwarder(configuration, dependencies.client, dependencies.i18nService);
}
// FIXME: figure out why `configuration` fails to typecheck
const config: any = configuration;
return new Forwarder(config, dependencies.client, dependencies.i18nService);
},
},
settings: {
initial: configuration.forwarder.defaultSettings,
constraints: {},
account: configuration.forwarder.settings
account: configuration.forwarder.settings,
},
policy: {
type: PolicyType.PasswordGenerator,
@ -254,7 +264,7 @@ export function toCredentialGeneratorConfiguration<Settings extends ApiSettings
toConstraints(_policy: NoPolicy) {
return new IdentityConstraint<Settings>();
},
}
},
} satisfies CredentialGeneratorConfiguration<Settings, NoPolicy>);
return forwarder;

View File

@ -9,6 +9,13 @@ import { FirefoxRelay } from "../integration/firefox-relay";
import { ForwardEmail } from "../integration/forward-email";
import { SimpleLogin } from "../integration/simple-login";
/** Fixed list of integrations available to the application
* @example
*
* // Use `toCredentialGeneratorConfiguration(id :ForwarderIntegration)`
* // to convert an integration to a generator configuration
* const generator = toCredentialGeneratorConfiguration(Integrations.AddyIo);
*/
export const Integrations = Object.freeze({
AddyIo,
DuckDuckGo,
@ -18,13 +25,13 @@ export const Integrations = Object.freeze({
SimpleLogin,
} as const);
const integrations = Object.fromEntries(Object.values(Integrations).map((i) => [i.id, i as ForwarderConfiguration<object>]));
const integrations = new Map(Object.values(Integrations).map((i) => [i.id, i]));
export function getForwarderConfiguration(id: IntegrationId) : ForwarderConfiguration<ApiSettings> {
const maybeForwarder = integrations[id as string];
export function getForwarderConfiguration(id: IntegrationId): ForwarderConfiguration<ApiSettings> {
const maybeForwarder = integrations.get(id);
if("forwarder" in maybeForwarder) {
return maybeForwarder as ForwarderConfiguration<ApiSettings>
if ("forwarder" in maybeForwarder) {
return maybeForwarder as ForwarderConfiguration<ApiSettings>;
} else {
return null;
}

View File

@ -36,7 +36,7 @@ import { isDynamic } from "@bitwarden/common/tools/state/state-constraints-depen
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
import { Randomizer } from "../abstractions";
import { Generators, toCredentialGeneratorConfiguration } from "../data";
import { Generators, getForwarderConfiguration, toCredentialGeneratorConfiguration } from "../data";
import { availableAlgorithms } from "../policies/available-algorithms-policy";
import { mapPolicyToConstraints } from "../rx";
import {
@ -45,9 +45,12 @@ import {
CredentialCategory,
CredentialGeneratorInfo,
CredentialPreference,
isForwarderIntegration
isForwarderIntegration,
} from "../types";
import { CredentialGeneratorConfiguration as Configuration, GeneratorDependencyProvider } from "../types/credential-generator-configuration";
import {
CredentialGeneratorConfiguration as Configuration,
GeneratorDependencyProvider,
} from "../types/credential-generator-configuration";
import { GeneratorConstraints } from "../types/generator-constraints";
import { PREFERENCES } from "./credential-preferences";
@ -65,7 +68,7 @@ type Generate$Dependencies = Simplify<Partial<OnDependency> & Partial<UserDepend
*/
website$?: Observable<string>;
integration$?: Observable<IntegrationId>
integration$?: Observable<IntegrationId>;
};
type Algorithms$Dependencies = Partial<UserDependency>;
@ -76,14 +79,14 @@ export class CredentialGeneratorService {
private stateProvider: StateProvider,
private policyService: PolicyService,
private apiService: ApiService,
private i18nService: I18nService
private i18nService: I18nService,
) {}
private getDependencyProvider() : GeneratorDependencyProvider {
private getDependencyProvider(): GeneratorDependencyProvider {
return {
client: new RestClient(this.apiService, this.i18nService),
i18nService: this.i18nService,
randomizer: this.randomizer
randomizer: this.randomizer,
};
}
@ -105,10 +108,7 @@ export class CredentialGeneratorService {
// stream blocks until all of these values are received
const website$ = dependencies?.website$ ?? new BehaviorSubject<string>(null);
const integration$ = dependencies?.integration$ ?? new BehaviorSubject<IntegrationId>(null);
const request$ = combineLatest([website$, integration$]).pipe(
map(([website, integration]) => ({ website, integration }))
);
const request$ = website$.pipe(map((website) => ({ website })));
const settings$ = this.settings$(configuration, dependencies);
// monitor completion
@ -183,7 +183,9 @@ export class CredentialGeneratorService {
return policies$;
}),
map((available) => {
const filtered = algorithms.filter((c) => isForwarderIntegration(c.id) || available.has(c.id));
const filtered = algorithms.filter(
(c) => isForwarderIntegration(c.id) || available.has(c.id),
);
return filtered;
}),
);
@ -212,8 +214,14 @@ export class CredentialGeneratorService {
* @returns the requested metadata, or `null` if the metadata wasn't found.
*/
algorithm(id: CredentialAlgorithm): CredentialGeneratorInfo {
if(isForwarderIntegration(id)) {
return toCredentialGeneratorConfiguration(id);
if (isForwarderIntegration(id)) {
const forwarder = getForwarderConfiguration(id.forwarder);
if (!forwarder) {
throw new Error(`Invalid forwarder id: ${id.forwarder}`);
}
const generator = toCredentialGeneratorConfiguration(forwarder);
return generator;
} else {
return Generators[id];
}

View File

@ -9,14 +9,19 @@ import { CredentialAlgorithm, CredentialCategory, PolicyConfiguration } from "..
import { CredentialGenerator } from "./credential-generator";
export type GeneratorDependencyProvider = {
randomizer: Randomizer,
client: RestClient,
i18nService: I18nService
randomizer: Randomizer;
client: RestClient;
i18nService: I18nService;
};
/** Credential generator metadata common across credential generators */
export type CredentialGeneratorInfo = {
/** Uniquely identifies the credential configuration
* @example
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
* // to pattern test whether the credential describes a forwarder algorithm
* const meta : CredentialGeneratorInfo = // ...
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {};
*/
id: CredentialAlgorithm;
@ -37,7 +42,13 @@ export type CredentialGeneratorInfo = {
onlyOnRequest: boolean;
};
/** Credential generator metadata that relies upon typed setting and policy definitions. */
/** Credential generator metadata that relies upon typed setting and policy definitions.
* @example
* // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)`
* // to pattern test whether the credential describes a forwarder algorithm
* const meta : CredentialGeneratorInfo = // ...
* const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {};
*/
export type CredentialGeneratorConfiguration<Settings, Policy> = CredentialGeneratorInfo & {
/** An algorithm that generates credentials when ran. */
engine: {