From c885af54141bb77a57b619109087f40910fb63de Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Tue, 17 Feb 2026 10:55:47 -0500 Subject: [PATCH] lambda auth update custom app --- .../Diagnostics/DiagnosticDescriptors.cs | 65 ++ .../Generator.cs | 168 ++++ .../Models/AnnotationReport.cs | 5 + .../Attributes/HttpApiAttributeBuilder.cs | 6 +- .../HttpApiAuthorizerAttributeBuilder.cs | 78 ++ .../Attributes/RestApiAttributeBuilder.cs | 9 +- .../RestApiAuthorizerAttributeBuilder.cs | 76 ++ .../Models/AuthorizerModel.cs | 99 +++ .../Models/ILambdaFunctionSerializable.cs | 8 +- .../Models/LambdaFunctionModel.cs | 5 +- .../Models/LambdaFunctionModelBuilder.cs | 22 + .../TypeFullNames.cs | 3 + .../Writers/CloudFormationWriter.cs | 247 +++++- .../APIGateway/HttpApiAttribute.cs | 7 + .../APIGateway/HttpApiAuthorizerAttribute.cs | 69 ++ .../APIGateway/RestApiAttribute.cs | 7 + .../APIGateway/RestApiAuthorizerAttribute.cs | 76 ++ .../WriterTests/CloudFormationWriterTests.cs | 1 + .../AuthorizerFunction.cs | 15 +- .../ProtectedFunction.cs | 14 +- .../aws-lambda-tools-defaults.json | 8 +- .../serverless.template | 836 +++++++----------- .../src/Function/Function.cs | 16 + .../src/Function/Function.csproj | 12 + .../serverless.template | 322 +++---- .../TestServerlessApp/AuthorizerFunctions.cs | 192 ++++ .../TestServerlessApp/serverless.template | 325 +++++++ 27 files changed, 1969 insertions(+), 722 deletions(-) create mode 100644 Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs create mode 100644 Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.cs create mode 100644 Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/AuthorizerModel.cs create mode 100644 Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs create mode 100644 Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs create mode 100644 Libraries/test/TestCustomAuthorizerApp/src/Function/Function.cs create mode 100644 Libraries/test/TestCustomAuthorizerApp/src/Function/Function.csproj create mode 100644 Libraries/test/TestServerlessApp/AuthorizerFunctions.cs diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs index a606e5e88..69ecec8eb 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs @@ -153,5 +153,70 @@ 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 InvalidAuthorizerPayloadFormatVersion = new DiagnosticDescriptor( + id: "AWSLambda0126", + title: "Invalid Payload Format Version", + messageFormat: "Invalid PayloadFormatVersion '{0}'. Must be \"1.0\" or \"2.0\".", + 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); } } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs index 3001b6144..097dd5278 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; @@ -168,6 +170,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; @@ -296,5 +305,164 @@ 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(); + + if (attributeFullName == TypeFullNames.HttpApiAuthorizerAttribute) + { + return HttpApiAuthorizerAttributeBuilder.BuildModel(attribute, lambdaResourceName); + } + + if (attributeFullName == TypeFullNames.RestApiAuthorizerAttribute) + { + return RestApiAuthorizerAttributeBuilder.BuildModel(attribute, lambdaResourceName); + } + } + + 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 PayloadFormatVersion for HTTP API authorizers + if (model.AuthorizerType == AuthorizerType.HttpApi) + { + if (model.PayloadFormatVersion != "1.0" && model.PayloadFormatVersion != "2.0") + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.InvalidAuthorizerPayloadFormatVersion, methodLocation, model.PayloadFormatVersion)); + 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; + } + } + + 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..8df840490 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/HttpApiAuthorizerAttributeBuilder.cs @@ -0,0 +1,78 @@ +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.Name): + attribute.Name = namedArg.Value.Value as string; + break; + 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.PayloadFormatVersion): + attribute.PayloadFormatVersion = namedArg.Value.Value as string ?? "2.0"; + 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 + { + Name = attribute.Name, + LambdaResourceName = lambdaResourceName, + AuthorizerType = AuthorizerType.HttpApi, + IdentityHeader = attribute.IdentityHeader, + ResultTtlInSeconds = attribute.ResultTtlInSeconds, + EnableSimpleResponses = attribute.EnableSimpleResponses, + PayloadFormatVersion = attribute.PayloadFormatVersion + }; + } + } +} \ 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..b981c6ed0 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/RestApiAuthorizerAttributeBuilder.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 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.Name): + attribute.Name = namedArg.Value.Value as string; + break; + 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 + { + Name = attribute.Name, + 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..645279b21 --- /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. Valid values: "1.0" or "2.0". + /// Only applicable for HTTP API authorizers. + /// + public string PayloadFormatVersion { 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/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..a64836e11 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 = "CustomHttpApi"; + private const string REST_API_RESOURCE_NAME = "CustomRestApi"; // 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,24 @@ public void ApplyReport(AnnotationReport report) ProcessTemplateDescription(report); + // Build authorizer lookup for processing events with Auth configuration + var authorizerLookup = report.Authorizers.ToDictionary(a => a.Name, a => a); + + // 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 +108,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 +117,7 @@ private void ProcessLambdaFunction(ILambdaFunctionSerializable lambdaFunction, s ApplyLambdaFunctionDefaults(lambdaFunctionPath, propertiesPath, lambdaFunction.Runtime); ProcessLambdaFunctionProperties(lambdaFunction, propertiesPath, relativeProjectUri); - ProcessLambdaFunctionEventAttributes(lambdaFunction); + ProcessLambdaFunctionEventAttributes(lambdaFunction, authorizerLookup); } /// @@ -180,7 +190,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 +201,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 +221,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 +230,22 @@ 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); + 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 +259,221 @@ 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); + 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 CustomHttpApi resource) + if (httpApiAuthorizers.Any()) + { + ProcessHttpApiAuthorizers(httpApiAuthorizers); + } + + // Process REST API authorizers (add to CustomRestApi resource) + if (restApiAuthorizers.Any()) + { + ProcessRestApiAuthorizers(restApiAuthorizers); + } + } + + /// + /// Writes HTTP API (API Gateway V2) authorizers to the CustomHttpApi 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 CustomHttpApi 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 + _templateWriter.SetToken($"{authorizerPath}.AuthorizerPayloadFormatVersion", authorizer.PayloadFormatVersion); + + // 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); + + // AuthorizerResultTtlInSeconds (only if caching is enabled) + if (authorizer.ResultTtlInSeconds > 0) + { + _templateWriter.SetToken($"{authorizerPath}.FunctionInvokeRole", null); // Required for caching + } + } + } + + /// + /// Writes REST API (API Gateway V1) authorizers to the CustomRestApi 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 CustomRestApi 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"); + } + } + } + + /// + /// Removes orphaned authorizers from the serverless template. + /// Authorizers are now defined inline within the API resources (CustomHttpApi and CustomRestApi). + /// 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 CustomHttpApi + 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 CustomRestApi + 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}"); + } + } + /// /// Writes all properties associated with to the serverless template. /// 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..e69e59b9a --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/HttpApiAuthorizerAttribute.cs @@ -0,0 +1,69 @@ +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. + /// + /// + /// + /// [LambdaFunction] + /// [HttpApiAuthorizer(Name = "MyAuthorizer")] + /// public APIGatewayCustomAuthorizerV2SimpleResponse Authorize(APIGatewayCustomAuthorizerV2Request request) + /// { + /// // Validate token and return authorization response + /// } + /// + /// [LambdaFunction] + /// [HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = "MyAuthorizer")] + /// public string ProtectedEndpoint() + /// { + /// return "Hello, authenticated user!"; + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public class HttpApiAuthorizerAttribute : Attribute + { + /// + /// Required. Unique name to identify this authorizer. Other functions reference this name + /// via the property. + /// + public string Name { get; set; } + + /// + /// 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. Valid values: "1.0" or "2.0". + /// Defaults to "2.0". + /// + public string PayloadFormatVersion { get; set; } = "2.0"; + + /// + /// 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..611b3be14 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/APIGateway/RestApiAuthorizerAttribute.cs @@ -0,0 +1,76 @@ +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 . + /// + /// + /// + /// [LambdaFunction] + /// [RestApiAuthorizer(Name = "TokenAuthorizer", Type = RestApiAuthorizerType.Token)] + /// public APIGatewayCustomAuthorizerResponse Authorize(APIGatewayCustomAuthorizerRequest request) + /// { + /// var token = request.AuthorizationToken; + /// // Validate token and return IAM policy response + /// } + /// + /// [LambdaFunction] + /// [RestApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = "TokenAuthorizer")] + /// public string ProtectedEndpoint() + /// { + /// return "Hello, authenticated user!"; + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public class RestApiAuthorizerAttribute : Attribute + { + /// + /// Required. Unique name to identify this authorizer. Other functions reference this name + /// via the property. + /// + public string Name { get; set; } + + /// + /// 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/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs index 965b61e1c..df51d12f3 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/CloudFormationWriterTests.cs @@ -874,6 +874,7 @@ public class LambdaFunctionModelTest : ILambdaFunctionSerializable 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/TestCustomAuthorizerApp/AuthorizerFunction.cs b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs index 0b6193563..1fc03f2e1 100644 --- a/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/AuthorizerFunction.cs @@ -1,3 +1,5 @@ +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; @@ -34,6 +36,12 @@ 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( + Name = "HttpApiLambdaAuthorizer", + IdentityHeader = "authorization", + EnableSimpleResponses = true, + PayloadFormatVersion = "2.0")] public APIGatewayCustomAuthorizerV2SimpleResponse HttpApiAuthorize( APIGatewayCustomAuthorizerV2Request request, ILambdaContext context) @@ -110,6 +118,11 @@ public APIGatewayCustomAuthorizerV2SimpleResponse HttpApiAuthorize( /// REST API Lambda Authorizer (Token-based authorizer) /// Returns an IAM policy document along with custom context values /// + [LambdaFunction(ResourceName = "RestApiAuthorizer")] + [RestApiAuthorizer( + Name = "RestApiLambdaAuthorizer", + Type = RestApiAuthorizerType.Token, + IdentityHeader = "Authorization")] public APIGatewayCustomAuthorizerResponse RestApiAuthorize( APIGatewayCustomAuthorizerRequest request, ILambdaContext context) @@ -215,4 +228,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..39734a579 100644 --- a/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs +++ b/Libraries/test/TestCustomAuthorizerApp/ProtectedFunction.cs @@ -17,7 +17,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 = "HttpApiLambdaAuthorizer")] public string GetProtectedData( APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) @@ -87,7 +87,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 = "HttpApiLambdaAuthorizer")] public object GetUserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -122,7 +122,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 = "RestApiLambdaAuthorizer")] public object GetRestUserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -148,7 +148,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 = "HttpApiLambdaAuthorizer")] public object GetHttpApiV1UserInfo( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -174,7 +174,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 = "HttpApiLambdaAuthorizer")] public IHttpResult GetIHttpResult( [FromCustomAuthorizer(Name = "userId")] string userId, [FromCustomAuthorizer(Name = "email")] string email, @@ -196,7 +196,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 = "HttpApiLambdaAuthorizer")] public object GetNonStringUserInfo( APIGatewayHttpApiV2ProxyRequest request, [FromCustomAuthorizer(Name = "numericTenantId")] int tenantId, @@ -244,4 +244,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/aws-lambda-tools-defaults.json b/Libraries/test/TestCustomAuthorizerApp/aws-lambda-tools-defaults.json index 53198ee82..365878413 100644 --- a/Libraries/test/TestCustomAuthorizerApp/aws-lambda-tools-defaults.json +++ b/Libraries/test/TestCustomAuthorizerApp/aws-lambda-tools-defaults.json @@ -8,8 +8,8 @@ "configuration": "Release", "template": "serverless.template", "template-parameters": "", - "s3-bucket": "test-custom-authorizer-app", +"s3-bucket" : "test-custom-authorizer-85104052", "s3-prefix": "TestCustomAuthorizerApp/", - "stack-name": "test-custom-authorizer", - "function-architecture": "x86_64" -} \ No newline at end of file +"stack-name" : "test-custom-authorizer-85104052", +"function-architecture" : "x86_64" +} diff --git a/Libraries/test/TestCustomAuthorizerApp/serverless.template b/Libraries/test/TestCustomAuthorizerApp/serverless.template index 56ae0368d..53913b3b0 100644 --- a/Libraries/test/TestCustomAuthorizerApp/serverless.template +++ b/Libraries/test/TestCustomAuthorizerApp/serverless.template @@ -1,622 +1,388 @@ { "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" + "CustomHttpApi": { + "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" + ] + } + } } } } }, - "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/*" + "CustomRestApi": { + "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" } }, - "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": "dotnet8", + "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", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "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", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "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": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "CustomHttpApi" + } + } + } } } }, - "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", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "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": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "CustomHttpApi" + } + } + } } } }, - "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" ] - }, - "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": "dotnet8", + "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" + } + } } } }, "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", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "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": "RestApiLambdaAuthorizer" + }, + "RestApiId": { + "Ref": "CustomRestApi" + } + } } } } }, - "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", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "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": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "CustomHttpApi" + } + } + } } } }, "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", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "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": "HttpApiLambdaAuthorizer" + }, + "ApiId": { + "Ref": "CustomHttpApi" + } + } + } } } }, "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", - "CodeUri": ".", "Runtime": "dotnet8", + "CodeUri": ".", + "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" - } - } - }, - "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", - "Properties": { - "ApiId": { - "Ref": "ServerlessHttpApi" - }, - "RouteKey": "GET /api/nonstring-user-info", - "AuthorizationType": "CUSTOM", - "AuthorizerId": { - "Ref": "HttpApiAuthorizer" - }, - "Target": { - "Fn::Sub": "integrations/${NonStringUserInfoIntegration}" + ], + "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": "CustomHttpApi" + } + } + } } } } - }, - "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" - } - } } } \ No newline at end of file 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/TestExecutableServerlessApp/serverless.template b/Libraries/test/TestExecutableServerlessApp/serverless.template index ac43959b7..1912a4498 100644 --- a/Libraries/test/TestExecutableServerlessApp/serverless.template +++ b/Libraries/test/TestExecutableServerlessApp/serverless.template @@ -22,19 +22,10 @@ } }, "Resources": { - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { + "TestServerlessAppVoidExampleVoidReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -51,33 +42,15 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "OkResponseWithHeader" - } - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheader/{x}", - "Method": "GET" - } + "ANNOTATIONS_HANDLER": "VoidReturn" } } } }, - "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { + "TestServerlessAppDynamicExampleDynamicReturnGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -94,33 +67,15 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "OkResponseWithHeaderAsync" - } - }, - "Events": { - "RootGet": { - "Type": "Api", - "Properties": { - "Path": "/okresponsewithheaderasync/{x}", - "Method": "GET" - } + "ANNOTATIONS_HANDLER": "DynamicReturn" } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { + "TestServerlessAppDynamicExampleDynamicInputGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { "MemorySize": 512, @@ -137,21 +92,12 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv2/{x}", - "Method": "GET" - } + "ANNOTATIONS_HANDLER": "DynamicInput" } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { + "GreeterSayHello": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -161,12 +107,13 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method" + "Method", + "PayloadFormatVersion" ] } }, "Properties": { - "MemorySize": 512, + "MemorySize": 1024, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -180,21 +127,22 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2Async" + "ANNOTATIONS_HANDLER": "SayHello" } }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/notfoundwithheaderv2async/{x}", - "Method": "GET" + "Path": "/Greeter/SayHello", + "Method": "GET", + "PayloadFormatVersion": "1.0" } } } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { + "GreeterSayHelloAsync": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -211,7 +159,7 @@ }, "Properties": { "MemorySize": 512, - "Timeout": 30, + "Timeout": 50, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -224,14 +172,14 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1" + "ANNOTATIONS_HANDLER": "SayHelloAsync" } }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/notfoundwithheaderv1/{x}", + "Path": "/Greeter/SayHelloAsync", "Method": "GET", "PayloadFormatVersion": "1.0" } @@ -239,52 +187,29 @@ } } }, - "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { + "ToLower": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations", - "SyncedEvents": [ - "RootGet" - ], - "SyncedEventProperties": { - "RootGet": [ - "Path", - "Method", - "PayloadFormatVersion" - ] - } + "Tool": "Amazon.Lambda.Annotations" }, "Properties": { + "Runtime": "provided.al2", + "CodeUri": ".", "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, + "PackageType": "Zip", + "Handler": "TestExecutableServerlessApp", "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1Async" - } - }, - "Events": { - "RootGet": { - "Type": "HttpApi", - "Properties": { - "Path": "/notfoundwithheaderv1async/{x}", - "Method": "GET", - "PayloadFormatVersion": "1.0" - } + "ANNOTATIONS_HANDLER": "ToLower" } } } }, - "TestServerlessAppDynamicExampleDynamicReturnGenerated": { + "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -304,37 +229,34 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "DynamicReturn" + "ANNOTATIONS_HANDLER": "HasIntrinsic" } } } }, - "TestServerlessAppDynamicExampleDynamicInputGenerated": { + "TestServerlessAppParameterlessMethodWithResponseNoParameterWithResponseGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" }, "Properties": { + "Runtime": "provided.al2", + "CodeUri": ".", "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], - "PackageType": "Image", - "ImageUri": ".", - "ImageConfig": { - "Command": [ - "TestExecutableServerlessApp" - ] - }, + "PackageType": "Zip", + "Handler": "TestExecutableServerlessApp", "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "DynamicInput" + "ANNOTATIONS_HANDLER": "NoParameterWithResponse" } } } }, - "GreeterSayHello": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -344,13 +266,12 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, "Properties": { - "MemorySize": 1024, + "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" @@ -364,22 +285,21 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "SayHello" + "ANNOTATIONS_HANDLER": "OkResponseWithHeader" } }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/Greeter/SayHello", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/okresponsewithheader/{x}", + "Method": "GET" } } } } }, - "GreeterSayHelloAsync": { + "TestServerlessAppCustomizeResponseExamplesOkResponseWithHeaderAsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -389,14 +309,13 @@ "SyncedEventProperties": { "RootGet": [ "Path", - "Method", - "PayloadFormatVersion" + "Method" ] } }, "Properties": { "MemorySize": 512, - "Timeout": 50, + "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], @@ -409,25 +328,33 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "SayHelloAsync" + "ANNOTATIONS_HANDLER": "OkResponseWithHeaderAsync" } }, "Events": { "RootGet": { - "Type": "HttpApi", + "Type": "Api", "Properties": { - "Path": "/Greeter/SayHelloAsync", - "Method": "GET", - "PayloadFormatVersion": "1.0" + "Path": "/okresponsewithheaderasync/{x}", + "Method": "GET" } } } } }, - "TestServerlessAppIntrinsicExampleHasIntrinsicGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2Generated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -444,12 +371,21 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "HasIntrinsic" + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv2/{x}", + "Method": "GET" + } } } } }, - "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV2AsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -478,65 +414,111 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NullableHeaderHttpApi" + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV2Async" } }, "Events": { "RootGet": { "Type": "HttpApi", "Properties": { - "Path": "/nullableheaderhttpapi", + "Path": "/notfoundwithheaderv2async/{x}", "Method": "GET" } } } } }, - "TestServerlessAppParameterlessMethodsNoParameterGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1Generated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } }, "Properties": { - "Runtime": "provided.al2", - "CodeUri": ".", "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], - "PackageType": "Zip", - "Handler": "TestExecutableServerlessApp", + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NoParameter" + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv1/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } } } } }, - "TestServerlessAppParameterlessMethodWithResponseNoParameterWithResponseGenerated": { + "TestServerlessAppCustomizeResponseExamplesNotFoundResponseWithHeaderV1AsyncGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } }, "Properties": { - "Runtime": "provided.al2", - "CodeUri": ".", "MemorySize": 512, "Timeout": 30, "Policies": [ "AWSLambdaBasicExecutionRole" ], - "PackageType": "Zip", - "Handler": "TestExecutableServerlessApp", + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestExecutableServerlessApp" + ] + }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "NoParameterWithResponse" + "ANNOTATIONS_HANDLER": "NotFoundResponseWithHeaderV1Async" + } + }, + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/notfoundwithheaderv1async/{x}", + "Method": "GET", + "PayloadFormatVersion": "1.0" + } } } } }, - "TestExecutableServerlessAppSourceGenerationSerializationExampleGetPersonGenerated": { + "TestServerlessAppNullableReferenceTypeExampleNullableHeaderHttpApiGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations", @@ -565,14 +547,14 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "GetPerson" + "ANNOTATIONS_HANDLER": "NullableHeaderHttpApi" } }, "Events": { "RootGet": { - "Type": "Api", + "Type": "HttpApi", "Properties": { - "Path": "/", + "Path": "/nullableheaderhttpapi", "Method": "GET" } } @@ -604,7 +586,7 @@ } } }, - "ToLower": { + "TestServerlessAppParameterlessMethodsNoParameterGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { "Tool": "Amazon.Lambda.Annotations" @@ -621,7 +603,7 @@ "Handler": "TestExecutableServerlessApp", "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "ToLower" + "ANNOTATIONS_HANDLER": "NoParameter" } } } @@ -651,10 +633,19 @@ } } }, - "TestServerlessAppVoidExampleVoidReturnGenerated": { + "TestExecutableServerlessAppSourceGenerationSerializationExampleGetPersonGenerated": { "Type": "AWS::Serverless::Function", "Metadata": { - "Tool": "Amazon.Lambda.Annotations" + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -671,7 +662,16 @@ }, "Environment": { "Variables": { - "ANNOTATIONS_HANDLER": "VoidReturn" + "ANNOTATIONS_HANDLER": "GetPerson" + } + }, + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/", + "Method": "GET" + } } } } diff --git a/Libraries/test/TestServerlessApp/AuthorizerFunctions.cs b/Libraries/test/TestServerlessApp/AuthorizerFunctions.cs new file mode 100644 index 000000000..2e0011a1b --- /dev/null +++ b/Libraries/test/TestServerlessApp/AuthorizerFunctions.cs @@ -0,0 +1,192 @@ +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; +using System.Collections.Generic; + +namespace TestServerlessApp +{ + /// + /// Example demonstrating Lambda Authorizer annotations + /// + public class AuthorizerFunctions + { + /// + /// HTTP API Lambda Authorizer - validates tokens and returns user context + /// + [LambdaFunction(ResourceName = "HttpApiAuthorizer")] + [HttpApiAuthorizer(Name = "MyHttpAuthorizer")] + 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 }; + } + + /// + /// Protected HTTP API endpoint - requires authorization + /// + [LambdaFunction(ResourceName = "ProtectedHttpApiFunction")] + [HttpApi(LambdaHttpMethod.Get, "/api/protected", Authorizer = "MyHttpAuthorizer")] + 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" }; + } + + /// + /// Another protected HTTP API endpoint using the same authorizer + /// + [LambdaFunction(ResourceName = "AdminHttpApiFunction")] + [HttpApi(LambdaHttpMethod.Get, "/api/admin", Authorizer = "MyHttpAuthorizer")] + public object AdminEndpoint( + [FromCustomAuthorizer(Name = "userId")] string userId, + [FromCustomAuthorizer(Name = "role")] string role) + { + return new { Message = $"Hello admin {userId}!", Role = role }; + } + + /// + /// Public HTTP API endpoint - no authorization required + /// + [LambdaFunction(ResourceName = "PublicHttpApiFunction")] + [HttpApi(LambdaHttpMethod.Get, "/api/public")] + public string PublicEndpoint() + { + return "This is a public endpoint"; + } + + /// + /// REST API Lambda Authorizer with Token type + /// + [LambdaFunction(ResourceName = "RestApiAuthorizer")] + [RestApiAuthorizer(Name = "MyRestAuthorizer", Type = RestApiAuthorizerType.Token)] + public APIGatewayCustomAuthorizerResponse AuthorizeRestApi( + APIGatewayCustomAuthorizerRequest request, + ILambdaContext context) + { + // Token is directly available for Token-type authorizers + 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 Deny policy for invalid tokens + 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 } + } + } + } + }; + } + + /// + /// Protected REST API endpoint + /// + [LambdaFunction(ResourceName = "ProtectedRestApiFunction")] + [RestApi(LambdaHttpMethod.Get, "/api/rest/protected", Authorizer = "MyRestAuthorizer")] + public object GetRestProtectedResource( + [FromCustomAuthorizer(Name = "userId")] string userId) + { + return new { UserId = userId, Source = "REST API" }; + } + + /// + /// HTTP API Authorizer with custom header and caching + /// + [LambdaFunction(ResourceName = "ApiKeyAuthorizer")] + [HttpApiAuthorizer( + Name = "ApiKeyAuth", + 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 }; + } + + /// + /// Endpoint protected by API key authorizer + /// + [LambdaFunction(ResourceName = "ApiKeyProtectedFunction")] + [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 }; + } + + private bool IsValidToken(string token) + { + return token?.StartsWith("Bearer ") == true; + } + } +} \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index 0e3befbe1..b238c84ee 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -1175,6 +1175,331 @@ } } } + }, + "HttpApiAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp::TestServerlessApp.AuthorizerFunctions_AuthorizeHttpApi_Generated::AuthorizeHttpApi" + } + }, + "ProtectedHttpApiFunction": { + "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": "TestServerlessApp::TestServerlessApp.AuthorizerFunctions_GetProtectedResource_Generated::GetProtectedResource", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/protected", + "Method": "GET", + "Auth": { + "Authorizer": "MyHttpAuthorizer" + }, + "ApiId": { + "Ref": "CustomHttpApi" + } + } + } + } + } + }, + "AdminHttpApiFunction": { + "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": "TestServerlessApp::TestServerlessApp.AuthorizerFunctions_AdminEndpoint_Generated::AdminEndpoint", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/admin", + "Method": "GET", + "Auth": { + "Authorizer": "MyHttpAuthorizer" + }, + "ApiId": { + "Ref": "CustomHttpApi" + } + } + } + } + } + }, + "PublicHttpApiFunction": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "RootGet" + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp::TestServerlessApp.AuthorizerFunctions_PublicEndpoint_Generated::PublicEndpoint", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/public", + "Method": "GET" + } + } + } + } + }, + "RestApiAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp::TestServerlessApp.AuthorizerFunctions_AuthorizeRestApi_Generated::AuthorizeRestApi" + } + }, + "ProtectedRestApiFunction": { + "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": "TestServerlessApp::TestServerlessApp.AuthorizerFunctions_GetRestProtectedResource_Generated::GetRestProtectedResource", + "Events": { + "RootGet": { + "Type": "Api", + "Properties": { + "Path": "/api/rest/protected", + "Method": "GET", + "Auth": { + "Authorizer": "MyRestAuthorizer" + }, + "RestApiId": { + "Ref": "CustomRestApi" + } + } + } + } + } + }, + "ApiKeyAuthorizer": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp::TestServerlessApp.AuthorizerFunctions_ValidateApiKey_Generated::ValidateApiKey" + } + }, + "ApiKeyProtectedFunction": { + "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": "TestServerlessApp::TestServerlessApp.AuthorizerFunctions_ExternalEndpoint_Generated::ExternalEndpoint", + "Events": { + "RootGet": { + "Type": "HttpApi", + "Properties": { + "Path": "/api/external", + "Method": "GET", + "Auth": { + "Authorizer": "ApiKeyAuth" + }, + "ApiId": { + "Ref": "CustomHttpApi" + } + } + } + } + } + }, + "CustomHttpApi": { + "Type": "AWS::Serverless::HttpApi", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "Auth": { + "Authorizers": { + "MyHttpAuthorizer": { + "FunctionArn": { + "Fn::GetAtt": [ + "HttpApiAuthorizer", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "2.0", + "EnableSimpleResponses": true, + "Identity": { + "Headers": [ + "Authorization" + ] + } + }, + "ApiKeyAuth": { + "FunctionArn": { + "Fn::GetAtt": [ + "ApiKeyAuthorizer", + "Arn" + ] + }, + "AuthorizerPayloadFormatVersion": "2.0", + "EnableSimpleResponses": true, + "Identity": { + "Headers": [ + "X-Api-Key" + ] + } + } + } + } + } + }, + "CustomRestApi": { + "Type": "AWS::Serverless::Api", + "Properties": { + "StageName": "Prod", + "Auth": { + "Authorizers": { + "MyRestAuthorizer": { + "FunctionArn": { + "Fn::GetAtt": [ + "RestApiAuthorizer", + "Arn" + ] + }, + "Identity": { + "Header": "Authorization" + }, + "FunctionPayloadType": "TOKEN" + } + } + } + }, + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + } } }, "Outputs": {