1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-16793] port credential generator service to providers (#14071)

* introduce extension service
* deprecate legacy forwarder types
* eliminate repeat algorithm emissions
* extend logging to preference management
* align forwarder ids with vendor ids
* fix duplicate policy emissions; debugging required logger enhancements

-----

Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
This commit is contained in:
✨ Audrey ✨
2025-05-27 09:51:14 -04:00
committed by GitHub
parent f4f659c52a
commit 97a591e738
140 changed files with 3720 additions and 4085 deletions

View File

@@ -0,0 +1,63 @@
import { IntegrationId } from "@bitwarden/common/tools/integration";
export type ForwarderId = IntegrationId;
/** Metadata format for email forwarding services. */
export type ForwarderMetadata = {
/** The unique identifier for the forwarder. */
id: ForwarderId;
/** The name of the service the forwarder queries. */
name: string;
/** Whether the forwarder is valid for self-hosted instances of Bitwarden. */
validForSelfHosted: boolean;
};
/** Metadata about an email forwarding service.
* @remarks This is used to populate the forwarder selection list
* and to identify forwarding services in error messages.
*/
export const Forwarders = Object.freeze({
/** For https://addy.io/ */
AddyIo: Object.freeze({
id: "anonaddy",
name: "Addy.io",
validForSelfHosted: true,
} as ForwarderMetadata),
/** For https://duckduckgo.com/email/ */
DuckDuckGo: Object.freeze({
id: "duckduckgo",
name: "DuckDuckGo",
validForSelfHosted: false,
} as ForwarderMetadata),
/** For https://www.fastmail.com. */
Fastmail: Object.freeze({
id: "fastmail",
name: "Fastmail",
validForSelfHosted: true,
} as ForwarderMetadata),
/** For https://relay.firefox.com/ */
FirefoxRelay: Object.freeze({
id: "firefoxrelay",
name: "Firefox Relay",
validForSelfHosted: false,
} as ForwarderMetadata),
/** For https://forwardemail.net/ */
ForwardEmail: Object.freeze({
id: "forwardemail",
name: "Forward Email",
validForSelfHosted: true,
} as ForwarderMetadata),
/** For https://simplelogin.io/ */
SimpleLogin: Object.freeze({
id: "simplelogin",
name: "SimpleLogin",
validForSelfHosted: true,
} as ForwarderMetadata),
});

View File

@@ -1,13 +1,12 @@
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { IntegrationId } from "@bitwarden/common/tools/integration";
import { VendorId } from "@bitwarden/common/tools/extension";
import { UserId } from "@bitwarden/common/types/guid";
import {
GeneratorService,
DefaultPassphraseGenerationOptions,
DefaultPasswordGenerationOptions,
Policies,
PassphraseGenerationOptions,
PassphraseGeneratorPolicy,
PasswordGenerationOptions,
@@ -38,12 +37,17 @@ const PasswordGeneratorOptionsEvaluator = policies.PasswordGeneratorOptionsEvalu
function createPassphraseGenerator(
options: PassphraseGenerationOptions = {},
policy: PassphraseGeneratorPolicy = Policies.Passphrase.disabledValue,
policy?: PassphraseGeneratorPolicy,
) {
let savedOptions = options;
const generator = mock<GeneratorService<PassphraseGenerationOptions, PassphraseGeneratorPolicy>>({
evaluator$(id: UserId) {
const evaluator = new PassphraseGeneratorOptionsEvaluator(policy);
const active = policy ?? {
minNumberWords: 0,
capitalize: false,
includeNumber: false,
};
const evaluator = new PassphraseGeneratorOptionsEvaluator(active);
return of(evaluator);
},
options$(id: UserId) {
@@ -63,12 +67,21 @@ function createPassphraseGenerator(
function createPasswordGenerator(
options: PasswordGenerationOptions = {},
policy: PasswordGeneratorPolicy = Policies.Password.disabledValue,
policy?: PasswordGeneratorPolicy,
) {
let savedOptions = options;
const generator = mock<GeneratorService<PasswordGenerationOptions, PasswordGeneratorPolicy>>({
evaluator$(id: UserId) {
const evaluator = new PasswordGeneratorOptionsEvaluator(policy);
const active = policy ?? {
minLength: 0,
useUppercase: false,
useLowercase: false,
useNumbers: false,
numberCount: 0,
useSpecial: false,
specialCount: 0,
};
const evaluator = new PasswordGeneratorOptionsEvaluator(active);
return of(evaluator);
},
options$(id: UserId) {
@@ -118,7 +131,13 @@ describe("LegacyPasswordGenerationService", () => {
describe("generatePassword", () => {
it("invokes the inner password generator to generate passwords", async () => {
const innerPassword = createPasswordGenerator();
const generator = new LegacyPasswordGenerationService(null, null, innerPassword, null, null);
const generator = new LegacyPasswordGenerationService(
null!,
null!,
innerPassword,
null!,
null!,
);
const options = { type: "password" } as PasswordGeneratorOptions;
await generator.generatePassword(options);
@@ -129,11 +148,11 @@ describe("LegacyPasswordGenerationService", () => {
it("invokes the inner passphrase generator to generate passphrases", async () => {
const innerPassphrase = createPassphraseGenerator();
const generator = new LegacyPasswordGenerationService(
null,
null,
null,
null!,
null!,
null!,
innerPassphrase,
null,
null!,
);
const options = { type: "passphrase" } as PasswordGeneratorOptions;
@@ -147,11 +166,11 @@ describe("LegacyPasswordGenerationService", () => {
it("invokes the inner passphrase generator", async () => {
const innerPassphrase = createPassphraseGenerator();
const generator = new LegacyPasswordGenerationService(
null,
null,
null,
null!,
null!,
null!,
innerPassphrase,
null,
null!,
);
const options = {} as PasswordGeneratorOptions;
@@ -185,7 +204,7 @@ describe("LegacyPasswordGenerationService", () => {
const navigation = createNavigationGenerator({
type: "passphrase",
username: "word",
forwarder: "simplelogin" as IntegrationId,
forwarder: "simplelogin" as VendorId,
});
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
@@ -193,7 +212,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const [result] = await generator.getOptions();
@@ -220,16 +239,16 @@ describe("LegacyPasswordGenerationService", () => {
});
it("sets default options when an inner service lacks a value", async () => {
const innerPassword = createPasswordGenerator(null);
const innerPassphrase = createPassphraseGenerator(null);
const navigation = createNavigationGenerator(null);
const innerPassword = createPasswordGenerator(null!);
const innerPassphrase = createPassphraseGenerator(null!);
const navigation = createNavigationGenerator(null!);
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
accountService,
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const [result] = await generator.getOptions();
@@ -277,7 +296,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const [, policy] = await generator.getOptions();
@@ -323,7 +342,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
@@ -363,7 +382,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options);
@@ -409,7 +428,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({});
@@ -441,7 +460,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const options = {
type: "password" as const,
@@ -474,7 +493,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const options = {
type: "passphrase" as const,
@@ -496,7 +515,7 @@ describe("LegacyPasswordGenerationService", () => {
const navigation = createNavigationGenerator({
type: "password",
username: "forwarded",
forwarder: "firefoxrelay" as IntegrationId,
forwarder: "firefoxrelay" as VendorId,
});
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
@@ -504,7 +523,7 @@ describe("LegacyPasswordGenerationService", () => {
navigation,
innerPassword,
innerPassphrase,
null,
null!,
);
const options = {
type: "passphrase" as const,
@@ -533,9 +552,9 @@ describe("LegacyPasswordGenerationService", () => {
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
accountService,
null,
null,
null,
null!,
null!,
null!,
history,
);
@@ -552,9 +571,9 @@ describe("LegacyPasswordGenerationService", () => {
const accountService = mockAccountServiceWith(SomeUser);
const generator = new LegacyPasswordGenerationService(
accountService,
null,
null,
null,
null!,
null!,
null!,
history,
);

View File

@@ -1,6 +1,15 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { AddyIo } from "@bitwarden/common/tools/extension/vendor/addyio";
import { DuckDuckGo } from "@bitwarden/common/tools/extension/vendor/duckduckgo";
import { Fastmail } from "@bitwarden/common/tools/extension/vendor/fastmail";
import { ForwardEmail } from "@bitwarden/common/tools/extension/vendor/forwardemail";
import { Mozilla } from "@bitwarden/common/tools/extension/vendor/mozilla";
import { SimpleLogin } from "@bitwarden/common/tools/extension/vendor/simplelogin";
import { IntegrationId } from "@bitwarden/common/tools/integration";
import { UserId } from "@bitwarden/common/types/guid";
import {
ApiOptions,
@@ -13,13 +22,6 @@ import {
DefaultCatchallOptions,
DefaultEffUsernameOptions,
EffUsernameGenerationOptions,
DefaultAddyIoOptions,
DefaultDuckDuckGoOptions,
DefaultFastmailOptions,
DefaultFirefoxRelayOptions,
DefaultForwardEmailOptions,
DefaultSimpleLoginOptions,
Forwarders,
DefaultSubaddressOptions,
SubaddressGenerationOptions,
policies,
@@ -169,7 +171,7 @@ describe("LegacyUsernameGenerationService", () => {
// set up an arbitrary forwarder for the username test; all forwarders tested in their own tests
const options = {
type: "forwarded",
forwardedService: Forwarders.AddyIo.id,
forwardedService: AddyIo.id,
} as UsernameGeneratorOptions;
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, null);
addyIo.generate.mockResolvedValue("addyio@example.com");
@@ -249,7 +251,7 @@ describe("LegacyUsernameGenerationService", () => {
describe("generateForwarded", () => {
it("should generate a AddyIo username", async () => {
const options = {
forwardedService: Forwarders.AddyIo.id,
forwardedService: AddyIo.id,
forwardedAnonAddyApiToken: "token",
forwardedAnonAddyBaseUrl: "https://example.com",
forwardedAnonAddyDomain: "example.com",
@@ -284,7 +286,7 @@ describe("LegacyUsernameGenerationService", () => {
it("should generate a DuckDuckGo username", async () => {
const options = {
forwardedService: Forwarders.DuckDuckGo.id,
forwardedService: DuckDuckGo.id,
forwardedDuckDuckGoToken: "token",
website: "example.com",
} as UsernameGeneratorOptions;
@@ -315,7 +317,7 @@ describe("LegacyUsernameGenerationService", () => {
it("should generate a Fastmail username", async () => {
const options = {
forwardedService: Forwarders.Fastmail.id,
forwardedService: Fastmail.id,
forwardedFastmailApiToken: "token",
website: "example.com",
} as UsernameGeneratorOptions;
@@ -346,7 +348,7 @@ describe("LegacyUsernameGenerationService", () => {
it("should generate a FirefoxRelay username", async () => {
const options = {
forwardedService: Forwarders.FirefoxRelay.id,
forwardedService: Mozilla.id,
forwardedFirefoxApiToken: "token",
website: "example.com",
} as UsernameGeneratorOptions;
@@ -377,7 +379,7 @@ describe("LegacyUsernameGenerationService", () => {
it("should generate a ForwardEmail username", async () => {
const options = {
forwardedService: Forwarders.ForwardEmail.id,
forwardedService: ForwardEmail.id,
forwardedForwardEmailApiToken: "token",
forwardedForwardEmailDomain: "example.com",
website: "example.com",
@@ -410,7 +412,7 @@ describe("LegacyUsernameGenerationService", () => {
it("should generate a SimpleLogin username", async () => {
const options = {
forwardedService: Forwarders.SimpleLogin.id,
forwardedService: SimpleLogin.id,
forwardedSimpleLoginApiKey: "token",
forwardedSimpleLoginBaseUrl: "https://example.com",
website: "example.com",
@@ -449,7 +451,7 @@ describe("LegacyUsernameGenerationService", () => {
const navigation = createNavigationGenerator({
type: "username",
username: "catchall",
forwarder: Forwarders.AddyIo.id,
forwarder: AddyIo.id,
});
const catchall = createGenerator<CatchallGenerationOptions>(
@@ -557,7 +559,7 @@ describe("LegacyUsernameGenerationService", () => {
subaddressEmail: "foo@example.com",
catchallType: "random",
catchallDomain: "example.com",
forwardedService: Forwarders.AddyIo.id,
forwardedService: AddyIo.id,
forwardedAnonAddyApiToken: "addyIoToken",
forwardedAnonAddyDomain: "addyio.example.com",
forwardedAnonAddyBaseUrl: "https://addyio.api.example.com",
@@ -583,21 +585,36 @@ describe("LegacyUsernameGenerationService", () => {
null,
DefaultSubaddressOptions,
);
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(
null,
DefaultAddyIoOptions,
);
const duckDuckGo = createGenerator<ApiOptions>(null, DefaultDuckDuckGoOptions);
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(
null,
DefaultFastmailOptions,
);
const firefoxRelay = createGenerator<ApiOptions>(null, DefaultFirefoxRelayOptions);
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(
null,
DefaultForwardEmailOptions,
);
const simpleLogin = createGenerator<SelfHostedApiOptions>(null, DefaultSimpleLoginOptions);
const addyIo = createGenerator<SelfHostedApiOptions & EmailDomainOptions>(null, {
website: null,
baseUrl: "https://app.addy.io",
token: "",
domain: "",
});
const duckDuckGo = createGenerator<ApiOptions>(null, {
website: null,
token: "",
});
const fastmail = createGenerator<ApiOptions & EmailPrefixOptions>(null, {
website: "",
domain: "",
prefix: "",
token: "",
});
const firefoxRelay = createGenerator<ApiOptions>(null, {
website: null,
token: "",
});
const forwardEmail = createGenerator<ApiOptions & EmailDomainOptions>(null, {
website: null,
token: "",
domain: "",
});
const simpleLogin = createGenerator<SelfHostedApiOptions>(null, {
website: null,
baseUrl: "https://app.simplelogin.io",
token: "",
});
const generator = new LegacyUsernameGenerationService(
account,
@@ -624,16 +641,16 @@ describe("LegacyUsernameGenerationService", () => {
subaddressType: DefaultSubaddressOptions.subaddressType,
subaddressEmail: DefaultSubaddressOptions.subaddressEmail,
forwardedService: DefaultGeneratorNavigation.forwarder,
forwardedAnonAddyApiToken: DefaultAddyIoOptions.token,
forwardedAnonAddyDomain: DefaultAddyIoOptions.domain,
forwardedAnonAddyBaseUrl: DefaultAddyIoOptions.baseUrl,
forwardedDuckDuckGoToken: DefaultDuckDuckGoOptions.token,
forwardedFastmailApiToken: DefaultFastmailOptions.token,
forwardedFirefoxApiToken: DefaultFirefoxRelayOptions.token,
forwardedForwardEmailApiToken: DefaultForwardEmailOptions.token,
forwardedForwardEmailDomain: DefaultForwardEmailOptions.domain,
forwardedSimpleLoginApiKey: DefaultSimpleLoginOptions.token,
forwardedSimpleLoginBaseUrl: DefaultSimpleLoginOptions.baseUrl,
forwardedAnonAddyApiToken: "",
forwardedAnonAddyDomain: "",
forwardedAnonAddyBaseUrl: "https://app.addy.io",
forwardedDuckDuckGoToken: "",
forwardedFastmailApiToken: "",
forwardedFirefoxApiToken: "",
forwardedForwardEmailApiToken: "",
forwardedForwardEmailDomain: "",
forwardedSimpleLoginApiKey: "",
forwardedSimpleLoginBaseUrl: "https://app.simplelogin.io",
});
});
});
@@ -678,7 +695,7 @@ describe("LegacyUsernameGenerationService", () => {
subaddressEmail: "foo@example.com",
catchallType: "random",
catchallDomain: "example.com",
forwardedService: Forwarders.AddyIo.id,
forwardedService: AddyIo.id as IntegrationId,
forwardedAnonAddyApiToken: "addyIoToken",
forwardedAnonAddyDomain: "addyio.example.com",
forwardedAnonAddyBaseUrl: "https://addyio.api.example.com",
@@ -697,7 +714,7 @@ describe("LegacyUsernameGenerationService", () => {
expect(navigation.saveOptions).toHaveBeenCalledWith(SomeUser, {
type: "password",
username: "catchall",
forwarder: Forwarders.AddyIo.id,
forwarder: AddyIo.id,
});
expect(catchall.saveOptions).toHaveBeenCalledWith(SomeUser, {

View File

@@ -3,6 +3,7 @@
import { zip, firstValueFrom, map, concatMap, combineLatest } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { Vendor } from "@bitwarden/common/tools/extension/vendor/data";
import { IntegrationRequest } from "@bitwarden/common/tools/integration/rpc";
import { UserId } from "@bitwarden/common/types/guid";
import {
@@ -14,13 +15,13 @@ import {
GeneratorService,
CatchallGenerationOptions,
EffUsernameGenerationOptions,
Forwarders,
SubaddressGenerationOptions,
UsernameGeneratorType,
ForwarderId,
} from "@bitwarden/generator-core";
import { GeneratorNavigationService, GeneratorNavigation } from "@bitwarden/generator-navigation";
import { Forwarders } from "./forwarders";
import { UsernameGeneratorOptions } from "./username-generation-options";
import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction";
@@ -89,12 +90,14 @@ export class LegacyUsernameGenerationService implements UsernameGenerationServic
const stored = this.toStoredOptions(options);
switch (options.forwardedService) {
case Forwarders.AddyIo.id:
case Vendor.addyio:
return this.addyIo.generate(stored.forwarders.addyIo);
case Forwarders.DuckDuckGo.id:
return this.duckDuckGo.generate(stored.forwarders.duckDuckGo);
case Forwarders.Fastmail.id:
return this.fastmail.generate(stored.forwarders.fastmail);
case Forwarders.FirefoxRelay.id:
case Vendor.mozilla:
return this.firefoxRelay.generate(stored.forwarders.firefoxRelay);
case Forwarders.ForwardEmail.id:
return this.forwardEmail.generate(stored.forwarders.forwardEmail);
@@ -232,22 +235,24 @@ export class LegacyUsernameGenerationService implements UsernameGenerationServic
options: MappedOptions,
) {
switch (forwarder) {
case "anonaddy":
case Forwarders.AddyIo.id:
case Vendor.addyio:
await this.addyIo.saveOptions(account, options.forwarders.addyIo);
return true;
case "duckduckgo":
case Forwarders.DuckDuckGo.id:
await this.duckDuckGo.saveOptions(account, options.forwarders.duckDuckGo);
return true;
case "fastmail":
case Forwarders.Fastmail.id:
await this.fastmail.saveOptions(account, options.forwarders.fastmail);
return true;
case "firefoxrelay":
case Forwarders.FirefoxRelay.id:
case Vendor.mozilla:
await this.firefoxRelay.saveOptions(account, options.forwarders.firefoxRelay);
return true;
case "forwardemail":
case Forwarders.ForwardEmail.id:
await this.forwardEmail.saveOptions(account, options.forwarders.forwardEmail);
return true;
case "simplelogin":
case Forwarders.SimpleLogin.id:
await this.simpleLogin.saveOptions(account, options.forwarders.simpleLogin);
return true;
default: