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

rough example

This commit is contained in:
✨ Audrey ✨ 2024-10-09 15:36:35 -04:00
parent dc1f014ad8
commit f26ab56a5d
No known key found for this signature in database
GPG Key ID: 0CF8B4C0D9088B97
7 changed files with 179 additions and 17 deletions

View File

@ -1,5 +1,7 @@
import { Simplify } from "type-fest";
import { IntegrationId } from "./integration";
/** Constraints that are shared by all primitive field types */
type PrimitiveConstraint = {
/** `true` indicates the field is required; otherwise the field is optional */
@ -144,4 +146,6 @@ export type VaultItemRequest = {
/** Options that provide contextual information about the application state
* when a generator is invoked.
*/
export type GenerationRequest = Partial<VaultItemRequest>;
export type GenerationRequest = Partial<VaultItemRequest> & Partial<{
integration: IntegrationId | null
}>;

View File

@ -5,7 +5,7 @@ export const PasswordAlgorithms = Object.freeze(["password", "passphrase"] as co
export const UsernameAlgorithms = Object.freeze(["username"] as const);
/** Types of email addresses that may be generated by the credential generator */
export const EmailAlgorithms = Object.freeze(["catchall", "forwarder", "subaddress"] as const);
export const EmailAlgorithms = Object.freeze(["catchall", "forwarder", "subaddress", "addyio"] as const);
/** All types of credentials that may be generated by the credential generator */
export const CredentialAlgorithms = Object.freeze([

View File

@ -1,9 +1,11 @@
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
import { Randomizer } from "../abstractions";
import { EmailRandomizer, PasswordRandomizer, UsernameRandomizer } from "../engine";
import { Forwarder } from "../engine/forwarder";
import { AddyIoSettings } from "../integration";
import {
DefaultPolicyEvaluator,
DynamicPasswordPolicyConstraints,
@ -25,6 +27,7 @@ import {
CredentialGenerator,
CredentialGeneratorConfiguration,
EffUsernameGenerationOptions,
GeneratorDependencyProvider,
NoPolicy,
PassphraseGenerationOptions,
PassphraseGeneratorPolicy,
@ -33,6 +36,7 @@ import {
SubaddressGenerationOptions,
} from "../types";
import { DefaultAddyIoOptions } from "./default-addy-io-options";
import { DefaultCatchallOptions } from "./default-catchall-options";
import { DefaultEffUsernameOptions } from "./default-eff-username-options";
import { DefaultPassphraseBoundaries } from "./default-passphrase-boundaries";
@ -47,8 +51,8 @@ const PASSPHRASE = Object.freeze({
nameKey: "passphrase",
onlyOnRequest: false,
engine: {
create(randomizer: Randomizer): CredentialGenerator<PassphraseGenerationOptions> {
return new PasswordRandomizer(randomizer);
create(dependencies: GeneratorDependencyProvider): CredentialGenerator<PassphraseGenerationOptions> {
return new PasswordRandomizer(dependencies.randomizer);
},
},
settings: {
@ -84,8 +88,8 @@ const PASSWORD = Object.freeze({
nameKey: "password",
onlyOnRequest: false,
engine: {
create(randomizer: Randomizer): CredentialGenerator<PasswordGenerationOptions> {
return new PasswordRandomizer(randomizer);
create(dependencies: GeneratorDependencyProvider): CredentialGenerator<PasswordGenerationOptions> {
return new PasswordRandomizer(dependencies.randomizer);
},
},
settings: {
@ -129,8 +133,8 @@ const USERNAME = Object.freeze({
nameKey: "randomWord",
onlyOnRequest: false,
engine: {
create(randomizer: Randomizer): CredentialGenerator<EffUsernameGenerationOptions> {
return new UsernameRandomizer(randomizer);
create(dependencies: GeneratorDependencyProvider): CredentialGenerator<EffUsernameGenerationOptions> {
return new UsernameRandomizer(dependencies.randomizer);
},
},
settings: {
@ -160,8 +164,8 @@ const CATCHALL = Object.freeze({
descriptionKey: "catchallEmailDesc",
onlyOnRequest: false,
engine: {
create(randomizer: Randomizer): CredentialGenerator<CatchallGenerationOptions> {
return new EmailRandomizer(randomizer);
create(dependencies: GeneratorDependencyProvider): CredentialGenerator<CatchallGenerationOptions> {
return new EmailRandomizer(dependencies.randomizer);
},
},
settings: {
@ -191,8 +195,8 @@ const SUBADDRESS = Object.freeze({
descriptionKey: "plusAddressedEmailDesc",
onlyOnRequest: false,
engine: {
create(randomizer: Randomizer): CredentialGenerator<SubaddressGenerationOptions> {
return new EmailRandomizer(randomizer);
create(dependencies: GeneratorDependencyProvider): CredentialGenerator<SubaddressGenerationOptions> {
return new EmailRandomizer(dependencies.randomizer);
},
},
settings: {
@ -215,6 +219,42 @@ const SUBADDRESS = Object.freeze({
},
} satisfies CredentialGeneratorConfiguration<SubaddressGenerationOptions, NoPolicy>);
// FIXME: forwarders should dynamically extend generators; they shouldn't
// have their own entries. They're included here solely in order to quickly
// create generator configurations during UI modernization
const ADDYIO = Object.freeze({
id: "addyio",
category: "email",
nameKey: "addyIoKey",
onlyOnRequest: true,
engine: {
create(dependencies: GeneratorDependencyProvider) {
return new Forwarder(dependencies.client, dependencies.i18nService);
}
},
settings: {
initial: DefaultAddyIoOptions,
constraints: {},
account: new UserKeyDefinition<AddyIoSettings>(GENERATOR_DISK, "addyIoGenerator", {
deserializer: (value) => value,
clearOn: [],
})
},
policy: {
type: PolicyType.PasswordGenerator,
disabledValue: {},
combine(_acc: NoPolicy, _policy: Policy) {
return {};
},
createEvaluator(_policy: NoPolicy) {
return new DefaultPolicyEvaluator<AddyIoSettings>();
},
toConstraints(_policy: NoPolicy) {
return new IdentityConstraint<AddyIoSettings>();
},
}
} satisfies CredentialGeneratorConfiguration<AddyIoSettings, NoPolicy>);
/** Generator configurations */
export const Generators = Object.freeze({
/** Passphrase generator configuration */
@ -231,4 +271,6 @@ export const Generators = Object.freeze({
/** Email subaddress generator configuration */
subaddress: SUBADDRESS,
addyio: ADDYIO,
});

View File

@ -1,3 +1,7 @@
import { IntegrationId } from "@bitwarden/common/tools/integration";
import { ApiSettings } from "@bitwarden/common/tools/integration/rpc";
import { ForwarderConfiguration } from "../engine";
import { AddyIo } from "../integration/addy-io";
import { DuckDuckGo } from "../integration/duck-duck-go";
import { Fastmail } from "../integration/fastmail";
@ -13,3 +17,9 @@ export const Integrations = Object.freeze({
ForwardEmail,
SimpleLogin,
} as const);
const integrations = Object.fromEntries(Object.values(Integrations).map((i) => [i.id, i as ForwarderConfiguration<object>]));
export function getIntegration(id: IntegrationId) : ForwarderConfiguration<ApiSettings> {
return integrations[id as string]
}

View File

@ -0,0 +1,80 @@
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ApiSettings, IntegrationRequest, RestClient } from "@bitwarden/common/tools/integration/rpc";
import { GenerationRequest } from "@bitwarden/common/tools/types";
import { getIntegration } from "../data";
import {
CredentialGenerator,
GeneratedCredential,
} from "../types";
import { AccountRequest, ForwarderConfiguration } from "./forwarder-configuration";
import { ForwarderContext } from "./forwarder-context";
import { CreateForwardingAddressRpc, GetAccountIdRpc } from "./rpc";
/** Generation algorithms that produce randomized email addresses */
export class Forwarder
implements
CredentialGenerator<ApiSettings>
{
/** Instantiates the email randomizer
* @param random data source for random data
*/
constructor(private client: RestClient, private i18nService: I18nService) {}
async generate(
request: GenerationRequest,
settings: ApiSettings,
) {
if(!request.integration) {
throw new Error("Invalid integration request received by generator.");
}
const integration = getIntegration(request.integration);
if(!integration) {
throw new Error("Invalid integration request received by generator.");
}
const requestOptions: IntegrationRequest & AccountRequest = { website: request.website };
const getAccount = await this.getAccountId(integration, settings);
if (getAccount) {
requestOptions.accountId = await this.client.fetchJson(getAccount, requestOptions);
}
const create = this.createForwardingAddress(integration, settings);
const result = await this.client.fetchJson(create, requestOptions);
return new GeneratedCredential(result, "forwarder", Date.now());
}
private createContext<Settings>(
configuration: ForwarderConfiguration<Settings>,
settings: Settings,
) {
return new ForwarderContext(configuration, settings, this.i18nService);
}
private createForwardingAddress<Settings extends ApiSettings>(
configuration: ForwarderConfiguration<Settings>,
settings: Settings,
) {
const context = this.createContext(configuration, settings);
const rpc = new CreateForwardingAddressRpc<Settings>(configuration, context);
return rpc;
}
private getAccountId<Settings extends ApiSettings>(
configuration: ForwarderConfiguration<Settings>,
settings: Settings,
) {
if (!configuration.forwarder.getAccountId) {
return null;
}
const context = this.createContext(configuration, settings);
const rpc = new GetAccountIdRpc<Settings>(configuration, context);
return rpc;
}
}

View File

@ -22,12 +22,15 @@ import { Simplify } from "type-fest";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import {
OnDependency,
SingleUserDependency,
UserDependency,
} from "@bitwarden/common/tools/dependencies";
import { IntegrationId } from "@bitwarden/common/tools/integration";
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
import { isDynamic } from "@bitwarden/common/tools/state/state-constraints-dependency";
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
@ -42,7 +45,7 @@ import {
CredentialGeneratorInfo,
CredentialPreference,
} from "../types";
import { CredentialGeneratorConfiguration as Configuration } 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";
@ -59,6 +62,8 @@ type Generate$Dependencies = Simplify<Partial<OnDependency> & Partial<UserDepend
* When `website$` errors, the generator forwards the error.
*/
website$?: Observable<string>;
integration$?: Observable<IntegrationId>
};
type Algorithms$Dependencies = Partial<UserDependency>;
@ -68,8 +73,18 @@ export class CredentialGeneratorService {
private randomizer: Randomizer,
private stateProvider: StateProvider,
private policyService: PolicyService,
private client: RestClient,
private i18nService: I18nService
) {}
private getDependencyProvider() : GeneratorDependencyProvider {
return {
client: this.client,
i18nService: this.i18nService,
randomizer: this.randomizer
};
}
// FIXME: the rxjs methods of this service can be a lot more resilient if
// `Subjects` are introduced where sharing occurs
@ -84,11 +99,14 @@ export class CredentialGeneratorService {
dependencies?: Generate$Dependencies,
) {
// instantiate the engine
const engine = configuration.engine.create(this.randomizer);
const engine = configuration.engine.create(this.getDependencyProvider());
// stream blocks until all of these values are received
const website$ = dependencies?.website$ ?? new BehaviorSubject<string>(null);
const request$ = website$.pipe(map((website) => ({ website })));
const integration$ = dependencies?.integration$ ?? new BehaviorSubject<IntegrationId>(null);
const request$ = combineLatest([website$, integration$]).pipe(
map(([website, integration]) => ({ website, integration }))
);
const settings$ = this.settings$(configuration, dependencies);
// monitor completion

View File

@ -1,4 +1,6 @@
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
import { Constraints } from "@bitwarden/common/tools/types";
import { Randomizer } from "../abstractions";
@ -6,6 +8,12 @@ import { CredentialAlgorithm, CredentialCategory, PolicyConfiguration } from "..
import { CredentialGenerator } from "./credential-generator";
export type GeneratorDependencyProvider = {
randomizer: Randomizer,
client: RestClient,
i18nService: I18nService
};
/** Credential generator metadata common across credential generators */
export type CredentialGeneratorInfo = {
/** Uniquely identifies the credential configuration
@ -40,7 +48,7 @@ export type CredentialGeneratorConfiguration<Settings, Policy> = CredentialGener
// the credential generator, but engine configurations should return
// the underlying type. `create` may be able to do double-duty w/ an
// engine definition if `CredentialGenerator` can be made covariant.
create: (randomizer: Randomizer) => CredentialGenerator<Settings>;
create: (randomizer: GeneratorDependencyProvider) => CredentialGenerator<Settings>;
};
/** Defines the stored parameters for credential generation */
settings: {