Skip to content

Commit

Permalink
[fix] Auth Override not working with DefinitionBody fix (#3328)
Browse files Browse the repository at this point in the history
  • Loading branch information
paulhcsun authored Sep 8, 2023
1 parent 725a312 commit 263fb5b
Show file tree
Hide file tree
Showing 12 changed files with 1,981 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from unittest.case import skipIf

from integration.config.service_names import API_KEY, COGNITO, REST_API
from integration.helpers.base_test import BaseTest
from integration.helpers.deployer.utils.retry import retry
from integration.helpers.exception import StatusCodeError
from integration.helpers.resource import current_region_does_not_support


@skipIf(
current_region_does_not_support([COGNITO, API_KEY, REST_API]), "Cognito is not supported in this testing region"
)
class TestApiWithAuthorizerOverrideApiAuth(BaseTest):
def test_authorizer_override_api_auth(self):
self.create_and_verify_stack("combination/api_with_authorizer_override_api_auth")

stack_outputs = self.get_stack_outputs()

base_url = stack_outputs["ApiUrl"]

# Default case with no Auth override
self.verify_authorized_request(base_url + "lambda-request?authorization=allow", 200)
self.verify_authorized_request(base_url + "lambda-request", 401)

# Override Auth to NONE, lambda request should pass without authorization
self.verify_authorized_request(base_url + "lambda-request-override-none", 200)

# Override Auth to CognitoUserPool, lambda request should fail with authorization for lambda request
self.verify_authorized_request(base_url + "lambda-request-override-cognito?authorization=allow", 401)

@retry(StatusCodeError, 10, 0.25)
def verify_authorized_request(
self,
url,
expected_status_code,
header_key=None,
header_value=None,
):
if not header_key or not header_value:
response = self.do_get_request_with_logging(url)
else:
headers = {header_key: header_value}
response = self.do_get_request_with_logging(url, headers)
status = response.status_code

if status != expected_status_code:
raise StatusCodeError(
f"Request to {url} failed with status: {status}, expected status: {expected_status_code}"
)

if not header_key or not header_value:
self.assertEqual(
status, expected_status_code, "Request to " + url + " must return HTTP " + str(expected_status_code)
)
else:
self.assertEqual(
status,
expected_status_code,
"Request to "
+ url
+ " ("
+ header_key
+ ": "
+ header_value
+ ") must return HTTP "
+ str(expected_status_code),
)


def get_authorizer_by_name(authorizers, name):
for authorizer in authorizers:
if authorizer["name"] == name:
return authorizer
return None


def get_resource_by_path(resources, path):
for resource in resources:
if resource["path"] == path:
return resource
return None


def get_method(resources, path, rest_api_id, apigw_client):
resource = get_resource_by_path(resources, path)
return apigw_client.get_method(restApiId=rest_api_id, resourceId=resource["id"], httpMethod="GET")
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[
{
"LogicalResourceId": "MyApi",
"ResourceType": "AWS::ApiGateway::RestApi"
},
{
"LogicalResourceId": "MyApiMyLambdaRequestAuthAuthorizerPermission",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyApiProdStage",
"ResourceType": "AWS::ApiGateway::Stage"
},
{
"LogicalResourceId": "MyCognitoUserPool",
"ResourceType": "AWS::Cognito::UserPool"
},
{
"LogicalResourceId": "MyCognitoUserPoolClient",
"ResourceType": "AWS::Cognito::UserPoolClient"
},
{
"LogicalResourceId": "MyApiDeployment",
"ResourceType": "AWS::ApiGateway::Deployment"
},
{
"LogicalResourceId": "MyFunction",
"ResourceType": "AWS::Lambda::Function"
},
{
"LogicalResourceId": "MyFunctionRole",
"ResourceType": "AWS::IAM::Role"
},
{
"LogicalResourceId": "MyFunctionLambdaRequestPermissionProd",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyFunctionLambdaRequestOverrideNonePermissionProd",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyFunctionLambdaRequestOverrideCognitoPermissionProd",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyLambdaAuthFunction",
"ResourceType": "AWS::Lambda::Function"
},
{
"LogicalResourceId": "MyLambdaAuthFunctionRole",
"ResourceType": "AWS::IAM::Role"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
Resources:
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
DefinitionBody:
# Simple AWS Proxy API
swagger: '2.0'
info:
version: '2016-09-23T22:23:23Z'
title: Simple Api
schemes:
- https
paths:
/lambda-request:
get:
x-amazon-apigateway-integration:
type: aws_proxy
uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations
httpMethod: POST
passthroughBehavior: when_no_match
/lambda-request-override-none:
get:
x-amazon-apigateway-integration:
type: aws_proxy
uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations
httpMethod: POST
passthroughBehavior: when_no_match
/lambda-request-override-cognito:
get:
x-amazon-apigateway-integration:
type: aws_proxy
uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations
httpMethod: POST
passthroughBehavior: when_no_match
Auth:
Authorizers:
MyCognitoAuthorizer:
UserPoolArn:
Fn::GetAtt: MyCognitoUserPool.Arn
MyLambdaRequestAuth:
FunctionPayloadType: REQUEST
FunctionArn:
Fn::GetAtt: MyLambdaAuthFunction.Arn
Identity:
QueryStrings:
- authorization
DefaultAuthorizer: MyLambdaRequestAuth

MyFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
exports.handler = async (event, context, callback) => {
return {
statusCode: 200,
body: 'Success'
}
}
Handler: index.handler
Runtime: nodejs16.x
Events:
LambdaRequest:
Type: Api
Properties:
RestApiId:
Ref: MyApi
Method: get
Auth:
Authorizer: MyLambdaRequestAuth
Path: /lambda-request
LambdaRequestOverrideNone:
Type: Api
Properties:
RestApiId:
Ref: MyApi
Method: get
Auth:
Authorizer: NONE
OverrideApiAuth: true
Path: /lambda-request-override-none
LambdaRequestOverrideCognito:
Type: Api
Properties:
RestApiId:
Ref: MyApi
Method: get
Auth:
Authorizer: MyCognitoAuthorizer
OverrideApiAuth: true
Path: /lambda-request-override-cognito

MyLambdaAuthFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs16.x
InlineCode: |
exports.handler = async (event, context, callback) => {
const auth = event.queryStringParameters.authorization
const policyDocument = {
Version: '2012-10-17',
Statement: [{
Action: 'execute-api:Invoke',
Effect: auth && auth.toLowerCase() === 'allow' ? 'Allow' : 'Deny',
Resource: event.methodArn
}]
}
return {
principalId: 'user',
context: {},
policyDocument
}
}
MyCognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: MyCognitoUserPool

MyCognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId:
Ref: MyCognitoUserPool
ClientName: MyCognitoUserPoolClient
GenerateSecret: false

Outputs:
ApiUrl:
Description: API endpoint URL for Prod environment
Value:
Fn::Sub: https://${MyApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/Prod/

Parameters:
OverrideApiAuthValue:
Type: String
Default: true

Metadata:
SamTransformTest: true
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ class ApiAuth(BaseModel):
Authorizer: Optional[str] = apiauth("Authorizer")
InvokeRole: Optional[SamIntrinsicable[str]] = apiauth("InvokeRole")
ResourcePolicy: Optional[ResourcePolicy] = apiauth("ResourcePolicy")
# TODO explicitly mention in docs that intrinsics are not supported for OverrideApiAuth
OverrideApiAuth: Optional[bool] # TODO Add Docs


class RequestModel(BaseModel):
Expand Down
29 changes: 28 additions & 1 deletion samtranslator/model/eventsources/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def]
resources = []

function = kwargs.get("function")
intrinsics_resolver = kwargs.get("intrinsics_resolver")
intrinsics_resolver: IntrinsicsResolver = kwargs["intrinsics_resolver"]

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

swagger_body = explicit_api.get("DefinitionBody")

# Previously overriding the DefaultAuthorizer in event source Auth would not work properly when DefinitionBody
# is included in the template. This is because call to update and save the DefinitionBody with any auth
# overrides was beings skipped due to the check on __MANAGE_SWAGGER above which is only set when no
# DefinitionBody is set.
# A new opt-in property, OverrideApiAuth, is added at the event source Auth level which is checked below and
# makes the necessary call to add_auth_to_swagger() to update and save the DefinitionBody with any auth
# overrides.
# We make the call to add_auth_to_swagger() in two separate places because _add_swagger_integration() deals
# specifically with cases where DefinitionBody is not defined, and below for when DefinitionBody is defined.
if swagger_body and self.Auth and self.Auth.get("OverrideApiAuth"):
# TODO: refactor to remove this cast
stage = cast(str, self.Stage)
editor = SwaggerEditor(swagger_body)
self.add_auth_to_swagger(
self.Auth,
explicit_api,
api_id,
self.relative_id,
self.Method,
self.Path,
stage,
editor,
intrinsics_resolver,
)
explicit_api["DefinitionBody"] = editor.swagger
return resources

def _get_permissions(self, resources_to_link): # type: ignore[no-untyped-def]
Expand Down
4 changes: 4 additions & 0 deletions samtranslator/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -247702,6 +247702,10 @@
"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\\.",
"title": "InvokeRole"
},
"OverrideApiAuth": {
"title": "Overrideapiauth",
"type": "boolean"
},
"ResourcePolicy": {
"allOf": [
{
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 @@ -73,6 +73,10 @@
"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\\.",
"title": "InvokeRole"
},
"OverrideApiAuth": {
"title": "Overrideapiauth",
"type": "boolean"
},
"ResourcePolicy": {
"allOf": [
{
Expand Down
Loading

0 comments on commit 263fb5b

Please sign in to comment.