-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Add support for builtin API Gateway authorizers #381
Changes from 10 commits
35162b8
33cf604
8ff56f6
cdc975b
2caaff8
2cdde29
6789322
107c07e
a234e45
8aa07e7
c95b37b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -417,6 +417,7 @@ def __init__(self, app_name, configure_logs=True): | |
self.configure_logs = configure_logs | ||
self.log = logging.getLogger(self.app_name) | ||
self._authorizers = {} | ||
self.builtin_auth_handlers = [] | ||
if self.configure_logs: | ||
self._configure_logging() | ||
|
||
|
@@ -461,6 +462,27 @@ def define_authorizer(self, name, header, auth_type, provider_arns=None): | |
'provider_arns': provider_arns, | ||
} | ||
|
||
def authorizer(self, name=None, **kwargs): | ||
def _register_authorizer(auth_func): | ||
auth_name = name | ||
if auth_name is None: | ||
auth_name = auth_func.__name__ | ||
ttl_seconds = kwargs.pop('ttl_seconds', None) | ||
execution_role = kwargs.pop('execution_role', None) | ||
if kwargs: | ||
raise TypeError( | ||
'TypeError: authorizer() got unexpected keyword ' | ||
'arguments: %s' % ', '.join(list(kwargs))) | ||
auth_config = BuiltinAuthConfig( | ||
name=auth_name, | ||
handler_string='app.%s' % auth_func.__name__, | ||
ttl_seconds=ttl_seconds, | ||
execution_role=execution_role, | ||
) | ||
self.builtin_auth_handlers.append(auth_config) | ||
return ChaliceAuthorizer(name, auth_func, auth_config) | ||
return _register_authorizer | ||
|
||
def route(self, path, **kwargs): | ||
def _register_view(view_func): | ||
self._add_route(path, view_func, **kwargs) | ||
|
@@ -626,3 +648,120 @@ def _add_cors_headers(self, response, cors): | |
for name, value in cors.get_access_control_headers().items(): | ||
if name not in response.headers: | ||
response.headers[name] = value | ||
|
||
|
||
class BuiltinAuthConfig(object): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What was the reason for calling it Builtin as opposed to Custom? Custom falls more in-line with what API Gateway uses in its documentation. Or maybe just calling them AuthConfig's? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There'a already a custom authorizer in the chalice code. The nomenclature that made the most sense to be (given what's already there):
|
||
def __init__(self, name, handler_string, ttl_seconds=None, | ||
execution_role=None): | ||
# We'd also support all the misc config options you can set. | ||
self.name = name | ||
self.handler_string = handler_string | ||
self.ttl_seconds = ttl_seconds | ||
self.execution_role = execution_role | ||
|
||
|
||
class ChaliceAuthorizer(object): | ||
def __init__(self, name, func, config): | ||
self.name = name | ||
self.func = func | ||
self.config = config | ||
|
||
def __call__(self, event, content): | ||
auth_request = self._transform_event(event) | ||
result = self.func(auth_request) | ||
if isinstance(result, AuthResponse): | ||
return result.to_dict(auth_request) | ||
return result | ||
|
||
def _transform_event(self, event): | ||
return AuthRequest(event['type'], | ||
event['authorizationToken'], | ||
event['methodArn']) | ||
|
||
|
||
class AuthRequest(object): | ||
def __init__(self, auth_type, token, method_arn): | ||
self.auth_type = auth_type | ||
self.token = token | ||
self.method_arn = method_arn | ||
|
||
|
||
class AuthResponse(object): | ||
ALL_HTTP_METHODS = ['DELETE', 'HEAD', 'OPTIONS', | ||
'PATCH', 'POST', 'PUT', 'GET'] | ||
|
||
def __init__(self, routes, principal_id, context=None): | ||
self.routes = routes | ||
self.principal_id = principal_id | ||
# The request is used to generate full qualified ARNs | ||
# that we need for the resource portion of the returned | ||
# policy. | ||
if context is None: | ||
context = {} | ||
self.context = context | ||
|
||
def to_dict(self, request): | ||
return { | ||
'context': self.context, | ||
'principalId': self.principal_id, | ||
'policyDocument': self._generate_policy(request), | ||
} | ||
|
||
def _generate_policy(self, request): | ||
allowed_resources = self._generate_allowed_resources(request) | ||
return { | ||
'Version': '2012-10-17', | ||
'Statement': [ | ||
{ | ||
'Action': 'execute-api:Invoke', | ||
'Effect': 'Allow', | ||
'Resource': allowed_resources, | ||
} | ||
] | ||
} | ||
|
||
def _generate_allowed_resources(self, request): | ||
allowed_resources = [] | ||
for route in self.routes: | ||
if isinstance(route, AuthRoute): | ||
methods = route.methods | ||
path = route.path | ||
else: | ||
# If 'route' is just a string, then they've | ||
# opted not to use the AuthRoute(), so we'll | ||
# generate a policy that allows all HTTP methods. | ||
methods = self.ALL_HTTP_METHODS | ||
path = route | ||
for method in methods: | ||
allowed_resources.append( | ||
self._generate_arn(path, request, method)) | ||
return allowed_resources | ||
|
||
def _generate_arn(self, route, request, method='*'): | ||
incoming_arn = request.method_arn | ||
parts = incoming_arn.rsplit(':', 1) | ||
# "arn:aws:execute-api:us-west-2:123:rest-api-id/dev/GET/needs/auth" | ||
# Then we pull out the rest-api-id and stage, such that: | ||
# base = ['rest-api-id', 'stage'] | ||
base = parts[-1].split('/')[:2] | ||
# Now we add in the path components and rejoin everything | ||
# back together to make a full arn. | ||
# We're also assuming all HTTP methods (via '*') for now. | ||
# To support per HTTP method routes the API will need to be updated. | ||
# We also need to strip off the leading ``/`` so it can be | ||
# '/'.join(...)'d properly. | ||
base.extend([method, route[1:]]) | ||
last_arn_segment = '/'.join(base) | ||
if route == '/': | ||
# We have to special case the '/' case. For whatever | ||
# reason, API gateway adds an extra '/' to the method_arn | ||
# of the auth request, so we need to do the same thing. | ||
last_arn_segment += '/' | ||
final_arn = '%s:%s' % (parts[0], last_arn_segment) | ||
return final_arn | ||
|
||
|
||
class AuthRoute(object): | ||
def __init__(self, path, methods): | ||
self.path = path | ||
self.methods = methods |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
import shutil | ||
import json | ||
import re | ||
import uuid | ||
|
||
import botocore.session # noqa | ||
from botocore.exceptions import ClientError | ||
|
@@ -275,7 +276,7 @@ def get_role_arn_for_name(self, name): | |
try: | ||
role = client.get_role(RoleName=name) | ||
except client.exceptions.NoSuchEntityException: | ||
raise ValueError("No role ARN found for: %s" % name) | ||
raise ResourceDoesNotExistError("No role ARN found for: %s" % name) | ||
return role['Role']['Arn'] | ||
|
||
def delete_role_policy(self, role_name, policy_name): | ||
|
@@ -507,12 +508,14 @@ def get_sdk_download_stream(self, rest_api_id, | |
return response['body'] | ||
|
||
def add_permission_for_apigateway(self, function_name, region_name, | ||
account_id, rest_api_id, random_id): | ||
# type: (str, str, str, str, str) -> None | ||
account_id, rest_api_id, random_id=None): | ||
# type: (str, str, str, str, Optional[str]) -> None | ||
"""Authorize API gateway to invoke a lambda function.""" | ||
client = self._client('lambda') | ||
source_arn = self._build_source_arn_str(region_name, account_id, | ||
rest_api_id) | ||
if random_id is None: | ||
random_id = self._random_id() | ||
client.add_permission( | ||
Action='lambda:InvokeFunction', | ||
FunctionName=function_name, | ||
|
@@ -564,3 +567,40 @@ def _client(self, service_name): | |
self._client_cache[service_name] = self._session.create_client( | ||
service_name) | ||
return self._client_cache[service_name] | ||
|
||
def add_permission_for_authorizer(self, rest_api_id, function_arn, | ||
random_id=None): | ||
# type: (str, str, Optional[str]) -> None | ||
client = self._client('apigateway') | ||
# This is actually a paginated operation, but botocore does not | ||
# support this style of pagination right now. The max authorizers | ||
# for an API is 10, so we're ok for now. We will need to circle | ||
# back on this eventually. | ||
authorizers = client.get_authorizers(restApiId=rest_api_id) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may want to be using a paginator here. Unfortunately, I do not think botocore has one yet. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does not. I couldn't find in their docs what the page size, but I think this might be a forward compatibility thing in their API. The max authorizers for an API is 10 (http://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html) so I don't think we'll be affected. I can add a comment about that though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That works too. |
||
for authorizer in authorizers['items']: | ||
if function_arn in authorizer['authorizerUri']: | ||
authorizer_id = authorizer['id'] | ||
break | ||
else: | ||
raise ResourceDoesNotExistError( | ||
"Unable to find authorizer associated " | ||
"with function ARN: %s" % function_arn) | ||
parts = function_arn.split(':') | ||
region_name = parts[3] | ||
account_id = parts[4] | ||
function_name = parts[-1] | ||
source_arn = ("arn:aws:execute-api:%s:%s:%s/authorizers/%s" % | ||
(region_name, account_id, rest_api_id, authorizer_id)) | ||
if random_id is None: | ||
random_id = self._random_id() | ||
self._client('lambda').add_permission( | ||
Action='lambda:InvokeFunction', | ||
FunctionName=function_name, | ||
StatementId=random_id, | ||
Principal='apigateway.amazonaws.com', | ||
SourceArn=source_arn, | ||
) | ||
|
||
def _random_id(self): | ||
# type: () -> str | ||
return str(uuid.uuid4()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably want to put validation in on if they pass in kwargs that do not get popped off like we do in
route()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll update.