From e35b52ecae66bfe2b59ba9be14603fe4fa0d0709 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Wed, 31 Aug 2022 13:05:37 +0800 Subject: [PATCH 1/3] feat: load secret from files --- src/lib/secrets.ts | 24 +++++++++++++++++++ src/server/constants.ts | 6 +++-- test/lib/index.test.ts | 1 + test/lib/secrets.ts | 53 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 src/lib/secrets.ts create mode 100644 test/lib/secrets.ts diff --git a/src/lib/secrets.ts b/src/lib/secrets.ts new file mode 100644 index 00000000..aee3649c --- /dev/null +++ b/src/lib/secrets.ts @@ -0,0 +1,24 @@ +import fs from 'fs/promises' + +export const getSecret = async (key: string) => { + if (!key) { + return '' + } + + const env = process.env[key] + if (env) { + return env + } + + const file = process.env[key + '_FILE'] + if (!file) { + return '' + } + + return await fs.readFile(file, { encoding: 'utf8' }).catch((e) => { + if (e.code == 'ENOENT') { + return '' + } + throw e + }) +} diff --git a/src/server/constants.ts b/src/server/constants.ts index db2ad05b..06a2a84b 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -1,12 +1,14 @@ +import { getSecret } from '../lib/secrets' + export const PG_META_HOST = process.env.PG_META_HOST || '0.0.0.0' export const PG_META_PORT = Number(process.env.PG_META_PORT || 1337) -export const CRYPTO_KEY = process.env.CRYPTO_KEY || 'SAMPLE_KEY' +export const CRYPTO_KEY = (await getSecret('CRYPTO_KEY')) || 'SAMPLE_KEY' const PG_META_DB_HOST = process.env.PG_META_DB_HOST || 'localhost' const PG_META_DB_NAME = process.env.PG_META_DB_NAME || 'postgres' const PG_META_DB_USER = process.env.PG_META_DB_USER || 'postgres' const PG_META_DB_PORT = Number(process.env.PG_META_DB_PORT) || 5432 -const PG_META_DB_PASSWORD = process.env.PG_META_DB_PASSWORD || 'postgres' +const PG_META_DB_PASSWORD = (await getSecret('PG_META_DB_PASSWORD')) || 'postgres' const PG_META_DB_SSL_MODE = process.env.PG_META_DB_SSL_MODE || 'disable' const PG_CONN_TIMEOUT_SECS = Number(process.env.PG_CONN_TIMEOUT_SECS || 15) diff --git a/test/lib/index.test.ts b/test/lib/index.test.ts index 74ca9140..464c61a1 100644 --- a/test/lib/index.test.ts +++ b/test/lib/index.test.ts @@ -2,6 +2,7 @@ // TODO: Test server. import './query' import './config' +import './secrets' import './version' import './schemas' import './types' diff --git a/test/lib/secrets.ts b/test/lib/secrets.ts new file mode 100644 index 00000000..2395025d --- /dev/null +++ b/test/lib/secrets.ts @@ -0,0 +1,53 @@ +import { readFile } from 'fs/promises' +import { getSecret } from '../../src/lib/secrets' + +jest.mock('fs/promises') + +describe('getSecret', () => { + const value = 'dummy' + + beforeEach(() => { + // Clears env var + jest.resetModules() + }) + + afterEach(() => { + delete process.env.SECRET + delete process.env.SECRET_FILE + }) + + it('loads from env', async () => { + process.env.SECRET = value + const res = await getSecret('SECRET') + expect(res).toBe(value) + }) + + it('loads from file', async () => { + process.env.SECRET_FILE = '/run/secrets/db_password' + jest.mocked(readFile, true).mockResolvedValueOnce(value) + const res = await getSecret('SECRET') + expect(res).toBe(value) + }) + + it('defaults to empty string', async () => { + expect(await getSecret('')).toBe('') + expect(await getSecret('SECRET')).toBe('') + }) + + it('default on file not found', async () => { + process.env.SECRET_FILE = '/run/secrets/db_password' + const e: NodeJS.ErrnoException = new Error('no such file or directory') + e.code = 'ENOENT' + jest.mocked(readFile, true).mockRejectedValueOnce(e) + const res = await getSecret('SECRET') + expect(res).toBe('') + }) + + it('throws on permission denied', async () => { + process.env.SECRET_FILE = '/run/secrets/db_password' + const e: NodeJS.ErrnoException = new Error('permission denied') + e.code = 'EACCES' + jest.mocked(readFile, true).mockRejectedValueOnce(e) + expect(getSecret('SECRET')).rejects.toThrow() + }) +}) From 881c0d8d2340b5f1712e245c317657716bd18283 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Wed, 4 Jan 2023 14:11:19 +0800 Subject: [PATCH 2/3] chore: fix tests by using dynamic imports --- src/lib/secrets.ts | 3 ++- src/server/constants.ts | 2 +- test/lib/secrets.ts | 15 +++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/lib/secrets.ts b/src/lib/secrets.ts index aee3649c..5cd58e44 100644 --- a/src/lib/secrets.ts +++ b/src/lib/secrets.ts @@ -1,4 +1,5 @@ -import fs from 'fs/promises' +// Use dynamic import to support module mock +const fs = await import('fs/promises') export const getSecret = async (key: string) => { if (!key) { diff --git a/src/server/constants.ts b/src/server/constants.ts index 06a2a84b..b27be94d 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -1,4 +1,4 @@ -import { getSecret } from '../lib/secrets' +import { getSecret } from '../lib/secrets.js' export const PG_META_HOST = process.env.PG_META_HOST || '0.0.0.0' export const PG_META_PORT = Number(process.env.PG_META_PORT || 1337) diff --git a/test/lib/secrets.ts b/test/lib/secrets.ts index 2395025d..1f714c16 100644 --- a/test/lib/secrets.ts +++ b/test/lib/secrets.ts @@ -1,7 +1,10 @@ -import { readFile } from 'fs/promises' -import { getSecret } from '../../src/lib/secrets' +import { jest } from '@jest/globals' -jest.mock('fs/promises') +jest.unstable_mockModule('fs/promises', () => ({ + readFile: jest.fn(), +})) +const { readFile } = await import('fs/promises') +const { getSecret } = await import('../../src/lib/secrets') describe('getSecret', () => { const value = 'dummy' @@ -24,7 +27,7 @@ describe('getSecret', () => { it('loads from file', async () => { process.env.SECRET_FILE = '/run/secrets/db_password' - jest.mocked(readFile, true).mockResolvedValueOnce(value) + jest.mocked(readFile).mockResolvedValueOnce(value) const res = await getSecret('SECRET') expect(res).toBe(value) }) @@ -38,7 +41,7 @@ describe('getSecret', () => { process.env.SECRET_FILE = '/run/secrets/db_password' const e: NodeJS.ErrnoException = new Error('no such file or directory') e.code = 'ENOENT' - jest.mocked(readFile, true).mockRejectedValueOnce(e) + jest.mocked(readFile).mockRejectedValueOnce(e) const res = await getSecret('SECRET') expect(res).toBe('') }) @@ -47,7 +50,7 @@ describe('getSecret', () => { process.env.SECRET_FILE = '/run/secrets/db_password' const e: NodeJS.ErrnoException = new Error('permission denied') e.code = 'EACCES' - jest.mocked(readFile, true).mockRejectedValueOnce(e) + jest.mocked(readFile).mockRejectedValueOnce(e) expect(getSecret('SECRET')).rejects.toThrow() }) }) From 4ba9e265eb1e2e426528ae07173ab17563c43ea9 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Wed, 4 Jan 2023 16:09:39 +0800 Subject: [PATCH 3/3] chore: add reference comment --- test/lib/secrets.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/lib/secrets.ts b/test/lib/secrets.ts index 1f714c16..2b41a7ff 100644 --- a/test/lib/secrets.ts +++ b/test/lib/secrets.ts @@ -1,5 +1,6 @@ import { jest } from '@jest/globals' +// Ref: https://jestjs.io/docs/ecmascript-modules jest.unstable_mockModule('fs/promises', () => ({ readFile: jest.fn(), }))