Skip to content

Commit 089859e

Browse files
Connor Robertsonhoffa
andauthored
fix: Defining CORS when ApiKeyRequired is true results in an OPTIONS method that requires an API key (#2981)
Co-authored-by: Christoffer Rehn <[email protected]>
1 parent d6c0468 commit 089859e

16 files changed

+1812
-5
lines changed

integration/combination/test_api_with_cors.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class TestApiWithCors(BaseTest):
1515
[
1616
"combination/api_with_cors",
1717
"combination/api_with_cors_openapi",
18+
"combination/api_with_cors_and_apikey",
1819
]
1920
)
2021
def test_cors(self, file_name):
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[
2+
{
3+
"LogicalResourceId": "MyApi",
4+
"ResourceType": "AWS::ApiGateway::RestApi"
5+
},
6+
{
7+
"LogicalResourceId": "MyApiDeployment",
8+
"ResourceType": "AWS::ApiGateway::Deployment"
9+
},
10+
{
11+
"LogicalResourceId": "MyApidevStage",
12+
"ResourceType": "AWS::ApiGateway::Stage"
13+
},
14+
{
15+
"LogicalResourceId": "ApiGatewayLambdaRole",
16+
"ResourceType": "AWS::IAM::Role"
17+
},
18+
{
19+
"LogicalResourceId": "MyFunction",
20+
"ResourceType": "AWS::Lambda::Function"
21+
},
22+
{
23+
"LogicalResourceId": "MyFunctionRole",
24+
"ResourceType": "AWS::IAM::Role"
25+
}
26+
]
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
3+
Transform:
4+
- AWS::Serverless-2016-10-31
5+
6+
Globals:
7+
Api:
8+
Auth:
9+
ApiKeyRequired: true
10+
AddApiKeyRequiredToCorsPreflight: false
11+
12+
Resources:
13+
14+
MyFunction:
15+
Type: AWS::Serverless::Function
16+
Properties:
17+
Handler: index.handler
18+
InlineCode: |
19+
exports.handler = async function (event) {
20+
return {
21+
statusCode: 200,
22+
body: JSON.stringify({ message: "Hello, SAM!" }),
23+
}
24+
}
25+
Runtime: nodejs16.x
26+
27+
ApiGatewayLambdaRole:
28+
Type: AWS::IAM::Role
29+
Properties:
30+
AssumeRolePolicyDocument:
31+
Version: '2012-10-17'
32+
Statement:
33+
- Effect: Allow
34+
Principal: {Service: apigateway.amazonaws.com}
35+
Action: sts:AssumeRole
36+
Policies:
37+
- PolicyName: AllowInvokeLambdaFunctions
38+
PolicyDocument:
39+
Version: '2012-10-17'
40+
Statement:
41+
- Effect: Allow
42+
Action: lambda:InvokeFunction
43+
Resource: '*'
44+
45+
MyApi:
46+
Type: AWS::Serverless::Api
47+
Properties:
48+
Cors:
49+
AllowMethods: "'methods'"
50+
AllowHeaders: "'headers'"
51+
AllowOrigin: "'origins'"
52+
MaxAge: "'600'"
53+
Auth:
54+
ApiKeyRequired: true
55+
StageName: dev
56+
DefinitionBody:
57+
openapi: 3.0.1
58+
paths:
59+
/apione:
60+
get:
61+
x-amazon-apigateway-integration:
62+
credentials:
63+
Fn::Sub: ${ApiGatewayLambdaRole.Arn}
64+
uri:
65+
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations
66+
passthroughBehavior: when_no_match
67+
httpMethod: POST
68+
type: aws_proxy
69+
/apitwo:
70+
get:
71+
x-amazon-apigateway-integration:
72+
credentials:
73+
Fn::Sub: ${ApiGatewayLambdaRole.Arn}
74+
uri:
75+
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations
76+
passthroughBehavior: when_no_match
77+
httpMethod: POST
78+
type: aws_proxy
79+
80+
81+
82+
Outputs:
83+
ApiUrl:
84+
Description: URL of your API endpoint
85+
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/"
86+
Metadata:
87+
SamTransformTest: true

