Skip to content

Commit 263fb5b

Browse files
authored
[fix] Auth Override not working with DefinitionBody fix (#3328)
1 parent 725a312 commit 263fb5b

File tree

12 files changed

+1981
-7
lines changed

12 files changed

+1981
-7
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from unittest.case import skipIf
2+
3+
from integration.config.service_names import API_KEY, COGNITO, REST_API
4+
from integration.helpers.base_test import BaseTest
5+
from integration.helpers.deployer.utils.retry import retry
6+
from integration.helpers.exception import StatusCodeError
7+
from integration.helpers.resource import current_region_does_not_support
8+
9+
10+
@skipIf(
11+
current_region_does_not_support([COGNITO, API_KEY, REST_API]), "Cognito is not supported in this testing region"
12+
)
13+
class TestApiWithAuthorizerOverrideApiAuth(BaseTest):
14+
def test_authorizer_override_api_auth(self):
15+
self.create_and_verify_stack("combination/api_with_authorizer_override_api_auth")
16+
17+
stack_outputs = self.get_stack_outputs()
18+
19+
base_url = stack_outputs["ApiUrl"]
20+
21+
# Default case with no Auth override
22+
self.verify_authorized_request(base_url + "lambda-request?authorization=allow", 200)
23+
self.verify_authorized_request(base_url + "lambda-request", 401)
24+
25+
# Override Auth to NONE, lambda request should pass without authorization
26+
self.verify_authorized_request(base_url + "lambda-request-override-none", 200)
27+
28+
# Override Auth to CognitoUserPool, lambda request should fail with authorization for lambda request
29+
self.verify_authorized_request(base_url + "lambda-request-override-cognito?authorization=allow", 401)
30+
31+
@retry(StatusCodeError, 10, 0.25)
32+
def verify_authorized_request(
33+
self,
34+
url,
35+
expected_status_code,
36+
header_key=None,
37+
header_value=None,
38+
):
39+
if not header_key or not header_value:
40+
response = self.do_get_request_with_logging(url)
41+
else:
42+
headers = {header_key: header_value}
43+
response = self.do_get_request_with_logging(url, headers)
44+
status = response.status_code
45+
46+
if status != expected_status_code:
47+
raise StatusCodeError(
48+
f"Request to {url} failed with status: {status}, expected status: {expected_status_code}"
49+
)
50+
51+
if not header_key or not header_value:
52+
self.assertEqual(
53+
status, expected_status_code, "Request to " + url + " must return HTTP " + str(expected_status_code)
54+
)
55+
else:
56+
self.assertEqual(
57+
status,
58+
expected_status_code,
59+
"Request to "
60+
+ url
61+
+ " ("
62+
+ header_key
63+
+ ": "
64+
+ header_value
65+
+ ") must return HTTP "
66+
+ str(expected_status_code),
67+
)
68+
69+
70+
def get_authorizer_by_name(authorizers, name):
71+
for authorizer in authorizers:
72+
if authorizer["name"] == name:
73+
return authorizer
74+
return None
75+
76+
77+
def get_resource_by_path(resources, path):
78+
for resource in resources:
79+
if resource["path"] == path:
80+
return resource
81+
return None
82+
83+
84+
def get_method(resources, path, rest_api_id, apigw_client):
85+
resource = get_resource_by_path(resources, path)
86+
return apigw_client.get_method(restApiId=rest_api_id, resourceId=resource["id"], httpMethod="GET")
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
[
2+
{
3+
"LogicalResourceId": "MyApi",
4+
"ResourceType": "AWS::ApiGateway::RestApi"
5+
},
6+
{
7+
"LogicalResourceId": "MyApiMyLambdaRequestAuthAuthorizerPermission",
8+
"ResourceType": "AWS::Lambda::Permission"
9+
},
10+
{
11+
"LogicalResourceId": "MyApiProdStage",
12+
"ResourceType": "AWS::ApiGateway::Stage"
13+
},
14+
{
15+
"LogicalResourceId": "MyCognitoUserPool",
16+
"ResourceType": "AWS::Cognito::UserPool"
17+
},
18+
{
19+
"LogicalResourceId": "MyCognitoUserPoolClient",
20+
"ResourceType": "AWS::Cognito::UserPoolClient"
21+
},
22+
{
23+
"LogicalResourceId": "MyApiDeployment",
24+
"ResourceType": "AWS::ApiGateway::Deployment"
25+
},
26+
{
27+
"LogicalResourceId": "MyFunction",
28+
"ResourceType": "AWS::Lambda::Function"
29+
},
30+
{
31+
"LogicalResourceId": "MyFunctionRole",
32+
"ResourceType": "AWS::IAM::Role"
33+
},
34+
{
35+
"LogicalResourceId": "MyFunctionLambdaRequestPermissionProd",
36+
"ResourceType": "AWS::Lambda::Permission"
37+
},
38+
{
39+
"LogicalResourceId": "MyFunctionLambdaRequestOverrideNonePermissionProd",
40+
"ResourceType": "AWS::Lambda::Permission"
41+
},
42+
{
43+
"LogicalResourceId": "MyFunctionLambdaRequestOverrideCognitoPermissionProd",
44+
"ResourceType": "AWS::Lambda::Permission"
45+
},
46+
{
47+
"LogicalResourceId": "MyLambdaAuthFunction",
48+
"ResourceType": "AWS::Lambda::Function"
49+
},
50+
{
51+
"LogicalResourceId": "MyLambdaAuthFunctionRole",
52+
"ResourceType": "AWS::IAM::Role"
53+
}
54+
]
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
Resources:
2+
MyApi:
3+
Type: AWS::Serverless::Api
4+
Properties:
5+
StageName: Prod
6+
DefinitionBody:
7+
# Simple AWS Proxy API
8+
swagger: '2.0'
9+
info:
10+
version: '2016-09-23T22:23:23Z'
11+
title: Simple Api
12+
schemes:
13+
- https
14+
paths:
15+
/lambda-request:
16+
get:
17+
x-amazon-apigateway-integration:
18+
type: aws_proxy
19+
uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations
20+
httpMethod: POST
21+
passthroughBehavior: when_no_match
22+
/lambda-request-override-none:
23+
get:
24+
x-amazon-apigateway-integration:
25+
type: aws_proxy
26+
uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations
27+
httpMethod: POST
28+
passthroughBehavior: when_no_match
29+
/lambda-request-override-cognito:
30+
get:
31+
x-amazon-apigateway-integration:
32+
type: aws_proxy
33+
uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations
34+
httpMethod: POST
35+
passthroughBehavior: when_no_match
36+
Auth:
37+
Authorizers:
38+
MyCognitoAuthorizer:
39+
UserPoolArn:
40+
Fn::GetAtt: MyCognitoUserPool.Arn
41+
MyLambdaRequestAuth:
42+
FunctionPayloadType: REQUEST
43+
FunctionArn:
44+
Fn::GetAtt: MyLambdaAuthFunction.Arn
45+
Identity:
46+
QueryStrings:
47+
- authorization
48+
DefaultAuthorizer: MyLambdaRequestAuth
49+
50+
MyFunction:
51+
Type: AWS::Serverless::Function
52+
Properties:
53+
InlineCode: |
54+
exports.handler = async (event, context, callback) => {
55+
return {
56+
statusCode: 200,
57+
body: 'Success'
58+
}
59+
}
60+
Handler: index.handler
61+
Runtime: nodejs16.x
62+
Events:
63+
LambdaRequest:
64+
Type: Api
65+
Properties:
66+
RestApiId:
67+
Ref: MyApi
68+
Method: get
69+
Auth:
70+
Authorizer: MyLambdaRequestAuth
71+
Path: /lambda-request
72+
LambdaRequestOverrideNone:
73+
Type: Api
74+
Properties:
75+
RestApiId:
76+
Ref: MyApi
77+
Method: get
78+
Auth:
79+
Authorizer: NONE
80+
OverrideApiAuth: true
81+
Path: /lambda-request-override-none
82+
LambdaRequestOverrideCognito:
83+
Type: Api
84+
Properties:
85+
RestApiId:
86+
Ref: MyApi
87+
Method: get
88+
Auth:
89+
Authorizer: MyCognitoAuthorizer
90+
OverrideApiAuth: true
91+
Path: /lambda-request-override-cognito
92+
93+
MyLambdaAuthFunction:
94+
Type: AWS::Serverless::Function
95+
Properties:
96+
Handler: index.handler
97+
Runtime: nodejs16.x
98+
InlineCode: |
99+
exports.handler = async (event, context, callback) => {
100+
const auth = event.queryStringParameters.authorization
101+
const policyDocument = {
102+
Version: '2012-10-17',
103+
Statement: [{
104+
Action: 'execute-api:Invoke',
105+
Effect: auth && auth.toLowerCase() === 'allow' ? 'Allow' : 'Deny',
106+
Resource: event.methodArn
107+
}]
108+
}
109+
110+
return {
111+
principalId: 'user',
112+
context: {},
113+
policyDocument
114+
}
115+
}
116+
117+
MyCognitoUserPool:
118+
Type: AWS::Cognito::UserPool
119+
Properties:
120+
UserPoolName: MyCognitoUserPool
121+
122+
MyCognitoUserPoolClient:
123+
Type: AWS::Cognito::UserPoolClient
124+
Properties:
125+
UserPoolId:
126+
Ref: MyCognitoUserPool
127+
ClientName: MyCognitoUserPoolClient
128+
GenerateSecret: false
129+
130+
Outputs:
131+
ApiUrl:
132+
Description: API endpoint URL for Prod environment
133+
Value:
134+
Fn::Sub: https://${MyApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/Prod/
135+
136+
Parameters:
137+
OverrideApiAuthValue:
138+
Type: String
139+
Default: true
140+
141+
Metadata:
142+
SamTransformTest: true

samtranslator/internal/schema_source/aws_serverless_function.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ class ApiAuth(BaseModel):
253253
Authorizer: Optional[str] = apiauth("Authorizer")
254254
InvokeRole: Optional[SamIntrinsicable[str]] = apiauth("InvokeRole")
255255
ResourcePolicy: Optional[ResourcePolicy] = apiauth("ResourcePolicy")
256+
# TODO explicitly mention in docs that intrinsics are not supported for OverrideApiAuth
257+
OverrideApiAuth: Optional[bool] # TODO Add Docs
256258

257259

258260
class RequestModel(BaseModel):

samtranslator/model/eventsources/push.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def]
727727
resources = []
728728

