From e8c8d77b94ae5d49cc06d7a11527acf7f3650dcd Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Mon, 7 Dec 2020 16:56:02 +0000 Subject: [PATCH] chore(kms): convert kms tests from nodeunit to jest (#11911) Straight conversion from nodeunit to jest, with no changes, except: * Fixed one error message with a typo * Fixed a throws exception to pass (was erroneously passing before) * Converted a few output-based tests to use `toHaveOutput` rather than synthesizing and doing a partial template match. As always, each time I convert a module from nodeunit to jest I find at least one broken test. Two tests this time! ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-kms/.gitignore | 3 +- packages/@aws-cdk/aws-kms/.npmignore | 3 +- packages/@aws-cdk/aws-kms/jest.config.js | 2 + packages/@aws-cdk/aws-kms/lib/alias.ts | 2 +- packages/@aws-cdk/aws-kms/package.json | 3 +- packages/@aws-cdk/aws-kms/test/alias.test.ts | 220 ++++++++ packages/@aws-cdk/aws-kms/test/key.test.ts | 435 ++++++++++++++ packages/@aws-cdk/aws-kms/test/test.alias.ts | 250 --------- packages/@aws-cdk/aws-kms/test/test.key.ts | 529 ------------------ .../test/test.via-service-principal.ts | 50 -- .../test/via-service-principal.test.ts | 44 ++ 11 files changed, 707 insertions(+), 834 deletions(-) create mode 100644 packages/@aws-cdk/aws-kms/jest.config.js create mode 100644 packages/@aws-cdk/aws-kms/test/alias.test.ts create mode 100644 packages/@aws-cdk/aws-kms/test/key.test.ts delete mode 100644 packages/@aws-cdk/aws-kms/test/test.alias.ts delete mode 100644 packages/@aws-cdk/aws-kms/test/test.key.ts delete mode 100644 packages/@aws-cdk/aws-kms/test/test.via-service-principal.ts create mode 100644 packages/@aws-cdk/aws-kms/test/via-service-principal.test.ts diff --git a/packages/@aws-cdk/aws-kms/.gitignore b/packages/@aws-cdk/aws-kms/.gitignore index 86fc837df8fca..a82230b5888d0 100644 --- a/packages/@aws-cdk/aws-kms/.gitignore +++ b/packages/@aws-cdk/aws-kms/.gitignore @@ -15,4 +15,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kms/.npmignore b/packages/@aws-cdk/aws-kms/.npmignore index a94c531529866..9e88226921c33 100644 --- a/packages/@aws-cdk/aws-kms/.npmignore +++ b/packages/@aws-cdk/aws-kms/.npmignore @@ -23,4 +23,5 @@ tsconfig.json # exclude cdk artifacts **/cdk.out junit.xml -test/ \ No newline at end of file +test/ +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kms/jest.config.js b/packages/@aws-cdk/aws-kms/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-kms/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kms/lib/alias.ts b/packages/@aws-cdk/aws-kms/lib/alias.ts index 7e622ef95fae9..af538f6f17780 100644 --- a/packages/@aws-cdk/aws-kms/lib/alias.ts +++ b/packages/@aws-cdk/aws-kms/lib/alias.ts @@ -151,7 +151,7 @@ export class Alias extends AliasBase { public readonly keyArn = Stack.of(this).formatArn({ service: 'kms', resource: aliasName }); public readonly keyId = aliasName; public readonly aliasName = aliasName; - public get aliasTargetKey(): IKey { throw new Error('Cannot access aliasTargetKey on an Alias imnported by Alias.fromAliasName().'); } + public get aliasTargetKey(): IKey { throw new Error('Cannot access aliasTargetKey on an Alias imported by Alias.fromAliasName().'); } public addAlias(_alias: string): Alias { throw new Error('Cannot call addAlias on an Alias imported by Alias.fromAliasName().'); } public addToResourcePolicy(_statement: iam.PolicyStatement, _allowNoOp?: boolean): iam.AddToResourcePolicyResult { return { statementAdded: false }; diff --git a/packages/@aws-cdk/aws-kms/package.json b/packages/@aws-cdk/aws-kms/package.json index d9c0c40fc3239..65c05ca946fbf 100644 --- a/packages/@aws-cdk/aws-kms/package.json +++ b/packages/@aws-cdk/aws-kms/package.json @@ -55,6 +55,7 @@ }, "cdk-build": { "cloudformation": "AWS::KMS", + "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": "true" } @@ -73,11 +74,9 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-kms/test/alias.test.ts b/packages/@aws-cdk/aws-kms/test/alias.test.ts new file mode 100644 index 0000000000000..b39ea8bf5b232 --- /dev/null +++ b/packages/@aws-cdk/aws-kms/test/alias.test.ts @@ -0,0 +1,220 @@ +import '@aws-cdk/assert/jest'; +import { ArnPrincipal, PolicyStatement } from '@aws-cdk/aws-iam'; +import { App, CfnOutput, Construct, Stack } from '@aws-cdk/core'; +import { Alias } from '../lib/alias'; +import { IKey, Key } from '../lib/key'; + +test('default alias', () => { + const app = new App(); + const stack = new Stack(app, 'Test'); + const key = new Key(stack, 'Key'); + + new Alias(stack, 'Alias', { targetKey: key, aliasName: 'alias/foo' }); + + expect(stack).toHaveResource('AWS::KMS::Alias', { + AliasName: 'alias/foo', + TargetKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, + }); +}); + +test('add "alias/" prefix if not given.', () => { + const app = new App(); + const stack = new Stack(app, 'Test'); + + const key = new Key(stack, 'Key', { + enableKeyRotation: true, + enabled: false, + }); + + new Alias(stack, 'Alias', { + aliasName: 'foo', + targetKey: key, + }); + + expect(stack).toHaveResource('AWS::KMS::Alias', { + AliasName: 'alias/foo', + TargetKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, + }); +}); + +test('can create alias directly while creating the key', () => { + const app = new App(); + const stack = new Stack(app, 'Test'); + + new Key(stack, 'Key', { + enableKeyRotation: true, + enabled: false, + alias: 'foo', + }); + + expect(stack).toHaveResource('AWS::KMS::Alias', { + AliasName: 'alias/foo', + TargetKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, + }); +}); + +test('fails if alias is "alias/" (and nothing more)', () => { + const app = new App(); + const stack = new Stack(app, 'Test'); + + const key = new Key(stack, 'MyKey', { + enableKeyRotation: true, + enabled: false, + }); + + expect(() => new Alias(stack, 'Alias', { + aliasName: 'alias/', + targetKey: key, + })).toThrow(/Alias must include a value after/); +}); + +test('fails if alias contains illegal characters', () => { + const app = new App(); + const stack = new Stack(app, 'Test'); + + const key = new Key(stack, 'MyKey', { + enableKeyRotation: true, + enabled: false, + }); + + expect(() => new Alias(stack, 'Alias', { + aliasName: 'alias/@Nope', + targetKey: key, + })).toThrow('a-zA-Z0-9:/_-'); +}); + +test('fails if alias starts with "alias/aws/"', () => { + const app = new App(); + const stack = new Stack(app, 'Test'); + + const key = new Key(stack, 'MyKey', { + enableKeyRotation: true, + enabled: false, + }); + + expect(() => new Alias(stack, 'Alias1', { + aliasName: 'alias/aws/', + targetKey: key, + })).toThrow(/Alias cannot start with alias\/aws\/: alias\/aws\//); + + expect(() => new Alias(stack, 'Alias2', { + aliasName: 'alias/aws/Awesome', + targetKey: key, + })).toThrow(/Alias cannot start with alias\/aws\/: alias\/aws\/Awesome/); + + expect(() => new Alias(stack, 'Alias3', { + aliasName: 'alias/AWS/awesome', + targetKey: key, + })).toThrow(/Alias cannot start with alias\/aws\/: alias\/AWS\/awesome/); +}); + +test('can be used wherever a key is expected', () => { + const stack = new Stack(); + + const myKey = new Key(stack, 'MyKey', { + enableKeyRotation: true, + enabled: false, + }); + const myAlias = new Alias(stack, 'MyAlias', { + targetKey: myKey, + aliasName: 'alias/myAlias', + }); + + /* eslint-disable cdk/no-core-construct */ + class MyConstruct extends Construct { + constructor(scope: Construct, id: string, key: IKey) { + super(scope, id); + + new CfnOutput(stack, 'OutId', { + value: key.keyId, + }); + new CfnOutput(stack, 'OutArn', { + value: key.keyArn, + }); + } + } + new MyConstruct(stack, 'MyConstruct', myAlias); + /* eslint-enable cdk/no-core-construct */ + + expect(stack).toHaveOutput({ + outputName: 'OutId', + outputValue: 'alias/myAlias', + }); + expect(stack).toHaveOutput({ + outputName: 'OutArn', + outputValue: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':kms:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':alias/myAlias', + ]], + }, + }); +}); + +test('imported alias by name - can be used where a key is expected', () => { + const stack = new Stack(); + + const myAlias = Alias.fromAliasName(stack, 'MyAlias', 'alias/myAlias'); + + /* eslint-disable cdk/no-core-construct */ + class MyConstruct extends Construct { + constructor(scope: Construct, id: string, key: IKey) { + super(scope, id); + + new CfnOutput(stack, 'OutId', { + value: key.keyId, + }); + new CfnOutput(stack, 'OutArn', { + value: key.keyArn, + }); + } + } + new MyConstruct(stack, 'MyConstruct', myAlias); + /* eslint-enable cdk/no-core-construct */ + + expect(stack).toHaveOutput({ + outputName: 'OutId', + outputValue: 'alias/myAlias', + }); + expect(stack).toHaveOutput({ + outputName: 'OutArn', + outputValue: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':kms:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':alias/myAlias', + ]], + }, + }); +}); + +test('imported alias by name - will throw an error when accessing the key', () => { + const stack = new Stack(); + + const myAlias = Alias.fromAliasName(stack, 'MyAlias', 'alias/myAlias'); + + expect(() => myAlias.aliasTargetKey).toThrow('Cannot access aliasTargetKey on an Alias imported by Alias.fromAliasName().'); +}); + +test('fails if alias policy is invalid', () => { + const app = new App(); + const stack = new Stack(app, 'my-stack'); + const key = new Key(stack, 'MyKey'); + const alias = new Alias(stack, 'Alias', { targetKey: key, aliasName: 'alias/foo' }); + + alias.addToResourcePolicy(new PolicyStatement({ + resources: ['*'], + principals: [new ArnPrincipal('arn')], + })); + + expect(() => app.synth()).toThrow(/A PolicyStatement must specify at least one \'action\' or \'notAction\'/); +}); diff --git a/packages/@aws-cdk/aws-kms/test/key.test.ts b/packages/@aws-cdk/aws-kms/test/key.test.ts new file mode 100644 index 0000000000000..93b5d1bc67e31 --- /dev/null +++ b/packages/@aws-cdk/aws-kms/test/key.test.ts @@ -0,0 +1,435 @@ +import { ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import { App, CfnOutput, RemovalPolicy, Stack, Tags } from '@aws-cdk/core'; +import { Key } from '../lib'; + +const ACTIONS: string[] = [ + 'kms:Create*', + 'kms:Describe*', + 'kms:Enable*', + 'kms:List*', + 'kms:Put*', + 'kms:Update*', + 'kms:Revoke*', + 'kms:Disable*', + 'kms:Get*', + 'kms:Delete*', + 'kms:ScheduleKeyDeletion', + 'kms:CancelKeyDeletion', + 'kms:GenerateDataKey', + 'kms:TagResource', + 'kms:UntagResource', +]; + +test('default key', () => { + const stack = new Stack(); + + new Key(stack, 'MyKey'); + + expect(stack).toMatchTemplate({ + Resources: { + MyKey6AB29FA6: { + Type: 'AWS::KMS::Key', + Properties: { + KeyPolicy: { + Statement: [ + { + Action: ACTIONS, + Effect: 'Allow', + Principal: { + AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] }, + }, + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, + }, + }); +}); + +test('default with no retention', () => { + const app = new App(); + const stack = new Stack(app, 'TestStack'); + + new Key(stack, 'MyKey', { removalPolicy: RemovalPolicy.DESTROY }); + + expect(stack).toHaveResource('AWS::KMS::Key', { DeletionPolicy: 'Delete', UpdateReplacePolicy: 'Delete' }, ResourcePart.CompleteDefinition); +}); + +test('default with some permission', () => { + const app = new App(); + const stack = new Stack(app, 'Test'); + + const key = new Key(stack, 'MyKey'); + const p = new iam.PolicyStatement({ resources: ['*'], actions: ['kms:encrypt'] }); + p.addArnPrincipal('arn'); + key.addToResourcePolicy(p); + + expect(stack).toMatchTemplate({ + Resources: { + MyKey6AB29FA6: { + Type: 'AWS::KMS::Key', + Properties: { + KeyPolicy: { + Statement: [ + { + Action: ACTIONS, + Effect: 'Allow', + Principal: { + AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] }, + }, + Resource: '*', + }, + { + Action: 'kms:encrypt', + Effect: 'Allow', + Principal: { + AWS: 'arn', + }, + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, + }, + }); + +}); + +test('key with some options', () => { + const stack = new Stack(); + + const key = new Key(stack, 'MyKey', { + enableKeyRotation: true, + enabled: false, + }); + const p = new iam.PolicyStatement({ resources: ['*'], actions: ['kms:encrypt'] }); + p.addArnPrincipal('arn'); + key.addToResourcePolicy(p); + + Tags.of(key).add('tag1', 'value1'); + Tags.of(key).add('tag2', 'value2'); + Tags.of(key).add('tag3', ''); + + expect(stack).toMatchTemplate({ + Resources: { + MyKey6AB29FA6: { + Type: 'AWS::KMS::Key', + Properties: { + KeyPolicy: { + Statement: [ + { + Action: ACTIONS, + Effect: 'Allow', + Principal: { + AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] }, + }, + Resource: '*', + }, + { + Action: 'kms:encrypt', + Effect: 'Allow', + Principal: { + AWS: 'arn', + }, + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + Enabled: false, + EnableKeyRotation: true, + Tags: [ + { + Key: 'tag1', + Value: 'value1', + }, + { + Key: 'tag2', + Value: 'value2', + }, + { + Key: 'tag3', + Value: '', + }, + ], + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, + }, + }); +}); + +test('addAlias creates an alias', () => { + const app = new App(); + const stack = new Stack(app, 'Test'); + + const key = new Key(stack, 'MyKey', { + enableKeyRotation: true, + enabled: false, + }); + + const alias = key.addAlias('alias/xoo'); + expect(alias.aliasName).toBeDefined(); + + expect(stack).toCountResources('AWS::KMS::Alias', 1); + expect(stack).toHaveResource('AWS::KMS::Alias', { + AliasName: 'alias/xoo', + TargetKeyId: { + 'Fn::GetAtt': [ + 'MyKey6AB29FA6', + 'Arn', + ], + }, + }); +}); + +test('can run multiple addAlias', () => { + const app = new App(); + const stack = new Stack(app, 'Test'); + + const key = new Key(stack, 'MyKey', { + enableKeyRotation: true, + enabled: false, + }); + + const alias1 = key.addAlias('alias/alias1'); + const alias2 = key.addAlias('alias/alias2'); + expect(alias1.aliasName).toBeDefined(); + expect(alias2.aliasName).toBeDefined(); + + expect(stack).toCountResources('AWS::KMS::Alias', 2); + expect(stack).toHaveResource('AWS::KMS::Alias', { + AliasName: 'alias/alias1', + TargetKeyId: { + 'Fn::GetAtt': [ + 'MyKey6AB29FA6', + 'Arn', + ], + }, + }); + expect(stack).toHaveResource('AWS::KMS::Alias', { + AliasName: 'alias/alias2', + TargetKeyId: { + 'Fn::GetAtt': [ + 'MyKey6AB29FA6', + 'Arn', + ], + }, + }); +}); + +test('grant decrypt on a key', () => { + // GIVEN + const stack = new Stack(); + const key = new Key(stack, 'Key'); + const user = new iam.User(stack, 'User'); + + // WHEN + key.grantDecrypt(user); + + // THEN + expect(stack).toHaveResource('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + // This one is there by default + { + Action: ACTIONS, + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] } }, + Resource: '*', + }, + // This is the interesting one + { + Action: 'kms:Decrypt', + Effect: 'Allow', + Principal: { AWS: { 'Fn::GetAtt': ['User00B015A1', 'Arn'] } }, + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'kms:Decrypt', + Effect: 'Allow', + Resource: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, + }, + ], + Version: '2012-10-17', + }, + }); + +}); + +test('grant for a principal in a dependent stack works correctly', () => { + const app = new App(); + + const principalStack = new Stack(app, 'PrincipalStack'); + const principal = new iam.Role(principalStack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + }); + + const keyStack = new Stack(app, 'KeyStack'); + const key = new Key(keyStack, 'Key'); + + principalStack.addDependency(keyStack); + + key.grantEncrypt(principal); + + expect(keyStack).toHaveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + { + // owning account management permissions - we don't care about them in this test + }, + { + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Effect: 'Allow', + Principal: { + AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] }, + }, + Resource: '*', + }, + ], + }, + }); + +}); + +test('keyId resolves to a Ref', () => { + const stack = new Stack(); + const key = new Key(stack, 'MyKey'); + + new CfnOutput(stack, 'Out', { + value: key.keyId, + }); + + expect(stack).toHaveOutput({ + outputName: 'Out', + outputValue: { Ref: 'MyKey6AB29FA6' }, + }); +}); + +test('enablePolicyControl changes key policy to allow IAM control', () => { + const stack = new Stack(); + new Key(stack, 'MyKey', { trustAccountIdentities: true }); + expect(stack).toHaveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + { + Action: 'kms:*', + Effect: 'Allow', + Principal: { + AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] }, + }, + Resource: '*', + }, + ], + }, + }); +}); + +test('fails if key policy has no actions', () => { + const app = new App(); + const stack = new Stack(app, 'my-stack'); + const key = new Key(stack, 'MyKey'); + + key.addToResourcePolicy(new iam.PolicyStatement({ + resources: ['*'], + principals: [new iam.ArnPrincipal('arn')], + })); + + expect(() => app.synth()).toThrow(/A PolicyStatement must specify at least one \'action\' or \'notAction\'/); +}); + +test('fails if key policy has no IAM principals', () => { + const app = new App(); + const stack = new Stack(app, 'my-stack'); + const key = new Key(stack, 'MyKey'); + + key.addToResourcePolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: ['kms:*'], + })); + + expect(() => app.synth()).toThrow(/A PolicyStatement used in a resource-based policy must specify at least one IAM principal/); +}); + +describe('imported keys', () => { + test('throw an error when providing something that is not a valid key ARN', () => { + const stack = new Stack(); + + expect(() => { + Key.fromKeyArn(stack, 'Imported', 'arn:aws:kms:us-east-1:123456789012:key'); + }).toThrow(/KMS key ARN must be in the format 'arn:aws:kms:::key\/', got: 'arn:aws:kms:us-east-1:123456789012:key'/); + + }); + + test('can have aliases added to them', () => { + const stack2 = new Stack(); + const myKeyImported = Key.fromKeyArn(stack2, 'MyKeyImported', + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'); + + // addAlias can be called on imported keys. + myKeyImported.addAlias('alias/hello'); + + expect(myKeyImported.keyId).toEqual('12345678-1234-1234-1234-123456789012'); + + expect(stack2).toMatchTemplate({ + Resources: { + MyKeyImportedAliasB1C5269F: { + Type: 'AWS::KMS::Alias', + Properties: { + AliasName: 'alias/hello', + TargetKeyId: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', + }, + }, + }, + }); + }); + +}); + +describe('addToResourcePolicy allowNoOp and there is no policy', () => { + test('succeed if set to true (default)', () => { + const stack = new Stack(); + + const key = Key.fromKeyArn(stack, 'Imported', + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'); + + key.addToResourcePolicy(new iam.PolicyStatement({ resources: ['*'], actions: ['*'] })); + + }); + + test('fails if set to false', () => { + const stack = new Stack(); + + const key = Key.fromKeyArn(stack, 'Imported', + 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'); + + expect(() => { + key.addToResourcePolicy(new iam.PolicyStatement({ resources: ['*'], actions: ['*'] }), /* allowNoOp */ false); + }).toThrow('Unable to add statement to IAM resource policy for KMS key: "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"'); + + }); +}); diff --git a/packages/@aws-cdk/aws-kms/test/test.alias.ts b/packages/@aws-cdk/aws-kms/test/test.alias.ts deleted file mode 100644 index 7d10d326ebf96..0000000000000 --- a/packages/@aws-cdk/aws-kms/test/test.alias.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { expect, haveResource, SynthUtils } from '@aws-cdk/assert'; -import { ArnPrincipal, PolicyStatement } from '@aws-cdk/aws-iam'; -import { App, CfnOutput, Construct, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import { Alias } from '../lib/alias'; -import { IKey, Key } from '../lib/key'; - -/* eslint-disable quote-props */ - -export = { - 'default alias'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'Test'); - const key = new Key(stack, 'Key'); - - new Alias(stack, 'Alias', { targetKey: key, aliasName: 'alias/foo' }); - - expect(stack).to(haveResource('AWS::KMS::Alias', { - AliasName: 'alias/foo', - TargetKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, - })); - - test.done(); - }, - - 'add "alias/" prefix if not given.'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'Test'); - - const key = new Key(stack, 'Key', { - enableKeyRotation: true, - enabled: false, - }); - - new Alias(stack, 'Alias', { - aliasName: 'foo', - targetKey: key, - }); - - expect(stack).to(haveResource('AWS::KMS::Alias', { - AliasName: 'alias/foo', - TargetKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, - })); - - test.done(); - }, - - 'can create alias directly while creating the key'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'Test'); - - new Key(stack, 'Key', { - enableKeyRotation: true, - enabled: false, - alias: 'foo', - }); - - expect(stack).to(haveResource('AWS::KMS::Alias', { - AliasName: 'alias/foo', - TargetKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, - })); - - test.done(); - }, - - 'fails if alias is "alias/" (and nothing more)'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'Test'); - - const key = new Key(stack, 'MyKey', { - enableKeyRotation: true, - enabled: false, - }); - - test.throws(() => new Alias(stack, 'Alias', { - aliasName: 'alias/', - targetKey: key, - })); - - test.done(); - }, - - 'fails if alias contains illegal characters'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'Test'); - - const key = new Key(stack, 'MyKey', { - enableKeyRotation: true, - enabled: false, - }); - - test.throws(() => new Alias(stack, 'Alias', { - aliasName: 'alias/@Nope', - targetKey: key, - }), 'a-zA-Z0-9:/_-'); - - test.done(); - }, - - 'fails if alias starts with "alias/aws/"'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'Test'); - - const key = new Key(stack, 'MyKey', { - enableKeyRotation: true, - enabled: false, - }); - - test.throws(() => new Alias(stack, 'Alias1', { - aliasName: 'alias/aws/', - targetKey: key, - }), /Alias cannot start with alias\/aws\/: alias\/aws\//); - - test.throws(() => new Alias(stack, 'Alias2', { - aliasName: 'alias/aws/Awesome', - targetKey: key, - }), /Alias cannot start with alias\/aws\/: alias\/aws\/Awesome/); - - test.throws(() => new Alias(stack, 'Alias3', { - aliasName: 'alias/AWS/awesome', - targetKey: key, - }), /Alias cannot start with alias\/aws\/: alias\/AWS\/awesome/); - - test.done(); - }, - - 'can be used wherever a key is expected'(test: Test) { - const stack = new Stack(); - - const myKey = new Key(stack, 'MyKey', { - enableKeyRotation: true, - enabled: false, - }); - const myAlias = new Alias(stack, 'MyAlias', { - targetKey: myKey, - aliasName: 'alias/myAlias', - }); - - /* eslint-disable cdk/no-core-construct */ - class MyConstruct extends Construct { - constructor(scope: Construct, id: string, key: IKey) { - super(scope, id); - - new CfnOutput(stack, 'OutId', { - value: key.keyId, - }); - new CfnOutput(stack, 'OutArn', { - value: key.keyArn, - }); - } - } - /* eslint-enable cdk/no-core-construct */ - - new MyConstruct(stack, 'MyConstruct', myAlias); - - const template = SynthUtils.synthesize(stack).template.Outputs; - - test.deepEqual(template, { - 'OutId': { - 'Value': 'alias/myAlias', - }, - 'OutArn': { - 'Value': { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':kms:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':alias/myAlias', - ]], - }, - }, - }); - - test.done(); - }, - - 'imported alias by name - can be used where a key is expected'(test: Test) { - const stack = new Stack(); - - const myAlias = Alias.fromAliasName(stack, 'MyAlias', 'alias/myAlias'); - - /* eslint-disable cdk/no-core-construct */ - class MyConstruct extends Construct { - constructor(scope: Construct, id: string, key: IKey) { - super(scope, id); - - new CfnOutput(stack, 'OutId', { - value: key.keyId, - }); - new CfnOutput(stack, 'OutArn', { - value: key.keyArn, - }); - } - } - /* eslint-enable cdk/no-core-construct */ - - new MyConstruct(stack, 'MyConstruct', myAlias); - - const template = SynthUtils.synthesize(stack).template.Outputs; - - test.deepEqual(template, { - 'OutId': { - 'Value': 'alias/myAlias', - }, - 'OutArn': { - 'Value': { - 'Fn::Join': ['', [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':kms:', - { Ref: 'AWS::Region' }, - ':', - { Ref: 'AWS::AccountId' }, - ':alias/myAlias', - ]], - }, - }, - }); - - test.done(); - }, - - 'imported alias by name - will throw an error when accessing the key'(test: Test) { - const stack = new Stack(); - - const myAlias = Alias.fromAliasName(stack, 'MyAlias', 'alias/myAlias'); - - test.throws(() => myAlias.aliasTargetKey, 'Cannot access aliasTargetKey on an Alias imported by Alias.fromAliasName().'); - - test.done(); - }, - - 'fails if alias policy is invalid'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'my-stack'); - const key = new Key(stack, 'MyKey'); - const alias = new Alias(stack, 'Alias', { targetKey: key, aliasName: 'alias/foo' }); - - alias.addToResourcePolicy(new PolicyStatement({ - resources: ['*'], - principals: [new ArnPrincipal('arn')], - })); - - test.throws(() => app.synth(), /A PolicyStatement must specify at least one \'action\' or \'notAction\'/); - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-kms/test/test.key.ts b/packages/@aws-cdk/aws-kms/test/test.key.ts deleted file mode 100644 index bcbaaec7f1c13..0000000000000 --- a/packages/@aws-cdk/aws-kms/test/test.key.ts +++ /dev/null @@ -1,529 +0,0 @@ -import { - countResources, - exactlyMatchTemplate, - expect, - haveResource, - haveResourceLike, - ResourcePart, - SynthUtils, -} from '@aws-cdk/assert'; -import * as iam from '@aws-cdk/aws-iam'; -import { App, CfnOutput, RemovalPolicy, Stack, Tags } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import { Key } from '../lib'; - -/* eslint-disable quote-props */ -const ACTIONS: string[] = [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', -]; - -export = { - 'default key'(test: Test) { - const stack = new Stack(); - - new Key(stack, 'MyKey'); - - expect(stack).to(exactlyMatchTemplate({ - Resources: { - MyKey6AB29FA6: { - Type: 'AWS::KMS::Key', - Properties: { - KeyPolicy: { - Statement: [ - { - Action: ACTIONS, - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, - ], - Version: '2012-10-17', - }, - }, - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', - }, - }, - })); - test.done(); - }, - - 'default with no retention'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'TestStack'); - - new Key(stack, 'MyKey', { removalPolicy: RemovalPolicy.DESTROY }); - - expect(stack).to(haveResource('AWS::KMS::Key', { DeletionPolicy: 'Delete', UpdateReplacePolicy: 'Delete' }, ResourcePart.CompleteDefinition)); - test.done(); - }, - - 'default with some permission'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'Test'); - - const key = new Key(stack, 'MyKey'); - const p = new iam.PolicyStatement({ resources: ['*'], actions: ['kms:encrypt'] }); - p.addArnPrincipal('arn'); - key.addToResourcePolicy(p); - - expect(stack).to(exactlyMatchTemplate({ - Resources: { - MyKey6AB29FA6: { - Type: 'AWS::KMS::Key', - Properties: { - KeyPolicy: { - Statement: [ - { - Action: ACTIONS, - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, - { - Action: 'kms:encrypt', - Effect: 'Allow', - Principal: { - AWS: 'arn', - }, - Resource: '*', - }, - ], - Version: '2012-10-17', - }, - }, - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', - }, - }, - })); - - test.done(); - }, - - 'key with some options'(test: Test) { - const stack = new Stack(); - - const key = new Key(stack, 'MyKey', { - enableKeyRotation: true, - enabled: false, - }); - const p = new iam.PolicyStatement({ resources: ['*'], actions: ['kms:encrypt'] }); - p.addArnPrincipal('arn'); - key.addToResourcePolicy(p); - - Tags.of(key).add('tag1', 'value1'); - Tags.of(key).add('tag2', 'value2'); - Tags.of(key).add('tag3', ''); - - expect(stack).to(exactlyMatchTemplate({ - Resources: { - MyKey6AB29FA6: { - Type: 'AWS::KMS::Key', - Properties: { - KeyPolicy: { - Statement: [ - { - Action: ACTIONS, - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, - { - Action: 'kms:encrypt', - Effect: 'Allow', - Principal: { - AWS: 'arn', - }, - Resource: '*', - }, - ], - Version: '2012-10-17', - }, - Enabled: false, - EnableKeyRotation: true, - Tags: [ - { - Key: 'tag1', - Value: 'value1', - }, - { - Key: 'tag2', - Value: 'value2', - }, - { - Key: 'tag3', - Value: '', - }, - ], - }, - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', - }, - }, - })); - - test.done(); - }, - - 'addAlias creates an alias'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'Test'); - - const key = new Key(stack, 'MyKey', { - enableKeyRotation: true, - enabled: false, - }); - - const alias = key.addAlias('alias/xoo'); - test.ok(alias.aliasName); - - expect(stack).to(countResources('AWS::KMS::Alias', 1)); - expect(stack).to(haveResource('AWS::KMS::Alias', { - AliasName: 'alias/xoo', - TargetKeyId: { - 'Fn::GetAtt': [ - 'MyKey6AB29FA6', - 'Arn', - ], - }, - })); - test.done(); - }, - - 'can run multiple addAlias'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'Test'); - - const key = new Key(stack, 'MyKey', { - enableKeyRotation: true, - enabled: false, - }); - - const alias1 = key.addAlias('alias/alias1'); - const alias2 = key.addAlias('alias/alias2'); - test.ok(alias1.aliasName); - test.ok(alias2.aliasName); - - expect(stack).to(countResources('AWS::KMS::Alias', 2)); - expect(stack).to(haveResource('AWS::KMS::Alias', { - AliasName: 'alias/alias1', - TargetKeyId: { - 'Fn::GetAtt': [ - 'MyKey6AB29FA6', - 'Arn', - ], - }, - })); - expect(stack).to(haveResource('AWS::KMS::Alias', { - AliasName: 'alias/alias2', - TargetKeyId: { - 'Fn::GetAtt': [ - 'MyKey6AB29FA6', - 'Arn', - ], - }, - })); - test.done(); - }, - - 'grant decrypt on a key'(test: Test) { - // GIVEN - const stack = new Stack(); - const key = new Key(stack, 'Key'); - const user = new iam.User(stack, 'User'); - - // WHEN - key.grantDecrypt(user); - - // THEN - expect(stack).to(haveResource('AWS::KMS::Key', { - KeyPolicy: { - Statement: [ - // This one is there by default - { - Action: ACTIONS, - Effect: 'Allow', - Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] } }, - Resource: '*', - }, - // This is the interesting one - { - Action: 'kms:Decrypt', - Effect: 'Allow', - Principal: { AWS: { 'Fn::GetAtt': ['User00B015A1', 'Arn'] } }, - Resource: '*', - }, - ], - Version: '2012-10-17', - }, - })); - - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: 'kms:Decrypt', - Effect: 'Allow', - Resource: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, - }, - ], - Version: '2012-10-17', - }, - })); - - test.done(); - }, - - 'grant for a principal in a dependent stack works correctly'(test: Test) { - const app = new App(); - - const principalStack = new Stack(app, 'PrincipalStack'); - const principal = new iam.Role(principalStack, 'Role', { - assumedBy: new iam.AnyPrincipal(), - }); - - const keyStack = new Stack(app, 'KeyStack'); - const key = new Key(keyStack, 'Key'); - - principalStack.addDependency(keyStack); - - key.grantEncrypt(principal); - - expect(keyStack).to(haveResourceLike('AWS::KMS::Key', { - 'KeyPolicy': { - 'Statement': [ - { - // owning account management permissions - we don't care about them in this test - }, - { - 'Action': [ - 'kms:Encrypt', - 'kms:ReEncrypt*', - 'kms:GenerateDataKey*', - ], - 'Effect': 'Allow', - 'Principal': { - 'AWS': { - 'Fn::Join': ['', [ - 'arn:', - { 'Ref': 'AWS::Partition' }, - ':iam::', - { 'Ref': 'AWS::AccountId' }, - ':root', - ]], - }, - }, - 'Resource': '*', - }, - ], - }, - })); - - test.done(); - }, - - 'keyId resolves to a Ref'(test: Test) { - const stack = new Stack(); - const key = new Key(stack, 'MyKey'); - - new CfnOutput(stack, 'Out', { - value: key.keyId, - }); - - const template = SynthUtils.synthesize(stack).template.Outputs; - - test.deepEqual(template, { - 'Out': { - 'Value': { - 'Ref': 'MyKey6AB29FA6', - }, - }, - }); - - test.done(); - }, - 'enablePolicyControl changes key policy to allow IAM control'(test: Test) { - const stack = new Stack(); - new Key(stack, 'MyKey', { trustAccountIdentities: true }); - expect(stack).to(haveResourceLike('AWS::KMS::Key', { - 'KeyPolicy': { - 'Statement': [ - { - 'Action': 'kms:*', - 'Effect': 'Allow', - 'Principal': { - 'AWS': { - 'Fn::Join': ['', [ - 'arn:', - { 'Ref': 'AWS::Partition' }, - ':iam::', - { 'Ref': 'AWS::AccountId' }, - ':root', - ]], - }, - }, - 'Resource': '*', - }, - ], - }, - })); - test.done(); - }, - - 'fails if key policy has no actions'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'my-stack'); - const key = new Key(stack, 'MyKey'); - - key.addToResourcePolicy(new iam.PolicyStatement({ - resources: ['*'], - principals: [new iam.ArnPrincipal('arn')], - })); - - test.throws(() => app.synth(), /A PolicyStatement must specify at least one \'action\' or \'notAction\'/); - test.done(); - }, - - 'fails if key policy has no IAM principals'(test: Test) { - const app = new App(); - const stack = new Stack(app, 'my-stack'); - const key = new Key(stack, 'MyKey'); - - key.addToResourcePolicy(new iam.PolicyStatement({ - resources: ['*'], - actions: ['kms:*'], - })); - - test.throws(() => app.synth(), /A PolicyStatement used in a resource-based policy must specify at least one IAM principal/); - test.done(); - }, - - 'imported keys': { - 'throw an error when providing something that is not a valid key ARN'(test: Test) { - const stack = new Stack(); - - test.throws(() => { - Key.fromKeyArn(stack, 'Imported', 'arn:aws:kms:us-east-1:123456789012:key'); - }, /KMS key ARN must be in the format 'arn:aws:kms:::key\/', got: 'arn:aws:kms:us-east-1:123456789012:key'/); - - test.done(); - }, - - 'can have aliases added to them'(test: Test) { - const stack2 = new Stack(); - const myKeyImported = Key.fromKeyArn(stack2, 'MyKeyImported', - 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'); - - // addAlias can be called on imported keys. - myKeyImported.addAlias('alias/hello'); - - test.equal(myKeyImported.keyId, '12345678-1234-1234-1234-123456789012'); - - expect(stack2).toMatch({ - Resources: { - MyKeyImportedAliasB1C5269F: { - Type: 'AWS::KMS::Alias', - Properties: { - AliasName: 'alias/hello', - TargetKeyId: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012', - }, - }, - }, - }); - - test.done(); - }, - - 'addToResourcePolicy allowNoOp and there is no policy': { - 'succeed if set to true (default)'(test: Test) { - const stack = new Stack(); - - const key = Key.fromKeyArn(stack, 'Imported', - 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'); - - key.addToResourcePolicy(new iam.PolicyStatement({ resources: ['*'], actions: ['*'] })); - - test.done(); - }, - - 'fails if set to false'(test: Test) { - const stack = new Stack(); - - const key = Key.fromKeyArn(stack, 'Imported', - 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'); - - test.throws(() => { - key.addToResourcePolicy(new iam.PolicyStatement({ resources: ['*'], actions: ['*'] }), /* allowNoOp */ false); - }, 'Unable to add statement to IAM resource policy for KMS key: "foo/bar"'); - - test.done(); - }, - }, - }, -}; diff --git a/packages/@aws-cdk/aws-kms/test/test.via-service-principal.ts b/packages/@aws-cdk/aws-kms/test/test.via-service-principal.ts deleted file mode 100644 index 5e415c6057f5b..0000000000000 --- a/packages/@aws-cdk/aws-kms/test/test.via-service-principal.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as iam from '@aws-cdk/aws-iam'; -import { Test } from 'nodeunit'; -import * as kms from '../lib'; - -export = { - 'Via service, any principal'(test: Test) { - // WHEN - const statement = new iam.PolicyStatement({ - actions: ['abc:call'], - principals: [new kms.ViaServicePrincipal('bla.amazonaws.com')], - resources: ['*'], - }); - - // THEN - test.deepEqual(statement.toStatementJson(), { - Action: 'abc:call', - Condition: { StringEquals: { 'kms:ViaService': 'bla.amazonaws.com' } }, - Effect: 'Allow', - Principal: '*', - Resource: '*', - }); - - test.done(); - }, - - 'Via service, principal with conditions'(test: Test) { - // WHEN - const statement = new iam.PolicyStatement({ - actions: ['abc:call'], - principals: [new kms.ViaServicePrincipal('bla.amazonaws.com', new iam.OrganizationPrincipal('o-1234'))], - resources: ['*'], - }); - - // THEN - test.deepEqual(statement.toStatementJson(), { - Action: 'abc:call', - Condition: { - StringEquals: { - 'kms:ViaService': 'bla.amazonaws.com', - 'aws:PrincipalOrgID': 'o-1234', - }, - }, - Effect: 'Allow', - Principal: '*', - Resource: '*', - }); - - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-kms/test/via-service-principal.test.ts b/packages/@aws-cdk/aws-kms/test/via-service-principal.test.ts new file mode 100644 index 0000000000000..53d0d33d02933 --- /dev/null +++ b/packages/@aws-cdk/aws-kms/test/via-service-principal.test.ts @@ -0,0 +1,44 @@ +import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '../lib'; + +test('Via service, any principal', () => { + // WHEN + const statement = new iam.PolicyStatement({ + actions: ['abc:call'], + principals: [new kms.ViaServicePrincipal('bla.amazonaws.com')], + resources: ['*'], + }); + + // THEN + expect(statement.toStatementJson()).toEqual({ + Action: 'abc:call', + Condition: { StringEquals: { 'kms:ViaService': 'bla.amazonaws.com' } }, + Effect: 'Allow', + Principal: '*', + Resource: '*', + }); +}); + +test('Via service, principal with conditions', () => { + // WHEN + const statement = new iam.PolicyStatement({ + actions: ['abc:call'], + principals: [new kms.ViaServicePrincipal('bla.amazonaws.com', new iam.OrganizationPrincipal('o-1234'))], + resources: ['*'], + }); + + // THEN + expect(statement.toStatementJson()).toEqual({ + Action: 'abc:call', + Condition: { + StringEquals: { + 'kms:ViaService': 'bla.amazonaws.com', + 'aws:PrincipalOrgID': 'o-1234', + }, + }, + Effect: 'Allow', + Principal: '*', + Resource: '*', + }); +});