Skip to content

Commit 4aa2e88

Browse files
authored
Added FromCustomAuthorizerAttribute and tests. (#1466)
1 parent ec0aca7 commit 4aa2e88

File tree

57 files changed

+6158
-1117
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+6158
-1117
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Projects": [
3+
{
4+
"Name": "Amazon.Lambda.Annotations",
5+
"Type": "Minor",
6+
"ChangelogMessages": [
7+
"Added FromCustomAuthorizerAttribute"
8+
]
9+
}
10+
]
11+
}

Libraries/Amazon.Lambda.Annotations.slnf

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
"src\\Amazon.Lambda.Serialization.SystemTextJson\\Amazon.Lambda.Serialization.SystemTextJson.csproj",
1212
"test\\Amazon.Lambda.Annotations.SourceGenerators.Tests\\Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj",
1313
"test\\TestExecutableServerlessApp\\TestExecutableServerlessApp.csproj",
14+
"test\\IntegrationTests.Helpers\\IntegrationTests.Helpers.csproj",
15+
"test\\TestCustomAuthorizerApp\\TestCustomAuthorizerApp.csproj",
16+
"test\\TestCustomAuthorizerApp.IntegrationTests\\TestCustomAuthorizerApp.IntegrationTests.csproj",
1417
"test\\TestServerlessApp.IntegrationTests\\TestServerlessApp.IntegrationTests.csproj",
1518
"test\\TestServerlessApp.NET8\\TestServerlessApp.NET8.csproj",
1619
"test\\TestServerlessApp\\TestServerlessApp.csproj"
1720
]
1821
}
19-
}
22+
}

Libraries/Libraries.sln

Lines changed: 555 additions & 10 deletions
Large diffs are not rendered by default.

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
5454
Type = TypeModelBuilder.Build(att.AttributeClass, context)
5555
};
5656
}
57+
else if(att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.FromCustomAuthorizerAttribute), SymbolEqualityComparer.Default))
58+
{
59+
var data = FromCustomAuthorizerAttributeBuilder.Build(att);
60+
model = new AttributeModel<FromCustomAuthorizerAttribute>
61+
{
62+
Data = data,
63+
Type = TypeModelBuilder.Build(att.AttributeClass, context)
64+
};
65+
}
5766
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.HttpApiAttribute), SymbolEqualityComparer.Default))
5867
{
5968
var data = HttpApiAttributeBuilder.Build(att);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Amazon.Lambda.Annotations.APIGateway;
2+
using Microsoft.CodeAnalysis;
3+
4+
namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
5+
{
6+
public class FromCustomAuthorizerAttributeBuilder
7+
{
8+
public static FromCustomAuthorizerAttribute Build(AttributeData att)
9+
{
10+
var data = new FromCustomAuthorizerAttribute();
11+
foreach (var pair in att.NamedArguments)
12+
{
13+
if (pair.Key == nameof(data.Name) && pair.Value.Value is string value)
14+
{
15+
data.Name = value;
16+
}
17+
}
18+
19+
return data;
20+
}
21+
}
22+
}

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/APIGatewaySetupParameters.cs

Lines changed: 588 additions & 79 deletions
Large diffs are not rendered by default.

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/APIGatewaySetupParameters.tt

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,177 @@
247247
validationErrors.Add($"Value {__request__.Body} at 'body' failed to satisfy constraint: {e.Message}");
248248
}
249249

