Skip to content

Commit

Permalink
feat: Add EnableFunctionDefaultPermissions property to HTTP Api (#3001)
Browse files Browse the repository at this point in the history
Co-authored-by: Christoffer Rehn <1280602+hoffa@users.noreply.github.com>
  • Loading branch information
aaythapa and hoffa committed Mar 13, 2023
1 parent e2dd6f6 commit c5690d5
Show file tree
Hide file tree
Showing 13 changed files with 1,232 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class LambdaAuthorizer(BaseModel):
EnableSimpleResponses: Optional[bool] = lambdaauthorizer("EnableSimpleResponses")
FunctionArn: SamIntrinsicable[str] = lambdaauthorizer("FunctionArn")
FunctionInvokeRole: Optional[SamIntrinsicable[str]] = lambdaauthorizer("FunctionInvokeRole")
EnableFunctionDefaultPermissions: Optional[bool] # TODO: add docs
Identity: Optional[LambdaAuthorizerIdentity] = lambdaauthorizer("Identity")


Expand Down
63 changes: 61 additions & 2 deletions samtranslator/model/api/http_api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
ApiGatewayV2Stage,
)
from samtranslator.model.exceptions import InvalidResourceException
from samtranslator.model.intrinsics import fnGetAtt, is_intrinsic, is_intrinsic_no_value, ref
from samtranslator.model.intrinsics import fnGetAtt, fnSub, is_intrinsic, is_intrinsic_no_value, ref
from samtranslator.model.lambda_ import LambdaPermission
from samtranslator.model.route53 import Route53RecordSetGroup
from samtranslator.model.s3_utils.uri_parser import parse_s3_uri
from samtranslator.open_api.open_api import OpenApiEditor
from samtranslator.translator.arn_generator import ArnGenerator
from samtranslator.translator.logical_id_generator import LogicalIdGenerator
from samtranslator.utils.types import Intrinsicable
from samtranslator.utils.utils import InvalidValueType, dict_deep_get
Expand Down Expand Up @@ -511,6 +513,60 @@ def _add_tags(self) -> None:
open_api_editor.add_tags(self.tags)
self.definition_body = open_api_editor.openapi

def _get_permission(
self, authorizer_name: str, authorizer_lambda_function_arn: str, api_arn: str
) -> LambdaPermission:
"""Constructs and returns the Lambda Permission resource allowing the Authorizer to invoke the function.
:returns: the permission resource
:rtype: model.lambda_.LambdaPermission
"""

resource = "${__ApiId__}/authorizers/*"
source_arn = fnSub(
ArnGenerator.generate_arn(partition="${AWS::Partition}", service="execute-api", resource=resource),
{"__ApiId__": api_arn},
)

lambda_permission = LambdaPermission(
self.logical_id + authorizer_name + "AuthorizerPermission", attributes=self.passthrough_resource_attributes
)
lambda_permission.Action = "lambda:InvokeFunction"
lambda_permission.FunctionName = authorizer_lambda_function_arn
lambda_permission.Principal = "apigateway.amazonaws.com"
lambda_permission.SourceArn = source_arn

return lambda_permission

def _construct_authorizer_lambda_permission(self, http_api: ApiGatewayV2HttpApi) -> List[LambdaPermission]:
if not self.auth:
return []

auth_properties = AuthProperties(**self.auth)
authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.EnableIamAuthorizer)

if not authorizers:
return []

permissions: List[LambdaPermission] = []

for authorizer_name, authorizer in authorizers.items():
# Construct permissions for Lambda Authorizers only
# Http Api shouldn't create the permissions by default (when its none)
if (
not authorizer.function_arn
or authorizer.enable_function_default_permissions is None
or not authorizer.enable_function_default_permissions
):
continue

permission = self._get_permission(
authorizer_name, authorizer.function_arn, http_api.get_runtime_attr("http_api_id")
)
permissions.append(permission)

return permissions