samtranslator/internal/schema_source/aws_serverless_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class UsagePlan(BaseModel):
100100

101101
class Auth(BaseModel):
102102
AddDefaultAuthorizerToCorsPreflight: Optional[bool] = auth("AddDefaultAuthorizerToCorsPreflight")
103+
AddApiKeyRequiredToCorsPreflight: Optional[bool] # TODO Add Docs
103104
ApiKeyRequired: Optional[bool] = auth("ApiKeyRequired")
104105
Authorizers: Optional[
105106
Dict[

samtranslator/model/api/api_generator.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,13 @@
5252
"DefaultAuthorizer",
5353
"InvokeRole",
5454
"AddDefaultAuthorizerToCorsPreflight",
55+
"AddApiKeyRequiredToCorsPreflight",
5556
"ApiKeyRequired",
5657
"ResourcePolicy",
5758
"UsagePlan",
5859
],
5960
)
60-
AuthProperties.__new__.__defaults__ = (None, None, None, True, None, None, None)
61+
AuthProperties.__new__.__defaults__ = (None, None, None, True, True, None, None, None)
6162
UsagePlanProperties = namedtuple(
6263
"UsagePlanProperties", ["CreateUsagePlan", "Description", "Quota", "Tags", "Throttle", "UsagePlanName"]
6364
)
@@ -752,7 +753,7 @@ def _add_auth(self) -> None:
752753

753754
if auth_properties.ApiKeyRequired:
754755
swagger_editor.add_apikey_security_definition()
755-
self._set_default_apikey_required(swagger_editor)
756+
self._set_default_apikey_required(swagger_editor, auth_properties.AddApiKeyRequiredToCorsPreflight)
756757

