Skip to content

Commit

Permalink
feat: usageplans for auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Shreya Gangishetty committed Jan 15, 2020
1 parent 25d3c4d commit 80d2cf4
Show file tree
Hide file tree
Showing 17 changed files with 643 additions and 945 deletions.
11 changes: 0 additions & 11 deletions examples/2016-10-31/usage_plan/README.md

This file was deleted.

2 changes: 1 addition & 1 deletion examples/2016-10-31/usage_plan/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Conditions:
Parameters:
UsagePlanType:
Type: String
Default: SINGLE
Default: PER_API

Globals:
Api:
Expand Down
93 changes: 51 additions & 42 deletions samtranslator/model/api/api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@
],
)
AuthProperties.__new__.__defaults__ = (None, None, None, True, None, None, None)
UsagePlanProperties = namedtuple("_UsagePlanProperties", ["CreateUsagePlan", "UsagePlanId"])
UsagePlanProperties.__new__.__defaults__ = (None, None)
UsagePlanProperties = namedtuple(
"_UsagePlanProperties", ["CreateUsagePlan", "Description", "Quota", "Tags", "Throttle", "UsagePlanName"]
)
UsagePlanProperties.__new__.__defaults__ = (None, None, None, None, None, None)

GatewayResponseProperties = ["ResponseParameters", "ResponseTemplates", "StatusCode"]

Expand Down Expand Up @@ -534,7 +536,7 @@ def _construct_usage_plan(self, rest_api_stage=None):
"""Constructs and returns the ApiGateway UsagePlan, ApiGateway UsagePlanKey, ApiGateway ApiKey for Auth.
:param model.apigateway.ApiGatewayStage stage: the stage of rest api
:returns: the UsagePlan, UsagePlanKey, ApiKey for this SAM Api
:returns: UsagePlan, UsagePlanKey, ApiKey for this rest Api
:rtype: model.apigateway.ApiGatewayUsagePlan, model.apigateway.ApiGatewayUsagePlanKey,
model.apigateway.ApiGatewayApiKey
"""
Expand All @@ -544,7 +546,6 @@ def _construct_usage_plan(self, rest_api_stage=None):
if auth_properties.UsagePlan is None:
return []
usage_plan_properties = auth_properties.UsagePlan

# throws error if the property invalid/ unsupported for UsagePlan
if not all(key in UsagePlanProperties._fields for key in usage_plan_properties.keys()):
raise InvalidResourceException(self.logical_id, "Invalid property for 'UsagePlan'")
Expand All @@ -553,42 +554,33 @@ def _construct_usage_plan(self, rest_api_stage=None):
usage_plan = None
api_key = None
usage_plan_key = None
# doesn't allow giving UsagePlanId when CreateUsagePlan is SINGLE or SHARED
if create_usage_plan not in ["SHARED", "SINGLE", "NONE"]:

if create_usage_plan is None:
raise InvalidResourceException(self.logical_id, "'CreateUsagePlan' is a required field for UsagePlan")
if create_usage_plan not in ["SHARED", "PER_API", "NONE"]:
raise InvalidResourceException(
self.logical_id, "'CreateUsagePlan' accepts only " "NONE, SINGLE and SHARED values"
self.logical_id, "'CreateUsagePlan' accepts only NONE, PER_API and SHARED values"
)

if usage_plan_properties.get("CreateUsagePlan") == "SINGLE":
if usage_plan_properties.get("UsagePlanId") is not None:
usage_plan_logical_id = usage_plan_properties.get("UsagePlanId")
if type(usage_plan_logical_id) == dict:
usage_plan_logical_id = usage_plan_logical_id.get("Ref")

api_key = self._construct_api_key(usage_plan_logical_id, create_usage_plan, rest_api_stage)
usage_plan_key = self._construct_usage_plan_key(usage_plan_logical_id, create_usage_plan, api_key)
return api_key, usage_plan_key
else:
usage_plan_logical_id = self.logical_id + "UsagePlan"
# create a usage plan for this Api
usage_plan = ApiGatewayUsagePlan(logical_id=usage_plan_logical_id, depends_on=[self.logical_id])
api_stages = list()
api_stage = dict()
api_stage["ApiId"] = ref(self.logical_id)
api_stage["Stage"] = ref(rest_api_stage.logical_id)
api_stages.append(api_stage)
usage_plan.ApiStages = api_stages

