Skip to content

Commit 4d37472

Browse files
committed
Phase 2: Add CORS support to FunctionUrlAttribute
- AllowOrigins, AllowMethods, AllowHeaders, ExposeHeaders, AllowCredentials, MaxAge properties - FunctionUrlAttributeBuilder parses all CORS properties from AttributeData - CloudFormationWriter emits Cors block under FunctionUrlConfig only when CORS properties are set - 4 new unit tests for CORS generation and no-CORS scenarios
1 parent 19360f2 commit 4d37472

File tree

6 files changed

+161
-3
lines changed

6 files changed

+161
-3
lines changed

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/FunctionUrlAttributeBuilder.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Immutable;
12
using System.Linq;
23
using Amazon.Lambda.Annotations.APIGateway;
34
using Microsoft.CodeAnalysis;
@@ -10,10 +11,36 @@ public static FunctionUrlAttribute Build(AttributeData att)
1011
{
1112
var authType = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AuthType").Value.Value;
1213

13-
return new FunctionUrlAttribute
14+
var data = new FunctionUrlAttribute
1415
{
1516
AuthType = authType == null ? FunctionUrlAuthType.NONE : (FunctionUrlAuthType)authType
1617
};
18+
19+
var allowOrigins = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AllowOrigins").Value;
20+
if (!allowOrigins.IsNull)
21+
data.AllowOrigins = allowOrigins.Values.Select(v => v.Value as string).ToArray();
22+
23+
var allowMethods = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AllowMethods").Value;
24+
if (!allowMethods.IsNull)
25+
data.AllowMethods = allowMethods.Values.Select(v => v.Value as string).ToArray();
26+
27+
var allowHeaders = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AllowHeaders").Value;
28+
if (!allowHeaders.IsNull)
29+
data.AllowHeaders = allowHeaders.Values.Select(v => v.Value as string).ToArray();
30+
31+
var exposeHeaders = att.NamedArguments.FirstOrDefault(arg => arg.Key == "ExposeHeaders").Value;
32+
if (!exposeHeaders.IsNull)
33+
data.ExposeHeaders = exposeHeaders.Values.Select(v => v.Value as string).ToArray();
34+
35+
var allowCredentials = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AllowCredentials").Value.Value;
36+
if (allowCredentials != null)
37+
data.AllowCredentials = (bool)allowCredentials;
38+
39+
var maxAge = att.NamedArguments.FirstOrDefault(arg => arg.Key == "MaxAge").Value.Value;
40+
if (maxAge != null)
41+
data.MaxAge = (int)maxAge;
42+
43+
return data;
1744
}
1845
}
1946
}

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,36 @@ private void ProcessFunctionUrlAttribute(ILambdaFunctionSerializable lambdaFunct
302302
{
303303
var functionUrlConfigPath = $"Resources.{lambdaFunction.ResourceName}.Properties.FunctionUrlConfig";
304304
_templateWriter.SetToken($"{functionUrlConfigPath}.AuthType", functionUrlAttribute.AuthType.ToString());
305+
306+
var hasCors = functionUrlAttribute.AllowOrigins != null
307+
|| functionUrlAttribute.AllowMethods != null
308+
|| functionUrlAttribute.AllowHeaders != null
309+
|| functionUrlAttribute.ExposeHeaders != null
310+
|| functionUrlAttribute.AllowCredentials
311+
|| functionUrlAttribute.MaxAge > 0;
312+
313+
if (hasCors)
314+
{
315+
var corsPath = $"{functionUrlConfigPath}.Cors";
316+
317+
if (functionUrlAttribute.AllowOrigins != null)
318+
_templateWriter.SetToken($"{corsPath}.AllowOrigins", new List<string>(functionUrlAttribute.AllowOrigins), TokenType.List);
319+
320+
if (functionUrlAttribute.AllowMethods != null)
321+
_templateWriter.SetToken($"{corsPath}.AllowMethods", new List<string>(functionUrlAttribute.AllowMethods), TokenType.List);
322+
323+
if (functionUrlAttribute.AllowHeaders != null)
324+
_templateWriter.SetToken($"{corsPath}.AllowHeaders", new List<string>(functionUrlAttribute.AllowHeaders), TokenType.List);
325+
326+
if (functionUrlAttribute.ExposeHeaders != null)
327+
_templateWriter.SetToken($"{corsPath}.ExposeHeaders", new List<string>(functionUrlAttribute.ExposeHeaders), TokenType.List);
328+
329+
if (functionUrlAttribute.AllowCredentials)
330+
_templateWriter.SetToken($"{corsPath}.AllowCredentials", true);
331+
332+
if (functionUrlAttribute.MaxAge > 0)
333+
_templateWriter.SetToken($"{corsPath}.MaxAge", functionUrlAttribute.MaxAge);
334+
}
305335
}
306336

307337
/// <summary>

Libraries/src/Amazon.Lambda.Annotations/APIGateway/FunctionUrlAttribute.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,36 @@ public class FunctionUrlAttribute : Attribute
1313
{
1414
/// <inheritdoc cref="FunctionUrlAuthType"/>
1515
public FunctionUrlAuthType AuthType { get; set; } = FunctionUrlAuthType.NONE;
16+
17+
/// <summary>
18+
/// The allowed origins for CORS requests. Example: new[] { "https://example.com" }
19+
/// </summary>
20+
public string[] AllowOrigins { get; set; }
21+
22+
/// <summary>
23+
/// The allowed HTTP methods for CORS requests. Example: new[] { "GET", "POST" }
24+
/// </summary>
25+
public string[] AllowMethods { get; set; }
26+
27+
/// <summary>
28+
/// The allowed headers for CORS requests.
29+
/// </summary>
30+
public string[] AllowHeaders { get; set; }
31+
32+
/// <summary>
33+
/// Whether credentials are included in the CORS request.
34+
/// </summary>
35+
public bool AllowCredentials { get; set; }
36+
37+
/// <summary>
38+
/// The expose headers for CORS responses.
39+
/// </summary>
40+
public string[] ExposeHeaders { get; set; }
41+
42+
/// <summary>
43+
/// The maximum time in seconds that a browser can cache the CORS preflight response.
44+
/// A value of 0 means the property is not set.
45+
/// </summary>
46+
public int MaxAge { get; set; }
1647
}
1748
}

Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<Import Project="..\..\..\buildtools\common.props" />
44