757758
if auth_properties.ResourcePolicy:
758759
SwaggerEditor.validate_is_dict(
@@ -1224,9 +1225,9 @@ def _set_default_authorizer(
12241225
add_default_auth_to_preflight=add_default_auth_to_preflight,
12251226
)
12261227

1227-
def _set_default_apikey_required(self, swagger_editor: SwaggerEditor) -> None:
1228+
def _set_default_apikey_required(self, swagger_editor: SwaggerEditor, required_options_api_key: bool) -> None:
12281229
for path in swagger_editor.iter_on_path():
1229-
swagger_editor.set_path_default_apikey_required(path)
1230+
swagger_editor.set_path_default_apikey_required(path, required_options_api_key)
12301231

12311232
def _set_endpoint_configuration(self, rest_api: ApiGatewayRestApi, value: Union[str, Dict[str, Any]]) -> None:
12321233
"""

samtranslator/schema/schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197274,6 +197274,10 @@
197274197274
"samtranslator__internal__schema_source__aws_serverless_api__Auth": {
197275197275
"additionalProperties": false,
197276197276
"properties": {
197277+
"AddApiKeyRequiredToCorsPreflight": {
197278+
"title": "Addapikeyrequiredtocorspreflight",
197279+
"type": "boolean"
197280+
},
197277197281
"AddDefaultAuthorizerToCorsPreflight": {
197278197282
"description": "If the `DefaultAuthorizer` and `Cors` properties are set, then setting `AddDefaultAuthorizerToCorsPreflight` will cause the default authorizer to be added to the `Options` property in the OpenAPI section\\. \n*Type*: Boolean \n*Required*: No \n*Default*: True \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",
197279197283
"markdownDescription": "If the `DefaultAuthorizer` and `Cors` properties are set, then setting `AddDefaultAuthorizerToCorsPreflight` will cause the default authorizer to be added to the `Options` property in the OpenAPI section\\. \n*Type*: Boolean \n*Required*: No \n*Default*: True \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",

samtranslator/swagger/swagger.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,14 +612,16 @@ def set_path_default_authorizer( # noqa: too-many-branches
612612
if "AWS_IAM" in method_definition["security"][0]:
613613
self.add_awsiam_security_definition()
614614

615-
def set_path_default_apikey_required(self, path: str) -> None:
615+
def set_path_default_apikey_required(self, path: str, required_options_api_key: bool = True) -> None:
616616
"""
617617
Add the ApiKey security as required for each method on this path unless ApiKeyRequired
618618
was defined at the Function/Path/Method level. This is intended to be used to set the
619619
apikey security restriction for all api methods based upon the default configured in the
620620
Serverless API.
621621
622622
:param string path: Path name
623+
:param bool required_options_api_key: Bool of whether to add the ApiKeyRequired
624+
to OPTIONS preflight requests.
623625
"""
624626

625627
for method_name, method_definition in self.iter_on_all_methods_for_path(path): # type: ignore[no-untyped-call]
@@ -673,6 +675,9 @@ def set_path_default_apikey_required(self, path: str) -> None:
673675

674676
security = existing_non_apikey_security + apikey_security
675677

678+
if method_name == "options" and not required_options_api_key:
679+
security = existing_non_apikey_security
680+
676681
if security != existing_security:
677682
method_definition["security"] = security
678683

@@ -691,10 +696,12 @@ def add_auth_to_method(self, path: str, method_name: str, auth: Dict[str, Any],
691696
method_scopes = auth and auth.get("AuthorizationScopes")
692697
api_auth = api and api.get("Auth")
693698
authorizers = api_auth and api_auth.get("Authorizers")
699+
694700
if method_authorizer:
695701
self._set_method_authorizer(path, method_name, method_authorizer, authorizers, method_scopes) # type: ignore[no-untyped-call]
696702

697703
method_apikey_required = auth and auth.get("ApiKeyRequired")
704+
698705
if method_apikey_required is not None:
699706
self._set_method_apikey_handling(path, method_name, method_apikey_required) # type: ignore[no-untyped-call]
700707

schema_source/sam.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3015,6 +3015,10 @@
30153015
"samtranslator__internal__schema_source__aws_serverless_api__Auth": {
30163016
"additionalProperties": false,
30173017
"properties": {
3018+
"AddApiKeyRequiredToCorsPreflight": {
3019+
"title": "Addapikeyrequiredtocorspreflight",
3020+
"type": "boolean"
3021+
},
30183022
"AddDefaultAuthorizerToCorsPreflight": {
30193023
"description": "If the `DefaultAuthorizer` and `Cors` properties are set, then setting `AddDefaultAuthorizerToCorsPreflight` will cause the default authorizer to be added to the `Options` property in the OpenAPI section\\. \n*Type*: Boolean \n*Required*: No \n*Default*: True \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",
30203024
"markdownDescription": "If the `DefaultAuthorizer` and `Cors` properties are set, then setting `AddDefaultAuthorizerToCorsPreflight` will cause the default authorizer to be added to the `Options` property in the OpenAPI section\\. \n*Type*: Boolean \n*Required*: No \n*Default*: True \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
3+
Transform:
4+
- AWS::Serverless-2016-10-31
5+
6+
Globals:
7+
Api:
8+
Auth:
9+
ApiKeyRequired: true
10+
AddApiKeyRequiredToCorsPreflight: false
11+
12+
Resources:
13+
14+
MyFunction:
15+
Type: AWS::Serverless::Function
16+
Properties:
17+
Handler: index.handler
18+
InlineCode: |
19+
exports.handler = async function (event) {
20+
return {
21+
statusCode: 200,
22+
body: JSON.stringify({ message: "Hello, SAM!" }),
23+
}
24+
}
25+
Runtime: nodejs16.x
26+
27+
ApiGatewayLambdaRole:
28+
Type: AWS::IAM::Role
29+
Properties:
30+
AssumeRolePolicyDocument:
31+
Version: '2012-10-17'
32+
Statement:
33+
- Effect: Allow
34+
Principal: {Service: apigateway.amazonaws.com}
35+
Action: sts:AssumeRole
36+
Policies:
37+
- PolicyName: AllowInvokeLambdaFunctions
38+
PolicyDocument:
39+
Version: '2012-10-17'
40+
Statement:
41+
- Effect: Allow
42+
Action: lambda:InvokeFunction
43+
Resource: '*'
44+
45+
MyApi:
46+
Type: AWS::Serverless::Api
47+
Properties:
48+
Cors:
49+
AllowMethods: "'methods'"
50+
AllowHeaders: "'headers'"
51+
AllowOrigin: "'origins'"
52+
MaxAge: "'600'"
53+
Auth:
54+
ApiKeyRequired: true
55+
StageName: dev
56+
DefinitionBody:
57+
openapi: 3.0.1
58+
paths:
59+
/apione:
60+
get:
61+
x-amazon-apigateway-integration:
62+
credentials:
63+
Fn::Sub: ${ApiGatewayLambdaRole.Arn}
64+
uri:
65+
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations
66+
passthroughBehavior: when_no_match
67+
httpMethod: POST
68+
type: aws_proxy
69+
/apitwo:
70+
get:
71+
x-amazon-apigateway-integration:
72+
credentials:
73+
Fn::Sub: ${ApiGatewayLambdaRole.Arn}
74+
uri:
75+
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations
76+
passthroughBehavior: when_no_match
77+
httpMethod: POST
78+
type: aws_proxy
79+
80+
81+
82+
Outputs:
83+
ApiUrl:
84+
Description: URL of your API endpoint
85+
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/"
86+
Metadata:
87+
SamTransformTest: true
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
3+
Transform:
4+
- AWS::Serverless-2016-10-31
5+
6+
Resources:
7+
8+
MyFunction:
9+
Type: AWS::Serverless::Function
10+
Properties:
11+
Handler: index.handler
12+
InlineCode: |
13+
exports.handler = async function (event) {
14+
return {
15+
statusCode: 200,
16+
body: JSON.stringify({ message: "Hello, SAM!" }),
17+
}
18+
}
19+
Runtime: nodejs16.x
20+
21+
ApiGatewayLambdaRole:
22+
Type: AWS::IAM::Role
23+
Properties:
24+
AssumeRolePolicyDocument:
25+
Version: '2012-10-17'
26+
Statement:
27+
- Effect: Allow
28+
Principal: {Service: apigateway.amazonaws.com}
29+
Action: sts:AssumeRole
30+
Policies:
31+
- PolicyName: AllowInvokeLambdaFunctions
32+
PolicyDocument:
33+
Version: '2012-10-17'
34+
Statement:
35+
- Effect: Allow
36+
Action: lambda:InvokeFunction
37+
Resource: '*'
38+
39+
MyApi:
40+
Type: AWS::Serverless::Api
41+
Properties:
42+
Cors: "'*'"
43+
Auth:
44+
ApiKeyRequired: true
45+
AddApiKeyRequiredToCorsPreflight: false
46+
StageName: dev
47+
DefinitionBody:
48+
openapi: 3.0.1
49+
paths:
50+
/hello:
51+
get:
52+
x-amazon-apigateway-integration:
53+
credentials:
54+
Fn::Sub: ${ApiGatewayLambdaRole.Arn}
55+
uri:
56+
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations
57+
passthroughBehavior: when_no_match
58+
httpMethod: POST
59+
type: aws_proxy
60+
61+
62+
63+
Outputs:
64+
WebEndpoint:
65+
Description: API Gateway endpoint URL
66+
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/hello"

0 commit comments

Comments
 (0)