From 11d4afe63357bd62746bb4f71e520bf4d46b4749 Mon Sep 17 00:00:00 2001 From: markussiebert Date: Thu, 5 May 2022 21:55:48 +0200 Subject: [PATCH] chore: better test coverage for cdk(#54) improve test coverage --- API.md | 4 +- src/index.ts | 48 +++++++------- test/secret.test.ts | 157 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 168 insertions(+), 41 deletions(-) diff --git a/API.md b/API.md index c15ce3a3..e31ab80c 100644 --- a/API.md +++ b/API.md @@ -180,12 +180,12 @@ Grants reading the secret value to some role. ##### `grantWrite` ```typescript -public grantWrite(grantee: IGrantable): Grant +public grantWrite(_grantee: IGrantable): Grant ``` Grants writing and updating the secret value to some role. -###### `grantee`Required +###### `_grantee`Required - *Type:* aws-cdk-lib.aws_iam.IGrantable diff --git a/src/index.ts b/src/index.ts index 93111e57..ab50effe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -187,22 +187,26 @@ export class SopsSync extends Construct { let sopsAsset: Asset | undefined = undefined; let sopsInline: { Content: string; Hash: string } | undefined = undefined; let sopsS3File: { Bucket: string; Key: string } | undefined = undefined; - if (uploadType === UploadType.INLINE) { - sopsInline = { - Content: fs.readFileSync(props.sopsFilePath).toString('base64'), - // We calculate the hash the same way as it would be done by new Asset(..) - so we can ensure stable version names even if switching from INLINE to ASSET and viceversa. - Hash: FileSystem.fingerprint(props.sopsFilePath), - }; - } else if (uploadType === UploadType.ASSET) { - sopsAsset = new Asset(this, 'Asset', { - path: props.sopsFilePath, - }); - sopsS3File = { - Bucket: sopsAsset.bucket.bucketName, - Key: sopsAsset.s3ObjectKey, - }; - } else { - throw new Error(`Unsupported UploadType: ${uploadType}`); + + switch (uploadType) { + case UploadType.INLINE: { + sopsInline = { + Content: fs.readFileSync(props.sopsFilePath).toString('base64'), + // We calculate the hash the same way as it would be done by new Asset(..) - so we can ensure stable version names even if switching from INLINE to ASSET and viceversa. + Hash: FileSystem.fingerprint(props.sopsFilePath), + }; + break; + } + case UploadType.ASSET: { + sopsAsset = new Asset(this, 'Asset', { + path: props.sopsFilePath, + }); + sopsS3File = { + Bucket: sopsAsset.bucket.bucketName, + Key: sopsAsset.s3ObjectKey, + }; + break; + } } if (provider.role !== undefined) { @@ -330,19 +334,19 @@ export class SopsSecret extends Construct implements ISecret { public grantRead(grantee: IGrantable, versionStages?: string[]): Grant { return this.secret.grantRead(grantee, versionStages); } - public grantWrite(grantee: IGrantable): Grant { - return this.secret.grantWrite(grantee); + public grantWrite(_grantee: IGrantable): Grant { + throw new Error( + `Method grantWrite(...) not allowed as this secret is managed by SopsSync`, + ); } public addRotationSchedule( id: string, options: RotationScheduleOptions, ): RotationSchedule { throw new Error( - `Method not allowed as this secret is managed by SopsSync!\nid: ${id}\noptions: ${JSON.stringify( + `Method addTotationSchedule('${id}', ${JSON.stringify( options, - null, - 2, - )}`, + )}) not allowed as this secret is managed by SopsSync`, ); } public addToResourcePolicy( diff --git a/test/secret.test.ts b/test/secret.test.ts index 5987e120..63004820 100644 --- a/test/secret.test.ts +++ b/test/secret.test.ts @@ -1,8 +1,9 @@ -import { App, Stack } from 'aws-cdk-lib'; +import { App, SecretValue, Stack } from 'aws-cdk-lib'; import { Match, Template } from 'aws-cdk-lib/assertions'; +import { Role } from 'aws-cdk-lib/aws-iam'; import { Key } from 'aws-cdk-lib/aws-kms'; import { Function, InlineCode, Runtime } from 'aws-cdk-lib/aws-lambda'; -import { SopsSecret } from '../src'; +import { SopsSecret, SopsSyncProvider, UploadType } from '../src'; const keyStatements = [ { @@ -13,6 +14,45 @@ const keyStatements = [ }, ]; +test('Upload type ASSET', () => { + const app = new App(); + const stack = new Stack(app, 'SecretIntegration'); + + new SopsSecret(stack, 'SopsSecret', { + sopsFilePath: 'test-secrets/yaml/sopsfile.enc-kms.yaml', + uploadType: UploadType.ASSET, + }); + Template.fromStack(stack).hasResource('Custom::SopsSync', { + Properties: Match.objectLike({ + SopsS3File: { + Bucket: { + 'Fn::Sub': 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + }, + Key: '6d7a109a504c02a3455d09067a3695a4355cf3c2c914b6f3e949e20d6a741128.yaml', + }, + }), + }); +}); + +test('Upload type INLINE', () => { + const app = new App(); + const stack = new Stack(app, 'SecretIntegration'); + + new SopsSecret(stack, 'SopsSecret', { + sopsFilePath: 'test-secrets/yaml/sopsfile.enc-kms.yaml', + uploadType: UploadType.INLINE, + }); + Template.fromStack(stack).hasResource('Custom::SopsSync', { + Properties: Match.objectLike({ + SopsInline: { + Content: + 'aGVsbG86IEVOQ1tBRVMyNTZfR0NNLGRhdGE6c3N3YlJIY2NJdWlzYkd5TyttRy9FV2IrMUpjdHFOVW9yT3pRVWFNUm4zSGtBeVd0cmVOdVp5aFlZbGt2Y2c9PSxpdjpYcEhpbGh5Q1JYYThjbEdpRWNrcHF1Q3FTWkdXQTgwSDBlRFVtUjgweGhFPSx0YWc6V3N1eEk0bFVId0pFYXM3Nk9EUXhVQT09LHR5cGU6c3RyXQpleGFtcGxlX2tleTogRU5DW0FFUzI1Nl9HQ00sZGF0YTovNE90aUgzbm9vVFZadXpDK2c9PSxpdjpSNnFyTlk3R0F4Y0l3UjQyYnZFMjc3NHVmK0lmUTlwdjlVQTc4TG1pdFUwPSx0YWc6akZvZ1ZzeWN1ZlNYUnZCS1hMVkE5Zz09LHR5cGU6c3RyXQojRU5DW0FFUzI1Nl9HQ00sZGF0YTphRmpIbCtLSzM3T0lzRmtoQnFiV0FhMD0saXY6WlNRUDY1UWg2aXNnUUw2YVFxazlRNGJQQ3hxZC9rMHcrOXF6bmF1bTFQQT0sdGFnOjdDVXVxbmRHNGIzSjN2L2c1T0tPMlE9PSx0eXBlOmNvbW1lbnRdCmV4YW1wbGVfYXJyYXk6CiAgICAtIEVOQ1tBRVMyNTZfR0NNLGRhdGE6VXhXZnNCYUdjZ3JlZ21EZktNST0saXY6WEJzMk9Db0hXT21vYmoxT1BkL2NsYlVKRmJQVGRBZG90RkhWR1FtMVRQMD0sdGFnOkNUVDE1WllkMW5HOGtKYXJERExaTEE9PSx0eXBlOnN0cl0KICAgIC0gRU5DW0FFUzI1Nl9HQ00sZGF0YTpRUDhaS2E4OG43U25KYUsvWkxrPSxpdjpRN2FCc1dySXYwRzhEMzNJdzY1S1JxT1J3cXlVV2phQXZJQlNiV05IQVd3PSx0YWc6d0JCN3RSY2M2eVJ2NWNBNTNrNWpvZz09LHR5cGU6c3RyXQpleGFtcGxlX251bWJlcjogRU5DW0FFUzI1Nl9HQ00sZGF0YTo0MVF2Sm9DSjVNdUVMUT09LGl2OlFleEpNK3J3aHVaY3hETDNUREVzd3BrOHF3VWVVNmJHazVpNVRDZHVpRWs9LHRhZzpyenlrTVlybUh5djNVSGpFR1JyTkhnPT0sdHlwZTpmbG9hdF0KZXhhbXBsZV9ib29sZWFuczoKICAgIC0gRU5DW0FFUzI1Nl9HQ00sZGF0YToyc0ZoRGc9PSxpdjpoQk1aMHgvMVJKQy8yTmVRL0JFdWdQRFZDQ29udlZIQ1prMVRhazl2bG44PSx0YWc6Q0pxeXg3eUF0Tm5GQUR2RGFrZlhiQT09LHR5cGU6Ym9vbF0KICAgIC0gRU5DW0FFUzI1Nl9HQ00sZGF0YTo1MVQzaDlBPSxpdjowVnV3TUp3VVJUOEF5WkVBcHRRQzVrZDVrK2RmWkRIUjU4TXI0WjRUVklJPSx0YWc6bVRLMXZRRTllWGxoQWtMUW9namRmdz09LHR5cGU6Ym9vbF0Kc29wczoKICAgIGttczoKICAgICAgICAtIGFybjogYXJuOmF3czprbXM6YXdzLXJlZ2lvbi0xOjEyMzQ1Njc4OTAxMTprZXkvMDAwMDAwMDAtMTIzNC00MzIxLWFiY2QtMTIzNGFiY2QxMmFiCiAgICAgICAgICBjcmVhdGVkX2F0OiAiMjAyMi0wNC0wM1QxNzozNDo0NVoiCiAgICAgICAgICBlbmM6IEFRSUNBSGlTZ1pvTFA2ZkRyVUJZWVBVMm9KT0IvM3FGQVI1bUVZdVpZMkRRcXpZckJnR0FQUytTaU81eWIvYlhiVWRvVVBlWkFBQUFmakI4QmdrcWhraUc5dzBCQndhZ2J6QnRBZ0VBTUdnR0NTcUdTSWIzRFFFSEFUQWVCZ2xnaGtnQlpRTUVBUzR3RVFRTVhaUHBQVTNHaWJJT05LNlZBZ0VRZ0R1MTU0WXBuWW9lMmY4WUZ1V2VCcEdYZkRkYXVkNW9NRGZxdXdxWTJVV0c4Y2xuWlY5MzU1eitWN2NxQ2krNFBFQm9hdmVNTExjTFlzTE9BQT09CiAgICAgICAgICBhd3NfcHJvZmlsZTogIiIKICAgIGdjcF9rbXM6IFtdCiAgICBhenVyZV9rdjogW10KICAgIGhjX3ZhdWx0OiBbXQogICAgYWdlOiBbXQogICAgbGFzdG1vZGlmaWVkOiAiMjAyMi0wNC0wM1QxNzozNDo1NloiCiAgICBtYWM6IEVOQ1tBRVMyNTZfR0NNLGRhdGE6cURpL0NhQUdmWGJjQ3hQWHhIUlZuRGlQM2F5TGhMNStPcmR2K3JDeUYvTTBpQmxPNlBFQmV6NG1XKzBKRTRGOEU4YTNsTUpPUmhhRUdUbVJ0R3l6UFhtNjJEb05McFZlcHFqOGZkQk5nenhEUWRiVFgyeWZQb1NsdFNteUNwN2xwbmJVakd3U0hWSHNHSzgrMUVFb29jVG44MWVmUDBoeXRSU29jWUVLT05ZPSxpdjo4dlo0MTdOaHA2Qk8ya0U1SWM0SFJrektqUzc5ZW5yZlZKbHNZZXE1OUZvPSx0YWc6bXBFK090bytIUExkMnRzV3V0ZXVTQT09LHR5cGU6c3RyXQogICAgcGdwOiBbXQogICAgdW5lbmNyeXB0ZWRfc3VmZml4OiBfdW5lbmNyeXB0ZWQKICAgIHZlcnNpb246IDMuNy4yCg==', + Hash: '6d7a109a504c02a3455d09067a3695a4355cf3c2c914b6f3e949e20d6a741128', + }, + }), + }); +}); + test('Throw exception on non existent sops secret', () => { const app = new App(); const stack = new Stack(app, 'SecretIntegration'); @@ -24,6 +64,45 @@ test('Throw exception on non existent sops secret', () => { ).toThrowError('File test-secrets/does-not-exist.json does not exist!'); }); +test('Age Key passed', () => { + const app = new App(); + const stack = new Stack(app, 'SecretIntegration'); + + new SopsSecret(stack, 'SopsSecret', { + sopsFilePath: 'test-secrets/yaml/sopsfile.enc-kms.yaml', + sopsAgeKey: SecretValue.plainText('SOME-KEY'), + }); + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { + Properties: Match.objectLike({ + Environment: Match.objectLike({ + Variables: Match.objectLike({ + SOPS_AGE_KEY: 'SOME-KEY', + }), + }), + }), + }); +}); + +test('Age Key add', () => { + const app = new App(); + const stack = new Stack(app, 'SecretIntegration'); + + const provider = new SopsSyncProvider(stack, 'Provider'); + provider.addAgeKey(SecretValue.plainText('SOME-KEY')); + new SopsSecret(stack, 'SopsSecret', { + sopsFilePath: 'test-secrets/yaml/sopsfile.enc-kms.yaml', + }); + Template.fromStack(stack).hasResource('AWS::Lambda::Function', { + Properties: Match.objectLike({ + Environment: Match.objectLike({ + Variables: Match.objectLike({ + SOPS_AGE_KEY: 'SOME-KEY', + }), + }), + }), + }); +}); + test('KMS Key lookup from sopsfile: json', () => { const app = new App(); const stack = new Stack(app, 'SecretIntegration'); @@ -122,6 +201,18 @@ test('Derive correct format: json', () => { }); }); +test('Exception when derive format: notsupported', () => { + const app = new App(); + const stack = new Stack(app, 'SecretIntegration'); + + expect( + () => + new SopsSecret(stack, 'SopsSecret', { + sopsFilePath: 'test-secrets/json/sopsfile.enc-age.notsupported', + }), + ).toThrowError('Unsupported sopsFileFormat notsupported'); +}); + test('Set format: json', () => { const app = new App(); const stack = new Stack(app, 'SecretIntegration'); @@ -150,19 +241,7 @@ test('Set format: yaml', () => { }); }); -test('Exception when set format: notsupported', () => { - const app = new App(); - const stack = new Stack(app, 'SecretIntegration'); - - expect( - () => - new SopsSecret(stack, 'SopsSecret', { - sopsFilePath: 'test-secrets/json/sopsfile.enc-age.notsupported', - }), - ).toThrowError('Unsupported sopsFileFormat notsupported'); -}); - -test('secretValueFromJson(...)', () => { +test('Methods of SopsSync implemented', () => { const app = new App(); const stack = new Stack(app, 'SecretIntegration'); const secret = new SopsSecret(stack, 'SopsSecret', { @@ -174,7 +253,9 @@ test('secretValueFromJson(...)', () => { runtime: Runtime.NODEJS_14_X, handler: 'test', environment: { - stringValue: secret.secretValueFromJson('test').toString(), + secretValueFromJson: secret.secretValueFromJson('test').toString(), + currentVersionId: secret.currentVersionId().toString(), + secretValue: secret.secretValue.toString(), }, }); @@ -184,7 +265,7 @@ test('secretValueFromJson(...)', () => { Runtime: 'nodejs14.x', Environment: { Variables: { - stringValue: { + secretValueFromJson: { 'Fn::Join': [ '', [ @@ -200,8 +281,50 @@ test('secretValueFromJson(...)', () => { ], ], }, + currentVersionId: { + 'Fn::GetAtt': ['SopsSecretSopsSync7D825417', 'VersionId'], + }, + secretValue: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'SopsSecretF929FB43', + }, + ':SecretString:::', + { + 'Fn::GetAtt': ['SopsSecretSopsSync7D825417', 'VersionId'], + }, + '}}', + ], + ], + }, }, }, }), }); }); + +test('Methods of SopsSync not implemented', () => { + const app = new App(); + const stack = new Stack(app, 'SecretIntegration'); + const secret = new SopsSecret(stack, 'SopsSecret', { + sopsFilePath: 'test-secrets/json/sopsfile.enc-age.json', + }); + + expect(() => secret.addRotationSchedule('something', {})).toThrowError( + `Method addTotationSchedule('something', {}) not allowed as this secret is managed by SopsSync`, + ); + expect(() => + secret.grantWrite( + Role.fromRoleArn( + stack, + 'Role', + 'arn:aws:iam::123456789012:role/SecretAccess', + ), + ), + ).toThrowError( + `Method grantWrite(...) not allowed as this secret is managed by SopsSync`, + ); +});