diff --git a/CHANGELOG.md b/CHANGELOG.md index 0386d2dbfb9a6..d060fb6f61a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,50 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.42.0](https://github.com/aws/aws-cdk/compare/v1.41.0...v1.42.0) (2020-05-27) + + +### ⚠ BREAKING CHANGES + +* **cloudtrail:** API signatures of `addS3EventSelectors` and +`addLambdaEventSelectors` have changed. Their parameters are now +strongly typed to accept `IBucket` and `IFunction` respectively. +* **cloudtrail:** `addS3EventSelectors` and `addLambdaEventSelectors` +can no longer be used to configure all S3 data events or all Lambda data +events. Two new APIs `logAllS3DataEvents()` and +`logAllLambdaDataEvents()` have been introduced to achieve this. +* **cloudtrail:** The property `snsTopic` is now of the type `ITopic`. + +### Features + +* **cfnspec:** cloudformation spec v14.4.0 ([#8195](https://github.com/aws/aws-cdk/issues/8195)) ([99e7330](https://github.com/aws/aws-cdk/commit/99e7330fc5fc140964c47d8c6dbaee2b46b382e1)) +* **cloudtrail:** create cloudwatch event without needing to create a Trail ([#8076](https://github.com/aws/aws-cdk/issues/8076)) ([0567a23](https://github.com/aws/aws-cdk/commit/0567a2360ac713e3171c9a82767611174dadb6c6)), closes [#6716](https://github.com/aws/aws-cdk/issues/6716) +* **cloudtrail:** user specified log group ([#8079](https://github.com/aws/aws-cdk/issues/8079)) ([0a3785b](https://github.com/aws/aws-cdk/commit/0a3785b7626633fcbdf26ab793c70f2bc017314b)), closes [#6162](https://github.com/aws/aws-cdk/issues/6162) +* **codeguruprofiler:** ProfilingGroup ([#7895](https://github.com/aws/aws-cdk/issues/7895)) ([995088a](https://github.com/aws/aws-cdk/commit/995088abb00d9c75adbb65845998a8328bb5ba14)) +* **codepipeline:** use a special bootstrapless synthesizer for cross-region support Stacks ([#8091](https://github.com/aws/aws-cdk/issues/8091)) ([575f1db](https://github.com/aws/aws-cdk/commit/575f1db0474327c61c4ac626608c9f443ce231d2)), closes [#8082](https://github.com/aws/aws-cdk/issues/8082) +* **cognito:** user pool - case sensitivity for sign in ([460394f](https://github.com/aws/aws-cdk/commit/460394f3dc4737cee80504d6c8ef106ecc3b67d5)), closes [#7988](https://github.com/aws/aws-cdk/issues/7988) [#7235](https://github.com/aws/aws-cdk/issues/7235) +* **core:** CfnJson enables intrinsics in hash keys ([#8099](https://github.com/aws/aws-cdk/issues/8099)) ([195cd40](https://github.com/aws/aws-cdk/commit/195cd405d9f0869875de2ec78661aee3af2c7c7d)), closes [#8084](https://github.com/aws/aws-cdk/issues/8084) +* **eks:** improve security using IRSA conditions ([#8084](https://github.com/aws/aws-cdk/issues/8084)) ([35a01a0](https://github.com/aws/aws-cdk/commit/35a01a079af40da291007da08af6690c9a81c101)) +* **elbv2:** Supports new types of listener rule conditions ([#7848](https://github.com/aws/aws-cdk/issues/7848)) ([3d30ffa](https://github.com/aws/aws-cdk/commit/3d30ffa38c51ae26686287e993af445ea3067766)), closes [#3888](https://github.com/aws/aws-cdk/issues/3888) +* **secretsmanager:** adds grantWrite to Secret ([#7858](https://github.com/aws/aws-cdk/issues/7858)) ([3fed84b](https://github.com/aws/aws-cdk/commit/3fed84ba9eec3f53c662966e366aa629209b7bf5)) +* **sns:** add support for subscription DLQ in SNS ([383cdb8](https://github.com/aws/aws-cdk/commit/383cdb86effeafdf5d0767ed379b16b3d78a933b)) +* **stepfunctions:** new service integration classes for Lambda, SNS, and SQS ([#7946](https://github.com/aws/aws-cdk/issues/7946)) ([c038848](https://github.com/aws/aws-cdk/commit/c0388483524832ca7863de4ee9c472b8ab39de8e)), closes [#6715](https://github.com/aws/aws-cdk/issues/6715) [#6489](https://github.com/aws/aws-cdk/issues/6489) +* **stepfunctions:** support paths in Pass state ([#8070](https://github.com/aws/aws-cdk/issues/8070)) ([86eac6a](https://github.com/aws/aws-cdk/commit/86eac6af074bf78a921c52d613eca0dd4a514a49)), closes [#7181](https://github.com/aws/aws-cdk/issues/7181) +* **stepfunctions-tasks:** task for starting a job run in AWS Glue ([#8143](https://github.com/aws/aws-cdk/issues/8143)) ([a721e67](https://github.com/aws/aws-cdk/commit/a721e670cdc9888cd67ef1a24021004e18bfd23c)) + + +### Bug Fixes + +* **apigateway:** contextAccountId in AccessLogField incorrectly resolves to requestId ([7b89e80](https://github.com/aws/aws-cdk/commit/7b89e805c716fa73d41cc97fcb728634e7a59136)), closes [#7952](https://github.com/aws/aws-cdk/issues/7952) [#7951](https://github.com/aws/aws-cdk/issues/7951) +* **autoscaling:** add noDevice as a volume type ([#7253](https://github.com/aws/aws-cdk/issues/7253)) ([751958b](https://github.com/aws/aws-cdk/commit/751958b69225fdfc52622781c618f5a77f881fb6)), closes [#7242](https://github.com/aws/aws-cdk/issues/7242) +* **aws-eks:** kubectlEnabled: false conflicts with addNodegroup ([#8119](https://github.com/aws/aws-cdk/issues/8119)) ([8610889](https://github.com/aws/aws-cdk/commit/86108890a51443dc06ec6325038c7b19cbdaee76)), closes [#7993](https://github.com/aws/aws-cdk/issues/7993) +* **cli:** paper cuts ([#8164](https://github.com/aws/aws-cdk/issues/8164)) ([af2ea60](https://github.com/aws/aws-cdk/commit/af2ea60e7ae4aaab17ddd10a9142e1809b4c8246)) +* **dynamodb:** the maximum number of nonKeyAttributes is 100, not 20 ([#8186](https://github.com/aws/aws-cdk/issues/8186)) ([0393528](https://github.com/aws/aws-cdk/commit/03935280f1addef392c9b4460737cce8bb2eb8c9)), closes [#8095](https://github.com/aws/aws-cdk/issues/8095) +* **eks:** unable to add multiple service accounts ([#8122](https://github.com/aws/aws-cdk/issues/8122)) ([524440c](https://github.com/aws/aws-cdk/commit/524440c5454d15276c92581a08d4ee7cad1790eb)) +* **events:** cannot use the same target account for 2 cross-account event sources ([#8068](https://github.com/aws/aws-cdk/issues/8068)) ([395c07c](https://github.com/aws/aws-cdk/commit/395c07c0cac7739743fc71d71fddd8880b608ead)), closes [#8010](https://github.com/aws/aws-cdk/issues/8010) +* **lambda-nodejs:** build fails on Windows ([#8140](https://github.com/aws/aws-cdk/issues/8140)) ([04490b1](https://github.com/aws/aws-cdk/commit/04490b134a05ec34523541a3ca282ba8957a7964)), closes [#8107](https://github.com/aws/aws-cdk/issues/8107) +* **cloudtrail:** better typed event selector apis ([#8097](https://github.com/aws/aws-cdk/issues/8097)) ([0028778](https://github.com/aws/aws-cdk/commit/0028778c0f00f2faa8dad25345cd17f311fad5da)) + ## [1.41.0](https://github.com/aws/aws-cdk/compare/v1.40.0...v1.41.0) (2020-05-21) diff --git a/lerna.json b/lerna.json index baa3940d032fb..b533a6ac4d33c 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.41.0" + "version": "1.42.0" } diff --git a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts index 713b662808ad4..03685cfc9c413 100644 --- a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts @@ -32,6 +32,13 @@ export interface PipelineDeployStackActionProps { */ readonly createChangeSetRunOrder?: number; + /** + * The name of the CodePipeline action creating the ChangeSet. + * + * @default 'ChangeSet' + */ + readonly createChangeSetActionName?: string; + /** * The runOrder for the CodePipeline action executing the ChangeSet. * @@ -39,6 +46,13 @@ export interface PipelineDeployStackActionProps { */ readonly executeChangeSetRunOrder?: number; + /** + * The name of the CodePipeline action creating the ChangeSet. + * + * @default 'Execute' + */ + readonly executeChangeSetActionName?: string; + /** * IAM role to assume when deploying changes. * @@ -116,7 +130,7 @@ export class PipelineDeployStackAction implements codepipeline.IAction { const changeSetName = props.changeSetName || 'CDK-CodePipeline-ChangeSet'; const capabilities = cfnCapabilities(props.adminPermissions, props.capabilities); this.prepareChangeSetAction = new cpactions.CloudFormationCreateReplaceChangeSetAction({ - actionName: 'ChangeSet', + actionName: props.createChangeSetActionName ?? 'ChangeSet', changeSetName, runOrder: createChangeSetRunOrder, stackName: props.stack.stackName, @@ -126,7 +140,7 @@ export class PipelineDeployStackAction implements codepipeline.IAction { capabilities, }); this.executeChangeSetAction = new cpactions.CloudFormationExecuteChangeSetAction({ - actionName: 'Execute', + actionName: props.executeChangeSetActionName ?? 'Execute', changeSetName, runOrder: executeChangeSetRunOrder, stackName: this.stack.stackName, diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts index d765eb887c14a..918279b480b30 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, isSuperObject } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike, isSuperObject } from '@aws-cdk/assert'; import * as cfn from '@aws-cdk/aws-cloudformation'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -406,6 +406,43 @@ export = nodeunit.testCase({ ); test.done(); }, + + 'allows overriding the ChangeSet and Execute action names'(test: nodeunit.Test) { + const stack = getTestStack(); + const selfUpdatingPipeline = createSelfUpdatingStack(stack); + selfUpdatingPipeline.pipeline.addStage({ + stageName: 'Deploy', + actions: [ + new PipelineDeployStackAction({ + input: selfUpdatingPipeline.synthesizedApp, + adminPermissions: true, + stack, + createChangeSetActionName: 'Prepare', + executeChangeSetActionName: 'Deploy', + }), + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + {}, + {}, + { + Name: 'Deploy', + Actions: [ + { + Name: 'Prepare', + }, + { + Name: 'Deploy', + }, + ], + }, + ], + })); + + test.done(); + }, }); class FakeAction implements codepipeline.IAction { diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 419078f88aebc..c72dba724f878 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -1,7 +1,7 @@ import { CfnResource, Construct, Lazy, RemovalPolicy, Resource, Stack } from '@aws-cdk/core'; import * as crypto from 'crypto'; import { CfnDeployment } from './apigateway.generated'; -import { IRestApi, RestApi } from './restapi'; +import { IRestApi, RestApi, SpecRestApi } from './restapi'; export interface DeploymentProps { /** @@ -155,7 +155,7 @@ class LatestDeploymentResource extends CfnDeployment { * add via `addToLogicalId`. */ protected prepare() { - if (this.api instanceof RestApi) { // Ignore IRestApi that are imported + if (this.api instanceof RestApi || this.api instanceof SpecRestApi) { // Ignore IRestApi that are imported // Add CfnRestApi to the logical id so a new deployment is triggered when any of its properties change. const cfnRestApiCF = (this.api.node.defaultChild as any)._toCloudFormation(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json index 71dd02f17ab9a..bcf74c12601fa 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json @@ -44,7 +44,7 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB49": { + "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -60,7 +60,7 @@ "Ref": "myapi4C7BF186" }, "DeploymentId": { - "Ref": "myapiDeployment92F2CB49" + "Ref": "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd" }, "StageName": "prod" } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json index 3eaae1ff8fd58..e319d4fb28ccd 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json @@ -53,7 +53,7 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB49": { + "myapiDeployment92F2CB49a59bca458e4fac1fcd742212ded42a65": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -69,7 +69,7 @@ "Ref": "myapi4C7BF186" }, "DeploymentId": { - "Ref": "myapiDeployment92F2CB49" + "Ref": "myapiDeployment92F2CB49a59bca458e4fac1fcd742212ded42a65" }, "StageName": "prod" } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts index 9c005cc849edc..6fb8770796824 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts @@ -69,6 +69,14 @@ export interface BitBucketSourceActionProps extends codepipeline.CommonAwsAction * @experimental */ export class BitBucketSourceAction extends Action { + /** + * The name of the property that holds the ARN of the CodeStar Connection + * inside of the CodePipeline Artifact's metadata. + * + * @internal + */ + public static readonly _CONNECTION_ARN_PROPERTY = 'CodeStarConnectionArnProperty'; + private readonly props: BitBucketSourceActionProps; constructor(props: BitBucketSourceActionProps) { @@ -98,6 +106,14 @@ export class BitBucketSourceAction extends Action { // the action needs to write the output to the pipeline bucket options.bucket.grantReadWrite(options.role); + // if codeBuildCloneOutput is true, + // save the connectionArn in the Artifact instance + // to be read by the CodeBuildAction later + if (this.props.codeBuildCloneOutput === true) { + this.props.output.setMetadata(BitBucketSourceAction._CONNECTION_ARN_PROPERTY, + this.props.connectionArn); + } + return { configuration: { ConnectionArn: this.props.connectionArn, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts index 48bdfed738c31..53d789b665262 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts @@ -2,6 +2,7 @@ import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; +import { BitBucketSourceAction } from '..'; import { Action } from '../action'; /** @@ -153,6 +154,19 @@ export class CodeBuildAction extends Action { }); } + // if any of the inputs come from the BitBucketSourceAction + // with codeBuildCloneOutput=true, + // grant the Project's Role to use the connection + for (const inputArtifact of this.actionProperties.inputs || []) { + const connectionArn = inputArtifact.getMetadata(BitBucketSourceAction._CONNECTION_ARN_PROPERTY); + if (connectionArn) { + this.props.project.addToRolePolicy(new iam.PolicyStatement({ + actions: ['codestar-connections:UseConnection'], + resources: [connectionArn], + })); + } + } + const configuration: any = { ProjectName: this.props.project.projectName, EnvironmentVariables: this.props.environmentVariables && diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index af13cef9e8ade..8f8cb92d237ef 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", - "@types/lodash": "^4.14.152", + "@types/lodash": "^4.14.153", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts index 90ed1a4159134..f245a720a2fd9 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts @@ -12,32 +12,8 @@ export = { 'produces the correct configuration when added to a pipeline'(test: Test) { const stack = new Stack(); - const sourceOutput = new codepipeline.Artifact(); - new codepipeline.Pipeline(stack, 'Pipeline', { - stages: [ - { - stageName: 'Source', - actions: [ - new cpactions.BitBucketSourceAction({ - actionName: 'BitBucket', - owner: 'aws', - repo: 'aws-cdk', - output: sourceOutput, - connectionArn: 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', - }), - ], - }, - { - stageName: 'Build', - actions: [ - new cpactions.CodeBuildAction({ - actionName: 'CodeBuild', - project: new codebuild.PipelineProject(stack, 'MyProject'), - input: sourceOutput, - }), - ], - }, - ], + createBitBucketAndCodeBuildPipeline(stack, { + codeBuildCloneOutput: false, }); expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { @@ -73,4 +49,69 @@ export = { test.done(); }, }, + + 'setting codeBuildCloneOutput=true adds permission to use the connection to the following CodeBuild Project'(test: Test) { + const stack = new Stack(); + + createBitBucketAndCodeBuildPipeline(stack, { + codeBuildCloneOutput: true, + }); + + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + ], + }, + {}, + {}, + {}, + {}, + { + 'Action': 'codestar-connections:UseConnection', + 'Effect': 'Allow', + 'Resource': 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + }, + ], + }, + })); + + test.done(); + }, }; + +function createBitBucketAndCodeBuildPipeline(stack: Stack, props: { codeBuildCloneOutput: boolean }): void { + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new cpactions.BitBucketSourceAction({ + actionName: 'BitBucket', + owner: 'aws', + repo: 'aws-cdk', + output: sourceOutput, + connectionArn: 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + codeBuildCloneOutput: props.codeBuildCloneOutput, + }), + ], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'CodeBuild', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput, + outputs: [new codepipeline.Artifact()], + }), + ], + }, + ], + }); +} diff --git a/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts b/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts index 79339691272b6..fab9b46edcfe6 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts @@ -17,6 +17,7 @@ export class Artifact { } private _artifactName?: string; + private readonly metadata: { [key: string]: any } = {}; constructor(artifactName?: string) { validation.validateArtifactName(artifactName); @@ -80,6 +81,25 @@ export class Artifact { }; } + /** + * Add arbitrary extra payload to the artifact under a given key. + * This can be used by CodePipeline actions to communicate data between themselves. + * If metadata was already present under the given key, + * it will be overwritten with the new value. + */ + public setMetadata(key: string, value: any): void { + this.metadata[key] = value; + } + + /** + * Retrieve the metadata stored in this artifact under the given key. + * If there is no metadata stored under the given key, + * null will be returned. + */ + public getMetadata(key: string): any { + return this.metadata[key]; + } + public toString() { return this.artifactName; } diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 26669b9b4f968..8c2952b9e04bd 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -477,4 +477,32 @@ pool.addDomain('CustomDomain', { Read more about [Using the Amazon Cognito Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain-prefix.html) and [Using Your Own -Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html). \ No newline at end of file +Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html). + +The `signInUrl()` methods returns the fully qualified URL to the login page for the user pool. This page comes from the +hosted UI configured with Cognito. Learn more at [Hosted UI with the Amazon Cognito +Console](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-integration.html#cognito-user-pools-create-an-app-integration). + +```ts +const userpool = new UserPool(this, 'UserPool', { + // ... +}); +const client = userpool.addClient('Client', { + // ... + oAuth: { + flows: { + implicitCodeGrant: true, + }, + callbackUrls: [ + 'https://myapp.com/home', + 'https://myapp.com/users', + ] + } +}) +const domain = userpool.addDomain('Domain', { + // ... +}); +const signInUrl = domain.signInUrl(client, { + redirectUrl: 'https://myapp.com/home', // must be a URL configured under 'callbackUrls' with the client +}) +``` diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index a4bf4953aa71d..72e800cdcedf7 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -47,22 +47,22 @@ export interface OAuthSettings { /** * OAuth flows that are allowed with this client. * @see - the 'Allowed OAuth Flows' section at https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html - * @default - all OAuth flows disabled + * @default {authorizationCodeGrant:true,implicitCodeGrant:true} */ - readonly flows: OAuthFlows; + readonly flows?: OAuthFlows; /** * List of allowed redirect URLs for the identity providers. - * @default - no callback URLs + * @default - ['https://example.com'] if either authorizationCodeGrant or implicitCodeGrant flows are enabled, no callback URLs otherwise. */ readonly callbackUrls?: string[]; /** * OAuth scopes that are allowed with this client. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html - * @default - no OAuth scopes are configured. + * @default [OAuthScope.PHONE,OAuthScope.EMAIL,OAuthScope.OPENID,OAuthScope.PROFILE,OAuthScope.COGNITO_ADMIN] */ - readonly scopes: OAuthScope[]; + readonly scopes?: OAuthScope[]; } /** @@ -236,6 +236,10 @@ export class UserPoolClient extends Resource implements IUserPoolClient { } public readonly userPoolClientId: string; + /** + * The OAuth flows enabled for this client. + */ + public readonly oAuthFlows: OAuthFlows; private readonly _userPoolClientName?: string; /* @@ -249,14 +253,28 @@ export class UserPoolClient extends Resource implements IUserPoolClient { constructor(scope: Construct, id: string, props: UserPoolClientProps) { super(scope, id); + this.oAuthFlows = props.oAuth?.flows ?? { + implicitCodeGrant: true, + authorizationCodeGrant: true, + }; + + let callbackUrls: string[] | undefined = props.oAuth?.callbackUrls; + if (this.oAuthFlows.authorizationCodeGrant || this.oAuthFlows.implicitCodeGrant) { + if (callbackUrls === undefined) { + callbackUrls = [ 'https://example.com' ]; + } else if (callbackUrls.length === 0) { + throw new Error('callbackUrl must not be empty when codeGrant or implicitGrant OAuth flows are enabled.'); + } + } + const resource = new CfnUserPoolClient(this, 'Resource', { clientName: props.userPoolClientName, generateSecret: props.generateSecret, userPoolId: props.userPool.userPoolId, explicitAuthFlows: this.configureAuthFlows(props), - allowedOAuthFlows: this.configureOAuthFlows(props.oAuth), + allowedOAuthFlows: this.configureOAuthFlows(), allowedOAuthScopes: this.configureOAuthScopes(props.oAuth), - callbackUrLs: (props.oAuth?.callbackUrls && props.oAuth?.callbackUrls.length > 0) ? props.oAuth?.callbackUrls : undefined, + callbackUrLs: callbackUrls && callbackUrls.length > 0 ? callbackUrls : undefined, allowedOAuthFlowsUserPoolClient: props.oAuth ? true : undefined, preventUserExistenceErrors: this.configurePreventUserExistenceErrors(props.preventUserExistenceErrors), supportedIdentityProviders: this.configureIdentityProviders(props), @@ -291,20 +309,14 @@ export class UserPoolClient extends Resource implements IUserPoolClient { return authFlows; } - private configureOAuthFlows(oAuth?: OAuthSettings): string[] | undefined { - if (oAuth?.flows.authorizationCodeGrant || oAuth?.flows.implicitCodeGrant) { - if (oAuth?.callbackUrls === undefined || oAuth?.callbackUrls.length === 0) { - throw new Error('callbackUrl must be specified when codeGrant or implicitGrant OAuth flows are enabled.'); - } - if (oAuth?.flows.clientCredentials) { - throw new Error('clientCredentials OAuth flow cannot be selected along with codeGrant or implicitGrant.'); - } + private configureOAuthFlows(): string[] | undefined { + if ((this.oAuthFlows.authorizationCodeGrant || this.oAuthFlows.implicitCodeGrant) && this.oAuthFlows.clientCredentials) { + throw new Error('clientCredentials OAuth flow cannot be selected along with codeGrant or implicitGrant.'); } - const oAuthFlows: string[] = []; - if (oAuth?.flows.clientCredentials) { oAuthFlows.push('client_credentials'); } - if (oAuth?.flows.implicitCodeGrant) { oAuthFlows.push('implicit'); } - if (oAuth?.flows.authorizationCodeGrant) { oAuthFlows.push('code'); } + if (this.oAuthFlows.clientCredentials) { oAuthFlows.push('client_credentials'); } + if (this.oAuthFlows.implicitCodeGrant) { oAuthFlows.push('implicit'); } + if (this.oAuthFlows.authorizationCodeGrant) { oAuthFlows.push('code'); } if (oAuthFlows.length === 0) { return undefined; @@ -312,16 +324,15 @@ export class UserPoolClient extends Resource implements IUserPoolClient { return oAuthFlows; } - private configureOAuthScopes(oAuth?: OAuthSettings): string[] | undefined { - const oAuthScopes = new Set(oAuth?.scopes.map((x) => x.scopeName)); + private configureOAuthScopes(oAuth?: OAuthSettings): string[] { + const scopes = oAuth?.scopes ?? [ OAuthScope.PROFILE, OAuthScope.PHONE, OAuthScope.EMAIL, OAuthScope.OPENID, + OAuthScope.COGNITO_ADMIN ]; + const scopeNames = new Set(scopes.map((x) => x.scopeName)); const autoOpenIdScopes = [ OAuthScope.PHONE, OAuthScope.EMAIL, OAuthScope.PROFILE ]; - if (autoOpenIdScopes.reduce((agg, s) => agg || oAuthScopes.has(s.scopeName), false)) { - oAuthScopes.add(OAuthScope.OPENID.scopeName); - } - if (oAuthScopes.size > 0) { - return Array.from(oAuthScopes); + if (autoOpenIdScopes.reduce((agg, s) => agg || scopeNames.has(s.scopeName), false)) { + scopeNames.add(OAuthScope.OPENID.scopeName); } - return undefined; + return Array.from(scopeNames); } private configurePreventUserExistenceErrors(prevent?: boolean): string | undefined { diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts index b1518861e2fbb..e829cd2c03713 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts @@ -1,8 +1,9 @@ import { ICertificate } from '@aws-cdk/aws-certificatemanager'; -import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { Construct, IResource, Resource, Stack } from '@aws-cdk/core'; import { AwsCustomResource, AwsCustomResourcePolicy, AwsSdkCall, PhysicalResourceId } from '@aws-cdk/custom-resources'; import { CfnUserPoolDomain } from './cognito.generated'; import { IUserPool } from './user-pool'; +import { UserPoolClient } from './user-pool-client'; /** * Represents a user pool domain. @@ -80,6 +81,7 @@ export interface UserPoolDomainProps extends UserPoolDomainOptions { */ export class UserPoolDomain extends Resource implements IUserPoolDomain { public readonly domainName: string; + private isCognitoDomain: boolean; constructor(scope: Construct, id: string, props: UserPoolDomainProps) { super(scope, id); @@ -92,6 +94,8 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain { throw new Error('domainPrefix for cognitoDomain can contain only lowercase alphabets, numbers and hyphens'); } + this.isCognitoDomain = !!props.cognitoDomain; + const domainName = props.cognitoDomain?.domainPrefix || props.customDomain?.domainName!; const resource = new CfnUserPoolDomain(this, 'Resource', { userPoolId: props.userPool.userPoolId, @@ -126,4 +130,48 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain { }); return customResource.getResponseField('DomainDescription.CloudFrontDistribution'); } + + /** + * The URL to the hosted UI associated with this domain + */ + public baseUrl(): string { + if (this.isCognitoDomain) { + return `https://${this.domainName}.auth.${Stack.of(this).region}.amazoncognito.com`; + } + return `https://${this.domainName}`; + } + + /** + * The URL to the sign in page in this domain using a specific UserPoolClient + * @param client [disable-awslint:ref-via-interface] the user pool client that the UI will use to interact with the UserPool + * @param options options to customize the behaviour of this method. + */ + public signInUrl(client: UserPoolClient, options: SignInUrlOptions): string { + let responseType: string; + if (client.oAuthFlows.authorizationCodeGrant) { + responseType = 'code'; + } else if (client.oAuthFlows.implicitCodeGrant) { + responseType = 'token'; + } else { + throw new Error('signInUrl is not supported for clients without authorizationCodeGrant or implicitCodeGrant flow enabled'); + } + const path = options.signInPath ?? '/login'; + return `${this.baseUrl()}${path}?client_id=${client.userPoolClientId}&response_type=${responseType}&redirect_uri=${options.redirectUri}`; + } +} + +/** + * Options to customize the behaviour of `signInUrl()` + */ +export interface SignInUrlOptions { + /** + * Where to redirect to after sign in + */ + readonly redirectUri: string; + + /** + * The path in the URI where the sign-in page is located + * @default '/login' + */ + readonly signInPath?: string; } diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 24fef0a42db70..a0bc9a32d2874 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -3,7 +3,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import { Construct, Duration, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { CfnUserPool } from './cognito.generated'; import { ICustomAttribute, RequiredAttributes } from './user-pool-attr'; -import { IUserPoolClient, UserPoolClient, UserPoolClientOptions } from './user-pool-client'; +import { UserPoolClient, UserPoolClientOptions } from './user-pool-client'; import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain'; /** @@ -526,33 +526,52 @@ export interface IUserPool extends IResource { readonly userPoolArn: string; /** - * Create a user pool client. + * Add a new app client to this user pool. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html + */ + addClient(id: string, options?: UserPoolClientOptions): UserPoolClient; + + /** + * Associate a domain to this user pool. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html */ - addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient; + addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain; +} + +abstract class UserPoolBase extends Resource implements IUserPool { + public abstract readonly userPoolId: string; + public abstract readonly userPoolArn: string; + + public addClient(id: string, options?: UserPoolClientOptions): UserPoolClient { + return new UserPoolClient(this, id, { + userPool: this, + ...options, + }); + } + + public addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain { + return new UserPoolDomain(this, id, { + userPool: this, + ...options, + }); + } } /** * Define a Cognito User Pool */ -export class UserPool extends Resource implements IUserPool { +export class UserPool extends UserPoolBase { /** * Import an existing user pool based on its id. */ public static fromUserPoolId(scope: Construct, id: string, userPoolId: string): IUserPool { - class Import extends Resource implements IUserPool { + class Import extends UserPoolBase { public readonly userPoolId = userPoolId; public readonly userPoolArn = Stack.of(this).formatArn({ service: 'cognito-idp', resource: 'userpool', resourceName: userPoolId, }); - - public addClient(clientId: string, options?: UserPoolClientOptions): IUserPoolClient { - return new UserPoolClient(this, clientId, { - userPool: this, - ...options, - }); - } } return new Import(scope, id); } @@ -669,28 +688,6 @@ export class UserPool extends Resource implements IUserPool { (this.triggers as any)[operation.operationName] = fn.functionArn; } - /** - * Add a new app client to this user pool. - * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html - */ - public addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient { - return new UserPoolClient(this, id, { - userPool: this, - ...options, - }); - } - - /** - * Associate a domain to this user pool. - * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html - */ - public addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain { - return new UserPoolDomain(this, id, { - userPool: this, - ...options, - }); - } - private addLambdaPermission(fn: lambda.IFunction, name: string): void { const capitalize = name.charAt(0).toUpperCase() + name.slice(1); fn.addPermission(`${capitalize}Cognito`, { diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json index edc3f5bc635dc..c39124006db33 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json @@ -79,8 +79,7 @@ "email", "openid", "profile", - "aws.cognito.signin.user.admin", - "my-resource-server/my-scope" + "aws.cognito.signin.user.admin" ], "CallbackURLs": [ "https://redirect-here.myapp.com" diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts index 92a8bd8f19321..6856739811bb3 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts @@ -27,7 +27,6 @@ userpool.addClient('myuserpoolclient', { OAuthScope.OPENID, OAuthScope.PROFILE, OAuthScope.COGNITO_ADMIN, - OAuthScope.custom('my-resource-server/my-scope'), ], callbackUrls: [ 'https://redirect-here.myapp.com' ], }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json new file mode 100644 index 0000000000000..254b68b5d32b1 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json @@ -0,0 +1,126 @@ +{ + "Resources": { + "UserPoolsmsRole4EA729DD": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "integuserpooldomainsigninurlUserPool1325E89F" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cognito-idp.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "sns-publish" + } + ] + } + }, + "UserPool6BA7E5F2": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsConfiguration": { + "ExternalId": "integuserpooldomainsigninurlUserPool1325E89F", + "SnsCallerArn": { + "Fn::GetAtt": [ + "UserPoolsmsRole4EA729DD", + "Arn" + ] + } + }, + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + } + }, + "UserPoolDomainD0EA232A": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "cdk-integ-user-pool-domain", + "UserPoolId": { + "Ref": "UserPool6BA7E5F2" + } + } + }, + "UserPoolUserPoolClient40176907": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "UserPool6BA7E5F2" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + } + }, + "Outputs": { + "SignInUrl": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "UserPoolDomainD0EA232A" + }, + ".auth.", + { + "Ref": "AWS::Region" + }, + ".amazoncognito.com/login?client_id=", + { + "Ref": "UserPoolUserPoolClient40176907" + }, + "&response_type=code&redirect_uri=https://example.com" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.ts new file mode 100644 index 0000000000000..c02f116ccc691 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.ts @@ -0,0 +1,31 @@ +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { UserPool } from '../lib'; + +/* + * Stack verification steps: + * * Run the command `curl -sS -D - '' -o /dev/null` should return HTTP/2 200. + * * It didn't work if it returns 302 or 400. + */ + +const app = new App(); +const stack = new Stack(app, 'integ-user-pool-domain-signinurl'); + +const userpool = new UserPool(stack, 'UserPool'); + +const domain = userpool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cdk-integ-user-pool-domain', + }, +}); + +const client = userpool.addClient('UserPoolClient', { + oAuth: { + callbackUrls: [ 'https://example.com' ], + }, +}); + +new CfnOutput(stack, 'SignInUrl', { + value: domain.signInUrl(client, { + redirectUri: 'https://example.com', + }), +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json index c2f53d327d120..b14204b367441 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json @@ -83,6 +83,20 @@ "UserPoolId": { "Ref": "myuserpool01998219" }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], "ClientName": "signup-test", "GenerateSecret": false, "SupportedIdentityProviders": [ diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json index 3d1701d150895..02893c7ef113f 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json @@ -75,27 +75,41 @@ } } }, + "myuserpoolmyuserpooldomainEE1E11AF": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "integ-user-pool-signup-link", + "UserPoolId": { + "Ref": "myuserpool01998219" + } + } + }, "myuserpoolclient8A58A3E4": { "Type": "AWS::Cognito::UserPoolClient", "Properties": { "UserPoolId": { "Ref": "myuserpool01998219" }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], "ClientName": "signup-test", "GenerateSecret": false, "SupportedIdentityProviders": [ "COGNITO" ] } - }, - "myuserpooldomain": { - "Type": "AWS::Cognito::UserPoolDomain", - "Properties": { - "Domain": "integuserpoolsignuplinkmyuserpoolA8374994", - "UserPoolId": { - "Ref": "myuserpool01998219" - } - } } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts index 089249329fdbc..92f0452010f22 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts @@ -1,5 +1,5 @@ import { App, CfnOutput, Stack } from '@aws-cdk/core'; -import { CfnUserPoolDomain, UserPool, UserPoolClient, VerificationEmailStyle } from '../lib'; +import { UserPool, UserPoolClient, VerificationEmailStyle } from '../lib'; /* * Stack verification steps: @@ -41,10 +41,10 @@ const client = new UserPoolClient(stack, 'myuserpoolclient', { generateSecret: false, }); -// replace with L2 once Domain support is available -new CfnUserPoolDomain(stack, 'myuserpooldomain', { - userPoolId: userpool.userPoolId, - domain: userpool.node.uniqueId, +userpool.addDomain('myuserpooldomain', { + cognitoDomain: { + domainPrefix: 'integ-user-pool-signup-link', + }, }); new CfnOutput(stack, 'user-pool-id', { diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index ae3ad7b8d7271..37ece9b7fe3e8 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -17,6 +17,9 @@ describe('User Pool Client', () => { // THEN expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { UserPoolId: stack.resolve(pool.userPoolId), + AllowedOAuthFlows: [ 'implicit', 'code' ], + AllowedOAuthScopes: [ 'profile', 'phone', 'email', 'openid', 'aws.cognito.signin.user.admin' ], + CallbackURLs: [ 'https://example.com' ], SupportedIdentityProviders: [ 'COGNITO' ], }); }); @@ -92,21 +95,6 @@ describe('User Pool Client', () => { }); }); - test('AllowedOAuthFlows is absent by default', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'Pool'); - - // WHEN - pool.addClient('Client'); - - // THEN - expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { - AllowedOAuthFlows: ABSENT, - // AllowedOAuthFlowsUserPoolClient: ABSENT, - }); - }); - test('AllowedOAuthFlows are correctly named', () => { // GIVEN const stack = new Stack(); @@ -119,7 +107,6 @@ describe('User Pool Client', () => { authorizationCodeGrant: true, implicitCodeGrant: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, }); @@ -128,7 +115,6 @@ describe('User Pool Client', () => { flows: { clientCredentials: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, }); @@ -145,28 +131,72 @@ describe('User Pool Client', () => { }); }); - test('fails when callbackUrls are not specified for codeGrant or implicitGrant', () => { + test('callbackUrl defaults are correctly chosen', () => { const stack = new Stack(); const pool = new UserPool(stack, 'Pool'); - expect(() => pool.addClient('Client1', { + pool.addClient('Client1', { oAuth: { - flows: { authorizationCodeGrant: true }, - scopes: [ OAuthScope.PHONE ], + flows: { + clientCredentials: true, + }, }, - })).toThrow(/callbackUrl must be specified/); + }); - expect(() => pool.addClient('Client2', { + pool.addClient('Client2', { + oAuth: { + flows: { + authorizationCodeGrant: true, + }, + }, + }); + + pool.addClient('Client3', { + oAuth: { + flows: { + implicitCodeGrant: true, + }, + }, + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'client_credentials' ], + CallbackURLs: ABSENT, + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'implicit' ], + CallbackURLs: [ 'https://example.com' ], + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'code' ], + CallbackURLs: [ 'https://example.com' ], + }); + }); + + test('fails when callbackUrls is empty for codeGrant or implicitGrant', () => { + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + expect(() => pool.addClient('Client1', { oAuth: { flows: { implicitCodeGrant: true }, - scopes: [ OAuthScope.PHONE ], + callbackUrls: [], }, - })).toThrow(/callbackUrl must be specified/); + })).toThrow(/callbackUrl must not be empty/); expect(() => pool.addClient('Client3', { + oAuth: { + flows: { authorizationCodeGrant: true }, + callbackUrls: [], + }, + })).toThrow(/callbackUrl must not be empty/); + + expect(() => pool.addClient('Client4', { oAuth: { flows: { clientCredentials: true }, - scopes: [ OAuthScope.PHONE ], + callbackUrls: [], }, })).not.toThrow(); }); @@ -181,7 +211,6 @@ describe('User Pool Client', () => { authorizationCodeGrant: true, clientCredentials: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, })).toThrow(/clientCredentials OAuth flow cannot be selected/); @@ -192,7 +221,6 @@ describe('User Pool Client', () => { implicitCodeGrant: true, clientCredentials: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, })).toThrow(/clientCredentials OAuth flow cannot be selected/); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts index 8aa2a7972732b..b2a9c2bb326ad 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts @@ -125,4 +125,67 @@ describe('User Pool Client', () => { }, }); }); + + describe('signInUrl', () => { + test('returns the expected URL', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + const domain = pool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cognito-domain-prefix', + }, + }); + const client = pool.addClient('Client', { + oAuth: { + callbackUrls: [ 'https://example.com' ], + }, + }); + + // WHEN + const signInUrl = domain.signInUrl(client, { + redirectUri: 'https://example.com', + }); + + // THEN + expect(stack.resolve(signInUrl)).toEqual({ + 'Fn::Join': [ + '', [ + 'https://', + { Ref: 'PoolDomainCFC71F56' }, + '.auth.', + { Ref: 'AWS::Region' }, + '.amazoncognito.com/login?client_id=', + { Ref: 'PoolClient8A3E5EB7' }, + '&response_type=code&redirect_uri=https://example.com', + ], + ], + }); + }); + + test('correctly uses the signInPath', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + const domain = pool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cognito-domain-prefix', + }, + }); + const client = pool.addClient('Client', { + oAuth: { + callbackUrls: [ 'https://example.com' ], + }, + }); + + // WHEN + const signInUrl = domain.signInUrl(client, { + redirectUri: 'https://example.com', + signInPath: '/testsignin', + }); + + // THEN + expect(signInUrl).toMatch(/amazoncognito\.com\/testsignin\?/); + }); + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts index e076d9e79bd2f..83d4863b751c3 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -818,6 +818,35 @@ test('addClient', () => { }); }); +test('addDomain', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const userpool = new UserPool(stack, 'Pool'); + userpool.addDomain('UserPoolDomain', { + cognitoDomain: { + domainPrefix: 'userpooldomain', + }, + }); + const imported = UserPool.fromUserPoolId(stack, 'imported', 'imported-userpool-id'); + imported.addDomain('UserPoolImportedDomain', { + cognitoDomain: { + domainPrefix: 'userpoolimporteddomain', + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolDomain', { + Domain: 'userpooldomain', + UserPoolId: stack.resolve(userpool.userPoolId), + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolDomain', { + Domain: 'userpoolimporteddomain', + UserPoolId: stack.resolve(imported.userPoolId), + }); +}); + function fooFunction(scope: Construct, name: string): lambda.IFunction { return new lambda.Function(scope, name, { functionName: name, diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index f63ff13a25cf4..1c1802f039153 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -412,7 +412,7 @@ export interface ITable extends IResource { export interface TableAttributes { /** * The ARN of the dynamodb table. - * One of this, or {@link tabeName}, is required. + * One of this, or {@link tableName}, is required. * * @default - no table arn */ @@ -420,7 +420,7 @@ export interface TableAttributes { /** * The table name of the dynamodb table. - * One of this, or {@link tabeArn}, is required. + * One of this, or {@link tableArn}, is required. * * @default - no table name */ @@ -439,6 +439,28 @@ export interface TableAttributes { * @default - no key */ readonly encryptionKey?: kms.IKey; + + /** + * The name of the global indexes set for this Table. + * Note that you need to set either this property, + * or {@link localIndexes}, + * if you want methods like grantReadData() + * to grant permissions for indexes as well as the table itself. + * + * @default - no global indexes + */ + readonly globalIndexes?: string[]; + + /** + * The name of the local indexes set for this Table. + * Note that you need to set either this property, + * or {@link globalIndexes}, + * if you want methods like grantReadData() + * to grant permissions for indexes as well as the table itself. + * + * @default - no local indexes + */ + readonly localIndexes?: string[]; } abstract class TableBase extends Resource implements ITable { @@ -682,7 +704,7 @@ abstract class TableBase extends Resource implements ITable { private combinedGrant( grantee: iam.IGrantable, opts: {keyActions?: string[], tableActions?: string[], streamActions?: string[]}, - ) { + ): iam.Grant { if (opts.tableActions) { const resources = [this.tableArn, Lazy.stringValue({ produce: () => this.hasIndex ? `${this.tableArn}/index/*` : Aws.NO_VALUE }), @@ -773,6 +795,8 @@ export class Table extends TableBase { public readonly tableArn: string; public readonly tableStreamArn?: string; public readonly encryptionKey?: kms.IKey; + protected readonly hasIndex = (attrs.globalIndexes ?? []).length > 0 || + (attrs.localIndexes ?? []).length > 0; constructor(_tableArn: string, tableName: string, tableStreamArn?: string) { super(scope, id); @@ -781,10 +805,6 @@ export class Table extends TableBase { this.tableStreamArn = tableStreamArn; this.encryptionKey = attrs.encryptionKey; } - - protected get hasIndex(): boolean { - return false; - } } let name: string; diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 068cfaf5b0edb..c0c0fe9633ac0 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -2182,6 +2182,63 @@ describe('import', () => { Roles: [stack.resolve(role.roleName)], }); }); + + test('creates the correct index grant if indexes have been provided when importing', () => { + const stack = new Stack(); + + const table = Table.fromTableAttributes(stack, 'ImportedTable', { + tableName: 'MyTableName', + globalIndexes: ['global'], + localIndexes: ['local'], + }); + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + }); + + table.grantReadData(role); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'dynamodb:BatchGetItem', + 'dynamodb:GetRecords', + 'dynamodb:GetShardIterator', + 'dynamodb:Query', + 'dynamodb:GetItem', + 'dynamodb:Scan', + ], + Resource: [ + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':dynamodb:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':table/MyTableName', + ]], + }, + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':dynamodb:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':table/MyTableName/index/*', + ]], + }, + ], + }, + ], + }, + }); + }); }); }); diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 292d921efb0fa..367e4dc8206d9 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -68,7 +68,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/aws-lambda": "^8.10.39", - "@types/lodash": "^4.14.152", + "@types/lodash": "^4.14.153", "@types/nodeunit": "^0.0.31", "@types/sinon": "^9.0.3", "aws-sdk": "^2.681.0", diff --git a/packages/@aws-cdk/aws-redshift/README.md b/packages/@aws-cdk/aws-redshift/README.md index 2bf53e2033f34..05736c4c15c2c 100644 --- a/packages/@aws-cdk/aws-redshift/README.md +++ b/packages/@aws-cdk/aws-redshift/README.md @@ -9,4 +9,52 @@ --- +### Starting a Redshift Cluster Database + +To set up a Redshift cluster, define a `Cluster`. It will be launched in a VPC. +You can specify a VPC, otherwise one will be created. The nodes are always launched in private subnets and are encrypted by default. + +``` typescript +import redshift = require('@aws-cdk/aws-redshift'); +... +const cluster = new redshift.Cluster(this, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc + }); +``` +By default, the master password will be generated and stored in AWS Secrets Manager. + +A default database named `default_db` will be created in the cluster. To change the name of this database set the `defaultDatabaseName` attribute in the constructor properties. + +### Connecting + +To control who can access the cluster, use the `.connections` attribute. Redshift Clusters have +a default port, so you don't need to specify the port: + +```ts +cluster.connections.allowFromAnyIpv4('Open to the world'); +``` + +The endpoint to access your database cluster will be available as the `.clusterEndpoint` attribute: + +```ts +cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" +``` + +### Rotating credentials + +When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically: +```ts +cluster.addRotationSingleUser(); // Will rotate automatically after 30 days +``` + +The multi user rotation scheme is also available: +```ts +cluster.addRotationMultiUser('MyUser', { + secret: myImportedSecret +}); +``` + This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. diff --git a/packages/@aws-cdk/aws-redshift/lib/cluster.ts b/packages/@aws-cdk/aws-redshift/lib/cluster.ts new file mode 100644 index 0000000000000..48caa7aabf1db --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/cluster.ts @@ -0,0 +1,540 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import { Construct, Duration, IResource, RemovalPolicy, Resource, SecretValue, Token } from '@aws-cdk/core'; +import { DatabaseSecret } from './database-secret'; +import { Endpoint } from './endpoint'; +import { IClusterParameterGroup } from './parameter-group'; +import { CfnCluster, CfnClusterSubnetGroup } from './redshift.generated'; + +/** + * Possible Node Types to use in the cluster + * used for defining {@link ClusterProps.nodeType}. + */ +export enum NodeType { + /** + * ds2.xlarge + */ + DS2_XLARGE = 'ds2.xlarge', + /** + * ds2.8xlarge + */ + DS2_8XLARGE = 'ds2.8xlarge', + /** + * dc1.large + */ + DC1_LARGE = 'dc1.large', + /** + * dc1.8xlarge + */ + DC1_8XLARGE = 'dc1.8xlarge', + /** + * dc2.large + */ + DC2_LARGE = 'dc2.large', + /** + * dc2.8xlarge + */ + DC2_8XLARGE = 'dc2.8xlarge', + /** + * ra3.16xlarge + */ + RA3_16XLARGE = 'ra3.16xlarge', +} + +/** + * What cluster type to use. + * Used by {@link ClusterProps.clusterType} + */ +export enum ClusterType { + /** + * single-node cluster, the {@link ClusterProps.numberOfNodes} parameter is not required + */ + SINGLE_NODE = 'single-node', + /** + * multi-node cluster, set the amount of nodes using {@link ClusterProps.numberOfNodes} parameter + */ + MULTI_NODE = 'multi-node', +} + +/** + * Username and password combination + */ +export interface Login { + /** + * Username + */ + readonly masterUsername: string; + + /** + * Password + * + * Do not put passwords in your CDK code directly. + * + * @default a Secrets Manager generated password + */ + readonly masterPassword?: SecretValue; + + /** + * KMS encryption key to encrypt the generated secret. + * + * @default default master key + */ + readonly encryptionKey?: kms.IKey; +} + +/** + * Options to add the multi user rotation + */ +export interface RotationMultiUserOptions { + /** + * The secret to rotate. It must be a JSON string with the following format: + * ``` + * { + * "engine": , + * "host": , + * "username": , + * "password": , + * "dbname": , + * "port": , + * "masterarn": + * } + * ``` + */ + readonly secret: secretsmanager.ISecret; + + /** + * Specifies the number of days after the previous rotation before + * Secrets Manager triggers the next automatic rotation. + * + * @default Duration.days(30) + */ + readonly automaticallyAfter?: Duration; +} + +/** + * Create a Redshift Cluster with a given number of nodes. + * Implemented by {@link Cluster} via {@link ClusterBase}. + */ +export interface ICluster extends IResource, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget { + /** + * Name of the cluster + * + * @attribute ClusterName + */ + readonly clusterName: string; + + /** + * The endpoint to use for read/write operations + * + * @attribute EndpointAddress,EndpointPort + */ + readonly clusterEndpoint: Endpoint; +} + +/** + * Properties that describe an existing cluster instance + */ +export interface ClusterAttributes { + /** + * The security groups of the redshift cluster + * + * @default no security groups will be attached to the import + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * Identifier for the cluster + */ + readonly clusterName: string; + + /** + * Cluster endpoint address + */ + readonly clusterEndpointAddress: string; + + /** + * Cluster endpoint port + */ + readonly clusterEndpointPort: number; + +} + +/** + * Properties for a new database cluster + */ +export interface ClusterProps { + + /** + * An optional identifier for the cluster + * + * @default - A name is automatically generated. + */ + readonly clusterName?: string; + + /** + * Additional parameters to pass to the database engine + * https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-parameter-groups.html + * + * @default - No parameter group. + */ + readonly parameterGroup?: IClusterParameterGroup; + + /** + * Number of compute nodes in the cluster + * + * Value must be at least 1 and no more than 100. + * + * @default 1 + */ + readonly numberOfNodes?: number; + + /** + * The node type to be provisioned for the cluster. + * + * @default {@link NodeType.DC2_LARGE} + */ + readonly nodeType?: NodeType; + + /** + * Settings for the individual instances that are launched + * + * @default {@link ClusterType.MULTI_NODE} + */ + readonly clusterType?: ClusterType; + + /** + * What port to listen on + * + * @default - The default for the engine is used. + */ + readonly port?: number; + + /** + * Whether to enable encryption of data at rest in the cluster. + * + * @default true + */ + readonly encrypted?: boolean + + /** + * The KMS key to use for encryption of data at rest. + * + * @default - AWS-managed key, if encryption at rest is enabled + */ + readonly encryptionKey?: kms.IKey; + + /** + * A preferred maintenance window day/time range. Should be specified as a range ddd:hh24:mi-ddd:hh24:mi (24H Clock UTC). + * + * Example: 'Sun:23:45-Mon:00:15' + * + * @default - 30-minute window selected at random from an 8-hour block of time for + * each AWS Region, occurring on a random day of the week. + * @see https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_UpgradeDBInstance.Maintenance.html#Concepts.DBMaintenance + */ + readonly preferredMaintenanceWindow?: string; + + /** + * The VPC to place the cluster in. + */ + readonly vpc: ec2.IVpc; + + /** + * Where to place the instances within the VPC + * + * @default private subnets + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * Security group. + * + * @default a new security group is created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * Username and password for the administrative user + */ + readonly masterUser: Login; + + /** + * A list of AWS Identity and Access Management (IAM) role that can be used by the cluster to access other AWS services. + * Specify a maximum of 10 roles. + * + * @default - No role is attached to the cluster. + */ + readonly roles?: iam.IRole[]; + + /** + * Name of a database which is automatically created inside the cluster + * + * @default - default_db + */ + readonly defaultDatabaseName?: string; + + /** + * Bucket to send logs to. + * Logging information includes queries and connection attempts, for the specified Amazon Redshift cluster. + * + * @default - No Logs + */ + readonly loggingBucket?: s3.IBucket + + /** + * Prefix used for logging + * + * @default - no prefix + */ + readonly loggingKeyPrefix?: string + + /** + * The removal policy to apply when the cluster and its instances are removed + * from the stack or replaced during an update. + * + * @default RemovalPolicy.RETAIN + */ + readonly removalPolicy?: RemovalPolicy +} + +/** + * A new or imported clustered database. + */ +abstract class ClusterBase extends Resource implements ICluster { + /** + * Name of the cluster + */ + public abstract readonly clusterName: string; + + /** + * The endpoint to use for read/write operations + */ + public abstract readonly clusterEndpoint: Endpoint; + + /** + * Access to the network connections + */ + public abstract readonly connections: ec2.Connections; + + /** + * Renders the secret attachment target specifications. + */ + public asSecretAttachmentTarget(): secretsmanager.SecretAttachmentTargetProps { + return { + targetId: this.clusterName, + targetType: secretsmanager.AttachmentTargetType.REDSHIFT_CLUSTER, + }; + } +} + +/** + * Create a Redshift cluster a given number of nodes. + * + * @resource AWS::Redshift::Cluster + */ +export class Cluster extends ClusterBase { + /** + * Import an existing DatabaseCluster from properties + */ + public static fromClusterAttributes(scope: Construct, id: string, attrs: ClusterAttributes): ICluster { + class Import extends ClusterBase { + public readonly connections = new ec2.Connections({ + securityGroups: attrs.securityGroups, + defaultPort: ec2.Port.tcp(attrs.clusterEndpointPort), + }); + public readonly clusterName = attrs.clusterName; + public readonly instanceIdentifiers: string[] = []; + public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.clusterEndpointPort); + } + + return new Import(scope, id); + } + + /** + * Identifier of the cluster + */ + public readonly clusterName: string; + + /** + * The endpoint to use for read/write operations + */ + public readonly clusterEndpoint: Endpoint; + + /** + * Access to the network connections + */ + public readonly connections: ec2.Connections; + + /** + * The secret attached to this cluster + */ + public readonly secret?: secretsmanager.ISecret; + + private readonly singleUserRotationApplication: secretsmanager.SecretRotationApplication; + private readonly multiUserRotationApplication: secretsmanager.SecretRotationApplication; + + /** + * The VPC where the DB subnet group is created. + */ + private readonly vpc: ec2.IVpc; + + /** + * The subnets used by the DB subnet group. + */ + private readonly vpcSubnets?: ec2.SubnetSelection; + + constructor(scope: Construct, id: string, props: ClusterProps) { + super(scope, id); + + this.vpc = props.vpc; + this.vpcSubnets = props.vpcSubnets ? props.vpcSubnets : { + subnetType: ec2.SubnetType.PRIVATE, + }; + + const removalPolicy = props.removalPolicy ? props.removalPolicy : RemovalPolicy.RETAIN; + + const { subnetIds } = this.vpc.selectSubnets(this.vpcSubnets); + + const subnetGroup = new CfnClusterSubnetGroup(this, 'Subnets', { + description: `Subnets for ${id} Redshift cluster`, + subnetIds, + }); + + subnetGroup.applyRemovalPolicy(removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + const securityGroups = props.securityGroups !== undefined ? + props.securityGroups : [new ec2.SecurityGroup(this, 'SecurityGroup', { + description: 'Redshift security group', + vpc: this.vpc, + securityGroupName: 'redshift SG', + })]; + + const securityGroupIds = securityGroups.map(sg => sg.securityGroupId); + + let secret: DatabaseSecret | undefined; + if (!props.masterUser.masterPassword) { + secret = new DatabaseSecret(this, 'Secret', { + username: props.masterUser.masterUsername, + encryptionKey: props.masterUser.encryptionKey, + }); + } + + const clusterType = props.clusterType || ClusterType.MULTI_NODE; + const nodeCount = props.numberOfNodes !== undefined ? props.numberOfNodes : (clusterType === ClusterType.MULTI_NODE ? 2 : 1); + + if (clusterType === ClusterType.MULTI_NODE && nodeCount < 2) { + throw new Error('Number of nodes for cluster type multi-node must be at least 2'); + } + + if (props.encrypted === false && props.encryptionKey !== undefined) { + throw new Error('Cannot set property encryptionKey without enabling encryption!'); + } + + this.singleUserRotationApplication = secretsmanager.SecretRotationApplication.REDSHIFT_ROTATION_SINGLE_USER; + this.multiUserRotationApplication = secretsmanager.SecretRotationApplication.REDSHIFT_ROTATION_MULTI_USER; + + let loggingProperties; + if (props.loggingBucket) { + loggingProperties = { + bucketName: props.loggingBucket.bucketName, + s3KeyPrefix: props.loggingKeyPrefix, + }; + } + + const cluster = new CfnCluster(this, 'Resource', { + // Basic + allowVersionUpgrade: true, + automatedSnapshotRetentionPeriod: 1, + clusterType, + clusterIdentifier: props.clusterName, + clusterSubnetGroupName: subnetGroup.ref, + vpcSecurityGroupIds: securityGroupIds, + port: props.port, + clusterParameterGroupName: props.parameterGroup && props.parameterGroup.clusterParameterGroupName, + // Admin + masterUsername: secret ? secret.secretValueFromJson('username').toString() : props.masterUser.masterUsername, + masterUserPassword: secret + ? secret.secretValueFromJson('password').toString() + : (props.masterUser.masterPassword + ? props.masterUser.masterPassword.toString() + : 'default'), + preferredMaintenanceWindow: props.preferredMaintenanceWindow, + nodeType: props.nodeType || NodeType.DC2_LARGE, + numberOfNodes: nodeCount, + loggingProperties, + iamRoles: props.roles ? props.roles.map(role => role.roleArn) : undefined, + dbName: props.defaultDatabaseName || 'default_db', + publiclyAccessible: false, + // Encryption + kmsKeyId: props.encryptionKey && props.encryptionKey.keyArn, + encrypted: props.encrypted !== undefined ? props.encrypted : true, + }); + + cluster.applyRemovalPolicy(removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + this.clusterName = cluster.ref; + + // create a number token that represents the port of the cluster + const portAttribute = Token.asNumber(cluster.attrEndpointPort); + this.clusterEndpoint = new Endpoint(cluster.attrEndpointAddress, portAttribute); + + if (secret) { + this.secret = secret.attach(this); + } + + const defaultPort = ec2.Port.tcp(this.clusterEndpoint.port); + this.connections = new ec2.Connections({ securityGroups, defaultPort }); + } + + /** + * Adds the single user rotation of the master password to this cluster. + * + * @param [automaticallyAfter=Duration.days(30)] Specifies the number of days after the previous rotation + * before Secrets Manager triggers the next automatic rotation. + */ + public addRotationSingleUser(automaticallyAfter?: Duration): secretsmanager.SecretRotation { + if (!this.secret) { + throw new Error('Cannot add single user rotation for a cluster without secret.'); + } + + const id = 'RotationSingleUser'; + const existing = this.node.tryFindChild(id); + if (existing) { + throw new Error('A single user rotation was already added to this cluster.'); + } + + return new secretsmanager.SecretRotation(this, id, { + secret: this.secret, + automaticallyAfter, + application: this.singleUserRotationApplication, + vpc: this.vpc, + vpcSubnets: this.vpcSubnets, + target: this, + }); + } + + /** + * Adds the multi user rotation to this cluster. + */ + public addRotationMultiUser(id: string, options: RotationMultiUserOptions): secretsmanager.SecretRotation { + if (!this.secret) { + throw new Error('Cannot add multi user rotation for a cluster without secret.'); + } + return new secretsmanager.SecretRotation(this, id, { + secret: options.secret, + masterSecret: this.secret, + automaticallyAfter: options.automaticallyAfter, + application: this.multiUserRotationApplication, + vpc: this.vpc, + vpcSubnets: this.vpcSubnets, + target: this, + }); + } +} diff --git a/packages/@aws-cdk/aws-redshift/lib/database-secret.ts b/packages/@aws-cdk/aws-redshift/lib/database-secret.ts new file mode 100644 index 0000000000000..7e7617be2be83 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/database-secret.ts @@ -0,0 +1,39 @@ +import * as kms from '@aws-cdk/aws-kms'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import { Construct } from '@aws-cdk/core'; + +/** + * Construction properties for a DatabaseSecret. + */ +export interface DatabaseSecretProps { + /** + * The username. + */ + readonly username: string; + + /** + * The KMS key to use to encrypt the secret. + * + * @default default master key + */ + readonly encryptionKey?: kms.IKey; +} + +/** + * A database secret. + * + * @resource AWS::SecretsManager::Secret + */ +export class DatabaseSecret extends secretsmanager.Secret { + constructor(scope: Construct, id: string, props: DatabaseSecretProps) { + super(scope, id, { + encryptionKey: props.encryptionKey, + generateSecretString: { + passwordLength: 30, // Redshift password could be up to 64 characters + secretStringTemplate: JSON.stringify({ username: props.username }), + generateStringKey: 'password', + excludeCharacters: '"@/\\\ \'', + }, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/lib/endpoint.ts b/packages/@aws-cdk/aws-redshift/lib/endpoint.ts new file mode 100644 index 0000000000000..0ee19b8d82113 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/endpoint.ts @@ -0,0 +1,31 @@ +import { Token } from '@aws-cdk/core'; + +/** + * Connection endpoint of a redshift cluster + * + * Consists of a combination of hostname and port. + */ +export class Endpoint { + /** + * The hostname of the endpoint + */ + public readonly hostname: string; + + /** + * The port of the endpoint + */ + public readonly port: number; + + /** + * The combination of "HOSTNAME:PORT" for this endpoint + */ + public readonly socketAddress: string; + + constructor(address: string, port: number) { + this.hostname = address; + this.port = port; + + const portDesc = Token.isUnresolved(port) ? Token.asString(port) : port; + this.socketAddress = `${address}:${portDesc}`; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/lib/index.ts b/packages/@aws-cdk/aws-redshift/lib/index.ts index e1441fcf6bb03..6d5e5d00bb134 100644 --- a/packages/@aws-cdk/aws-redshift/lib/index.ts +++ b/packages/@aws-cdk/aws-redshift/lib/index.ts @@ -1,2 +1,7 @@ +export * from './cluster'; +export * from './parameter-group'; +export * from './database-secret'; +export * from './endpoint'; + // AWS::Redshift CloudFormation Resources: export * from './redshift.generated'; diff --git a/packages/@aws-cdk/aws-redshift/lib/parameter-group.ts b/packages/@aws-cdk/aws-redshift/lib/parameter-group.ts new file mode 100644 index 0000000000000..ea5698b235628 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/parameter-group.ts @@ -0,0 +1,77 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { CfnClusterParameterGroup } from './redshift.generated'; + +/** + * A parameter group + */ +export interface IClusterParameterGroup extends IResource { + /** + * The name of this parameter group + * + * @attribute + */ + readonly clusterParameterGroupName: string; +} + +/** + * A new cluster or instance parameter group + */ +abstract class ClusterParameterGroupBase extends Resource implements IClusterParameterGroup { + /** + * The name of the parameter group + */ + public abstract readonly clusterParameterGroupName: string; +} + +/** + * Properties for a parameter group + */ +export interface ClusterParameterGroupProps { + /** + * Description for this parameter group + * + * @default a CDK generated description + */ + readonly description?: string; + + /** + * The parameters in this parameter group + */ + readonly parameters: { [name: string]: string }; +} + +/** + * A cluster parameter group + * + * @resource AWS::Redshift::ClusterParameterGroup + */ +export class ClusterParameterGroup extends ClusterParameterGroupBase { + /** + * Imports a parameter group + */ + public static fromClusterParameterGroupName(scope: Construct, id: string, clusterParameterGroupName: string): IClusterParameterGroup { + class Import extends Resource implements IClusterParameterGroup { + public readonly clusterParameterGroupName = clusterParameterGroupName; + } + return new Import(scope, id); + } + + /** + * The name of the parameter group + */ + public readonly clusterParameterGroupName: string; + + constructor(scope: Construct, id: string, props: ClusterParameterGroupProps) { + super(scope, id); + + const resource = new CfnClusterParameterGroup(this, 'Resource', { + description: props.description || 'Cluster parameter group for family redshift-1.0', + parameterGroupFamily: 'redshift-1.0', + parameters: Object.entries(props.parameters).map(([name, value]) => { + return {parameterName: name, parameterValue: value}; + }), + }); + + this.clusterParameterGroupName = resource.ref; + } +} diff --git a/packages/@aws-cdk/aws-redshift/package.json b/packages/@aws-cdk/aws-redshift/package.json index 07283d9304b9f..3b645e15ba91e 100644 --- a/packages/@aws-cdk/aws-redshift/package.json +++ b/packages/@aws-cdk/aws-redshift/package.json @@ -66,20 +66,39 @@ "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", + "jest": "^25.5.3", "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, + "awslint": { + "exclude": [ + "docs-public-apis:@aws-cdk/aws-redshift.ParameterGroupParameters.parameterName", + "docs-public-apis:@aws-cdk/aws-redshift.ParameterGroupParameters.parameterValue", + "props-physical-name:@aws-cdk/aws-redshift.ClusterParameterGroupProps", + "props-physical-name:@aws-cdk/aws-redshift.DatabaseSecretProps" + ] + }, "stability": "experimental", "maturity": "cfn-only", "awscdkio": { diff --git a/packages/@aws-cdk/aws-redshift/test/cluster.test.ts b/packages/@aws-cdk/aws-redshift/test/cluster.test.ts new file mode 100644 index 0000000000000..385a2f53208b5 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/test/cluster.test.ts @@ -0,0 +1,329 @@ +import { expect as cdkExpect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as kms from '@aws-cdk/aws-kms'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; + +import { Cluster, ClusterParameterGroup, ClusterType, NodeType } from '../lib'; + +test('check that instantiation works', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + masterPassword: cdk.SecretValue.plainText('tooshort'), + }, + vpc, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + Properties: { + AllowVersionUpgrade: true, + MasterUsername: 'admin', + MasterUserPassword: 'tooshort', + ClusterType: 'multi-node', + AutomatedSnapshotRetentionPeriod: 1, + Encrypted: true, + NumberOfNodes: 2, + NodeType: 'dc2.large', + DBName: 'default_db', + PubliclyAccessible: false, + ClusterSubnetGroupName: { Ref: 'RedshiftSubnetsDFE70E0A' }, + VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['RedshiftSecurityGroup796D74A7', 'GroupId'] }], + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + cdkExpect(stack).to(haveResource('AWS::Redshift::ClusterSubnetGroup', { + Properties: { + Description: 'Subnets for Redshift Redshift cluster', + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + { Ref: 'VPCPrivateSubnet3Subnet3EDCD457' }, + ], + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); +}); + +test('can create a cluster with imported vpc and security group', () => { + // GIVEN + const stack = testStack(); + const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { + vpcId: 'VPC12345', + }); + const sg = ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'SecurityGroupId12345'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + masterPassword: cdk.SecretValue.plainText('tooshort'), + }, + vpc, + securityGroups: [sg], + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + ClusterSubnetGroupName: { Ref: 'RedshiftSubnetsDFE70E0A' }, + MasterUsername: 'admin', + MasterUserPassword: 'tooshort', + VpcSecurityGroupIds: ['SecurityGroupId12345'], + })); +}); + +test('creates a secret when master credentials are not specified', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + MasterUsername: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'RedshiftSecretA08D42D6', + }, + ':SecretString:username::}}', + ], + ], + }, + MasterUserPassword: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'RedshiftSecretA08D42D6', + }, + ':SecretString:password::}}', + ], + ], + }, + })); + + cdkExpect(stack).to(haveResource('AWS::SecretsManager::Secret', { + GenerateSecretString: { + ExcludeCharacters: '"@/\\\ \'', + GenerateStringKey: 'password', + PasswordLength: 30, + SecretStringTemplate: '{"username":"admin"}', + }, + })); +}); + +test('SIngle Node CLusters spawn only single node', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + nodeType: NodeType.DC1_8XLARGE, + clusterType: ClusterType.SINGLE_NODE, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + ClusterType: 'single-node', + NodeType: 'dc1.8xlarge', + NumberOfNodes: 1, + })); +}); + +test('create an encrypted cluster with custom KMS key', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + encryptionKey: new kms.Key(stack, 'Key'), + vpc, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + KmsKeyId: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + })); +}); + +test('cluster with parameter group', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const group = new ClusterParameterGroup(stack, 'Params', { + description: 'bye', + parameters: { + param: 'value', + }, + }); + + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + parameterGroup: group, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + ClusterParameterGroupName: { Ref: 'ParamsA8366201' }, + })); + +}); + +test('imported cluster with imported security group honors allowAllOutbound', () => { + // GIVEN + const stack = testStack(); + + const cluster = Cluster.fromClusterAttributes(stack, 'Database', { + clusterEndpointAddress: 'addr', + clusterName: 'identifier', + clusterEndpointPort: 3306, + securityGroups: [ + ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + ], + }); + + // WHEN + cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); + + // THEN + cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: 'sg-123456789', + })); +}); + +test('can create a cluster with logging enabled', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const bucket = s3.Bucket.fromBucketName(stack, 'bucket', 'logging-bucket'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + loggingBucket: bucket, + loggingKeyPrefix: 'prefix', + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + LoggingProperties: { + BucketName: 'logging-bucket', + S3KeyPrefix: 'prefix', + }, + })); +}); + +test('throws when trying to add rotation to a cluster without secret', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const cluster = new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + masterPassword: cdk.SecretValue.plainText('tooshort'), + }, + vpc, + }); + + // THEN + expect(() => { + cluster.addRotationSingleUser(); + }).toThrowError(); + +}); + +test('throws validation error when trying to set encryptionKey without enabling encryption', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const key = new kms.Key(stack, 'kms-key'); + + // WHEN + const props = { + encrypted: false, + encryptionKey: key, + masterUser: { + masterUsername: 'admin', + }, + vpc, + }; + + // THEN + expect(() => { + new Cluster(stack, 'Redshift', props ); + }).toThrowError(); + +}); + +test('throws when trying to add single user rotation multiple times', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + }); + + // WHEN + cluster.addRotationSingleUser(); + + // THEN + expect(() => { + cluster.addRotationSingleUser(); + }).toThrowError(); +}); + +function testStack() { + const stack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); + stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); + return stack; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/test/parameter-group.test.ts b/packages/@aws-cdk/aws-redshift/test/parameter-group.test.ts new file mode 100644 index 0000000000000..ca5923ee36ba6 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/test/parameter-group.test.ts @@ -0,0 +1,29 @@ +import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import { ClusterParameterGroup } from '../lib'; + +test('create a cluster parameter group', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ClusterParameterGroup(stack, 'Params', { + description: 'desc', + parameters: { + param: 'value', + }, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::ClusterParameterGroup', { + Description: 'desc', + ParameterGroupFamily: 'redshift-1.0', + Parameters: [ + { + ParameterName: 'param', + ParameterValue: 'value', + }, + ], + })); + +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/test/redshift.test.ts b/packages/@aws-cdk/aws-redshift/test/redshift.test.ts deleted file mode 100644 index e394ef336bfb4..0000000000000 --- a/packages/@aws-cdk/aws-redshift/test/redshift.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assert/jest'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index 485821ebe8fb7..a0927130ac58f 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -151,7 +151,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/lodash": "^4.14.152", + "@types/lodash": "^4.14.153", "@types/node": "^10.17.21", "@types/nodeunit": "^0.0.31", "@types/minimatch": "^3.0.3", diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 0ddee7d957395..09a93983b382d 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -39,7 +39,7 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^3.0.0", + "@typescript-eslint/eslint-plugin": "^3.0.2", "@typescript-eslint/parser": "^2.19.2", "awslint": "0.0.0", "colors": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index 1b23abd893193..657b0b9087378 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1507,10 +1507,10 @@ dependencies: jszip "*" -"@types/lodash@^4.14.152": - version "4.14.152" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.152.tgz#7e7679250adce14e749304cdb570969f77ec997c" - integrity sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg== +"@types/lodash@^4.14.153": + version "4.14.153" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.153.tgz#5cb7dded0649f1df97938ac5ffc4f134e9e9df98" + integrity sha512-lYniGRiRfZf2gGAR9cfRC3Pi5+Q1ziJCKqPmjZocigrSJUVPWf7st1BtSJ8JOeK0FLXVndQ1IjUjTco9CXGo/Q== "@types/md5@^2.2.0": version "2.2.0" @@ -1658,12 +1658,12 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.3.tgz#38fb31d82ed07dea87df6bd565721d11979fd761" integrity sha512-mhdQq10tYpiNncMkg1vovCud5jQm+rWeRVz6fxjCJlY6uhDlAn9GnMSmBa2DQwqPf/jS5YR0K/xChDEh1jdOQg== -"@typescript-eslint/eslint-plugin@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.0.0.tgz#02f8ec6b5ce814bda80dfc22463f108bed1f699b" - integrity sha512-lcZ0M6jD4cqGccYOERKdMtg+VWpoq3NSnWVxpc/AwAy0zhkUYVioOUZmfNqiNH8/eBNGhCn6HXd6mKIGRgNc1Q== +"@typescript-eslint/eslint-plugin@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.0.2.tgz#4a114a066e2f9659b25682ee59d4866e15a17ec3" + integrity sha512-ER3bSS/A/pKQT/hjMGCK8UQzlL0yLjuCZ/G8CDFJFVTfl3X65fvq2lNYqOG8JPTfrPa2RULCdwfOyFjZEMNExQ== dependencies: - "@typescript-eslint/experimental-utils" "3.0.0" + "@typescript-eslint/experimental-utils" "3.0.2" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" @@ -1679,13 +1679,13 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.0.0.tgz#1ddf53eeb61ac8eaa9a77072722790ac4f641c03" - integrity sha512-BN0vmr9N79M9s2ctITtChRuP1+Dls0x/wlg0RXW1yQ7WJKPurg6X3Xirv61J2sjPif4F8SLsFMs5Nzte0WYoTQ== +"@typescript-eslint/experimental-utils@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.0.2.tgz#bb2131baede8df28ec5eacfa540308ca895e5fee" + integrity sha512-4Wc4EczvoY183SSEnKgqAfkj1eLtRgBQ04AAeG+m4RhTVyaazxc1uI8IHf0qLmu7xXe9j1nn+UoDJjbmGmuqXQ== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "3.0.0" + "@typescript-eslint/typescript-estree" "3.0.2" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -1712,10 +1712,10 @@ semver "^6.3.0" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.0.0.tgz#fa40e1b76ccff880130be054d9c398e96004bf42" - integrity sha512-nevQvHyNghsfLrrByzVIH4ZG3NROgJ8LZlfh3ddwPPH4CH7W4GAiSx5qu+xHuX5pWsq6q/eqMc1io840ZhAnUg== +"@typescript-eslint/typescript-estree@3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.0.2.tgz#67a1ce4307ebaea43443fbf3f3be7e2627157293" + integrity sha512-cs84mxgC9zQ6viV8MEcigfIKQmKtBkZNDYf8Gru2M+MhnA6z9q0NFMZm2IEzKqAwN8lY5mFVd1Z8DiHj6zQ3Tw== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" @@ -1897,11 +1897,6 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" -app-root-path@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a" - integrity sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA== - append-transform@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" @@ -2130,7 +2125,7 @@ available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: dependencies: array-filter "^1.0.0" -aws-sdk-mock@^5.0.0, aws-sdk-mock@^5.1.0: +aws-sdk-mock@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/aws-sdk-mock/-/aws-sdk-mock-5.1.0.tgz#6f2c0bd670d7f378c906a8dd806f812124db71aa" integrity sha512-Wa5eCSo8HX0Snqb7FdBylaXMmfrAWoWZ+d7MFhiYsgHPvNvMEGjV945FF2qqE1U0Tolr1ALzik1fcwgaOhqUWQ== @@ -2139,21 +2134,6 @@ aws-sdk-mock@^5.0.0, aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.596.0: - version "2.684.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.684.0.tgz#14e313893b3225511d12f4a0985e7b0613603006" - integrity sha512-OkSMKIbRTBd3YV5iAklJxZFyLg0jRO2XW6+WhMuDBHMEs8aRbZw4iAD85wFC8tG7X9o0kcjnWfZUDq7MK1dcDg== - dependencies: - buffer "4.9.1" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.15.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - uuid "3.3.2" - xml2js "0.4.19" - aws-sdk@^2.637.0, aws-sdk@^2.681.0: version "2.681.0" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.681.0.tgz#09eeedb5ca49813dfc637908abe408ae114a6824" @@ -3671,16 +3651,6 @@ dot-prop@^4.2.0: dependencies: is-obj "^1.0.0" -dotenv-json@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dotenv-json/-/dotenv-json-1.0.0.tgz#fc7f672aafea04bed33818733b9f94662332815c" - integrity sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ== - -dotenv@^8.0.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" - integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== - dotgitignore@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/dotgitignore/-/dotgitignore-2.1.0.tgz#a4b15a4e4ef3cf383598aaf1dfa4a04bcc089b7b" @@ -3863,11 +3833,6 @@ escodegen@1.x.x, escodegen@^1.11.1: optionalDependencies: source-map "~0.6.1" -eslint-config-standard@^14.1.0: - version "14.1.1" - resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz#830a8e44e7aef7de67464979ad06b406026c56ea" - integrity sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg== - eslint-import-resolver-node@^0.3.2, eslint-import-resolver-node@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404" @@ -3895,15 +3860,7 @@ eslint-module-utils@^2.4.1: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-es@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz#0f5f5da5f18aa21989feebe8a73eadefb3432976" - integrity sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ== - dependencies: - eslint-utils "^1.4.2" - regexpp "^3.0.0" - -eslint-plugin-import@^2.19.1, eslint-plugin-import@^2.20.2: +eslint-plugin-import@^2.20.2: version "2.20.2" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz#91fc3807ce08be4837141272c8b99073906e588d" integrity sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg== @@ -3921,28 +3878,6 @@ eslint-plugin-import@^2.19.1, eslint-plugin-import@^2.20.2: read-pkg-up "^2.0.0" resolve "^1.12.0" -eslint-plugin-node@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz#fd1adbc7a300cf7eb6ac55cf4b0b6fc6e577f5a6" - integrity sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ== - dependencies: - eslint-plugin-es "^2.0.0" - eslint-utils "^1.4.2" - ignore "^5.1.1" - minimatch "^3.0.4" - resolve "^1.10.1" - semver "^6.1.0" - -eslint-plugin-promise@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" - integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== - -eslint-plugin-standard@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4" - integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ== - eslint-scope@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" @@ -3951,7 +3886,7 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^1.4.2, eslint-utils@^1.4.3: +eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== @@ -5009,11 +4944,6 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.6.tgz#643194ad4bf2712f37852e386b6998eff0db2106" - integrity sha512-cgXgkypZBcCnOgSihyeqbo6gjIaIyDqPQB7Ra4vhE9m6kigdGoQDMHjviFhRZo3IMlRy6yElosoviMs5YxZXUA== - immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -5976,7 +5906,7 @@ jest-worker@^25.5.0: merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^25.4.0, jest@^25.5.0, jest@^25.5.2, jest@^25.5.3, jest@^25.5.4: +jest@^25.4.0, jest@^25.5.2, jest@^25.5.3, jest@^25.5.4: version "25.5.4" resolved "https://registry.yarnpkg.com/jest/-/jest-25.5.4.tgz#f21107b6489cfe32b076ce2adcadee3587acb9db" integrity sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ== @@ -6261,24 +6191,6 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -lambda-leak@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lambda-leak/-/lambda-leak-2.0.0.tgz#771985d3628487f6e885afae2b54510dcfb2cd7e" - integrity sha1-dxmF02KEh/boha+uK1RRDc+yzX4= - -lambda-tester@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/lambda-tester/-/lambda-tester-3.6.0.tgz#ceb7d4f4f0da768487a05cff37dcd088508b5247" - integrity sha512-F2ZTGWCLyIR95o/jWK46V/WnOCFAEUG/m/V7/CLhPJ7PCM+pror1rZ6ujP3TkItSGxUfpJi0kqwidw+M/nEqWw== - dependencies: - app-root-path "^2.2.1" - dotenv "^8.0.0" - dotenv-json "^1.0.0" - lambda-leak "^2.0.0" - semver "^6.1.1" - uuid "^3.3.2" - vandium-utils "^1.1.1" - lazystream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" @@ -6884,7 +6796,7 @@ mkdirp@*, mkdirp@1.x: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -7015,17 +6927,6 @@ nise@^4.0.1: just-extend "^4.0.2" path-to-regexp "^1.7.0" -nock@^11.7.0: - version "11.9.1" - resolved "https://registry.yarnpkg.com/nock/-/nock-11.9.1.tgz#2b026c5beb6d0dbcb41e7e4cefa671bc36db9c61" - integrity sha512-U5wPctaY4/ar2JJ5Jg4wJxlbBfayxgKbiAeGh+a1kk6Pwnc2ZEuKviLyDSG6t0uXl56q7AALIxoM6FJrBSsVXA== - dependencies: - debug "^4.1.0" - json-stringify-safe "^5.0.1" - lodash "^4.17.13" - mkdirp "^0.5.0" - propagate "^2.0.0" - nock@^12.0.3: version "12.0.3" resolved "https://registry.yarnpkg.com/nock/-/nock-12.0.3.tgz#83f25076dbc4c9aa82b5cdf54c9604c7a778d1c9" @@ -8395,7 +8296,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.6, resolve@^1.10.1, resolve@^1.17.0: +resolve@^1.1.6, resolve@^1.17.0: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== @@ -8553,11 +8454,6 @@ semver-intersect@^1.4.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@6.x, semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - semver@7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.1.tgz#29104598a197d6cbe4733eeecbe968f7b43a9667" @@ -8568,6 +8464,11 @@ semver@7.x, semver@^7.2.2, semver@^7.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== +semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -9541,22 +9442,6 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= -ts-jest@^25.3.1: - version "25.5.1" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7" - integrity sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw== - dependencies: - bs-logger "0.x" - buffer-from "1.x" - fast-json-stable-stringify "2.x" - json5 "2.x" - lodash.memoize "4.x" - make-error "1.x" - micromatch "4.x" - mkdirp "0.x" - semver "6.x" - yargs-parser "18.x" - ts-jest@^26.0.0: version "26.0.0" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.0.0.tgz#957b802978249aaf74180b9dcb17b4fd787ad6f3" @@ -9907,11 +9792,6 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" -vandium-utils@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/vandium-utils/-/vandium-utils-1.2.0.tgz#44735de4b7641a05de59ebe945f174e582db4f59" - integrity sha1-RHNd5LdkGgXeWevpRfF05YLbT1k= - verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"