Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fix] Auth Override not working with DefinitionBody fix #3328

Merged
merged 14 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]
ssenchenko marked this conversation as resolved.
Show resolved Hide resolved

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
ssenchenko marked this conversation as resolved.
Show resolved Hide resolved
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