Skip to content

feat(parameters): SecretsProvider support #1206

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
666 changes: 664 additions & 2 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions packages/parameters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,10 @@
"secrets",
"serverless",
"nodejs"
]
}
],
"devDependencies": {
"@aws-sdk/client-secrets-manager": "^3.238.0",
"aws-sdk-client-mock": "^2.0.1",
"aws-sdk-client-mock-jest": "^2.0.1"
}
}
8 changes: 5 additions & 3 deletions packages/parameters/src/BaseProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ExpirableValue } from './ExpirableValue';
import { TRANSFORM_METHOD_BINARY, TRANSFORM_METHOD_JSON } from './constants';
import { GetParameterError, TransformParameterError } from './Exceptions';
import type { BaseProviderInterface, GetMultipleOptionsInterface, GetOptionsInterface, TransformOptions } from './types';
import type { SecretsGetOptionsInterface } from './types/SecretsProvider';

// These providers are dinamycally intialized on first use of the helper functions
const DEFAULT_PROVIDERS: Record<string, BaseProvider> = {};
Expand Down Expand Up @@ -38,8 +39,9 @@ abstract class BaseProvider implements BaseProviderInterface {
* this should be an acceptable tradeoff.
*
* @param {string} name - Parameter name
* @param {GetOptionsInterface} options - Options to configure maximum age, trasformation, AWS SDK options, or force fetch
* @param {GetOptionsInterface|SecretsGetOptionsInterface} options - Options to configure maximum age, trasformation, AWS SDK options, or force fetch
*/
public async get(name: string, options?: SecretsGetOptionsInterface): Promise<undefined | string | Uint8Array | Record<string, unknown>>;
public async get(name: string, options?: GetOptionsInterface): Promise<undefined | string | Uint8Array | Record<string, unknown>> {
const configs = new GetOptions(options);
const key = [ name, configs.transform ].toString();
Expand All @@ -58,7 +60,7 @@ abstract class BaseProvider implements BaseProviderInterface {
}

if (value && configs.transform) {
value = transformValue(value, configs.transform, true);
value = transformValue(value, configs.transform, true, name);
}

if (value) {
Expand Down Expand Up @@ -130,7 +132,7 @@ abstract class BaseProvider implements BaseProviderInterface {

}

const transformValue = (value: string | Uint8Array | undefined, transform: TransformOptions, throwOnTransformError: boolean, key: string = ''): string | Record<string, unknown> | undefined => {
const transformValue = (value: string | Uint8Array | undefined, transform: TransformOptions, throwOnTransformError: boolean, key: string): string | Record<string, unknown> | undefined => {
try {
const normalizedTransform = transform.toLowerCase();
if (
Expand Down
58 changes: 58 additions & 0 deletions packages/parameters/src/secrets/SecretsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { BaseProvider } from '../BaseProvider';
import {
SecretsManagerClient,
GetSecretValueCommand
} from '@aws-sdk/client-secrets-manager';
import type { GetSecretValueCommandInput } from '@aws-sdk/client-secrets-manager';
import type {
SecretsProviderOptions,
SecretsGetOptionsInterface
} from '../types/SecretsProvider';

class SecretsProvider extends BaseProvider {
public client: SecretsManagerClient;

public constructor (config?: SecretsProviderOptions) {
super();

const clientConfig = config?.clientConfig || {};
this.client = new SecretsManagerClient(clientConfig);
}

public async get(
name: string,
options?: SecretsGetOptionsInterface
): Promise<undefined | string | Uint8Array | Record<string, unknown>> {
return super.get(name, options);
}

protected async _get(
name: string,
options?: SecretsGetOptionsInterface
): Promise<string | Uint8Array | undefined> {
const sdkOptions: GetSecretValueCommandInput = {
...(options?.sdkOptions || {}),
SecretId: name,
};

const result = await this.client.send(new GetSecretValueCommand(sdkOptions));

if (result.SecretString) return result.SecretString;

return result.SecretBinary;
}

/**
* Retrieving multiple parameter values is not supported with AWS Secrets Manager.
*/
protected async _getMultiple(
_path: string,
_options?: unknown
): Promise<Record<string, string | undefined>> {
throw new Error('Method not implemented.');
}
}

export {
SecretsProvider,
};
15 changes: 15 additions & 0 deletions packages/parameters/src/secrets/getSecret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { DEFAULT_PROVIDERS } from '../BaseProvider';
import { SecretsProvider } from './SecretsProvider';
import type { SecretsGetOptionsInterface } from '../types/SecretsProvider';

const getSecret = async (name: string, options?: SecretsGetOptionsInterface): Promise<undefined | string | Uint8Array | Record<string, unknown>> => {
if (!DEFAULT_PROVIDERS.hasOwnProperty('secrets')) {
DEFAULT_PROVIDERS.secrets = new SecretsProvider();
}

return DEFAULT_PROVIDERS.secrets.get(name, options);
};

export {
getSecret
};
2 changes: 2 additions & 0 deletions packages/parameters/src/secrets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './SecretsProvider';
export * from './getSecret';
6 changes: 1 addition & 5 deletions packages/parameters/src/types/BaseProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ interface GetOptionsInterface {
transform?: TransformOptions
}

interface GetMultipleOptionsInterface {
maxAge?: number
forceFetch?: boolean
sdkOptions?: unknown
transform?: string
interface GetMultipleOptionsInterface extends GetOptionsInterface {
throwOnTransformError?: boolean
}

Expand Down
15 changes: 15 additions & 0 deletions packages/parameters/src/types/SecretsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { GetOptionsInterface } from './BaseProvider';
import type { SecretsManagerClientConfig, GetSecretValueCommandInput } from '@aws-sdk/client-secrets-manager';

interface SecretsProviderOptions {
clientConfig?: SecretsManagerClientConfig
}

interface SecretsGetOptionsInterface extends GetOptionsInterface {
sdkOptions?: Omit<Partial<GetSecretValueCommandInput>, 'SecretId'>
}

export type {
SecretsProviderOptions,
SecretsGetOptionsInterface,
};
15 changes: 15 additions & 0 deletions packages/parameters/tests/unit/BaseProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,21 @@ describe('Class: BaseProvider', () => {
expect(value).toEqual('my-value');

});

test('when called with an auto transform, and the value is a valid JSON, it returns the parsed value', async () => {

// Prepare
const mockData = JSON.stringify({ foo: 'bar' });
const provider = new TestProvider();
jest.spyOn(provider, '_get').mockImplementation(() => new Promise((resolve, _reject) => resolve(mockData)));

// Act
const value = await provider.get('my-parameter.json', { transform: 'auto' });

// Assess
expect(value).toStrictEqual({ foo: 'bar' });

});

});

Expand Down
122 changes: 122 additions & 0 deletions packages/parameters/tests/unit/SecretsProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Test SecretsProvider class
*
* @group unit/parameters/SecretsProvider/class
*/
import { SecretsProvider } from '../../src/secrets';
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import type { GetSecretValueCommandInput } from '@aws-sdk/client-secrets-manager';
import { mockClient } from 'aws-sdk-client-mock';
import 'aws-sdk-client-mock-jest';

const encoder = new TextEncoder();

describe('Class: SecretsProvider', () => {

const client = mockClient(SecretsManagerClient);

beforeEach(() => {
jest.clearAllMocks();
});

describe('Method: _get', () => {

test('when called with only a name, it gets the secret string', async () => {

// Prepare
const provider = new SecretsProvider();
const secretName = 'foo';
client.on(GetSecretValueCommand).resolves({
SecretString: 'bar',
});

// Act
const result = await provider.get(secretName);

// Assess
expect(result).toBe('bar');

});

test('when called with only a name, it gets the secret binary', async () => {

// Prepare
const provider = new SecretsProvider();
const secretName = 'foo';
const mockData = encoder.encode('my-value');
client.on(GetSecretValueCommand).resolves({
SecretBinary: mockData,
});

// Act
const result = await provider.get(secretName);

// Assess
expect(result).toBe(mockData);

});

test('when called with a name and sdkOptions, it gets the secret using the options provided', async () => {

// Prepare
const provider = new SecretsProvider();
const secretName = 'foo';
client.on(GetSecretValueCommand).resolves({
SecretString: 'bar',
});

// Act
await provider.get(secretName, {
sdkOptions: {
VersionId: 'test-version',
}
});

// Assess
expect(client).toReceiveCommandWith(GetSecretValueCommand, {
SecretId: secretName,
VersionId: 'test-version',
});

});

test('when called with sdkOptions that override arguments passed to the method, it gets the secret using the arguments', async () => {

// Prepare
const provider = new SecretsProvider();
const secretName = 'foo';
client.on(GetSecretValueCommand).resolves({
SecretString: 'bar',
});

// Act
await provider.get(secretName, {
sdkOptions: {
SecretId: 'test-secret',
} as unknown as GetSecretValueCommandInput,
});

// Assess
expect(client).toReceiveCommandWith(GetSecretValueCommand, {
SecretId: secretName,
});

});

});

describe('Method: _getMultiple', () => {

test('when called, it throws an error', async () => {

// Prepare
const provider = new SecretsProvider();

// Act & Assess
await expect(provider.getMultiple('foo')).rejects.toThrow('Method not implemented.');

});

});

});
62 changes: 62 additions & 0 deletions packages/parameters/tests/unit/getSecret.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Test getSecret function
*
* @group unit/parameters/SecretsProvider/getSecret/function
*/
import { DEFAULT_PROVIDERS } from '../../src/BaseProvider';
import { SecretsProvider, getSecret } from '../../src/secrets';
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import { mockClient } from 'aws-sdk-client-mock';
import 'aws-sdk-client-mock-jest';

const encoder = new TextEncoder();

describe('Function: getSecret', () => {

const client = mockClient(SecretsManagerClient);

beforeEach(() => {
jest.clearAllMocks();
});

test('when called and a default provider doesn\'t exist, it instantiates one and returns the value', async () => {

// Prepare
const secretName = 'foo';
const secretValue = 'bar';
client.on(GetSecretValueCommand).resolves({
SecretString: secretValue,
});

// Act
const result = await getSecret(secretName);

// Assess
expect(client).toReceiveCommandWith(GetSecretValueCommand, { SecretId: secretName });
expect(result).toBe(secretValue);

});

test('when called and a default provider exists, it uses it and returns the value', async () => {

// Prepare
const provider = new SecretsProvider();
DEFAULT_PROVIDERS.secrets = provider;
const secretName = 'foo';
const secretValue = 'bar';
const binary = encoder.encode(secretValue);
client.on(GetSecretValueCommand).resolves({
SecretBinary: binary,
});

// Act
const result = await getSecret(secretName);

// Assess
expect(client).toReceiveCommandWith(GetSecretValueCommand, { SecretId: secretName });
expect(result).toStrictEqual(binary);
expect(DEFAULT_PROVIDERS.secrets).toBe(provider);

});

});