-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement function to initialise correct adapter from env vars (#…
- Loading branch information
Showing
8 changed files
with
253 additions
and
16 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Do NOT import specific adapters here, because some SDKs do some heavy lifting on import (e.g., | ||
// call APIs). | ||
|
||
export { initKmsProviderFromEnv } from './lib/init'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
/* tslint:disable:max-classes-per-file */ | ||
import { EnvVarError } from 'env-var'; | ||
|
||
import { configureMockEnvVars } from '../testUtils/envVars'; | ||
import { initKmsProviderFromEnv } from './init'; | ||
import { GcpKmsConfig } from './gcp/GcpKmsConfig'; | ||
import { KmsError } from './KmsError'; | ||
|
||
class MockGcpSdkClient {} | ||
|
||
class MockAwsSdkClient { | ||
constructor(public readonly config: any) {} | ||
} | ||
|
||
let gcpSdkImported = false; | ||
jest.mock('@google-cloud/kms', () => { | ||
gcpSdkImported = true; | ||
return { | ||
KeyManagementServiceClient: MockGcpSdkClient, | ||
}; | ||
}); | ||
let awsSdkImported = false; | ||
jest.mock('@aws-sdk/client-kms', () => { | ||
awsSdkImported = true; | ||
return { ...jest.requireActual('@aws-sdk/client-kms'), KMSClient: MockAwsSdkClient }; | ||
}); | ||
beforeEach(() => { | ||
gcpSdkImported = false; | ||
awsSdkImported = false; | ||
}); | ||
|
||
describe('initKmsProviderFromEnv', () => { | ||
const mockEnvVars = configureMockEnvVars(); | ||
|
||
const GCP_REQUIRED_ENV_VARS = { | ||
GCP_KMS_LOCATION: 'westeros-3', | ||
GCP_KMS_KEYRING: 'my-precious', | ||
GCP_KMS_PROTECTION_LEVEL: 'HSM', | ||
} as const; | ||
|
||
test('Unknown adapter should be refused', async () => { | ||
const invalidAdapter = 'potato'; | ||
await expect(() => initKmsProviderFromEnv(invalidAdapter as any)).rejects.toThrowWithMessage( | ||
KmsError, | ||
`Invalid adapter (${invalidAdapter})`, | ||
); | ||
}); | ||
|
||
test('Adapters should be imported lazily', async () => { | ||
expect(gcpSdkImported).toBeFalse(); | ||
expect(awsSdkImported).toBeFalse(); | ||
|
||
mockEnvVars(GCP_REQUIRED_ENV_VARS); | ||
await initKmsProviderFromEnv('GCP'); | ||
expect(gcpSdkImported).toBeTrue(); | ||
expect(awsSdkImported).toBeFalse(); | ||
|
||
await initKmsProviderFromEnv('AWS'); | ||
expect(awsSdkImported).toBeTrue(); | ||
}); | ||
|
||
describe('GPC', () => { | ||
beforeEach(() => { | ||
mockEnvVars(GCP_REQUIRED_ENV_VARS); | ||
}); | ||
|
||
test.each(Object.getOwnPropertyNames(GCP_REQUIRED_ENV_VARS))( | ||
'Environment variable %s should be present', | ||
async (envVar) => { | ||
mockEnvVars({ ...GCP_REQUIRED_ENV_VARS, [envVar]: undefined }); | ||
|
||
await expect(initKmsProviderFromEnv('GCP')).rejects.toThrowWithMessage( | ||
EnvVarError, | ||
new RegExp(envVar), | ||
); | ||
}, | ||
); | ||
|
||
test('Provider should be returned if env vars are present', async () => { | ||
const provider = await initKmsProviderFromEnv('GCP'); | ||
|
||
const { GcpKmsRsaPssProvider } = await import('./gcp/GcpKmsRsaPssProvider'); | ||
expect(provider).toBeInstanceOf(GcpKmsRsaPssProvider); | ||
expect(provider).toHaveProperty('client', expect.any(MockGcpSdkClient)); | ||
expect(provider).toHaveProperty<GcpKmsConfig>('config', { | ||
keyRing: GCP_REQUIRED_ENV_VARS.GCP_KMS_KEYRING, | ||
location: GCP_REQUIRED_ENV_VARS.GCP_KMS_LOCATION, | ||
protectionLevel: GCP_REQUIRED_ENV_VARS.GCP_KMS_PROTECTION_LEVEL, | ||
}); | ||
}); | ||
|
||
test('GCP_KMS_DESTROY_SCHEDULED_DURATION_SECONDS should be honoured if set', async () => { | ||
const seconds = 123; | ||
mockEnvVars({ | ||
...GCP_REQUIRED_ENV_VARS, | ||
GCP_KMS_DESTROY_SCHEDULED_DURATION_SECONDS: seconds.toString(), | ||
}); | ||
|
||
const provider = await initKmsProviderFromEnv('GCP'); | ||
|
||
expect(provider).toHaveProperty('config.destroyScheduledDurationSeconds', seconds); | ||
}); | ||
|
||
test('Invalid GCP_KMS_PROTECTION_LEVEL should be refused', async () => { | ||
mockEnvVars({ ...GCP_REQUIRED_ENV_VARS, GCP_KMS_PROTECTION_LEVEL: 'potato' }); | ||
|
||
await expect(initKmsProviderFromEnv('GCP')).rejects.toThrowWithMessage( | ||
EnvVarError, | ||
/GCP_KMS_PROTECTION_LEVEL/, | ||
); | ||
}); | ||
}); | ||
|
||
describe('AWS', () => { | ||
test('AWS KMS provider should be output', async () => { | ||
const provider = await initKmsProviderFromEnv('AWS'); | ||
|
||
const { AwsKmsRsaPssProvider } = await import('./aws/AwsKmsRsaPssProvider'); | ||
expect(provider).toBeInstanceOf(AwsKmsRsaPssProvider); | ||
expect(provider).toHaveProperty('client.config', { | ||
endpoint: undefined, | ||
region: undefined, | ||
}); | ||
}); | ||
|
||
test('AWS_KMS_ENDPOINT should be honoured if present', async () => { | ||
const endpoint = 'https://kms.example.com'; | ||
mockEnvVars({ AWS_KMS_ENDPOINT: endpoint }); | ||
|
||
const provider = await initKmsProviderFromEnv('AWS'); | ||
|
||
expect(provider).toHaveProperty('client.config.endpoint', endpoint); | ||
}); | ||
|
||
test('AWS_KMS_REGION should be honoured if present', async () => { | ||
const region = 'westeros-3'; | ||
mockEnvVars({ AWS_KMS_REGION: region }); | ||
|
||
const provider = await initKmsProviderFromEnv('AWS'); | ||
|
||
expect(provider).toHaveProperty('client.config.region', region); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { get as getEnvVar } from 'env-var'; | ||
|
||
import { KmsError } from './KmsError'; | ||
|
||
import { GcpKmsConfig } from './gcp/GcpKmsConfig'; | ||
import { KmsRsaPssProvider } from './KmsRsaPssProvider'; | ||
import { GcpKmsRsaPssProvider } from './gcp/GcpKmsRsaPssProvider'; | ||
|
||
const INITIALISERS: { readonly [key: string]: () => Promise<KmsRsaPssProvider> } = { | ||
AWS: initAwsProvider, | ||
GCP: initGcpProvider, | ||
}; | ||
|
||
export async function initKmsProviderFromEnv(adapter: string): Promise<KmsRsaPssProvider> { | ||
const init = INITIALISERS[adapter]; | ||
if (!init) { | ||
throw new KmsError(`Invalid adapter (${adapter})`); | ||
} | ||
return init(); | ||
} | ||
|
||
export async function initAwsProvider(): Promise<KmsRsaPssProvider> { | ||
// Avoid import-time side effects (e.g., expensive API calls) | ||
const { AwsKmsRsaPssProvider } = await import('./aws/AwsKmsRsaPssProvider'); | ||
const { KMSClient } = await import('@aws-sdk/client-kms'); | ||
return new AwsKmsRsaPssProvider( | ||
new KMSClient({ | ||
endpoint: getEnvVar('AWS_KMS_ENDPOINT').asString(), | ||
region: getEnvVar('AWS_KMS_REGION').asString(), | ||
}), | ||
); | ||
} | ||
|
||
export async function initGcpProvider(): Promise<KmsRsaPssProvider> { | ||
const kmsConfig: GcpKmsConfig = { | ||
location: getEnvVar('GCP_KMS_LOCATION').required().asString(), | ||
keyRing: getEnvVar('GCP_KMS_KEYRING').required().asString(), | ||
protectionLevel: getEnvVar('GCP_KMS_PROTECTION_LEVEL').required().asEnum(['SOFTWARE', 'HSM']), | ||
destroyScheduledDurationSeconds: getEnvVar( | ||
'GCP_KMS_DESTROY_SCHEDULED_DURATION_SECONDS', | ||
).asIntPositive(), | ||
}; | ||
|
||
// Avoid import-time side effects (e.g., expensive API calls) | ||
const { KeyManagementServiceClient } = await import('@google-cloud/kms'); | ||
return new GcpKmsRsaPssProvider(new KeyManagementServiceClient(), kmsConfig); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import envVar from 'env-var'; | ||
|
||
interface EnvVarSet { | ||
readonly [key: string]: string | undefined; | ||
} | ||
|
||
export function configureMockEnvVars(envVars: EnvVarSet = {}): (envVars: EnvVarSet) => void { | ||
const mockEnvVarGet = jest.spyOn(envVar, 'get'); | ||
|
||
function setEnvVars(newEnvVars: EnvVarSet): void { | ||
mockEnvVarGet.mockReset(); | ||
mockEnvVarGet.mockImplementation((...args: readonly any[]) => { | ||
const originalEnvVar = jest.requireActual('env-var'); | ||
const env = originalEnvVar.from(newEnvVars); | ||
|
||
return env.get(...args); | ||
}); | ||
} | ||
|
||
beforeAll(() => setEnvVars(envVars)); | ||
beforeEach(() => setEnvVars(envVars)); | ||
|
||
afterAll(() => { | ||
mockEnvVarGet.mockRestore(); | ||
}); | ||
|
||
return (newEnvVars: EnvVarSet) => setEnvVars(newEnvVars); | ||
} |