def _set_default_authorizer(
self,
open_api_editor: OpenApiEditor,
Expand Down Expand Up @@ -582,6 +638,7 @@ def _get_authorizers(
identity=authorizer.get("Identity"),
authorizer_payload_format_version=authorizer.get("AuthorizerPayloadFormatVersion"),
enable_simple_responses=authorizer.get("EnableSimpleResponses"),
enable_function_default_permissions=authorizer.get("EnableFunctionDefaultPermissions"),
)
return authorizers

Expand Down Expand Up @@ -719,6 +776,7 @@ def to_cloudformation(
Optional[ApiGatewayV2DomainName],
Optional[List[ApiGatewayV2ApiMapping]],
Optional[Route53RecordSetGroup],
Optional[List[LambdaPermission]],
]:
"""Generates CloudFormation resources from a SAM HTTP API resource
Expand All @@ -727,6 +785,7 @@ def to_cloudformation(
"""
http_api = self._construct_http_api()
domain, basepath_mapping, route53 = self._construct_api_domain(http_api, route53_record_set_groups)
permissions = self._construct_authorizer_lambda_permission(http_api)
stage = self._construct_stage()

return http_api, stage, domain, basepath_mapping, route53
return http_api, stage, domain, basepath_mapping, route53, permissions
14 changes: 14 additions & 0 deletions samtranslator/model/apigatewayv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def __init__( # type: ignore[no-untyped-def] # noqa: too-many-arguments
authorizer_payload_format_version=None,
enable_simple_responses=None,
is_aws_iam_authorizer=False,
enable_function_default_permissions=None,
):
"""
Creates an authorizer for use in V2 Http Apis
Expand All @@ -103,6 +104,7 @@ def __init__( # type: ignore[no-untyped-def] # noqa: too-many-arguments
self.authorizer_payload_format_version = authorizer_payload_format_version
self.enable_simple_responses = enable_simple_responses
self.is_aws_iam_authorizer = is_aws_iam_authorizer
self.enable_function_default_permissions = enable_function_default_permissions

self._validate_input_parameters()

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

if enable_function_default_permissions is not None:
sam_expect(
enable_function_default_permissions,
api_logical_id,
f"Authorizers.{name}.EnableFunctionDefaultPermissions",
).to_be_a_bool()

def _get_auth_type(self) -> str:
if self.is_aws_iam_authorizer:
return "AWS_IAM"
Expand Down Expand Up @@ -166,6 +175,11 @@ def _validate_input_parameters(self) -> None:
self.api_logical_id, "EnableSimpleResponses must be defined only for Lambda Authorizer."
)

if self.enable_function_default_permissions is not None and authorizer_type != "REQUEST":
raise InvalidResourceException(
self.api_logical_id, "EnableFunctionDefaultPermissions must be defined only for Lambda Authorizer."
)

def _validate_jwt_authorizer(self) -> None:
if not self.jwt_configuration:
raise InvalidResourceException(
Expand Down
3 changes: 3 additions & 0 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -1392,9 +1392,12 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def]
domain,
basepath_mapping,
route53,
permissions,
) = api_generator.to_cloudformation(kwargs.get("route53_record_set_groups", {}))

resources.append(http_api)
if permissions:
resources.extend(permissions)
if domain:
resources.append(domain)
if basepath_mapping:
Expand Down
4 changes: 4 additions & 0 deletions samtranslator/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -195743,6 +195743,10 @@
"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\\.",
"title": "AuthorizerPayloadFormatVersion"
},
"EnableFunctionDefaultPermissions": {
"title": "Enablefunctiondefaultpermissions",
"type": "boolean"
},
"EnableSimpleResponses": {
"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\\.",
"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\\.",
Expand Down
4 changes: 4 additions & 0 deletions schema_source/sam.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1561,6 +1561,10 @@
"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\\.",
"title": "AuthorizerPayloadFormatVersion"
},
"EnableFunctionDefaultPermissions": {
"title": "Enablefunctiondefaultpermissions",
"type": "boolean"
},
"EnableSimpleResponses": {
"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\\.",
"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\\.",
Expand Down
15 changes: 15 additions & 0 deletions tests/model/test_api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,21 @@ def test_create_authorizer_fails_with_enable_simple_responses_non_lambda(self):
+ "EnableSimpleResponses must be defined only for Lambda Authorizer.",
)

def test_create_authorizer_fails_with_enable_function_default_permissions_non_lambda(self):
with pytest.raises(InvalidResourceException) as e:
ApiGatewayV2Authorizer(
api_logical_id="logicalId",
name="authName",
jwt_configuration={"config": "value"},
authorization_scopes=["scope1", "scope2"],
enable_function_default_permissions=True,
)
self.assertEqual(
e.value.message,
"Resource with id [logicalId] is invalid. "
+ "EnableFunctionDefaultPermissions must be defined only for Lambda Authorizer.",
)

@mock.patch(
"samtranslator.model.apigatewayv2.ApiGatewayV2Authorizer._get_auth_type", mock.MagicMock(return_value="JWT")
)
Expand Down
23 changes: 23 additions & 0 deletions tests/translator/input/error_http_api_invalid_lambda_auth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,26 @@ Resources:
AuthorizerPayloadFormatVersion: 2.0
EnableSimpleResponses: true
DefaultAuthorizer: LambdaAuth

MyApi3:
Type: AWS::Serverless::HttpApi
Properties:
Auth:
Authorizers:
LambdaAuth:
FunctionArn: !GetAtt MyAuthFn.Arn
EnableFunctionDefaultPermissions: foo
AuthorizerPayloadFormatVersion: 2.0
DefaultAuthorizer: LambdaAuth

MyApi4:
Type: AWS::Serverless::HttpApi
Properties:
Auth:
Authorizers:
NonLambdaAuth:
JwtConfiguration:
audience: https://test-sam.com
issuer: https://test-sam.com
EnableFunctionDefaultPermissions: foo
DefaultAuthorizer: LambdaAuth
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
Resources:
HttpApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3://sam-demo-bucket/todo_list.zip
Handler: index.restapi
Runtime: python3.7
Events:
Basic:
Type: HttpApi
Properties:
Path: /basic
Method: GET
ApiId: !Ref MyApi

MyAuthFn:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3://bucket/key
Handler: index.handler
Runtime: nodejs12.x

MyApi:
Type: AWS::Serverless::HttpApi
Properties:
Tags:
Tag1: value1
Tag2: value2
Auth:
Authorizers:
LambdaAuthWithEnablePropertyTrue:
# should create permissions resource for this auth
FunctionArn: !GetAtt MyAuthFn.Arn
EnableFunctionDefaultPermissions: true
AuthorizerPayloadFormatVersion: 1.0
LambdaAuthNoEnableProperty:
# should not create permissions resource for this auth as http api doesn't create the resource by default
FunctionArn: !GetAtt MyAuthFn.Arn
AuthorizerPayloadFormatVersion: 1.0
LambdaAuthWithEnablePropertySetFalse:
# should not create permissions resource for this auth
FunctionArn: !GetAtt MyAuthFn.Arn
AuthorizerPayloadFormatVersion: 1.0
EnableFunctionDefaultPermissions: false
LambdaAuthFull:
# should create permissions resource for this auth
FunctionArn: !GetAtt MyAuthFn.Arn
FunctionInvokeRole: !GetAtt MyAuthFnRole.Arn
EnableFunctionDefaultPermissions: true
Identity:
Context:
- contextVar
Headers:
- Authorization
QueryStrings:
- petId
StageVariables:
- stageVar
ReauthorizeEvery: 60
AuthorizerPayloadFormatVersion: 2.0
EnableSimpleResponses: true
DefaultAuthorizer: LambdaAuthWithEnablePropertyTrue
Loading

0 comments on commit c5690d5

Please sign in to comment.