api_key = self._construct_api_key(usage_plan_logical_id, create_usage_plan, rest_api_stage)
usage_plan_key = self._construct_usage_plan_key(usage_plan_logical_id, create_usage_plan, api_key)
if create_usage_plan == "NONE":
return []

# create usage plan for this api only
elif usage_plan_properties.get("CreateUsagePlan") == "PER_API":
usage_plan_logical_id = self.logical_id + "UsagePlan"
usage_plan = ApiGatewayUsagePlan(logical_id=usage_plan_logical_id, depends_on=[self.logical_id])
api_stages = list()
api_stage = dict()
api_stage["ApiId"] = ref(self.logical_id)
api_stage["Stage"] = ref(rest_api_stage.logical_id)
api_stages.append(api_stage)
usage_plan.ApiStages = api_stages

api_key = self._construct_api_key(usage_plan_logical_id, create_usage_plan, rest_api_stage)
usage_plan_key = self._construct_usage_plan_key(usage_plan_logical_id, create_usage_plan, api_key)

# create a usage plan for all the Apis
elif create_usage_plan == "SHARED":
if usage_plan_properties.get("UsagePlanId") is not None:
raise InvalidResourceException(
self.logical_id,
'Invalid property "UsagePlanId". This field ' 'can be given only when "CreateUsagePlan" is SINGLE',
)
# create a usage plan for all the apis
usage_plan_logical_id = "ServerlessUsagePlan"
ApiGenerator.depends_on_shared.append(self.logical_id)
usage_plan = ApiGatewayUsagePlan(
Expand All @@ -602,17 +594,26 @@ def _construct_usage_plan(self, rest_api_stage=None):

api_key = self._construct_api_key(usage_plan_logical_id, create_usage_plan, rest_api_stage)
usage_plan_key = self._construct_usage_plan_key(usage_plan_logical_id, create_usage_plan, api_key)
elif create_usage_plan == "NONE":
if usage_plan_properties.get("UsagePlanId") is not None:
raise InvalidResourceException(
self.logical_id,
'Invalid property "UsagePlanId". This field ' 'can be given only when "CreateUsagePlan" is SINGLE',
)
return []

if usage_plan_properties.get("UsagePlanName"):
usage_plan.UsagePlanName = usage_plan_properties.get("UsagePlanName")
if usage_plan_properties.get("Description"):
usage_plan.Description = usage_plan_properties.get("Description")
if usage_plan_properties.get("Quota"):
usage_plan.Quota = usage_plan_properties.get("Quota")
if usage_plan_properties.get("Tags"):
usage_plan.Tags = usage_plan_properties.get("Tags")
if usage_plan_properties.get("Throttle"):
usage_plan.Throttle = usage_plan_properties.get("Throttle")
return usage_plan, api_key, usage_plan_key

def _construct_api_key(self, usage_plan_logical_id, create_usage_plan, rest_api_stage):
"""
:param usage_plan_logical_id: String
:param create_usage_plan: String
:param rest_api_stage: model.apigateway.ApiGatewayStage stage: the stage of rest api
:return: api_key model.apigateway.ApiGatewayApiKey resource which is created for the given usage plan
"""
if create_usage_plan == "SHARED":
# create an api key resource for all the apis
api_key_logical_id = "ServerlessApiKey"
Expand All @@ -623,6 +624,7 @@ def _construct_api_key(self, usage_plan_logical_id, create_usage_plan, rest_api_
stage_key["StageName"] = ref(rest_api_stage.logical_id)
ApiGenerator.stage_keys_shared.append(stage_key)
api_key.StageKeys = ApiGenerator.stage_keys_shared
# for create_usage_plan = "PER_API"
else:
# create an api key resource for this api
api_key_logical_id = self.logical_id + "ApiKey"
Expand All @@ -637,9 +639,16 @@ def _construct_api_key(self, usage_plan_logical_id, create_usage_plan, rest_api_
return api_key

def _construct_usage_plan_key(self, usage_plan_logical_id, create_usage_plan, api_key):
"""
:param usage_plan_logical_id: String
:param create_usage_plan: String
:param api_key: model.apigateway.ApiGatewayApiKey resource
:return: model.apigateway.ApiGatewayUsagePlanKey resource that contains the mapping between usage plan and api key
"""
if create_usage_plan == "SHARED":
# create a mapping between api key and the usage plan
usage_plan_key_logical_id = "ServerlessUsagePlanKey"
# for create_usage_plan = "PER_API"
else:
# create a mapping between api key and the usage plan
usage_plan_key_logical_id = self.logical_id + "UsagePlanKey"
Expand Down
9 changes: 8 additions & 1 deletion samtranslator/model/apigateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,14 @@ class ApiGatewayBasePathMapping(Resource):

class ApiGatewayUsagePlan(Resource):
resource_type = "AWS::ApiGateway::UsagePlan"
property_types = {"ApiStages": PropertyType(False, is_type(list))}
property_types = {
"ApiStages": PropertyType(False, is_type(list)),
"Description": PropertyType(False, is_str()),
"Quota": PropertyType(False, is_type(dict)),
"Tags": PropertyType(False, list_of(dict)),
"Throttle": PropertyType(False, is_type(dict)),
"UsagePlanName": PropertyType(False, is_str()),
}
runtime_attrs = {"usage_plan_id": lambda self: ref(self.logical_id)}


Expand Down
1 change: 1 addition & 0 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,7 @@ def to_cloudformation(self, **kwargs):
resources.extend(basepath_mapping)
if route53:
resources.extend([route53])
# contains usage plan, api key and usageplan key resources
if usage_plan_resources:
resources.extend(usage_plan_resources)
return resources
Expand Down
66 changes: 21 additions & 45 deletions tests/translator/input/api_with_usageplans.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,28 @@ Resources:
StageName: Prod
Auth:
UsagePlan:
CreateUsagePlan: SINGLE
UsagePlanId: !Ref MyApiTwoUsagePlan
CreateUsagePlan: PER_API
UsagePlanName: SomeRandomName
Description: "Description for usage plan"
Throttle:
BurstLimit: 1000
RateLimit: 1000
Tags:
- Key: key1
Value: value1
- Key: key2
Value: value2
Quota:
Limit: 10
Offset: 10
Period: MONTH

MyApiThree:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Auth:
UsagePlan:
CreateUsagePlan: SINGLE

MyApiFour:
MyApiThree:
Type: AWS::Serverless::Api
Properties:
StageName: Prod

MyApiTwoUsagePlan:
Type: AWS::ApiGateway::UsagePlan
DependsOn:
- MyApiTwo
Properties:
ApiStages:
- ApiId: !Ref MyApiTwo
Stage: !Ref MyApiTwo.Stage

MyFunctionOne:
Type: AWS::Serverless::Function
Properties:
Expand Down Expand Up @@ -88,30 +85,13 @@ Resources:
Ref: MyApiTwo
Method: get
Path: /path/two

MyFunctionThree:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs12.x
InlineCode: |
exports.handler = async (event) => {
return {
statusCode: 200,
body: JSON.stringify(event),
headers: {}
}
}
Events:
ApiKey:
ImplicitApiEvent:
Type: Api
Properties:
RestApiId:
Ref: MyApiThree
Method: get
Path: /path/three
Path: /path/event

MyFunctionFour:
MyFunctionThree:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Expand All @@ -129,9 +109,9 @@ Resources:
Type: Api
Properties:
RestApiId:
Ref: MyApiFour
Ref: MyApiThree
Method: get
Path: /path/four
Path: /path/three

Outputs:
ApiOneUrl:
Expand All @@ -146,7 +126,3 @@ Outputs:
Description: "API endpoint URL for Prod environment"
Value:
Fn::Sub: 'https://${MyApiThree}.execute-api.${AWS::Region}.amazonaws.com/Prod/'
ApiFourUrl:
Description: "API endpoint URL for Prod environment"
Value:
Fn::Sub: 'https://${MyApiFour}.execute-api.${AWS::Region}.amazonaws.com/Prod/'
2 changes: 1 addition & 1 deletion tests/translator/input/api_with_usageplans_intrinsics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Conditions:
Parameters:
UsagePlanType:
Type: String
Default: SINGLE
Default: PER_API

Globals:
Api:
Expand Down

This file was deleted.

Loading

0 comments on commit 80d2cf4

Please sign in to comment.