Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
168 changes: 168 additions & 0 deletions Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}

Comment on lines +173 to +179
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The PR introduces ValidateAuthorizerModel / ValidateAuthorizerReferences, but they are never invoked. As a result: (1) missing/invalid authorizer config won't produce diagnostics, and (2) duplicates or bad references can reach CloudFormationWriter and crash (e.g., via ToDictionary). Call these validations after collecting annotationReport.Authorizers / annotationReport.LambdaFunctions and treat failures as fatal for template sync.

Copilot uses AI. Check for mistakes.
var template = new LambdaFunctionTemplate(lambdaFunctionModel);

string sourceText;
Expand Down Expand Up @@ -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));
}

/// <summary>
/// Extracts authorizer model from method symbol if it has HttpApiAuthorizer or RestApiAuthorizer attribute.
/// </summary>
/// <param name="methodSymbol">The method symbol to check for authorizer attributes</param>
/// <param name="lambdaResourceName">The CloudFormation resource name for the Lambda function</param>
/// <returns>AuthorizerModel if an authorizer attribute is found, null otherwise</returns>
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;
}

/// <summary>
/// Validates an authorizer model.
/// </summary>
/// <param name="model">The authorizer model to validate</param>
/// <param name="attributeName">The name of the attribute for error messages</param>
/// <param name="methodLocation">The location of the method for diagnostic reporting</param>
/// <param name="diagnosticReporter">The diagnostic reporter for validation errors</param>
/// <returns>True if valid, false otherwise</returns>
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;
}

/// <summary>
/// Validates authorizer references in lambda functions.
/// </summary>
/// <param name="annotationReport">The annotation report containing all functions and authorizers</param>
/// <param name="diagnosticReporter">The diagnostic reporter for validation errors</param>
/// <returns>True if all authorizer references are valid, false otherwise</returns>
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<HttpApiAttribute>);
var usesRestApi = function.Attributes.Any(a => a is AttributeModel<RestApiAttribute>);

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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ public class AnnotationReport
/// </summary>
public IList<ILambdaFunctionSerializable> LambdaFunctions { get; } = new List<ILambdaFunctionSerializable>();

/// <summary>
/// Collection of Lambda authorizers detected in the project
/// </summary>
public IList<AuthorizerModel> Authorizers { get; } = new List<AuthorizerModel>();

/// <summary>
/// Path to the CloudFormation template for the Lambda project
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Linq;
using Amazon.Lambda.Annotations.APIGateway;
using Microsoft.CodeAnalysis;

namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
{
/// <summary>
/// Builder for <see cref="HttpApiAuthorizerAttribute"/>.
/// </summary>
public static class HttpApiAuthorizerAttributeBuilder
{
/// <summary>
/// Builds an <see cref="HttpApiAuthorizerAttribute"/> from the Roslyn attribute data.
/// </summary>
/// <param name="att">The attribute data from Roslyn</param>
/// <returns>The populated attribute instance</returns>
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;
}

/// <summary>
/// Builds an <see cref="AuthorizerModel"/> from the attribute and lambda function resource name.
/// </summary>
/// <param name="att">The attribute data from Roslyn</param>
/// <param name="lambdaResourceName">The CloudFormation resource name for the Lambda function</param>
/// <returns>The populated authorizer model</returns>
public static AuthorizerModel BuildModel(AttributeData att, string lambdaResourceName)
{
var attribute = Build(att);
return BuildModel(attribute, lambdaResourceName);
}

/// <summary>
/// Builds an <see cref="AuthorizerModel"/> from the attribute and lambda function resource name.
/// </summary>
/// <param name="attribute">The parsed attribute</param>
/// <param name="lambdaResourceName">The CloudFormation resource name for the Lambda function</param>
/// <returns>The populated authorizer model</returns>
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
};
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using Amazon.Lambda.Annotations.APIGateway;
using Microsoft.CodeAnalysis;

Expand All @@ -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;
}
}
}
}
Loading
Loading