729729
function = kwargs.get("function")
730-
intrinsics_resolver = kwargs.get("intrinsics_resolver")
730+
intrinsics_resolver: IntrinsicsResolver = kwargs["intrinsics_resolver"]
731731

732732
if not function:
733733
raise TypeError("Missing required keyword argument: function")
@@ -743,6 +743,33 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def]
743743
if explicit_api.get("__MANAGE_SWAGGER") or explicit_api.get("MergeDefinitions"):
744744
self._add_swagger_integration(explicit_api, api_id, function, intrinsics_resolver) # type: ignore[no-untyped-call]
745745

746+
swagger_body = explicit_api.get("DefinitionBody")
747+
748+
# Previously overriding the DefaultAuthorizer in event source Auth would not work properly when DefinitionBody
749+
# is included in the template. This is because call to update and save the DefinitionBody with any auth
750+
# overrides was beings skipped due to the check on __MANAGE_SWAGGER above which is only set when no
751+
# DefinitionBody is set.
752+
# A new opt-in property, OverrideApiAuth, is added at the event source Auth level which is checked below and
753+
# makes the necessary call to add_auth_to_swagger() to update and save the DefinitionBody with any auth
754+
# overrides.
755+
# We make the call to add_auth_to_swagger() in two separate places because _add_swagger_integration() deals
756+
# specifically with cases where DefinitionBody is not defined, and below for when DefinitionBody is defined.
757+
if swagger_body and self.Auth and self.Auth.get("OverrideApiAuth"):
758+
# TODO: refactor to remove this cast
759+
stage = cast(str, self.Stage)
760+
editor = SwaggerEditor(swagger_body)
761+
self.add_auth_to_swagger(
762+
self.Auth,
763+
explicit_api,
764+
api_id,
765+
self.relative_id,
766+
self.Method,
767+
self.Path,
768+
stage,
769+
editor,
770+
intrinsics_resolver,
771+
)
772+
explicit_api["DefinitionBody"] = editor.swagger
746773
return resources
747774

