Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cloudfront): function URL origin access control L2 construct #31339

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
bc5f929
oac
watany-dev Sep 6, 2024
80f07f7
wip: oac fin
watany-dev Sep 6, 2024
a6386c1
oai
watany-dev Sep 6, 2024
da605ea
fix oai
watany-dev Sep 6, 2024
b2baafa
remove oai
watany-dev Sep 7, 2024
5a7352a
snapshoted
watany-dev Sep 7, 2024
8e8e2ac
Merge branch 'main' into lambda-url-oac
watany-dev Sep 7, 2024
7fcbea3
fix doctest
watany-dev Sep 7, 2024
12f13f9
Merge branches 'lambda-url-oac' and 'lambda-url-oac' of https://githu…
watany-dev Sep 7, 2024
b257227
snapshotted
watany-dev Sep 7, 2024
8f24a4a
fix doctest
watany-dev Sep 7, 2024
81c0785
fixed
watany-dev Sep 7, 2024
47775c8
refactor
watany-dev Sep 8, 2024
df0c972
Merge branch 'main' into lambda-url-oac
watany-dev Sep 8, 2024
a43bf40
hide private class
watany-dev Sep 10, 2024
3731370
nest the new tests under a new describe
watany-dev Sep 10, 2024
f660a83
just leave out the props altogether
watany-dev Sep 10, 2024
2ad9170
to private method
watany-dev Sep 10, 2024
989141b
we also check the AWS::Lambda::Permission resource exists in the temp…
watany-dev Sep 10, 2024
7226680
we handle permissions for imported lambda functions
watany-dev Sep 10, 2024
7f9ba73
re integtest
watany-dev Sep 11, 2024
a577a65
re integ
watany-dev Sep 11, 2024
beeb0f4
fix
watany-dev Sep 12, 2024
c129a19
Merge branch 'main' into lambda-url-oac
watany-dev Sep 12, 2024
64c92a7
Merge branch 'aws:main' into lambda-url-oac
watany-dev Sep 13, 2024
ad5778d
update integ
watany-dev Sep 13, 2024
5802d54
Merge branch 'main' into lambda-url-oac
watany-dev Sep 13, 2024
258e464
update the url to lambdaFunctionUrl
watany-dev Sep 26, 2024
8ada469
props?.originAccessControl
watany-dev Sep 26, 2024
d8b0b16
refactor test case
watany-dev Sep 26, 2024
1e4983b
test case that accepts only fnUrl
watany-dev Sep 26, 2024
16eb227
fixed
watany-dev Sep 26, 2024
693abfe
Merge branch 'main' into lambda-url-oac
watany-dev Sep 26, 2024
92f7e71
revert this.oac
watany-dev Sep 27, 2024
adc12d3
integ
watany-dev Sep 27, 2024
e4817ff
Authtype.None
watany-dev Sep 27, 2024
fd9f9b4
Split OAC Configuration Test for Default and Custom Signing Behavior
watany-dev Sep 28, 2024
fea5a32
split integ
watany-dev Sep 28, 2024
873777b
update integ
watany-dev Sep 28, 2024
fcf8db0
update doc
watany-dev Oct 1, 2024
2375bcd
remove duplicate test
watany-dev Oct 1, 2024
a6f5726
Merge branch 'aws:main' into lambda-url-oac
watany-dev Oct 5, 2024
fce4e73
adding warning
watany-dev Oct 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions packages/aws-cdk-lib/aws-cloudfront-origins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -633,3 +633,51 @@ new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: { origin: new origins.FunctionUrlOrigin(fnUrl) },
});
```

### Lambda Function URL with Origin Access Control (OAC)
You can also configure the Lambda function URL with Origin Access Control (OAC) to manage permissions and security. The withOriginAccessControl() method automatically configures this setup for enhanced security.

```ts
import * as lambda from 'aws-cdk-lib/aws-lambda';

declare const fn: lambda.Function;

const fnUrl = fn.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
});

new cloudfront.Distribution(stack, 'MyDistribution', {
defaultBehavior: {
origin: origins.FunctionUrlOrigin.withOriginAccessControl(fnUrl),
},
});
```

If you want to explicitly add OAC for more customized access control, you can use the originAccessControl option as shown below.

```ts
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';

declare const fn: lambda.Function;

const fnUrl = fn.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
});

// Define a custom OAC
const oac = new cloudfront.FunctionUrlOriginAccessControl(stack, 'MyOAC', {
originAccessControlName: 'CustomLambdaOAC',
signing: cloudfront.Signing.SIGV4_ALWAYS,
});

