From 4d4d684e6127609e172d091a078ea2c6000e8b3f Mon Sep 17 00:00:00 2001 From: Andrew Plummer Date: Tue, 3 Mar 2020 23:55:13 +0000 Subject: [PATCH 1/2] Handle whole file secret --- index.ts | 6 ++++-- provider/index.ts | 28 ++++++++++++++++++++------ provider/tests/index.test.ts | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/index.ts b/index.ts index 0e5aaf0..1e281dc 100644 --- a/index.ts +++ b/index.ts @@ -27,7 +27,8 @@ export interface SopsSecretsManagerProps { readonly asset?: s3Assets.Asset; readonly path?: string; readonly kmsKey?: kms.IKey; - readonly mappings: SopsSecretsManagerMappings; + readonly mappings?: SopsSecretsManagerMappings; + readonly wholeFile?: boolean; readonly fileType?: SopsSecretsManagerFileType; } @@ -101,7 +102,8 @@ export class SopsSecretsManager extends cdk.Construct { S3Path: this.asset.s3ObjectKey, SourceHash: this.asset.sourceHash, KMSKeyArn: props.kmsKey?.keyArn, - Mappings: JSON.stringify(props.mappings), + Mappings: JSON.stringify(props.mappings || {}), + WholeFile: props.wholeFile || false, FileType: props.fileType, }, }); diff --git a/provider/index.ts b/provider/index.ts index 98938fd..1da20a2 100644 --- a/provider/index.ts +++ b/provider/index.ts @@ -19,11 +19,16 @@ type MappedValues = { [name: string]: string; }; +interface SopsWholeFileData { + data: string; +} + interface ResourceProperties { KMSKeyArn: string | undefined; S3Bucket: string; S3Path: string; Mappings: string; // json encoded Mappings; + WholeFile: boolean; SecretArn: string; SourceHash: string; FileType: string | undefined; @@ -56,11 +61,15 @@ interface Response { type Event = CreateEvent | UpdateEvent | DeleteEvent; -const determineFileType = (s3Path: string, fileType: string | undefined): string => { +const determineFileType = (s3Path: string, fileType: string | undefined, wholeFile: boolean): string => { if (fileType) { return fileType; } + if (wholeFile) { + return 'json'; + } + const parts = s3Path.split('.') as Array; return parts.pop() as string; }; @@ -152,12 +161,12 @@ const resolveMappings = (data: unknown, mappings: Mappings): MappedValues => { return mapped; }; -const setValuesInSecret = async (values: MappedValues, secretArn: string): Promise => { +const setSecretString = async (secretString: string, secretArn: string): Promise => { const secretsManager = new aws.SecretsManager(); return secretsManager .putSecretValue({ SecretId: secretArn, - SecretString: JSON.stringify(values), + SecretString: secretString, }) .promise() .then(() => { @@ -170,6 +179,7 @@ const handleCreate = async (event: CreateEvent): Promise => { const s3BucketName = event.ResourceProperties.S3Bucket; const s3Path = event.ResourceProperties.S3Path; const mappings = JSON.parse(event.ResourceProperties.Mappings) as Mappings; + const wholeFile = event.ResourceProperties.WholeFile; const secretArn = event.ResourceProperties.SecretArn; // const sourceHash = event.ResourceProperties.SourceHash; const fileType = event.ResourceProperties.FileType; @@ -183,9 +193,15 @@ const handleCreate = async (event: CreateEvent): Promise => { }) .promise(); - const data = await sopsDecode((obj.Body as Buffer).toString('utf-8'), determineFileType(s3Path, fileType), kmsKeyArn); - const mappedValues = resolveMappings(data, mappings); - await setValuesInSecret(mappedValues, secretArn); + const data = await sopsDecode((obj.Body as Buffer).toString('utf-8'), determineFileType(s3Path, fileType, wholeFile), kmsKeyArn); + + if (wholeFile) { + const wholeFileData = (data as SopsWholeFileData).data || ''; + await setSecretString(wholeFileData, secretArn); + } else { + const mappedValues = resolveMappings(data, mappings); + await setSecretString(JSON.stringify(mappedValues), secretArn); + } return Promise.resolve({ PhysicalResourceId: `secretdata_${secretArn}`, diff --git a/provider/tests/index.test.ts b/provider/tests/index.test.ts index c7a13df..945ac71 100644 --- a/provider/tests/index.test.ts +++ b/provider/tests/index.test.ts @@ -109,6 +109,7 @@ describe('onCreate', () => { path: ['a'], }, }), + WholeFile: false, SecretArn: 'mysecretarn', SourceHash: '123', FileType: undefined, @@ -165,6 +166,7 @@ describe('onCreate', () => { encoding: 'json', }, }), + WholeFile: false, SecretArn: 'mysecretarn', SourceHash: '123', FileType: undefined, @@ -183,6 +185,42 @@ describe('onCreate', () => { b: 'c', }); }); + + test('whole file', async () => { + setMockSpawn({ + stdoutData: JSON.stringify({ + data: 'mysecretdata', + }), + }); + + await onEvent({ + RequestType: 'Create', + ResourceProperties: { + KMSKeyArn: undefined, + S3Bucket: 'mys3bucket', + S3Path: 'mys3path.txt', + Mappings: JSON.stringify({}), + WholeFile: true, + SecretArn: 'mysecretarn', + SourceHash: '123', + FileType: undefined, + }, + }); + + expect(childProcess.spawn as jest.Mock).toBeCalledWith( + 'sh', + ['-c', 'cat', '-', '|', path.normalize(path.join(__dirname, '../sops')), '-d', '--input-type', 'json', '--output-type', 'json', '/dev/stdin'], + { + shell: true, + stdio: 'pipe', + }, + ); + + expect(mockSecretsManagerPutSecretValue).toBeCalledWith({ + SecretId: 'mysecretarn', + SecretString: 'mysecretdata', + }); + }); }); describe('onDelete', () => { From dc26946e7f3b23ba6f8a2413f57bdadd5c512f9b Mon Sep 17 00:00:00 2001 From: Andrew Plummer Date: Tue, 3 Mar 2020 23:58:39 +0000 Subject: [PATCH 2/2] Better config erroring --- index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/index.ts b/index.ts index 1e281dc..bcaac0d 100644 --- a/index.ts +++ b/index.ts @@ -93,6 +93,12 @@ export class SopsSecretsManager extends cdk.Construct { } this.asset = this.getAsset(props.asset, props.path); + if (props.wholeFile && props.mappings) { + throw new Error('Cannot set mappings and set wholeFile to true'); + } else if (!props.wholeFile && !props.mappings) { + throw new Error('Must set mappings or set wholeFile to true'); + } + new cfn.CustomResource(this, 'Resource', { provider: SopsSecretsManagerProvider.getOrCreate(this), resourceType: 'Custom::SopsSecretsManager',