250+
<#
251+
}
252+
}
253+
else if (parameter.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromCustomAuthorizerAttribute))
254+
{
255+
var fromAuthorizerAttribute = parameter.Attributes?.FirstOrDefault(att => att.Type.FullName == TypeFullNames.FromCustomAuthorizerAttribute) as AttributeModel<Amazon.Lambda.Annotations.APIGateway.FromCustomAuthorizerAttribute>;
256+
257+
// Use parameter name as key, if Name has not specified explicitly in the attribute definition.
258+
var authKey = fromAuthorizerAttribute?.Data?.Name ?? parameter.Name;
259+
// REST API and HTTP API v1 both use APIGatewayProxyRequest where RequestContext.Authorizer is a dictionary.
260+
// Only HTTP API v2 uses APIGatewayHttpApiV2ProxyRequest with RequestContext.Authorizer.Lambda as the dictionary.
261+
if(restApiAttribute != null || httpApiAttribute?.Data.Version == Amazon.Lambda.Annotations.APIGateway.HttpApiVersion.V1)
262+
{
263+
#>
264+
var <#= parameter.Name #> = default(<#= parameter.Type.FullName #>);
265+
if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("<#= authKey #>") == false)
266+
{
267+
<#= "#if NET6_0_OR_GREATER" #>
268+
__context__.Logger.LogDebug("Authorizer attribute '<#= authKey #>' was missing, returning unauthorized.");
269+
<#= "#else" #>
270+
__context__.Logger.Log("Authorizer attribute '<#= authKey #>' was missing, returning unauthorized.");
271+
<#= "#endif" #>
272+
var __unauthorized__ = new <#= restApiAttribute != null || httpApiAttribute?.Data.Version == Amazon.Lambda.Annotations.APIGateway.HttpApiVersion.V1 ? "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse" : "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse" #>
273+
{
274+
Headers = new Dictionary<string, string>
275+
{
276+
{"Content-Type", "application/json"},
277+
{"x-amzn-ErrorType", "AccessDeniedException"}
278+
},
279+
StatusCode = 401
280+
};
281+
<#
282+
if(_model.LambdaMethod.ReturnsIHttpResults)
283+
{
284+
#>
285+
var __unauthorizedStream__ = new System.IO.MemoryStream();
286+
serializer.Serialize(__unauthorized__, __unauthorizedStream__);
287+
__unauthorizedStream__.Position = 0;
288+
return __unauthorizedStream__;
289+
<#
290+
}
291+
else
292+
{
293+
#>
294+
return __unauthorized__;
295+
<#
296+
}
297+
#>
298+
}
299+
300+
try
301+
{
302+
var __authValue_<#= parameter.Name #>__ = __request__.RequestContext.Authorizer["<#= authKey #>"];
303+
<#= parameter.Name #> = (<#= parameter.Type.FullName #>)Convert.ChangeType(__authValue_<#= parameter.Name #>__?.ToString(), typeof(<#= parameter.Type.FullNameWithoutAnnotations #>));
304+
}
305+
catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException)
306+
{
307+
<#= "#if NET6_0_OR_GREATER" #>
308+
__context__.Logger.LogError(e, "Failed to convert authorizer attribute '<#= authKey #>', returning unauthorized.");
309+
<#= "#else" #>
310+
__context__.Logger.Log("Failed to convert authorizer attribute '<#= authKey #>', returning unauthorized. Exception: " + e.ToString());
311+
<#= "#endif" #>
312+
var __unauthorized__ = new <#= restApiAttribute != null || httpApiAttribute?.Data.Version == Amazon.Lambda.Annotations.APIGateway.HttpApiVersion.V1 ? "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse" : "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse" #>
313+
{
314+
Headers = new Dictionary<string, string>
315+
{
316+
{"Content-Type", "application/json"},
317+
{"x-amzn-ErrorType", "AccessDeniedException"}
318+
},
319+
StatusCode = 401
320+
};
321+
<#
322+
if(_model.LambdaMethod.ReturnsIHttpResults)
323+
{
324+
#>
325+
var __unauthorizedStream__ = new System.IO.MemoryStream();
326+
serializer.Serialize(__unauthorized__, __unauthorizedStream__);
327+
__unauthorizedStream__.Position = 0;
328+
return __unauthorizedStream__;
329+
<#
330+
}
331+
else
332+
{
333+
#>
334+
return __unauthorized__;
335+
<#
336+
}
337+
#>
338+
}
339+
340+
<#
341+
}
342+
else
343+
{
344+
#>
345+
var <#= parameter.Name #> = default(<#= parameter.Type.FullName #>);
346+
if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("<#= authKey #>") == false)
347+
{
348+
<#= "#if NET6_0_OR_GREATER" #>
349+
__context__.Logger.LogDebug("Authorizer attribute '<#= authKey #>' was missing, returning unauthorized.");
350+
<#= "#else" #>
351+
__context__.Logger.Log("Authorizer attribute '<#= authKey #>' was missing, returning unauthorized.");
352+
<#= "#endif" #>
353+
var __unauthorized__ = new <#= restApiAttribute != null || httpApiAttribute?.Data.Version == Amazon.Lambda.Annotations.APIGateway.HttpApiVersion.V1 ? "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse" : "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse" #>
354+
{
355+
Headers = new Dictionary<string, string>
356+
{
357+
{"Content-Type", "application/json"},
358+
{"x-amzn-ErrorType", "AccessDeniedException"}
359+
},
360+
StatusCode = 401
361+
};
362+
<#
363+
if(_model.LambdaMethod.ReturnsIHttpResults)
364+
{
365+
#>
366+
var __unauthorizedStream__ = new System.IO.MemoryStream();
367+
serializer.Serialize(__unauthorized__, __unauthorizedStream__);
368+
__unauthorizedStream__.Position = 0;
369+
return __unauthorizedStream__;
370+
<#
371+
}
372+
else
373+
{
374+
#>
375+
return __unauthorized__;
376+
<#
377+
}
378+
#>
379+
}
380+
381+
try
382+
{
383+
var __authValue_<#= parameter.Name #>__ = __request__.RequestContext.Authorizer.Lambda["<#= authKey #>"];
384+
<#= parameter.Name #> = (<#= parameter.Type.FullName #>)Convert.ChangeType(__authValue_<#= parameter.Name #>__?.ToString(), typeof(<#= parameter.Type.FullNameWithoutAnnotations #>));
385+
}
386+
catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException)
387+
{
388+
<#= "#if NET6_0_OR_GREATER" #>
389+
__context__.Logger.LogError(e, "Failed to convert authorizer attribute '<#= authKey #>', returning unauthorized.");
390+
<#= "#else" #>
391+
__context__.Logger.Log("Failed to convert authorizer attribute '<#= authKey #>', returning unauthorized. Exception: " + e.ToString());
392+
<#= "#endif" #>
393+
var __unauthorized__ = new <#= restApiAttribute != null || httpApiAttribute?.Data.Version == Amazon.Lambda.Annotations.APIGateway.HttpApiVersion.V1 ? "Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse" : "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse" #>
394+
{
395+
Headers = new Dictionary<string, string>
396+
{
397+
{"Content-Type", "application/json"},
398+
{"x-amzn-ErrorType", "AccessDeniedException"}
399+
},
400+
StatusCode = 401
401+
};
402+
<#
403+
if(_model.LambdaMethod.ReturnsIHttpResults)
404+
{
405+
#>
406+
var __unauthorizedStream__ = new System.IO.MemoryStream();
407+
serializer.Serialize(__unauthorized__, __unauthorizedStream__);
408+
__unauthorizedStream__.Position = 0;
409+
return __unauthorizedStream__;
410+
<#
411+
}
412+
else
413+
{
414+
#>
415+
return __unauthorized__;
416+
<#
417+
}
418+
#>
419+
}
420+
250421
<#
251422
}
252423
}
@@ -315,4 +486,4 @@
315486

