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

feat: usage plans support for Api Auth #1179

Merged
merged 12 commits into from
Jan 21, 2020
Merged
Show file tree
Hide file tree
Changes from 10 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
96 changes: 96 additions & 0 deletions examples/2016-10-31/usage_plan/template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
Conditions:
C1:
Fn::Equals:
- true
- true
keetonian marked this conversation as resolved.
Show resolved Hide resolved

Parameters:
UsagePlanType:
Type: String
Default: PER_API

Globals:
Api:
OpenApiVersion: 3.0.0
Auth:
ApiKeyRequired: true
UsagePlan:
CreateUsagePlan: !Ref UsagePlanType

Resources:
MyApiOne:
Condition: C1
Type: AWS::Serverless::Api
Properties:
StageName: Prod
keetonian marked this conversation as resolved.
Show resolved Hide resolved

MyApiTwo:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Auth:
UsagePlan:
CreateUsagePlan: SHARED
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this not create a UsagePlan now?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it creates a usage plan with logical id ServerlessUsagePlan


MyApiTwoUsagePlan:
Type: AWS::ApiGateway::UsagePlan
DependsOn:
- MyApiTwo
Properties:
ApiStages:
- ApiId: !Ref MyApiTwo
Stage: !Ref MyApiTwo.Stage
keetonian marked this conversation as resolved.
Show resolved Hide resolved

MyFunctionOne:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs8.10
InlineCode: |
exports.handler = async (event) => {
return {
statusCode: 200,
body: JSON.stringify(event),
headers: {}
}
}
Events:
ApiKey:
Type: Api
Properties:
RestApiId:
Ref: MyApiOne
Method: get
Path: /path/one

MyFunctionTwo:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs8.10
InlineCode: |
exports.handler = async (event) => {
return {
statusCode: 200,
body: JSON.stringify(event),
headers: {}
}
}
Events:
ApiKey:
Type: Api
Properties:
RestApiId:
Ref: MyApiTwo
Method: get
Path: /path/two
Outputs:
ApiOneUrl:
Description: "API endpoint URL for Prod environment"
Value:
Fn::Sub: 'https://${MyApiOne}.execute-api.${AWS::Region}.amazonaws.com/Prod/'

ApiTwoUrl:
Description: "API endpoint URL for Prod environment"
Value:
Fn::Sub: 'https://${MyApiTwo}.execute-api.${AWS::Region}.amazonaws.com/Prod/'
146 changes: 144 additions & 2 deletions samtranslator/model/api/api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
ApiGatewayResponse,
ApiGatewayDomainName,
ApiGatewayBasePathMapping,
ApiGatewayUsagePlan,
ApiGatewayUsagePlanKey,
ApiGatewayApiKey,
)
from samtranslator.model.route53 import Route53RecordSetGroup
from samtranslator.model.exceptions import InvalidResourceException
Expand Down Expand Up @@ -37,14 +40,24 @@
"AddDefaultAuthorizerToCorsPreflight",
"ApiKeyRequired",
"ResourcePolicy",
"UsagePlan",
],
)
AuthProperties.__new__.__defaults__ = (None, None, None, True, None, None)
AuthProperties.__new__.__defaults__ = (None, None, None, True, None, None, None)
UsagePlanProperties = namedtuple(
"_UsagePlanProperties", ["CreateUsagePlan", "Description", "Quota", "Tags", "Throttle", "UsagePlanName"]
)
UsagePlanProperties.__new__.__defaults__ = (None, None, None, None, None, None)

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


class ApiGenerator(object):
usage_plan_shared = False
stage_keys_shared = list()
api_stages_shared = list()
depends_on_shared = list()

def __init__(
self,
logical_id,
Expand Down Expand Up @@ -392,8 +405,9 @@ def to_cloudformation(self, redeploy_restapi_parameters):

stage = self._construct_stage(deployment, swagger, redeploy_restapi_parameters)
permissions = self._construct_authorizer_lambda_permission()
usage_plan = self._construct_usage_plan(rest_api_stage=stage)

return rest_api, deployment, stage, permissions, domain, basepath_mapping, route53
return rest_api, deployment, stage, permissions, domain, basepath_mapping, route53, usage_plan

def _add_cors(self):
"""
Expand Down Expand Up @@ -518,6 +532,134 @@ def _add_auth(self):

self.definition_body = self._openapi_postprocess(swagger_editor.swagger)

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: UsagePlan, UsagePlanKey, ApiKey for this rest Api
:rtype: model.apigateway.ApiGatewayUsagePlan, model.apigateway.ApiGatewayUsagePlanKey,
model.apigateway.ApiGatewayApiKey
"""
if not self.auth:
return []
auth_properties = AuthProperties(**self.auth)
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()):
praneetap marked this conversation as resolved.
Show resolved Hide resolved
raise InvalidResourceException(self.logical_id, "Invalid property for 'UsagePlan'")

