Handle missing encryption key for the EncryptedMetadataManager (#159)
* Handle missing encryption key for the EncryptedMetadataManager * Fancy up the documentation * Throw an error instead of process exit
This commit is contained in:
parent
5a4d316228
commit
51284efa00
3 changed files with 128 additions and 54 deletions
|
@ -66,3 +66,19 @@ const settings = new MetadataManager({
|
||||||
mutateMetadata: (md) => mutateMetadata(client, md),
|
mutateMetadata: (md) => mutateMetadata(client, md),
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# EncryptedMetadataManager
|
||||||
|
|
||||||
|
This manager encrypts add the layer of encryption for all the stored data.
|
||||||
|
To operate correctly, the encryption key needs to be passed to the constructor:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
new EncryptedMetadataManager({
|
||||||
|
encryptionKey: process.env.SECRET_KEY, // secrets should be saved in the environment variables, never in the source code
|
||||||
|
fetchMetadata: () => fetchAllMetadata(client),
|
||||||
|
mutateMetadata: (metadata) => mutateMetadata(client, metadata),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
> If encryption key won't be passed, the application will exit.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DecryptCallback,
|
DecryptCallback,
|
||||||
|
@ -19,66 +19,109 @@ const entryForDomainY = { key: "a__y.com", value: "domain y value" };
|
||||||
const metadata = [initialEntry, entryForDomainX, entryForDomainY, encryptedEntry];
|
const metadata = [initialEntry, entryForDomainX, entryForDomainY, encryptedEntry];
|
||||||
|
|
||||||
describe("settings-manager", () => {
|
describe("settings-manager", () => {
|
||||||
describe("metadata-manager", () => {
|
describe("encrypted-metadata-manager", () => {
|
||||||
const fetchMock = vi.fn(async () => metadata);
|
describe("Constructor", () => {
|
||||||
const mutateMock = vi.fn(async (md: MetadataEntry[]) => [...metadata, ...md]);
|
const initialEnv = { ...process.env };
|
||||||
const manager = new EncryptedMetadataManager({
|
|
||||||
fetchMetadata: fetchMock,
|
afterEach(() => {
|
||||||
mutateMetadata: mutateMock,
|
process.env = { ...initialEnv };
|
||||||
encryptionKey: "key",
|
vi.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Process exit should be called when no encryption key is set if the environment type is production", async () => {
|
||||||
|
// @ts-expect-error
|
||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
const fetchMock = vi.fn(async () => metadata);
|
||||||
|
const mutateMock = vi.fn(async (md: MetadataEntry[]) => [...metadata, ...md]);
|
||||||
|
expect(
|
||||||
|
() =>
|
||||||
|
new EncryptedMetadataManager({
|
||||||
|
fetchMetadata: fetchMock,
|
||||||
|
mutateMetadata: mutateMock,
|
||||||
|
// @ts-expect-error
|
||||||
|
encryptionKey: undefined,
|
||||||
|
})
|
||||||
|
).toThrowError(
|
||||||
|
"Encryption key for the EncryptedMetadataManager has not been set. Setting it for the production environments is necessary. You can find more in the documentation: https://github.com/saleor/saleor-app-sdk/blob/main/docs/settings-manager.md"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("If env type is different than production (development/test) use placeholder value", async () => {
|
||||||
|
const fetchMock = vi.fn(async () => metadata);
|
||||||
|
const mutateMock = vi.fn(async (md: MetadataEntry[]) => [...metadata, ...md]);
|
||||||
|
const manager = new EncryptedMetadataManager({
|
||||||
|
fetchMetadata: fetchMock,
|
||||||
|
mutateMetadata: mutateMock,
|
||||||
|
// @ts-expect-error
|
||||||
|
encryptionKey: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(manager.encryptionKey).toBe("CHANGE_ME");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
describe("Manager operations", () => {
|
||||||
vi.restoreAllMocks();
|
const fetchMock = vi.fn(async () => metadata);
|
||||||
});
|
const mutateMock = vi.fn(async (md: MetadataEntry[]) => [...metadata, ...md]);
|
||||||
|
const manager = new EncryptedMetadataManager({
|
||||||
it("Set encrypted value in the metadata", async () => {
|
|
||||||
const newEntry = { key: "new", value: "new value" };
|
|
||||||
|
|
||||||
await manager.set(newEntry);
|
|
||||||
const mutateValue = mutateMock.mock.lastCall![0][0].value;
|
|
||||||
|
|
||||||
// Encrypted value should be encrypted, alphanumeric value and different than input
|
|
||||||
expect(mutateValue).toMatch(/^[\d\w]+$/);
|
|
||||||
expect(mutateValue).not.toEqual(newEntry.key);
|
|
||||||
// Set method should populate cache with updated values, so fetch is never called
|
|
||||||
expect(fetchMock).toBeCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Get encrypted data from metadata", async () => {
|
|
||||||
const value = await manager.get(encryptedEntry.key);
|
|
||||||
expect(value).toMatch("new value");
|
|
||||||
// make sure encrypted metadata is different than decrypted value
|
|
||||||
expect(value).not.toEqual(encryptedEntry.value);
|
|
||||||
expect(fetchMock).toBeCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Use custom encryption callbacks", async () => {
|
|
||||||
const newEntry = { key: "new", value: "new value" };
|
|
||||||
|
|
||||||
// dummy encryption - join value and string together
|
|
||||||
const customEncrypt: EncryptCallback = (value, secret) => value + secret;
|
|
||||||
// dummy decryption - remove secret from end of the "encrypted" value
|
|
||||||
const customDecrypt: DecryptCallback = (value, secret) =>
|
|
||||||
value.substr(0, value.length - secret.length);
|
|
||||||
|
|
||||||
const customManager = new EncryptedMetadataManager({
|
|
||||||
fetchMetadata: fetchMock,
|
fetchMetadata: fetchMock,
|
||||||
mutateMetadata: mutateMock,
|
mutateMetadata: mutateMock,
|
||||||
encryptionKey: "key",
|
encryptionKey: "key",
|
||||||
encryptionMethod: customEncrypt,
|
|
||||||
decryptionMethod: customDecrypt,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await customManager.set(newEntry);
|
beforeEach(() => {
|
||||||
// value send to the API should be encrypted with custom method
|
vi.restoreAllMocks();
|
||||||
const mutateValue = mutateMock.mock.lastCall![0][0].value;
|
});
|
||||||
expect(mutateValue).toMatch("new valuekey");
|
|
||||||
|
|
||||||
// value from get should be "decrypted" using custom method
|
it("Set encrypted value in the metadata", async () => {
|
||||||
expect(await customManager.get(newEntry.key)).toMatch("new value");
|
const newEntry = { key: "new", value: "new value" };
|
||||||
// Set method should populate cache with updated values, so fetch is never called
|
|
||||||
expect(fetchMock).toBeCalledTimes(0);
|
await manager.set(newEntry);
|
||||||
|
const mutateValue = mutateMock.mock.lastCall![0][0].value;
|
||||||
|
|
||||||
|
// Encrypted value should be encrypted, alphanumeric value and different than input
|
||||||
|
expect(mutateValue).toMatch(/^[\d\w]+$/);
|
||||||
|
expect(mutateValue).not.toEqual(newEntry.key);
|
||||||
|
// Set method should populate cache with updated values, so fetch is never called
|
||||||
|
expect(fetchMock).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Get encrypted data from metadata", async () => {
|
||||||
|
const value = await manager.get(encryptedEntry.key);
|
||||||
|
expect(value).toMatch("new value");
|
||||||
|
// make sure encrypted metadata is different than decrypted value
|
||||||
|
expect(value).not.toEqual(encryptedEntry.value);
|
||||||
|
expect(fetchMock).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Use custom encryption callbacks", async () => {
|
||||||
|
const newEntry = { key: "new", value: "new value" };
|
||||||
|
|
||||||
|
// dummy encryption - join value and string together
|
||||||
|
const customEncrypt: EncryptCallback = (value, secret) => value + secret;
|
||||||
|
// dummy decryption - remove secret from end of the "encrypted" value
|
||||||
|
const customDecrypt: DecryptCallback = (value, secret) =>
|
||||||
|
value.substr(0, value.length - secret.length);
|
||||||
|
|
||||||
|
const customManager = new EncryptedMetadataManager({
|
||||||
|
fetchMetadata: fetchMock,
|
||||||
|
mutateMetadata: mutateMock,
|
||||||
|
encryptionKey: "key",
|
||||||
|
encryptionMethod: customEncrypt,
|
||||||
|
decryptionMethod: customDecrypt,
|
||||||
|
});
|
||||||
|
|
||||||
|
await customManager.set(newEntry);
|
||||||
|
// value send to the API should be encrypted with custom method
|
||||||
|
const mutateValue = mutateMock.mock.lastCall![0][0].value;
|
||||||
|
expect(mutateValue).toMatch("new valuekey");
|
||||||
|
|
||||||
|
// value from get should be "decrypted" using custom method
|
||||||
|
expect(await customManager.get(newEntry.key)).toMatch("new value");
|
||||||
|
// Set method should populate cache with updated values, so fetch is never called
|
||||||
|
expect(fetchMock).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -71,7 +71,22 @@ export class EncryptedMetadataManager implements SettingsManager {
|
||||||
fetchMetadata,
|
fetchMetadata,
|
||||||
mutateMetadata,
|
mutateMetadata,
|
||||||
});
|
});
|
||||||
this.encryptionKey = encryptionKey;
|
if (encryptionKey) {
|
||||||
|
this.encryptionKey = encryptionKey;
|
||||||
|
} else {
|
||||||
|
console.warn("Encrypted Metadata Manager secret key has not been set.");
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
console.error("Can't start the application without the secret key.");
|
||||||
|
throw new Error(
|
||||||
|
"Encryption key for the EncryptedMetadataManager has not been set. Setting it for the production environments is necessary. You can find more in the documentation: https://github.com/saleor/saleor-app-sdk/blob/main/docs/settings-manager.md"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.warn(
|
||||||
|
"WARNING: Encrypted Metadata Manager encryption key has not been set. For production deployments, it need's to be set"
|
||||||
|
);
|
||||||
|
console.warn("Using placeholder value for the development.");
|
||||||
|
this.encryptionKey = "CHANGE_ME";
|
||||||
|
}
|
||||||
this.encryptionMethod = encryptionMethod || encrypt;
|
this.encryptionMethod = encryptionMethod || encrypt;
|
||||||
this.decryptionMethod = decryptionMethod || decrypt;
|
this.decryptionMethod = decryptionMethod || decrypt;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue