Skip to content

Commit c5690d5

Browse files
aaythapahoffa
andauthored
feat: Add EnableFunctionDefaultPermissions property to HTTP Api (#3001)
Co-authored-by: Christoffer Rehn <[email protected]>
1 parent e2dd6f6 commit c5690d5

13 files changed

+1232
-10
lines changed

samtranslator/internal/schema_source/aws_serverless_httpapi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class LambdaAuthorizer(BaseModel):
4747
EnableSimpleResponses: Optional[bool] = lambdaauthorizer("EnableSimpleResponses")
4848
FunctionArn: SamIntrinsicable[str] = lambdaauthorizer("FunctionArn")
4949
FunctionInvokeRole: Optional[SamIntrinsicable[str]] = lambdaauthorizer("FunctionInvokeRole")
50+
EnableFunctionDefaultPermissions: Optional[bool] # TODO: add docs
5051
Identity: Optional[LambdaAuthorizerIdentity] = lambdaauthorizer("Identity")
5152

5253

samtranslator/model/api/http_api_generator.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
ApiGatewayV2Stage,
1212
)
1313
from samtranslator.model.exceptions import InvalidResourceException
14-
from samtranslator.model.intrinsics import fnGetAtt, is_intrinsic, is_intrinsic_no_value, ref
14+
from samtranslator.model.intrinsics import fnGetAtt, fnSub, is_intrinsic, is_intrinsic_no_value, ref
15+
from samtranslator.model.lambda_ import LambdaPermission
1516
from samtranslator.model.route53 import Route53RecordSetGroup
1617
from samtranslator.model.s3_utils.uri_parser import parse_s3_uri
1718
from samtranslator.open_api.open_api import OpenApiEditor
19+
from samtranslator.translator.arn_generator import ArnGenerator
1820
from samtranslator.translator.logical_id_generator import LogicalIdGenerator
1921
from samtranslator.utils.types import Intrinsicable
2022
from samtranslator.utils.utils import InvalidValueType, dict_deep_get
@@ -511,6 +513,60 @@ def _add_tags(self) -> None:
511513
open_api_editor.add_tags(self.tags)
512514
self.definition_body = open_api_editor.openapi
513515

516+
def _get_permission(
517+
self, authorizer_name: str, authorizer_lambda_function_arn: str, api_arn: str
518+
) -> LambdaPermission:
519+
"""Constructs and returns the Lambda Permission resource allowing the Authorizer to invoke the function.
520+
521+
:returns: the permission resource
522+
:rtype: model.lambda_.LambdaPermission
523+
"""
524+
525+
resource = "${__ApiId__}/authorizers/*"
526+
source_arn = fnSub(
527+
ArnGenerator.generate_arn(partition="${AWS::Partition}", service="execute-api", resource=resource),
528+
{"__ApiId__": api_arn},
529+
)
530+
531+
lambda_permission = LambdaPermission(
532+
self.logical_id + authorizer_name + "AuthorizerPermission", attributes=self.passthrough_resource_attributes
533+
)
534+
lambda_permission.Action = "lambda:InvokeFunction"
535+
lambda_permission.FunctionName = authorizer_lambda_function_arn
536+
lambda_permission.Principal = "apigateway.amazonaws.com"
537+
lambda_permission.SourceArn = source_arn
538+
539+
return lambda_permission
540+
541+
def _construct_authorizer_lambda_permission(self, http_api: ApiGatewayV2HttpApi) -> List[LambdaPermission]:
542+
if not self.auth:
543+
return []
544+
545+
auth_properties = AuthProperties(**self.auth)
546+
authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.EnableIamAuthorizer)
547+
548+
if not authorizers:
549+
return []
550+
551+
permissions: List[LambdaPermission] = []
552+
553+
for authorizer_name, authorizer in authorizers.items():
554+
# Construct permissions for Lambda Authorizers only
555+
# Http Api shouldn't create the permissions by default (when its none)
556+
if (
557+
not authorizer.function_arn
558+
or authorizer.enable_function_default_permissions is None
559+
or not authorizer.enable_function_default_permissions
560+
):
561+
continue
562+
563+
permission = self._get_permission(
564+
authorizer_name, authorizer.function_arn, http_api.get_runtime_attr("http_api_id")
565+
)
566+
permissions.append(permission)
567+
568+
return permissions
569+
514570
def _set_default_authorizer(
515571
self,
516572
open_api_editor: OpenApiEditor,
@@ -582,6 +638,7 @@ def _get_authorizers(
582638
identity=authorizer.get("Identity"),
583639
authorizer_payload_format_version=authorizer.get("AuthorizerPayloadFormatVersion"),
584640
enable_simple_responses=authorizer.get("EnableSimpleResponses"),
641+
enable_function_default_permissions=authorizer.get("EnableFunctionDefaultPermissions"),
585642
)
586643
return authorizers
587644

@@ -719,6 +776,7 @@ def to_cloudformation(
719776
Optional[ApiGatewayV2DomainName],
720777
Optional[List[ApiGatewayV2ApiMapping]],
721778
Optional[Route53RecordSetGroup],
779+
Optional[List[LambdaPermission]],
722780
]:
723781
"""Generates CloudFormation resources from a SAM HTTP API resource
724782
@@ -727,6 +785,7 @@ def to_cloudformation(
727785
"""
728786
http_api = self._construct_http_api()
729787
domain, basepath_mapping, route53 = self._construct_api_domain(http_api, route53_record_set_groups)
788+
permissions = self._construct_authorizer_lambda_permission(http_api)
730789
stage = self._construct_stage()
731790

732-
return http_api, stage, domain, basepath_mapping, route53
791+
return http_api, stage, domain, basepath_mapping, route53, permissions

samtranslator/model/apigatewayv2.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def __init__( # type: ignore[no-untyped-def] # noqa: too-many-arguments
8888
authorizer_payload_format_version=None,
8989
enable_simple_responses=None,
9090
is_aws_iam_authorizer=False,
91+
enable_function_default_permissions=None,
9192
):
9293
"""
9394
Creates an authorizer for use in V2 Http Apis
@@ -103,6 +104,7 @@ def __init__( # type: ignore[no-untyped-def] # noqa: too-many-arguments
103104
self.authorizer_payload_format_version = authorizer_payload_format_version
104105
self.enable_simple_responses = enable_simple_responses
105106
self.is_aws_iam_authorizer = is_aws_iam_authorizer
107+
self.enable_function_default_permissions = enable_function_default_permissions
106108

107109
self._validate_input_parameters()
108110

@@ -115,6 +117,13 @@ def __init__( # type: ignore[no-untyped-def] # noqa: too-many-arguments
115117
if authorizer_type == "REQUEST":
116118
self._validate_lambda_authorizer()
117119

120+
if enable_function_default_permissions is not None:
121+
sam_expect(
122+
enable_function_default_permissions,
123+
api_logical_id,
124+
f"Authorizers.{name}.EnableFunctionDefaultPermissions",
125+
).to_be_a_bool()
126+
118127
def _get_auth_type(self) -> str:
119128
if self.is_aws_iam_authorizer:
120129
return "AWS_IAM"
@@ -166,6 +175,11 @@ def _validate_input_parameters(self) -> None:
166175
self.api_logical_id, "EnableSimpleResponses must be defined only for Lambda Authorizer."
167176
)
168177

178+
if self.enable_function_default_permissions is not None and authorizer_type != "REQUEST":
179+
raise InvalidResourceException(
180+
self.api_logical_id, "EnableFunctionDefaultPermissions must be defined only for Lambda Authorizer."
181+
)
182+
169183
def _validate_jwt_authorizer(self) -> None:
170184
if not self.jwt_configuration:
171185
raise InvalidResourceException(

samtranslator/model/sam_resources.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,9 +1392,12 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def]
13921392
domain,
13931393
basepath_mapping,
13941394
route53,
1395+
permissions,
13951396
) = api_generator.to_cloudformation(kwargs.get("route53_record_set_groups", {}))
13961397

13971398
resources.append(http_api)
1399+
if permissions:
1400+
resources.extend(permissions)
13981401
if domain:
13991402
resources.append(domain)
14001403
if basepath_mapping:

samtranslator/schema/schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195743,6 +195743,10 @@
195743195743
"markdownDescription": "Specifies the format of the payload sent to an HTTP API Lambda authorizer\\. Required for HTTP API Lambda authorizers\\. \nThis is passed through to the `authorizerPayloadFormatVersion` section of an `x-amazon-apigateway-authorizer` in the `securitySchemes` section of an OpenAPI definition\\. \n*Valid values*: `1.0` or `2.0` \n*Type*: String \n*Required*: Yes \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",
195744195744
"title": "AuthorizerPayloadFormatVersion"
195745195745
},
195746+
"EnableFunctionDefaultPermissions": {
195747+
"title": "Enablefunctiondefaultpermissions",
195748+
"type": "boolean"
195749+
},
195746195750
"EnableSimpleResponses": {
195747195751
"description": "Specifies whether a Lambda authorizer returns a response in a simple format\\. By default, a Lambda authorizer must return an AWS Identity and Access Management \\(IAM\\) policy\\. If enabled, the Lambda authorizer can return a boolean value instead of an IAM policy\\. \nThis is passed through to the `enableSimpleResponses` section of an `x-amazon-apigateway-authorizer` in the `securitySchemes` section of an OpenAPI definition\\. \n*Type*: Boolean \n*Required*: No \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",
195748195752
"markdownDescription": "Specifies whether a Lambda authorizer returns a response in a simple format\\. By default, a Lambda authorizer must return an AWS Identity and Access Management \\(IAM\\) policy\\. If enabled, the Lambda authorizer can return a boolean value instead of an IAM policy\\. \nThis is passed through to the `enableSimpleResponses` section of an `x-amazon-apigateway-authorizer` in the `securitySchemes` section of an OpenAPI definition\\. \n*Type*: Boolean \n*Required*: No \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",

schema_source/sam.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,6 +1561,10 @@
15611561
"markdownDescription": "Specifies the format of the payload sent to an HTTP API Lambda authorizer\\. Required for HTTP API Lambda authorizers\\. \nThis is passed through to the `authorizerPayloadFormatVersion` section of an `x-amazon-apigateway-authorizer` in the `securitySchemes` section of an OpenAPI definition\\. \n*Valid values*: `1.0` or `2.0` \n*Type*: String \n*Required*: Yes \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",
15621562
"title": "AuthorizerPayloadFormatVersion"
15631563
},
1564+
"EnableFunctionDefaultPermissions": {
1565+
"title": "Enablefunctiondefaultpermissions",
1566+
"type": "boolean"
1567+
},
15641568
"EnableSimpleResponses": {
15651569
"description": "Specifies whether a Lambda authorizer returns a response in a simple format\\. By default, a Lambda authorizer must return an AWS Identity and Access Management \\(IAM\\) policy\\. If enabled, the Lambda authorizer can return a boolean value instead of an IAM policy\\. \nThis is passed through to the `enableSimpleResponses` section of an `x-amazon-apigateway-authorizer` in the `securitySchemes` section of an OpenAPI definition\\. \n*Type*: Boolean \n*Required*: No \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",
15661570
"markdownDescription": "Specifies whether a Lambda authorizer returns a response in a simple format\\. By default, a Lambda authorizer must return an AWS Identity and Access Management \\(IAM\\) policy\\. If enabled, the Lambda authorizer can return a boolean value instead of an IAM policy\\. \nThis is passed through to the `enableSimpleResponses` section of an `x-amazon-apigateway-authorizer` in the `securitySchemes` section of an OpenAPI definition\\. \n*Type*: Boolean \n*Required*: No \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",

tests/model/test_api_v2.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,21 @@ def test_create_authorizer_fails_with_enable_simple_responses_non_lambda(self):
168168
+ "EnableSimpleResponses must be defined only for Lambda Authorizer.",
169169
)
170170

171+
def test_create_authorizer_fails_with_enable_function_default_permissions_non_lambda(self):
172+
with pytest.raises(InvalidResourceException) as e:
173+
ApiGatewayV2Authorizer(
174+
api_logical_id="logicalId",
175+
name="authName",
176+
jwt_configuration={"config": "value"},
177+
authorization_scopes=["scope1", "scope2"],
178+
enable_function_default_permissions=True,
179+
)
180+
self.assertEqual(
181+
e.value.message,
182+
"Resource with id [logicalId] is invalid. "
183+
+ "EnableFunctionDefaultPermissions must be defined only for Lambda Authorizer.",
184+
)
185+
171186
@mock.patch(
172187
"samtranslator.model.apigatewayv2.ApiGatewayV2Authorizer._get_auth_type", mock.MagicMock(return_value="JWT")
173188
)

tests/translator/input/error_http_api_invalid_lambda_auth.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,26 @@ Resources:
7777
AuthorizerPayloadFormatVersion: 2.0
7878
EnableSimpleResponses: true
7979
DefaultAuthorizer: LambdaAuth
80+
81+
MyApi3:
82+
Type: AWS::Serverless::HttpApi
83+
Properties:
84+
Auth:
85+
Authorizers:
86+
LambdaAuth:
87+
FunctionArn: !GetAtt MyAuthFn.Arn
88+
EnableFunctionDefaultPermissions: foo
89+
AuthorizerPayloadFormatVersion: 2.0
90+
DefaultAuthorizer: LambdaAuth
91+
92+
MyApi4:
93+
Type: AWS::Serverless::HttpApi
94+
Properties:
95+
Auth:
96+
Authorizers:
97+
NonLambdaAuth:
98+
JwtConfiguration:
99+
audience: https://test-sam.com
100+
issuer: https://test-sam.com
101+
EnableFunctionDefaultPermissions: foo
102+
DefaultAuthorizer: LambdaAuth
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
Resources:
2+
HttpApiFunction:
3+
Type: AWS::Serverless::Function
4+
Properties:
5+
CodeUri: s3://sam-demo-bucket/todo_list.zip
6+
Handler: index.restapi
7+
Runtime: python3.7
8+
Events:
9+
Basic:
10+
Type: HttpApi
11+
Properties:
12+
Path: /basic
13+
Method: GET
14+
ApiId: !Ref MyApi
15+
16+
MyAuthFn:
17+
Type: AWS::Serverless::Function
18+
Properties:
19+
CodeUri: s3://bucket/key
20+
Handler: index.handler
21+
Runtime: nodejs12.x
22+
23+
MyApi:
24+
Type: AWS::Serverless::HttpApi
25+
Properties:
26+
Tags:
27+
Tag1: value1
28+
Tag2: value2
29+
Auth:
30+
Authorizers:
31+
LambdaAuthWithEnablePropertyTrue:
32+
# should create permissions resource for this auth
33+
FunctionArn: !GetAtt MyAuthFn.Arn
34+
EnableFunctionDefaultPermissions: true
35+
AuthorizerPayloadFormatVersion: 1.0
36+
LambdaAuthNoEnableProperty:
37+
# should not create permissions resource for this auth as http api doesn't create the resource by default
38+
FunctionArn: !GetAtt MyAuthFn.Arn
39+
AuthorizerPayloadFormatVersion: 1.0
40+
LambdaAuthWithEnablePropertySetFalse:
41+
# should not create permissions resource for this auth
42+
FunctionArn: !GetAtt MyAuthFn.Arn
43+
AuthorizerPayloadFormatVersion: 1.0
44+
EnableFunctionDefaultPermissions: false
45+
LambdaAuthFull:
46+
# should create permissions resource for this auth
47+
FunctionArn: !GetAtt MyAuthFn.Arn
48+
FunctionInvokeRole: !GetAtt MyAuthFnRole.Arn
49+
EnableFunctionDefaultPermissions: true
50+
Identity:
51+
Context:
52+
- contextVar
53+
Headers:
54+
- Authorization
55+
QueryStrings:
56+
- petId
57+
StageVariables:
58+
- stageVar
59+
ReauthorizeEvery: 60
60+
AuthorizerPayloadFormatVersion: 2.0
61+
EnableSimpleResponses: true
62+
DefaultAuthorizer: LambdaAuthWithEnablePropertyTrue

0 commit comments

Comments
 (0)