// Set up Lambda Function URL with OAC in CloudFront Distribution
new cloudfront.Distribution(stack, 'MyDistribution', {
defaultBehavior: {
origin: origins.FunctionUrlOrigin.withOriginAccessControl(fnUrl, {
originAccessControl: oac,
}),
},
});
```
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Construct } from 'constructs';
import { validateSecondsInRangeOrUndefined } from './private/utils';
import * as cloudfront from '../../aws-cloudfront';
import * as iam from '../../aws-iam';
import * as lambda from '../../aws-lambda';
import * as cdk from '../../core';

Expand Down Expand Up @@ -30,10 +32,35 @@ export interface FunctionUrlOriginProps extends cloudfront.OriginProps {
readonly keepaliveTimeout?: cdk.Duration;
}

/**
* Properties for configuring a origin using a standard Lambda Functions URLs.
*/
export interface FunctionUrlOriginBaseProps extends cloudfront.OriginProps { }

/**
* Properties for configuring a Lambda Functions URLs with OAC.
*/
export interface FunctionUrlOriginWithOACProps extends FunctionUrlOriginProps {
/**
* An optional Origin Access Control
*
* @default - an Origin Access Control will be created.
*/
readonly originAccessControl?: cloudfront.IOriginAccessControl;

}

/**
* An Origin for a Lambda Function URL.
*/
export class FunctionUrlOrigin extends cloudfront.OriginBase {
/**
* Create a Functions URL Origin with Origin Access Control (OAC) configured
*/
public static withOriginAccessControl(url: lambda.IFunctionUrl, props?: FunctionUrlOriginWithOACProps): cloudfront.IOrigin {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we update the url to lambdaFunctionUrl to be consistent across the code and also readable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed. 258e464

return new FunctionUrlOriginWithOAC(url, props);
}

constructor(lambdaFunctionUrl: lambda.IFunctionUrl, private readonly props: FunctionUrlOriginProps = {}) {
// Lambda Function URL is of the form 'https://<lambda-id>.lambda-url.<region>.on.aws/'
// No need to split URL as we do with REST API, the entire URL is needed
Expand All @@ -52,4 +79,66 @@ export class FunctionUrlOrigin extends cloudfront.OriginBase {
originKeepaliveTimeout: this.props.keepaliveTimeout?.toSeconds(),
};
}
}

/**
* An Origin for a Lambda Function URL with OAC.
*/
export class FunctionUrlOriginWithOAC extends cloudfront.OriginBase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep this class private as it's an implementation detail and not intended for public usage

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed.
a43bf40

private originAccessControl?: cloudfront.IOriginAccessControl;
private functionArn: string

constructor(lambdaFunctionUrl: lambda.IFunctionUrl, props: FunctionUrlOriginWithOACProps = {}) {
const domainName = cdk.Fn.select(2, cdk.Fn.split('/', lambdaFunctionUrl.url));
super(domainName, props);

this.functionArn = lambdaFunctionUrl.functionArn;
this.originAccessControl = props.originAccessControl;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since props can be empty object, do we need optional chaining operator ( ?.) while accessing the prop?

props?.originAccessControl

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed. 8ada469


validateSecondsInRangeOrUndefined('readTimeout', 1, 180, props.readTimeout);
validateSecondsInRangeOrUndefined('keepaliveTimeout', 1, 180, props.keepaliveTimeout);
}

protected renderCustomOriginConfig(): cloudfront.CfnDistribution.CustomOriginConfigProperty | undefined {
return {
originSslProtocols: [cloudfront.OriginSslPolicy.TLS_V1_2],
originProtocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need originReadTimeout and originKeepaliveTimeout to be set for FunctionUrlOriginWithOAC similar to this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed. 16eb227

}

public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig {
const originBindConfig = super.bind(scope, options);
const distributionId = options.distributionId;

if (!this.originAccessControl) {
const cfnOriginAccessControl = new cloudfront.CfnOriginAccessControl(scope, 'LambdaOriginAccessControl', {
originAccessControlConfig: {
name: 'OAC for Lambda Function URL',
originAccessControlOriginType: 'lambda',
signingBehavior: 'always',
signingProtocol: 'sigv4',
},
});

this.originAccessControl = {
originAccessControlId: cfnOriginAccessControl.attrId,
} as cloudfront.IOriginAccessControl;

const lambdaFunction = lambda.Function.fromFunctionArn(scope, 'ReferencedLambdaFunction', this.functionArn);

lambdaFunction.addPermission('AllowCloudFrontServicePrincipal', {
principal: new iam.ServicePrincipal('cloudfront.amazonaws.com'),
action: 'lambda:InvokeFunctionUrl',
sourceArn: `arn:${cdk.Aws}:cloudfront::${cdk.Stack.of(scope).account}:distribution/${distributionId}`,
});
}

return {
...originBindConfig,
originProperty: {
...originBindConfig.originProperty!,
originAccessControlId: this.originAccessControl?.originAccessControlId,
},
};
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Template, Match } from '../../assertions';
import * as cloudfront from '../../aws-cloudfront';
import * as lambda from '../../aws-lambda';
import { Stack } from '../../core';
import * as cdk from '../../core';
import { FunctionUrlOrigin } from '../lib';

let stack: Stack;
Expand Down Expand Up @@ -41,3 +44,174 @@ test('Correctly renders the origin for a Lambda Function URL', () => {
},
});
});

test('Correctly sets readTimeout and keepaliveTimeout', () => {
const fn = new lambda.Function(stack, 'MyFunction', {
code: lambda.Code.fromInline('exports.handler = async () => {};'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_20_X,
});

const fnUrl = fn.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
});

const origin = new FunctionUrlOrigin(fnUrl, {
readTimeout: cdk.Duration.seconds(120),
keepaliveTimeout: cdk.Duration.seconds(60),
});

const originBindConfig = origin.bind(stack, { originId: 'StackOriginLambdaFunctionURL' });

expect(stack.resolve(originBindConfig.originProperty)).toMatchObject({
customOriginConfig: {
originReadTimeout: 120,
originKeepaliveTimeout: 60,
},
});
});

test('Correctly adds permission to Lambda for CloudFront', () => {
const fn = new lambda.Function(stack, 'MyFunction', {
code: lambda.Code.fromInline('exports.handler = async () => {};'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_20_X,
});

const fnUrl = fn.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
});

const distribution = new cloudfront.Distribution(stack, 'MyDistribution', {
defaultBehavior: {
origin: FunctionUrlOrigin.withOriginAccessControl(fnUrl, {
originAccessControl: undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is undefined passed here? Could we just leave out the props altogether?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed.
f660a83

}),
},
});

const template = Template.fromStack(stack);

template.hasResourceProperties('AWS::CloudFront::Distribution', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this test is checking for the correct permissions, can we also check the AWS::Lambda::Permission resource exists in the template and has the expected policy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed.
7226680

DistributionConfig: {
Origins: Match.arrayWith([
Match.objectLike({
DomainName: {
'Fn::Select': [
2,
{
'Fn::Split': [
'/',
{
'Fn::GetAtt': ['MyFunctionFunctionUrlFF6DE78C', 'FunctionUrl'],
},
],
},
],
},
OriginAccessControlId: Match.objectLike({
'Fn::GetAtt': [
Match.stringLikeRegexp('MyDistributionOrigin.*LambdaOriginAccessControl.*'),
'Id',
],
}),
}),
]),
},
});
});

test('Correctly configures CloudFront Distribution with Origin Access Control', () => {
const fn = new lambda.Function(stack, 'MyFunction', {
code: lambda.Code.fromInline('exports.handler = async () => {};'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_20_X,
});

const fnUrl = fn.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
});

new cloudfront.Distribution(stack, 'MyDistribution', {
defaultBehavior: {
origin: FunctionUrlOrigin.withOriginAccessControl(fnUrl, {
originAccessControl: undefined,
}),
},
});

const template = Template.fromStack(stack);

template.hasResourceProperties('AWS::CloudFront::Distribution', {
DistributionConfig: {
Origins: Match.arrayWith([
Match.objectLike({
DomainName: {
'Fn::Select': [
2,
{
'Fn::Split': [
'/',
{
'Fn::GetAtt': ['MyFunctionFunctionUrlFF6DE78C', 'FunctionUrl'],
},
],
},
],
},
OriginAccessControlId: Match.objectLike({
'Fn::GetAtt': [
Match.stringLikeRegexp('MyDistributionOrigin.*LambdaOriginAccessControl.*'),
'Id',
],
}),
}),
]),
},
});

template.hasResourceProperties('AWS::CloudFront::OriginAccessControl', {
OriginAccessControlConfig: {
OriginAccessControlOriginType: 'lambda',
SigningBehavior: 'always',
SigningProtocol: 'sigv4',
},
});
});

test('Correctly configures CloudFront Distribution with a custom Origin Access Control', () => {
const fn = new lambda.Function(stack, 'MyFunction', {
code: lambda.Code.fromInline('exports.handler = async () => {};'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_20_X,
});

const fnUrl = fn.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
});

// Custom OAC configuration
const oac = new cloudfront.FunctionUrlOriginAccessControl(stack, 'CustomOAC', {
originAccessControlName: 'CustomLambdaOAC',
signing: cloudfront.Signing.SIGV4_ALWAYS,
});

new cloudfront.Distribution(stack, 'MyDistribution', {
defaultBehavior: {
origin: FunctionUrlOrigin.withOriginAccessControl(fnUrl, {
originAccessControl: oac,
}),
},
});

const template = Template.fromStack(stack);

template.hasResourceProperties('AWS::CloudFront::OriginAccessControl', {
OriginAccessControlConfig: {
Name: 'CustomLambdaOAC',
OriginAccessControlOriginType: 'lambda',
SigningBehavior: 'always',
SigningProtocol: 'sigv4',
},
});
});
Loading
Loading