diff --git a/packages/@aws-cdk/aws-scheduler-alpha/README.md b/packages/@aws-cdk/aws-scheduler-alpha/README.md index af835a8ea02d7..3dd6f1cbc6d2f 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/README.md +++ b/packages/@aws-cdk/aws-scheduler-alpha/README.md @@ -201,7 +201,31 @@ Executing cross-account and cross-region targets are not supported yet. ### Specifying Encryption key -TODO: Not yet implemented. See section in [L2 Event Bridge Scheduler RFC](https://github.com/aws/aws-cdk-rfcs/blob/master/text/0474-event-bridge-scheduler-l2.md) +EventBridge Scheduler integrates with AWS Key Management Service (AWS KMS) to encrypt and decrypt your data using an AWS KMS key. +EventBridge Scheduler supports two types of KMS keys: AWS owned keys, and customer managed keys. + +By default, all events in Scheduler are encrypted with a key that AWS owns and manages. +If you wish you can also provide a customer managed key to encrypt and decrypt the payload that your schedule delivers to its target using the `key` property. +Target classes will automatically add AWS KMS Decrypt permission to your schedule's execution role permissions policy. + +```ts +declare const key: kms.Key; +declare const fn: lambda.Function; + +const target = new targets.LambdaInvoke(fn, { + input: ScheduleTargetInput.fromObject({ + payload: 'useful', + }), +}); + +const schedule = new Schedule(this, 'Schedule', { + schedule: ScheduleExpression.rate(Duration.minutes(10)), + target, + key, +}); +``` + +> Visit [Data protection in Amazon EventBridge Scheduler](https://docs.aws.amazon.com/scheduler/latest/UserGuide/data-protection.html) for more details. ## Error-handling diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts index 5842e7fbb6e90..036d95313eec9 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts @@ -1,5 +1,6 @@ import { IResource, Resource } from 'aws-cdk-lib'; import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; +import * as kms from 'aws-cdk-lib/aws-kms'; import { CfnSchedule } from 'aws-cdk-lib/aws-scheduler'; import { Construct } from 'constructs'; import { IGroup } from './group'; @@ -22,6 +23,11 @@ export interface ISchedule extends IResource { * The arn of the schedule. */ readonly scheduleArn: string; + + /** + * The customer managed KMS key that EventBridge Scheduler will use to encrypt and decrypt your data. + */ + readonly key?: kms.IKey; } /** @@ -67,6 +73,13 @@ export interface ScheduleProps { * @default true */ readonly enabled?: boolean; + + /** + * The customer managed KMS key that EventBridge Scheduler will use to encrypt and decrypt your data. + * + * @default - All events in Scheduler are encrypted with a key that AWS owns and manages. + */ + readonly key?: kms.IKey; } /** @@ -179,6 +192,11 @@ export class Schedule extends Resource implements ISchedule { */ public readonly scheduleName: string; + /** + * The customer managed KMS key that EventBridge Scheduler will use to encrypt and decrypt your data. + */ + readonly key?: kms.IKey; + constructor(scope: Construct, id: string, props: ScheduleProps) { super(scope, id, { physicalName: props.scheduleName, @@ -188,6 +206,11 @@ export class Schedule extends Resource implements ISchedule { const targetConfig = props.target.bind(this); + this.key = props.key; + if (this.key) { + this.key.grantEncryptDecrypt(targetConfig.role); + } + const resource = new CfnSchedule(this, 'Resource', { name: this.physicalName, flexibleTimeWindow: { mode: 'OFF' }, @@ -195,6 +218,7 @@ export class Schedule extends Resource implements ISchedule { scheduleExpressionTimezone: props.schedule.timeZone?.timezoneName, groupName: this.group?.groupName, state: (props.enabled ?? true) ? 'ENABLED' : 'DISABLED', + kmsKeyArn: this.key?.keyArn, target: { arn: targetConfig.arn, roleArn: targetConfig.role.roleArn, diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.assets.json b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.assets.json index e497b1a7d58e9..7d8aaf4c06d21 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.assets.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.assets.json @@ -1,7 +1,7 @@ { - "version": "34.0.0", + "version": "33.0.0", "files": { - "ea2b130e655c0c74a4099d37ab0b44701887641ac2253eec676e8048f199737e": { + "99b9aff7b7d42e6c47dd13d7034964f12b93cbe638fde0815e636313a0e7c9b0": { "source": { "path": "aws-cdk-scheduler-schedule.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "ea2b130e655c0c74a4099d37ab0b44701887641ac2253eec676e8048f199737e.json", + "objectKey": "99b9aff7b7d42e6c47dd13d7034964f12b93cbe638fde0815e636313a0e7c9b0.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.template.json b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.template.json index fd059201b67bf..69f6f034ea5c6 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.template.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.template.json @@ -67,6 +67,37 @@ } } }, + "RoleDefaultPolicy5FFB7DAB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:ReEncrypt*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ScheduleKey7E6B3A92", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RoleDefaultPolicy5FFB7DAB", + "Roles": [ + { + "Ref": "Role1ABCC5F0" + } + ] + } + }, "DefaultSchedule597B0B2C": { "Type": "AWS::Scheduler::Schedule", "Properties": { @@ -128,6 +159,72 @@ "Statistic": "Sum", "Threshold": 1 } + }, + "ScheduleKey7E6B3A92": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "CustomerKmsSchedule12B1FEFE": { + "Type": "AWS::Scheduler::Schedule", + "Properties": { + "FlexibleTimeWindow": { + "Mode": "OFF" + }, + "KmsKeyArn": { + "Fn::GetAtt": [ + "ScheduleKey7E6B3A92", + "Arn" + ] + }, + "ScheduleExpression": "rate(12 hours)", + "ScheduleExpressionTimezone": "Etc/UTC", + "State": "ENABLED", + "Target": { + "Arn": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } + } + } } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/cdk.out b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/cdk.out index 2313ab5436501..560dae10d018f 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"34.0.0"} \ No newline at end of file +{"version":"33.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integ.json b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integ.json index c1aec1a40f53f..8bd3af4b50e53 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "34.0.0", + "version": "33.0.0", "testCases": { "integtest-schedule/DefaultTest": { "stacks": [ diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integtestscheduleDefaultTestDeployAssert24CB3896.assets.json b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integtestscheduleDefaultTestDeployAssert24CB3896.assets.json index 8f8a003c1b5ba..0ec5b6018b44a 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integtestscheduleDefaultTestDeployAssert24CB3896.assets.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/integtestscheduleDefaultTestDeployAssert24CB3896.assets.json @@ -1,5 +1,5 @@ { - "version": "34.0.0", + "version": "33.0.0", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/manifest.json b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/manifest.json index cfd3e45618128..89f544c6f0432 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "34.0.0", + "version": "33.0.0", "artifacts": { "aws-cdk-scheduler-schedule.assets": { "type": "cdk:asset-manifest", @@ -18,7 +18,7 @@ "validateOnSynth": false, "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}/ea2b130e655c0c74a4099d37ab0b44701887641ac2253eec676e8048f199737e.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/99b9aff7b7d42e6c47dd13d7034964f12b93cbe638fde0815e636313a0e7c9b0.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -52,6 +52,12 @@ "data": "Role1ABCC5F0" } ], + "/aws-cdk-scheduler-schedule/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "RoleDefaultPolicy5FFB7DAB" + } + ], "/aws-cdk-scheduler-schedule/DefaultSchedule/Resource": [ { "type": "aws:cdk:logicalId", @@ -70,6 +76,18 @@ "data": "AllSchedulerErrorsAlarmA3246F8C" } ], + "/aws-cdk-scheduler-schedule/ScheduleKey/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ScheduleKey7E6B3A92" + } + ], + "/aws-cdk-scheduler-schedule/CustomerKmsSchedule/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomerKmsSchedule12B1FEFE" + } + ], "/aws-cdk-scheduler-schedule/BootstrapVersion": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/tree.json b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/tree.json index 816d0ed175086..a16dc5b4a296f 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/tree.json @@ -135,6 +135,55 @@ "fqn": "aws-cdk-lib.aws_iam.CfnRole", "version": "0.0.0" } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-scheduler-schedule/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-scheduler-schedule/Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:ReEncrypt*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ScheduleKey7E6B3A92", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "RoleDefaultPolicy5FFB7DAB", + "roles": [ + { + "Ref": "Role1ABCC5F0" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Policy", + "version": "0.0.0" + } } }, "constructInfo": { @@ -181,7 +230,7 @@ } }, "constructInfo": { - "fqn": "aws-cdk-lib.Resource", + "fqn": "@aws-cdk/aws-scheduler-alpha.Schedule", "version": "0.0.0" } }, @@ -224,7 +273,7 @@ } }, "constructInfo": { - "fqn": "aws-cdk-lib.Resource", + "fqn": "@aws-cdk/aws-scheduler-alpha.Schedule", "version": "0.0.0" } }, @@ -258,6 +307,106 @@ "version": "0.0.0" } }, + "ScheduleKey": { + "id": "ScheduleKey", + "path": "aws-cdk-scheduler-schedule/ScheduleKey", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-scheduler-schedule/ScheduleKey/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::KMS::Key", + "aws:cdk:cloudformation:props": { + "keyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kms.CfnKey", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kms.Key", + "version": "0.0.0" + } + }, + "CustomerKmsSchedule": { + "id": "CustomerKmsSchedule", + "path": "aws-cdk-scheduler-schedule/CustomerKmsSchedule", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-scheduler-schedule/CustomerKmsSchedule/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Scheduler::Schedule", + "aws:cdk:cloudformation:props": { + "flexibleTimeWindow": { + "mode": "OFF" + }, + "kmsKeyArn": { + "Fn::GetAtt": [ + "ScheduleKey7E6B3A92", + "Arn" + ] + }, + "scheduleExpression": "rate(12 hours)", + "scheduleExpressionTimezone": "Etc/UTC", + "state": "ENABLED", + "target": { + "arn": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + }, + "roleArn": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_scheduler.CfnSchedule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-scheduler-alpha.Schedule", + "version": "0.0.0" + } + }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "aws-cdk-scheduler-schedule/BootstrapVersion", diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.ts b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.ts index 11563b6beddae..23ae4d845bbc5 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.ts @@ -1,7 +1,9 @@ +/// !cdk-integ aws-cdk-scheduler-schedule import { IntegTest } from '@aws-cdk/integ-tests-alpha'; import * as cdk from 'aws-cdk-lib'; import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; import * as iam from 'aws-cdk-lib/aws-iam'; +import * as kms from 'aws-cdk-lib/aws-kms'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as scheduler from '../lib'; @@ -49,6 +51,13 @@ new cloudwatch.Alarm(stack, 'AllSchedulerErrorsAlarm', { evaluationPeriods: 1, }); +const key = new kms.Key(stack, 'ScheduleKey'); +new scheduler.Schedule(stack, 'CustomerKmsSchedule', { + schedule: expression, + target: target, + key, +}); + new IntegTest(app, 'integtest-schedule', { testCases: [stack], }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/schedule.test.ts b/packages/@aws-cdk/aws-scheduler-alpha/test/schedule.test.ts index b03726114302f..004e9934e281e 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/schedule.test.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/schedule.test.ts @@ -2,6 +2,7 @@ import { App, Stack, Duration } from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; import * as iam from 'aws-cdk-lib/aws-iam'; +import * as kms from 'aws-cdk-lib/aws-kms'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import { IScheduleTarget, Schedule, ScheduleTargetConfig } from '../lib'; import { ScheduleExpression } from '../lib/schedule-expression'; @@ -101,4 +102,30 @@ describe('Schedule', () => { expect(metric.statistic).toEqual('Maximum'); expect(metric.period).toEqual(Duration.minutes(1)); }); + + test('schedule can use customer managed KMS key', () => { + // GIVEN + const key = new kms.Key(stack, 'Key'); + + // WHEN + new Schedule(stack, 'TestSchedule', { + schedule: expr, + target: new SomeLambdaTarget(func, role), + key, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: ['kms:Decrypt', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, + }, + ], + Version: '2012-10-17', + }, + }); + }); }); \ No newline at end of file