Skip to content

Commit

Permalink
chore: better test coverage for cdk(#54)
Browse files Browse the repository at this point in the history
improve test coverage
  • Loading branch information
markussiebert authored May 5, 2022
1 parent ef6c7a5 commit 11d4afe
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 41 deletions.
4 changes: 2 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,12 @@ Grants reading the secret value to some role.
##### `grantWrite` <a name="grantWrite" id="cdk-sops-secrets.SopsSecret.grantWrite"></a>

```typescript
public grantWrite(grantee: IGrantable): Grant
public grantWrite(_grantee: IGrantable): Grant
```

Grants writing and updating the secret value to some role.

###### `grantee`<sup>Required</sup> <a name="grantee" id="cdk-sops-secrets.SopsSecret.grantWrite.parameter.grantee"></a>
###### `_grantee`<sup>Required</sup> <a name="_grantee" id="cdk-sops-secrets.SopsSecret.grantWrite.parameter._grantee"></a>

- *Type:* aws-cdk-lib.aws_iam.IGrantable

Expand Down
48 changes: 26 additions & 22 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down
157 changes: 140 additions & 17 deletions test/secret.test.ts
Original file line number Diff line number Diff line change
@@ -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 = [
{
Expand All @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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', {
Expand All @@ -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(),
},
});

Expand All @@ -184,7 +265,7 @@ test('secretValueFromJson(...)', () => {
Runtime: 'nodejs14.x',
Environment: {
Variables: {
stringValue: {
secretValueFromJson: {
'Fn::Join': [
'',
[
Expand All @@ -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`,
);
});

0 comments on commit 11d4afe

Please sign in to comment.