316487
<#
317488
}
318-
#>
489+
#>

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public static class TypeFullNames
3131
public const string FromHeaderAttribute = "Amazon.Lambda.Annotations.APIGateway.FromHeaderAttribute";
3232
public const string FromBodyAttribute = "Amazon.Lambda.Annotations.APIGateway.FromBodyAttribute";
3333
public const string FromRouteAttribute = "Amazon.Lambda.Annotations.APIGateway.FromRouteAttribute";
34+
public const string FromCustomAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.FromCustomAuthorizerAttribute";
3435

3536
public const string SQSEvent = "Amazon.Lambda.SQSEvents.SQSEvent";
3637
public const string SQSBatchResponse = "Amazon.Lambda.SQSEvents.SQSBatchResponse";
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
3+
namespace Amazon.Lambda.Annotations.APIGateway
4+
{
5+
/// <summary>
6+
/// Maps this parameter to a custom authorizer item
7+
/// </summary>
8+
/// <remarks>
9+
/// Will try to get the specified key from Custom Authorizer values
10+
/// </remarks>
11+
[AttributeUsage(AttributeTargets.Parameter)]
12+
public class FromCustomAuthorizerAttribute : Attribute, INamedAttribute
13+
{
14+
/// <summary>
15+
/// Key of the value
16+
/// </summary>
17+
public string Name { get; set; }
18+
}
19+
}

Libraries/src/Amazon.Lambda.Annotations/README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,22 @@ parameter to the `LambdaFunction` must be the event object and the event source
894894
* Map method parameter to HTTP request body. If parameter is a complex type then request body will be assumed to be JSON and deserialized into the type.
895895
* FromServices
896896
* Map method parameter to registered service in IServiceProvider
897+
* FromCustomAuthorizer
898+
* Map method parameter to a custom authorizer context value. Use the `Name` property to specify the key in the authorizer context if it differs from the parameter name. Returns HTTP 401 Unauthorized if the key is not found or type conversion fails.
899+
900+
Example using `FromCustomAuthorizer` to access values set by a custom Lambda authorizer:
901+
```csharp
902+
[LambdaFunction]
903+
[HttpApi(LambdaHttpMethod.Get, "/api/protected")]
904+
public async Task ProtectedEndpoint(
905+
[FromCustomAuthorizer(Name = "userId")] string userId,
906+
[FromCustomAuthorizer(Name = "tenantId")] int tenantId,
907+
ILambdaContext context)
908+
{
909+
context.Logger.LogLine($"User {userId} from tenant {tenantId}");
910+
// userId and tenantId are automatically extracted from the custom authorizer context
911+
}
912+
```
897913

898914
### Customizing responses for API Gateway Lambda functions
899915

@@ -940,4 +956,4 @@ The content type is determined using the following rules.
940956

941957
## Project References
942958

943-
If API Gateway event attributes, such as `RestAPI` or `HttpAPI`, are being used then a package reference to `Amazon.Lambda.APIGatewayEvents` must be added to the project, otherwise the project will not compile. We do not include it by default in order to keep the `Amazon.Lambda.Annotations` library lightweight.
959+
If API Gateway event attributes, such as `RestAPI` or `HttpAPI`, are being used then a package reference to `Amazon.Lambda.APIGatewayEvents` must be added to the project, otherwise the project will not compile. We do not include it by default in order to keep the `Amazon.Lambda.Annotations` library lightweight.

0 commit comments

Comments
 (0)