create_usage_plan = usage_plan_properties.get("CreateUsagePlan")
usage_plan = None
api_key = None
usage_plan_key = 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, PER_API and SHARED values"
praneetap marked this conversation as resolved.
Show resolved Hide resolved
)

ShreyaGangishetty marked this conversation as resolved.
Show resolved Hide resolved
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":
usage_plan_logical_id = "ServerlessUsagePlan"
ApiGenerator.depends_on_shared.append(self.logical_id)
usage_plan = ApiGatewayUsagePlan(
logical_id=usage_plan_logical_id, depends_on=ApiGenerator.depends_on_shared
)
api_stage = dict()
api_stage["ApiId"] = ref(self.logical_id)
api_stage["Stage"] = ref(rest_api_stage.logical_id)
ApiGenerator.api_stages_shared.append(api_stage)
usage_plan.ApiStages = ApiGenerator.api_stages_shared

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 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")
keetonian marked this conversation as resolved.
Show resolved Hide resolved
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"
api_key = ApiGatewayApiKey(logical_id=api_key_logical_id, depends_on=[usage_plan_logical_id])
api_key.Enabled = True
stage_key = dict()
stage_key["RestApiId"] = ref(self.logical_id)
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"
api_key = ApiGatewayApiKey(logical_id=api_key_logical_id, depends_on=[usage_plan_logical_id])
api_key.Enabled = True
stage_keys = list()
stage_key = dict()
stage_key["RestApiId"] = ref(self.logical_id)
stage_key["StageName"] = ref(rest_api_stage.logical_id)
stage_keys.append(stage_key)
api_key.StageKeys = stage_keys
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"

usage_plan_key = ApiGatewayUsagePlanKey(logical_id=usage_plan_key_logical_id, depends_on=[api_key.logical_id])
usage_plan_key.KeyId = ref(api_key.logical_id)
usage_plan_key.KeyType = "API_KEY"
usage_plan_key.UsagePlanId = ref(usage_plan_logical_id)

return usage_plan_key

def _add_gateway_responses(self):
"""
Add Gateway Response configuration to the Swagger file, if necessary
Expand Down
37 changes: 37 additions & 0 deletions samtranslator/model/apigateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,43 @@ class ApiGatewayBasePathMapping(Resource):
}


class ApiGatewayUsagePlan(Resource):
resource_type = "AWS::ApiGateway::UsagePlan"
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)}


class ApiGatewayUsagePlanKey(Resource):
resource_type = "AWS::ApiGateway::UsagePlanKey"
property_types = {
"KeyId": PropertyType(True, is_str()),
"KeyType": PropertyType(True, is_str()),
"UsagePlanId": PropertyType(True, is_str()),
}


class ApiGatewayApiKey(Resource):
resource_type = "AWS::ApiGateway::ApiKey"
property_types = {
"CustomerId": PropertyType(False, is_str()),
"Description": PropertyType(False, is_str()),
"Enabled": PropertyType(False, is_type(bool)),
"GenerateDistinctId": PropertyType(False, is_type(bool)),
"Name": PropertyType(False, is_str()),
"StageKeys": PropertyType(False, is_type(list)),
"Value": PropertyType(False, is_str()),
}

runtime_attrs = {"api_key_id": lambda self: ref(self.logical_id)}


class ApiGatewayAuthorizer(object):
_VALID_FUNCTION_PAYLOAD_TYPES = [None, "TOKEN", "REQUEST"]

Expand Down
7 changes: 6 additions & 1 deletion samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,8 @@ def to_cloudformation(self, **kwargs):
intrinsics_resolver = kwargs["intrinsics_resolver"]
self.BinaryMediaTypes = intrinsics_resolver.resolve_parameter_refs(self.BinaryMediaTypes)
self.Domain = intrinsics_resolver.resolve_parameter_refs(self.Domain)
self.Auth = intrinsics_resolver.resolve_parameter_refs(self.Auth)

redeploy_restapi_parameters = kwargs.get("redeploy_restapi_parameters")

api_generator = ApiGenerator(
Expand Down Expand Up @@ -797,7 +799,7 @@ def to_cloudformation(self, **kwargs):
domain=self.Domain,
)

rest_api, deployment, stage, permissions, domain, basepath_mapping, route53 = api_generator.to_cloudformation(
rest_api, deployment, stage, permissions, domain, basepath_mapping, route53, usage_plan_resources = api_generator.to_cloudformation(
redeploy_restapi_parameters
)

Expand All @@ -809,6 +811,9 @@ 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
Loading