Skip to content

Commit

Permalink
[PM-5974] introduce ForwarderGeneratorStrategy (#8207)
Browse files Browse the repository at this point in the history
* update defaults to include `website` parameter
* update utilities tests to include `website` parameter
  • Loading branch information
audreyality authored Mar 7, 2024
1 parent a5c78fb commit c731831
Show file tree
Hide file tree
Showing 19 changed files with 610 additions and 156 deletions.
54 changes: 54 additions & 0 deletions libs/common/src/tools/generator/key-definition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import {
SUBADDRESS_SETTINGS,
PASSPHRASE_SETTINGS,
PASSWORD_SETTINGS,
SIMPLE_LOGIN_FORWARDER,
FORWARD_EMAIL_FORWARDER,
FIREFOX_RELAY_FORWARDER,
FASTMAIL_FORWARDER,
DUCK_DUCK_GO_FORWARDER,
ADDY_IO_FORWARDER,
} from "./key-definitions";

describe("Key definitions", () => {
Expand Down Expand Up @@ -48,6 +54,54 @@ describe("Key definitions", () => {
});
});

describe("ADDY_IO_FORWARDER", () => {
it("should pass through deserialization", () => {
const value: any = {};
const result = ADDY_IO_FORWARDER.deserializer(value);
expect(result).toBe(value);
});
});

describe("DUCK_DUCK_GO_FORWARDER", () => {
it("should pass through deserialization", () => {
const value: any = {};
const result = DUCK_DUCK_GO_FORWARDER.deserializer(value);
expect(result).toBe(value);
});
});

describe("FASTMAIL_FORWARDER", () => {
it("should pass through deserialization", () => {
const value: any = {};
const result = FASTMAIL_FORWARDER.deserializer(value);
expect(result).toBe(value);
});
});

describe("FIREFOX_RELAY_FORWARDER", () => {
it("should pass through deserialization", () => {
const value: any = {};
const result = FIREFOX_RELAY_FORWARDER.deserializer(value);
expect(result).toBe(value);
});
});

describe("FORWARD_EMAIL_FORWARDER", () => {
it("should pass through deserialization", () => {
const value: any = {};
const result = FORWARD_EMAIL_FORWARDER.deserializer(value);
expect(result).toBe(value);
});
});

describe("SIMPLE_LOGIN_FORWARDER", () => {
it("should pass through deserialization", () => {
const value: any = {};
const result = SIMPLE_LOGIN_FORWARDER.deserializer(value);
expect(result).toBe(value);
});
});

describe("ENCRYPTED_HISTORY", () => {
it("should pass through deserialization", () => {
const value = {};
Expand Down
54 changes: 54 additions & 0 deletions libs/common/src/tools/generator/key-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { GeneratedPasswordHistory } from "./password/generated-password-history"
import { PasswordGenerationOptions } from "./password/password-generation-options";
import { CatchallGenerationOptions } from "./username/catchall-generator-options";
import { EffUsernameGenerationOptions } from "./username/eff-username-generator-options";
import {
ApiOptions,
EmailDomainOptions,
EmailPrefixOptions,
SelfHostedApiOptions,
} from "./username/options/forwarder-options";
import { SubaddressGenerationOptions } from "./username/subaddress-generator-options";

/** plaintext password generation options */
Expand Down Expand Up @@ -52,6 +58,54 @@ export const SUBADDRESS_SETTINGS = new KeyDefinition<SubaddressGenerationOptions
},
);

export const ADDY_IO_FORWARDER = new KeyDefinition<SelfHostedApiOptions & EmailDomainOptions>(
GENERATOR_DISK,
"addyIoForwarder",
{
deserializer: (value) => value,
},
);

export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition<ApiOptions>(
GENERATOR_DISK,
"duckDuckGoForwarder",
{
deserializer: (value) => value,
},
);

export const FASTMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailPrefixOptions>(
GENERATOR_DISK,
"fastmailForwarder",
{
deserializer: (value) => value,
},
);

export const FIREFOX_RELAY_FORWARDER = new KeyDefinition<ApiOptions>(
GENERATOR_DISK,
"firefoxRelayForwarder",
{
deserializer: (value) => value,
},
);

export const FORWARD_EMAIL_FORWARDER = new KeyDefinition<ApiOptions & EmailDomainOptions>(
GENERATOR_DISK,
"forwardEmailForwarder",
{
deserializer: (value) => value,
},
);

export const SIMPLE_LOGIN_FORWARDER = new KeyDefinition<SelfHostedApiOptions>(
GENERATOR_DISK,
"simpleLoginForwarder",
{
deserializer: (value) => value,
},
);

/** encrypted password generation history */
export const ENCRYPTED_HISTORY = new KeyDefinition<GeneratedPasswordHistory>(
GENERATOR_DISK,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { mock } from "jest-mock-extended";

import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
import { DUCK_DUCK_GO_FORWARDER } from "../key-definitions";
import { SecretState } from "../state/secret-state";

import { ForwarderGeneratorStrategy } from "./forwarder-generator-strategy";
import { ApiOptions } from "./options/forwarder-options";

class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> {
constructor(
encryptService: EncryptService,
keyService: CryptoService,
stateProvider: StateProvider,
) {
super(encryptService, keyService, stateProvider);
}

get key() {
// arbitrary.
return DUCK_DUCK_GO_FORWARDER;
}
}

const SomeUser = "some user" as UserId;
const AnotherUser = "another user" as UserId;

describe("ForwarderGeneratorStrategy", () => {
const encryptService = mock<EncryptService>();
const keyService = mock<CryptoService>();
const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser));

describe("durableState", () => {
it("constructs a secret state", () => {
const strategy = new TestForwarder(encryptService, keyService, stateProvider);

const result = strategy.durableState(SomeUser);

expect(result).toBeInstanceOf(SecretState);
});

it("returns the same secret state for a single user", () => {
const strategy = new TestForwarder(encryptService, keyService, stateProvider);

const firstResult = strategy.durableState(SomeUser);
const secondResult = strategy.durableState(SomeUser);

expect(firstResult).toBe(secondResult);
});

it("returns a different secret state for a different user", () => {
const strategy = new TestForwarder(encryptService, keyService, stateProvider);

const firstResult = strategy.durableState(SomeUser);
const secondResult = strategy.durableState(AnotherUser);

expect(firstResult).not.toBe(secondResult);
});
});

it("evaluator returns the default policy evaluator", () => {
const strategy = new TestForwarder(null, null, null);

const result = strategy.evaluator(null);

expect(result).toBeInstanceOf(DefaultPolicyEvaluator);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { PolicyType } from "../../../admin-console/enums";
import { Policy } from "../../../admin-console/models/domain/policy";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { KeyDefinition, StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { GeneratorStrategy } from "../abstractions";
import { DefaultPolicyEvaluator } from "../default-policy-evaluator";
import { NoPolicy } from "../no-policy";
import { PaddedDataPacker } from "../state/padded-data-packer";
import { SecretClassifier } from "../state/secret-classifier";
import { SecretState } from "../state/secret-state";
import { UserKeyEncryptor } from "../state/user-key-encryptor";

import { ApiOptions } from "./options/forwarder-options";

const ONE_MINUTE = 60 * 1000;
const OPTIONS_FRAME_SIZE = 512;

/** An email forwarding service configurable through an API. */
export abstract class ForwarderGeneratorStrategy<
Options extends ApiOptions,
> extends GeneratorStrategy<Options, NoPolicy> {
/** Initializes the generator strategy
* @param encryptService protects sensitive forwarder options
* @param keyService looks up the user key when protecting data.
* @param stateProvider creates the durable state for options storage
*/
constructor(
private readonly encryptService: EncryptService,
private readonly keyService: CryptoService,
private stateProvider: StateProvider,
) {
super();
// Uses password generator since there aren't policies
// specific to usernames.
this.policy = PolicyType.PasswordGenerator;

this.cache_ms = ONE_MINUTE;
}

private durableStates = new Map<UserId, SecretState<Options, Record<string, never>>>();

/** {@link GeneratorStrategy.durableState} */
durableState = (userId: UserId) => {
let state = this.durableStates.get(userId);

if (!state) {
const encryptor = this.createEncryptor();
state = SecretState.from(userId, this.key, this.stateProvider, encryptor);
this.durableStates.set(userId, state);
}

return state;
};

private createEncryptor() {
// always exclude request properties
const classifier = SecretClassifier.allSecret<Options>().exclude("website");

// construct the encryptor
const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE);
return new UserKeyEncryptor(this.encryptService, this.keyService, classifier, packer);
}

/** Determine where forwarder configuration is stored */
protected abstract readonly key: KeyDefinition<Options>;

/** {@link GeneratorStrategy.evaluator} */
evaluator = (_policy: Policy) => {
return new DefaultPolicyEvaluator<Options>();
};
}
Loading

0 comments on commit c731831

Please sign in to comment.