From 61a91392cf9ba00f78752b4fb99f144a5fe800d8 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Tue, 23 Jan 2018 22:13:57 +0000 Subject: [PATCH] mgr/dashboard_v2: Auth API Signed-off-by: Ricardo Marques --- src/pybind/mgr/dashboard_v2/auth.py | 72 ++++++++++++++++ .../mgr/dashboard_v2/ceph_module_mock.py | 11 ++- src/pybind/mgr/dashboard_v2/module.py | 18 +++- .../mgr/dashboard_v2/tests/test_auth.py | 86 +++++++++++++++++++ .../mgr/dashboard_v2/tests/test_ping.py | 4 +- 5 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 src/pybind/mgr/dashboard_v2/auth.py create mode 100644 src/pybind/mgr/dashboard_v2/tests/test_auth.py diff --git a/src/pybind/mgr/dashboard_v2/auth.py b/src/pybind/mgr/dashboard_v2/auth.py new file mode 100644 index 00000000000..911b10212b0 --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/auth.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +import bcrypt +import cherrypy +import time +from cherrypy import tools + +class Auth(object): + """ + Provide login and logout actions. + + Supported config-keys: + + | KEY | DEFAULT | DESCR | + -------------------------------------------------------------------------------------------- + | username | None | Username | + | password | None | Password encrypted using bcrypt | + | session-expire | 1200 | Session will expire after seconds without activity | + """ + + SESSION_KEY = '_username' + SESSION_KEY_TS = '_username_ts' + + DEFAULT_SESSION_EXPIRE = 1200 + + def __init__(self, module): + self.module = module + self.log = self.module.log + + @cherrypy.expose + @cherrypy.tools.allow(methods=['POST']) + @tools.json_out() + def login(self, username=None, password=None): + now = int(time.time()) + config_username = self.module.get_localized_config('username', None) + config_password = self.module.get_localized_config('password', None) + hash_password = bcrypt.hashpw(password.encode('utf8'), config_password) + if username == config_username and hash_password == config_password: + cherrypy.session.regenerate() + cherrypy.session[Auth.SESSION_KEY] = username + cherrypy.session[Auth.SESSION_KEY_TS] = now + self.log.debug("Login successful") + return {'username': username} + else: + cherrypy.response.status = 403 + self.log.debug("Login fail") + return {'detail': 'Invalid credentials'} + + @cherrypy.expose + @cherrypy.tools.allow(methods=['POST']) + def logout(self): + self.log.debug("Logout successful") + cherrypy.session[Auth.SESSION_KEY] = None + cherrypy.session[Auth.SESSION_KEY_TS] = None + + def check_auth(self): + username = cherrypy.session.get(Auth.SESSION_KEY) + if not username: + self.log.debug("Unauthorized") + raise cherrypy.HTTPError(401, + 'You are not authorized to access that resource') + now = int(time.time()) + expires = int(self.module.get_localized_config('session-expire', Auth.DEFAULT_SESSION_EXPIRE)) + if expires > 0: + username_ts = cherrypy.session.get(Auth.SESSION_KEY_TS, None) + if username_ts and username_ts < now - expires: + cherrypy.session[Auth.SESSION_KEY] = None + cherrypy.session[Auth.SESSION_KEY_TS] = None + self.log.debug("Session expired.") + raise cherrypy.HTTPError(401, + 'Session expired. You are not authorized to access that resource') + cherrypy.session[Auth.SESSION_KEY_TS] = now diff --git a/src/pybind/mgr/dashboard_v2/ceph_module_mock.py b/src/pybind/mgr/dashboard_v2/ceph_module_mock.py index c7468d7b26b..c8f866f61b7 100644 --- a/src/pybind/mgr/dashboard_v2/ceph_module_mock.py +++ b/src/pybind/mgr/dashboard_v2/ceph_module_mock.py @@ -16,11 +16,20 @@ class BaseMgrStandbyModule(object): class BaseMgrModule(object): def __init__(self, py_modules_ptr, this_ptr): - pass + self.config_key_map = {} def _ceph_get_version(self): return "ceph-13.0.0" + def _ceph_get_mgr_id(self): + return "x" + + def _ceph_set_config(self, key, value): + self.config_key_map[key] = value + + def _ceph_get_config(self, key): + return self.config_key_map.get(key, None) + def _ceph_log(self, *args): pass diff --git a/src/pybind/mgr/dashboard_v2/module.py b/src/pybind/mgr/dashboard_v2/module.py index f989f5feb4a..151930820c6 100644 --- a/src/pybind/mgr/dashboard_v2/module.py +++ b/src/pybind/mgr/dashboard_v2/module.py @@ -9,6 +9,7 @@ import os import cherrypy from cherrypy import tools +from auth import Auth from mgr_module import MgrModule # cherrypy likes to sys.exit on error. don't let it take us down too! @@ -52,7 +53,22 @@ class Module(MgrModule): cherrypy.config.update({'server.socket_host': server_addr, 'server.socket_port': int(server_port), }) - cherrypy.tree.mount(Module.HelloWorld(self), "/") + auth = Auth(self) + cherrypy.tools.autenticate = cherrypy.Tool('before_handler', auth.check_auth) + noauth_required_config = { + '/': { + 'tools.autenticate.on': False, + 'tools.sessions.on': True + } + } + auth_required_config = { + '/': { + 'tools.autenticate.on': True, + 'tools.sessions.on': True + } + } + cherrypy.tree.mount(auth, "/api/auth", config=noauth_required_config) + cherrypy.tree.mount(Module.HelloWorld(self), "/api/hello", config=auth_required_config) cherrypy.engine.start() self.log.info("Waiting for engine...") cherrypy.engine.block() diff --git a/src/pybind/mgr/dashboard_v2/tests/test_auth.py b/src/pybind/mgr/dashboard_v2/tests/test_auth.py new file mode 100644 index 00000000000..b03dfc17f0e --- /dev/null +++ b/src/pybind/mgr/dashboard_v2/tests/test_auth.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +import time +from cherrypy.lib.sessions import RamSession +from cherrypy.test import helper +from mock import patch + +from ..auth import Auth +from ..module import Module, cherrypy + +class Ping(object): + @cherrypy.expose + @cherrypy.tools.allow(methods=['POST']) + def ping(self): + pass + +class AuthTest(helper.CPWebCase): + @staticmethod + def setup_server(): + module = Module('dashboard', None, None) + auth = Auth(module) + cherrypy.tools.autenticate = cherrypy.Tool('before_handler', auth.check_auth) + cherrypy.tree.mount(auth, "/api/auth") + cherrypy.tree.mount(Ping(), "/api/test", + config={'/': {'tools.autenticate.on': True}}) + module.set_localized_config('session-expire','2') + module.set_localized_config('username','admin') + module.set_localized_config('password', + '$2b$12$KunrLI/uq7pqjvwUcAhIZu.B1dAGZ3liB8KFIJUOqZC.5/bEEmBQG') + + def test_login_valid(self): + sess_mock = RamSession() + with patch('cherrypy.session', sess_mock, create=True): + self.getPage("/api/auth/login", + body="username=admin&password=admin", + method='POST') + self.assertStatus('200 OK') + self.assertBody('{"username": "admin"}') + self.assertEquals(sess_mock.get(Auth.SESSION_KEY), 'admin') + + def test_login_invalid(self): + sess_mock = RamSession() + with patch('cherrypy.session', sess_mock, create=True): + self.getPage("/api/auth/login", + body="username=admin&password=invalid", + method='POST') + self.assertStatus('403 Forbidden') + self.assertBody('{"detail": "Invalid credentials"}') + self.assertEquals(sess_mock.get(Auth.SESSION_KEY), None) + + def test_logout(self): + sess_mock = RamSession() + with patch('cherrypy.session', sess_mock, create=True): + self.getPage("/api/auth/login", + body="username=admin&password=admin", + method='POST') + self.assertEquals(sess_mock.get(Auth.SESSION_KEY), 'admin') + self.getPage("/api/auth/logout", method='POST') + self.assertStatus('200 OK') + self.assertBody('') + self.assertEquals(sess_mock.get(Auth.SESSION_KEY), None) + + def test_session_expire(self): + sess_mock = RamSession() + with patch('cherrypy.session', sess_mock, create=True): + self.getPage("/api/auth/login", + body="username=admin&password=admin", + method='POST') + self.assertStatus('200 OK') + self.assertEquals(sess_mock.get(Auth.SESSION_KEY), 'admin') + self.getPage("/api/test/ping", method='POST') + self.assertStatus('200 OK') + self.assertEquals(sess_mock.get(Auth.SESSION_KEY), 'admin') + time.sleep(3) + self.getPage("/api/test/ping", method='POST') + self.assertStatus('401 Unauthorized') + self.assertEquals(sess_mock.get(Auth.SESSION_KEY), None) + + def test_unauthorized(self): + sess_mock = RamSession() + with patch('cherrypy.session', sess_mock, create=True): + self.getPage("/api/test/ping", method='POST') + self.assertStatus('401 Unauthorized') + self.assertEquals(sess_mock.get(Auth.SESSION_KEY), None) diff --git a/src/pybind/mgr/dashboard_v2/tests/test_ping.py b/src/pybind/mgr/dashboard_v2/tests/test_ping.py index 385e9f9b99b..cca2241ba97 100644 --- a/src/pybind/mgr/dashboard_v2/tests/test_ping.py +++ b/src/pybind/mgr/dashboard_v2/tests/test_ping.py @@ -10,10 +10,10 @@ class SimpleCPTest(helper.CPWebCase): @staticmethod def setup_server(): module = Module('attic', None, None) - cherrypy.tree.mount(Module.HelloWorld(module)) + cherrypy.tree.mount(Module.HelloWorld(module), "/api/hello") def test_ping(self): - self.getPage("/ping") + self.getPage("/api/hello/ping") self.assertStatus('200 OK') self.assertBody('"pong"') -- 2.39.5