748775
def _get_permissions(self, resources_to_link): # type: ignore[no-untyped-def]

samtranslator/schema/schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247702,6 +247702,10 @@
247702247702
"markdownDescription": "Specifies the `InvokeRole` to use for `AWS_IAM` authorization\\. \n*Type*: String \n*Required*: No \n*Default*: `CALLER_CREDENTIALS` \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\. \n*Additional notes*: `CALLER_CREDENTIALS` maps to `arn:aws:iam::*:user/*`, which uses the caller credentials to invoke the endpoint\\.",
247703247703
"title": "InvokeRole"
247704247704
},
247705+
"OverrideApiAuth": {
247706+
"title": "Overrideapiauth",
247707+
"type": "boolean"
247708+
},
247705247709
"ResourcePolicy": {
247706247710
"allOf": [
247707247711
{

schema_source/sam.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@
7373
"markdownDescription": "Specifies the `InvokeRole` to use for `AWS_IAM` authorization\\. \n*Type*: String \n*Required*: No \n*Default*: `CALLER_CREDENTIALS` \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\. \n*Additional notes*: `CALLER_CREDENTIALS` maps to `arn:aws:iam::*:user/*`, which uses the caller credentials to invoke the endpoint\\.",
7474
"title": "InvokeRole"
7575
},
76+
"OverrideApiAuth": {
77+
"title": "Overrideapiauth",
78+
"type": "boolean"
79+
},
7680
"ResourcePolicy": {
7781
"allOf": [
7882
{

0 commit comments

Comments
 (0)