diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e18651f1b19c..df38cbd61a42e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 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.61.1](https://github.com/aws/aws-cdk/compare/v1.61.0...v1.61.1) (2020-08-28) + + +### Bug Fixes + +* **cli:** unable to upgrade new style bootstrap to version ([#10030](https://github.com/aws/aws-cdk/issues/10030)) ([8d3e422](https://github.com/aws/aws-cdk/commit/8d3e422809c29da926bae878276619a59ae82ecb)), closes [#10016](https://github.com/aws/aws-cdk/issues/10016) + ## [1.61.0](https://github.com/aws/aws-cdk/compare/v1.60.0...v1.61.0) (2020-08-27) @@ -39,7 +46,7 @@ All notable changes to this project will be documented in this file. See [standa ### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES * **cloudfront:** Distribution: `.domains` must be specified if `certificate` is provided. -* **appsync:** **appsync.addXxxDataSource** `name` and `description` props are now optional and in an `DataSourceOptions` interface. +* **appsync:** **appsync.addXxxDataSource** `name` and `description` props are now optional and in an `DataSourceOptions` interface. - **appsync**: the props `name` and `description` in `addXxxDataSource` have been moved into new props `options` of type `DataSourceOptions` - **appsync**: `DataSourceOptions.name` defaults to id - **appsync**: `DataSourceOptions.description` defaults to undefined diff --git a/lerna.json b/lerna.json index 6cd02888d14d3..87746feb847f4 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.61.0" + "version": "1.61.1" } diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index 80e9b88abc055..4251336f017ea 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -533,7 +533,7 @@ export class GraphQLApi extends GraphqlApiBase { userPoolId: config.userPool.userPoolId, awsRegion: config.userPool.stack.region, appIdClientRegex: config.appIdClientRegex, - defaultAction: config.defaultAction, + defaultAction: config.defaultAction || UserPoolDefaultAction.ALLOW, }; } diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-auth.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-auth.test.ts index e9ad68e4e66e0..fabde1d59c95e 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync-auth.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync-auth.test.ts @@ -250,6 +250,7 @@ describe('AppSync User Pool Authorization', () => { AuthenticationType: 'AMAZON_COGNITO_USER_POOLS', UserPoolConfig: { AwsRegion: { Ref: 'AWS::Region' }, + DefaultAction: 'ALLOW', UserPoolId: { Ref: 'pool056F3F7E' }, }, }); @@ -371,6 +372,7 @@ describe('AppSync User Pool Authorization', () => { AuthenticationType: 'AMAZON_COGNITO_USER_POOLS', UserPoolConfig: { AwsRegion: { Ref: 'AWS::Region' }, + DefaultAction: 'ALLOW', UserPoolId: { Ref: 'pool056F3F7E' }, }, AdditionalAuthenticationProviders: [ diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts index 715864b209bc1..9882fead1cf12 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.ts @@ -9,7 +9,6 @@ import { MappingTemplate, PrimaryKey, Schema, - UserPoolDefaultAction, Values, } from '../lib'; @@ -42,7 +41,6 @@ const api = new GraphQLApi(stack, 'Api', { authorizationType: AuthorizationType.USER_POOL, userPoolConfig: { userPool, - defaultAction: UserPoolDefaultAction.ALLOW, }, }, additionalAuthorizationModes: [ diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index ed8cb9d5eaea2..fd3aac1b32bba 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -82,7 +82,7 @@ new cloudfront.Distribution(this, 'myDist', { }); ``` -## From an HTTP endpoint +#### From an HTTP endpoint Origins can also be created from any other HTTP endpoint, given the domain name, and optionally, other origin properties. @@ -207,6 +207,7 @@ new cloudfront.Distribution(this, 'myDist', { { functionVersion: myFunc.currentVersion, eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, + includeBody: true, // Optional - defaults to false }, ], }, @@ -246,6 +247,18 @@ new cloudfront.Distribution(this, 'myDist', { }); ``` +### Importing Distributions + +Existing distributions can be imported as well; note that like most imported constructs, an imported distribution cannot be modified. +However, it can be used as a reference for other higher-level constructs. + +```ts +const distribution = cloudfront.Distribution.fromDistributionAttributes(scope, 'ImportedDist', { + domainName: 'd111111abcdef8.cloudfront.net', + distributionId: '012345ABCDEF', +}); +``` + ## CloudFrontWebDistribution API - Stable ![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) @@ -305,7 +318,7 @@ Example: [create a distrubution with an iam certificate example](test/example.iam-cert-alias.lit.ts) -#### Restrictions +### Restrictions CloudFront supports adding restrictions to your distribution. diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index a53d38fb054be..117e79cc26345 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -621,6 +621,15 @@ export interface EdgeLambda { /** The type of event in response to which should the function be invoked. */ readonly eventType: LambdaEdgeEventType; + + /** + * Allows a Lambda function to have read access to the body content. + * Only valid for "request" event types (`ORIGIN_REQUEST` or `VIEWER_REQUEST`). + * See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-include-body-access.html + * + * @default false + */ + readonly includeBody?: boolean; } /** diff --git a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts index 18b4d75aac2c7..2eca5e66362f5 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts @@ -1,5 +1,6 @@ +import * as iam from '@aws-cdk/aws-iam'; import { CfnDistribution } from '../cloudfront.generated'; -import { AddBehaviorOptions, ViewerProtocolPolicy } from '../distribution'; +import { AddBehaviorOptions, EdgeLambda, LambdaEdgeEventType, ViewerProtocolPolicy } from '../distribution'; /** * Properties for specifying custom behaviors for origins. @@ -24,6 +25,9 @@ export class CacheBehavior { constructor(originId: string, private readonly props: CacheBehaviorProps) { this.originId = originId; + + this.validateEdgeLambdas(props.edgeLambdas); + this.grantEdgeLambdaFunctionExecutionRole(props.edgeLambdas); } /** @@ -51,8 +55,29 @@ export class CacheBehavior { ? this.props.edgeLambdas.map(edgeLambda => ({ lambdaFunctionArn: edgeLambda.functionVersion.edgeArn, eventType: edgeLambda.eventType.toString(), + includeBody: edgeLambda.includeBody, })) : undefined, }; } + + private validateEdgeLambdas(edgeLambdas?: EdgeLambda[]) { + const includeBodyEventTypes = [LambdaEdgeEventType.ORIGIN_REQUEST, LambdaEdgeEventType.VIEWER_REQUEST]; + if (edgeLambdas && edgeLambdas.some(lambda => lambda.includeBody && !includeBodyEventTypes.includes(lambda.eventType))) { + throw new Error('\'includeBody\' can only be true for ORIGIN_REQUEST or VIEWER_REQUEST event types.'); + } + } + + private grantEdgeLambdaFunctionExecutionRole(edgeLambdas?: EdgeLambda[]) { + if (!edgeLambdas || edgeLambdas.length === 0) { return; } + edgeLambdas.forEach((edgeLambda) => { + const role = edgeLambda.functionVersion.role; + if (role && role instanceof iam.Role && role.assumeRolePolicy) { + role.assumeRolePolicy.addStatements(new iam.PolicyStatement({ + actions: ['sts:AssumeRole'], + principals: [new iam.ServicePrincipal('edgelambda.amazonaws.com')], + })); + } + }); + } } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts index 4a4ebcede0b9b..972726dac93af 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts @@ -424,6 +424,15 @@ export interface LambdaFunctionAssociation { * A version of the lambda to associate */ readonly lambdaFunction: lambda.IVersion; + + /** + * Allows a Lambda function to have read access to the body content. + * Only valid for "request" event types (`ORIGIN_REQUEST` or `VIEWER_REQUEST`). + * See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-include-body-access.html + * + * @default false + */ + readonly includeBody?: boolean; } export interface ViewerCertificateOptions { @@ -628,6 +637,27 @@ interface BehaviorWithOrigin extends Behavior { readonly targetOriginId: string; } +/** + * Attributes used to import a Distribution. + * + * @experimental + */ +export interface CloudFrontWebDistributionAttributes { + /** + * The generated domain name of the Distribution, such as d111111abcdef8.cloudfront.net. + * + * @attribute + */ + readonly domainName: string; + + /** + * The distribution ID for this distribution. + * + * @attribute + */ + readonly distributionId: string; +} + /** * Amazon CloudFront is a global content delivery network (CDN) service that securely delivers data, videos, * applications, and APIs to your viewers with low latency and high transfer speeds. @@ -659,6 +689,25 @@ interface BehaviorWithOrigin extends Behavior { * @resource AWS::CloudFront::Distribution */ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribution { + + /** + * Creates a construct that represents an external (imported) distribution. + */ + public static fromDistributionAttributes(scope: cdk.Construct, id: string, attrs: CloudFrontWebDistributionAttributes): IDistribution { + return new class extends cdk.Resource implements IDistribution { + public readonly domainName: string; + public readonly distributionDomainName: string; + public readonly distributionId: string; + + constructor() { + super(scope, id); + this.domainName = attrs.domainName; + this.distributionDomainName = attrs.domainName; + this.distributionId = attrs.distributionId; + } + }(); + } + /** * The logging bucket for this CloudFront distribution. * If logging is not enabled for this distribution - this property will be undefined. @@ -892,11 +941,17 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu toReturn = Object.assign(toReturn, { pathPattern: input.pathPattern }); } if (input.lambdaFunctionAssociations) { + const includeBodyEventTypes = [LambdaEdgeEventType.ORIGIN_REQUEST, LambdaEdgeEventType.VIEWER_REQUEST]; + if (input.lambdaFunctionAssociations.some(fna => fna.includeBody && !includeBodyEventTypes.includes(fna.eventType))) { + throw new Error('\'includeBody\' can only be true for ORIGIN_REQUEST or VIEWER_REQUEST event types.'); + } + toReturn = Object.assign(toReturn, { lambdaFunctionAssociations: input.lambdaFunctionAssociations .map(fna => ({ eventType: fna.eventType, lambdaFunctionArn: fna.lambdaFunction && fna.lambdaFunction.edgeArn, + includeBody: fna.includeBody, })), }); diff --git a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts index 0a8b5933e7f59..4a0bd425c2cc2 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/distribution.test.ts @@ -468,6 +468,7 @@ describe('with Lambda@Edge functions', () => { { functionVersion: lambdaFunction.currentVersion, eventType: LambdaEdgeEventType.ORIGIN_REQUEST, + includeBody: true, }, ], }, @@ -479,6 +480,7 @@ describe('with Lambda@Edge functions', () => { LambdaFunctionAssociations: [ { EventType: 'origin-request', + IncludeBody: true, LambdaFunctionARN: { Ref: 'FunctionCurrentVersion4E2B2261477a5ae8059bbaa7813f752292c0f65e', }, @@ -489,6 +491,42 @@ describe('with Lambda@Edge functions', () => { }); }); + test('edgelambda.amazonaws.com is added to the trust policy of lambda', () => { + new Distribution(stack, 'MyDist', { + defaultBehavior: { + origin, + edgeLambdas: [ + { + functionVersion: lambdaFunction.currentVersion, + eventType: LambdaEdgeEventType.ORIGIN_REQUEST, + }, + ], + }, + }); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'edgelambda.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + }); + test('can add an edge lambdas to additional behaviors', () => { new Distribution(stack, 'MyDist', { defaultBehavior: { origin }, diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda.expected.json b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda.expected.json new file mode 100644 index 0000000000000..ef6030afded59 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda.expected.json @@ -0,0 +1,103 @@ +{ + "Resources": { + "LambdaServiceRoleA8ED4D3B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "edgelambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "LambdaD247545B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "foo" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaServiceRoleA8ED4D3B", + "Arn" + ] + }, + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "LambdaServiceRoleA8ED4D3B" + ] + }, + "LambdaCurrentVersionDF706F6A97fb843e9bd06fcd2bb15eeace80e13e": { + "Type": "AWS::Lambda::Version", + "Properties": { + "FunctionName": { + "Ref": "LambdaD247545B" + } + } + }, + "DistB3B78991": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "ForwardedValues": { + "QueryString": false + }, + "LambdaFunctionAssociations": [ + { + "EventType": "origin-request", + "LambdaFunctionARN": { + "Ref": "LambdaCurrentVersionDF706F6A97fb843e9bd06fcd2bb15eeace80e13e" + } + } + ], + "TargetOriginId": "integdistributionlambdaDistOrigin133A13098", + "ViewerProtocolPolicy": "allow-all" + }, + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Origins": [ + { + "CustomOriginConfig": { + "OriginProtocolPolicy": "https-only" + }, + "DomainName": "www.example.com", + "Id": "integdistributionlambdaDistOrigin133A13098" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda.ts b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda.ts new file mode 100644 index 0000000000000..52bc6c1a320c5 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda.ts @@ -0,0 +1,25 @@ +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import * as cloudfront from '../lib'; +import { TestOrigin } from './test-origin'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integ-distribution-lambda', { env: { region: 'us-east-1' } }); + +const lambdaFunction = new lambda.Function(stack, 'Lambda', { + code: lambda.Code.fromInline('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, +}); + +new cloudfront.Distribution(stack, 'Dist', { + defaultBehavior: { + origin: new TestOrigin('www.example.com'), + edgeLambdas: [{ + functionVersion: lambdaFunction.currentVersion, + eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, + }], + }, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts index 2570e889d8587..58111d9fc2ef8 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts @@ -1,13 +1,15 @@ import '@aws-cdk/assert/jest'; +import * as lambda from '@aws-cdk/aws-lambda'; import { App, Stack } from '@aws-cdk/core'; -import { AllowedMethods, CachedMethods, ViewerProtocolPolicy } from '../../lib'; +import { AllowedMethods, CachedMethods, LambdaEdgeEventType, ViewerProtocolPolicy } from '../../lib'; import { CacheBehavior } from '../../lib/private/cache-behavior'; let app: App; +let stack: Stack; beforeEach(() => { app = new App(); - new Stack(app, 'Stack', { + stack = new Stack(app, 'Stack', { env: { account: '1234', region: 'testregion' }, }); }); @@ -26,6 +28,7 @@ test('renders the minimum template with an origin and path specified', () => { }); test('renders with all properties specified', () => { + const fnVersion = lambda.Version.fromVersionArn(stack, 'Version', 'arn:aws:lambda:testregion:111111111111:function:myTestFun:v1'); const behavior = new CacheBehavior('origin_id', { pathPattern: '*', allowedMethods: AllowedMethods.ALLOW_ALL, @@ -35,6 +38,11 @@ test('renders with all properties specified', () => { forwardQueryStringCacheKeys: ['user_id', 'auth'], smoothStreaming: true, viewerProtocolPolicy: ViewerProtocolPolicy.HTTPS_ONLY, + edgeLambdas: [{ + eventType: LambdaEdgeEventType.ORIGIN_REQUEST, + includeBody: true, + functionVersion: fnVersion, + }], }); expect(behavior._renderBehavior()).toEqual({ @@ -49,5 +57,23 @@ test('renders with all properties specified', () => { }, smoothStreaming: true, viewerProtocolPolicy: 'https-only', + lambdaFunctionAssociations: [{ + lambdaFunctionArn: 'arn:aws:lambda:testregion:111111111111:function:myTestFun:v1', + eventType: 'origin-request', + includeBody: true, + }], }); }); + +test('throws if edgeLambda includeBody is set for wrong event type', () => { + const fnVersion = lambda.Version.fromVersionArn(stack, 'Version', 'arn:aws:lambda:testregion:111111111111:function:myTestFun:v1'); + + expect(() => new CacheBehavior('origin_id', { + pathPattern: '*', + edgeLambdas: [{ + eventType: LambdaEdgeEventType.ORIGIN_RESPONSE, + includeBody: true, + functionVersion: fnVersion, + }], + })).toThrow(/'includeBody' can only be true for ORIGIN_REQUEST or VIEWER_REQUEST event types./); +}); diff --git a/packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts index c6cd639a7262a..ea2fbf3f22295 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts @@ -444,6 +444,7 @@ nodeunitShim({ lambdaFunctionAssociations: [{ eventType: LambdaEdgeEventType.ORIGIN_REQUEST, lambdaFunction: lambdaFunction.currentVersion, + includeBody: true, }], }, ], @@ -457,6 +458,7 @@ nodeunitShim({ 'LambdaFunctionAssociations': [ { 'EventType': 'origin-request', + 'IncludeBody': true, 'LambdaFunctionARN': { 'Ref': 'LambdaCurrentVersionDF706F6A97fb843e9bd06fcd2bb15eeace80e13e', }, @@ -545,6 +547,38 @@ nodeunitShim({ test.done(); }, + 'throws when associating a lambda with includeBody and a response event type'(test: Test) { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const sourceBucket = new s3.Bucket(stack, 'Bucket'); + + const fnVersion = lambda.Version.fromVersionArn(stack, 'Version', 'arn:aws:lambda:testregion:111111111111:function:myTestFun:v1'); + + test.throws(() => { + new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: sourceBucket, + }, + behaviors: [ + { + isDefaultBehavior: true, + lambdaFunctionAssociations: [{ + eventType: LambdaEdgeEventType.VIEWER_RESPONSE, + includeBody: true, + lambdaFunction: fnVersion, + }], + }, + ], + }, + ], + }); + }, /'includeBody' can only be true for ORIGIN_REQUEST or VIEWER_REQUEST event types./); + + test.done(); + }, + 'distribution has a defaultChild'(test: Test) { const stack = new cdk.Stack(); const sourceBucket = new s3.Bucket(stack, 'Bucket'); @@ -1288,4 +1322,17 @@ nodeunitShim({ }, }, }, + + 'existing distributions can be imported'(test: Test) { + const stack = new cdk.Stack(); + const dist = CloudFrontWebDistribution.fromDistributionAttributes(stack, 'ImportedDist', { + domainName: 'd111111abcdef8.cloudfront.net', + distributionId: '012345ABCDEF', + }); + + test.equals(dist.distributionDomainName, 'd111111abcdef8.cloudfront.net'); + test.equals(dist.distributionId, '012345ABCDEF'); + + test.done(); + }, }); diff --git a/packages/@aws-cdk/aws-lambda/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index f7b048969b185..96eb204852563 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -346,12 +346,24 @@ export abstract class FunctionBase extends Resource implements IFunction { return this.node; } + /** + * Translate IPrincipal to something we can pass to AWS::Lambda::Permissions + * + * Do some nasty things because `Permission` supports a subset of what the + * full IAM principal language supports, and we may not be able to parse strings + * outright because they may be tokens. + * + * Try to recognize some specific Principal classes first, then try a generic + * fallback. + */ private parsePermissionPrincipal(principal?: iam.IPrincipal) { if (!principal) { return undefined; } - // use duck-typing, not instance of + // Try some specific common classes first. + // use duck-typing, not instance of + // @deprecated: after v2, we can change these to 'instanceof' if ('accountId' in principal) { return (principal as iam.AccountPrincipal).accountId; } @@ -364,6 +376,19 @@ export abstract class FunctionBase extends Resource implements IFunction { return (principal as iam.ArnPrincipal).arn; } + // Try a best-effort approach to support simple principals that are not any of the predefined + // classes, but are simple enough that they will fit into the Permission model. Main target + // here: imported Roles, Users, Groups. + // + // The principal cannot have conditions and must have a single { AWS: [arn] } entry. + const json = principal.policyFragment.principalJson; + if (Object.keys(principal.policyFragment.conditions).length === 0 && json.AWS) { + if (typeof json.AWS === 'string') { return json.AWS; } + if (Array.isArray(json.AWS) && json.AWS.length === 1 && typeof json.AWS[0] === 'string') { + return json.AWS[0]; + } + } + throw new Error(`Invalid principal type for Lambda permission statement: ${principal.constructor.name}. ` + 'Supported: AccountPrincipal, ArnPrincipal, ServicePrincipal'); } diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime.ts b/packages/@aws-cdk/aws-lambda/lib/runtime.ts index e32772fcb4007..56ffbecd19b3b 100644 --- a/packages/@aws-cdk/aws-lambda/lib/runtime.ts +++ b/packages/@aws-cdk/aws-lambda/lib/runtime.ts @@ -97,6 +97,11 @@ export class Runtime { */ public static readonly JAVA_8 = new Runtime('java8', RuntimeFamily.JAVA); + /** + * The Java 8 Corretto runtime (java8.al2) + */ + public static readonly JAVA_8_CORRETTO = new Runtime('java8.al2', RuntimeFamily.JAVA); + /** * The Java 11 runtime (java11) */ @@ -152,6 +157,11 @@ export class Runtime { */ public static readonly PROVIDED = new Runtime('provided', RuntimeFamily.OTHER); + /** + * The custom provided runtime (provided) + */ + public static readonly PROVIDED_AL2 = new Runtime('provided.al2', RuntimeFamily.OTHER); + /** * The name of this runtime, as expected by the Lambda resource. */ diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index 2a86ee4bbfbe9..c399a70e5e6ec 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import { ABSENT, expect, haveResource, MatchStyle, ResourcePart } from '@aws-cdk/assert'; +import { ABSENT, expect, haveResource, MatchStyle, ResourcePart, arrayWith, objectLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as logs from '@aws-cdk/aws-logs'; @@ -1168,6 +1168,66 @@ export = { }, }, + 'grantInvoke with an imported role (in the same account)'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, undefined, { + env: { account: '123456789012' }, + }); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + }); + + // WHEN + fn.grantInvoke(iam.Role.fromRoleArn(stack, 'ForeignRole', 'arn:aws:iam::123456789012:role/someRole')); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: objectLike({ + Statement: arrayWith( + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: { 'Fn::GetAtt': ['Function76856677', 'Arn'] }, + }, + ), + }), + Roles: ['someRole'], + })); + + test.done(); + }, + + 'grantInvoke with an imported role (from a different account)'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, undefined, { + env: { account: '3333' }, + }); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + }); + + // WHEN + fn.grantInvoke(iam.Role.fromRoleArn(stack, 'ForeignRole', 'arn:aws:iam::123456789012:role/someRole')); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: 'arn:aws:iam::123456789012:role/someRole', + })); + + test.done(); + }, + 'Can use metricErrors on a lambda Function'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 829e7237f33e6..b6883d7a75ccb 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -324,3 +324,27 @@ const instance = new rds.DatabaseInstance(this, 'Instance', { // ... }); ``` + +### Option Groups + +Some DB engines offer additional features that make it easier to manage data and databases, and to provide additional security for your database. +Amazon RDS uses option groups to enable and configure these features. An option group can specify features, called options, +that are available for a particular Amazon RDS DB instance. + +```ts +const vpc: ec2.IVpc = ...; +const securityGroup: ec2.ISecurityGroup = ...; +new rds.OptionGroup(stack, 'Options', { + engine: DatabaseInstanceEngine.oracleSe({ + version: OracleLegacyEngineVersion.VER_11_2, + }), + configurations: [ + { + name: 'OEM', + port: 5500, + vpc, + securityGroups: [securityGroup], // Optional - a default group will be created if not provided. + }, + ], +}); +``` diff --git a/packages/@aws-cdk/aws-rds/lib/option-group.ts b/packages/@aws-cdk/aws-rds/lib/option-group.ts index 418c71d1c7c6e..f2e8ddf69078d 100644 --- a/packages/@aws-cdk/aws-rds/lib/option-group.ts +++ b/packages/@aws-cdk/aws-rds/lib/option-group.ts @@ -53,6 +53,14 @@ export interface OptionConfiguration { * @default - no VPC */ readonly vpc?: ec2.IVpc; + + /** + * Optional list of security groups to use for this option, if `vpc` is specified. + * If no groups are provided, a default one will be created. + * + * @default - a default group will be created if `port` or `vpc` are specified. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; } /** @@ -135,20 +143,22 @@ export class OptionGroup extends Resource implements IOptionGroup { throw new Error('`port` and `vpc` must be specified together.'); } - const securityGroup = new ec2.SecurityGroup(this, `SecurityGroup${config.name}`, { - description: `Security group for ${config.name} option`, - vpc: config.vpc, - }); + const securityGroups = config.securityGroups && config.securityGroups.length > 0 + ? config.securityGroups + : [new ec2.SecurityGroup(this, `SecurityGroup${config.name}`, { + description: `Security group for ${config.name} option`, + vpc: config.vpc, + })]; this.optionConnections[config.name] = new ec2.Connections({ - securityGroups: [securityGroup], + securityGroups: securityGroups, defaultPort: ec2.Port.tcp(config.port), }); configuration = { ...configuration, port: config.port, - vpcSecurityGroupMemberships: [securityGroup.securityGroupId], + vpcSecurityGroupMemberships: securityGroups.map(sg => sg.securityGroupId), }; } diff --git a/packages/@aws-cdk/aws-rds/test/test.option-group.ts b/packages/@aws-cdk/aws-rds/test/test.option-group.ts index 6152f45e881cd..d4129b6aebd5a 100644 --- a/packages/@aws-cdk/aws-rds/test/test.option-group.ts +++ b/packages/@aws-cdk/aws-rds/test/test.option-group.ts @@ -36,7 +36,7 @@ export = { test.done(); }, - 'option group with security groups'(test: Test) { + 'option group with new security group'(test: Test) { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -96,6 +96,51 @@ export = { test.done(); }, + 'option group with existing security group'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const securityGroup = new ec2.SecurityGroup(stack, 'CustomSecurityGroup', { vpc }); + new OptionGroup(stack, 'Options', { + engine: DatabaseInstanceEngine.oracleSe({ + version: OracleLegacyEngineVersion.VER_11_2, + }), + configurations: [ + { + name: 'OEM', + port: 1158, + vpc, + securityGroups: [securityGroup], + }, + ], + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::OptionGroup', { + EngineName: 'oracle-se', + MajorEngineVersion: '11.2', + OptionGroupDescription: 'Option group for oracle-se 11.2', + OptionConfigurations: [ + { + OptionName: 'OEM', + Port: 1158, + VpcSecurityGroupMemberships: [ + { + 'Fn::GetAtt': [ + 'CustomSecurityGroupE5E500E5', + 'GroupId', + ], + }, + ], + }, + ], + })); + + test.done(); + }, + 'throws when using an option with port and no vpc'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts index f8b4c73d00320..16a74559cf98f 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts @@ -253,7 +253,7 @@ export class SageMakerCreateTrainingJob extends sfn.TaskStateBase implements iam ...(channel.dataSource.s3DataSource.s3DataDistributionType ? { S3DataDistributionType: channel.dataSource.s3DataSource.s3DataDistributionType } : {}), - ...(channel.dataSource.s3DataSource.attributeNames ? { AtttributeNames: channel.dataSource.s3DataSource.attributeNames } : {}), + ...(channel.dataSource.s3DataSource.attributeNames ? { AttributeNames: channel.dataSource.s3DataSource.attributeNames } : {}), }, }, ...(channel.compressionType ? { CompressionType: channel.compressionType } : {}), diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts index e4cd2ef1ce77f..b61ebd4173762 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts @@ -153,6 +153,7 @@ test('create complex training job', () => { recordWrapperType: tasks.RecordWrapperType.RECORD_IO, dataSource: { s3DataSource: { + attributeNames: ['source-ref', 'class'], s3DataType: tasks.S3DataType.S3_PREFIX, s3Location: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(stack, 'InputBucketA', 'mybucket'), 'mytrainpath'), }, @@ -165,6 +166,7 @@ test('create complex training job', () => { recordWrapperType: tasks.RecordWrapperType.RECORD_IO, dataSource: { s3DataSource: { + attributeNames: ['source-ref', 'class'], s3DataType: tasks.S3DataType.S3_PREFIX, s3Location: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(stack, 'InputBucketB', 'mybucket'), 'mytestpath'), }, @@ -230,6 +232,7 @@ test('create complex training job', () => { ContentType: 'image/jpeg', DataSource: { S3DataSource: { + AttributeNames: ['source-ref', 'class'], S3DataType: 'S3Prefix', S3Uri: { 'Fn::Join': ['', ['https://s3.', { Ref: 'AWS::Region' }, '.', { Ref: 'AWS::URLSuffix' }, '/mybucket/mytrainpath']], @@ -244,6 +247,7 @@ test('create complex training job', () => { ContentType: 'image/jpeg', DataSource: { S3DataSource: { + AttributeNames: ['source-ref', 'class'], S3DataType: 'S3Prefix', S3Uri: { 'Fn::Join': ['', ['https://s3.', { Ref: 'AWS::Region' }, '.', { Ref: 'AWS::URLSuffix' }, '/mybucket/mytestpath']], diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts index 38d0a7cf5cb19..9b311a5b84403 100644 --- a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts @@ -13,11 +13,12 @@ export function flatten(object: object): { [key: string]: string } { {}, ...function _flatten(child: any, path: string[] = []): any { return [].concat(...Object.keys(child) - .map(key => - typeof child[key] === 'object' && child[key] !== null - ? _flatten(child[key], path.concat([key])) - : ({ [path.concat([key]).join('.')]: child[key] }), - )); + .map(key => { + const childKey = Buffer.isBuffer(child[key]) ? child[key].toString('utf8') : child[key]; + return typeof childKey === 'object' && childKey !== null + ? _flatten(childKey, path.concat([key])) + : ({ [path.concat([key]).join('.')]: childKey }); + })); }(object), ); } diff --git a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts index 3b1cad57178de..44684ab4282e1 100644 --- a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts +++ b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts @@ -411,6 +411,24 @@ test('flatten correctly flattens a nested object', () => { }); }); +test('flatten correctly flattens an object with buffers', () => { + expect(flatten({ + body: Buffer.from('body'), + nested: { + buffer: Buffer.from('buffer'), + array: [ + Buffer.from('array.0'), + Buffer.from('array.1'), + ], + }, + })).toEqual({ + 'body': 'body', + 'nested.buffer': 'buffer', + 'nested.array.0': 'array.0', + 'nested.array.1': 'array.1', + }); +}); + test('installs the latest SDK', async () => { const tmpPath = '/tmp/node_modules/aws-sdk'; @@ -455,4 +473,7 @@ test('installs the latest SDK', async () => { expect(request.isDone()).toBeTruthy(); expect(() => require.resolve(tmpPath)).not.toThrow(); + + // clean up aws-sdk install + await fs.remove(tmpPath); }); diff --git a/packages/aws-cdk/lib/api/aws-auth/aws-sdk-inifile.ts b/packages/aws-cdk/lib/api/aws-auth/aws-sdk-inifile.ts new file mode 100644 index 0000000000000..af12393fff381 --- /dev/null +++ b/packages/aws-cdk/lib/api/aws-auth/aws-sdk-inifile.ts @@ -0,0 +1,161 @@ +import * as AWS from 'aws-sdk'; + + +/** + * Hack-fix + * + * There are a number of issues in the upstream version of SharedIniFileCredentials + * that need fixing: + * + * 1. The upstream aws-sdk contains an incorrect instantiation of an `AWS.STS` + * client, which *should* have taken the region from the requested profile + * but doesn't. It will use the region from the default profile, which + * may not exist, defaulting to `us-east-1` (since we switched to + * AWS_STS_REGIONAL_ENDPOINTS=regional, that default is not even allowed anymore + * and the absence of a default region will lead to an error). + * + * 2. The simple fix is to get the region from the `config` file. profiles + * are made up of a combination of `credentials` and `config`, and the region is + * generally in `config` with the rest in `credentials`. However, a bug in + * `getProfilesFromSharedConfig` overwrites ALL `config` data with `credentials` + * data, so we also need to do extra work to fish the `region` out of the config. + * + * See https://github.com/aws/aws-sdk-js/issues/3418 for all the gory details. + */ +export class PatchedSharedIniFileCredentials extends AWS.SharedIniFileCredentials { + declare private profile: string; + declare private filename: string; + declare private disableAssumeRole: boolean; + declare private options: Record; + declare private roleArn: string; + declare private httpOptions?: AWS.HTTPOptions; + declare private tokenCodeFn?: (mfaSerial: string, callback: (err?: Error, token?: string) => void) => void; + + public loadRoleProfile( + creds: Record>, + roleProfile: Record, + callback: (err?: Error, data?: any) => void) { + + // Need to duplicate the whole implementation here -- the function is long and has been written in + // such a way that there are no small monkey patches possible. + + if (this.disableAssumeRole) { + throw (AWS as any).util.error( + new Error('Role assumption profiles are disabled. ' + + 'Failed to load profile ' + this.profile + + ' from ' + creds.filename), + { code: 'SharedIniFileCredentialsProviderFailure' }, + ); + } + + var self = this; + var roleArn = roleProfile.role_arn; + var roleSessionName = roleProfile.role_session_name; + var externalId = roleProfile.external_id; + var mfaSerial = roleProfile.mfa_serial; + var sourceProfileName = roleProfile.source_profile; + + if (!sourceProfileName) { + throw (AWS as any).util.error( + new Error('source_profile is not set using profile ' + this.profile), + { code: 'SharedIniFileCredentialsProviderFailure' }, + ); + } + + var sourceProfileExistanceTest = creds[sourceProfileName]; + + if (typeof sourceProfileExistanceTest !== 'object') { + throw (AWS as any).util.error( + new Error('source_profile ' + sourceProfileName + ' using profile ' + + this.profile + ' does not exist'), + { code: 'SharedIniFileCredentialsProviderFailure' }, + ); + } + + var sourceCredentials = new AWS.SharedIniFileCredentials( + (AWS as any).util.merge(this.options || {}, { + profile: sourceProfileName, + preferStaticCredentials: true, + }), + ); + + // --------- THIS IS NEW ---------------------- + const profiles = loadProfilesProper(this.filename); + const region = profiles[this.profile]?.region ?? profiles.default?.region ?? 'us-east-1'; + // --------- /THIS IS NEW ---------------------- + + this.roleArn = roleArn; + var sts = new AWS.STS({ + credentials: sourceCredentials, + region, + httpOptions: this.httpOptions, + }); + + var roleParams: AWS.STS.AssumeRoleRequest = { + RoleArn: roleArn, + RoleSessionName: roleSessionName || 'aws-sdk-js-' + Date.now(), + }; + + if (externalId) { + roleParams.ExternalId = externalId; + } + + if (mfaSerial && self.tokenCodeFn) { + roleParams.SerialNumber = mfaSerial; + self.tokenCodeFn(mfaSerial, function(err, token) { + if (err) { + var message; + if (err instanceof Error) { + message = err.message; + } else { + message = err; + } + callback( + (AWS as any).util.error( + new Error('Error fetching MFA token: ' + message), + { code: 'SharedIniFileCredentialsProviderFailure' }, + )); + return; + } + + roleParams.TokenCode = token; + sts.assumeRole(roleParams, callback); + }); + return; + } + sts.assumeRole(roleParams, callback); + + } +} + +/** + * A function to load profiles from disk that MERGES credentials and config instead of overwriting + * + * @see https://github.com/aws/aws-sdk-js/blob/5ae5a7d7d24d1000dbc089cc15f8ed2c7b06c542/lib/util.js#L956 + */ +function loadProfilesProper(filename: string) { + const util = (AWS as any).util; // Does exists even though there aren't any typings for it + const iniLoader = util.iniLoader; + const profiles: Record> = {}; + let profilesFromConfig: Record> = {}; + if (process.env[util.configOptInEnv]) { + profilesFromConfig = iniLoader.loadFrom({ + isConfig: true, + filename: process.env[util.sharedConfigFileEnv], + }); + } + var profilesFromCreds: Record> = iniLoader.loadFrom({ + filename: filename || + (process.env[util.configOptInEnv] && process.env[util.sharedCredentialsFileEnv]), + }); + for (const [name, profile] of Object.entries(profilesFromConfig)) { + profiles[name] = profile; + } + for (const [name, profile] of Object.entries(profilesFromCreds)) { + profiles[name] = { + ...profiles[name], + ...profile, + }; + } + return profiles; +} diff --git a/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts b/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts index 6d85e08310128..515c0a548643a 100644 --- a/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts +++ b/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts @@ -6,6 +6,7 @@ import * as AWS from 'aws-sdk'; import * as fs from 'fs-extra'; import * as promptly from 'promptly'; import { debug } from '../../logging'; +import { PatchedSharedIniFileCredentials } from './aws-sdk-inifile'; import { SharedIniFile } from './sdk_ini_file'; /** @@ -44,7 +45,7 @@ export class AwsCliCompatible { // Force reading the `config` file if it exists by setting the appropriate // environment variable. await forceSdkToReadConfigIfPresent(); - sources.push(() => new AWS.SharedIniFileCredentials({ + sources.push(() => new PatchedSharedIniFileCredentials({ profile, filename: credentialsFileName(), httpOptions: options.httpOptions, @@ -310,3 +311,4 @@ async function tokenCodeFn(serialArn: string, cb: (err?: Error, token?: string) cb(err); } } + diff --git a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts index 9182e291fee17..53a38108b7d87 100644 --- a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts +++ b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts @@ -6,7 +6,7 @@ import * as fs from 'fs-extra'; import { Mode, SdkProvider } from '../aws-auth'; import { deployStack, DeployStackResult } from '../deploy-stack'; import { DEFAULT_TOOLKIT_STACK_NAME, ToolkitInfo } from '../toolkit-info'; -import { BOOTSTRAP_VERSION_OUTPUT, BootstrapEnvironmentOptions } from './bootstrap-props'; +import { BOOTSTRAP_VERSION_OUTPUT, BootstrapEnvironmentOptions, BOOTSTRAP_VERSION_RESOURCE } from './bootstrap-props'; /** * Perform the actual deployment of a bootstrap stack, given a template and some parameters @@ -61,7 +61,7 @@ export async function deployBootstrapStack( function bootstrapVersionFromTemplate(template: any): number { const versionSources = [ template.Outputs?.[BOOTSTRAP_VERSION_OUTPUT]?.Value, - template.Resources?.[BOOTSTRAP_VERSION_OUTPUT]?.Properties?.Value, + template.Resources?.[BOOTSTRAP_VERSION_RESOURCE]?.Properties?.Value, ]; for (const vs of versionSources) { diff --git a/packages/aws-cdk/test/api/sdk-provider.test.ts b/packages/aws-cdk/test/api/sdk-provider.test.ts index 9d7bd5d701e39..e72225bbf82ee 100644 --- a/packages/aws-cdk/test/api/sdk-provider.test.ts +++ b/packages/aws-cdk/test/api/sdk-provider.test.ts @@ -32,53 +32,6 @@ beforeEach(() => { logging.setLogLevel(logging.LogLevel.TRACE); - bockfs({ - '/home/me/.bxt/credentials': dedent(` - [default] - aws_access_key_id=${uid}access - aws_secret_access_key=secret - - [foo] - aws_access_key_id=${uid}fooccess - aws_secret_access_key=secret - - [assumer] - aws_access_key_id=${uid}assumer - aws_secret_access_key=secret - - [mfa] - aws_access_key_id=${uid}mfaccess - aws_secret_access_key=secret - `), - '/home/me/.bxt/config': dedent(` - [default] - region=eu-bla-5 - - [profile foo] - region=eu-west-1 - - [profile boo] - aws_access_key_id=${uid}booccess - aws_secret_access_key=boocret - # No region here - - [profile assumable] - role_arn=arn:aws:iam::12356789012:role/Assumable - source_profile=assumer - - [profile assumer] - region=us-east-2 - - [profile mfa] - region=eu-west-1 - - [profile mfa-role] - source_profile=mfa - role_arn=arn:aws:iam::account:role/role - mfa_serial=arn:aws:iam::account:mfa/user - `), - }); - SDKMock.mock('STS', 'getCallerIdentity', (cb: AwsCallback) => { return cb(null, { Account: `${uid}the_account_#`, @@ -104,10 +57,8 @@ beforeEach(() => { defaultEnv = cxapi.EnvironmentUtils.make(`${uid}the_account_#`, 'def'); - // Set environment variables that we want - process.env.AWS_CONFIG_FILE = bockfs.path('/home/me/.bxt/config'); - process.env.AWS_SHARED_CREDENTIALS_FILE = bockfs.path('/home/me/.bxt/credentials'); // Scrub some environment variables that might be set if we're running on CodeBuild which will interfere with the tests. + delete process.env.AWS_PROFILE; delete process.env.AWS_REGION; delete process.env.AWS_DEFAULT_REGION; delete process.env.AWS_ACCESS_KEY_ID; @@ -122,146 +73,249 @@ afterEach(() => { bockfs.restore(); }); -describe('CLI compatible credentials loading', () => { - test('default config credentials', async () => { - // WHEN - const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions }); - - // THEN - expect(provider.defaultRegion).toEqual('eu-bla-5'); - await expect(provider.defaultAccount()).resolves.toEqual({ accountId: `${uid}the_account_#`, partition: 'aws-here' }); - const sdk = await provider.forEnvironment({ ...defaultEnv, region: 'rgn' }, Mode.ForReading); - expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}access`); - expect(sdkConfig(sdk).region).toEqual('rgn'); - }); - - test('unknown account and region uses current', async () => { - // WHEN - const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions }); +describe('with default config files', () => { + beforeEach(() => { + bockfs({ + '/home/me/.bxt/credentials': dedent(` + [default] + aws_access_key_id=${uid}access + aws_secret_access_key=secret + + [foo] + aws_access_key_id=${uid}fooccess + aws_secret_access_key=secret + + [assumer] + aws_access_key_id=${uid}assumer + aws_secret_access_key=secret + + [mfa] + aws_access_key_id=${uid}mfaccess + aws_secret_access_key=secret + `), + '/home/me/.bxt/config': dedent(` + [default] + region=eu-bla-5 + + [profile foo] + region=eu-west-1 + + [profile boo] + aws_access_key_id=${uid}booccess + aws_secret_access_key=boocret + # No region here + + [profile assumable] + role_arn=arn:aws:iam::12356789012:role/Assumable + source_profile=assumer + + [profile assumer] + region=us-east-2 + + [profile mfa] + region=eu-west-1 + + [profile mfa-role] + source_profile=mfa + role_arn=arn:aws:iam::account:role/role + mfa_serial=arn:aws:iam::account:mfa/user + `), + }); - // THEN - const sdk = await provider.forEnvironment(cxapi.EnvironmentUtils.make(cxapi.UNKNOWN_ACCOUNT, cxapi.UNKNOWN_REGION), Mode.ForReading); - expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}access`); - expect(sdkConfig(sdk).region).toEqual('eu-bla-5'); + // Set environment variables that we want + process.env.AWS_CONFIG_FILE = bockfs.path('/home/me/.bxt/config'); + process.env.AWS_SHARED_CREDENTIALS_FILE = bockfs.path('/home/me/.bxt/credentials'); }); - test('mixed profile credentials', async () => { - // WHEN - const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'foo' }); + describe('CLI compatible credentials loading', () => { + test('default config credentials', async () => { + // WHEN + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions }); + + // THEN + expect(provider.defaultRegion).toEqual('eu-bla-5'); + await expect(provider.defaultAccount()).resolves.toEqual({ accountId: `${uid}the_account_#`, partition: 'aws-here' }); + const sdk = await provider.forEnvironment({ ...defaultEnv, region: 'rgn' }, Mode.ForReading); + expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}access`); + expect(sdkConfig(sdk).region).toEqual('rgn'); + }); - // THEN - expect(provider.defaultRegion).toEqual('eu-west-1'); - await expect(provider.defaultAccount()).resolves.toEqual({ accountId: `${uid}the_account_#`, partition: 'aws-here' }); - const sdk = await provider.forEnvironment(defaultEnv, Mode.ForReading); - expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}fooccess`); - }); + test('unknown account and region uses current', async () => { + // WHEN + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions }); - test('pure config credentials', async () => { - // WHEN - const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'boo' }); + // THEN + const sdk = await provider.forEnvironment(cxapi.EnvironmentUtils.make(cxapi.UNKNOWN_ACCOUNT, cxapi.UNKNOWN_REGION), Mode.ForReading); + expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}access`); + expect(sdkConfig(sdk).region).toEqual('eu-bla-5'); + }); - // THEN - expect(provider.defaultRegion).toEqual('eu-bla-5'); // Fall back to default config - await expect(provider.defaultAccount()).resolves.toEqual({ accountId: `${uid}the_account_#`, partition: 'aws-here' }); - const sdk = await provider.forEnvironment(defaultEnv, Mode.ForReading); - expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}booccess`); - }); + test('mixed profile credentials', async () => { + // WHEN + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'foo' }); - test('mfa_serial in profile will ask user for token', async () => { - // WHEN - const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'mfa-role' }); - - // THEN - try { - await provider.withAssumedRole('arn:aws:iam::account:role/role', undefined, undefined); - } catch (e) { - // Mock response was set to fail with message test to make sure we don't call STS - expect(e.message).toEqual('Error fetching MFA token: test'); - } - }); + // THEN + expect(provider.defaultRegion).toEqual('eu-west-1'); + await expect(provider.defaultAccount()).resolves.toEqual({ accountId: `${uid}the_account_#`, partition: 'aws-here' }); + const sdk = await provider.forEnvironment(defaultEnv, Mode.ForReading); + expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}fooccess`); + }); - test('different account throws', async () => { - const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'boo' }); + test('pure config credentials', async () => { + // WHEN + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'boo' }); - await expect(provider.forEnvironment({ ...defaultEnv, account: `${uid}some_account_#` }, Mode.ForReading)).rejects.toThrow('Need to perform AWS calls'); - }); + // THEN + expect(provider.defaultRegion).toEqual('eu-bla-5'); // Fall back to default config + await expect(provider.defaultAccount()).resolves.toEqual({ accountId: `${uid}the_account_#`, partition: 'aws-here' }); + const sdk = await provider.forEnvironment(defaultEnv, Mode.ForReading); + expect(sdkConfig(sdk).credentials!.accessKeyId).toEqual(`${uid}booccess`); + }); - test('even when using a profile to assume another profile, STS calls goes through the proxy', async () => { - // Messy mocking - let called = false; - jest.mock('proxy-agent', () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - class FakeAgent extends require('https').Agent { - public addRequest(_: any, __: any) { - // FIXME: this error takes 6 seconds to be completely handled. It - // might be retries in the SDK somewhere, or something about the Node - // event loop. I've spent an hour trying to figure it out and I can't, - // and I gave up. We'll just have to live with this until someone gets - // inspired. - const error = new Error('ABORTED BY TEST'); - (error as any).code = 'RequestAbortedError'; - (error as any).retryable = false; - called = true; - throw error; - } + test('mfa_serial in profile will ask user for token', async () => { + // WHEN + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'mfa-role' }); + + // THEN + try { + await provider.withAssumedRole('arn:aws:iam::account:role/role', undefined, undefined); + } catch (e) { + // Mock response was set to fail with message test to make sure we don't call STS + expect(e.message).toEqual('Error fetching MFA token: test'); } - return FakeAgent; }); - // WHEN - const provider = await SdkProvider.withAwsCliCompatibleDefaults({ - ...defaultCredOptions, - profile: 'assumable', - httpOptions: { - proxyAddress: 'http://DOESNTMATTER/', - }, + test('different account throws', async () => { + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'boo' }); + + await expect(provider.forEnvironment({ ...defaultEnv, account: `${uid}some_account_#` }, Mode.ForReading)).rejects.toThrow('Need to perform AWS calls'); }); - await provider.defaultAccount(); + test('even when using a profile to assume another profile, STS calls goes through the proxy', async () => { + // Messy mocking + let called = false; + jest.mock('proxy-agent', () => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + class FakeAgent extends require('https').Agent { + public addRequest(_: any, __: any) { + // FIXME: this error takes 6 seconds to be completely handled. It + // might be retries in the SDK somewhere, or something about the Node + // event loop. I've spent an hour trying to figure it out and I can't, + // and I gave up. We'll just have to live with this until someone gets + // inspired. + const error = new Error('ABORTED BY TEST'); + (error as any).code = 'RequestAbortedError'; + (error as any).retryable = false; + called = true; + throw error; + } + } + return FakeAgent; + }); - // THEN -- the fake proxy agent got called, we don't care about the result - expect(called).toEqual(true); - }); + // WHEN + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ + ...defaultCredOptions, + profile: 'assumable', + httpOptions: { + proxyAddress: 'http://DOESNTMATTER/', + }, + }); + + await provider.defaultAccount(); + + // THEN -- the fake proxy agent got called, we don't care about the result + expect(called).toEqual(true); + }); + + test('error we get from assuming a role is useful', async () => { + // GIVEN + // Because of the way ChainableTemporaryCredentials gets its STS client, it's not mockable + // using 'mock-aws-sdk'. So instead, we have to mess around with its internals. + function makeAssumeRoleFail(s: ISDK) { + (s as any).credentials.service.assumeRole = jest.fn().mockImplementation((_request, cb) => { + cb(new Error('Nope!')); + }); + } - test('error we get from assuming a role is useful', async () => { - // GIVEN - // Because of the way ChainableTemporaryCredentials gets its STS client, it's not mockable - // using 'mock-aws-sdk'. So instead, we have to mess around with its internals. - function makeAssumeRoleFail(s: ISDK) { - (s as any).credentials.service.assumeRole = jest.fn().mockImplementation((_request, cb) => { - cb(new Error('Nope!')); + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ + ...defaultCredOptions, + httpOptions: { + proxyAddress: 'http://localhost:8080/', + }, }); - } - const provider = await SdkProvider.withAwsCliCompatibleDefaults({ - ...defaultCredOptions, - httpOptions: { - proxyAddress: 'http://localhost:8080/', - }, + // WHEN + const sdk = await provider.withAssumedRole('bla.role.arn', undefined, undefined); + makeAssumeRoleFail(sdk); + + // THEN - error message contains both a helpful hint and the underlying AssumeRole message + await expect(sdk.s3().listBuckets().promise()).rejects.toThrow('did you bootstrap'); + await expect(sdk.s3().listBuckets().promise()).rejects.toThrow('Nope!'); }); + }); - // WHEN - const sdk = await provider.withAssumedRole('bla.role.arn', undefined, undefined); - makeAssumeRoleFail(sdk); + describe('Plugins', () => { + test('does not use plugins if current credentials are for expected account', async () => { + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions }); + await provider.forEnvironment(defaultEnv, Mode.ForReading); + expect(pluginQueried).toEqual(false); + }); - // THEN - error message contains both a helpful hint and the underlying AssumeRole message - await expect(sdk.s3().listBuckets().promise()).rejects.toThrow('did you bootstrap'); - await expect(sdk.s3().listBuckets().promise()).rejects.toThrow('Nope!'); + test('uses plugin for other account', async () => { + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions }); + await provider.forEnvironment({ ...defaultEnv, account: `${uid}plugin_account_#` }, Mode.ForReading); + expect(pluginQueried).toEqual(true); + }); }); }); -describe('Plugins', () => { - test('does not use plugins if current credentials are for expected account', async () => { - const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions }); - await provider.forEnvironment(defaultEnv, Mode.ForReading); - expect(pluginQueried).toEqual(false); +test('can assume role without a [default] profile', async () => { + // GIVEN + bockfs({ + '/home/me/.bxt/credentials': dedent(` + [assumer] + aws_access_key_id=${uid}assumer + aws_secret_access_key=secret + + [assumable] + role_arn=arn:aws:iam::12356789012:role/Assumable + source_profile=assumer + `), + '/home/me/.bxt/config': dedent(` + [profile assumable] + region=eu-bla-5 + `), + }); + + SDKMock.mock('STS', 'assumeRole', (_request: AWS.STS.AssumeRoleRequest, cb: AwsCallback) => { + return cb(null, { + Credentials: { + AccessKeyId: `${uid}access`, // Needs UID in here otherwise key will be cached + Expiration: new Date(Date.now() + 10000), + SecretAccessKey: 'b', + SessionToken: 'c', + }, + }); }); - test('uses plugin for other account', async () => { - const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions }); - await provider.forEnvironment({ ...defaultEnv, account: `${uid}plugin_account_#` }, Mode.ForReading); - expect(pluginQueried).toEqual(true); + // Set environment variables that we want + process.env.AWS_CONFIG_FILE = bockfs.path('/home/me/.bxt/config'); + process.env.AWS_SHARED_CREDENTIALS_FILE = bockfs.path('/home/me/.bxt/credentials'); + + // WHEN + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ + ...defaultCredOptions, + profile: 'assumable', + httpOptions: { + proxyAddress: 'http://DOESNTMATTER/', + }, }); + + const account = await provider.defaultAccount(); + + // THEN + expect(account?.accountId).toEqual(`${uid}the_account_#`); }); /** diff --git a/packages/monocdk-experiment/package.json b/packages/monocdk-experiment/package.json index c77e56abe1f37..e4eb7cf36e3cc 100644 --- a/packages/monocdk-experiment/package.json +++ b/packages/monocdk-experiment/package.json @@ -89,7 +89,6 @@ ], "dependencies": { "case": "1.6.3", - "constructs": "^3.0.4", "fs-extra": "^9.0.1", "jsonschema": "^1.2.5", "minimatch": "^3.0.4", @@ -97,6 +96,7 @@ "yaml": "1.10.0" }, "devDependencies": { + "constructs": "^3.0.4", "@aws-cdk/alexa-ask": "0.0.0", "@aws-cdk/app-delivery": "0.0.0", "@aws-cdk/assets": "0.0.0",