-
Notifications
You must be signed in to change notification settings - Fork 494
Expand file tree
/
Copy pathLambdaFunctionValidator.cs
More file actions
334 lines (289 loc) · 18.6 KB
/
LambdaFunctionValidator.cs
File metadata and controls
334 lines (289 loc) · 18.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics;
using Amazon.Lambda.Annotations.SourceGenerator.Extensions;
using Amazon.Lambda.Annotations.SourceGenerator.Models;
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
using Amazon.Lambda.Annotations.S3;
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace Amazon.Lambda.Annotations.SourceGenerator.Validation
{
internal static class LambdaFunctionValidator
{
// Only allow alphanumeric characters
private static readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$");
// Regex for the 'Name' property for API Gateway attributes - https://docs.aws.amazon.com/apigateway/latest/developerguide/request-response-data-mappings.html
private static readonly Regex _parameterAttributeNameRegex = new Regex("^[a-zA-Z0-9._$-]+$");
internal static bool ValidateFunction(GeneratorExecutionContext context, IMethodSymbol lambdaMethodSymbol, Location methodLocation, LambdaFunctionModel lambdaFunctionModel, DiagnosticReporter diagnosticReporter)
{
var diagnostics = new List<Diagnostic>();
// Validate the resource name
if (!_resourceNameRegex.IsMatch(lambdaFunctionModel.ResourceName))
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidResourceName, methodLocation));
}
// Check the handler length does not exceed 127 characters when the package type is set to zip
// The official AWS docs state a 128 character limit on the Lambda handler. However, there is an open issue where the last character is stripped off
// when the handler is exactly 128 characters long. Hence, we are enforcing a 127 character limit.
// https://github.com/aws/aws-lambda-dotnet/issues/1642
if (lambdaFunctionModel.PackageType == LambdaPackageType.Zip && lambdaFunctionModel.Handler.Length > 127)
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.MaximumHandlerLengthExceeded, methodLocation, lambdaFunctionModel.Handler));
}
// Check for Serializer attribute
if (!lambdaMethodSymbol.ContainingAssembly.HasAttribute(context, TypeFullNames.LambdaSerializerAttribute))
{
if (!lambdaMethodSymbol.HasAttribute(context, TypeFullNames.LambdaSerializerAttribute))
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.MissingLambdaSerializer, methodLocation));
}
}
// Check for multiple event types
if (lambdaFunctionModel.LambdaMethod.Events.Count > 1)
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.MultipleEventsNotSupported, methodLocation));
// If multiple event types are encountered then return early without validating each individual event
// since at this point we do not know which event type does the user want to preserve
return ReportDiagnostics(diagnosticReporter, diagnostics);
}
// Validate Events
ValidateApiGatewayEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateSqsEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateS3Events(lambdaFunctionModel, methodLocation, diagnostics);
return ReportDiagnostics(diagnosticReporter, diagnostics);
}
internal static bool ValidateDependencies(GeneratorExecutionContext context, IMethodSymbol lambdaMethodSymbol, Location methodLocation, DiagnosticReporter diagnosticReporter)
{
// Check for references to "Amazon.Lambda.APIGatewayEvents" if the Lambda method is annotated with RestApi, HttpApi, or authorizer attributes.
if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.RestApiAttribute) || lambdaMethodSymbol.HasAttribute(context, TypeFullNames.HttpApiAttribute)
|| lambdaMethodSymbol.HasAttribute(context, TypeFullNames.HttpApiAuthorizerAttribute) || lambdaMethodSymbol.HasAttribute(context, TypeFullNames.RestApiAuthorizerAttribute))
{
if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.APIGatewayEvents") == null)
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.APIGatewayEvents"));
return false;
}
}
// Check for references to "Amazon.Lambda.SQSEvents" if the Lambda method is annotated with SQSEvent attribute.
if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.SQSEventAttribute))
{
if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.SQSEvents") == null)
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.SQSEvents"));
return false;
}
}
// Check for references to "Amazon.Lambda.S3Events" if the Lambda method is annotated with S3Event attribute.
if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.S3EventAttribute))
{
if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.S3Events") == null)
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.S3Events"));
return false;
}
}
return true;
}
private static void ValidateApiGatewayEvents(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List<Diagnostic> diagnostics)
{
var isApiEvent = lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.API);
var isAuthorizerEvent = lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.Authorizer);
// IHttpResult is only valid for API Gateway events (not Authorizer events)
if (!isApiEvent && lambdaFunctionModel.LambdaMethod.ReturnsIHttpResults)
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.HttpResultsOnNonApiFunction, methodLocation));
}
// IAuthorizerResult is only valid for Authorizer events
if (!isAuthorizerEvent && lambdaFunctionModel.LambdaMethod.ReturnsIAuthorizerResult)
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.AuthorizerResultOnNonAuthorizerFunction, methodLocation));
}
// If the method does not contain any API or Authorizer events, then it cannot have
// parameters that are annotated with HTTP API attributes.
// Authorizer functions also support FromHeader, FromQuery, FromRoute attributes.
if (!isApiEvent && !isAuthorizerEvent)
{
foreach (var parameter in lambdaFunctionModel.LambdaMethod.Parameters)
{
if (parameter.Attributes.Any(att =>
att.Type.FullName == TypeFullNames.FromBodyAttribute ||
att.Type.FullName == TypeFullNames.FromHeaderAttribute ||
att.Type.FullName == TypeFullNames.FromRouteAttribute ||
att.Type.FullName == TypeFullNames.FromQueryAttribute))
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.ApiParametersOnNonApiFunction, methodLocation));
}
}
return;
}
// Authorizer-specific parameter validation
if (isAuthorizerEvent)
{
foreach (var parameter in lambdaFunctionModel.LambdaMethod.Parameters)
{
// [FromBody] is not supported on authorizer functions
if (parameter.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromBodyAttribute))
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.FromBodyNotSupportedOnAuthorizer, methodLocation));
}
// Validate [FromQuery] parameter types - only primitive types allowed
if (parameter.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromQueryAttribute))
{
if (!parameter.Type.IsPrimitiveType() && !parameter.Type.IsPrimitiveEnumerableType())
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.UnsupportedMethodParameterType, methodLocation, parameter.Name, parameter.Type.FullName));
}
}
// Validate attribute names for FromQuery, FromRoute, and FromHeader
foreach (var att in parameter.Attributes)
{
var parameterAttributeName = string.Empty;
switch (att.Type.FullName)
{
case TypeFullNames.FromQueryAttribute:
var fromQueryAttribute = (AttributeModel<APIGateway.FromQueryAttribute>)att;
parameterAttributeName = fromQueryAttribute.Data.Name;
break;
case TypeFullNames.FromRouteAttribute:
var fromRouteAttribute = (AttributeModel<APIGateway.FromRouteAttribute>)att;
parameterAttributeName = fromRouteAttribute.Data.Name;
break;
case TypeFullNames.FromHeaderAttribute:
var fromHeaderAttribute = (AttributeModel<APIGateway.FromHeaderAttribute>)att;
parameterAttributeName = fromHeaderAttribute.Data.Name;
break;
default:
break;
}
if (!string.IsNullOrEmpty(parameterAttributeName) && !_parameterAttributeNameRegex.IsMatch(parameterAttributeName))
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidParameterAttributeName, methodLocation, parameterAttributeName, parameter.Name));
}
}
}
return;
}
// Validate FromRoute, FromQuery and FromHeader parameters
foreach (var parameter in lambdaFunctionModel.LambdaMethod.Parameters)
{
if (parameter.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromQueryAttribute))
{
var fromQueryAttribute = parameter.Attributes.First(att => att.Type.FullName == TypeFullNames.FromQueryAttribute) as AttributeModel<APIGateway.FromQueryAttribute>;
// Use parameter name as key, if Name has not specified explicitly in the attribute definition.
var parameterKey = fromQueryAttribute?.Data?.Name ?? parameter.Name;
if (!parameter.Type.IsPrimitiveType() && !parameter.Type.IsPrimitiveEnumerableType())
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.UnsupportedMethodParameterType, methodLocation, parameter.Name, parameter.Type.FullName));
}
}
foreach (var att in parameter.Attributes)
{
var parameterAttributeName = string.Empty;
switch (att.Type.FullName)
{
case TypeFullNames.FromQueryAttribute:
var fromQueryAttribute = (AttributeModel<APIGateway.FromQueryAttribute>)att;
parameterAttributeName = fromQueryAttribute.Data.Name;
break;
case TypeFullNames.FromRouteAttribute:
var fromRouteAttribute = (AttributeModel<APIGateway.FromRouteAttribute>)att;
parameterAttributeName = fromRouteAttribute.Data.Name;
break;
case TypeFullNames.FromHeaderAttribute:
var fromHeaderAttribute = (AttributeModel<APIGateway.FromHeaderAttribute>)att;
parameterAttributeName = fromHeaderAttribute.Data.Name;
break;
default:
break;
}
if (!string.IsNullOrEmpty(parameterAttributeName) && !_parameterAttributeNameRegex.IsMatch(parameterAttributeName))
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidParameterAttributeName, methodLocation, parameterAttributeName, parameter.Name));
}
}
}
}
private static void ValidateSqsEvents(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List<Diagnostic> diagnostics)
{
// If the method does not contain any SQS events, then simply return early
if (!lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.SQS))
{
return;
}
// Validate SQSEventAttributes
foreach (var att in lambdaFunctionModel.Attributes)
{
if (att.Type.FullName != TypeFullNames.SQSEventAttribute)
continue;
var sqsEventAttribute = ((AttributeModel<SQSEventAttribute>)att).Data;
var validationErrors = sqsEventAttribute.Validate();
validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidSqsEventAttribute, methodLocation, errorMessage)));
}
// Validate method parameters - When using SQSEventAttribute, the method signature must be (SQSEvent sqsEvent) or (SQSEvent sqsEvent, ILambdaContext context)
var parameters = lambdaFunctionModel.LambdaMethod.Parameters;
if (parameters.Count == 0 ||
parameters.Count > 2 ||
(parameters.Count == 1 && parameters[0].Type.FullName != TypeFullNames.SQSEvent) ||
(parameters.Count == 2 && (parameters[0].Type.FullName != TypeFullNames.SQSEvent || parameters[1].Type.FullName != TypeFullNames.ILambdaContext)))
{
var errorMessage = $"When using the {nameof(SQSEventAttribute)}, the Lambda method can accept at most 2 parameters. " +
$"The first parameter is required and must be of type {TypeFullNames.SQSEvent}. " +
$"The second parameter is optional and must be of type {TypeFullNames.ILambdaContext}.";
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
}
// Validate method return type - When using SQSEventAttribute, the return type must be either void, Task, SQSBatchResponse or Task<SQSBatchResponse>
if (!lambdaFunctionModel.LambdaMethod.ReturnsVoidTaskOrSqsBatchResponse)
{
var errorMessage = $"When using the {nameof(SQSEventAttribute)}, the Lambda method can return either void, {TypeFullNames.Task}, {TypeFullNames.SQSBatchResponse} or Task<{TypeFullNames.SQSBatchResponse}>";
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
}
}
private static void ValidateS3Events(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List<Diagnostic> diagnostics)
{
if (!lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.S3))
return;
// Validate S3EventAttributes
foreach (var att in lambdaFunctionModel.Attributes)
{
if (att.Type.FullName != TypeFullNames.S3EventAttribute)
continue;
var s3EventAttribute = ((AttributeModel<S3EventAttribute>)att).Data;
var validationErrors = s3EventAttribute.Validate();
validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidS3EventAttribute, methodLocation, errorMessage)));
}
// Validate method parameters - first param must be S3Event, optional second param ILambdaContext
var parameters = lambdaFunctionModel.LambdaMethod.Parameters;
if (parameters.Count == 0 ||
parameters.Count > 2 ||
(parameters.Count == 1 && parameters[0].Type.FullName != TypeFullNames.S3Event) ||
(parameters.Count == 2 && (parameters[0].Type.FullName != TypeFullNames.S3Event || parameters[1].Type.FullName != TypeFullNames.ILambdaContext)))
{
var errorMessage = $"When using the {nameof(S3EventAttribute)}, the Lambda method can accept at most 2 parameters. " +
$"The first parameter is required and must be of type {TypeFullNames.S3Event}. " +
$"The second parameter is optional and must be of type {TypeFullNames.ILambdaContext}.";
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
}
// Validate method return type - must be void or Task
if (!lambdaFunctionModel.LambdaMethod.ReturnsVoid && !lambdaFunctionModel.LambdaMethod.ReturnsVoidTask)
{
var errorMessage = $"When using the {nameof(S3EventAttribute)}, the Lambda method can return either void or {TypeFullNames.Task}";
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
}
}
private static bool ReportDiagnostics(DiagnosticReporter diagnosticReporter, List<Diagnostic> diagnostics)
{
var isValid = true;
foreach (var diagnostic in diagnostics)
{
diagnosticReporter.Report(diagnostic);
if (diagnostic.Severity == DiagnosticSeverity.Error)
{
isValid = false;
}
}
return isValid;
}
}
}