From 06f69419f0f4a5777a76dc137a78897e59bb53b1 Mon Sep 17 00:00:00 2001 From: SrMouraSilva Date: Sun, 18 Mar 2018 14:15:17 -0300 Subject: [PATCH] #33 Defined username and password for webservice components. Improve auth tests --- CHANGES | 1 + setup.py | 1 + webservice/database/__init__.py | 0 webservice/database/database.py | 43 ++++++++++++++++ webservice/handler/auth_handler.py | 17 +++++-- webservice/handler/plugins_reload_handler.py | 3 -- webservice/properties.py | 17 +++++++ webservice/util/auth.py | 12 ++++- webservice/util/handler_utils.py | 3 +- wstest/handler/auth_handler_test.py | 53 ++++++++++++++++++++ wstest/rest_facade.py | 11 +++- 11 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 webservice/database/__init__.py create mode 100644 webservice/database/database.py diff --git a/CHANGES b/CHANGES index 23f2672..862dc4c 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,7 @@ Version 0.4.0 - released 05/dd/18 - Breaking change! - See http://pedalpi.github.io/WebService/ for details + - Components that uses WebService can be use WSParameters.COMPONENT_USERNAME and WSParameters.COMPONENT_PASSWORD for auth - Improve errors when is not passed all parameters diff --git a/setup.py b/setup.py index 6072ea6..3d9665c 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ def readme(): packages=[ 'webservice', + 'webservice/database', 'webservice/handler', 'webservice/search', 'webservice/util', diff --git a/webservice/database/__init__.py b/webservice/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/webservice/database/database.py b/webservice/database/database.py new file mode 100644 index 0000000..d04db3c --- /dev/null +++ b/webservice/database/database.py @@ -0,0 +1,43 @@ +from webservice.properties import WSProperties + + +class UsersDatabase(object): + """ + Management of username + """ + + def __init__(self, controller): + """ + :param ComponentDataController controller: + """ + self._controller = controller + + self._users = self._read_data(controller) + + def _read_data(self, controller): + if WSProperties.USER not in controller[WSProperties.DATA_KEY]: + self._save_users({'pedal pi': 'pedal pi'}) + + return controller[WSProperties.DATA_KEY][WSProperties.USER] + + def _save_users(self, users): + data = self._controller[WSProperties.DATA_KEY] + data[WSProperties.USER] = users + self._controller[WSProperties.DATA_KEY] = data + + def auth(self, username, password): + return username in self._users \ + and self._users[username] == password + + def update(self, username, new_password): + """ + Updates the user password + + :param string username: + :param string new_password: + """ + if username in self._users: + raise KeyError('Username "{}" not registered'.format(username)) + + self._users[username] = new_password + self._save_users(self._users) diff --git a/webservice/handler/auth_handler.py b/webservice/handler/auth_handler.py index 742773c..e7b2683 100644 --- a/webservice/handler/auth_handler.py +++ b/webservice/handler/auth_handler.py @@ -15,28 +15,35 @@ import datetime import jwt +from application.controller.component_data_controller import ComponentDataController +from webservice.database.database import UsersDatabase from webservice.handler.abstract_request_handler import AbstractRequestHandler +from webservice.properties import WSProperties from webservice.util.auth import JWTAuth from webservice.util.auth import RequiresAuthMixing +from webservice.util.handler_utils import exception class AuthHandler(RequiresAuthMixing, AbstractRequestHandler): """ Based on https://github.com/paulorodriguesxv/tornado-json-web-token-jwt - - Handle to auth method. - This method aim to provide a new authorization token - There is a fake payload (for tutorial purpose) """ + database = None def initialize(self, app, webservice): super(AuthHandler, self).initialize(app, webservice) + self.database = UsersDatabase(app.controller(ComponentDataController)) + @exception(Exception, 500) def post(self): """ :return The generated token """ - if self.request_data != {"username": "pedal pi", "password": "pedal pi"}: + username = self.request_data["username"] + password = self.request_data["password"] + + if not self.database.auth(username, password) \ + and not WSProperties.auth_client_component(username, password): self.unauthorized("Invalid username or password") return diff --git a/webservice/handler/plugins_reload_handler.py b/webservice/handler/plugins_reload_handler.py index 468946a..fddda9f 100644 --- a/webservice/handler/plugins_reload_handler.py +++ b/webservice/handler/plugins_reload_handler.py @@ -19,9 +19,6 @@ class PluginsReloadHandler(RequiresAuthMixing, AbstractRequestHandler): - def prepare(self): - self.auth() - def prepare(self): self.auth() diff --git a/webservice/properties.py b/webservice/properties.py index becea5b..f2c60e2 100644 --- a/webservice/properties.py +++ b/webservice/properties.py @@ -12,9 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. +from webservice.util.auth import generate_random_string + class WSProperties(object): DATA_KEY = 'WebService' DEVICE_NAME = 'device_name' + USER = 'user' + + """ Default username for component clients""" + COMPONENT_USERNAME = generate_random_string(16) + """ Default password for component clients""" + COMPONENT_PASSWORD = generate_random_string(16) + @staticmethod + def auth_client_component(username, password): + """ + :param string username: + :param string password: + :return: + """ + return username == WSProperties.COMPONENT_USERNAME \ + and password == WSProperties.COMPONENT_PASSWORD diff --git a/webservice/util/auth.py b/webservice/util/auth.py index 5f49230..44ef909 100644 --- a/webservice/util/auth.py +++ b/webservice/util/auth.py @@ -50,11 +50,21 @@ def unauthorized(self, message): self.send(401, {"error": message}) +def generate_random_string(size): + """ + Generate a random string with size specified + Like python 3.6 secrets + + :param int size: Size of the random string that will be generated + """ + return binascii.hexlify(os.urandom(size)).decode('ascii') + + class JWTAuth(object): AUTHORIZATION_HEADER = 'Authorization' AUTHORIZATION_METHOD = 'bearer' - SECRET_KEY = binascii.hexlify(os.urandom(16)).decode('ascii') + SECRET_KEY = generate_random_string(16) @staticmethod def auth_token(token): diff --git a/webservice/util/handler_utils.py b/webservice/util/handler_utils.py index e25cc9a..999d4e3 100644 --- a/webservice/util/handler_utils.py +++ b/webservice/util/handler_utils.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import traceback import sys +import traceback from functools import wraps + class integer(object): """ Convert the informed args to integer diff --git a/wstest/handler/auth_handler_test.py b/wstest/handler/auth_handler_test.py index 7237aa0..e080875 100644 --- a/wstest/handler/auth_handler_test.py +++ b/wstest/handler/auth_handler_test.py @@ -12,6 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +import unittest + +import requests +from webservice.properties import WSProperties from wstest.handler.handler_test import Test @@ -22,7 +27,55 @@ def test_post_correct_username(self): self.assertEqual(Test.SUCCESS, response.status_code) + @unittest.skipIf('Pycharm' in os.environ['PWD'], 'Ignore if the test are running in Pycharm') + def test_post_component_username(self): + response = self.rest.auth(WSProperties.COMPONENT_USERNAME, WSProperties.COMPONENT_PASSWORD) + + self.assertEqual(Test.SUCCESS, response.status_code) + def test_post_wrong_username(self): + response = self.rest.auth(username="wrong username") + + self.assertEqual(Test.AUTH_ERROR, response.status_code) + + def test_post_invalid_data(self): + response = self.rest.post('auth', {}) + + self.assertEqual(Test.SERVER_ERROR, response.status_code) + + def test_post_wrong_password(self): response = self.rest.auth(password="wrong password") self.assertEqual(Test.AUTH_ERROR, response.status_code) + + def test_auth_missing_authorization(self): + response = self.custom_get('banks', authorization=None) + self.assertEqual(Test.AUTH_ERROR, response.status_code) + + def test_auth_invalid_token(self): + response = self.custom_get('banks', 'bearer invalid-token') + self.assertEqual(Test.AUTH_ERROR, response.status_code) + + def test_auth_missing_token(self): + response = self.custom_get('banks', 'bearer') + self.assertEqual(Test.AUTH_ERROR, response.status_code) + + def test_auth_invalid_header_authorization(self): + response = self.custom_get('banks', 'bearer-wrong {}'.format(self.rest.token)) + self.assertEqual(Test.AUTH_ERROR, response.status_code) + + def test_ignore_method(self): + response = self.rest.options('banks') + self.assertEqual(204, response.status_code) + + def custom_get(self, url, authorization=None): + headers = {'content-type': 'application/json'} + + if authorization: + headers['Authorization'] = authorization + + print('[GET]', self.rest.address + url) + return requests.get( + self.rest.address + url, + headers=headers + ) \ No newline at end of file diff --git a/wstest/rest_facade.py b/wstest/rest_facade.py index ffb1ca8..161e606 100644 --- a/wstest/rest_facade.py +++ b/wstest/rest_facade.py @@ -59,11 +59,18 @@ def delete(self, url): headers=self.headers() ) + def options(self, url): + print('[OPTIONS]', self.address + url) + return requests.options( + self.address + url, + headers=self.headers() + ) + # ********************** # Auth # ********************** - def auth(self, password="pedal pi"): - return self.post('auth', {"username": "pedal pi", "password": password}) + def auth(self, username="pedal pi", password="pedal pi"): + return self.post('auth', {"username": username, "password": password}) # ********************** # Banks