Skip to content

Commit

Permalink
Add creds validation to handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
ogolovatyi authored and ogolovatyi committed Feb 21, 2019
1 parent 0bdbc8f commit 3f55658
Show file tree
Hide file tree
Showing 15 changed files with 160 additions and 56 deletions.
42 changes: 36 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ To run the unit test suite:
python tests\runtests.py
```

Alternatevly you can run unit tests to collect code coverage data. First
install `pytest`:

```sh
pip install pytest
```

And then run `pytest` either for server or tools test, or even combined:

```sh
pytest tabpy-server/server_tests/ --cov=tabpy-server/tabpy_server
pytest tabpy-tools/tools_tests/ --cov=tabpy-tools/tabpy_tools --cov-append
```


## Linux and Mac Specific Steps

If you have downloaded Tabpy and would like to manually install Tabpy Server
Expand Down Expand Up @@ -92,18 +107,33 @@ Access-Control-Allow-Methods = GET, OPTIONS, POST
## Code styling

On github repo for merge request `pycodestyle` is used to check Python code against our
style conventions. You can run install it and run locally for file where modifications
were made:
style conventions.

You need to install `pycodestyle` locally:

```sh
pip install pycodestyle
pycodestyle <file.py>
```

And then run it for file where modifications were made, e.g.:

```sh
pycodestyle tabpy-server/server_tests/test_pwd_file.py
```

For reported errors and warnings either fix them manually or auto-format files with
`autopep8`:
`autopep8`.

To install `autopep8` run the next command:

```sh
pip install autopep8
autopep8 -i <file.py>
```
```

And then you can run the tool for a file. In the example below `-i`
option tells `autopep8` to update the file. Without the option it
outputs formated code to console.

```sh
autopep8 -i tabpy-server/server_tests/test_pwd_file.py
```
18 changes: 9 additions & 9 deletions tabpy-server/server_tests/test_validate_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from tabpy_server.handlers.util import (
validate_basic_auth_credentials,
handle_authentication,
handle_basic_authentication,
check_and_validate_basic_auth_credentials)

from unittest.mock import patch, call
Expand Down Expand Up @@ -153,37 +153,37 @@ def setUp(self):
}

