diff --git a/.autover/changes/09045e59-ab51-459e-b450-43fcb082d084.json b/.autover/changes/09045e59-ab51-459e-b450-43fcb082d084.json new file mode 100644 index 000000000..66108b5e5 --- /dev/null +++ b/.autover/changes/09045e59-ab51-459e-b450-43fcb082d084.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.Annotations", + "Type": "Patch", + "ChangelogMessages": [ + "Added Authorizers annotation" + ] + } + ] +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj index 9e68cf47f..3c894f59b 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj @@ -63,6 +63,11 @@ + + diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md index 17d4678ce..a13794296 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md @@ -1,2 +1,16 @@ ; Unshipped analyzer release ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +AWSLambda0120 | AWSLambdaCSharpGenerator | Error | Authorizer Name Required +AWSLambda0121 | AWSLambdaCSharpGenerator | Error | HTTP API Authorizer Not Found +AWSLambda0122 | AWSLambdaCSharpGenerator | Error | REST API Authorizer Not Found +AWSLambda0123 | AWSLambdaCSharpGenerator | Error | Authorizer Type Mismatch +AWSLambda0124 | AWSLambdaCSharpGenerator | Error | Authorizer Type Mismatch +AWSLambda0125 | AWSLambdaCSharpGenerator | Error | Duplicate Authorizer Name +AWSLambda0127 | AWSLambdaCSharpGenerator | Error | Invalid Result TTL +AWSLambda0128 | AWSLambdaCSharpGenerator | Warning | Authorizer Payload Version Mismatch +AWSLambda0129 | AWSLambdaCSharpGenerator | Error | Missing LambdaFunction Attribute diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs index a606e5e88..7e843ca52 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs @@ -153,5 +153,78 @@ public static class DiagnosticDescriptors category: "AWSLambdaCSharpGenerator", DiagnosticSeverity.Error, isEnabledByDefault: true); + + // Authorizer diagnostics (ALA0019-ALA0027 per design document) + public static readonly DiagnosticDescriptor AuthorizerMissingName = new DiagnosticDescriptor( + id: "AWSLambda0120", + title: "Authorizer Name Required", + messageFormat: "The Name property is required on [{0}] attribute", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor HttpApiAuthorizerNotFound = new DiagnosticDescriptor( + id: "AWSLambda0121", + title: "HTTP API Authorizer Not Found", + messageFormat: "Authorizer '{0}' referenced in [HttpApi] attribute does not exist. Add [HttpApiAuthorizer(Name = \"{0}\")] to an authorizer function.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RestApiAuthorizerNotFound = new DiagnosticDescriptor( + id: "AWSLambda0122", + title: "REST API Authorizer Not Found", + messageFormat: "Authorizer '{0}' referenced in [RestApi] attribute does not exist. Add [RestApiAuthorizer(Name = \"{0}\")] to an authorizer function.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor HttpApiAuthorizerTypeMismatch = new DiagnosticDescriptor( + id: "AWSLambda0123", + title: "Authorizer Type Mismatch", + messageFormat: "Cannot use REST API authorizer '{0}' with [HttpApi] attribute. Use an [HttpApiAuthorizer] instead.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor RestApiAuthorizerTypeMismatch = new DiagnosticDescriptor( + id: "AWSLambda0124", + title: "Authorizer Type Mismatch", + messageFormat: "Cannot use HTTP API authorizer '{0}' with [RestApi] attribute. Use a [RestApiAuthorizer] instead.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor DuplicateAuthorizerName = new DiagnosticDescriptor( + id: "AWSLambda0125", + title: "Duplicate Authorizer Name", + messageFormat: "Duplicate authorizer name '{0}'. Authorizer names must be unique within the same API type.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidAuthorizerResultTtl = new DiagnosticDescriptor( + id: "AWSLambda0127", + title: "Invalid Result TTL", + messageFormat: "Invalid ResultTtlInSeconds '{0}'. Must be between 0 and 3600.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor AuthorizerPayloadVersionMismatch = new DiagnosticDescriptor( + id: "AWSLambda0128", + title: "Authorizer Payload Version Mismatch", + messageFormat: "The authorizer '{0}' uses AuthorizerPayloadFormatVersion {1} but the endpoint uses HttpApiVersion {2}. This may cause unexpected behavior.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor MissingLambdaFunctionAttribute = new DiagnosticDescriptor( + id: "AWSLambda0129", + title: "Missing LambdaFunction Attribute", + messageFormat: "Method has [{0}] attribute but is missing the required [LambdaFunction] attribute. Add [LambdaFunction] to this method.", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); } } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs index 3001b6144..5caca1cee 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs @@ -1,7 +1,9 @@ +using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics; using Amazon.Lambda.Annotations.SourceGenerator.Extensions; using Amazon.Lambda.Annotations.SourceGenerator.FileIO; using Amazon.Lambda.Annotations.SourceGenerator.Models; +using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; using Amazon.Lambda.Annotations.SourceGenerator.Templates; using Amazon.Lambda.Annotations.SourceGenerator.Writers; using Microsoft.CodeAnalysis; @@ -149,6 +151,17 @@ public void Execute(GeneratorExecutionContext context) configureMethodSymbol = configureHostBuilderMethodSymbol; } + // Check for methods that have secondary attributes (HttpApi, RestApi, HttpApiAuthorizer, etc.) + // but are missing the required [LambdaFunction] attribute + foreach (var (method, attributeName) in receiver.MethodsWithMissingLambdaFunction) + { + diagnosticReporter.Report(Diagnostic.Create( + DiagnosticDescriptors.MissingLambdaFunctionAttribute, + Location.Create(method.SyntaxTree, method.Span), + attributeName)); + foundFatalError = true; + } + var annotationReport = new AnnotationReport(); var templateHandler = new CloudFormationTemplateHandler(_fileManager, _directoryManager); @@ -168,6 +181,13 @@ public void Execute(GeneratorExecutionContext context) continue; } + // Check for authorizer attributes on this Lambda function + var authorizerModel = ExtractAuthorizerModel(lambdaMethodSymbol, lambdaFunctionModel.ResourceName); + if (authorizerModel != null) + { + annotationReport.Authorizers.Add(authorizerModel); + } + var template = new LambdaFunctionTemplate(lambdaFunctionModel); string sourceText; @@ -206,6 +226,23 @@ public void Execute(GeneratorExecutionContext context) context.AddSource("Program.g.cs", SourceText.From(executableAssembly.TransformText().ToEnvironmentLineEndings(), Encoding.UTF8, SourceHashAlgorithm.Sha256)); } + // Validate authorizer models and references before syncing CloudFormation template + foreach (var authorizerModel in annotationReport.Authorizers) + { + var attributeName = authorizerModel.AuthorizerType == AuthorizerType.HttpApi + ? "HttpApiAuthorizer" + : "RestApiAuthorizer"; + if (!ValidateAuthorizerModel(authorizerModel, attributeName, Location.None, diagnosticReporter)) + { + foundFatalError = true; + } + } + + if (!ValidateAuthorizerReferences(annotationReport, diagnosticReporter)) + { + foundFatalError = true; + } + // Run the CloudFormation sync if any LambdaMethods exists. Also run if no LambdaMethods exists but there is a // CloudFormation template in case orphaned functions in the template need to be removed. // Both checks are required because if there is no template but there are LambdaMethods the CF template the template will be created. @@ -296,5 +333,191 @@ public void Initialize(GeneratorInitializationContext context) // Register a syntax receiver that will be created for each generation pass context.RegisterForSyntaxNotifications(() => new SyntaxReceiver(_fileManager, _directoryManager)); } + + /// + /// Extracts authorizer model from method symbol if it has HttpApiAuthorizer or RestApiAuthorizer attribute. + /// + /// The method symbol to check for authorizer attributes + /// The CloudFormation resource name for the Lambda function + /// AuthorizerModel if an authorizer attribute is found, null otherwise + private static AuthorizerModel ExtractAuthorizerModel(IMethodSymbol methodSymbol, string lambdaResourceName) + { + foreach (var attribute in methodSymbol.GetAttributes()) + { + var attributeFullName = attribute.AttributeClass?.ToDisplayString(); + + AuthorizerModel model = null; + + if (attributeFullName == TypeFullNames.HttpApiAuthorizerAttribute) + { + model = HttpApiAuthorizerAttributeBuilder.BuildModel(attribute, lambdaResourceName); + } + else if (attributeFullName == TypeFullNames.RestApiAuthorizerAttribute) + { + model = RestApiAuthorizerAttributeBuilder.BuildModel(attribute, lambdaResourceName); + } + + if (model != null) + { + // The authorizer name is always derived from the method name + model.Name = methodSymbol.Name; + return model; + } + } + + return null; + } + + /// + /// Validates an authorizer model. + /// + /// The authorizer model to validate + /// The name of the attribute for error messages + /// The location of the method for diagnostic reporting + /// The diagnostic reporter for validation errors + /// True if valid, false otherwise + private static bool ValidateAuthorizerModel(AuthorizerModel model, string attributeName, Location methodLocation, DiagnosticReporter diagnosticReporter) + { + var isValid = true; + + // Validate Name is provided + if (string.IsNullOrEmpty(model.Name)) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.AuthorizerMissingName, methodLocation, attributeName)); + isValid = false; + } + + // Validate ResultTtlInSeconds + if (model.ResultTtlInSeconds < 0 || model.ResultTtlInSeconds > 3600) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.InvalidAuthorizerResultTtl, methodLocation, model.ResultTtlInSeconds.ToString())); + isValid = false; + } + + return isValid; + } + + /// + /// Validates authorizer references in lambda functions. + /// + /// The annotation report containing all functions and authorizers + /// The diagnostic reporter for validation errors + /// True if all authorizer references are valid, false otherwise + private static bool ValidateAuthorizerReferences(AnnotationReport annotationReport, DiagnosticReporter diagnosticReporter) + { + var isValid = true; + + // Build lookups for authorizers by type + var httpApiAuthorizers = annotationReport.Authorizers + .Where(a => a.AuthorizerType == AuthorizerType.HttpApi) + .ToDictionary(a => a.Name, a => a); + var restApiAuthorizers = annotationReport.Authorizers + .Where(a => a.AuthorizerType == AuthorizerType.RestApi) + .ToDictionary(a => a.Name, a => a); + + // Check for duplicate authorizer names within the same API type + var httpApiAuthorizerNames = annotationReport.Authorizers + .Where(a => a.AuthorizerType == AuthorizerType.HttpApi) + .GroupBy(a => a.Name) + .Where(g => g.Count() > 1) + .Select(g => g.Key); + + foreach (var duplicateName in httpApiAuthorizerNames) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.DuplicateAuthorizerName, Location.None, duplicateName)); + isValid = false; + } + + var restApiAuthorizerNames = annotationReport.Authorizers + .Where(a => a.AuthorizerType == AuthorizerType.RestApi) + .GroupBy(a => a.Name) + .Where(g => g.Count() > 1) + .Select(g => g.Key); + + foreach (var duplicateName in restApiAuthorizerNames) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.DuplicateAuthorizerName, Location.None, duplicateName)); + isValid = false; + } + + // Validate authorizer references in functions + foreach (var function in annotationReport.LambdaFunctions) + { + var authorizerName = function.Authorizer; + if (string.IsNullOrEmpty(authorizerName)) + { + continue; + } + + // Check if this function uses HttpApi or RestApi + var usesHttpApi = function.Attributes.Any(a => a is AttributeModel); + var usesRestApi = function.Attributes.Any(a => a is AttributeModel); + + if (usesHttpApi) + { + if (!httpApiAuthorizers.ContainsKey(authorizerName)) + { + // Check if it exists as a REST API authorizer (type mismatch) + if (restApiAuthorizers.ContainsKey(authorizerName)) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.HttpApiAuthorizerTypeMismatch, Location.None, authorizerName)); + } + else + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.HttpApiAuthorizerNotFound, Location.None, authorizerName)); + } + isValid = false; + } + else + { + // Check for payload format version mismatch between authorizer and endpoint + var matchedAuthorizer = httpApiAuthorizers[authorizerName]; + var httpApiAttr = function.Attributes + .OfType>() + .Select(a => a.Data) + .FirstOrDefault(); + + if (httpApiAttr != null) + { + var endpointVersion = httpApiAttr.Version; + var authorizerVersion = matchedAuthorizer.AuthorizerPayloadFormatVersion; + + // Compare: HttpApiVersion.V1 corresponds to AuthorizerPayloadFormatVersion.V1, etc. + if ((endpointVersion == HttpApiVersion.V1 && authorizerVersion != AuthorizerPayloadFormatVersion.V1) || + (endpointVersion == HttpApiVersion.V2 && authorizerVersion != AuthorizerPayloadFormatVersion.V2)) + { + var authorizerVersionStr = authorizerVersion == AuthorizerPayloadFormatVersion.V1 ? "1.0" : "2.0"; + var endpointVersionStr = endpointVersion == HttpApiVersion.V1 ? "V1" : "V2"; + diagnosticReporter.Report(Diagnostic.Create( + DiagnosticDescriptors.AuthorizerPayloadVersionMismatch, + Location.None, + authorizerName, + authorizerVersionStr, + endpointVersionStr)); + } + } + } + } + + if (usesRestApi) + { + if (!restApiAuthorizers.ContainsKey(authorizerName)) + { + // Check if it exists as an HTTP API authorizer (type mismatch) + if (httpApiAuthorizers.ContainsKey(authorizerName)) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.RestApiAuthorizerTypeMismatch, Location.None, authorizerName)); + } + else + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.RestApiAuthorizerNotFound, Location.None, authorizerName)); + } + isValid = false; + } + } + } + + return isValid; + } } } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AnnotationReport.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AnnotationReport.cs index dbd76b458..419817a14 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AnnotationReport.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AnnotationReport.cs @@ -9,6 +9,11 @@ public class AnnotationReport /// public IList LambdaFunctions { get; } = new List(); + /// + /// Collection of Lambda authorizers detected in the project + /// + public IList Authorizers { get; } = new List(); + /// /// Path to the CloudFormation template for the Lambda project /// diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAttributeBuilder.cs index 15a6767bf..d21abd928 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAttributeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAttributeBuilder.cs @@ -17,12 +17,14 @@ public static HttpApiAttribute Build(AttributeData att) var method = (LambdaHttpMethod)att.ConstructorArguments[0].Value; var template = att.ConstructorArguments[1].Value as string; var version = att.NamedArguments.FirstOrDefault(arg => arg.Key == "Version").Value.Value; + var authorizer = att.NamedArguments.FirstOrDefault(arg => arg.Key == "Authorizer").Value.Value as string; var data = new HttpApiAttribute(method, template) { - Version = version == null ? HttpApiVersion.V2 : (HttpApiVersion)version + Version = version == null ? HttpApiVersion.V2 : (HttpApiVersion)version, + Authorizer = authorizer }; return data; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs new file mode 100644 index 000000000..d5c1776a6 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs @@ -0,0 +1,76 @@ +using System.Linq; +using Amazon.Lambda.Annotations.APIGateway; +using Microsoft.CodeAnalysis; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes +{ + /// + /// Builder for . + /// + public static class HttpApiAuthorizerAttributeBuilder + { + /// + /// Builds an from the Roslyn attribute data. + /// + /// The attribute data from Roslyn + /// The populated attribute instance + public static HttpApiAuthorizerAttribute Build(AttributeData att) + { + var attribute = new HttpApiAuthorizerAttribute(); + + foreach (var namedArg in att.NamedArguments) + { + switch (namedArg.Key) + { + case nameof(HttpApiAuthorizerAttribute.IdentityHeader): + attribute.IdentityHeader = namedArg.Value.Value as string ?? "Authorization"; + break; + case nameof(HttpApiAuthorizerAttribute.EnableSimpleResponses): + attribute.EnableSimpleResponses = namedArg.Value.Value is bool val ? val : true; + break; + case nameof(HttpApiAuthorizerAttribute.AuthorizerPayloadFormatVersion): + attribute.AuthorizerPayloadFormatVersion = namedArg.Value.Value is int enumVal + ? (AuthorizerPayloadFormatVersion)enumVal + : AuthorizerPayloadFormatVersion.V2; + break; + case nameof(HttpApiAuthorizerAttribute.ResultTtlInSeconds): + attribute.ResultTtlInSeconds = namedArg.Value.Value is int ttl ? ttl : 0; + break; + } + } + + return attribute; + } + + /// + /// Builds an from the attribute and lambda function resource name. + /// + /// The attribute data from Roslyn + /// The CloudFormation resource name for the Lambda function + /// The populated authorizer model + public static AuthorizerModel BuildModel(AttributeData att, string lambdaResourceName) + { + var attribute = Build(att); + return BuildModel(attribute, lambdaResourceName); + } + + /// + /// Builds an from the attribute and lambda function resource name. + /// + /// The parsed attribute + /// The CloudFormation resource name for the Lambda function + /// The populated authorizer model + public static AuthorizerModel BuildModel(HttpApiAuthorizerAttribute attribute, string lambdaResourceName) + { + return new AuthorizerModel + { + LambdaResourceName = lambdaResourceName, + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = attribute.IdentityHeader, + ResultTtlInSeconds = attribute.ResultTtlInSeconds, + EnableSimpleResponses = attribute.EnableSimpleResponses, + AuthorizerPayloadFormatVersion = attribute.AuthorizerPayloadFormatVersion + }; + } + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAttributeBuilder.cs index 1a9f44680..44ab96a10 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAttributeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAttributeBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Amazon.Lambda.Annotations.APIGateway; using Microsoft.CodeAnalysis; @@ -18,10 +19,14 @@ public static RestApiAttribute Build(AttributeData att) var method = (LambdaHttpMethod)att.ConstructorArguments[0].Value; var template = att.ConstructorArguments[1].Value as string; + var authorizer = att.NamedArguments.FirstOrDefault(arg => arg.Key == "Authorizer").Value.Value as string; - var data = new RestApiAttribute(method, template); + var data = new RestApiAttribute(method, template) + { + Authorizer = authorizer + }; return data; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs new file mode 100644 index 000000000..881a43bff --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs @@ -0,0 +1,72 @@ +using System.Linq; +using Amazon.Lambda.Annotations.APIGateway; +using Microsoft.CodeAnalysis; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes +{ + /// + /// Builder for . + /// + public static class RestApiAuthorizerAttributeBuilder + { + /// + /// Builds a from the Roslyn attribute data. + /// + /// The attribute data from Roslyn + /// The populated attribute instance + public static RestApiAuthorizerAttribute Build(AttributeData att) + { + var attribute = new RestApiAuthorizerAttribute(); + + foreach (var namedArg in att.NamedArguments) + { + switch (namedArg.Key) + { + case nameof(RestApiAuthorizerAttribute.IdentityHeader): + attribute.IdentityHeader = namedArg.Value.Value as string ?? "Authorization"; + break; + case nameof(RestApiAuthorizerAttribute.Type): + attribute.Type = namedArg.Value.Value is int typeVal + ? (RestApiAuthorizerType)typeVal + : RestApiAuthorizerType.Token; + break; + case nameof(RestApiAuthorizerAttribute.ResultTtlInSeconds): + attribute.ResultTtlInSeconds = namedArg.Value.Value is int ttl ? ttl : 0; + break; + } + } + + return attribute; + } + + /// + /// Builds an from the attribute and lambda function resource name. + /// + /// The attribute data from Roslyn + /// The CloudFormation resource name for the Lambda function + /// The populated authorizer model + public static AuthorizerModel BuildModel(AttributeData att, string lambdaResourceName) + { + var attribute = Build(att); + return BuildModel(attribute, lambdaResourceName); + } + + /// + /// Builds an from the attribute and lambda function resource name. + /// + /// The parsed attribute + /// The CloudFormation resource name for the Lambda function + /// The populated authorizer model + public static AuthorizerModel BuildModel(RestApiAuthorizerAttribute attribute, string lambdaResourceName) + { + return new AuthorizerModel + { + LambdaResourceName = lambdaResourceName, + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = attribute.IdentityHeader, + ResultTtlInSeconds = attribute.ResultTtlInSeconds, + RestApiAuthorizerType = attribute.Type + }; + } + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AuthorizerModel.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AuthorizerModel.cs new file mode 100644 index 000000000..ea6ee3833 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AuthorizerModel.cs @@ -0,0 +1,99 @@ +using Amazon.Lambda.Annotations.APIGateway; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models +{ + /// + /// Enumeration for the type of API Gateway authorizer + /// + public enum AuthorizerType + { + /// + /// HTTP API (API Gateway V2) authorizer + /// + HttpApi, + + /// + /// REST API (API Gateway V1) authorizer + /// + RestApi + } + + /// + /// Model representing a Lambda Authorizer configuration + /// + public class AuthorizerModel + { + /// + /// Unique name to identify this authorizer. Functions reference this name. + /// + public string Name { get; set; } + + /// + /// The CloudFormation resource name for the Lambda function that implements this authorizer. + /// This is derived from the LambdaFunctionAttribute's ResourceName or the generated method name. + /// + public string LambdaResourceName { get; set; } + + /// + /// The type of API Gateway authorizer (HTTP API or REST API) + /// + public AuthorizerType AuthorizerType { get; set; } + + /// + /// Header name to use as identity source. + /// + public string IdentityHeader { get; set; } + + /// + /// TTL in seconds for caching authorizer results. + /// + public int ResultTtlInSeconds { get; set; } + + // HTTP API specific properties + + /// + /// Whether to use simple responses (IsAuthorized: true/false) or IAM policy responses. + /// Only applicable for HTTP API authorizers. + /// + public bool EnableSimpleResponses { get; set; } + + /// + /// Authorizer payload format version. + /// Only applicable for HTTP API authorizers. + /// + public AuthorizerPayloadFormatVersion AuthorizerPayloadFormatVersion { get; set; } + + // REST API specific properties + + /// + /// Type of REST API authorizer: Token or Request. + /// Only applicable for REST API authorizers. + /// + public RestApiAuthorizerType RestApiAuthorizerType { get; set; } + + /// + /// Gets the identity source string formatted for CloudFormation. + /// + /// The formatted identity source string + public string GetIdentitySource() + { + if (AuthorizerType == AuthorizerType.HttpApi) + { + return $"$request.header.{IdentityHeader}"; + } + else + { + return $"method.request.header.{IdentityHeader}"; + } + } + + /// + /// Gets the CloudFormation resource name for this authorizer. + /// + /// The CloudFormation resource name + public string GetAuthorizerResourceName() + { + return $"{Name}Authorizer"; + } + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ILambdaFunctionSerializable.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ILambdaFunctionSerializable.cs index 6cb6161fa..d0b7fd20f 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ILambdaFunctionSerializable.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ILambdaFunctionSerializable.cs @@ -77,5 +77,11 @@ public interface ILambdaFunctionSerializable /// The assembly version of the Amazon.Lambda.Annotations.SourceGenerator package. /// string SourceGeneratorVersion { get; set; } + + /// + /// The name of the authorizer protecting this Lambda function endpoint. + /// Null or empty for public (unauthenticated) endpoints. + /// + string Authorizer { get; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModel.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModel.cs index fad1f5f53..fd47d2dab 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModel.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModel.cs @@ -78,9 +78,12 @@ public class LambdaFunctionModel : ILambdaFunctionSerializable /// public string SourceGeneratorVersion { get; set; } + /// + public string Authorizer { get; set; } + /// /// Indicates if the model is valid. /// public bool IsValid { get; set; } } -} \ No newline at end of file +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModelBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModelBuilder.cs index e828bee3d..db7fa4d67 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModelBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModelBuilder.cs @@ -1,7 +1,9 @@ using System; using System.Linq; +using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics; using Amazon.Lambda.Annotations.SourceGenerator.Extensions; +using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; using Amazon.Lambda.Annotations.SourceGenerator.Validation; using Microsoft.CodeAnalysis; @@ -40,11 +42,31 @@ private static LambdaFunctionModel Build(IMethodSymbol lambdaMethodSymbol, IMeth ?.Version.ToString(), IsExecutable = isExecutable, Runtime = runtime, + Authorizer = GetAuthorizerFromAttributes(lambdaMethod) }; return model; } + /// + /// Extracts the Authorizer name from HttpApi or RestApi attributes. + /// + private static string GetAuthorizerFromAttributes(LambdaMethodModel lambdaMethod) + { + foreach (var attribute in lambdaMethod.Attributes) + { + if (attribute is AttributeModel httpApiAttributeModel) + { + return httpApiAttributeModel.Data.Authorizer; + } + if (attribute is AttributeModel restApiAttributeModel) + { + return restApiAttributeModel.Data.Authorizer; + } + } + return null; + } + private static LambdaSerializerInfo GetSerializerInfoAttribute(GeneratorExecutionContext context, IMethodSymbol methodModel) { var serializerString = TypeFullNames.DefaultLambdaSerializer; diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs index ddc167b1e..a5d7ce9ab 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs @@ -11,12 +11,31 @@ namespace Amazon.Lambda.Annotations.SourceGenerator internal class SyntaxReceiver : ISyntaxContextReceiver { + /// + /// Secondary attribute names that require the [LambdaFunction] attribute to also be present. + /// The key is the attribute class name, the value is the user-friendly name for diagnostics. + /// + private static readonly Dictionary _secondaryAttributeNames = new Dictionary + { + { "HttpApiAuthorizerAttribute", "HttpApiAuthorizer" }, + { "RestApiAuthorizerAttribute", "RestApiAuthorizer" }, + { "HttpApiAttribute", "HttpApi" }, + { "RestApiAttribute", "RestApi" }, + { "SQSEventAttribute", "SQSEvent" } + }; + public List LambdaMethods { get; } = new List(); public List StartupClasses { get; private set; } = new List(); public List MethodDeclarations { get; } = new List(); + /// + /// Methods that have a secondary Lambda annotation attribute but are missing [LambdaFunction]. + /// Each entry is a tuple of the method syntax, its location, and the friendly name of the secondary attribute found. + /// + public List<(MethodDeclarationSyntax Method, string AttributeName)> MethodsWithMissingLambdaFunction { get; } = new List<(MethodDeclarationSyntax, string)>(); + /// /// Path to the directory containing the .csproj file /// @@ -57,10 +76,24 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) var methodSymbol = ModelExtensions.GetDeclaredSymbol( context.SemanticModel, methodDeclarationSyntax); - if (methodSymbol.GetAttributes().Any(attr => attr.AttributeClass.Name == nameof(LambdaFunctionAttribute))) + var attributes = methodSymbol.GetAttributes(); + if (attributes.Any(attr => attr.AttributeClass.Name == nameof(LambdaFunctionAttribute))) { LambdaMethods.Add(methodDeclarationSyntax); } + else + { + // Check if the method has a secondary attribute without [LambdaFunction] + foreach (var attr in attributes) + { + var attrName = attr.AttributeClass?.Name; + if (attrName != null && _secondaryAttributeNames.TryGetValue(attrName, out var friendlyName)) + { + MethodsWithMissingLambdaFunction.Add((methodDeclarationSyntax, friendlyName)); + break; // Only report once per method + } + } + } } // any class with at least one attribute is a candidate of Startup class diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs index a91cfaefa..56582a2e7 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs @@ -33,6 +33,9 @@ public static class TypeFullNames public const string FromRouteAttribute = "Amazon.Lambda.Annotations.APIGateway.FromRouteAttribute"; public const string FromCustomAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.FromCustomAuthorizerAttribute"; + public const string HttpApiAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.HttpApiAuthorizerAttribute"; + public const string RestApiAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.RestApiAuthorizerAttribute"; + public const string SQSEvent = "Amazon.Lambda.SQSEvents.SQSEvent"; public const string SQSBatchResponse = "Amazon.Lambda.SQSEvents.SQSBatchResponse"; public const string SQSEventAttribute = "Amazon.Lambda.Annotations.SQS.SQSEventAttribute"; diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs index 993a312ed..4c41bdbc5 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs @@ -10,6 +10,7 @@ using System.ComponentModel; using System.Linq; using System.Reflection; +using AuthorizerType = Amazon.Lambda.Annotations.SourceGenerator.Models.AuthorizerType; namespace Amazon.Lambda.Annotations.SourceGenerator.Writers { @@ -25,6 +26,8 @@ public class CloudFormationWriter : IAnnotationReportWriter private const string PARAMETERS = "Parameters"; private const string GET_ATTRIBUTE = "Fn::GetAtt"; private const string REF = "Ref"; + private const string HTTP_API_RESOURCE_NAME = "AnnotationsHttpApi"; + private const string REST_API_RESOURCE_NAME = "AnnotationsRestApi"; // Constants related to the message we append to the CloudFormation template description private const string BASE_DESCRIPTION = "This template is partially managed by Amazon.Lambda.Annotations"; @@ -59,17 +62,33 @@ public void ApplyReport(AnnotationReport report) ProcessTemplateDescription(report); + // Build authorizer lookup for processing events with Auth configuration + var authorizerLookup = new Dictionary(StringComparer.Ordinal); + foreach (var authorizer in report.Authorizers) + { + if (authorizerLookup.ContainsKey(authorizer.Name)) + { + _diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.DuplicateAuthorizerName, Location.None, authorizer.Name)); + continue; + } + authorizerLookup[authorizer.Name] = authorizer; + } + + // Process authorizers first (they need to exist before functions reference them) + ProcessAuthorizers(report.Authorizers); + var processedLambdaFunctions = new HashSet(); foreach (var lambdaFunction in report.LambdaFunctions) { if (!ShouldProcessLambdaFunction(lambdaFunction)) continue; - ProcessLambdaFunction(lambdaFunction, relativeProjectUri); + ProcessLambdaFunction(lambdaFunction, relativeProjectUri, authorizerLookup); processedLambdaFunctions.Add(lambdaFunction.ResourceName); } RemoveOrphanedLambdaFunctions(processedLambdaFunctions); + RemoveOrphanedAuthorizers(report.Authorizers); var content = _templateWriter.GetContent(); _fileManager.WriteAllText(report.CloudFormationTemplatePath, content); @@ -98,7 +117,7 @@ private bool ShouldProcessLambdaFunction(ILambdaFunctionSerializable lambdaFunct /// Captures different properties specified by and attributes specified by /// and writes it to the serverless template. /// - private void ProcessLambdaFunction(ILambdaFunctionSerializable lambdaFunction, string relativeProjectUri) + private void ProcessLambdaFunction(ILambdaFunctionSerializable lambdaFunction, string relativeProjectUri, Dictionary authorizerLookup) { var lambdaFunctionPath = $"Resources.{lambdaFunction.ResourceName}"; var propertiesPath = $"{lambdaFunctionPath}.Properties"; @@ -107,7 +126,7 @@ private void ProcessLambdaFunction(ILambdaFunctionSerializable lambdaFunction, s ApplyLambdaFunctionDefaults(lambdaFunctionPath, propertiesPath, lambdaFunction.Runtime); ProcessLambdaFunctionProperties(lambdaFunction, propertiesPath, relativeProjectUri); - ProcessLambdaFunctionEventAttributes(lambdaFunction); + ProcessLambdaFunctionEventAttributes(lambdaFunction, authorizerLookup); } /// @@ -180,7 +199,7 @@ private void ProcessPackageTypeProperty(ILambdaFunctionSerializable lambdaFuncti /// It also removes all events that exist in the serverless template but were not encountered during the current source generation pass. /// All events are specified under 'Resources.FUNCTION_NAME.Properties.Events' path. /// - private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable lambdaFunction) + private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable lambdaFunction, Dictionary authorizerLookup) { var currentSyncedEvents = new List(); var currentSyncedEventProperties = new Dictionary>(); @@ -191,11 +210,11 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la switch (attributeModel) { case AttributeModel httpApiAttributeModel: - eventName = ProcessHttpApiAttribute(lambdaFunction, httpApiAttributeModel.Data, currentSyncedEventProperties); + eventName = ProcessHttpApiAttribute(lambdaFunction, httpApiAttributeModel.Data, currentSyncedEventProperties, authorizerLookup); currentSyncedEvents.Add(eventName); break; case AttributeModel restApiAttributeModel: - eventName = ProcessRestApiAttribute(lambdaFunction, restApiAttributeModel.Data, currentSyncedEventProperties); + eventName = ProcessRestApiAttribute(lambdaFunction, restApiAttributeModel.Data, currentSyncedEventProperties, authorizerLookup); currentSyncedEvents.Add(eventName); break; case AttributeModel sqsAttributeModel: @@ -211,7 +230,7 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la /// /// Writes all properties associated with to the serverless template. /// - private string ProcessRestApiAttribute(ILambdaFunctionSerializable lambdaFunction, RestApiAttribute restApiAttribute, Dictionary> syncedEventProperties) + private string ProcessRestApiAttribute(ILambdaFunctionSerializable lambdaFunction, RestApiAttribute restApiAttribute, Dictionary> syncedEventProperties, Dictionary authorizerLookup) { var eventName = $"Root{restApiAttribute.Method}"; var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}"; @@ -220,13 +239,27 @@ private string ProcessRestApiAttribute(ILambdaFunctionSerializable lambdaFunctio SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Path", restApiAttribute.Template); SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Method", restApiAttribute.Method.ToString().ToUpper()); + // Set Auth configuration if authorizer is specified + // Use the authorizer name directly (not a CloudFormation Ref) since authorizers are defined inline in the API + // Also set RestApiId to link to our explicit ServerlessRestApi resource where the authorizer is defined + if (!string.IsNullOrEmpty(restApiAttribute.Authorizer) && authorizerLookup.TryGetValue(restApiAttribute.Authorizer, out var authorizer)) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Auth.Authorizer", authorizer.Name); + } + + // Always reference the shared API resource if it exists, so all REST API functions share one endpoint + if (_templateWriter.Exists($"Resources.{REST_API_RESOURCE_NAME}")) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"RestApiId.{REF}", REST_API_RESOURCE_NAME); + } + return eventName; } /// /// Writes all properties associated with to the serverless template. /// - private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunction, HttpApiAttribute httpApiAttribute, Dictionary> syncedEventProperties) + private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunction, HttpApiAttribute httpApiAttribute, Dictionary> syncedEventProperties, Dictionary authorizerLookup) { var eventName = $"Root{httpApiAttribute.Method}"; var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}"; @@ -240,9 +273,261 @@ private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunctio if (httpApiAttribute.Version == HttpApiVersion.V1) SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "PayloadFormatVersion", "1.0"); + // Set Auth configuration if authorizer is specified + // Use the authorizer name directly (not a CloudFormation Ref) since authorizers are defined inline in the API + // Also set ApiId to link to our explicit ServerlessHttpApi resource where the authorizer is defined + if (!string.IsNullOrEmpty(httpApiAttribute.Authorizer) && authorizerLookup.TryGetValue(httpApiAttribute.Authorizer, out var authorizer)) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Auth.Authorizer", authorizer.Name); + } + + // Always reference the shared API resource if it exists, so all HTTP API functions share one endpoint + if (_templateWriter.Exists($"Resources.{HTTP_API_RESOURCE_NAME}")) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"ApiId.{REF}", HTTP_API_RESOURCE_NAME); + } + return eventName; } + /// + /// Processes all authorizers and writes them to the serverless template as inline authorizers within the API resources. + /// AWS SAM expects authorizers to be defined within the Auth.Authorizers property of AWS::Serverless::HttpApi or AWS::Serverless::Api resources. + /// + private void ProcessAuthorizers(IList authorizers) + { + // Group authorizers by type + var httpApiAuthorizers = authorizers.Where(a => a.AuthorizerType == AuthorizerType.HttpApi).ToList(); + var restApiAuthorizers = authorizers.Where(a => a.AuthorizerType == AuthorizerType.RestApi).ToList(); + + // Process HTTP API authorizers (add to AnnotationsHttpApi resource) + if (httpApiAuthorizers.Any()) + { + ProcessHttpApiAuthorizers(httpApiAuthorizers); + } + + // Process REST API authorizers (add to AnnotationsRestApi resource) + if (restApiAuthorizers.Any()) + { + ProcessRestApiAuthorizers(restApiAuthorizers); + } + } + + /// + /// Writes HTTP API (API Gateway V2) authorizers to the AnnotationsHttpApi resource. + /// SAM expects authorizers to be defined inline in the Auth.Authorizers property. + /// + private void ProcessHttpApiAuthorizers(IList authorizers) + { + const string httpApiResourcePath = "Resources." + HTTP_API_RESOURCE_NAME; + + // Create the AnnotationsHttpApi resource if it doesn't exist + if (!_templateWriter.Exists(httpApiResourcePath)) + { + _templateWriter.SetToken($"{httpApiResourcePath}.Type", "AWS::Serverless::HttpApi"); + _templateWriter.SetToken($"{httpApiResourcePath}.Metadata.Tool", CREATION_TOOL); + } + + // Add each authorizer to the Auth.Authorizers map + foreach (var authorizer in authorizers) + { + var authorizerPath = $"{httpApiResourcePath}.Properties.Auth.Authorizers.{authorizer.Name}"; + + // FunctionArn - Reference to the Lambda function ARN + _templateWriter.SetToken($"{authorizerPath}.FunctionArn.{GET_ATTRIBUTE}", new List { authorizer.LambdaResourceName, "Arn" }, TokenType.List); + + // AuthorizerPayloadFormatVersion + var payloadFormatVersionString = authorizer.AuthorizerPayloadFormatVersion == AuthorizerPayloadFormatVersion.V1 ? "1.0" : "2.0"; + _templateWriter.SetToken($"{authorizerPath}.AuthorizerPayloadFormatVersion", payloadFormatVersionString); + + // EnableSimpleResponses + _templateWriter.SetToken($"{authorizerPath}.EnableSimpleResponses", authorizer.EnableSimpleResponses); + + // Identity.Headers - The header to use for identity source + _templateWriter.SetToken($"{authorizerPath}.Identity.Headers", new List { authorizer.IdentityHeader }, TokenType.List); + + // EnableFunctionDefaultPermissions tells SAM to automatically create the AWS::Lambda::Permission + // for the authorizer Lambda function. Unlike AWS::Serverless::Api (REST API), AWS::Serverless::HttpApi + // does NOT automatically create invoke permissions for Lambda authorizers defined in Auth.Authorizers. + // https://github.com/aws/serverless-application-model/issues/2933 + _templateWriter.SetToken($"{authorizerPath}.EnableFunctionDefaultPermissions", true); + + // AuthorizerResultTtlInSeconds - always write this value so SAM does not apply its default TTL. + // A value of 0 disables caching, which matches the attribute default. + _templateWriter.SetToken($"{authorizerPath}.AuthorizerResultTtlInSeconds", authorizer.ResultTtlInSeconds); + } + } + + /// + /// Writes REST API (API Gateway V1) authorizers to the AnnotationsRestApi resource. + /// SAM expects authorizers to be defined inline in the Auth.Authorizers property. + /// + private void ProcessRestApiAuthorizers(IList authorizers) + { + const string restApiResourcePath = "Resources." + REST_API_RESOURCE_NAME; + + // Create the AnnotationsRestApi resource if it doesn't exist + if (!_templateWriter.Exists(restApiResourcePath)) + { + _templateWriter.SetToken($"{restApiResourcePath}.Type", "AWS::Serverless::Api"); + // REST API requires explicit stage name + _templateWriter.SetToken($"{restApiResourcePath}.Properties.StageName", "Prod"); + _templateWriter.SetToken($"{restApiResourcePath}.Metadata.Tool", CREATION_TOOL); + } + + // Add each authorizer to the Auth.Authorizers map + foreach (var authorizer in authorizers) + { + var authorizerPath = $"{restApiResourcePath}.Properties.Auth.Authorizers.{authorizer.Name}"; + + // FunctionArn - Reference to the Lambda function ARN using GetAtt + _templateWriter.SetToken($"{authorizerPath}.FunctionArn.{GET_ATTRIBUTE}", new List { authorizer.LambdaResourceName, "Arn" }, TokenType.List); + + // Identity.Header - The header to use for identity source + _templateWriter.SetToken($"{authorizerPath}.Identity.Header", authorizer.IdentityHeader); + + // FunctionPayloadType - TOKEN or REQUEST + if (authorizer.RestApiAuthorizerType == RestApiAuthorizerType.Token) + { + _templateWriter.SetToken($"{authorizerPath}.FunctionPayloadType", "TOKEN"); + } + else + { + _templateWriter.SetToken($"{authorizerPath}.FunctionPayloadType", "REQUEST"); + } + + // AuthorizerResultTtlInSeconds - cache TTL for authorizer result (only when valid) + // API Gateway REST Lambda authorizer TTL must be between 1 and 3600 seconds. + var resultTtlInSeconds = authorizer.ResultTtlInSeconds; + const int minTtlInSeconds = 1; + const int maxTtlInSeconds = 3600; + + if (resultTtlInSeconds >= minTtlInSeconds && + resultTtlInSeconds <= maxTtlInSeconds) + { + _templateWriter.SetToken($"{authorizerPath}.AuthorizerResultTtlInSeconds", resultTtlInSeconds); + } + } + } + + /// + /// Removes orphaned authorizers from the serverless template. + /// Authorizers are now defined inline within the API resources (AnnotationsHttpApi and AnnotationsRestApi). + /// This method removes authorizers that were created by Lambda Annotations but no longer exist in the current compilation. + /// It also cleans up legacy standalone authorizer resources (AWS::ApiGatewayV2::Authorizer, AWS::ApiGateway::Authorizer) + /// and their associated Lambda permissions. + /// + private void RemoveOrphanedAuthorizers(IList currentAuthorizers) + { + if (!_templateWriter.Exists("Resources")) + { + return; + } + + // Get current authorizer names by type + var currentHttpApiAuthorizerNames = new HashSet( + currentAuthorizers.Where(a => a.AuthorizerType == AuthorizerType.HttpApi).Select(a => a.Name)); + var currentRestApiAuthorizerNames = new HashSet( + currentAuthorizers.Where(a => a.AuthorizerType == AuthorizerType.RestApi).Select(a => a.Name)); + + // Clean up orphaned inline authorizers in AnnotationsHttpApi + const string httpApiAuthorizersPath = "Resources." + HTTP_API_RESOURCE_NAME + ".Properties.Auth.Authorizers"; + if (_templateWriter.Exists(httpApiAuthorizersPath)) + { + var httpApiCreationTool = _templateWriter.GetToken($"Resources.{HTTP_API_RESOURCE_NAME}.Metadata.Tool", string.Empty); + if (string.Equals(httpApiCreationTool, CREATION_TOOL, StringComparison.Ordinal)) + { + var existingAuthorizerNames = _templateWriter.GetKeys(httpApiAuthorizersPath); + foreach (var authorizerName in existingAuthorizerNames) + { + if (!currentHttpApiAuthorizerNames.Contains(authorizerName)) + { + _templateWriter.RemoveToken($"{httpApiAuthorizersPath}.{authorizerName}"); + } + } + + // Clean up empty Auth structure + _templateWriter.RemoveTokenIfNullOrEmpty(httpApiAuthorizersPath); + _templateWriter.RemoveTokenIfNullOrEmpty($"Resources.{HTTP_API_RESOURCE_NAME}.Properties.Auth"); + } + } + + // Clean up orphaned inline authorizers in AnnotationsRestApi + const string restApiAuthorizersPath = "Resources." + REST_API_RESOURCE_NAME + ".Properties.Auth.Authorizers"; + if (_templateWriter.Exists(restApiAuthorizersPath)) + { + var restApiCreationTool = _templateWriter.GetToken($"Resources.{REST_API_RESOURCE_NAME}.Metadata.Tool", string.Empty); + if (string.Equals(restApiCreationTool, CREATION_TOOL, StringComparison.Ordinal)) + { + var existingAuthorizerNames = _templateWriter.GetKeys(restApiAuthorizersPath); + foreach (var authorizerName in existingAuthorizerNames) + { + if (!currentRestApiAuthorizerNames.Contains(authorizerName)) + { + _templateWriter.RemoveToken($"{restApiAuthorizersPath}.{authorizerName}"); + } + } + + // Clean up empty Auth structure + _templateWriter.RemoveTokenIfNullOrEmpty(restApiAuthorizersPath); + _templateWriter.RemoveTokenIfNullOrEmpty($"Resources.{REST_API_RESOURCE_NAME}.Properties.Auth"); + } + } + + // Clean up legacy standalone authorizer resources and permissions from older versions + var toRemove = new List(); + foreach (var resourceName in _templateWriter.GetKeys("Resources")) + { + var resourcePath = $"Resources.{resourceName}"; + var type = _templateWriter.GetToken($"{resourcePath}.Type", string.Empty); + var creationTool = _templateWriter.GetToken($"{resourcePath}.Metadata.Tool", string.Empty); + + if (!string.Equals(creationTool, CREATION_TOOL, StringComparison.Ordinal)) + { + continue; + } + + // Remove legacy standalone authorizer resources + if (string.Equals(type, "AWS::ApiGatewayV2::Authorizer", StringComparison.Ordinal) || + string.Equals(type, "AWS::ApiGateway::Authorizer", StringComparison.Ordinal)) + { + toRemove.Add(resourceName); + } + + // Remove legacy authorizer Lambda permissions + if (string.Equals(type, "AWS::Lambda::Permission", StringComparison.Ordinal) && + resourceName.EndsWith("AuthorizerPermission")) + { + toRemove.Add(resourceName); + } + } + + foreach (var resourceName in toRemove) + { + _templateWriter.RemoveToken($"Resources.{resourceName}"); + } + + // Remove the entire AnnotationsHttpApi resource if it was created by us and no longer has any HTTP API authorizers + if (!currentHttpApiAuthorizerNames.Any() + && _templateWriter.Exists($"Resources.{HTTP_API_RESOURCE_NAME}") + && string.Equals( + _templateWriter.GetToken($"Resources.{HTTP_API_RESOURCE_NAME}.Metadata.Tool", string.Empty), + CREATION_TOOL, StringComparison.Ordinal)) + { + _templateWriter.RemoveToken($"Resources.{HTTP_API_RESOURCE_NAME}"); + } + + // Remove the entire AnnotationsRestApi resource if it was created by us and no longer has any REST API authorizers + if (!currentRestApiAuthorizerNames.Any() + && _templateWriter.Exists($"Resources.{REST_API_RESOURCE_NAME}") + && string.Equals( + _templateWriter.GetToken($"Resources.{REST_API_RESOURCE_NAME}.Metadata.Tool", string.Empty), + CREATION_TOOL, StringComparison.Ordinal)) + { + _templateWriter.RemoveToken($"Resources.{REST_API_RESOURCE_NAME}"); + } + } + /// /// Writes all properties associated with to the serverless template. /// diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/AuthorizerPayloadFormatVersion.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/AuthorizerPayloadFormatVersion.cs new file mode 100644 index 000000000..8ca1360a9 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/AuthorizerPayloadFormatVersion.cs @@ -0,0 +1,19 @@ +namespace Amazon.Lambda.Annotations.APIGateway +{ + /// + /// The payload format version for an API Gateway HTTP API Lambda authorizer. + /// This maps to the AuthorizerPayloadFormatVersion property in the SAM template. + /// + public enum AuthorizerPayloadFormatVersion + { + /// + /// Payload format version 1.0 + /// + V1, + + /// + /// Payload format version 2.0 + /// + V2 + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAttribute.cs index eb4fc5a3a..954832cd6 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAttribute.cs @@ -22,6 +22,13 @@ public class HttpApiAttribute : Attribute /// public LambdaHttpMethod Method { get; set; } + /// + /// Name of the HTTP API Lambda authorizer to protect this endpoint. + /// Must match the Name property of an in this project. + /// Leave null/empty for public (unauthenticated) endpoints. + /// + public string Authorizer { get; set; } + /// /// Constructs a /// diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs new file mode 100644 index 000000000..17b7d0bf7 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs @@ -0,0 +1,65 @@ +using System; + +namespace Amazon.Lambda.Annotations.APIGateway +{ + /// + /// Marks this Lambda function as an HTTP API (API Gateway V2) authorizer. + /// Other functions can reference this authorizer using the HttpApi attribute's Authorizer property. + /// + /// + /// This attribute must be used in conjunction with the . + /// The authorizer function should return + /// when is true, or + /// when is false. + /// + /// + /// + /// // The authorizer name is automatically derived from the method name ("Authorize"). + /// [LambdaFunction] + /// [HttpApiAuthorizer] + /// public APIGatewayCustomAuthorizerV2SimpleResponse Authorize(APIGatewayCustomAuthorizerV2Request request) + /// { + /// // Validate token and return authorization response + /// } + /// + /// // Reference the authorizer using nameof for compile-time safety + /// [LambdaFunction] + /// [HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = nameof(Authorize))] + /// public string ProtectedEndpoint() + /// { + /// return "Hello, authenticated user!"; + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public class HttpApiAuthorizerAttribute : Attribute + { + /// + /// Header name to use as identity source. Defaults to "Authorization". + /// The generator translates this to "$request.header.{IdentityHeader}" for CloudFormation. + /// + public string IdentityHeader { get; set; } = "Authorization"; + + /// + /// Whether to use simple responses (IsAuthorized: true/false) or IAM policy responses. + /// Defaults to true for simpler implementation. + /// + /// + /// When true, the authorizer should return . + /// When false, the authorizer should return . + /// + public bool EnableSimpleResponses { get; set; } = true; + + /// + /// Authorizer payload format version. Defaults to . + /// Maps to the AuthorizerPayloadFormatVersion property in the SAM template. + /// + public AuthorizerPayloadFormatVersion AuthorizerPayloadFormatVersion { get; set; } = AuthorizerPayloadFormatVersion.V2; + + /// + /// TTL in seconds for caching authorizer results. 0 = no caching. Max = 3600. + /// Defaults to 0 (no caching). + /// + public int ResultTtlInSeconds { get; set; } = 0; + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAttribute.cs index 85e91a516..be1cbebac 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAttribute.cs @@ -19,6 +19,13 @@ public class RestApiAttribute : Attribute /// public LambdaHttpMethod Method { get; set; } + /// + /// Name of the REST API Lambda authorizer to protect this endpoint. + /// Must match the Name property of a in this project. + /// Leave null/empty for public (unauthenticated) endpoints. + /// + public string Authorizer { get; set; } + /// /// Constructs a /// diff --git a/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs new file mode 100644 index 000000000..b578e0d97 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs @@ -0,0 +1,72 @@ +using System; + +namespace Amazon.Lambda.Annotations.APIGateway +{ + /// + /// Type of REST API Lambda authorizer + /// + public enum RestApiAuthorizerType + { + /// + /// Token-based authorizer. Receives the token directly from the identity source. + /// The token is available via request.AuthorizationToken. + /// + Token, + + /// + /// Request-based authorizer. Receives the full request context including + /// headers, query strings, path parameters, and stage variables. + /// + Request + } + + /// + /// Marks this Lambda function as a REST API (API Gateway V1) authorizer. + /// Other functions can reference this authorizer using the RestApi attribute's Authorizer property. + /// + /// + /// This attribute must be used in conjunction with the . + /// The authorizer function should return . + /// + /// + /// + /// // The authorizer name is automatically derived from the method name ("Authorize"). + /// [LambdaFunction] + /// [RestApiAuthorizer(Type = RestApiAuthorizerType.Token)] + /// public APIGatewayCustomAuthorizerResponse Authorize(APIGatewayCustomAuthorizerRequest request) + /// { + /// var token = request.AuthorizationToken; + /// // Validate token and return IAM policy response + /// } + /// + /// // Reference the authorizer using nameof for compile-time safety + /// [LambdaFunction] + /// [RestApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = nameof(Authorize))] + /// public string ProtectedEndpoint() + /// { + /// return "Hello, authenticated user!"; + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public class RestApiAuthorizerAttribute : Attribute + { + /// + /// Header name to use as identity source. Defaults to "Authorization". + /// The generator translates this to "method.request.header.{IdentityHeader}" for CloudFormation. + /// + public string IdentityHeader { get; set; } = "Authorization"; + + /// + /// Type of authorizer: Token or Request. Defaults to Token. + /// Token authorizers receive just the token value; Request authorizers receive full request context. + /// + public RestApiAuthorizerType Type { get; set; } = RestApiAuthorizerType.Token; + + /// + /// TTL in seconds for caching authorizer results. 0 = no caching. Max = 3600. + /// Defaults to 0 (no caching). + /// + public int ResultTtlInSeconds { get; set; } = 0; + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations/README.md b/Libraries/src/Amazon.Lambda.Annotations/README.md index 93c0ac46b..681cc4be5 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/README.md +++ b/Libraries/src/Amazon.Lambda.Annotations/README.md @@ -19,6 +19,10 @@ Topics: - [Amazon API Gateway example](#amazon-api-gateway-example) - [Amazon S3 example](#amazon-s3-example) - [SQS Event Example](#sqs-event-example) + - [Custom Lambda Authorizer Example](#custom-lambda-authorizer-example) + - [HTTP API Authorizer](#http-api-authorizer) + - [REST API Authorizer](#rest-api-authorizer) + - [Authorizer Attribute Properties](#authorizer-attribute-properties) - [Getting build information](#getting-build-information) - [Lambda .NET Attributes Reference](#lambda-net-attributes-reference) - [Event Attributes](#event-attributes) @@ -847,6 +851,223 @@ The following SQS event source mapping will be generated for the `SQSMessageHand } ``` +## Custom Lambda Authorizer Example + +Lambda Annotations supports defining custom Lambda authorizers using attributes. Custom authorizers let you control access to your API Gateway endpoints by running a Lambda function that validates tokens or request parameters before the target function is invoked. The source generator automatically wires up the authorizer resources and references in the CloudFormation template. + +Two authorizer attributes are available: +* **`HttpApiAuthorizer`** — for HTTP API (API Gateway V2) endpoints +* **`RestApiAuthorizer`** — for REST API (API Gateway V1) endpoints + +### HTTP API Authorizer + +To create an HTTP API authorizer, decorate a Lambda function with the `[HttpApiAuthorizer]` attribute. The authorizer name is automatically derived from the method name, so other functions can reference it using `nameof()` for compile-time safety. + +**Step 1: Define the authorizer function** + +The authorizer function receives an `APIGatewayCustomAuthorizerV2Request` and returns an `APIGatewayCustomAuthorizerV2SimpleResponse` (when `EnableSimpleResponses` is true, which is the default). You can set context values that downstream functions can access via `[FromCustomAuthorizer]`. + +```csharp +[LambdaFunction(ResourceName = "HttpApiAuthorizer", PackageType = LambdaPackageType.Image)] +[HttpApiAuthorizer] +public APIGatewayCustomAuthorizerV2SimpleResponse AuthorizeHttpApi( + APIGatewayCustomAuthorizerV2Request request, + ILambdaContext context) +{ + var token = request.Headers?.GetValueOrDefault("authorization", ""); + + if (IsValidToken(token)) + { + return new APIGatewayCustomAuthorizerV2SimpleResponse + { + IsAuthorized = true, + Context = new Dictionary + { + { "userId", "user-123" }, + { "email", "user@example.com" }, + { "role", "admin" } + } + }; + } + + return new APIGatewayCustomAuthorizerV2SimpleResponse { IsAuthorized = false }; +} +``` + +**Step 2: Protect an endpoint with the authorizer** + +Reference the authorizer using `nameof()` in the `Authorizer` property of `[HttpApi]` for compile-time safety. If the authorizer method is renamed, both references update automatically. Use `[FromCustomAuthorizer]` on method parameters to automatically extract values from the authorizer context. + +```csharp +[LambdaFunction(ResourceName = "ProtectedHttpApiFunction", PackageType = LambdaPackageType.Image)] +[HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = nameof(AuthorizeHttpApi))] +public object GetProtectedResource( + [FromCustomAuthorizer(Name = "userId")] string userId, + [FromCustomAuthorizer(Name = "email")] string email) +{ + return new { UserId = userId, Email = email, Message = "This is a protected resource" }; +} +``` + +Multiple endpoints can share the same authorizer: + +```csharp +[LambdaFunction(ResourceName = "AdminHttpApiFunction", PackageType = LambdaPackageType.Image)] +[HttpApi(LambdaHttpMethod.Get, "/api/admin", Authorizer = nameof(AuthorizeHttpApi))] +public object AdminEndpoint( + [FromCustomAuthorizer(Name = "userId")] string userId, + [FromCustomAuthorizer(Name = "role")] string role) +{ + return new { Message = $"Hello admin {userId}!", Role = role }; +} +``` + +Endpoints without the `Authorizer` property remain public: + +```csharp +[LambdaFunction(ResourceName = "PublicHttpApiFunction", PackageType = LambdaPackageType.Image)] +[HttpApi(LambdaHttpMethod.Get, "/api/public")] +public string PublicEndpoint() +{ + return "This is a public endpoint"; +} +``` + +The source generator will produce CloudFormation that includes an `Auth` section on the `AnnotationsHttpApi` resource with the authorizer configuration, and each protected function's event will reference the authorizer by name. + +### REST API Authorizer + +REST API authorizers work similarly but use `[RestApiAuthorizer]` and `[RestApi]`. REST API authorizers support two types: +* **`RestApiAuthorizerType.Token`** — receives just the authorization token (default) +* **`RestApiAuthorizerType.Request`** — receives the full request context + +**Define a token-based REST API authorizer:** + +```csharp +[LambdaFunction(ResourceName = "RestApiAuthorizer", PackageType = LambdaPackageType.Image)] +[RestApiAuthorizer(Type = RestApiAuthorizerType.Token)] +public APIGatewayCustomAuthorizerResponse AuthorizeRestApi( + APIGatewayCustomAuthorizerRequest request, + ILambdaContext context) +{ + var token = request.AuthorizationToken; + + if (IsValidToken(token)) + { + return new APIGatewayCustomAuthorizerResponse + { + PrincipalID = "user-123", + PolicyDocument = new APIGatewayCustomAuthorizerPolicy + { + Version = "2012-10-17", + Statement = new List + { + new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement + { + Effect = "Allow", + Action = new HashSet { "execute-api:Invoke" }, + Resource = new HashSet { request.MethodArn } + } + } + }, + Context = new APIGatewayCustomAuthorizerContextOutput + { + ["userId"] = "user-123", + ["email"] = "user@example.com" + } + }; + } + + return new APIGatewayCustomAuthorizerResponse + { + PrincipalID = "anonymous", + PolicyDocument = new APIGatewayCustomAuthorizerPolicy + { + Version = "2012-10-17", + Statement = new List + { + new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement + { + Effect = "Deny", + Action = new HashSet { "execute-api:Invoke" }, + Resource = new HashSet { request.MethodArn } + } + } + } + }; +} +``` + +**Protect a REST API endpoint:** + +```csharp +[LambdaFunction(ResourceName = "ProtectedRestApiFunction", PackageType = LambdaPackageType.Image)] +[RestApi(LambdaHttpMethod.Get, "/api/rest/protected", Authorizer = nameof(AuthorizeRestApi))] +public object GetRestProtectedResource( + [FromCustomAuthorizer(Name = "userId")] string userId) +{ + return new { UserId = userId, Source = "REST API" }; +} +``` + +### Authorizer Attribute Properties + +**`HttpApiAuthorizerAttribute`** properties: + +| Property | Type | Default | Description | +|---|---|---|---| +| `IdentityHeader` | `string` | `"Authorization"` | Header used as the identity source. Translated to `$request.header.{value}` in CloudFormation. | +| `EnableSimpleResponses` | `bool` | `true` | When `true`, use simple responses (`IsAuthorized: true/false`). When `false`, use IAM policy responses. | +| `PayloadFormatVersion` | `string` | `"2.0"` | Authorizer payload format version. Valid values: `"1.0"` or `"2.0"`. | +| `ResultTtlInSeconds` | `int` | `0` | TTL in seconds for caching authorizer results. `0` = no caching. Max = `3600`. | + +**`RestApiAuthorizerAttribute`** properties: + +| Property | Type | Default | Description | +|---|---|---|---| +| `IdentityHeader` | `string` | `"Authorization"` | Header used as the identity source. Translated to `method.request.header.{value}` in CloudFormation. | +| `Type` | `RestApiAuthorizerType` | `Token` | Type of authorizer: `Token` (receives just the token) or `Request` (receives full request context). | +| `ResultTtlInSeconds` | `int` | `0` | TTL in seconds for caching authorizer results. `0` = no caching. Max = `3600`. | + +**Example with custom header and caching:** + +```csharp +[LambdaFunction(ResourceName = "ApiKeyAuthorizer", PackageType = LambdaPackageType.Image)] +[HttpApiAuthorizer( + IdentityHeader = "X-Api-Key", + ResultTtlInSeconds = 300)] +public APIGatewayCustomAuthorizerV2SimpleResponse ValidateApiKey( + APIGatewayCustomAuthorizerV2Request request, + ILambdaContext context) +{ + var apiKey = request.Headers?.GetValueOrDefault("x-api-key", ""); + + if (!string.IsNullOrEmpty(apiKey) && apiKey.StartsWith("valid-")) + { + return new APIGatewayCustomAuthorizerV2SimpleResponse + { + IsAuthorized = true, + Context = new Dictionary + { + { "clientId", "client-456" }, + { "tier", "premium" } + } + }; + } + + return new APIGatewayCustomAuthorizerV2SimpleResponse { IsAuthorized = false }; +} + +[LambdaFunction(ResourceName = "ApiKeyProtectedFunction", PackageType = LambdaPackageType.Image)] +[HttpApi(LambdaHttpMethod.Get, "/api/external", Authorizer = "ApiKeyAuth")] +public object ExternalEndpoint( + [FromCustomAuthorizer(Name = "clientId")] string clientId, + [FromCustomAuthorizer(Name = "tier")] string tier) +{ + return new { ClientId = clientId, Tier = tier }; +} +``` + ## Getting build information The source generator integrates with MSBuild's compiler error and warning reporting when there are problems generating the boiler plate code. @@ -876,9 +1097,13 @@ Event attributes configuring the source generator for the type of event to expec parameter to the `LambdaFunction` must be the event object and the event source must be configured outside of the code. * RestApi - * Configures the Lambda function to be called from an API Gateway REST API. The HTTP method and resource path are required to be set on the attribute. + * Configures the Lambda function to be called from an API Gateway REST API. The HTTP method and resource path are required to be set on the attribute. Use the `Authorizer` property to reference a `RestApiAuthorizer` by name. * HttpApi - * Configures the Lambda function to be called from an API Gateway HTTP API. The HTTP method, HTTP API payload version and resource path are required to be set on the attribute. + * Configures the Lambda function to be called from an API Gateway HTTP API. The HTTP method, HTTP API payload version and resource path are required to be set on the attribute. Use the `Authorizer` property to reference an `HttpApiAuthorizer` by name. +* HttpApiAuthorizer + * Marks a Lambda function as an HTTP API (API Gateway V2) custom authorizer. The authorizer name is automatically derived from the method name. Other functions reference it via `HttpApi.Authorizer` using `nameof()` for compile-time safety. +* RestApiAuthorizer + * Marks a Lambda function as a REST API (API Gateway V1) custom authorizer. The authorizer name is automatically derived from the method name. Other functions reference it via `RestApi.Authorizer` using `nameof()`. Use the `Type` property to choose between `Token` and `Request` authorizer types. * SQSEvent * Sets up event source mapping between the Lambda function and SQS queues. The SQS queue ARN is required to be set on the attribute. If users want to pass a reference to an existing SQS queue resource defined in their CloudFormation template, they can pass the SQS queue resource name prefixed with the '@' symbol. diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj index 9b80950f6..c8cc6f306 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj @@ -59,6 +59,14 @@ + + TestCustomAuthorizerApp\%(RecursiveDir)/%(FileName)%(Extension) + Always + + + + + TestExecutableServerlessApp\%(RecursiveDir)/%(FileName)%(Extension) Always diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs new file mode 100644 index 000000000..2611b8c62 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs @@ -0,0 +1,58 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class AuthorizerFunction_HttpApiAuthorizeV1_Generated + { + private readonly AuthorizerFunction authorizerFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public AuthorizerFunction_HttpApiAuthorizeV1_Generated() + { + SetExecutionEnvironment(); + authorizerFunction = new AuthorizerFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerResponse HttpApiAuthorizeV1(Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + return authorizerFunction.HttpApiAuthorizeV1(__request__, __context__); + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorize_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorize_Generated.g.cs new file mode 100644 index 000000000..921c6aa02 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_HttpApiAuthorize_Generated.g.cs @@ -0,0 +1,58 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class AuthorizerFunction_HttpApiAuthorize_Generated + { + private readonly AuthorizerFunction authorizerFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public AuthorizerFunction_HttpApiAuthorize_Generated() + { + SetExecutionEnvironment(); + authorizerFunction = new AuthorizerFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerV2SimpleResponse HttpApiAuthorize(Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerV2Request __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + return authorizerFunction.HttpApiAuthorize(__request__, __context__); + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#1.9.0.0"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_RestApiAuthorize_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_RestApiAuthorize_Generated.g.cs new file mode 100644 index 000000000..76acd4579 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/AuthorizerFunction_RestApiAuthorize_Generated.g.cs @@ -0,0 +1,58 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class AuthorizerFunction_RestApiAuthorize_Generated + { + private readonly AuthorizerFunction authorizerFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public AuthorizerFunction_RestApiAuthorize_Generated() + { + SetExecutionEnvironment(); + authorizerFunction = new AuthorizerFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerResponse RestApiAuthorize(Amazon.Lambda.APIGatewayEvents.APIGatewayCustomAuthorizerRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + return authorizerFunction.RestApiAuthorize(__request__, __context__); + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#1.9.0.0"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs new file mode 100644 index 000000000..62c34f770 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs @@ -0,0 +1,225 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_GetHttpApiV1UserInfo_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_GetHttpApiV1UserInfo_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetHttpApiV1UserInfo(Amazon.Lambda.APIGatewayEvents.APIGatewayProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var validationErrors = new List(); + + var userId = default(string); + if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("userId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_userId__ = __request__.RequestContext.Authorizer["userId"]; + userId = (string)Convert.ChangeType(__authValue_userId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'userId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'userId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var email = default(string); + if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("email") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_email__ = __request__.RequestContext.Authorizer["email"]; + email = (string)Convert.ChangeType(__authValue_email__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'email', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'email', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var tenantId = default(string); + if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("tenantId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'tenantId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'tenantId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_tenantId__ = __request__.RequestContext.Authorizer["tenantId"]; + tenantId = (string)Convert.ChangeType(__authValue_tenantId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'tenantId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'tenantId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + // return 400 Bad Request if there exists a validation error + if (validationErrors.Any()) + { + var errorResult = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Body = @$"{{""message"": ""{validationErrors.Count} validation error(s) detected: {string.Join(",", validationErrors)}""}}", + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "ValidationException"} + }, + StatusCode = 400 + }; + return errorResult; + } + + var response = protectedFunction.GetHttpApiV1UserInfo(userId, email, tenantId, __context__); + var memoryStream = new MemoryStream(); + serializer.Serialize(response, memoryStream); + memoryStream.Position = 0; + + // convert stream to string + StreamReader reader = new StreamReader( memoryStream ); + var body = reader.ReadToEnd(); + + return new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Body = body, + Headers = new Dictionary + { + {"Content-Type", "application/json"} + }, + StatusCode = 200 + }; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetIHttpResult_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetIHttpResult_Generated.g.cs new file mode 100644 index 000000000..b969dd378 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetIHttpResult_Generated.g.cs @@ -0,0 +1,185 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; +using Amazon.Lambda.Annotations.APIGateway; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_GetIHttpResult_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_GetIHttpResult_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public System.IO.Stream GetIHttpResult(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var validationErrors = new List(); + + var userId = default(string); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("userId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + var __unauthorizedStream__ = new System.IO.MemoryStream(); + serializer.Serialize(__unauthorized__, __unauthorizedStream__); + __unauthorizedStream__.Position = 0; + return __unauthorizedStream__; + } + + try + { + var __authValue_userId__ = __request__.RequestContext.Authorizer.Lambda["userId"]; + userId = (string)Convert.ChangeType(__authValue_userId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'userId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'userId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + var __unauthorizedStream__ = new System.IO.MemoryStream(); + serializer.Serialize(__unauthorized__, __unauthorizedStream__); + __unauthorizedStream__.Position = 0; + return __unauthorizedStream__; + } + + var email = default(string); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("email") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + var __unauthorizedStream__ = new System.IO.MemoryStream(); + serializer.Serialize(__unauthorized__, __unauthorizedStream__); + __unauthorizedStream__.Position = 0; + return __unauthorizedStream__; + } + + try + { + var __authValue_email__ = __request__.RequestContext.Authorizer.Lambda["email"]; + email = (string)Convert.ChangeType(__authValue_email__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'email', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'email', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + var __unauthorizedStream__ = new System.IO.MemoryStream(); + serializer.Serialize(__unauthorized__, __unauthorizedStream__); + __unauthorizedStream__.Position = 0; + return __unauthorizedStream__; + } + + // return 400 Bad Request if there exists a validation error + if (validationErrors.Any()) + { + var errorResult = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = @$"{{""message"": ""{validationErrors.Count} validation error(s) detected: {string.Join(",", validationErrors)}""}}", + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "ValidationException"} + }, + StatusCode = 400 + }; + var errorStream = new System.IO.MemoryStream(); + serializer.Serialize(errorResult, errorStream); + errorStream.Position = 0; + return errorStream; + } + + var httpResults = protectedFunction.GetIHttpResult(userId, email, __context__); + HttpResultSerializationOptions.ProtocolFormat serializationFormat = HttpResultSerializationOptions.ProtocolFormat.HttpApi; + HttpResultSerializationOptions.ProtocolVersion serializationVersion = HttpResultSerializationOptions.ProtocolVersion.V2; + var serializationOptions = new HttpResultSerializationOptions { Format = serializationFormat, Version = serializationVersion, Serializer = serializer }; + var response = httpResults.Serialize(serializationOptions); + return response; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetNonStringUserInfo_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetNonStringUserInfo_Generated.g.cs new file mode 100644 index 000000000..10301f09c --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetNonStringUserInfo_Generated.g.cs @@ -0,0 +1,225 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_GetNonStringUserInfo_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_GetNonStringUserInfo_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetNonStringUserInfo(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var validationErrors = new List(); + + var tenantId = default(int); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("numericTenantId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'numericTenantId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'numericTenantId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_tenantId__ = __request__.RequestContext.Authorizer.Lambda["numericTenantId"]; + tenantId = (int)Convert.ChangeType(__authValue_tenantId__?.ToString(), typeof(int)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'numericTenantId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'numericTenantId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var isAdmin = default(bool); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("isAdmin") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'isAdmin' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'isAdmin' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_isAdmin__ = __request__.RequestContext.Authorizer.Lambda["isAdmin"]; + isAdmin = (bool)Convert.ChangeType(__authValue_isAdmin__?.ToString(), typeof(bool)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'isAdmin', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'isAdmin', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var score = default(double); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("score") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'score' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'score' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_score__ = __request__.RequestContext.Authorizer.Lambda["score"]; + score = (double)Convert.ChangeType(__authValue_score__?.ToString(), typeof(double)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'score', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'score', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + // return 400 Bad Request if there exists a validation error + if (validationErrors.Any()) + { + var errorResult = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = @$"{{""message"": ""{validationErrors.Count} validation error(s) detected: {string.Join(",", validationErrors)}""}}", + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "ValidationException"} + }, + StatusCode = 400 + }; + return errorResult; + } + + var response = protectedFunction.GetNonStringUserInfo(__request__, tenantId, isAdmin, score, __context__); + var memoryStream = new MemoryStream(); + serializer.Serialize(response, memoryStream); + memoryStream.Position = 0; + + // convert stream to string + StreamReader reader = new StreamReader( memoryStream ); + var body = reader.ReadToEnd(); + + return new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = body, + Headers = new Dictionary + { + {"Content-Type", "application/json"} + }, + StatusCode = 200 + }; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetProtectedData_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetProtectedData_Generated.g.cs new file mode 100644 index 000000000..0b5f94fb3 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetProtectedData_Generated.g.cs @@ -0,0 +1,68 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_GetProtectedData_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_GetProtectedData_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetProtectedData(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var response = protectedFunction.GetProtectedData(__request__, __context__); + + return new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = response, + Headers = new Dictionary + { + {"Content-Type", "text/plain"} + }, + StatusCode = 200 + }; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetRestUserInfo_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetRestUserInfo_Generated.g.cs new file mode 100644 index 000000000..0ae3d3107 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetRestUserInfo_Generated.g.cs @@ -0,0 +1,225 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_GetRestUserInfo_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_GetRestUserInfo_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse GetRestUserInfo(Amazon.Lambda.APIGatewayEvents.APIGatewayProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var validationErrors = new List(); + + var userId = default(string); + if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("userId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_userId__ = __request__.RequestContext.Authorizer["userId"]; + userId = (string)Convert.ChangeType(__authValue_userId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'userId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'userId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var email = default(string); + if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("email") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_email__ = __request__.RequestContext.Authorizer["email"]; + email = (string)Convert.ChangeType(__authValue_email__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'email', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'email', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var tenantId = default(string); + if (__request__.RequestContext?.Authorizer == null || __request__.RequestContext?.Authorizer.ContainsKey("tenantId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'tenantId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'tenantId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_tenantId__ = __request__.RequestContext.Authorizer["tenantId"]; + tenantId = (string)Convert.ChangeType(__authValue_tenantId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'tenantId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'tenantId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + // return 400 Bad Request if there exists a validation error + if (validationErrors.Any()) + { + var errorResult = new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Body = @$"{{""message"": ""{validationErrors.Count} validation error(s) detected: {string.Join(",", validationErrors)}""}}", + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "ValidationException"} + }, + StatusCode = 400 + }; + return errorResult; + } + + var response = protectedFunction.GetRestUserInfo(userId, email, tenantId, __context__); + var memoryStream = new MemoryStream(); + serializer.Serialize(response, memoryStream); + memoryStream.Position = 0; + + // convert stream to string + StreamReader reader = new StreamReader( memoryStream ); + var body = reader.ReadToEnd(); + + return new Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse + { + Body = body, + Headers = new Dictionary + { + {"Content-Type", "application/json"} + }, + StatusCode = 200 + }; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetUserInfo_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetUserInfo_Generated.g.cs new file mode 100644 index 000000000..469a54036 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_GetUserInfo_Generated.g.cs @@ -0,0 +1,225 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_GetUserInfo_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_GetUserInfo_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse GetUserInfo(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var validationErrors = new List(); + + var userId = default(string); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("userId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'userId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'userId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_userId__ = __request__.RequestContext.Authorizer.Lambda["userId"]; + userId = (string)Convert.ChangeType(__authValue_userId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'userId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'userId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var email = default(string); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("email") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'email' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'email' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_email__ = __request__.RequestContext.Authorizer.Lambda["email"]; + email = (string)Convert.ChangeType(__authValue_email__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'email', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'email', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + var tenantId = default(string); + if (__request__.RequestContext?.Authorizer?.Lambda == null || __request__.RequestContext?.Authorizer?.Lambda.ContainsKey("tenantId") == false) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogDebug("Authorizer attribute 'tenantId' was missing, returning unauthorized."); +#else + __context__.Logger.Log("Authorizer attribute 'tenantId' was missing, returning unauthorized."); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + try + { + var __authValue_tenantId__ = __request__.RequestContext.Authorizer.Lambda["tenantId"]; + tenantId = (string)Convert.ChangeType(__authValue_tenantId__?.ToString(), typeof(string)); + } + catch (Exception e) when (e is InvalidCastException || e is FormatException || e is OverflowException || e is ArgumentException) + { +#if NET6_0_OR_GREATER + __context__.Logger.LogError(e, "Failed to convert authorizer attribute 'tenantId', returning unauthorized."); +#else + __context__.Logger.Log("Failed to convert authorizer attribute 'tenantId', returning unauthorized. Exception: " + e.ToString()); +#endif + var __unauthorized__ = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "AccessDeniedException"} + }, + StatusCode = 401 + }; + return __unauthorized__; + } + + // return 400 Bad Request if there exists a validation error + if (validationErrors.Any()) + { + var errorResult = new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = @$"{{""message"": ""{validationErrors.Count} validation error(s) detected: {string.Join(",", validationErrors)}""}}", + Headers = new Dictionary + { + {"Content-Type", "application/json"}, + {"x-amzn-ErrorType", "ValidationException"} + }, + StatusCode = 400 + }; + return errorResult; + } + + var response = protectedFunction.GetUserInfo(userId, email, tenantId, __context__); + var memoryStream = new MemoryStream(); + serializer.Serialize(response, memoryStream); + memoryStream.Position = 0; + + // convert stream to string + StreamReader reader = new StreamReader( memoryStream ); + var body = reader.ReadToEnd(); + + return new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = body, + Headers = new Dictionary + { + {"Content-Type", "application/json"} + }, + StatusCode = 200 + }; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_HealthCheck_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_HealthCheck_Generated.g.cs new file mode 100644 index 000000000..df53d3aa0 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ProtectedFunction_HealthCheck_Generated.g.cs @@ -0,0 +1,68 @@ +// + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using Amazon.Lambda.Core; + +namespace TestCustomAuthorizerApp +{ + public class ProtectedFunction_HealthCheck_Generated + { + private readonly ProtectedFunction protectedFunction; + private readonly Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer serializer; + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public ProtectedFunction_HealthCheck_Generated() + { + SetExecutionEnvironment(); + protectedFunction = new ProtectedFunction(); + serializer = new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer(); + } + + /// + /// The generated Lambda function handler for + /// + /// The API Gateway request object that will be processed by the Lambda function handler. + /// The ILambdaContext that provides methods for logging and describing the Lambda environment. + /// Result of the Lambda function execution + public Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse HealthCheck(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest __request__, Amazon.Lambda.Core.ILambdaContext __context__) + { + var response = protectedFunction.HealthCheck(__context__); + + return new Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse + { + Body = response, + Headers = new Dictionary + { + {"Content-Type", "text/plain"} + }, + StatusCode = 200 + }; + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("lib/amazon-lambda-annotations#{ANNOTATIONS_ASSEMBLY_VERSION}"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template new file mode 100644 index 000000000..f592ff292 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/customAuthorizerApp.template @@ -0,0 +1,428 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v{ANNOTATIONS_ASSEMBLY_VERSION}).", + "Resources": { + "AnnotationsHttpApi": { + "Type": "AWS::Serverless::HttpApi", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Auth": { + "Authorizers": { + "HttpApiAuthorize": { + "FunctionArn": { + "Fn::GetAtt": [ + "CustomAuthorizer", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "2.0", + "EnableSimpleResponses": true, + "Identity": { + "Headers": [ + "authorization" + ] + }, + "EnableFunctionDefaultPermissions": true, + "AuthorizerResultTtlInSeconds": 0 + }, + "HttpApiAuthorizeV1": { + "FunctionArn": { + "Fn::GetAtt": [ + "CustomAuthorizerV1", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "1.0", + "EnableSimpleResponses": false, + "Identity": { + "Headers": [ + "authorization" + ] + }, + "EnableFunctionDefaultPermissions": true, + "AuthorizerResultTtlInSeconds": 0 + } + } + } + } + }, + "AnnotationsRestApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "Prod", + "Auth": { + "Authorizers": { + "RestApiAuthorize": { + "FunctionArn": { + "Fn::GetAtt": [ + "RestApiAuthorizer", + "Arn" + ] + }, + "Identity": { + "Header": "Authorization" + }, + "FunctionPayloadType": "TOKEN" + } + } + } + }, + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + } + }, + "CustomAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorize_Generated::HttpApiAuthorize" + } + }, + "CustomAuthorizerV1": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorizeV1_Generated::HttpApiAuthorizeV1" + } + }, + "RestApiAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.AuthorizerFunction_RestApiAuthorize_Generated::RestApiAuthorize" + } + }, + "ProtectedEndpoint": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_GetProtectedData_Generated::GetProtectedData", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/protected", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiAuthorize" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "GetUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_GetUserInfo_Generated::GetUserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiAuthorize" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "HealthCheck": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_HealthCheck_Generated::HealthCheck", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/health", + "Method": "GET", + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "RestUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "RestApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_GetRestUserInfo_Generated::GetRestUserInfo", + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/api/rest-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "RestApiAuthorize" + }, + "RestApiId": { + "Ref": "AnnotationsRestApi" + } + } + } + } + } + }, + "HttpApiV1UserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_GetHttpApiV1UserInfo_Generated::GetHttpApiV1UserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/http-v1-user-info", + "Method": "GET", + "PayloadFormatVersion": "1.0", + "Auth": { + "Authorizer": "HttpApiAuthorizeV1" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "IHttpResultUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_GetIHttpResult_Generated::GetIHttpResult", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/ihttpresult-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiAuthorize" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "NonStringUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestCustomAuthorizerApp.ProtectedFunction_GetNonStringUserInfo_Generated::GetNonStringUserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/nonstring-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiAuthorize" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs index cfd8ac35f..1995743a2 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs @@ -1644,9 +1644,152 @@ public async Task CustomAuthorizerNonStringTest() Assert.Equal(expectedTemplateContent, actualTemplateContent); } + /// + /// Tests the full authorizer annotation template generation using TestCustomAuthorizerApp's AuthorizerFunction.cs. + /// This validates that [HttpApiAuthorizer] and [RestApiAuthorizer] attributes produce correct CloudFormation + /// template output including AnnotationsHttpApi/AnnotationsRestApi resources with Auth.Authorizers sections. + /// + [Fact] + public async Task CustomAuthorizerAppAuthorizerDefinitionsTest() + { + var expectedTemplateContent = await ReadSnapshotContent(Path.Combine("Snapshots", "ServerlessTemplates", "customAuthorizerApp.template")); + var expectedHttpApiAuthorizeGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "AuthorizerFunction_HttpApiAuthorize_Generated.g.cs")); + var expectedHttpApiAuthorizeV1Generated = await ReadSnapshotContent(Path.Combine("Snapshots", "AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs")); + var expectedRestApiAuthorizeGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "AuthorizerFunction_RestApiAuthorize_Generated.g.cs")); + var expectedGetProtectedDataGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetProtectedData_Generated.g.cs")); + var expectedGetUserInfoGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetUserInfo_Generated.g.cs")); + var expectedHealthCheckGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_HealthCheck_Generated.g.cs")); + var expectedGetRestUserInfoGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetRestUserInfo_Generated.g.cs")); + var expectedGetHttpApiV1UserInfoGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs")); + var expectedGetIHttpResultGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetIHttpResult_Generated.g.cs")); + var expectedGetNonStringUserInfoGenerated = await ReadSnapshotContent(Path.Combine("Snapshots", "ProtectedFunction_GetNonStringUserInfo_Generated.g.cs")); + + await new VerifyCS.Test + { + TestState = + { + Sources = + { + (Path.Combine("TestCustomAuthorizerApp", "AuthorizerFunction.cs"), File.ReadAllText(Path.Combine("TestCustomAuthorizerApp", "AuthorizerFunction.cs"))), + (Path.Combine("TestCustomAuthorizerApp", "ProtectedFunction.cs"), File.ReadAllText(Path.Combine("TestCustomAuthorizerApp", "ProtectedFunction.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaStartupAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "LambdaStartupAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "RestApiAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "RestApiAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAuthorizerAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAuthorizerAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "AuthorizerPayloadFormatVersion.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "AuthorizerPayloadFormatVersion.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "RestApiAuthorizerAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "RestApiAuthorizerAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "FromCustomAuthorizerAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "FromCustomAuthorizerAttribute.cs"))), + }, + GeneratedSources = + { + ( + typeof(SourceGenerator.Generator), + "AuthorizerFunction_HttpApiAuthorize_Generated.g.cs", + SourceText.From(expectedHttpApiAuthorizeGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs", + SourceText.From(expectedHttpApiAuthorizeV1Generated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "AuthorizerFunction_RestApiAuthorize_Generated.g.cs", + SourceText.From(expectedRestApiAuthorizeGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_GetProtectedData_Generated.g.cs", + SourceText.From(expectedGetProtectedDataGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_GetUserInfo_Generated.g.cs", + SourceText.From(expectedGetUserInfoGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_HealthCheck_Generated.g.cs", + SourceText.From(expectedHealthCheckGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_GetRestUserInfo_Generated.g.cs", + SourceText.From(expectedGetRestUserInfoGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs", + SourceText.From(expectedGetHttpApiV1UserInfoGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_GetIHttpResult_Generated.g.cs", + SourceText.From(expectedGetIHttpResultGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "ProtectedFunction_GetNonStringUserInfo_Generated.g.cs", + SourceText.From(expectedGetNonStringUserInfoGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ) + }, + ExpectedDiagnostics = + { + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("AuthorizerFunction_HttpApiAuthorizeV1_Generated.g.cs", expectedHttpApiAuthorizeV1Generated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("AuthorizerFunction_HttpApiAuthorize_Generated.g.cs", expectedHttpApiAuthorizeGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("AuthorizerFunction_RestApiAuthorize_Generated.g.cs", expectedRestApiAuthorizeGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetProtectedData_Generated.g.cs", expectedGetProtectedDataGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetUserInfo_Generated.g.cs", expectedGetUserInfoGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_HealthCheck_Generated.g.cs", expectedHealthCheckGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetRestUserInfo_Generated.g.cs", expectedGetRestUserInfoGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetHttpApiV1UserInfo_Generated.g.cs", expectedGetHttpApiV1UserInfoGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetIHttpResult_Generated.g.cs", expectedGetIHttpResultGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("ProtectedFunction_GetNonStringUserInfo_Generated.g.cs", expectedGetNonStringUserInfoGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestCustomAuthorizerApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent), + }, + ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + } + }.RunAsync(); + + var actualTemplateContent = File.ReadAllText(Path.Combine("TestCustomAuthorizerApp", "serverless.template")); + Assert.Equal(expectedTemplateContent, actualTemplateContent); + } + + [Fact] + public async Task VerifyMissingLambdaFunctionWithAuthorizerAttribute() + { + var test = new VerifyCS.Test() + { + TestState = + { + Sources = + { + (Path.Combine("TestServerlessApp", "FromScratch", "MissingLambdaFunctionWithAuthorizer.cs"), await File.ReadAllTextAsync(Path.Combine("TestServerlessApp", "FromScratch", "MissingLambdaFunctionWithAuthorizer.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaStartupAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "LambdaStartupAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAuthorizerAttribute.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "HttpApiAuthorizerAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "AuthorizerPayloadFormatVersion.cs"), await File.ReadAllTextAsync(Path.Combine("Amazon.Lambda.Annotations", "APIGateway", "AuthorizerPayloadFormatVersion.cs"))), + }, + GeneratedSources = + { + }, + ExpectedDiagnostics = + { + new DiagnosticResult("AWSLambda0129", DiagnosticSeverity.Error) + .WithSpan($"TestServerlessApp{Path.DirectorySeparatorChar}FromScratch{Path.DirectorySeparatorChar}MissingLambdaFunctionWithAuthorizer.cs", 10, 9, 20, 10) + .WithMessage("Method has [HttpApiAuthorizer] attribute but is missing the required [LambdaFunction] attribute. Add [LambdaFunction] to this method."), + }, + }, + }; + + await test.RunAsync(); + } + public void Dispose() { File.Delete(Path.Combine("TestServerlessApp", "serverless.template")); + File.Delete(Path.Combine("TestCustomAuthorizerApp", "serverless.template")); } private async static Task ReadSnapshotContent(string snapshotPath, bool trimContent = true) diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs index 965b61e1c..d2a728d5e 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs @@ -838,6 +838,13 @@ private LambdaFunctionModelTest GetLambdaFunctionModel(string handler = "MyAssem private AnnotationReport GetAnnotationReport(List lambdaFunctionModels, string projectRootDirectory = ProjectRootDirectory, string cloudFormationTemplatePath = ServerlessTemplateFilePath, bool isTelemetrySuppressed = false) + { + return GetAnnotationReport(lambdaFunctionModels, new List(), projectRootDirectory, cloudFormationTemplatePath, isTelemetrySuppressed); + } + + private AnnotationReport GetAnnotationReport(List lambdaFunctionModels, + List authorizers, + string projectRootDirectory = ProjectRootDirectory, string cloudFormationTemplatePath = ServerlessTemplateFilePath, bool isTelemetrySuppressed = false) { var annotationReport = new AnnotationReport { @@ -849,6 +856,10 @@ private AnnotationReport GetAnnotationReport(List l { annotationReport.LambdaFunctions.Add(model); } + foreach (var authorizer in authorizers) + { + annotationReport.Authorizers.Add(authorizer); + } return annotationReport; } @@ -859,21 +870,746 @@ private CloudFormationWriter GetCloudFormationWriter(IFileManager fileManager, I return new CloudFormationWriter(fileManager, directoryManager, templateWriter, diagnosticReporter); } - public class LambdaFunctionModelTest : ILambdaFunctionSerializable + #region Authorizer Tests + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void HttpApiAuthorizerProcessing(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "MyHttpAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify AnnotationsHttpApi resource was created + const string httpApiPath = "Resources.AnnotationsHttpApi"; + Assert.True(templateWriter.Exists(httpApiPath)); + Assert.Equal("AWS::Serverless::HttpApi", templateWriter.GetToken($"{httpApiPath}.Type")); + Assert.Equal("Amazon.Lambda.Annotations", templateWriter.GetToken($"{httpApiPath}.Metadata.Tool")); + + // Verify authorizer configuration + const string authorizerPath = "Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.MyHttpAuthorizer"; + Assert.True(templateWriter.Exists(authorizerPath)); + Assert.Equal(new List { "AuthorizerFunction", "Arn" }, templateWriter.GetToken>($"{authorizerPath}.FunctionArn.Fn::GetAtt")); + Assert.Equal("2.0", templateWriter.GetToken($"{authorizerPath}.AuthorizerPayloadFormatVersion")); + Assert.True(templateWriter.GetToken($"{authorizerPath}.EnableSimpleResponses")); + Assert.Equal(new List { "Authorization" }, templateWriter.GetToken>($"{authorizerPath}.Identity.Headers")); + Assert.True(templateWriter.GetToken($"{authorizerPath}.EnableFunctionDefaultPermissions")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void RestApiAuthorizerProcessing(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "MyRestAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Token + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify AnnotationsRestApi resource was created + const string restApiPath = "Resources.AnnotationsRestApi"; + Assert.True(templateWriter.Exists(restApiPath)); + Assert.Equal("AWS::Serverless::Api", templateWriter.GetToken($"{restApiPath}.Type")); + Assert.Equal("Amazon.Lambda.Annotations", templateWriter.GetToken($"{restApiPath}.Metadata.Tool")); + Assert.Equal("Prod", templateWriter.GetToken($"{restApiPath}.Properties.StageName")); + + // Verify authorizer configuration + const string authorizerPath = "Resources.AnnotationsRestApi.Properties.Auth.Authorizers.MyRestAuthorizer"; + Assert.True(templateWriter.Exists(authorizerPath)); + Assert.Equal(new List { "AuthorizerFunction", "Arn" }, templateWriter.GetToken>($"{authorizerPath}.FunctionArn.Fn::GetAtt")); + Assert.Equal("Authorization", templateWriter.GetToken($"{authorizerPath}.Identity.Header")); + Assert.Equal("TOKEN", templateWriter.GetToken($"{authorizerPath}.FunctionPayloadType")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void RestApiRequestAuthorizerProcessing(CloudFormationTemplateFormat templateFormat) { - public string MethodName { get; set; } - public string Handler { get; set; } - public bool IsExecutable { get; set; } - public string ResourceName { get; set; } - public uint? Timeout { get; set; } - public uint? MemorySize { get; set; } - public string Role { get; set; } - public string Policies { get; set; } - public string Runtime { get; set; } - public IList Attributes { get; set; } = new List(); - public string SourceGeneratorVersion { get; set; } - public LambdaPackageType PackageType { get; set; } = LambdaPackageType.Zip; - public string ReturnTypeFullName { get; set; } = "void"; + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "MyRequestAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "X-Custom-Header", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Request + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + const string authorizerPath = "Resources.AnnotationsRestApi.Properties.Auth.Authorizers.MyRequestAuthorizer"; + Assert.True(templateWriter.Exists(authorizerPath)); + Assert.Equal("REQUEST", templateWriter.GetToken($"{authorizerPath}.FunctionPayloadType")); + Assert.Equal("X-Custom-Header", templateWriter.GetToken($"{authorizerPath}.Identity.Header")); } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void HttpApiWithAuthorizerReference(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + + // Create the authorizer function + var authorizerFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + + // Create the protected function with HttpApi attribute referencing the authorizer + var protectedFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Protected", + "ProtectedFunction", 30, 512, null, null); + var httpApiAttribute = new AttributeModel() + { + Data = new HttpApiAttribute(LambdaHttpMethod.Get, "/api/protected") + { + Authorizer = "MyHttpAuthorizer" + } + }; + protectedFunctionModel.Attributes = new List { httpApiAttribute }; + protectedFunctionModel.Authorizer = "MyHttpAuthorizer"; + + var authorizer = new AuthorizerModel + { + Name = "MyHttpAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 + }; + + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { authorizerFunctionModel, protectedFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify the protected function's event has Auth configuration + const string eventPath = "Resources.ProtectedFunction.Properties.Events.RootGet"; + Assert.True(templateWriter.Exists(eventPath)); + Assert.Equal("HttpApi", templateWriter.GetToken($"{eventPath}.Type")); + Assert.Equal("/api/protected", templateWriter.GetToken($"{eventPath}.Properties.Path")); + Assert.Equal("GET", templateWriter.GetToken($"{eventPath}.Properties.Method")); + Assert.Equal("MyHttpAuthorizer", templateWriter.GetToken($"{eventPath}.Properties.Auth.Authorizer")); + Assert.Equal("AnnotationsHttpApi", templateWriter.GetToken($"{eventPath}.Properties.ApiId.Ref")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void RestApiWithAuthorizerReference(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + + // Create the authorizer function + var authorizerFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + + // Create the protected function with RestApi attribute referencing the authorizer + var protectedFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Protected", + "ProtectedFunction", 30, 512, null, null); + var restApiAttribute = new AttributeModel() + { + Data = new RestApiAttribute(LambdaHttpMethod.Get, "/api/protected") + { + Authorizer = "MyRestAuthorizer" + } + }; + protectedFunctionModel.Attributes = new List { restApiAttribute }; + protectedFunctionModel.Authorizer = "MyRestAuthorizer"; + + var authorizer = new AuthorizerModel + { + Name = "MyRestAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Token + }; + + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { authorizerFunctionModel, protectedFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify the protected function's event has Auth configuration + const string eventPath = "Resources.ProtectedFunction.Properties.Events.RootGet"; + Assert.True(templateWriter.Exists(eventPath)); + Assert.Equal("Api", templateWriter.GetToken($"{eventPath}.Type")); + Assert.Equal("/api/protected", templateWriter.GetToken($"{eventPath}.Properties.Path")); + Assert.Equal("GET", templateWriter.GetToken($"{eventPath}.Properties.Method")); + Assert.Equal("MyRestAuthorizer", templateWriter.GetToken($"{eventPath}.Properties.Auth.Authorizer")); + Assert.Equal("AnnotationsRestApi", templateWriter.GetToken($"{eventPath}.Properties.RestApiId.Ref")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void HttpApiAuthorizerWithCustomTtl(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "CachedAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 300, + EnableSimpleResponses = true, + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + const string authorizerPath = "Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.CachedAuthorizer"; + Assert.True(templateWriter.Exists(authorizerPath)); + + // When TTL > 0, FunctionInvokeRole is set (to null, signaling SAM to auto-generate the role for caching) + // Verify the authorizer was created with expected properties + Assert.Equal(new List { "AuthorizerFunction", "Arn" }, templateWriter.GetToken>($"{authorizerPath}.FunctionArn.Fn::GetAtt")); + Assert.Equal("2.0", templateWriter.GetToken($"{authorizerPath}.AuthorizerPayloadFormatVersion")); + Assert.Equal(300, templateWriter.GetToken($"{authorizerPath}.AuthorizerResultTtlInSeconds")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void HttpApiAuthorizerWithNoTtl_DoesNotSetFunctionInvokeRole(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "NoCacheAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + const string authorizerPath = "Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.NoCacheAuthorizer"; + Assert.True(templateWriter.Exists(authorizerPath)); + + // When TTL = 0, FunctionInvokeRole should NOT be set + Assert.False(templateWriter.Exists($"{authorizerPath}.FunctionInvokeRole")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void RemoveOrphanedHttpApiAuthorizer(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Start with an authorizer + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "OldAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT - First pass: create the authorizer + cloudFormationWriter.ApplyReport(report); + + // ASSERT - Authorizer exists + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.OldAuthorizer")); + + // ACT - Second pass: remove the authorizer (empty authorizer list) + var reportWithoutAuthorizer = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List()); + cloudFormationWriter.ApplyReport(reportWithoutAuthorizer); + + // ASSERT - Authorizer is removed + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.False(templateWriter.Exists("Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.OldAuthorizer")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void RemoveOrphanedRestApiAuthorizer(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Start with an authorizer + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "OldRestAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Token + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT - First pass: create the authorizer + cloudFormationWriter.ApplyReport(report); + + // ASSERT - Authorizer exists + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.AnnotationsRestApi.Properties.Auth.Authorizers.OldRestAuthorizer")); + + // ACT - Second pass: remove the authorizer + var reportWithoutAuthorizer = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List()); + cloudFormationWriter.ApplyReport(reportWithoutAuthorizer); + + // ASSERT - Authorizer is removed + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.False(templateWriter.Exists("Resources.AnnotationsRestApi.Properties.Auth.Authorizers.OldRestAuthorizer")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void SwitchFromRestApiToHttpApiAuthorizer_RemovesOrphanedRestApiResource(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Start with a REST API authorizer + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var restAuthorizer = new AuthorizerModel + { + Name = "MyAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Token + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { restAuthorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT - First pass: create REST API authorizer + cloudFormationWriter.ApplyReport(report); + + // ASSERT - AnnotationsRestApi exists, AnnotationsHttpApi does not + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.AnnotationsRestApi")); + Assert.False(templateWriter.Exists("Resources.AnnotationsHttpApi")); + + // ARRANGE - Switch to HTTP API authorizer + var httpAuthorizer = new AuthorizerModel + { + Name = "MyAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 + }; + var reportWithHttpApi = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { httpAuthorizer }); + + // ACT - Second pass: switch to HTTP API authorizer + cloudFormationWriter.ApplyReport(reportWithHttpApi); + + // ASSERT - AnnotationsHttpApi exists, AnnotationsRestApi is removed + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.AnnotationsHttpApi")); + Assert.False(templateWriter.Exists("Resources.AnnotationsRestApi")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void SwitchFromHttpApiToRestApiAuthorizer_RemovesOrphanedHttpApiResource(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Start with an HTTP API authorizer + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var httpAuthorizer = new AuthorizerModel + { + Name = "MyAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { httpAuthorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT - First pass: create HTTP API authorizer + cloudFormationWriter.ApplyReport(report); + + // ASSERT - AnnotationsHttpApi exists, AnnotationsRestApi does not + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.AnnotationsHttpApi")); + Assert.False(templateWriter.Exists("Resources.AnnotationsRestApi")); + + // ARRANGE - Switch to REST API authorizer + var restAuthorizer = new AuthorizerModel + { + Name = "MyAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Token + }; + var reportWithRestApi = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { restAuthorizer }); + + // ACT - Second pass: switch to REST API authorizer + cloudFormationWriter.ApplyReport(reportWithRestApi); + + // ASSERT - AnnotationsRestApi exists, AnnotationsHttpApi is removed + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.True(templateWriter.Exists("Resources.AnnotationsRestApi")); + Assert.False(templateWriter.Exists("Resources.AnnotationsHttpApi")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void CombinedAuthorizerAndProtectedFunction(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + + // Create the authorizer function + var authorizerFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.Auth::Authorize", + "MyAuthorizerFunction", 30, 512, null, null); + + // Create a protected function + var protectedFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.Api::GetData", + "GetDataFunction", 30, 512, null, null); + var httpApiAttribute = new AttributeModel() + { + Data = new HttpApiAttribute(LambdaHttpMethod.Get, "/data") + { + Authorizer = "LambdaAuthorizer" + } + }; + protectedFunctionModel.Attributes = new List { httpApiAttribute }; + protectedFunctionModel.Authorizer = "LambdaAuthorizer"; + + // Create an unprotected function + var publicFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.Api::Health", + "HealthFunction", 30, 512, null, null); + var publicHttpApiAttribute = new AttributeModel() + { + Data = new HttpApiAttribute(LambdaHttpMethod.Get, "/health") + }; + publicFunctionModel.Attributes = new List { publicHttpApiAttribute }; + + var authorizer = new AuthorizerModel + { + Name = "LambdaAuthorizer", + LambdaResourceName = "MyAuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + EnableSimpleResponses = true, + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2 + }; + + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { authorizerFunctionModel, protectedFunctionModel, publicFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify all three Lambda functions exist + Assert.True(templateWriter.Exists("Resources.MyAuthorizerFunction")); + Assert.True(templateWriter.Exists("Resources.GetDataFunction")); + Assert.True(templateWriter.Exists("Resources.HealthFunction")); + + // Verify AnnotationsHttpApi resource with authorizer + Assert.True(templateWriter.Exists("Resources.AnnotationsHttpApi")); + Assert.Equal("AWS::Serverless::HttpApi", templateWriter.GetToken("Resources.AnnotationsHttpApi.Type")); + Assert.True(templateWriter.Exists("Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.LambdaAuthorizer")); + Assert.Equal(new List { "MyAuthorizerFunction", "Arn" }, + templateWriter.GetToken>("Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.LambdaAuthorizer.FunctionArn.Fn::GetAtt")); + + // Verify protected function references authorizer + const string protectedEventPath = "Resources.GetDataFunction.Properties.Events.RootGet"; + Assert.Equal("LambdaAuthorizer", templateWriter.GetToken($"{protectedEventPath}.Properties.Auth.Authorizer")); + Assert.Equal("AnnotationsHttpApi", templateWriter.GetToken($"{protectedEventPath}.Properties.ApiId.Ref")); + + // Verify public function does NOT reference authorizer but DOES reference the shared API + const string publicEventPath = "Resources.HealthFunction.Properties.Events.RootGet"; + Assert.True(templateWriter.Exists(publicEventPath)); + Assert.False(templateWriter.Exists($"{publicEventPath}.Properties.Auth")); + Assert.Equal("AnnotationsHttpApi", templateWriter.GetToken($"{publicEventPath}.Properties.ApiId.Ref")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void CombinedRestApiAuthorizerAndProtectedFunction(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + + // Create the authorizer function + var authorizerFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.Auth::Authorize", + "MyAuthorizerFunction", 30, 512, null, null); + + // Create a protected function + var protectedFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.Api::GetData", + "GetDataFunction", 30, 512, null, null); + var restApiAttribute = new AttributeModel() + { + Data = new RestApiAttribute(LambdaHttpMethod.Get, "/data") + { + Authorizer = "LambdaAuthorizer" + } + }; + protectedFunctionModel.Attributes = new List { restApiAttribute }; + protectedFunctionModel.Authorizer = "LambdaAuthorizer"; + + // Create an unprotected function + var publicFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.Api::Health", + "HealthFunction", 30, 512, null, null); + var publicRestApiAttribute = new AttributeModel() + { + Data = new RestApiAttribute(LambdaHttpMethod.Get, "/health") + }; + publicFunctionModel.Attributes = new List { publicRestApiAttribute }; + + var authorizer = new AuthorizerModel + { + Name = "LambdaAuthorizer", + LambdaResourceName = "MyAuthorizerFunction", + AuthorizerType = AuthorizerType.RestApi, + IdentityHeader = "Authorization", + ResultTtlInSeconds = 0, + RestApiAuthorizerType = RestApiAuthorizerType.Token + }; + + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { authorizerFunctionModel, protectedFunctionModel, publicFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + // Verify all three Lambda functions exist + Assert.True(templateWriter.Exists("Resources.MyAuthorizerFunction")); + Assert.True(templateWriter.Exists("Resources.GetDataFunction")); + Assert.True(templateWriter.Exists("Resources.HealthFunction")); + + // Verify AnnotationsRestApi resource with authorizer + Assert.True(templateWriter.Exists("Resources.AnnotationsRestApi")); + Assert.Equal("AWS::Serverless::Api", templateWriter.GetToken("Resources.AnnotationsRestApi.Type")); + Assert.True(templateWriter.Exists("Resources.AnnotationsRestApi.Properties.Auth.Authorizers.LambdaAuthorizer")); + Assert.Equal(new List { "MyAuthorizerFunction", "Arn" }, + templateWriter.GetToken>("Resources.AnnotationsRestApi.Properties.Auth.Authorizers.LambdaAuthorizer.FunctionArn.Fn::GetAtt")); + + // Verify protected function references authorizer and shared API + const string protectedEventPath = "Resources.GetDataFunction.Properties.Events.RootGet"; + Assert.Equal("LambdaAuthorizer", templateWriter.GetToken($"{protectedEventPath}.Properties.Auth.Authorizer")); + Assert.Equal("AnnotationsRestApi", templateWriter.GetToken($"{protectedEventPath}.Properties.RestApiId.Ref")); + + // Verify public function does NOT reference authorizer but DOES reference the shared API + const string publicEventPath = "Resources.HealthFunction.Properties.Events.RootGet"; + Assert.True(templateWriter.Exists(publicEventPath)); + Assert.False(templateWriter.Exists($"{publicEventPath}.Properties.Auth")); + Assert.Equal("AnnotationsRestApi", templateWriter.GetToken($"{publicEventPath}.Properties.RestApiId.Ref")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void HttpApiAuthorizerWithCustomSettings(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Test non-default authorizer settings (V1 payload, simple responses disabled, custom header) + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel("MyAssembly::MyNamespace.MyType::Authorize", + "AuthorizerFunction", 30, 512, null, null); + var authorizer = new AuthorizerModel + { + Name = "CustomAuthorizer", + LambdaResourceName = "AuthorizerFunction", + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = "X-Api-Key", + ResultTtlInSeconds = 0, + EnableSimpleResponses = false, + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V1 + }; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport( + new List { lambdaFunctionModel }, + new List { authorizer }); + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + const string authorizerPath = "Resources.AnnotationsHttpApi.Properties.Auth.Authorizers.CustomAuthorizer"; + Assert.True(templateWriter.Exists(authorizerPath)); + Assert.Equal("1.0", templateWriter.GetToken($"{authorizerPath}.AuthorizerPayloadFormatVersion")); + Assert.False(templateWriter.GetToken($"{authorizerPath}.EnableSimpleResponses")); + Assert.Equal(new List { "X-Api-Key" }, + templateWriter.GetToken>($"{authorizerPath}.Identity.Headers")); + Assert.True(templateWriter.GetToken($"{authorizerPath}.EnableFunctionDefaultPermissions")); + } + + #endregion + } + + public class LambdaFunctionModelTest : ILambdaFunctionSerializable + { + public string MethodName { get; set; } + public string Handler { get; set; } + public bool IsExecutable { get; set; } + public string ResourceName { get; set; } + public uint? Timeout { get; set; } + public uint? MemorySize { get; set; } + public string Role { get; set; } + public string Policies { get; set; } + public string Runtime { get; set; } + public IList Attributes { get; set; } = new List(); + public string SourceGeneratorVersion { get; set; } + public LambdaPackageType PackageType { get; set; } = LambdaPackageType.Zip; + public string ReturnTypeFullName { get; set; } = "void"; + public string Authorizer { get; set; } } -} \ No newline at end of file +} diff --git a/Libraries/test/IntegrationTests.Helpers/CloudFormationHelper.cs b/Libraries/test/IntegrationTests.Helpers/CloudFormationHelper.cs index 11e943b75..43f4005c1 100644 --- a/Libraries/test/IntegrationTests.Helpers/CloudFormationHelper.cs +++ b/Libraries/test/IntegrationTests.Helpers/CloudFormationHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Threading.Tasks; using Amazon.CloudFormation; @@ -37,6 +38,12 @@ public async Task IsDeletedAsync(string stackName) public async Task DeleteStackAsync(string stackName) { + if (string.IsNullOrEmpty(stackName)) + { + Console.WriteLine("[CloudFormationHelper] WARNING: DeleteStackAsync called with null/empty stack name. Skipping."); + return; + } + if (!await StackExistsAsync(stackName)) return; @@ -49,6 +56,34 @@ public async Task DeleteStackAsync(string stackName) return stack?.Outputs.FirstOrDefault(x => string.Equals(x.OutputKey, outputKey))?.OutputValue; } + public async Task GetResourcePhysicalIdAsync(string stackName, string logicalResourceId) + { + try + { + var response = await _cloudFormationClient.DescribeStackResourcesAsync( + new DescribeStackResourcesRequest { StackName = stackName }); + + Console.WriteLine($"[CloudFormationHelper] Stack '{stackName}' has {response.StackResources.Count} resources: " + + string.Join(", ", response.StackResources.Select(r => $"{r.LogicalResourceId}={r.PhysicalResourceId} ({r.ResourceStatus})"))); + + var physicalId = response.StackResources + .FirstOrDefault(r => string.Equals(r.LogicalResourceId, logicalResourceId)) + ?.PhysicalResourceId; + + if (physicalId == null) + { + Console.WriteLine($"[CloudFormationHelper] WARNING: Logical resource '{logicalResourceId}' not found in stack '{stackName}'."); + } + + return physicalId; + } + catch (Exception ex) + { + Console.WriteLine($"[CloudFormationHelper] ERROR querying resource '{logicalResourceId}' in stack '{stackName}': {ex.GetType().Name}: {ex.Message}"); + throw; + } + } + private async Task GetStackAsync(string stackName) { if (!await StackExistsAsync(stackName)) diff --git a/Libraries/test/IntegrationTests.Helpers/CommandLineWrapper.cs b/Libraries/test/IntegrationTests.Helpers/CommandLineWrapper.cs index 31f705a45..274246771 100644 --- a/Libraries/test/IntegrationTests.Helpers/CommandLineWrapper.cs +++ b/Libraries/test/IntegrationTests.Helpers/CommandLineWrapper.cs @@ -47,6 +47,13 @@ public static async Task RunAsync(string command, string workingDirectory = "", await process.WaitForExitAsync(cancelToken); var log = sb.ToString(); + + if (process.ExitCode != 0) + { + throw new Exception( + $"Command '{command}' exited with code {process.ExitCode}.{Environment.NewLine}" + + $"Output:{Environment.NewLine}{log}"); + } } private static string GetSystemShell() diff --git a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs index 6f8a4b002..da9169201 100644 --- a/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs +++ b/Libraries/test/TestCustomAuthorizerApp.IntegrationTests/IntegrationTestContextFixture.cs @@ -23,7 +23,7 @@ public class IntegrationTestContextFixture : IAsyncLifetime public readonly HttpClient HttpClient; /// - /// HTTP API base URL (no trailing slash) + /// HTTP API base URL for endpoints attached to AnnotationsHttpApi (no trailing slash) /// public string HttpApiUrl = string.Empty; @@ -49,27 +49,42 @@ public IntegrationTestContextFixture() public async Task InitializeAsync() { var scriptPath = Path.Combine("..", "..", "..", "DeploymentScript.ps1"); + Console.WriteLine($"[IntegrationTest] Running deployment script: {scriptPath}"); await CommandLineWrapper.RunAsync($"pwsh {scriptPath}"); + Console.WriteLine("[IntegrationTest] Deployment script completed successfully."); _stackName = GetStackName(); _bucketName = GetBucketName(); + Console.WriteLine($"[IntegrationTest] Stack name: '{_stackName}', Bucket name: '{_bucketName}'"); Assert.False(string.IsNullOrEmpty(_stackName), "Stack name should not be empty"); Assert.False(string.IsNullOrEmpty(_bucketName), "Bucket name should not be empty"); - // Get API URLs from CloudFormation outputs - HttpApiUrl = await _cloudFormationHelper.GetOutputValueAsync(_stackName, "ApiUrl") ?? string.Empty; - RestApiUrl = await _cloudFormationHelper.GetOutputValueAsync(_stackName, "RestApiUrl") ?? string.Empty; + // Check stack status before querying resources + var stackStatus = await _cloudFormationHelper.GetStackStatusAsync(_stackName); + Console.WriteLine($"[IntegrationTest] Stack status after deployment: {stackStatus}"); + Assert.NotNull(stackStatus); + Assert.Equal(StackStatus.CREATE_COMPLETE, stackStatus); + + // Dynamically construct API URLs from stack resource physical IDs + // since the serverless.template is managed by the source generator and may not have Outputs + var region = "us-west-2"; + Console.WriteLine($"[IntegrationTest] Querying stack resources for '{_stackName}'..."); + var httpApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "AnnotationsHttpApi"); + var restApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "AnnotationsRestApi"); + Console.WriteLine($"[IntegrationTest] AnnotationsHttpApi: {httpApiId}, AnnotationsRestApi: {restApiId}"); + Assert.False(string.IsNullOrEmpty(httpApiId), $"CloudFormation resource 'AnnotationsHttpApi' was not found in stack '{_stackName}'."); + Assert.False(string.IsNullOrEmpty(restApiId), $"CloudFormation resource 'AnnotationsRestApi' was not found in stack '{_stackName}'."); + HttpApiUrl = $"https://{httpApiId}.execute-api.{region}.amazonaws.com"; + RestApiUrl = $"https://{restApiId}.execute-api.{region}.amazonaws.com/Prod"; LambdaFunctions = await LambdaHelper.FilterByCloudFormationStackAsync(_stackName); + Console.WriteLine($"[IntegrationTest] Found {LambdaFunctions.Count} Lambda functions: {string.Join(", ", LambdaFunctions.Select(f => f.Name ?? "(null)"))}"); - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatusAsync(_stackName)); Assert.True(await _s3Helper.BucketExistsAsync(_bucketName), $"S3 bucket {_bucketName} should exist"); - // There are 9 Lambda functions in TestCustomAuthorizerApp: - // CustomAuthorizer, RestApiAuthorizer, ProtectedEndpoint, GetUserInfo, HealthCheck, RestUserInfo, HttpApiV1UserInfo, IHttpResultUserInfo, NonStringUserInfo - Assert.Equal(9, LambdaFunctions.Count); - Assert.False(string.IsNullOrEmpty(HttpApiUrl), "HTTP API URL should not be empty"); - Assert.False(string.IsNullOrEmpty(RestApiUrl), "REST API URL should not be empty"); + // There are 10 Lambda functions in TestCustomAuthorizerApp: + // CustomAuthorizer, CustomAuthorizerV1, RestApiAuthorizer, ProtectedEndpoint, GetUserInfo, HealthCheck, RestUserInfo, HttpApiV1UserInfo, IHttpResultUserInfo, NonStringUserInfo + Assert.Equal(10, LambdaFunctions.Count); await LambdaHelper.WaitTillNotPending(LambdaFunctions.Where(x => x.Name != null).Select(x => x.Name!).ToList()); @@ -79,11 +94,27 @@ public async Task InitializeAsync() public async Task DisposeAsync() { - await _cloudFormationHelper.DeleteStackAsync(_stackName); - Assert.True(await _cloudFormationHelper.IsDeletedAsync(_stackName), $"The stack '{_stackName}' still exists and will have to be manually deleted from the AWS console."); - - await _s3Helper.DeleteBucketAsync(_bucketName); - Assert.False(await _s3Helper.BucketExistsAsync(_bucketName), $"The bucket '{_bucketName}' still exists and will have to be manually deleted from the AWS console."); + if (!string.IsNullOrEmpty(_stackName)) + { + Console.WriteLine($"[IntegrationTest] Cleaning up stack '{_stackName}'..."); + await _cloudFormationHelper.DeleteStackAsync(_stackName); + Assert.True(await _cloudFormationHelper.IsDeletedAsync(_stackName), $"The stack '{_stackName}' still exists and will have to be manually deleted from the AWS console."); + } + else + { + Console.WriteLine("[IntegrationTest] WARNING: Stack name is null/empty, skipping stack deletion."); + } + + if (!string.IsNullOrEmpty(_bucketName)) + { + Console.WriteLine($"[IntegrationTest] Cleaning up bucket '{_bucketName}'..."); + await _s3Helper.DeleteBucketAsync(_bucketName); + Assert.False(await _s3Helper.BucketExistsAsync(_bucketName), $"The bucket '{_bucketName}' still exists and will have to be manually deleted from the AWS console."); + } + else + { + Console.WriteLine("[IntegrationTest] WARNING: Bucket name is null/empty, skipping bucket deletion."); + } // Reset the aws-lambda-tools-defaults.json to original values var filePath = Path.Combine("..", "..", "..", "..", "TestCustomAuthorizerApp", "aws-lambda-tools-defaults.json"); diff --git a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs index 0b6193563..27e369893 100644 --- a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs @@ -1,5 +1,9 @@ +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; +using System.Collections.Generic; +using System; // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] @@ -34,6 +38,11 @@ public class AuthorizerFunction /// HTTP API Lambda Authorizer (Payload format 2.0 with simple response) /// Returns authorized status along with custom context that can be accessed via [FromCustomAuthorizer] /// + [LambdaFunction(ResourceName = "CustomAuthorizer")] + [HttpApiAuthorizer( + IdentityHeader = "authorization", + EnableSimpleResponses = true, + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2)] public APIGatewayCustomAuthorizerV2SimpleResponse HttpApiAuthorize( APIGatewayCustomAuthorizerV2Request request, ILambdaContext context) @@ -106,10 +115,96 @@ public APIGatewayCustomAuthorizerV2SimpleResponse HttpApiAuthorize( }; } + /// + /// HTTP API Lambda Authorizer (Payload format 1.0) + /// Used by HTTP API V1 endpoints that need payload format 1.0 + /// + [LambdaFunction(ResourceName = "CustomAuthorizerV1")] + [HttpApiAuthorizer( + IdentityHeader = "authorization", + EnableSimpleResponses = false, + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V1)] + public APIGatewayCustomAuthorizerResponse HttpApiAuthorizeV1( + APIGatewayCustomAuthorizerRequest request, + ILambdaContext context) + { + context.Logger.LogLine($"V1 Authorizer invoked"); + context.Logger.LogLine($"Authorization token: {request.AuthorizationToken}"); + + var authToken = request.AuthorizationToken ?? ""; + + // Deny if not a valid or partial-context token + if (!ValidTokens.Contains(authToken) && !PartialContextTokens.Contains(authToken)) + { + context.Logger.LogLine("V1 Authorizer: Denying request"); + return GenerateDenyPolicy("anonymous", request.MethodArn); + } + + // Check for partial-context tokens - authorize but return incomplete context + if (PartialContextTokens.Contains(authToken)) + { + context.Logger.LogLine("V1 Authorizer: Authorizing with partial context (missing expected keys)"); + + return new APIGatewayCustomAuthorizerResponse + { + PrincipalID = "partial-user", + PolicyDocument = new APIGatewayCustomAuthorizerPolicy + { + Version = "2012-10-17", + Statement = new List + { + new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement + { + Action = new HashSet { "execute-api:Invoke" }, + Effect = "Allow", + Resource = new HashSet { request.MethodArn } + } + } + }, + Context = new APIGatewayCustomAuthorizerContextOutput + { + // Intentionally return only unexpected keys, omitting userId, tenantId, email + ["unexpectedKey"] = "some-value" + } + }; + } + + context.Logger.LogLine("V1 Authorizer: Authorizing request with valid token"); + + return new APIGatewayCustomAuthorizerResponse + { + PrincipalID = "user-12345", + PolicyDocument = new APIGatewayCustomAuthorizerPolicy + { + Version = "2012-10-17", + Statement = new List + { + new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement + { + Action = new HashSet { "execute-api:Invoke" }, + Effect = "Allow", + Resource = new HashSet { request.MethodArn } + } + } + }, + Context = new APIGatewayCustomAuthorizerContextOutput + { + ["userId"] = "user-12345", + ["tenantId"] = "42", + ["userRole"] = "admin", + ["email"] = "test@example.com" + } + }; + } + /// /// REST API Lambda Authorizer (Token-based authorizer) /// Returns an IAM policy document along with custom context values /// + [LambdaFunction(ResourceName = "RestApiAuthorizer")] + [RestApiAuthorizer( + Type = RestApiAuthorizerType.Token, + IdentityHeader = "Authorization")] public APIGatewayCustomAuthorizerResponse RestApiAuthorize( APIGatewayCustomAuthorizerRequest request, ILambdaContext context) @@ -215,4 +310,4 @@ private APIGatewayCustomAuthorizerResponse GenerateDenyPolicy(string principalId } }; } -} +} \ No newline at end of file diff --git a/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs b/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs index d7a25ba65..0a68532f9 100644 --- a/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs @@ -1,3 +1,4 @@ +using System; using Amazon.Lambda.Annotations; using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.APIGatewayEvents; @@ -17,7 +18,7 @@ public class ProtectedFunction /// Debug endpoint to see what's in the RequestContext.Authorizer /// [LambdaFunction(ResourceName = "ProtectedEndpoint")] - [HttpApi(LambdaHttpMethod.Get, "/api/protected")] + [HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = nameof(AuthorizerFunction.HttpApiAuthorize))] public string GetProtectedData( APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) @@ -87,7 +88,7 @@ public string GetProtectedData( /// Another protected endpoint showing different usage - just getting the email. /// [LambdaFunction(ResourceName = "GetUserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/user-info")] + [HttpApi(LambdaHttpMethod.Get, "/api/user-info", Authorizer = nameof(AuthorizerFunction.HttpApiAuthorize))] public object GetUserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -122,7 +123,7 @@ public string HealthCheck(ILambdaContext context) /// REST API authorizers use a different context structure than HTTP API v2. /// [LambdaFunction(ResourceName = "RestUserInfo")] - [RestApi(LambdaHttpMethod.Get, "/api/rest-user-info")] + [RestApi(LambdaHttpMethod.Get, "/api/rest-user-info", Authorizer = nameof(AuthorizerFunction.RestApiAuthorize))] public object GetRestUserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -148,7 +149,7 @@ public object GetRestUserInfo( /// where RequestContext.Authorizer is a dictionary, not RequestContext.Authorizer.Lambda. /// [LambdaFunction(ResourceName = "HttpApiV1UserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/http-v1-user-info", Version = HttpApiVersion.V1)] + [HttpApi(LambdaHttpMethod.Get, "/api/http-v1-user-info", Version = HttpApiVersion.V1, Authorizer = nameof(AuthorizerFunction.HttpApiAuthorizeV1))] public object GetHttpApiV1UserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -174,7 +175,7 @@ public object GetHttpApiV1UserInfo( /// when authorizer context is missing (the handler returns Stream, not response object). /// [LambdaFunction(ResourceName = "IHttpResultUserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/ihttpresult-user-info")] + [HttpApi(LambdaHttpMethod.Get, "/api/ihttpresult-user-info", Authorizer = nameof(AuthorizerFunction.HttpApiAuthorize))] public IHttpResult GetIHttpResult( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -196,7 +197,7 @@ public IHttpResult GetIHttpResult( /// the Lambda authorizer context. /// [LambdaFunction(ResourceName = "NonStringUserInfo")] - [HttpApi(LambdaHttpMethod.Get, "/api/nonstring-user-info")] + [HttpApi(LambdaHttpMethod.Get, "/api/nonstring-user-info", Authorizer = nameof(AuthorizerFunction.HttpApiAuthorize))] public object GetNonStringUserInfo( APIGatewayHttpApiV2ProxyRequest request, [FromCustomAuthorizer(Name = "numericTenantId")] int tenantId, @@ -244,4 +245,4 @@ public object GetNonStringUserInfo( Message = "Successfully extracted non-string types from custom authorizer context!" }; } -} +} \ No newline at end of file diff --git a/Libraries/test/TestCustomAuthorizerApp/TestCustomAuthorizerApp.csproj b/Libraries/test/TestCustomAuthorizerApp/TestCustomAuthorizerApp.csproj index a9b30addb..6abb25b6c 100644 --- a/Libraries/test/TestCustomAuthorizerApp/TestCustomAuthorizerApp.csproj +++ b/Libraries/test/TestCustomAuthorizerApp/TestCustomAuthorizerApp.csproj @@ -1,6 +1,6 @@ - net8.0 + net6.0 enable enable true diff --git a/Libraries/test/TestCustomAuthorizerApp/serverless.template b/Libraries/test/TestCustomAuthorizerApp/serverless.template index 56ae0368d..f1714a728 100644 --- a/Libraries/test/TestCustomAuthorizerApp/serverless.template +++ b/Libraries/test/TestCustomAuthorizerApp/serverless.template @@ -1,621 +1,427 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "Test application demonstrating FromCustomAuthorizer attribute with both HTTP API and REST API Lambda Authorizers. This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", - "Globals": { - "Function": { - "Runtime": "dotnet8", - "MemorySize": 512, - "Timeout": 30, - "CodeUri": "." - } - }, + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", "Resources": { - "CustomAuthorizer": { - "Type": "AWS::Serverless::Function", - "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction::HttpApiAuthorize", - "Description": "Lambda Authorizer that validates requests and provides custom context values", - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "Environment": { - "Variables": { - "LAMBDA_NET_SERIALIZER_DEBUG": "true" + "AnnotationsHttpApi": { + "Type": "AWS::Serverless::HttpApi", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Auth": { + "Authorizers": { + "HttpApiAuthorize": { + "FunctionArn": { + "Fn::GetAtt": [ + "CustomAuthorizer", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "2.0", + "EnableSimpleResponses": true, + "Identity": { + "Headers": [ + "authorization" + ] + }, + "EnableFunctionDefaultPermissions": true, + "AuthorizerResultTtlInSeconds": 0 + }, + "HttpApiAuthorizeV1": { + "FunctionArn": { + "Fn::GetAtt": [ + "CustomAuthorizerV1", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "1.0", + "EnableSimpleResponses": false, + "Identity": { + "Headers": [ + "authorization" + ] + }, + "EnableFunctionDefaultPermissions": true, + "AuthorizerResultTtlInSeconds": 0 + } } } } }, - "CustomAuthorizerPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "CustomAuthorizer" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/authorizers/*" + "AnnotationsRestApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "Prod", + "Auth": { + "Authorizers": { + "RestApiAuthorize": { + "FunctionArn": { + "Fn::GetAtt": [ + "RestApiAuthorizer", + "Arn" + ] + }, + "Identity": { + "Header": "Authorization" + }, + "FunctionPayloadType": "TOKEN" + } + } } + }, + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" } }, - "ServerlessHttpApi": { - "Type": "AWS::ApiGatewayV2::Api", - "Properties": { - "Name": "TestCustomAuthorizerApi", - "ProtocolType": "HTTP", - "Description": "HTTP API with custom Lambda authorizer for testing FromCustomAuthorizer attribute" - } - }, - "HttpApiAuthorizer": { - "Type": "AWS::ApiGatewayV2::Authorizer", + "CustomAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "AuthorizerType": "REQUEST", - "AuthorizerUri": { - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${CustomAuthorizer.Arn}/invocations" - }, - "AuthorizerPayloadFormatVersion": "2.0", - "EnableSimpleResponses": true, - "IdentitySource": [ - "$request.header.authorization" + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" ], - "Name": "CustomLambdaAuthorizer" + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorize_Generated::HttpApiAuthorize" } }, - "ProtectedEndpoint": { + "RestApiAuthorizer": { "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetProtectedData_Generated::GetProtectedData", - "Description": "Protected endpoint demonstrating FromCustomAuthorizer attribute", - "PackageType": "Zip", + "Runtime": "dotnet6", "CodeUri": ".", - "Runtime": "dotnet8", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "ProtectedEndpointPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "ProtectedEndpoint" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/*/*/api/protected" - } + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_RestApiAuthorize_Generated::RestApiAuthorize" } }, - "ProtectedEndpointIntegration": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "ProtectedEndpoint", - "Arn" + "ProtectedEndpoint": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" ] - }, - "PayloadFormatVersion": "2.0" - } - }, - "ProtectedEndpointRoute": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/protected", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "HttpApiAuthorizer" - }, - "Target": { - "Fn::Sub": "integrations/${ProtectedEndpointIntegration}" } - } - }, - "GetUserInfo": { - "Type": "AWS::Serverless::Function", + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetUserInfo_Generated::GetUserInfo", - "Description": "Returns user info from custom authorizer context", - "PackageType": "Zip", + "Runtime": "dotnet6", "CodeUri": ".", - "Runtime": "dotnet8", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "GetUserInfoPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "GetUserInfo" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/*/*/api/user-info" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetProtectedData_Generated::GetProtectedData", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/protected", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiAuthorize" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, - "GetUserInfoIntegration": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "GetUserInfo", - "Arn" + "GetUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" ] - }, - "PayloadFormatVersion": "2.0" - } - }, - "GetUserInfoRoute": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/user-info", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "HttpApiAuthorizer" - }, - "Target": { - "Fn::Sub": "integrations/${GetUserInfoIntegration}" } - } - }, - "HealthCheck": { - "Type": "AWS::Serverless::Function", + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_HealthCheck_Generated::HealthCheck", - "Description": "Simple health check endpoint without authorizer", - "PackageType": "Zip", + "Runtime": "dotnet6", "CodeUri": ".", - "Runtime": "dotnet8", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "HealthCheckPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "HealthCheck" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/*/*/api/health" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetUserInfo_Generated::GetUserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiAuthorize" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, - "HealthCheckIntegration": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "HealthCheck", - "Arn" + "HealthCheck": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "ApiId.Ref" ] - }, - "PayloadFormatVersion": "2.0" - } - }, - "HealthCheckRoute": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/health", - "Target": { - "Fn::Sub": "integrations/${HealthCheckIntegration}" } - } - }, - "HttpApiStage": { - "Type": "AWS::ApiGatewayV2::Stage", + }, "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "StageName": "$default", - "AutoDeploy": true - } - }, - "RestApiAuthorizer": { - "Type": "AWS::Serverless::Function", - "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction::RestApiAuthorize", - "Description": "REST API Lambda Authorizer that validates requests and provides custom context values", + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "ServerlessRestApi": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Name": "TestCustomAuthorizerRestApi", - "Description": "REST API with custom Lambda authorizer for testing FromCustomAuthorizer attribute" - } - }, - "RestApiLambdaAuthorizer": { - "Type": "AWS::ApiGateway::Authorizer", - "Properties": { - "Name": "RestApiLambdaAuthorizer", - "RestApiId": { - "Ref": "ServerlessRestApi" - }, - "Type": "TOKEN", - "AuthorizerUri": { - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiAuthorizer.Arn}/invocations" - }, - "IdentitySource": "method.request.header.Authorization" - } - }, - "RestApiAuthorizerPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "RestApiAuthorizer" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessRestApi}/authorizers/*" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_HealthCheck_Generated::HealthCheck", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/health", + "Method": "GET", + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, "RestUserInfo": { "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "RestApiId.Ref" + ] + } + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetRestUserInfo_Generated::GetRestUserInfo", - "Description": "REST API endpoint demonstrating FromCustomAuthorizer attribute", - "PackageType": "Zip", + "Runtime": "dotnet6", "CodeUri": ".", - "Runtime": "dotnet8", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "RestUserInfoPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "RestUserInfo" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessRestApi}/*/*/*" - } - } - }, - "RestApiResource": { - "Type": "AWS::ApiGateway::Resource", - "Properties": { - "RestApiId": { - "Ref": "ServerlessRestApi" - }, - "ParentId": { - "Fn::GetAtt": [ - "ServerlessRestApi", - "RootResourceId" - ] - }, - "PathPart": "api" - } - }, - "RestApiUserInfoResource": { - "Type": "AWS::ApiGateway::Resource", - "Properties": { - "RestApiId": { - "Ref": "ServerlessRestApi" - }, - "ParentId": { - "Ref": "RestApiResource" - }, - "PathPart": "rest-user-info" - } - }, - "RestApiUserInfoMethod": { - "Type": "AWS::ApiGateway::Method", - "Properties": { - "RestApiId": { - "Ref": "ServerlessRestApi" - }, - "ResourceId": { - "Ref": "RestApiUserInfoResource" - }, - "HttpMethod": "GET", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "RestApiLambdaAuthorizer" - }, - "Integration": { - "Type": "AWS_PROXY", - "IntegrationHttpMethod": "POST", - "Uri": { - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestUserInfo.Arn}/invocations" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetRestUserInfo_Generated::GetRestUserInfo", + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/api/rest-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "RestApiAuthorize" + }, + "RestApiId": { + "Ref": "AnnotationsRestApi" + } + } } } } }, - "RestApiDeployment": { - "Type": "AWS::ApiGateway::Deployment", - "DependsOn": [ - "RestApiUserInfoMethod" - ], - "Properties": { - "RestApiId": { - "Ref": "ServerlessRestApi" - } - } - }, - "RestApiStage": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "ServerlessRestApi" - }, - "DeploymentId": { - "Ref": "RestApiDeployment" - }, - "StageName": "Prod" - } - }, "HttpApiV1UserInfo": { "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetHttpApiV1UserInfo_Generated::GetHttpApiV1UserInfo", - "Description": "HTTP API v1 endpoint demonstrating FromCustomAuthorizer attribute with payload format 1.0", - "PackageType": "Zip", + "Runtime": "dotnet6", "CodeUri": ".", - "Runtime": "dotnet8", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "HttpApiV1UserInfoPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "HttpApiV1UserInfo" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/*/*/api/http-v1-user-info" - } - } - }, - "HttpApiV1UserInfoIntegration": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "HttpApiV1UserInfo", - "Arn" - ] - }, - "PayloadFormatVersion": "1.0" - } - }, - "HttpApiV1UserInfoRoute": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/http-v1-user-info", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "HttpApiAuthorizer" - }, - "Target": { - "Fn::Sub": "integrations/${HttpApiV1UserInfoIntegration}" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetHttpApiV1UserInfo_Generated::GetHttpApiV1UserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/http-v1-user-info", + "Method": "GET", + "PayloadFormatVersion": "1.0", + "Auth": { + "Authorizer": "HttpApiAuthorizeV1" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, "IHttpResultUserInfo": { "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetIHttpResult_Generated::GetIHttpResult", - "Description": "IHttpResult endpoint demonstrating FromCustomAuthorizer with IHttpResult return type", - "PackageType": "Zip", + "Runtime": "dotnet6", "CodeUri": ".", - "Runtime": "dotnet8", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "IHttpResultUserInfoPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "IHttpResultUserInfo" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/*/*/api/ihttpresult-user-info" - } - } - }, - "IHttpResultUserInfoIntegration": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "IHttpResultUserInfo", - "Arn" - ] - }, - "PayloadFormatVersion": "2.0" - } - }, - "IHttpResultUserInfoRoute": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/ihttpresult-user-info", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "HttpApiAuthorizer" - }, - "Target": { - "Fn::Sub": "integrations/${IHttpResultUserInfoIntegration}" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetIHttpResult_Generated::GetIHttpResult", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/ihttpresult-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiAuthorize" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, "NonStringUserInfo": { "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, "Properties": { - "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetNonStringUserInfo_Generated::GetNonStringUserInfo", - "Description": "HTTP API v2 endpoint demonstrating FromCustomAuthorizer with non-string types (int, bool, double)", - "PackageType": "Zip", + "Runtime": "dotnet6", "CodeUri": ".", - "Runtime": "dotnet8", + "MemorySize": 512, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" - ] - } - }, - "NonStringUserInfoPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { - "Ref": "NonStringUserInfo" - }, - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ServerlessHttpApi}/*/*/api/nonstring-user-info" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetNonStringUserInfo_Generated::GetNonStringUserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/nonstring-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiAuthorize" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } } } }, - "NonStringUserInfoIntegration": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "NonStringUserInfo", - "Arn" - ] - }, - "PayloadFormatVersion": "2.0" - } - }, - "NonStringUserInfoRoute": { - "Type": "AWS::ApiGatewayV2::Route", + "CustomAuthorizerV1": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/nonstring-user-info", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "HttpApiAuthorizer" - }, - "Target": { - "Fn::Sub": "integrations/${NonStringUserInfoIntegration}" - } - } - } - }, - "Outputs": { - "ApiUrl": { - "Description": "HTTP API endpoint URL", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" - } - }, - "ProtectedEndpointUrl": { - "Description": "Protected endpoint URL (requires Authorization header)", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/api/protected" - } - }, - "UserInfoUrl": { - "Description": "User info endpoint URL (requires Authorization header)", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/api/user-info" - } - }, - "HealthCheckUrl": { - "Description": "Health check endpoint URL (no auth required)", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/api/health" - } - }, - "RestApiUrl": { - "Description": "REST API endpoint URL", - "Value": { - "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" - } - }, - "RestUserInfoUrl": { - "Description": "REST API User info endpoint URL (requires Authorization header)", - "Value": { - "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/api/rest-user-info" - } - }, - "HttpApiV1UserInfoUrl": { - "Description": "HTTP API v1 User info endpoint URL (requires Authorization header, uses payload format 1.0)", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/api/http-v1-user-info" - } - }, - "IHttpResultUserInfoUrl": { - "Description": "IHttpResult User info endpoint URL (requires Authorization header, demonstrates IHttpResult return type)", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/api/ihttpresult-user-info" - } - }, - "NonStringUserInfoUrl": { - "Description": "Non-string types User info endpoint URL (requires Authorization header, demonstrates int/bool/double type conversion)", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/api/nonstring-user-info" + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorizeV1_Generated::HttpApiAuthorizeV1" } } } diff --git a/Libraries/test/TestCustomAuthorizerApp/src/Function/Function.cs b/Libraries/test/TestCustomAuthorizerApp/src/Function/Function.cs new file mode 100644 index 000000000..8bfb9c2e7 --- /dev/null +++ b/Libraries/test/TestCustomAuthorizerApp/src/Function/Function.cs @@ -0,0 +1,16 @@ +using System; + +using Amazon.Lambda.Core; + +namespace Function +{ + public class Function + { + public dynamic FunctionHandler(dynamic eventTrigger) + { + Console.WriteLine(eventTrigger); + + return new {}; + } + } +} diff --git a/Libraries/test/TestCustomAuthorizerApp/src/Function/Function.csproj b/Libraries/test/TestCustomAuthorizerApp/src/Function/Function.csproj new file mode 100644 index 000000000..1c95e7ced --- /dev/null +++ b/Libraries/test/TestCustomAuthorizerApp/src/Function/Function.csproj @@ -0,0 +1,12 @@ + + + net8.0 + true + Lambda + + + + + + + diff --git a/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template b/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template new file mode 100644 index 000000000..efd73d7fd --- /dev/null +++ b/Libraries/test/TestCustomAuthorizerApp/src/Function/serverless.template @@ -0,0 +1,428 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", + "Resources": { + "AnnotationsHttpApi": { + "Type": "AWS::Serverless::HttpApi", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Auth": { + "Authorizers": { + "HttpApiLambdaAuthorizer": { + "FunctionArn": { + "Fn::GetAtt": [ + "CustomAuthorizer", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "2.0", + "EnableSimpleResponses": true, + "Identity": { + "Headers": [ + "authorization" + ] + }, + "EnableFunctionDefaultPermissions": true, + "AuthorizerResultTtlInSeconds": 0 + }, + "HttpApiLambdaAuthorizerV1": { + "FunctionArn": { + "Fn::GetAtt": [ + "CustomAuthorizerV1", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "1.0", + "EnableSimpleResponses": false, + "Identity": { + "Headers": [ + "authorization" + ] + }, + "EnableFunctionDefaultPermissions": true, + "AuthorizerResultTtlInSeconds": 0 + } + } + } + } + }, + "AnnotationsRestApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "Prod", + "Auth": { + "Authorizers": { + "RestApiLambdaAuthorizer": { + "FunctionArn": { + "Fn::GetAtt": [ + "RestApiAuthorizer", + "Arn" + ] + }, + "Identity": { + "Header": "Authorization" + }, + "FunctionPayloadType": "TOKEN" + } + } + } + }, + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + } + }, + "ProtectedEndpoint": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetProtectedData_Generated::GetProtectedData", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/protected", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "GetUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetUserInfo_Generated::GetUserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "HealthCheck": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_HealthCheck_Generated::HealthCheck", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/health", + "Method": "GET", + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "RestUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "RestApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetRestUserInfo_Generated::GetRestUserInfo", + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/api/rest-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "RestApiLambdaAuthorizer" + }, + "RestApiId": { + "Ref": "AnnotationsRestApi" + } + } + } + } + } + }, + "HttpApiV1UserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetHttpApiV1UserInfo_Generated::GetHttpApiV1UserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/http-v1-user-info", + "Method": "GET", + "PayloadFormatVersion": "1.0", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizerV1" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "IHttpResultUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetIHttpResult_Generated::GetIHttpResult", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/ihttpresult-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "NonStringUserInfo": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "Auth.Authorizer", + "ApiId.Ref" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.ProtectedFunction_GetNonStringUserInfo_Generated::GetNonStringUserInfo", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/nonstring-user-info", + "Method": "GET", + "Auth": { + "Authorizer": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "AnnotationsHttpApi" + } + } + } + } + } + }, + "CustomAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorize_Generated::HttpApiAuthorize" + } + }, + "RestApiAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_RestApiAuthorize_Generated::RestApiAuthorize" + } + }, + "CustomAuthorizerV1": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestCustomAuthorizerApp::TestCustomAuthorizerApp.AuthorizerFunction_HttpApiAuthorizeV1_Generated::HttpApiAuthorizeV1" + } + } + } +} \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1 b/Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1 index e47405192..7a5cd5644 100644 --- a/Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1 +++ b/Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1 @@ -58,6 +58,16 @@ try { throw "Failed to create the following bucket: $identifier" } + # Add TestQueue resource to serverless.template for integration testing + # The source generator creates an Fn::GetAtt reference to TestQueue but doesn't define the resource itself + $templatePath = ".\serverless.template" + $template = Get-Content $templatePath | Out-String | ConvertFrom-Json + if (-not $template.Resources.PSObject.Properties['TestQueue']) { + $template.Resources | Add-Member -NotePropertyName "TestQueue" -NotePropertyValue @{ Type = "AWS::SQS::Queue" } -Force + $template | ConvertTo-Json -Depth 100 | Set-Content $templatePath + Write-Host "Added TestQueue resource to serverless.template" + } + dotnet restore Write-Host "Creating CloudFormation Stack $identifier, Architecture $arch, Runtime $runtime" dotnet lambda deploy-serverless --template-parameters "ArchitectureTypeParameter=$arch" diff --git a/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs b/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs index 20ba65a32..2dcac07a1 100644 --- a/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs +++ b/Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; @@ -43,25 +44,46 @@ public IntegrationTestContextFixture() public async Task InitializeAsync() { var scriptPath = Path.Combine("..", "..", "..", "DeploymentScript.ps1"); + Console.WriteLine($"[IntegrationTest] Running deployment script: {scriptPath}"); await CommandLineWrapper.RunAsync($"pwsh {scriptPath}"); + Console.WriteLine("[IntegrationTest] Deployment script completed successfully."); _stackName = GetStackName(); _bucketName = GetBucketName(); - Assert.False(string.IsNullOrEmpty(_stackName)); - Assert.False(string.IsNullOrEmpty(_bucketName)); - - RestApiUrlPrefix = await _cloudFormationHelper.GetOutputValueAsync(_stackName, "RestApiURL"); - HttpApiUrlPrefix = await _cloudFormationHelper.GetOutputValueAsync(_stackName, "HttpApiURL"); - TestQueueARN = await _cloudFormationHelper.GetOutputValueAsync(_stackName, "TestQueueARN"); + Console.WriteLine($"[IntegrationTest] Stack name: '{_stackName}', Bucket name: '{_bucketName}'"); + Assert.False(string.IsNullOrEmpty(_stackName), "Stack name should not be empty"); + Assert.False(string.IsNullOrEmpty(_bucketName), "Bucket name should not be empty"); + + // Check stack status before querying resources + var stackStatus = await _cloudFormationHelper.GetStackStatusAsync(_stackName); + Console.WriteLine($"[IntegrationTest] Stack status after deployment: {stackStatus}"); + Assert.NotNull(stackStatus); + Assert.Equal(StackStatus.CREATE_COMPLETE, stackStatus); + + // Dynamically construct API URLs from stack resource physical IDs + // since the serverless.template is managed by the source generator and may not have Outputs + var region = "us-west-2"; + Console.WriteLine($"[IntegrationTest] Querying stack resources for '{_stackName}'..."); + var httpApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "ServerlessHttpApi"); + var restApiId = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "ServerlessRestApi"); + Console.WriteLine($"[IntegrationTest] ServerlessHttpApi: {httpApiId}, ServerlessRestApi: {restApiId}"); + HttpApiUrlPrefix = $"https://{httpApiId}.execute-api.{region}.amazonaws.com"; + RestApiUrlPrefix = $"https://{restApiId}.execute-api.{region}.amazonaws.com/Prod"; + + // Get the SQS queue ARN from the physical resource ID (which is the queue URL) + var queueUrl = await _cloudFormationHelper.GetResourcePhysicalIdAsync(_stackName, "TestQueue"); + Console.WriteLine($"[IntegrationTest] TestQueue URL: {queueUrl}"); + Assert.False(string.IsNullOrEmpty(queueUrl), $"CloudFormation resource 'TestQueue' was not found in stack '{_stackName}'."); + TestQueueARN = ConvertSqsUrlToArn(queueUrl); LambdaFunctions = await LambdaHelper.FilterByCloudFormationStackAsync(_stackName); + Console.WriteLine($"[IntegrationTest] Found {LambdaFunctions.Count} Lambda functions: {string.Join(", ", LambdaFunctions.Select(f => f.Name ?? "(null)"))}"); - Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatusAsync(_stackName)); - Assert.True(await _s3Helper.BucketExistsAsync(_bucketName)); + Assert.True(await _s3Helper.BucketExistsAsync(_bucketName), $"S3 bucket {_bucketName} should exist"); Assert.Equal(34, LambdaFunctions.Count); - Assert.False(string.IsNullOrEmpty(RestApiUrlPrefix)); - Assert.False(string.IsNullOrEmpty(RestApiUrlPrefix)); + Assert.False(string.IsNullOrEmpty(RestApiUrlPrefix), "RestApiUrlPrefix should not be empty"); + Assert.False(string.IsNullOrEmpty(HttpApiUrlPrefix), "HttpApiUrlPrefix should not be empty"); - await LambdaHelper.WaitTillNotPending(LambdaFunctions.Select(x => x.Name).ToList()); + await LambdaHelper.WaitTillNotPending(LambdaFunctions.Where(x => x.Name != null).Select(x => x.Name).ToList()); // Wait an additional 10 seconds for any other eventually consistency state to finish up. await Task.Delay(10000); @@ -69,11 +91,27 @@ public async Task InitializeAsync() public async Task DisposeAsync() { - await _cloudFormationHelper.DeleteStackAsync(_stackName); - Assert.True(await _cloudFormationHelper.IsDeletedAsync(_stackName), $"The stack '{_stackName}' still exists and will have to be manually deleted from the AWS console."); - - await _s3Helper.DeleteBucketAsync(_bucketName); - Assert.False(await _s3Helper.BucketExistsAsync(_bucketName), $"The bucket '{_bucketName}' still exists and will have to be manually deleted from the AWS console."); + if (!string.IsNullOrEmpty(_stackName)) + { + Console.WriteLine($"[IntegrationTest] Cleaning up stack '{_stackName}'..."); + await _cloudFormationHelper.DeleteStackAsync(_stackName); + Assert.True(await _cloudFormationHelper.IsDeletedAsync(_stackName), $"The stack '{_stackName}' still exists and will have to be manually deleted from the AWS console."); + } + else + { + Console.WriteLine("[IntegrationTest] WARNING: Stack name is null/empty, skipping stack deletion."); + } + + if (!string.IsNullOrEmpty(_bucketName)) + { + Console.WriteLine($"[IntegrationTest] Cleaning up bucket '{_bucketName}'..."); + await _s3Helper.DeleteBucketAsync(_bucketName); + Assert.False(await _s3Helper.BucketExistsAsync(_bucketName), $"The bucket '{_bucketName}' still exists and will have to be manually deleted from the AWS console."); + } + else + { + Console.WriteLine("[IntegrationTest] WARNING: Bucket name is null/empty, skipping bucket deletion."); + } var filePath = Path.Combine("..", "..", "..", "..", "TestServerlessApp", "aws-lambda-tools-defaults.json"); var token = JObject.Parse(await File.ReadAllTextAsync(filePath)); @@ -95,5 +133,21 @@ private string GetBucketName() var token = JObject.Parse(File.ReadAllText(filePath))["s3-bucket"]; return token.ToObject(); } + + /// + /// Converts an SQS queue URL (e.g. https://sqs.us-west-2.amazonaws.com/123456789012/queue-name) + /// to an ARN (e.g. arn:aws:sqs:us-west-2:123456789012:queue-name). + /// + private static string ConvertSqsUrlToArn(string queueUrl) + { + // SQS URL format: https://sqs.{region}.amazonaws.com/{account-id}/{queue-name} + var uri = new Uri(queueUrl); + var host = uri.Host; // sqs.us-west-2.amazonaws.com + var segments = uri.AbsolutePath.Trim('/').Split('/'); // [account-id, queue-name] + var region = host.Split('.')[1]; // us-west-2 + var accountId = segments[0]; + var queueName = segments[1]; + return $"arn:aws:sqs:{region}:{accountId}:{queueName}"; + } } } diff --git a/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs b/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs new file mode 100644 index 000000000..0833df387 --- /dev/null +++ b/Libraries/test/TestServerlessApp/FromScratch/MissingLambdaFunctionWithAuthorizer.cs @@ -0,0 +1,22 @@ +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; + +namespace TestServerlessApp.FromScratch +{ + public class MissingLambdaFunctionWithAuthorizer + { + [HttpApiAuthorizer( + AuthorizerPayloadFormatVersion = AuthorizerPayloadFormatVersion.V2)] + public APIGatewayCustomAuthorizerV2SimpleResponse Authorize( + APIGatewayCustomAuthorizerV2Request request, + ILambdaContext context) + { + return new APIGatewayCustomAuthorizerV2SimpleResponse + { + IsAuthorized = true + }; + } + } +} \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp/TestServerlessApp.csproj b/Libraries/test/TestServerlessApp/TestServerlessApp.csproj index a790ac481..921e3d372 100644 --- a/Libraries/test/TestServerlessApp/TestServerlessApp.csproj +++ b/Libraries/test/TestServerlessApp/TestServerlessApp.csproj @@ -8,6 +8,7 @@ + diff --git a/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json b/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json index 0b96350ff..86ffa5624 100644 --- a/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json +++ b/Libraries/test/TestServerlessApp/aws-lambda-tools-defaults.json @@ -13,7 +13,7 @@ "template": "serverless.template", "template-parameters": "", "docker-host-build-output-dir": "./bin/Release/lambda-publish", - "s3-bucket": "test-serverless-app", - "stack-name": "test-serverless-app", - "function-architecture": "x86_64" -} \ No newline at end of file +"s3-bucket" : "test-serverless-app-2c68d818", +"stack-name" : "test-serverless-app-2c68d818", +"function-architecture" : "x86_64" +} diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index 0e3befbe1..8a8ca51ee 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -1,38 +1,24 @@ { "AWSTemplateFormatVersion": "2010-09-09", "Transform": "AWS::Serverless-2016-10-31", - "Description": "An AWS Serverless Application. This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", - "Parameters": { - "ArchitectureTypeParameter": { - "Type": "String", - "Default": "x86_64", - "AllowedValues": [ - "x86_64", - "arm64" - ] - } - }, - "Globals": { - "Function": { - "Architectures": [ - { - "Ref": "ArchitectureTypeParameter" - } - ], - "Tags": { - "aws-tests": "TestServerlessApp", - "aws-repo": "aws-lambda-dotnet" - } - } - }, + "Description": "This template is partially managed by Amazon.Lambda.Annotations (v1.9.0.0).", "Resources": { "TestQueue": { "Type": "AWS::SQS::Queue" }, - "TestServerlessAppDynamicExampleDynamicReturnGenerated": { + "AuthNameFallbackTest": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -44,15 +30,33 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicReturn_Generated::DynamicReturn" + "TestServerlessApp::TestServerlessApp.AuthNameFallback_GetUserId_Generated::GetUserId" ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/authorizer-fallback", + "Method": "GET" + } + } } } }, - "TestServerlessAppDynamicExampleDynamicInputGenerated": { + "TestServerlessAppComplexCalculatorAddGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootPost" + ], + "SyncedEventProperties": { + "RootPost": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -64,20 +68,29 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicInput_Generated::DynamicInput" + "TestServerlessApp::TestServerlessApp.ComplexCalculator_Add_Generated::Add" ] + }, + "Events": { + "RootPost": { + "Type": "HttpApi", + "Properties": { + "Path": "/ComplexCalculator/Add", + "Method": "POST" + } + } } } }, - "TestServerlessAppFromScratchNoApiGatewayEventsReferenceToUpperGenerated": { + "TestServerlessAppComplexCalculatorSubtractGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootGet" + "RootPost" ], "SyncedEventProperties": { - "RootGet": [ + "RootPost": [ "Path", "Method" ] @@ -93,15 +106,15 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.FromScratch.NoApiGatewayEventsReference_ToUpper_Generated::ToUpper" + "TestServerlessApp::TestServerlessApp.ComplexCalculator_Subtract_Generated::Subtract" ] }, "Events": { - "RootGet": { + "RootPost": { "Type": "HttpApi", "Properties": { - "Path": "/{text}", - "Method": "GET" + "Path": "/ComplexCalculator/Subtract", + "Method": "POST" } } } @@ -145,7 +158,7 @@ } } }, - "SimpleCalculatorAdd": { + "HttpApiV1AuthorizerTest": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -155,7 +168,8 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "PayloadFormatVersion" ] } }, @@ -169,21 +183,22 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Add_Generated::Add" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated::HttpApiV1Authorizer" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/SimpleCalculator/Add", - "Method": "GET" + "Path": "/api/authorizer-v1", + "Method": "GET", + "PayloadFormatVersion": "1.0" } } } } }, - "SimpleCalculatorSubtract": { + "HttpApiNonString": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -207,21 +222,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Subtract_Generated::Subtract" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated::HttpApiWithNonString" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/SimpleCalculator/Subtract", + "Path": "/api/authorizer-non-string", "Method": "GET" } } } } }, - "SimpleCalculatorMultiply": { + "RestAuthorizerTest": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -245,21 +260,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Multiply_Generated::Multiply" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerRestExample_RestAuthorizer_Generated::RestAuthorizer" ] }, "Events": { "RootGet": { "Type": "Api", "Properties": { - "Path": "/SimpleCalculator/Multiply/{x}/{y}", + "Path": "/rest/authorizer", "Method": "GET" } } } } }, - "SimpleCalculatorDivideAsync": { + "TestServerlessAppCustomAuthorizerWithIHttpResultsExampleAuthorizerWithIHttpResultsGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -283,95 +298,31 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_DivideAsync_Generated::DivideAsync" + "TestServerlessApp::TestServerlessApp.CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated::AuthorizerWithIHttpResults" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/SimpleCalculator/DivideAsync/{x}/{y}", + "Path": "/authorizerihttpresults", "Method": "GET" } } } } }, - "PI": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Pi_Generated::Pi" - ] - } - } - }, - "Random": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Random_Generated::Random" - ] - } - } - }, - "Randoms": { - "Type": "AWS::Serverless::Function", - "Metadata": { - "Tool": "Amazon.Lambda.Annotations" - }, - "Properties": { - "MemorySize": 512, - "Timeout": 30, - "Policies": [ - "AWSLambdaBasicExecutionRole" - ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestServerlessApp::TestServerlessApp.SimpleCalculator_Randoms_Generated::Randoms" - ] - } - } - }, - "SQSMessageHandler": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "TestQueueEvent" + "RootGet" ], "SyncedEventProperties": { - "TestQueueEvent": [ - "Queue.Fn::GetAtt", - "BatchSize", - "FilterCriteria.Filters", - "FunctionResponseTypes", - "MaximumBatchingWindowInSeconds", - "ScalingConfig.MaximumConcurrency" + "RootGet": [ + "Path", + "Method" ] } }, @@ -379,46 +330,27 @@ "MemorySize": 512, "Timeout": 30, "Policies": [ - "AWSLambdaSQSQueueExecutionRole" + "AWSLambdaBasicExecutionRole" ], "PackageType": "Image", "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.SqsMessageProcessing_HandleMessage_Generated::HandleMessage" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeader_Generated::OkResponseWithHeader" ] }, "Events": { - "TestQueueEvent": { - "Type": "SQS", + "RootGet": { + "Type": "Api", "Properties": { - "BatchSize": 50, - "FilterCriteria": { - "Filters": [ - { - "Pattern": "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" - } - ] - }, - "FunctionResponseTypes": [ - "ReportBatchItemFailures" - ], - "MaximumBatchingWindowInSeconds": 5, - "ScalingConfig": { - "MaximumConcurrency": 5 - }, - "Queue": { - "Fn::GetAtt": [ - "TestQueue", - "Arn" - ] - } + "Path": "/okresponsewithheader/{x}", + "Method": "GET" } } } } }, - "HttpApiV1AuthorizerTest": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -428,8 +360,7 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, @@ -443,22 +374,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerHttpApiV1Example_HttpApiV1Authorizer_Generated::HttpApiV1Authorizer" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeaderAsync_Generated::OkResponseWithHeaderAsync" ] }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/api/authorizer-v1", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/okresponsewithheaderasync/{x}", + "Method": "GET" } } } } }, - "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -482,21 +412,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.NullableReferenceTypeExample_NullableHeaderHttpApi_Generated::NullableHeaderHttpApi" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2_Generated::NotFoundResponseWithHeaderV2" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/nullableheaderhttpapi", + "Path": "/notfoundwithheaderv2/{x}", "Method": "GET" } } } } }, - "TestServerlessAppCustomAuthorizerWithIHttpResultsExampleAuthorizerWithIHttpResultsGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -520,21 +450,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerWithIHttpResultsExample_AuthorizerWithIHttpResults_Generated::AuthorizerWithIHttpResults" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2Async_Generated::NotFoundResponseWithHeaderV2Async" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/authorizerihttpresults", + "Path": "/notfoundwithheaderv2async/{x}", "Method": "GET" } } } } }, - "GreeterSayHello": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -550,7 +480,7 @@ } }, "Properties": { - "MemorySize": 1024, + "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -559,14 +489,14 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.Greeter_SayHello_Generated::SayHello" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1_Generated::NotFoundResponseWithHeaderV1" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/Greeter/SayHello", + "Path": "/notfoundwithheaderv1/{x}", "Method": "GET", "PayloadFormatVersion": "1.0" } @@ -574,7 +504,7 @@ } } }, - "GreeterSayHelloAsync": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -591,7 +521,7 @@ }, "Properties": { "MemorySize": 512, - "Timeout": 50, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -599,14 +529,14 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.Greeter_SayHelloAsync_Generated::SayHelloAsync" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1Async_Generated::NotFoundResponseWithHeaderV1Async" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/Greeter/SayHelloAsync", + "Path": "/notfoundwithheaderv1async/{x}", "Method": "GET", "PayloadFormatVersion": "1.0" } @@ -614,7 +544,7 @@ } } }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithCustomSerializerGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -624,7 +554,8 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "PayloadFormatVersion" ] } }, @@ -638,33 +569,25 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeader_Generated::OkResponseWithHeader" + "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithCustomSerializer_Generated::OkResponseWithCustomSerializer" ] }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/okresponsewithheader/{x}", - "Method": "GET" + "Path": "/okresponsewithcustomserializerasync/{firstName}/{lastName}", + "Method": "GET", + "PayloadFormatVersion": "1.0" } } } } }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { + "TestServerlessAppDynamicExampleDynamicReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -676,33 +599,15 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithHeaderAsync_Generated::OkResponseWithHeaderAsync" + "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicReturn_Generated::DynamicReturn" ] - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheaderasync/{x}", - "Method": "GET" - } - } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { + "TestServerlessAppDynamicExampleDynamicInputGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -714,21 +619,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2_Generated::NotFoundResponseWithHeaderV2" + "TestServerlessApp::TestServerlessApp.DynamicExample_DynamicInput_Generated::DynamicInput" ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2/{x}", - "Method": "GET" - } - } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { + "TestServerlessAppFromScratchNoApiGatewayEventsReferenceToUpperGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -752,34 +648,24 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV2Async_Generated::NotFoundResponseWithHeaderV2Async" + "TestServerlessApp::TestServerlessApp.FromScratch.NoApiGatewayEventsReference_ToUpper_Generated::ToUpper" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/notfoundwithheaderv2async/{x}", + "Path": "/{text}", "Method": "GET" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { + "TestServerlessAppFromScratchNoSerializerAttributeReferenceToUpperGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -791,22 +677,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1_Generated::NotFoundResponseWithHeaderV1" + "TestServerlessApp::TestServerlessApp.FromScratch.NoSerializerAttributeReference_ToUpper_Generated::ToUpper" ] - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv1/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } - } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { + "GreeterSayHello": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -822,7 +698,7 @@ } }, "Properties": { - "MemorySize": 512, + "MemorySize": 1024, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -831,14 +707,14 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_NotFoundResponseWithHeaderV1Async_Generated::NotFoundResponseWithHeaderV1Async" + "TestServerlessApp::TestServerlessApp.Greeter_SayHello_Generated::SayHello" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/notfoundwithheaderv1async/{x}", + "Path": "/Greeter/SayHello", "Method": "GET", "PayloadFormatVersion": "1.0" } @@ -846,7 +722,7 @@ } } }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithCustomSerializerGenerated": { + "GreeterSayHelloAsync": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -863,7 +739,7 @@ }, "Properties": { "MemorySize": 512, - "Timeout": 30, + "Timeout": 50, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -871,14 +747,14 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomizeResponseExamples_OkResponseWithCustomSerializer_Generated::OkResponseWithCustomSerializer" + "TestServerlessApp::TestServerlessApp.Greeter_SayHelloAsync_Generated::SayHelloAsync" ] }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/okresponsewithcustomserializerasync/{firstName}/{lastName}", + "Path": "/Greeter/SayHelloAsync", "Method": "GET", "PayloadFormatVersion": "1.0" } @@ -886,7 +762,7 @@ } } }, - "TestServerlessAppFromScratchNoSerializerAttributeReferenceToUpperGenerated": { + "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -901,15 +777,24 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.FromScratch.NoSerializerAttributeReference_ToUpper_Generated::ToUpper" + "TestServerlessApp::TestServerlessApp.IntrinsicExample_HasIntrinsic_Generated::HasIntrinsic" ] } } }, - "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -921,12 +806,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.IntrinsicExample_HasIntrinsic_Generated::HasIntrinsic" + "TestServerlessApp::TestServerlessApp.NullableReferenceTypeExample_NullableHeaderHttpApi_Generated::NullableHeaderHttpApi" ] + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/nullableheaderhttpapi", + "Method": "GET" + } + } } } }, - "HttpApiNonString": { + "SimpleCalculatorAdd": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -950,21 +844,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerNonStringExample_HttpApiWithNonString_Generated::HttpApiWithNonString" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Add_Generated::Add" ] }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/api/authorizer-non-string", + "Path": "/SimpleCalculator/Add", "Method": "GET" } } } } }, - "AuthNameFallbackTest": { + "SimpleCalculatorSubtract": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -988,24 +882,33 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.AuthNameFallback_GetUserId_Generated::GetUserId" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Subtract_Generated::Subtract" ] }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/api/authorizer-fallback", + "Path": "/SimpleCalculator/Subtract", "Method": "GET" } } } } }, - "TestServerlessAppVoidExampleVoidReturnGenerated": { + "SimpleCalculatorMultiply": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -1017,12 +920,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.VoidExample_VoidReturn_Generated::VoidReturn" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Multiply_Generated::Multiply" ] + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/SimpleCalculator/Multiply/{x}/{y}", + "Method": "GET" + } + } } } }, - "RestAuthorizerTest": { + "SimpleCalculatorDivideAsync": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -1046,21 +958,21 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.CustomAuthorizerRestExample_RestAuthorizer_Generated::RestAuthorizer" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_DivideAsync_Generated::DivideAsync" ] }, "Events": { "RootGet": { "Type": "Api", "Properties": { - "Path": "/rest/authorizer", + "Path": "/SimpleCalculator/DivideAsync/{x}/{y}", "Method": "GET" } } } } }, - "TestServerlessAppTaskExampleTaskReturnGenerated": { + "PI": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -1075,12 +987,12 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.TaskExample_TaskReturn_Generated::TaskReturn" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Pi_Generated::Pi" ] } } }, - "ToUpper": { + "Random": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -1095,24 +1007,15 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.Sub1.Functions_ToUpper_Generated::ToUpper" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Random_Generated::Random" ] } } }, - "TestServerlessAppComplexCalculatorAddGenerated": { + "Randoms": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootPost" - ], - "SyncedEventProperties": { - "RootPost": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -1124,31 +1027,26 @@ "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.ComplexCalculator_Add_Generated::Add" + "TestServerlessApp::TestServerlessApp.SimpleCalculator_Randoms_Generated::Randoms" ] - }, - "Events": { - "RootPost": { - "Type": "HttpApi", - "Properties": { - "Path": "/ComplexCalculator/Add", - "Method": "POST" - } - } } } }, - "TestServerlessAppComplexCalculatorSubtractGenerated": { + "SQSMessageHandler": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ - "RootPost" + "TestQueueEvent" ], "SyncedEventProperties": { - "RootPost": [ - "Path", - "Method" + "TestQueueEvent": [ + "Queue.Fn::GetAtt", + "BatchSize", + "FilterCriteria.Filters", + "FunctionResponseTypes", + "MaximumBatchingWindowInSeconds", + "ScalingConfig.MaximumConcurrency" ] } }, @@ -1156,47 +1054,103 @@ "MemorySize": 512, "Timeout": 30, "Policies": [ - "AWSLambdaBasicExecutionRole" + "AWSLambdaSQSQueueExecutionRole" ], "PackageType": "Image", "ImageUri": ".", "ImageConfig": { "Command": [ - "TestServerlessApp::TestServerlessApp.ComplexCalculator_Subtract_Generated::Subtract" + "TestServerlessApp::TestServerlessApp.SqsMessageProcessing_HandleMessage_Generated::HandleMessage" ] }, "Events": { - "RootPost": { - "Type": "HttpApi", + "TestQueueEvent": { + "Type": "SQS", "Properties": { - "Path": "/ComplexCalculator/Subtract", - "Method": "POST" + "BatchSize": 50, + "FilterCriteria": { + "Filters": [ + { + "Pattern": "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" + } + ] + }, + "FunctionResponseTypes": [ + "ReportBatchItemFailures" + ], + "MaximumBatchingWindowInSeconds": 5, + "ScalingConfig": { + "MaximumConcurrency": 5 + }, + "Queue": { + "Fn::GetAtt": [ + "TestQueue", + "Arn" + ] + } } } } } - } - }, - "Outputs": { - "RestApiURL": { - "Description": "Rest API endpoint URL for Prod environment", - "Value": { - "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" + }, + "ToUpper": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.Sub1.Functions_ToUpper_Generated::ToUpper" + ] + } } }, - "HttpApiURL": { - "Description": "HTTP API endpoint URL for Prod environment", - "Value": { - "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" + "TestServerlessAppTaskExampleTaskReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.TaskExample_TaskReturn_Generated::TaskReturn" + ] + } } }, - "TestQueueARN": { - "Description": "ARN of the TestQueue resource", - "Value": { - "Fn::GetAtt": [ - "TestQueue", - "Arn" - ] + "TestServerlessAppVoidExampleVoidReturnGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.VoidExample_VoidReturn_Generated::VoidReturn" + ] + } } } }