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

Fix API v2 exception on incorrect API key. Fixes #5425 #6703

Merged
merged 8 commits into from
May 20, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- Fixed "send to trash" option not doing anything (Python 3.6 and higher) ([#6625](https://github.com/pymedusa/Medusa/pull/6625))
- Fixed setting episodes to archived in backlog overview ([#6636](https://github.com/pymedusa/Medusa/pull/6636))
- Fixed exception in Elte-Tracker provider when no result is found ([#6680](https://github.com/pymedusa/Medusa/pull/6680))
- Fixed exception in API v2 when an incorrect API key was provided, or none was provided ([#6703](https://github.com/pymedusa/Medusa/pull/6703))

## 0.3.1 (2019-03-20)

Expand Down
6 changes: 3 additions & 3 deletions medusa/server/api/v2/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ class AuthHandler(BaseRequestHandler):
#: allowed HTTP methods
allowed_methods = ('POST', )

def prepare(self):
"""Prepare."""
pass
def _check_authentication(self):
"""Override authentication check for the authentication endpoint."""
return None

def http_post(self, *args, **kwargs):
"""Request JWT."""
Expand Down
64 changes: 36 additions & 28 deletions medusa/server/api/v2/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@
from six import iteritems, string_types, text_type, viewitems

from tornado.gen import coroutine
from tornado.httpclient import HTTPError
from tornado.httputil import url_concat
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler
from tornado.web import HTTPError, RequestHandler

log = BraceAdapter(logging.getLogger(__name__))
log.logger.addHandler(logging.NullHandler())
Expand Down Expand Up @@ -56,11 +55,8 @@ class BaseRequestHandler(RequestHandler):
#: parent resource handler
parent_handler = None

def prepare(self):
def _check_authentication(self):
"""Check if JWT or API key is provided and valid."""
if self.request.method == 'OPTIONS':
return

api_key = self.get_argument('api_key', default=None) or self.request.headers.get('X-Api-Key')
if api_key and api_key == app.API_KEY:
return
Expand Down Expand Up @@ -90,13 +86,11 @@ def async_call(self, name, *args, **kwargs):
try:
method = getattr(self, 'http_' + name)
except AttributeError:
raise HTTPError(405, '{name} method is not allowed'.format(name=name.upper()))
raise HTTPError(405)

def blocking_call():
try:
return method(*args, **kwargs)
except Exception as error:
self._handle_request_exception(error)
result = self._check_authentication()
return method(*args, **kwargs) if result is None else result
Copy link
Contributor

Choose a reason for hiding this comment

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

Your kinda asuming that the method call cant raise exceptions?

Copy link
Contributor Author

@sharkykh sharkykh May 20, 2019

Choose a reason for hiding this comment

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

I'll try to explain as best I can.
Basically - yes.
But the difference is the RequestHandler class has an exception handler, and we handle everything in write_error now.

def write_error(self, status_code, *args, **kwargs):
"""Only send traceback if app.DEVELOPER is true."""
response = None
exc_info = kwargs.get('exc_info', None)
if exc_info and isinstance(exc_info[1], HTTPError):
error = exc_info[1].log_message or exc_info[1].reason
response = self.api_response(status=status_code, error=error)
elif app.DEVELOPER and exc_info:
self.set_header('content-type', 'text/plain')
self.set_status(500)
for line in traceback.format_exception(*exc_info):
self.write(line)
else:
response = self._internal_server_error()

HTTPErrors are handled as RESTful responses,
And anything other than HTTPError is handled as an actual exception and sends "Internal Server Error" as the response (or the traceback, if app.DEVELOPER is enabled, like before)

Copy link
Contributor Author

@sharkykh sharkykh May 20, 2019

Choose a reason for hiding this comment

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

Your kinda asuming that the method call cant raise exceptions?

Sorry, I actually misread that.
Quite the opposite, I plan on it, but as I explained in the comment above, it's all being handled (just in one location and not everywhere)


return IOLoop.current().run_in_executor(executor, blocking_call)

Expand Down Expand Up @@ -154,17 +148,41 @@ def put(self, *args, **kwargs):
if not self._finished:
self.finish(content)

def write_error(self, *args, **kwargs):
def write_error(self, status_code, *args, **kwargs):
"""Only send traceback if app.DEVELOPER is true."""
if app.DEVELOPER and 'exc_info' in kwargs:
response = None
exc_info = kwargs.get('exc_info', None)

if exc_info and isinstance(exc_info[1], HTTPError):
error = exc_info[1].log_message or exc_info[1].reason
response = self.api_response(status=status_code, error=error)
elif app.DEVELOPER and exc_info:
self.set_header('content-type', 'text/plain')
self.set_status(500)
for line in traceback.format_exception(*kwargs['exc_info']):
for line in traceback.format_exception(*exc_info):
self.write(line)
self.finish()
else:
response = self._internal_server_error()
self.finish(response)

self.finish(response)

def log_exception(self, typ, value, tb):
"""
Customize logging of uncaught exceptions.

Only logs unhandled exceptions, as ``HTTPErrors`` are common for a RESTful API handler.
"""
if not app.WEB_LOG:
return

if isinstance(value, HTTPError):
return

log.error('Uncaught exception: {summary}\n{req!r}', {
'summary': self._request_summary(),
'req': self.request,
'exc_info': (typ, value, tb),
})

def options(self, *args, **kwargs):
"""OPTIONS HTTP method."""
Expand Down Expand Up @@ -248,13 +266,6 @@ def create_app_handler(cls, base):

return cls.create_url(base, cls.name, *(cls.identifier, cls.path_param)), cls

def _handle_request_exception(self, error):
if isinstance(error, HTTPError):
response = self.api_response(error.code, error.message)
self.finish(response)
else:
super(BaseRequestHandler, self)._handle_request_exception(error)

def _ok(self, data=None, headers=None, stream=None, content_type=None):
return self.api_response(200, data=data, headers=headers, stream=stream, content_type=content_type)

Expand Down Expand Up @@ -335,11 +346,8 @@ def _get_limit(self, default=20, maximum=1000):
self._raise_bad_request_error('Invalid limit parameter')

def _paginate(self, data=None, data_generator=None, sort=None):
try:
arg_page = self._get_page()
arg_limit = self._get_limit()
except HTTPError as error:
return self._bad_request(error.message)
arg_page = self._get_page()
arg_limit = self._get_limit()

headers = {
'X-Pagination-Page': arg_page,
Expand Down
37 changes: 37 additions & 0 deletions tests/apiv2/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# coding=utf-8
"""Test /authentication route and authentication checks."""
from __future__ import unicode_literals

import json

import pytest


@pytest.mark.gen_test
def test_no_api_key(http_client, create_url):
# given
expected = {'error': 'No authorization token.'}

url = create_url('/log', api_key=None)

# when
response = yield http_client.fetch(url, raise_error=False)

# then
assert response.code == 401
assert expected == json.loads(response.body)


@pytest.mark.gen_test
def test_bad_api_key(http_client, create_url):
# given
expected = {'error': 'No authorization token.'}

url = create_url('/log', api_key='123')

# when
response = yield http_client.fetch(url, raise_error=False)

# then
assert response.code == 401
assert expected == json.loads(response.body)