Skip to content

Commit

Permalink
use new L2 constructs for FunctionUrlOrigin and S3BucketOrigin
Browse files Browse the repository at this point in the history
  • Loading branch information
its-felix committed Oct 22, 2024
1 parent 1870420 commit e990405
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 97 deletions.
28 changes: 14 additions & 14 deletions cdk/lib/constructs/api-lambda-construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ApiLambdaConstructProps {
}

export class ApiLambdaConstruct extends Construct {
readonly lambda: Function;
readonly functionURL: FunctionUrl;

constructor(scope: Construct, id: string, props: ApiLambdaConstructProps) {
Expand All @@ -39,7 +40,7 @@ export class ApiLambdaConstruct extends Construct {
this.ssmSecureString('/api/session/id_rsa.pub'),
];

const lambda = new Function(this, 'ApiLambda', {
this.lambda = new Function(this, 'ApiLambda', {
runtime: Runtime.PROVIDED_AL2023,
architecture: Architecture.ARM_64,
memorySize: 1024,
Expand Down Expand Up @@ -77,27 +78,26 @@ export class ApiLambdaConstruct extends Construct {
}),
});

lambda.addToRolePolicy(new PolicyStatement({
this.lambda.addToRolePolicy(new PolicyStatement({
effect: Effect.ALLOW,
actions: ['ssm:GetParameters'],
resources: [ssmGoogleClientId, ssmGoogleClientSecret, ssmSessionRsaPriv, ssmSessionRsaPub].map((v) => v.parameterArn),
}));

props.dataBucket.grantRead(lambda, 'processed/flights/*');
props.dataBucket.grantRead(lambda, 'processed/schedules/*');
props.dataBucket.grantRead(lambda, 'raw/ourairports_data/airports.csv');
props.dataBucket.grantRead(lambda, 'raw/ourairports_data/countries.csv');
props.dataBucket.grantRead(lambda, 'raw/ourairports_data/regions.csv');
props.dataBucket.grantRead(lambda, 'raw/LH_Public_Data/aircraft.json');
props.dataBucket.grantRead(this.lambda, 'processed/flights/*');
props.dataBucket.grantRead(this.lambda, 'processed/schedules/*');
props.dataBucket.grantRead(this.lambda, 'raw/ourairports_data/airports.csv');
props.dataBucket.grantRead(this.lambda, 'raw/ourairports_data/countries.csv');
props.dataBucket.grantRead(this.lambda, 'raw/ourairports_data/regions.csv');
props.dataBucket.grantRead(this.lambda, 'raw/LH_Public_Data/aircraft.json');

props.authBucket.grantReadWrite(lambda, 'authreq/*');
props.authBucket.grantReadWrite(lambda, 'federation/*');
props.authBucket.grantReadWrite(lambda, 'account/*');
props.authBucket.grantReadWrite(this.lambda, 'authreq/*');
props.authBucket.grantReadWrite(this.lambda, 'federation/*');
props.authBucket.grantReadWrite(this.lambda, 'account/*');

this.functionURL = new FunctionUrl(this, 'ApiLambdaFunctionUrl', {
function: lambda,
// https://github.com/pwrdrvr/lambda-url-signing check later
authType: FunctionUrlAuthType.NONE,
function: this.lambda,
authType: FunctionUrlAuthType.AWS_IAM,
invokeMode: InvokeMode.RESPONSE_STREAM,
});
}
Expand Down
77 changes: 38 additions & 39 deletions cdk/lib/constructs/cloudfront-construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,35 @@ import { Construct } from 'constructs';
import {
AllowedMethods,
CachePolicy, CfnOriginAccessControl,
Distribution, Function, FunctionCode, FunctionEventType, FunctionRuntime, HeadersFrameOption,
Distribution,
Function,
FunctionCode,
FunctionEventType,
FunctionRuntime,
HeadersFrameOption,
HttpVersion,
IDistribution,
OriginProtocolPolicy,
OriginRequestCookieBehavior,
OriginRequestHeaderBehavior,
OriginAccessControlOriginType,
OriginRequestPolicy,
OriginRequestQueryStringBehavior,
PriceClass,
ResponseHeadersPolicy,
S3OriginAccessControl,
SecurityPolicyProtocol,
Signing,
ViewerProtocolPolicy
} from 'aws-cdk-lib/aws-cloudfront';
import { HttpOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
import { FunctionUrlOrigin, S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager';
import { Duration, Fn, Stack } from 'aws-cdk-lib';
import { IFunctionUrl } from 'aws-cdk-lib/aws-lambda';
import { Bucket, IBucket } from 'aws-cdk-lib/aws-s3';
import { S3OriginWithOAC } from './s3-origin-with-oac';
import { Duration, Stack } from 'aws-cdk-lib';
import { IFunction, IFunctionUrl } from 'aws-cdk-lib/aws-lambda';
import { IBucket } from 'aws-cdk-lib/aws-s3';
import { CloudfrontUtil } from '../util/util';

export interface CloudfrontConstructProps {
domain: string;
certificateId: string;
uiResourcesBucket: IBucket;
apiLambda: IFunction;
apiLambdaFunctionURL: IFunctionUrl;
}

Expand Down Expand Up @@ -81,34 +86,23 @@ export class CloudfrontConstruct extends Construct {
});
// endregion

// region OriginRequestPolicy - which headers, cookies and query params should be forwarded to the origin
const allExceptHostOriginRequestPolicy = new OriginRequestPolicy(this, 'AllExceptHostORP', {
headerBehavior: OriginRequestHeaderBehavior.denyList('Host'),
cookieBehavior: OriginRequestCookieBehavior.all(),
queryStringBehavior: OriginRequestQueryStringBehavior.all(),
});
// endregion

// region origins
const apiLambdaOrigin = new HttpOrigin(Fn.select(2, Fn.split('/', props.apiLambdaFunctionURL.url)), {
protocolPolicy: OriginProtocolPolicy.HTTPS_ONLY,
const apiLambdaOrigin = new FunctionUrlOrigin(props.apiLambdaFunctionURL, {
customHeaders: { Forwarded: `host=${props.domain};proto=https` },
originShieldEnabled: true,
originShieldRegion: Stack.of(this).region,
originAccessControlId: new CfnOriginAccessControl(this, 'APILambdaOACv2', {
originAccessControlConfig: {
name: `${this.node.path.replace('/', '-')}-APILambdaOACv2`,
description: 'OAC to access API Lambda Function URL',
originAccessControlOriginType: OriginAccessControlOriginType.LAMBDA,
signingBehavior: 'always',
signingProtocol: 'sigv4',
},
}).attrId,
});
// endregion

const uiResourcesOAC = new CfnOriginAccessControl(this, 'UIResourcesOAC', {
originAccessControlConfig: {
// these names must be unique
name: `${this.node.path.replace('/', '-')}-UIResourcesOAC`,
description: 'OAC to access UI resources bucket',
originAccessControlOriginType: 's3',
signingBehavior: 'always',
signingProtocol: 'sigv4',
},
});

this.distribution = new Distribution(this, 'Distribution', {
priceClass: PriceClass.PRICE_CLASS_ALL,
httpVersion: HttpVersion.HTTP2_AND_3,
Expand All @@ -126,11 +120,13 @@ export class CloudfrontConstruct extends Construct {
),
minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021,
defaultBehavior: {
origin: new S3OriginWithOAC(
// prevent CF from adding its OriginAccessIdentity to the BucketPolicy since we're using OriginAccessControl (see below)
Bucket.fromBucketName(this, 'UIResourcesBucketCopy', props.uiResourcesBucket.bucketName),
{ oacId: uiResourcesOAC.getAtt('Id') },
),
origin: S3BucketOrigin.withOriginAccessControl(props.uiResourcesBucket, {
originAccessControl: new S3OriginAccessControl(this, 'UIResourcesOACv2', {
description: 'OAC to access UI resources bucket',
signing: Signing.SIGV4_ALWAYS,
}),
originAccessLevels: [], // manually configured with ListBucket via CloudfrontUtil
}),
compress: true,
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: AllowedMethods.ALLOW_GET_HEAD,
Expand Down Expand Up @@ -161,7 +157,7 @@ export class CloudfrontConstruct extends Construct {
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: AllowedMethods.ALLOW_ALL,
cachePolicy: CachePolicy.CACHING_DISABLED,
originRequestPolicy: allExceptHostOriginRequestPolicy,
originRequestPolicy: OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
responseHeadersPolicy: noCacheResponseHeadersPolicy,
},
'/auth/*': {
Expand All @@ -170,7 +166,7 @@ export class CloudfrontConstruct extends Construct {
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: AllowedMethods.ALLOW_ALL,
cachePolicy: CachePolicy.CACHING_DISABLED,
originRequestPolicy: allExceptHostOriginRequestPolicy,
originRequestPolicy: OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
responseHeadersPolicy: noCacheResponseHeadersPolicy,
},
'/data/*': {
Expand All @@ -179,13 +175,16 @@ export class CloudfrontConstruct extends Construct {
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
cachePolicy: CachePolicy.CACHING_OPTIMIZED,
originRequestPolicy: allExceptHostOriginRequestPolicy,
originRequestPolicy: OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
responseHeadersPolicy: cacheOverridableResponseHeadersPolicy,
},
},
enableLogging: false,
enabled: true,
});

CloudfrontUtil.addCloudfrontOACToBucketResourcePolicy(props.uiResourcesBucket, this.distribution, '', true);
CloudfrontUtil.addCloudfrontOACToLambdaResourcePolicy(id, props.apiLambda, this.distribution);
}
}

Expand Down
37 changes: 0 additions & 37 deletions cdk/lib/constructs/s3-origin-with-oac.ts

This file was deleted.

5 changes: 0 additions & 5 deletions cdk/lib/constructs/ui-resources-construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { IDistribution } from 'aws-cdk-lib/aws-cloudfront';
import { BlockPublicAccess, Bucket, BucketEncryption, IBucket } from 'aws-cdk-lib/aws-s3';
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
import { Construct } from 'constructs';
import { CloudfrontUtil } from '../util/util';

export type UIResourcesConstructProps = Record<string, unknown>;

Expand All @@ -22,10 +21,6 @@ export class UIResourcesConstruct extends Construct {
});
}

public grantRead(distribution: IDistribution, prefix: string = ''): void {
CloudfrontUtil.addCloudfrontOACToResourcePolicy(this.bucket, distribution, prefix, true);
}

public deployResourcesZip(resourcesZipPath: string, distribution: IDistribution): void {
new BucketDeployment(this, 'Deployment', {
destinationBucket: this.bucket,
Expand Down
2 changes: 1 addition & 1 deletion cdk/lib/stacks/website-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ export class WebsiteStack extends cdk.Stack {
domain: props.domain,
certificateId: props.certificateId,
uiResourcesBucket: uiResources.bucket,
apiLambda: api.lambda,
apiLambdaFunctionURL: api.functionURL,
});

uiResources.grantRead(cf.distribution);
uiResources.deployResourcesZip(props.uiResourcesZipPath, cf.distribution);

this.distribution = cf.distribution;
Expand Down
12 changes: 11 additions & 1 deletion cdk/lib/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Effect, PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { IBucket } from 'aws-cdk-lib/aws-s3';
import { IDistribution } from 'aws-cdk-lib/aws-cloudfront';
import { ArnFormat, Stack } from 'aws-cdk-lib';
import { FunctionUrlAuthType, IFunction } from 'aws-cdk-lib/aws-lambda';

export class CloudfrontUtil {
public static addCloudfrontOACToResourcePolicy(bucket: IBucket, distribution: IDistribution, prefix: string, allowList: boolean) {
public static addCloudfrontOACToBucketResourcePolicy(bucket: IBucket, distribution: IDistribution, prefix: string, allowList: boolean) {
const distributionArn = CloudfrontUtil.distributionArn(distribution);

bucket.addToResourcePolicy(new PolicyStatement({
Expand Down Expand Up @@ -36,6 +37,15 @@ export class CloudfrontUtil {
}
}

public static addCloudfrontOACToLambdaResourcePolicy(id: string, lambda: IFunction, distribution: IDistribution) {
lambda.addPermission(`AllowCF${id}InvokeFunctionUrl`, {
action: 'lambda:InvokeFunctionUrl',
principal: new ServicePrincipal('cloudfront.amazonaws.com'),
sourceArn: CloudfrontUtil.distributionArn(distribution),
functionUrlAuthType: FunctionUrlAuthType.AWS_IAM,
});
}

public static distributionArn(distribution: IDistribution): string {
return Stack.of(distribution).formatArn({
service: 'cloudfront',
Expand Down

0 comments on commit e990405

Please sign in to comment.