diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/cdk.out new file mode 100644 index 0000000000000..c6e612584e352 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"38.0.1"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/integ.json new file mode 100644 index 0000000000000..b3f8f40c776a5 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/integ.json @@ -0,0 +1,13 @@ +{ + "version": "38.0.1", + "testCases": { + "integ-kinesis-resource-policy/DefaultTest": { + "stacks": [ + "kinesis-resource-policy" + ], + "stackUpdateWorkflow": false, + "assertionStack": "integ-kinesis-resource-policy/DefaultTest/DeployAssert", + "assertionStackName": "integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C.assets.json new file mode 100644 index 0000000000000..10ed04c6cb3b2 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C.assets.json @@ -0,0 +1,19 @@ +{ + "version": "38.0.1", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/kinesis-resource-policy.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/kinesis-resource-policy.assets.json new file mode 100644 index 0000000000000..41aa262dfadaa --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/kinesis-resource-policy.assets.json @@ -0,0 +1,19 @@ +{ + "version": "38.0.1", + "files": { + "25f5843484c10a3b762cdda9cddcdbaf948c1d795dd2294a83ba77c6c1b732ef": { + "source": { + "path": "kinesis-resource-policy.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "25f5843484c10a3b762cdda9cddcdbaf948c1d795dd2294a83ba77c6c1b732ef.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/kinesis-resource-policy.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/kinesis-resource-policy.template.json new file mode 100644 index 0000000000000..7431a787d0228 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/kinesis-resource-policy.template.json @@ -0,0 +1,128 @@ +{ + "Resources": { + "MyStream5C050E93": { + "Type": "AWS::Kinesis::Stream", + "Properties": { + "RetentionPeriodHours": 24, + "ShardCount": 1, + "StreamEncryption": { + "Fn::If": [ + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions", + { + "Ref": "AWS::NoValue" + }, + { + "EncryptionType": "KMS", + "KeyId": "alias/aws/kinesis" + } + ] + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "MyStreamPolicyC34ACF94": { + "Type": "AWS::Kinesis::ResourcePolicy", + "Properties": { + "ResourceArn": { + "Fn::GetAtt": [ + "MyStream5C050E93", + "Arn" + ] + }, + "ResourcePolicy": { + "Statement": [ + { + "Action": [ + "kinesis:DescribeStreamSummary", + "kinesis:GetRecords" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": { + "Fn::GetAtt": [ + "MyStream5C050E93", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + } + } + } + }, + "Conditions": { + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions": { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-northwest-1" + ] + } + ] + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/manifest.json new file mode 100644 index 0000000000000..5b7e1577fd04f --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/manifest.json @@ -0,0 +1,127 @@ +{ + "version": "38.0.1", + "artifacts": { + "kinesis-resource-policy.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "kinesis-resource-policy.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "kinesis-resource-policy": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "kinesis-resource-policy.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "notificationArns": [], + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/25f5843484c10a3b762cdda9cddcdbaf948c1d795dd2294a83ba77c6c1b732ef.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "kinesis-resource-policy.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "kinesis-resource-policy.assets" + ], + "metadata": { + "/kinesis-resource-policy/MyStream/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyStream5C050E93" + } + ], + "/kinesis-resource-policy/MyStream/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyStreamPolicyC34ACF94" + } + ], + "/kinesis-resource-policy/AwsCdkKinesisEncryptedStreamsUnsupportedRegions": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsCdkKinesisEncryptedStreamsUnsupportedRegions" + } + ], + "/kinesis-resource-policy/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/kinesis-resource-policy/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "kinesis-resource-policy" + }, + "integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "notificationArns": [], + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "integkinesisresourcepolicyDefaultTestDeployAssert52C5D16C.assets" + ], + "metadata": { + "/integ-kinesis-resource-policy/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-kinesis-resource-policy/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-kinesis-resource-policy/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/tree.json new file mode 100644 index 0000000000000..1cb4e4dd9901b --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.js.snapshot/tree.json @@ -0,0 +1,211 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "kinesis-resource-policy": { + "id": "kinesis-resource-policy", + "path": "kinesis-resource-policy", + "children": { + "MyStream": { + "id": "MyStream", + "path": "kinesis-resource-policy/MyStream", + "children": { + "Resource": { + "id": "Resource", + "path": "kinesis-resource-policy/MyStream/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Kinesis::Stream", + "aws:cdk:cloudformation:props": { + "retentionPeriodHours": 24, + "shardCount": 1, + "streamEncryption": { + "Fn::If": [ + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions", + { + "Ref": "AWS::NoValue" + }, + { + "EncryptionType": "KMS", + "KeyId": "alias/aws/kinesis" + } + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kinesis.CfnStream", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "kinesis-resource-policy/MyStream/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "kinesis-resource-policy/MyStream/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Kinesis::ResourcePolicy", + "aws:cdk:cloudformation:props": { + "resourceArn": { + "Fn::GetAtt": [ + "MyStream5C050E93", + "Arn" + ] + }, + "resourcePolicy": { + "Statement": [ + { + "Action": [ + "kinesis:DescribeStreamSummary", + "kinesis:GetRecords" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": { + "Fn::GetAtt": [ + "MyStream5C050E93", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kinesis.CfnResourcePolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kinesis.ResourcePolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kinesis.Stream", + "version": "0.0.0" + } + }, + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions": { + "id": "AwsCdkKinesisEncryptedStreamsUnsupportedRegions", + "path": "kinesis-resource-policy/AwsCdkKinesisEncryptedStreamsUnsupportedRegions", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnCondition", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "kinesis-resource-policy/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "kinesis-resource-policy/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "integ-kinesis-resource-policy": { + "id": "integ-kinesis-resource-policy", + "path": "integ-kinesis-resource-policy", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "integ-kinesis-resource-policy/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "integ-kinesis-resource-policy/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "integ-kinesis-resource-policy/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-kinesis-resource-policy/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-kinesis-resource-policy/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.ts new file mode 100644 index 0000000000000..bd9911520e5c6 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-kinesis/test/integ.resource-policy.ts @@ -0,0 +1,23 @@ +import { App, Stack } from 'aws-cdk-lib'; +import { Stream } from 'aws-cdk-lib/aws-kinesis'; +import { AccountPrincipal, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; + +const app = new App(); +const stack = new Stack(app, 'kinesis-resource-policy'); + +const stream = new Stream(stack, 'MyStream'); + +stream.addToResourcePolicy(new PolicyStatement({ + resources: [stream.streamArn], + actions: [ + 'kinesis:DescribeStreamSummary', + 'kinesis:GetRecords', + ], + principals: [new AccountPrincipal(stack.account)], +})); + +new IntegTest(app, 'integ-kinesis-resource-policy', { + testCases: [stack], + stackUpdateWorkflow: false, +}); diff --git a/packages/aws-cdk-lib/aws-kinesis/README.md b/packages/aws-cdk-lib/aws-kinesis/README.md index b8b22dffda657..27f89e65b7626 100644 --- a/packages/aws-cdk-lib/aws-kinesis/README.md +++ b/packages/aws-cdk-lib/aws-kinesis/README.md @@ -15,6 +15,8 @@ intake and aggregation. - [Write Permissions](#write-permissions) - [Custom Permissions](#custom-permissions) - [Metrics](#metrics) + - [Resource Policy](#resource-policy) + ## Streams @@ -186,3 +188,49 @@ stream.metricGetRecordsSuccess(); // using pre-defined and overriding the statistic stream.metricGetRecordsSuccess({ statistic: 'Maximum' }); ``` + +### Resource Policy + +You can create a resource policy for a data stream. +For more information, see [Controlling access to Amazon Kinesis Data Streams resources using IAM](https://docs.aws.amazon.com/streams/latest/dev/controlling-access.html). + +A resource policy is automatically created when `addToResourcePolicy` is called, if one doesn't already exist. + +Using `addToResourcePolicy` is the simplest way to add a resource policy: + +```ts +const stream = new kinesis.Stream(this, 'MyStream'); + +// create a resource policy via addToResourcePolicy method +stream.addToResourcePolicy(new iam.PolicyStatement({ + resources: [stream.streamArn], + actions: ['kinesis:GetRecords'], + principals: [new iam.AnyPrincipal()], +})); +``` + +You can create a resource manually by using `ResourcePolicy`. +Also, you can set a custom policy document to `ResourcePolicy`. +If not, a blank policy document will be set. + +```ts +const stream = new kinesis.Stream(this, 'MyStream'); + +// create a custom policy document +const policyDocument = new iam.PolicyDocument({ + assignSids: true, + statements: [ + new iam.PolicyStatement({ + actions: ['kinesis:GetRecords'], + resources: [stream.streamArn], + principals: [new iam.AnyPrincipal()], + }), + ], +}); + +// create a resource policy manually +new kinesis.ResourcePolicy(this, 'ResourcePolicy', { + stream, + policyDocument, +}); +``` \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-kinesis/lib/index.ts b/packages/aws-cdk-lib/aws-kinesis/lib/index.ts index bb8cedbc5558c..c239368eaa87c 100644 --- a/packages/aws-cdk-lib/aws-kinesis/lib/index.ts +++ b/packages/aws-cdk-lib/aws-kinesis/lib/index.ts @@ -1,4 +1,5 @@ export * from './stream'; +export * from './resource-policy'; // AWS::Kinesis CloudFormation Resources: export * from './kinesis.generated'; diff --git a/packages/aws-cdk-lib/aws-kinesis/lib/resource-policy.ts b/packages/aws-cdk-lib/aws-kinesis/lib/resource-policy.ts new file mode 100644 index 0000000000000..3987268f52bfe --- /dev/null +++ b/packages/aws-cdk-lib/aws-kinesis/lib/resource-policy.ts @@ -0,0 +1,54 @@ +import { Construct } from 'constructs'; +import { CfnResourcePolicy } from './kinesis.generated'; +import { IStream } from './stream'; +import { PolicyDocument } from '../../aws-iam'; +import { Resource } from '../../core'; + +/** + * Properties to associate a data stream with a policy + */ +export interface ResourcePolicyProps { + /** + * The stream this policy applies to. + */ + readonly stream: IStream; + + /** + * IAM policy document to apply to a data stream. + * + * @default - empty policy document + */ + readonly policyDocument?: PolicyDocument; +} + +/** + * The policy for a data stream or registered consumer. + * + * Policies define the operations that are allowed on this resource. + * + * You almost never need to define this construct directly. + * + * All AWS resources that support resource policies have a method called + * `addToResourcePolicy()`, which will automatically create a new resource + * policy if one doesn't exist yet, otherwise it will add to the existing + * policy. + * + * Prefer to use `addToResourcePolicy()` instead. + */ +export class ResourcePolicy extends Resource { + /** + * The IAM policy document for this policy. + */ + public readonly document = new PolicyDocument(); + + constructor(scope: Construct, id: string, props: ResourcePolicyProps) { + super(scope, id); + + this.document = props.policyDocument ?? this.document; + + new CfnResourcePolicy(this, 'Resource', { + resourcePolicy: this.document, + resourceArn: props.stream.streamArn, + }); + } +} diff --git a/packages/aws-cdk-lib/aws-kinesis/lib/stream.ts b/packages/aws-cdk-lib/aws-kinesis/lib/stream.ts index 59ee9d4ff5d87..0fb74df769f50 100644 --- a/packages/aws-cdk-lib/aws-kinesis/lib/stream.ts +++ b/packages/aws-cdk-lib/aws-kinesis/lib/stream.ts @@ -1,10 +1,11 @@ import { Construct } from 'constructs'; import { KinesisMetrics } from './kinesis-fixed-canned-metrics'; import { CfnStream } from './kinesis.generated'; +import { ResourcePolicy } from './resource-policy'; import * as cloudwatch from '../../aws-cloudwatch'; import * as iam from '../../aws-iam'; import * as kms from '../../aws-kms'; -import { ArnFormat, Aws, CfnCondition, Duration, Fn, IResolvable, IResource, RemovalPolicy, Resource, Stack, Token } from '../../core'; +import { ArnFormat, Aws, CfnCondition, Duration, Fn, IResolvable, IResource, RemovalPolicy, Resource, ResourceProps, Stack, Token } from '../../core'; const READ_OPERATIONS = [ 'kinesis:DescribeStreamSummary', @@ -46,6 +47,15 @@ export interface IStream extends IResource { */ readonly encryptionKey?: kms.IKey; + /** + * Adds a statement to the IAM resource policy associated with this stream. + * + * If this stream was created in this stack (`new Stream`), a resource policy + * will be automatically created upon the first call to `addToResourcePolicy`. If + * the stream is imported (`Stream.import`), then this is a no-op. + */ + addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult; + /** * Grant read permissions for this stream and its contents to an IAM * principal (Role/Group/User). @@ -328,6 +338,41 @@ abstract class StreamBase extends Resource implements IStream { */ public abstract readonly encryptionKey?: kms.IKey; + /** + * Indicates if a stream resource policy should automatically be created upon + * the first call to `addToResourcePolicy`. + * + * Set by subclasses. + */ + protected abstract readonly autoCreatePolicy: boolean; + + private resourcePolicy?: ResourcePolicy; + + constructor(scope: Construct, id: string, props: ResourceProps = {}) { + super(scope, id, props); + + this.node.addValidation({ validate: () => this.resourcePolicy?.document.validateForResourcePolicy() ?? [] }); + } + + /** + * Adds a statement to the IAM resource policy associated with this stream. + * + * If this stream was created in this stack (`new Strem`), a resource policy + * will be automatically created upon the first call to `addToResourcePolicy`. If + * the stream is imported (`Stream.import`), then this is a no-op. + */ + public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { + if (!this.resourcePolicy && this.autoCreatePolicy) { + this.resourcePolicy = new ResourcePolicy(this, 'Policy', { stream: this }); + } + + if (this.resourcePolicy) { + this.resourcePolicy.document.addStatements(statement); + return { statementAdded: true, policyDependable: this.resourcePolicy }; + } + return { statementAdded: false }; + } + /** * Grant read permissions for this stream and its contents to an IAM * principal (Role/Group/User). @@ -747,6 +792,8 @@ export class Stream extends StreamBase { public readonly streamArn = attrs.streamArn; public readonly streamName = Stack.of(scope).splitArn(attrs.streamArn, ArnFormat.SLASH_RESOURCE_NAME).resourceName!; public readonly encryptionKey = attrs.encryptionKey; + + protected readonly autoCreatePolicy = false; } return new Import(scope, id, { @@ -760,6 +807,8 @@ export class Stream extends StreamBase { private readonly stream: CfnStream; + protected readonly autoCreatePolicy = true; + constructor(scope: Construct, id: string, props: StreamProps = {}) { super(scope, id, { physicalName: props.streamName, @@ -771,7 +820,7 @@ export class Stream extends StreamBase { if (streamMode === StreamMode.ON_DEMAND && shardCount !== undefined) { throw new Error(`streamMode must be set to ${StreamMode.PROVISIONED} (default) when specifying shardCount`); } - if ( (streamMode === StreamMode.PROVISIONED || streamMode === undefined) && shardCount === undefined) { + if ((streamMode === StreamMode.PROVISIONED || streamMode === undefined) && shardCount === undefined) { shardCount = 1; } @@ -849,7 +898,7 @@ export class Stream extends StreamBase { } if (encryptionType === StreamEncryption.UNENCRYPTED) { - return { }; + return {}; } if (encryptionType === StreamEncryption.MANAGED) { diff --git a/packages/aws-cdk-lib/aws-kinesis/test/resource-policy.test.ts b/packages/aws-cdk-lib/aws-kinesis/test/resource-policy.test.ts new file mode 100644 index 0000000000000..01cd727cf8aed --- /dev/null +++ b/packages/aws-cdk-lib/aws-kinesis/test/resource-policy.test.ts @@ -0,0 +1,45 @@ +import { Template } from '../../assertions'; +import * as iam from '../../aws-iam'; +import { Stack } from '../../core'; +import { ResourcePolicy, Stream } from '../lib'; + +describe('Kinesis resource policy', () => { + test('create resource policy', () => { + // GIVEN + const stack = new Stack(); + const stream = new Stream(stack, 'Stream', {}); + + // WHEN + const policyDocument = new iam.PolicyDocument({ + assignSids: true, + statements: [ + new iam.PolicyStatement({ + actions: ['kinesis:GetRecords'], + principals: [new iam.AnyPrincipal()], + resources: [stream.streamArn], + }), + ], + }); + + new ResourcePolicy(stack, 'ResourcePolicy', { + stream, + policyDocument, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Kinesis::ResourcePolicy', { + ResourcePolicy: { + Version: '2012-10-17', + Statement: [ + { + Sid: '0', + Action: 'kinesis:GetRecords', + Effect: 'Allow', + Principal: { AWS: '*' }, + Resource: stack.resolve(stream.streamArn), + }, + ], + }, + }); + }); +}); diff --git a/packages/aws-cdk-lib/aws-kinesis/test/stream.test.ts b/packages/aws-cdk-lib/aws-kinesis/test/stream.test.ts index 841c95877ef53..c9e7569bdf1d8 100644 --- a/packages/aws-cdk-lib/aws-kinesis/test/stream.test.ts +++ b/packages/aws-cdk-lib/aws-kinesis/test/stream.test.ts @@ -1289,4 +1289,32 @@ describe('Kinesis data streams', () => { DeletionPolicy: CfnDeletionPolicy.DELETE, }); }); + + test('addToResourcePolicy will automatically create a policy for this stream', () => { + // GIVEN + const stack = new Stack(); + const stream = new Stream(stack, 'Stream', {}); + + // WHEN + stream.addToResourcePolicy(new iam.PolicyStatement({ + actions: ['kinesis:GetRecords'], + principals: [new iam.AnyPrincipal()], + resources: [stream.streamArn], + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Kinesis::ResourcePolicy', { + ResourcePolicy: { + Version: '2012-10-17', + Statement: [ + { + Action: 'kinesis:GetRecords', + Effect: 'Allow', + Principal: { AWS: '*' }, + Resource: stack.resolve(stream.streamArn), + }, + ], + }, + }); + }); }); diff --git a/packages/aws-cdk-lib/awslint.json b/packages/aws-cdk-lib/awslint.json index d85aaef4834d9..987770519230f 100644 --- a/packages/aws-cdk-lib/awslint.json +++ b/packages/aws-cdk-lib/awslint.json @@ -124,6 +124,7 @@ "props-physical-name:aws-cdk-lib.aws_iam.OpenIdConnectProviderProps", "props-physical-name:aws-cdk-lib.aws_iam.SamlProviderProps", "props-physical-name:aws-cdk-lib.aws_kms.KeyProps", + "props-physical-name:aws-cdk-lib.aws_kinesis.ResourcePolicyProps", "props-physical-name:aws-cdk-lib.aws_lambda.CodeSigningConfigProps", "props-physical-name:aws-cdk-lib.aws_lambda.EventInvokeConfigProps", "props-physical-name:aws-cdk-lib.aws_lambda.EventSourceMappingProps",