diff --git a/packages/@aws-cdk/aws-glue/README.md b/packages/@aws-cdk/aws-glue/README.md index 77ee00834ddd7..5f841bda367ce 100644 --- a/packages/@aws-cdk/aws-glue/README.md +++ b/packages/@aws-cdk/aws-glue/README.md @@ -33,6 +33,40 @@ new glue.Database(stack, 'MyDatabase', { }); ``` +## SecurityConfiguration + +A `SecurityConfiguration` is a set of security properties that can be used by AWS Glue to encrypt data at rest. + +```ts +new glue.SecurityConfiguration(stack, 'MySecurityConfiguration', { + securityConfigurationName: 'name', + cloudWatchEncryption: { + mode: glue.CloudWatchEncryptionMode.KMS, + }, + jobBookmarksEncryption: { + mode: glue.JobBookmarksEncryptionMode.CLIENT_SIDE_KMS, + }, + s3Encryption: { + mode: glue.S3EncryptionMode.KMS, + }, +}); +``` + +By default, a shared KMS key is created for use with the encryption configurations that require one. You can also supply your own key for each encryption config, for example, for CloudWatch encryption: + +```ts +new glue.SecurityConfiguration(stack, 'MySecurityConfiguration', { + securityConfigurationName: 'name', + cloudWatchEncryption: { + mode: glue.CloudWatchEncryptionMode.KMS, + kmsKey: key, + }, +}); +``` + +See [documentation](https://docs.aws.amazon.com/glue/latest/dg/encryption-security-configuration.html) for more info for Glue encrypting data written by Crawlers, Jobs, and Development Endpoints. + + ## Table A Glue table describes a table of data in S3: its structure (column names and types), location of data (S3 objects with a common prefix in a S3 bucket), and format for the files (Json, Avro, Parquet, etc.): diff --git a/packages/@aws-cdk/aws-glue/lib/index.ts b/packages/@aws-cdk/aws-glue/lib/index.ts index f0370c21041b2..b219daf0996d2 100644 --- a/packages/@aws-cdk/aws-glue/lib/index.ts +++ b/packages/@aws-cdk/aws-glue/lib/index.ts @@ -1,7 +1,8 @@ // AWS::Glue CloudFormation Resources: export * from './glue.generated'; +export * from './data-format'; export * from './database'; export * from './schema'; -export * from './data-format'; +export * from './security-configuration'; export * from './table'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-glue/lib/security-configuration.ts b/packages/@aws-cdk/aws-glue/lib/security-configuration.ts new file mode 100644 index 0000000000000..f1e7297147794 --- /dev/null +++ b/packages/@aws-cdk/aws-glue/lib/security-configuration.ts @@ -0,0 +1,243 @@ +import * as kms from '@aws-cdk/aws-kms'; +import * as cdk from '@aws-cdk/core'; +import * as constructs from 'constructs'; +import { CfnSecurityConfiguration } from './glue.generated'; + +/** + * Interface representing a created or an imported {@link SecurityConfiguration}. + */ +export interface ISecurityConfiguration extends cdk.IResource { + /** + * The name of the security configuration. + * @attribute + */ + readonly securityConfigurationName: string; +} + +/** + * Encryption mode for S3. + * @see https://docs.aws.amazon.com/glue/latest/webapi/API_S3Encryption.html#Glue-Type-S3Encryption-S3EncryptionMode + */ +export enum S3EncryptionMode { + /** + * Server side encryption (SSE) with an Amazon S3-managed key. + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html + */ + S3_MANAGED = 'SSE-S3', + + /** + * Server-side encryption (SSE) with an AWS KMS key managed by the account owner. + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html + */ + KMS = 'SSE-KMS', +} + +/** + * Encryption mode for CloudWatch Logs. + * @see https://docs.aws.amazon.com/glue/latest/webapi/API_CloudWatchEncryption.html#Glue-Type-CloudWatchEncryption-CloudWatchEncryptionMode + */ +export enum CloudWatchEncryptionMode { + /** + * Server-side encryption (SSE) with an AWS KMS key managed by the account owner. + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html + */ + KMS = 'SSE-KMS', +} + +/** + * Encryption mode for Job Bookmarks. + * @see https://docs.aws.amazon.com/glue/latest/webapi/API_JobBookmarksEncryption.html#Glue-Type-JobBookmarksEncryption-JobBookmarksEncryptionMode + */ +export enum JobBookmarksEncryptionMode { + /** + * Client-side encryption (CSE) with an AWS KMS key managed by the account owner. + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingClientSideEncryption.html + */ + CLIENT_SIDE_KMS = 'CSE-KMS', +} + +/** + * S3 encryption configuration. + */ +export interface S3Encryption { + /** + * Encryption mode. + */ + readonly mode: S3EncryptionMode, + + /** + * The KMS key to be used to encrypt the data. + * @default no kms key if mode = S3_MANAGED. A key will be created if one is not provided and mode = KMS. + */ + readonly kmsKey?: kms.IKey, +} + +/** + * CloudWatch Logs encryption configuration. + */ +export interface CloudWatchEncryption { + /** + * Encryption mode + */ + readonly mode: CloudWatchEncryptionMode; + + /** + * The KMS key to be used to encrypt the data. + * @default A key will be created if one is not provided. + */ + readonly kmsKey?: kms.IKey, +} + +/** + * Job bookmarks encryption configuration. + */ +export interface JobBookmarksEncryption { + /** + * Encryption mode. + */ + readonly mode: JobBookmarksEncryptionMode; + + /** + * The KMS key to be used to encrypt the data. + * @default A key will be created if one is not provided. + */ + readonly kmsKey?: kms.IKey, +} + +/** + * Constructions properties of {@link SecurityConfiguration}. + */ +export interface SecurityConfigurationProps { + /** + * The name of the security configuration. + */ + readonly securityConfigurationName: string; + + /** + * The encryption configuration for Amazon CloudWatch Logs. + * @default no cloudwatch logs encryption. + */ + readonly cloudWatchEncryption?: CloudWatchEncryption, + + /** + * The encryption configuration for Glue Job Bookmarks. + * @default no job bookmarks encryption. + */ + readonly jobBookmarksEncryption?: JobBookmarksEncryption, + + /** + * The encryption configuration for Amazon Simple Storage Service (Amazon S3) data. + * @default no s3 encryption. + */ + readonly s3Encryption?: S3Encryption, +} + +/** + * A security configuration is a set of security properties that can be used by AWS Glue to encrypt data at rest. + * + * The following scenarios show some of the ways that you can use a security configuration. + * - Attach a security configuration to an AWS Glue crawler to write encrypted Amazon CloudWatch Logs. + * - Attach a security configuration to an extract, transform, and load (ETL) job to write encrypted Amazon Simple Storage Service (Amazon S3) targets and encrypted CloudWatch Logs. + * - Attach a security configuration to an ETL job to write its jobs bookmarks as encrypted Amazon S3 data. + * - Attach a security configuration to a development endpoint to write encrypted Amazon S3 targets. + */ +export class SecurityConfiguration extends cdk.Resource implements ISecurityConfiguration { + + /** + * Creates a Connection construct that represents an external security configuration. + * + * @param scope The scope creating construct (usually `this`). + * @param id The construct's id. + * @param securityConfigurationName name of external security configuration. + */ + public static fromSecurityConfigurationName(scope: constructs.Construct, id: string, + securityConfigurationName: string): ISecurityConfiguration { + + class Import extends cdk.Resource implements ISecurityConfiguration { + public readonly securityConfigurationName = securityConfigurationName; + } + return new Import(scope, id); + } + + /** + * The name of the security configuration. + * @attribute + */ + public readonly securityConfigurationName: string; + + /** + * The KMS key used in CloudWatch encryption if it requires a kms key. + */ + public readonly cloudWatchEncryptionKey?: kms.IKey; + + /** + * The KMS key used in job bookmarks encryption if it requires a kms key. + */ + public readonly jobBookmarksEncryptionKey?: kms.IKey; + + /** + * The KMS key used in S3 encryption if it requires a kms key. + */ + public readonly s3EncryptionKey?: kms.IKey; + + constructor(scope: constructs.Construct, id: string, props: SecurityConfigurationProps) { + super(scope, id, { + physicalName: props.securityConfigurationName, + }); + + if (!props.s3Encryption && !props.cloudWatchEncryption && !props.jobBookmarksEncryption) { + throw new Error('One of cloudWatchEncryption, jobBookmarksEncryption or s3Encryption must be defined'); + } + + const kmsKeyCreationRequired = + (props.s3Encryption && props.s3Encryption.mode === S3EncryptionMode.KMS && !props.s3Encryption.kmsKey) || + (props.cloudWatchEncryption && !props.cloudWatchEncryption.kmsKey) || + (props.jobBookmarksEncryption && !props.jobBookmarksEncryption.kmsKey); + const autoCreatedKmsKey = kmsKeyCreationRequired ? new kms.Key(this, 'Key') : undefined; + + let cloudWatchEncryption; + if (props.cloudWatchEncryption) { + this.cloudWatchEncryptionKey = props.cloudWatchEncryption.kmsKey || autoCreatedKmsKey; + cloudWatchEncryption = { + cloudWatchEncryptionMode: props.cloudWatchEncryption.mode, + kmsKeyArn: this.cloudWatchEncryptionKey?.keyArn, + }; + } + + let jobBookmarksEncryption; + if (props.jobBookmarksEncryption) { + this.jobBookmarksEncryptionKey = props.jobBookmarksEncryption.kmsKey || autoCreatedKmsKey; + jobBookmarksEncryption = { + jobBookmarksEncryptionMode: props.jobBookmarksEncryption.mode, + kmsKeyArn: this.jobBookmarksEncryptionKey?.keyArn, + }; + } + + let s3Encryptions; + if (props.s3Encryption) { + if (props.s3Encryption.mode === S3EncryptionMode.KMS) { + this.s3EncryptionKey = props.s3Encryption.kmsKey || autoCreatedKmsKey; + } + // NOTE: CloudFormations errors out if array is of length > 1. That's why the props don't expose an array + s3Encryptions = [{ + s3EncryptionMode: props.s3Encryption.mode, + kmsKeyArn: this.s3EncryptionKey?.keyArn, + }]; + } + + const resource = new CfnSecurityConfiguration(this, 'Resource', { + name: props.securityConfigurationName, + encryptionConfiguration: { + cloudWatchEncryption, + jobBookmarksEncryption, + s3Encryptions, + }, + }); + + this.securityConfigurationName = this.getResourceNameAttribute(resource.ref); + } +} diff --git a/packages/@aws-cdk/aws-glue/test/integ.security-configuration.expected.json b/packages/@aws-cdk/aws-glue/test/integ.security-configuration.expected.json new file mode 100644 index 0000000000000..00323985c78dc --- /dev/null +++ b/packages/@aws-cdk/aws-glue/test/integ.security-configuration.expected.json @@ -0,0 +1,193 @@ +{ + "Resources": { + "Key961B73FD": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "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" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "KeyedSC862A23F3": { + "Type": "AWS::Glue::SecurityConfiguration", + "Properties": { + "EncryptionConfiguration": { + "CloudWatchEncryption": { + "CloudWatchEncryptionMode": "SSE-KMS", + "KmsKeyArn": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + } + }, + "JobBookmarksEncryption": { + "JobBookmarksEncryptionMode": "CSE-KMS", + "KmsKeyArn": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + } + }, + "S3Encryptions": [ + { + "KmsKeyArn": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + }, + "S3EncryptionMode": "SSE-KMS" + } + ] + }, + "Name": "KeyedSC" + } + }, + "KeylessSCKey4D3DE803": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "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" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "KeylessSC42E312EC": { + "Type": "AWS::Glue::SecurityConfiguration", + "Properties": { + "EncryptionConfiguration": { + "CloudWatchEncryption": { + "CloudWatchEncryptionMode": "SSE-KMS", + "KmsKeyArn": { + "Fn::GetAtt": [ + "KeylessSCKey4D3DE803", + "Arn" + ] + } + }, + "JobBookmarksEncryption": { + "JobBookmarksEncryptionMode": "CSE-KMS", + "KmsKeyArn": { + "Fn::GetAtt": [ + "KeylessSCKey4D3DE803", + "Arn" + ] + } + }, + "S3Encryptions": [ + { + "KmsKeyArn": { + "Fn::GetAtt": [ + "KeylessSCKey4D3DE803", + "Arn" + ] + }, + "S3EncryptionMode": "SSE-KMS" + } + ] + }, + "Name": "KeylessSC" + } + }, + "S3SCE31C83BE": { + "Type": "AWS::Glue::SecurityConfiguration", + "Properties": { + "EncryptionConfiguration": { + "S3Encryptions": [ + { + "S3EncryptionMode": "SSE-S3" + } + ] + }, + "Name": "S3SC" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-glue/test/integ.security-configuration.ts b/packages/@aws-cdk/aws-glue/test/integ.security-configuration.ts new file mode 100644 index 0000000000000..e10ef4d140adc --- /dev/null +++ b/packages/@aws-cdk/aws-glue/test/integ.security-configuration.ts @@ -0,0 +1,50 @@ +import * as kms from '@aws-cdk/aws-kms'; +import * as cdk from '@aws-cdk/core'; +import * as glue from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-glue-security-configuration'); + +const key = new kms.Key(stack, 'Key'); + +// SecurityConfiguration for all 3 (s3, cloudwatch and job bookmarks) in modes requiring kms keys +new glue.SecurityConfiguration(stack, 'KeyedSC', { + securityConfigurationName: 'KeyedSC', + jobBookmarksEncryption: { + mode: glue.JobBookmarksEncryptionMode.CLIENT_SIDE_KMS, + kmsKey: key, + }, + cloudWatchEncryption: { + mode: glue.CloudWatchEncryptionMode.KMS, + kmsKey: key, + }, + s3Encryption: { + mode: glue.S3EncryptionMode.KMS, + kmsKey: key, + }, +}); + +// SecurityConfiguration for all 3 (s3, cloudwatch and job bookmarks) in modes requiring kms keys without one provided +new glue.SecurityConfiguration(stack, 'KeylessSC', { + securityConfigurationName: 'KeylessSC', + jobBookmarksEncryption: { + mode: glue.JobBookmarksEncryptionMode.CLIENT_SIDE_KMS, + }, + cloudWatchEncryption: { + mode: glue.CloudWatchEncryptionMode.KMS, + }, + s3Encryption: { + mode: glue.S3EncryptionMode.KMS, + }, +}); + +// SecurityConfiguration for s3 not requiring kms key +new glue.SecurityConfiguration(stack, 'S3SC', { + securityConfigurationName: 'S3SC', + s3Encryption: { + mode: glue.S3EncryptionMode.S3_MANAGED, + }, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-glue/test/security-configuration.test.ts b/packages/@aws-cdk/aws-glue/test/security-configuration.test.ts new file mode 100644 index 0000000000000..cd94a660dfcb3 --- /dev/null +++ b/packages/@aws-cdk/aws-glue/test/security-configuration.test.ts @@ -0,0 +1,122 @@ +import * as cdkassert from '@aws-cdk/assert'; +import * as kms from '@aws-cdk/aws-kms'; +import * as cdk from '@aws-cdk/core'; +import '@aws-cdk/assert/jest'; +import * as glue from '../lib'; + +test('throws when a security configuration has no encryption config', () => { + const stack = new cdk.Stack(); + + expect(() => new glue.SecurityConfiguration(stack, 'SecurityConfiguration', { + securityConfigurationName: 'name', + })).toThrowError(/One of cloudWatchEncryption, jobBookmarksEncryption or s3Encryption must be defined/); +}); + +test('a security configuration with encryption configuration requiring kms key and providing an explicit one', () => { + const stack = new cdk.Stack(); + const keyArn = 'arn:aws:kms:us-west-2:111122223333:key/test-key'; + const key = kms.Key.fromKeyArn(stack, 'ImportedKey', keyArn); + + const securityConfiguration = new glue.SecurityConfiguration(stack, 'SecurityConfiguration', { + securityConfigurationName: 'name', + cloudWatchEncryption: { + mode: glue.CloudWatchEncryptionMode.KMS, + kmsKey: key, + }, + }); + + expect(securityConfiguration.cloudWatchEncryptionKey?.keyArn).toEqual(keyArn); + expect(securityConfiguration.jobBookmarksEncryptionKey).toBeUndefined(); + expect(securityConfiguration.s3EncryptionKey).toBeUndefined(); + + cdkassert.expect(stack).to(cdkassert.haveResource('AWS::Glue::SecurityConfiguration', { + Name: 'name', + EncryptionConfiguration: { + CloudWatchEncryption: { + CloudWatchEncryptionMode: 'SSE-KMS', + KmsKeyArn: keyArn, + }, + }, + })); +}); + +test('a security configuration with an encryption configuration requiring kms key but not providing an explicit one', () => { + const stack = new cdk.Stack(); + + const securityConfiguration = new glue.SecurityConfiguration(stack, 'SecurityConfiguration', { + securityConfigurationName: 'name', + cloudWatchEncryption: { + mode: glue.CloudWatchEncryptionMode.KMS, + }, + }); + + expect(securityConfiguration.cloudWatchEncryptionKey).toBeDefined(); + expect(securityConfiguration.jobBookmarksEncryptionKey).toBeUndefined(); + expect(securityConfiguration.s3EncryptionKey).toBeUndefined(); + + cdkassert.expect(stack).to(cdkassert.haveResource('AWS::KMS::Key')); + + cdkassert.expect(stack).to(cdkassert.haveResource('AWS::Glue::SecurityConfiguration', { + Name: 'name', + EncryptionConfiguration: { + CloudWatchEncryption: { + CloudWatchEncryptionMode: 'SSE-KMS', + KmsKeyArn: stack.resolve(securityConfiguration.cloudWatchEncryptionKey?.keyArn), + }, + }, + })); +}); + +test('a security configuration with all encryption configs and mixed kms key inputs', () => { + const stack = new cdk.Stack(); + const keyArn = 'arn:aws:kms:us-west-2:111122223333:key/test-key'; + const key = kms.Key.fromKeyArn(stack, 'ImportedKey', keyArn); + + const securityConfiguration = new glue.SecurityConfiguration(stack, 'SecurityConfiguration', { + securityConfigurationName: 'name', + cloudWatchEncryption: { + mode: glue.CloudWatchEncryptionMode.KMS, + }, + jobBookmarksEncryption: { + mode: glue.JobBookmarksEncryptionMode.CLIENT_SIDE_KMS, + kmsKey: key, + }, + s3Encryption: { + mode: glue.S3EncryptionMode.S3_MANAGED, + }, + }); + + expect(securityConfiguration.cloudWatchEncryptionKey).toBeDefined(); + expect(securityConfiguration.jobBookmarksEncryptionKey?.keyArn).toEqual(keyArn); + expect(securityConfiguration.s3EncryptionKey).toBeUndefined(); + + cdkassert.expect(stack).to(cdkassert.haveResource('AWS::KMS::Key')); + + cdkassert.expect(stack).to(cdkassert.haveResource('AWS::Glue::SecurityConfiguration', { + Name: 'name', + EncryptionConfiguration: { + CloudWatchEncryption: { + CloudWatchEncryptionMode: 'SSE-KMS', + // auto-created kms key + KmsKeyArn: stack.resolve(securityConfiguration.cloudWatchEncryptionKey?.keyArn), + }, + JobBookmarksEncryption: { + JobBookmarksEncryptionMode: 'CSE-KMS', + // explicitly provided kms key + KmsKeyArn: keyArn, + }, + S3Encryptions: [{ + S3EncryptionMode: 'SSE-S3', + }], + }, + })); +}); + +test('fromSecurityConfigurationName', () => { + const stack = new cdk.Stack(); + const name = 'name'; + + const securityConfiguration = glue.SecurityConfiguration.fromSecurityConfigurationName(stack, 'ImportedSecurityConfiguration', name); + + expect(securityConfiguration.securityConfigurationName).toEqual(name); +});