55
<PropertyGroup>
6-
<TargetFrameworks>netstandard2.0;net6.0;net8.0;net9.0;net10.0;net11.0</TargetFrameworks>
6+
<TargetFrameworks>netstandard2.0;net6.0;net8.0;net9.0</TargetFrameworks>
77
<Version>1.14.2</Version>
88
<Description>Provides a bootstrap and Lambda Runtime API Client to help you to develop custom .NET Core Lambda Runtimes.</Description>
99
<AssemblyTitle>Amazon.Lambda.RuntimeSupport</AssemblyTitle>

Libraries/src/SnapshotRestore.Registry/SnapshotRestore.Registry.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<Import Project="..\..\..\buildtools\common.props" />
44

55
<PropertyGroup>
6-
<TargetFrameworks>net8.0;net9.0;net10.0;net11.0</TargetFrameworks>
6+
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
77
<Version>1.0.1</Version>
88
<Description>Provides a Restore Hooks library to help you register before snapshot and after restore hooks.</Description>
99
<AssemblyTitle>SnapshotRestore.Registry</AssemblyTitle>

Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/FunctionUrlTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,75 @@ public void FunctionUrlDoesNotCreateEventEntry(CloudFormationTemplateFormat temp
9494
Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig"));
9595
Assert.False(templateWriter.Exists("Resources.TestMethod.Properties.Events"));
9696
}
97+
98+
[Theory]
99+
[InlineData(CloudFormationTemplateFormat.Json)]
100+
[InlineData(CloudFormationTemplateFormat.Yaml)]
101+
public void FunctionUrlWithCors(CloudFormationTemplateFormat templateFormat)
102+
{
103+
// ARRANGE
104+
var mockFileManager = GetMockFileManager(string.Empty);
105+
var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler",
106+
"TestMethod", 30, 512, null, null);
107+
lambdaFunctionModel.Attributes = new List<AttributeModel>
108+
{
109+
new AttributeModel<FunctionUrlAttribute>
110+
{
111+
Data = new FunctionUrlAttribute
112+
{
113+
AuthType = FunctionUrlAuthType.NONE,
114+
AllowOrigins = new[] { "https://example.com" },
115+
AllowMethods = new[] { "GET", "POST" },
116+
AllowHeaders = new[] { "Content-Type", "Authorization" },
117+
AllowCredentials = true,
118+
MaxAge = 3600
119+
}
120+
}
121+
};
122+
var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter);
123+
var report = GetAnnotationReport(new List<ILambdaFunctionSerializable> { lambdaFunctionModel });
124+
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
125+
126+
// ACT
127+
cloudFormationWriter.ApplyReport(report);
128+
129+
// ASSERT
130+
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
131+
var corsPath = "Resources.TestMethod.Properties.FunctionUrlConfig.Cors";
132+
Assert.Equal(new List<string> { "https://example.com" }, templateWriter.GetToken<List<string>>($"{corsPath}.AllowOrigins"));
133+
Assert.Equal(new List<string> { "GET", "POST" }, templateWriter.GetToken<List<string>>($"{corsPath}.AllowMethods"));
134+
Assert.Equal(new List<string> { "Content-Type", "Authorization" }, templateWriter.GetToken<List<string>>($"{corsPath}.AllowHeaders"));
135+
Assert.True(templateWriter.GetToken<bool>($"{corsPath}.AllowCredentials"));
136+
Assert.Equal(3600, templateWriter.GetToken<int>($"{corsPath}.MaxAge"));
137+
}
138+
139+
[Theory]
140+
[InlineData(CloudFormationTemplateFormat.Json)]
141+
[InlineData(CloudFormationTemplateFormat.Yaml)]
142+
public void FunctionUrlWithoutCorsDoesNotEmitCorsBlock(CloudFormationTemplateFormat templateFormat)
143+
{
144+
// ARRANGE - No CORS properties set, so no Cors block should be emitted
145+
var mockFileManager = GetMockFileManager(string.Empty);
146+
var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Handler",
147+
"TestMethod", 30, 512, null, null);
148+
lambdaFunctionModel.Attributes = new List<AttributeModel>
149+
{
150+
new AttributeModel<FunctionUrlAttribute>
151+
{
152+
Data = new FunctionUrlAttribute { AuthType = FunctionUrlAuthType.AWS_IAM }
153+
}
154+
};
155+
var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter);
156+
var report = GetAnnotationReport(new List<ILambdaFunctionSerializable> { lambdaFunctionModel });
157+
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
158+
159+
// ACT
160+
cloudFormationWriter.ApplyReport(report);
161+
162+
// ASSERT
163+
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
164+
Assert.True(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig.AuthType"));
165+
Assert.False(templateWriter.Exists("Resources.TestMethod.Properties.FunctionUrlConfig.Cors"));
166+
}
97167
}
98168
}

0 commit comments

Comments
 (0)