Skip to content

Commit a185007

Browse files
authored
feat: load secret from files (#337)
* feat: load secret from files * chore: fix tests by using dynamic imports * chore: add reference comment
1 parent 7747679 commit a185007

File tree

4 files changed

+87
-2
lines changed

4 files changed

+87
-2
lines changed

src/lib/secrets.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Use dynamic import to support module mock
2+
const fs = await import('fs/promises')
3+
4+
export const getSecret = async (key: string) => {
5+
if (!key) {
6+
return ''
7+
}
8+
9+
const env = process.env[key]
10+
if (env) {
11+
return env
12+
}
13+
14+
const file = process.env[key + '_FILE']
15+
if (!file) {
16+
return ''
17+
}
18+
19+
return await fs.readFile(file, { encoding: 'utf8' }).catch((e) => {
20+
if (e.code == 'ENOENT') {
21+
return ''
22+
}
23+
throw e
24+
})
25+
}

src/server/constants.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import { getSecret } from '../lib/secrets.js'
2+
13
export const PG_META_HOST = process.env.PG_META_HOST || '0.0.0.0'
24
export const PG_META_PORT = Number(process.env.PG_META_PORT || 1337)
3-
export const CRYPTO_KEY = process.env.CRYPTO_KEY || 'SAMPLE_KEY'
5+
export const CRYPTO_KEY = (await getSecret('CRYPTO_KEY')) || 'SAMPLE_KEY'
46

57
const PG_META_DB_HOST = process.env.PG_META_DB_HOST || 'localhost'
68
const PG_META_DB_NAME = process.env.PG_META_DB_NAME || 'postgres'
79
const PG_META_DB_USER = process.env.PG_META_DB_USER || 'postgres'
810
const PG_META_DB_PORT = Number(process.env.PG_META_DB_PORT) || 5432
9-
const PG_META_DB_PASSWORD = process.env.PG_META_DB_PASSWORD || 'postgres'
11+
const PG_META_DB_PASSWORD = (await getSecret('PG_META_DB_PASSWORD')) || 'postgres'
1012
const PG_META_DB_SSL_MODE = process.env.PG_META_DB_SSL_MODE || 'disable'
1113

1214
const PG_CONN_TIMEOUT_SECS = Number(process.env.PG_CONN_TIMEOUT_SECS || 15)

test/lib/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// TODO: Test server.
33
import './query'
44
import './config'
5+
import './secrets'
56
import './version'
67
import './schemas'
78
import './types'

test/lib/secrets.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { jest } from '@jest/globals'
2+
3+
// Ref: https://jestjs.io/docs/ecmascript-modules
4+
jest.unstable_mockModule('fs/promises', () => ({
5+
readFile: jest.fn(),
6+
}))
7+
const { readFile } = await import('fs/promises')
8+
const { getSecret } = await import('../../src/lib/secrets')
9+
10+
describe('getSecret', () => {
11+
const value = 'dummy'
12+
13+
beforeEach(() => {
14+
// Clears env var
15+
jest.resetModules()
16+
})
17+
18+
afterEach(() => {
19+
delete process.env.SECRET
20+
delete process.env.SECRET_FILE
21+
})
22+
23+
it('loads from env', async () => {
24+
process.env.SECRET = value
25+
const res = await getSecret('SECRET')
26+
expect(res).toBe(value)
27+
})
28+
29+
it('loads from file', async () => {
30+
process.env.SECRET_FILE = '/run/secrets/db_password'
31+
jest.mocked(readFile).mockResolvedValueOnce(value)
32+
const res = await getSecret('SECRET')
33+
expect(res).toBe(value)
34+
})
35+
36+
it('defaults to empty string', async () => {
37+
expect(await getSecret('')).toBe('')
38+
expect(await getSecret('SECRET')).toBe('')
39+
})
40+
41+
it('default on file not found', async () => {
42+
process.env.SECRET_FILE = '/run/secrets/db_password'
43+
const e: NodeJS.ErrnoException = new Error('no such file or directory')
44+
e.code = 'ENOENT'
45+
jest.mocked(readFile).mockRejectedValueOnce(e)
46+
const res = await getSecret('SECRET')
47+
expect(res).toBe('')
48+
})
49+
50+
it('throws on permission denied', async () => {
51+
process.env.SECRET_FILE = '/run/secrets/db_password'
52+
const e: NodeJS.ErrnoException = new Error('permission denied')
53+
e.code = 'EACCES'
54+
jest.mocked(readFile).mockRejectedValueOnce(e)
55+
expect(getSecret('SECRET')).rejects.toThrow()
56+
})
57+
})

0 commit comments

Comments
 (0)