def test_given_no_api_version_expect_failure(self):
self.assertFalse(handle_authentication(
self.assertFalse(handle_basic_authentication(
{}, '', self.settings, self.credentials))

def test_given_unknown_api_version_expect_failure(self):
self.assertFalse(handle_authentication(
self.assertFalse(handle_basic_authentication(
{}, 'v0.314p', self.settings, self.credentials))

def test_given_auth_is_not_configured_expect_success(self):
self.assertTrue(handle_authentication(
self.assertTrue(handle_basic_authentication(
{}, 'v0.1a', self.settings, self.credentials))

def test_given_auth_method_not_provided_expect_failure(self):
self.assertFalse(handle_authentication(
self.assertFalse(handle_basic_authentication(
{}, 'v0.2beta', self.settings, self.credentials))

def test_given_auth_method_is_unknown_expect_failure(self):
self.assertFalse(handle_authentication(
self.assertFalse(handle_basic_authentication(
{}, 'v0.3gamma', self.settings, self.credentials))

def test_given_features_not_configured_expect_success(self):
self.assertTrue(handle_authentication(
self.assertTrue(handle_basic_authentication(
{}, 'v0.4yota', self.settings, self.credentials))

def test_given_headers_not_provided_expect_failure(self):
self.assertFalse(handle_authentication(
self.assertFalse(handle_basic_authentication(
{}, 'v1', self.settings, self.credentials))

def test_given_valid_creds_expect_success(self):
b64_username_pwd = base64.b64encode(
'user1:password'.encode('utf-8')).decode('utf-8')
self.assertTrue(handle_authentication(
self.assertTrue(handle_basic_authentication(
{
'Authorization': 'Basic {}'.format(b64_username_pwd)
},
Expand Down
24 changes: 8 additions & 16 deletions tabpy-server/tabpy_server/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,34 +98,26 @@ def _create_tornado_web_app(self):
# default to index.html
# (r"/", MainHandler),
(self.subdirectory + r'/query/([^/]+)', QueryPlaneHandler,
dict(tabpy_state=self.tabpy_state,
python_service=self.python_service)),
dict(app=self)),
(self.subdirectory + r'/status', StatusHandler,
dict(tabpy_state=self.tabpy_state,
python_service=self.python_service)),
dict(app=self)),
(self.subdirectory + r'/info', ServiceInfoHandler,
dict(tabpy_state=self.tabpy_state,
python_service=self.python_service)),
dict(app=self)),
(self.subdirectory + r'/endpoints', EndpointsHandler,
dict(tabpy_state=self.tabpy_state,
python_service=self.python_service)),
dict(app=self)),
(self.subdirectory + r'/endpoints/([^/]+)?', EndpointHandler,
dict(tabpy_state=self.tabpy_state,
python_service=self.python_service)),
dict(app=self)),
(self.subdirectory + r'/evaluate', EvaluationPlaneHandler,
dict(executor=executor,
tabpy_state=self.tabpy_state,
python_service=self.python_service)),
app=self)),
(self.subdirectory +
r'/configurations/endpoint_upload_destination',
UploadDestinationHandler,
dict(tabpy_state=self.tabpy_state,
python_service=self.python_service)),
dict(app=self)),
(self.subdirectory + r'/(.*)', tornado.web.StaticFileHandler,
dict(path=self.settings['static_path'],
default_filename="index.html",
tabpy_state=self.tabpy_state,
python_service=self.python_service)),
app=self)),
], debug=False, **self.settings)

return application
Expand Down
2 changes: 1 addition & 1 deletion tabpy-server/tabpy_server/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
from tabpy_server.handlers.status_handler import StatusHandler
from tabpy_server.handlers.upload_destination_handler\
import UploadDestinationHandler
from tabpy_server.handlers.util import handle_authentication
from tabpy_server.handlers.util import handle_basic_authentication
38 changes: 34 additions & 4 deletions tabpy-server/tabpy_server/handlers/base_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import simplejson
import logging

from tabpy_server.handlers.util import handle_basic_authentication

logger = logging.getLogger(__name__)
STAGING_THREAD = concurrent.futures.ThreadPoolExecutor(max_workers=3)
Expand All @@ -11,12 +12,13 @@
class BaseHandler(tornado.web.RequestHandler):
KEYS_TO_SANITIZE = ("api key", "api_key", "admin key", "admin_key")

def initialize(self, tabpy_state, python_service):
self.tabpy_state = tabpy_state
def initialize(self, app):
self.tabpy_state = app.tabpy_state
# set content type to application/json
self.set_header("Content-Type", "application/json")
self.port = self.settings['port']
self.python_service = python_service
self.python_service = app.python_service
self.credentials = app.credentials

def error_out(self, code, log_message, info=None):
self.set_status(code)
Expand Down Expand Up @@ -58,4 +60,32 @@ def _add_CORS_header(self):
def _sanitize_request_data(self, data, keys=KEYS_TO_SANITIZE):
"""Remove keys so that we can log safely"""
for key in keys:
data.pop(key, None)
data.pop(key, None)


def should_fail_with_not_authorized(self):
'''
Checks if authentication is required:
- if it is not returns false, None
- if it is required validates provided credentials
Returns
-------
bool
False if authentication is not required or is
required and validation for credentials passes.
True if validation for credentials failed.
'''
return handle_basic_authentication(self.request.headers,
"v1",
self.settings,
self.credentials)


def fail_with_not_authorized(self):
'''
Prepares server 401 response.
'''
logger.error('Failing with 401 for anothorized request')
self.set_status(401)
self.set_header('WWW-Authenticate', 'Basic realm="{}"'.format(self.tabpy_state.name))
18 changes: 16 additions & 2 deletions tabpy-server/tabpy_server/handlers/endpoint_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@


class EndpointHandler(ManagementHandler):
def initialize(self, tabpy_state, python_service):
super(EndpointHandler, self).initialize(tabpy_state, python_service)
def initialize(self, app):
super(EndpointHandler, self).initialize(app)


def get(self, endpoint_name):
if self.should_fail_with_not_authorized():
self.fail_with_not_authorized()
return

self._add_CORS_header()
if not endpoint_name:
self.write(simplejson.dumps(self.tabpy_state.get_endpoints()))
Expand All @@ -33,6 +38,10 @@ def get(self, endpoint_name):
@tornado.web.asynchronous
@gen.coroutine
def put(self, name):
if self.should_fail_with_not_authorized():
self.fail_with_not_authorized()
return

try:
if not self.request.body:
self.error_out(400, "Input body cannot be empty")
Expand Down Expand Up @@ -70,9 +79,14 @@ def put(self, name):
self.error_out(500, err_msg)
self.finish()


@tornado.web.asynchronous
@gen.coroutine
def delete(self, name):
if self.should_fail_with_not_authorized():
self.fail_with_not_authorized()
return

try:
endpoints = self.tabpy_state.get_endpoints(name)
if len(endpoints) == 0:
Expand Down
14 changes: 12 additions & 2 deletions tabpy-server/tabpy_server/handlers/endpoints_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,26 @@


class EndpointsHandler(ManagementHandler):
def initialize(self, tabpy_state, python_service):
super(EndpointsHandler, self).initialize(tabpy_state, python_service)
def initialize(self, app):
super(EndpointsHandler, self).initialize(app)


def get(self):
if self.should_fail_with_not_authorized():
self.fail_with_not_authorized()
return

self._add_CORS_header()
self.write(simplejson.dumps(self.tabpy_state.get_endpoints()))


@tornado.web.asynchronous
@gen.coroutine
def post(self):
if self.should_fail_with_not_authorized():
self.fail_with_not_authorized()
return

try:
if not self.request.body:
self.error_out(400, "Input body cannot be empty")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,18 @@ class EvaluationPlaneHandler(BaseHandler):
EvaluationPlaneHandler is responsible for running arbitrary python scripts.
'''

def initialize(self, executor, tabpy_state, python_service):
super(EvaluationPlaneHandler, self).initialize(tabpy_state, python_service)
def initialize(self, executor, app):
super(EvaluationPlaneHandler, self).initialize(app)
self.executor = executor


@tornado.web.asynchronous
@gen.coroutine
def post(self):
if self.should_fail_with_not_authorized():
self.fail_with_not_authorized()
return

self._add_CORS_header()
try:
body = simplejson.loads(self.request.body.decode('utf-8'))
Expand Down
4 changes: 2 additions & 2 deletions tabpy-server/tabpy_server/handlers/main_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@


class MainHandler(BaseHandler):
def initialize(self, tabpy_state, python_service):
super(MainHandler, self).initialize(tabpy_state, python_service)
def initialize(self, app):
super(MainHandler, self).initialize(app)

def get(self):
self._add_CORS_header()
Expand Down
4 changes: 2 additions & 2 deletions tabpy-server/tabpy_server/handlers/management_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ def copy_from_local(localpath, remotepath, is_dir=False):


class ManagementHandler(MainHandler):
def initialize(self, tabpy_state, python_service):
super(ManagementHandler, self).initialize(tabpy_state, python_service)
def initialize(self, app):
super(ManagementHandler, self).initialize(app)
self.port = self.settings['port']

def _get_protocol(self):
Expand Down
18 changes: 15 additions & 3 deletions tabpy-server/tabpy_server/handlers/query_plane_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ def _sanitize_request_data(data):


class QueryPlaneHandler(BaseHandler):
def initialize(self, tabpy_state, python_service):
super(QueryPlaneHandler, self).initialize(tabpy_state, python_service)
def initialize(self, app):
super(QueryPlaneHandler, self).initialize(app)

def _query(self, po_name, data, uid, qry):
"""
Expand Down Expand Up @@ -78,12 +78,15 @@ def _query(self, po_name, data, uid, qry):
# don't check API key (client does not send or receive data for OPTIONS,
# it just allows the client to subsequently make a POST request)
def options(self, pred_name):
if self.should_fail_with_not_authorized():
self.fail_with_not_authorized()
return

# add CORS headers if TabPy has a cors_origin specified
self._add_CORS_header()
self.write({})

def _handle_result(self, po_name, data, qry, uid):

(response_type, response, gls_time) = \
self._query(po_name, data, uid, qry)

Expand Down Expand Up @@ -192,6 +195,10 @@ def _get_actual_model(self, endpoint_name):

@tornado.web.asynchronous
def get(self, endpoint_name):
if self.should_fail_with_not_authorized():
self.fail_with_not_authorized()
return

start = time.time()
if sys.version_info > (3, 0):
endpoint_name = urllib.parse.unquote(endpoint_name)
Expand All @@ -200,8 +207,13 @@ def get(self, endpoint_name):
logger.debug("GET /query/{}".format(endpoint_name))
self._process_query(endpoint_name, start)


@tornado.web.asynchronous
def post(self, endpoint_name):
if self.should_fail_with_not_authorized():
self.fail_with_not_authorized()
return

start = time.time()
if sys.version_info > (3, 0):
endpoint_name = urllib.parse.unquote(endpoint_name)
Expand Down
Loading

0 comments on commit 3f55658

Please sign in to comment.