-
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 local custom authorizers #467
Conversation
Codecov Report
@@ Coverage Diff @@
## master #467 +/- ##
==========================================
+ Coverage 94.01% 94.26% +0.25%
==========================================
Files 18 18
Lines 2939 3103 +164
Branches 380 396 +16
==========================================
+ Hits 2763 2925 +162
Misses 129 129
- Partials 47 49 +2
Continue to review full report at Codecov.
|
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.
Code looks good! Only had a few minor comments.
def create_local_server(self, app_obj, port): | ||
# type: (Chalice, int) -> local.LocalDevServer | ||
return local.create_local_server(app_obj, port) | ||
def create_local_server(self, app_obj, config, port): |
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.
You don't appear to be testing this function
chalice/local.py
Outdated
self._initialize_lambda_context_attributes( | ||
function_name, memory_size) | ||
|
||
def _initialize_lambda_context_attributes(self, function_name, |
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.
It's a bit bizzare to define instance attributes outside of init, especially since you're not doing any complicated initialization or logic to set their initial values.
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 separated the logic that is specific to the actual lambda context initialization vs the initializations I have to do for my fake object to run.
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.
Yeah I'm surprised pylint didn't yell at you for this. Even if you want it extracted out to a separate function, I think they should also be "declared" in the __init__
.
chalice/local.py
Outdated
def _prepare_authorizer_event(self, lambda_event): | ||
# type: (EventType) -> EventType | ||
"""Translate event for an authorizer input.""" | ||
authorizer_event = lambda_event.copy() |
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.
How nested is this structure? if it's more than one level then maybe you should deepcopy.
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.
The event is a dict of entirely [str, Union[str, Dict[str, str]] so I don't think it needs to be deepcopied unless I am mistaken about something.
chalice/local.py
Outdated
try: | ||
route_entry = self._app_object.routes[resource_path][http_method] | ||
except KeyError: | ||
# If there a key error is raised when trying to get the route entry |
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.
s/If there/If/
chalice/local.py
Outdated
# here by returning no authorizer to avoid duplicating the logic. | ||
return None, None | ||
authorizer = route_entry.authorizer | ||
return authorizer, route_entry |
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.
If authorizer
is an attribute of route_entry
why are you not just returning route_entry
?
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.
Yes I think this made more sense before a refactor and I didn't think to update it.
chalice/local.py
Outdated
# Technically we only need to check for authorizer being `None` here | ||
# since the route_entry will always match authorizer but mypy needs us | ||
# to explicitly check both before we can use them. In any event, if | ||
# either of these are not defined then there was no authroizer or |
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.
You seem to have trailed off here.
return response | ||
|
||
|
||
class ChaliceRequestHandler(BaseHTTPRequestHandler): |
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.
A class doc string for this would be nice
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.
Some initial feedback, still going through it.
chalice/local.py
Outdated
self._initialize_lambda_context_attributes( | ||
function_name, memory_size) | ||
|
||
def _initialize_lambda_context_attributes(self, function_name, |
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.
Yeah I'm surprised pylint didn't yell at you for this. Even if you want it extracted out to a separate function, I think they should also be "declared" in the __init__
.
chalice/local.py
Outdated
# automatically generate our CORS headers. | ||
options_headers = self._autogen_options_headers(lambda_event) | ||
raise NoOptionsRouteDefined(options_headers) | ||
# The authroizer call will be a noop if there is no authroizer method |
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.
%s/authroizer/authorizer/g
chalice/local.py
Outdated
# BuiltinAuthConfig type. Anything else we will err on the side of | ||
# allowing local testing by simply admiting the request. Otherwise | ||
# there is no way for users to test their code in local mode | ||
warnings.warn(( |
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.
You shouldn't need the extra set of parens here.
chalice/local.py
Outdated
# particular view function. | ||
for statement in statements: | ||
if statement.get('Effect') == 'Allow' and \ | ||
statement.get('Action') == 'execute-api:Invoke': |
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.
These technically support wildcards as well. If you're not going to support them, you should probably make a note about eventually supporting them.
chalice/local.py
Outdated
pattern_suffix = '/' | ||
method_pattern = r'(?:\*|%s)' % route_entry.method | ||
route_entry_pattern = r'^.*?/%s%s%s$' % (method_pattern, | ||
route_entry.uri_pattern, |
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 don't think this is right. The uri pattern will include the placeholder values so something like this won't work:
@app.route('/foo/{name}', authorizer=demo_auth)
def index(name):
return {'context': app.current_request.context}
If I return AuthResponse(routes=['/foo/bar'], principal_id='user')
from my authorizer.
What if we mirrored things more closely to real IAM?
- Generate the resource ARN for the specific method/view
- Check if any of the resources in the returned policy match the ARN (accounting for wildcards).
I also would suggest extracting this policy matching out to a separate class so we can add a lot more tests for the edge cases (using pytest.paramtrize
).
bd86e3b
to
a47eb8c
Compare
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.
Implementation looks good! Just one little nitpick before I go over the tests.
chalice/local.py
Outdated
escaped_resource = re.escape(resource) | ||
resource_regex = escaped_resource.replace(r'\?', '.').replace( | ||
r'\*', '.*?') | ||
matcher = re.compile(resource_regex) |
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.
nitpick, but you don't need to compile if you're just immediately using it anyway
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.
Just had some minor feedback.
@@ -1,3 +1,4 @@ | |||
from chalice.local import LambdaContext |
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.
Import orders, chalice after 3rd party libs.
chalice/local.py
Outdated
return LocalDevServer(app_obj, config, port) | ||
|
||
|
||
class LocalArnBuilder(object): |
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.
s/Arn/ARN
chalice/local.py
Outdated
) | ||
|
||
|
||
class ArnMatcher(object): |
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.
s/Arn/ARN
chalice/local.py
Outdated
% authorizer.__class__.__name__ | ||
) | ||
return lambda_event, lambda_context | ||
auth_result = authorizer(auth_event, lambda_context) |
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 don't think this is quite right. If you don't have the appropriate auth header (Authorization
) in the request, you get a 401 and the authorizer isn't called at all.
chalice/local.py
Outdated
|
||
def _send_not_authorized_response(self, headers): | ||
# type: (HeaderType) -> None | ||
self.send_response(401) |
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.
An authorized error where we generate a policy that forbids access to a resource gives a 403, not a 401.
$ http https://rest-api-id.execute-api.us-west-2.amazonaws.com/api/ 'Authorization: deny'
HTTP/1.1 403 Forbidden
Connection: keep-alive
Content-Length: 60
Content-Type: application/json
Date: Thu, 24 Aug 2017 00:27:57 GMT
Via: 1.1 7f3f42df8af148df1f9f1ee7175987ad.cloudfront.net (CloudFront)
X-Amz-Cf-Id: 4ZhC3GokHeXr68EKakpF6xQWxVvTDZtQiGK253psOcWbPW3M980rGw==
X-Cache: Error from cloudfront
x-amzn-ErrorType: AccessDeniedException
x-amzn-RequestId: 13ff2f6d-8863-11e7-af78-e1cc9bcdaa69
{
"Message": "User is not authorized to access this resource"
}
chalice/local.py
Outdated
time_source = Clock() | ||
self._time_source = time_source | ||
self._start_time = self._current_time_millis() | ||
self._max_runtime = runtime_millis |
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.
runtime_millis
is pretty vague, but max_runtime_ms
is a lot clearer. Is there a reason to not map to call the arg something different than it's instance attribute name?
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 wanted to give the unit in the name is why its not a direct mirror. I agree with max_runtime_ms
so I'll do that.
tests/unit/test_local.py
Outdated
assert dev_server.server.server_port == 23456 | ||
|
||
|
||
class FakeTimeSource(object): |
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 usually put all the non test classes/functions at the top of the file before the actual tests.
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 was keeping it nearby for easier reference. I'll move it.
tests/unit/test_local.py
Outdated
event, context = authorizer.authorize(event, context) | ||
assert event['requestContext']['authorizer']['principalId'] == 'user' | ||
|
||
def test_can_authorize_with_lower_case_headers(self, lambda_context_args, |
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.
Isn't this identical to the previous test test_can_authorize_empty_path
?
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.
Yea...
tests/unit/test_local.py
Outdated
event, context = authorizer.authorize(event, context) | ||
# Assert that when the authorizer.authorize is called and there is no | ||
# authorizer defined for a particular route that it is a noop. | ||
assert event == event |
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.
?
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.
Fixed... still passes.
tests/unit/test_local.py
Outdated
# Assert that when the authorizer.authorize is called and there is no | ||
# authorizer defined for a particular route that it is a noop. | ||
assert event == event | ||
assert context == context |
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.
?
e006290
to
3712b8b
Compare
I'm just going through this now, but it looks like there's still some feedback that hasn't been incorporated: #467 (comment) The ARN generated for a route that has a captured param is still: |
0e58247
to
2030304
Compare
517261f
to
8403c5e
Compare
BuiltinAuthorizer will now work in local mode. Authorizer types that cannot be run locally will now simply allow the requests to proceed as if there were no authorizer to allow for local testing.
8403c5e
to
7305052
Compare
BuiltinAuthorizer will now work in local mode. Authorizer types that
cannot be run locally will now simply allow the requests to proceed as
if there were no authorizer to allow for local testing.
closes #404