]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: backend: JWT based authentication
authorRicardo Dias <rdias@suse.com>
Tue, 3 Jul 2018 10:32:54 +0000 (11:32 +0100)
committerRicardo Dias <rdias@suse.com>
Mon, 29 Oct 2018 15:47:14 +0000 (15:47 +0000)
Signed-off-by: Ricardo Dias <rdias@suse.com>
18 files changed:
ceph.spec.in
debian/control
qa/tasks/mgr/dashboard/helper.py
qa/tasks/mgr/dashboard/test_auth.py
qa/tasks/mgr/dashboard/test_role.py
qa/tasks/mgr/dashboard/test_user.py
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/controllers/auth.py
src/pybind/mgr/dashboard/controllers/docs.py
src/pybind/mgr/dashboard/controllers/role.py
src/pybind/mgr/dashboard/controllers/user.py
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/requirements.txt
src/pybind/mgr/dashboard/services/access_control.py
src/pybind/mgr/dashboard/services/auth.py
src/pybind/mgr/dashboard/tests/helper.py
src/pybind/mgr/dashboard/tests/test_access_control.py
src/pybind/mgr/dashboard/tools.py

index 70e8612de5736ff262b37112e6d71a1d81f91109..a0e47189219e0e873726e09a9ef90c0e65142644 100644 (file)
@@ -260,12 +260,14 @@ BuildRequires:    python3-Cython
 %if 0%{with make_check}
 %if 0%{?fedora} || 0%{?rhel}
 BuildRequires: python%{_python_buildid}-cherrypy
+BuildRequires: python%{_python_buildid}-jwt
 BuildRequires: python%{_python_buildid}-routes
 BuildRequires: python%{_python_buildid}-werkzeug
 BuildRequires: python%{_python_buildid}-bcrypt
 %endif
 %if 0%{?suse_version}
 BuildRequires: python%{_python_buildid}-CherryPy
+BuildRequires: python%{_python_buildid}-PyJWT
 BuildRequires: python%{_python_buildid}-Routes
 BuildRequires: python%{_python_buildid}-Werkzeug
 BuildRequires: python%{_python_buildid}-numpy-devel
@@ -420,6 +422,7 @@ Requires:       python%{_python_buildid}-pecan
 Requires:       python%{_python_buildid}-six
 %if 0%{?fedora} || 0%{?rhel}
 Requires:       python%{_python_buildid}-cherrypy
+Requires:       python%{_python_buildid}-jwt
 Requires:       python%{_python_buildid}-jinja2
 Requires:       python%{_python_buildid}-routes
 Requires:       python%{_python_buildid}-werkzeug
@@ -428,6 +431,7 @@ Requires:   python%{_python_buildid}-bcrypt
 %endif
 %if 0%{?suse_version}
 Requires:       python%{_python_buildid}-CherryPy
+Requires:       python%{_python_buildid}-PyJWT
 Requires:       python%{_python_buildid}-Routes
 Requires:       python%{_python_buildid}-Jinja2
 Requires:       python%{_python_buildid}-Werkzeug
index e4d0d9ad11ecef46ddb00cdc9a2d9459aaa0aa96..f79a3294d2760bb93e2ce55d19d337a1482823d9 100644 (file)
@@ -54,6 +54,7 @@ Build-Depends: bc,
                python (>= 2.7),
                python-all-dev,
                python-cherrypy3,
+               python-jwt,
                python-nose,
                python-pecan,
                python-bcrypt,
@@ -179,6 +180,7 @@ Package: ceph-mgr
 Architecture: linux-any
 Depends: ceph-base (= ${binary:Version}),
          python-cherrypy3,
+         python-jwt,
          python-jinja2,
          python-openssl,
          python-pecan,
index 610d07a7342d268254dfd5937ee2f840c0dec0a8..7fef4b903ae2997c6d2c84be82aabc521bb54ed1 100644 (file)
@@ -26,6 +26,7 @@ class DashboardTestCase(MgrTestCase):
     CEPHFS = False
 
     _session = None  # type: requests.sessions.Session
+    _token = None
     _resp = None  # type: requests.models.Response
     _loggedin = False
     _base_uri = None
@@ -71,12 +72,14 @@ class DashboardTestCase(MgrTestCase):
         if cls._loggedin:
             cls.logout()
         cls._post('/api/auth', {'username': username, 'password': password})
+        cls._token = cls.jsonBody()['token']
         cls._loggedin = True
 
     @classmethod
     def logout(cls):
         if cls._loggedin:
             cls._delete('/api/auth')
+            cls._token = None
             cls._loggedin = False
 
     @classmethod
@@ -101,6 +104,10 @@ class DashboardTestCase(MgrTestCase):
             return execute
         return wrapper
 
+    @classmethod
+    def set_jwt_token(cls, token):
+        cls._token = token
+
     @classmethod
     def setUpClass(cls):
         super(DashboardTestCase, cls).setUpClass()
@@ -134,6 +141,7 @@ class DashboardTestCase(MgrTestCase):
             # wait for mds restart to complete...
             cls.fs.wait_for_daemons()
 
+        cls._token = None
         cls._session = requests.Session()
         cls._resp = None
 
@@ -155,17 +163,24 @@ class DashboardTestCase(MgrTestCase):
     def _request(cls, url, method, data=None, params=None):
         url = "{}{}".format(cls._base_uri, url)
         log.info("request %s to %s", method, url)
+        headers = {
+            'Content-Type': 'application/json'
+        }
+        if cls._token:
+            headers['Authorization'] = "Bearer {}".format(cls._token)
+
         if method == 'GET':
-            cls._resp = cls._session.get(url, params=params, verify=False)
+            cls._resp = cls._session.get(url, params=params, verify=False,
+                                         headers=headers)
         elif method == 'POST':
             cls._resp = cls._session.post(url, json=data, params=params,
-                                          verify=False)
+                                          verify=False, headers=headers)
         elif method == 'DELETE':
             cls._resp = cls._session.delete(url, json=data, params=params,
-                                            verify=False)
+                                            verify=False, headers=headers)
         elif method == 'PUT':
             cls._resp = cls._session.put(url, json=data, params=params,
-                                         verify=False)
+                                         verify=False, headers=headers)
         else:
             assert False
         try:
index 0921b7d9b4f6f5bdb5872ce8fe16246a0ede1a6c..0b6ab0ab521c4df9577daa67df84d71f7fd7fdd4 100644 (file)
@@ -4,6 +4,8 @@ from __future__ import absolute_import
 
 import time
 
+import jwt
+
 from .helper import DashboardTestCase
 
 
@@ -14,58 +16,31 @@ class AuthTest(DashboardTestCase):
     def setUp(self):
         self.reset_session()
 
-    def test_a_set_login_credentials(self):
-        self.create_user('admin2', 'admin2', ['administrator'])
-        self._post("/api/auth", {'username': 'admin2', 'password': 'admin2'})
-        self.assertStatus(201)
-        # self.assertJsonBody({"username": "admin2"})
-        data = self.jsonBody()
-        self.assertIn('username', data)
-        self.assertEqual(data['username'], "admin2")
-        self.assertIn('permissions', data)
-        for scope, perms in data['permissions'].items():
+    def _validate_jwt_token(self, token, username, permissions):
+        payload = jwt.decode(token, verify=False)
+        self.assertIn('username', payload)
+        self.assertEqual(payload['username'], username)
+
+        for scope, perms in permissions.items():
             self.assertIsNotNone(scope)
             self.assertIn('read', perms)
             self.assertIn('update', perms)
             self.assertIn('create', perms)
             self.assertIn('delete', perms)
+
+    def test_a_set_login_credentials(self):
+        self.create_user('admin2', 'admin2', ['administrator'])
+        self._post("/api/auth", {'username': 'admin2', 'password': 'admin2'})
+        self.assertStatus(201)
+        data = self.jsonBody()
+        self._validate_jwt_token(data['token'], "admin2", data['permissions'])
         self.delete_user('admin2')
 
     def test_login_valid(self):
         self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
         self.assertStatus(201)
         data = self.jsonBody()
-        self.assertIn('username', data)
-        self.assertEqual(data['username'], "admin")
-        self.assertIn('permissions', data)
-        for scope, perms in data['permissions'].items():
-            self.assertIsNotNone(scope)
-            self.assertIn('read', perms)
-            self.assertIn('update', perms)
-            self.assertIn('create', perms)
-            self.assertIn('delete', perms)
-
-    def test_login_stay_signed_in(self):
-        self._post("/api/auth", {
-            'username': 'admin',
-            'password': 'admin',
-            'stay_signed_in': True})
-        self.assertStatus(201)
-        self.assertIn('session_id', self.cookies())
-        for cookie in self.cookies():
-            if cookie.name == 'session_id':
-                self.assertIsNotNone(cookie.expires)
-
-    def test_login_not_stay_signed_in(self):
-        self._post("/api/auth", {
-            'username': 'admin',
-            'password': 'admin',
-            'stay_signed_in': False})
-        self.assertStatus(201)
-        self.assertIn('session_id', self.cookies())
-        for cookie in self.cookies():
-            if cookie.name == 'session_id':
-                self.assertIsNone(cookie.expires)
+        self._validate_jwt_token(data['token'], "admin", data['permissions'])
 
     def test_login_invalid(self):
         self._post("/api/auth", {'username': 'admin', 'password': 'inval'})
@@ -89,23 +64,72 @@ class AuthTest(DashboardTestCase):
 
     def test_logout(self):
         self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
+        self.assertStatus(201)
+        data = self.jsonBody()
+        self._validate_jwt_token(data['token'], "admin", data['permissions'])
+        self.set_jwt_token(data['token'])
         self._delete("/api/auth")
         self.assertStatus(204)
-        self.assertBody('')
         self._get("/api/host")
         self.assertStatus(401)
+        self.set_jwt_token(None)
 
-    def test_session_expire(self):
-        self._ceph_cmd(['dashboard', 'set-session-expire', '2'])
+    def test_token_ttl(self):
+        self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '5'])
         self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
         self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
         self._get("/api/host")
         self.assertStatus(200)
-        time.sleep(3)
+        time.sleep(6)
         self._get("/api/host")
         self.assertStatus(401)
-        self._ceph_cmd(['dashboard', 'set-session-expire', '1200'])
+        self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '28800'])
+        self.set_jwt_token(None)
+
+    def test_remove_from_blacklist(self):
+        self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '5'])
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        # the following call adds the token to the blacklist
+        self._delete("/api/auth")
+        self.assertStatus(204)
+        self._get("/api/host")
+        self.assertStatus(401)
+        time.sleep(6)
+        self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '28800'])
+        self.set_jwt_token(None)
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        # the following call removes expired tokens from the blacklist
+        self._delete("/api/auth")
+        self.assertStatus(204)
 
     def test_unauthorized(self):
         self._get("/api/host")
         self.assertStatus(401)
+
+    def test_invalidate_token_by_admin(self):
+        self._get("/api/host")
+        self.assertStatus(401)
+        self.create_user('user', 'user', ['read-only'])
+        time.sleep(1)
+        self._post("/api/auth", {'username': 'user', 'password': 'user'})
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        self._get("/api/host")
+        self.assertStatus(200)
+        time.sleep(1)
+        self._ceph_cmd(['dashboard', 'ac-user-set-password', 'user', 'user2'])
+        time.sleep(1)
+        self._get("/api/host")
+        self.assertStatus(401)
+        self.set_jwt_token(None)
+        self._post("/api/auth", {'username': 'user', 'password': 'user2'})
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        self._get("/api/host")
+        self.assertStatus(200)
+        self.delete_user("user")
index 120279def3a28201d55c7998a3fd8a949275e600..6b0e35b244134e45bb5d46065ded20997fa633b9 100644 (file)
@@ -110,11 +110,12 @@ class RoleTest(DashboardTestCase):
                          component='role')
 
     def test_delete_role_associated_with_user(self):
+        self.create_user("user", "user", ['read-only'])
         self._create_role(name='role1',
                           description='Description 1',
                           scopes_permissions={'user': ['create', 'read', 'update', 'delete']})
         self.assertStatus(201)
-        self._put('/api/user/admin', {'roles': ['role1']})
+        self._put('/api/user/user', {'roles': ['role1']})
         self.assertStatus(200)
 
         self._delete('/api/role/role1')
@@ -122,10 +123,11 @@ class RoleTest(DashboardTestCase):
         self.assertError(code='role_is_associated_with_user',
                          component='role')
 
-        self._put('/api/user/admin', {'roles': ['administrator']})
+        self._put('/api/user/user', {'roles': ['administrator']})
         self.assertStatus(200)
         self._delete('/api/role/role1')
         self.assertStatus(204)
+        self.delete_user("user")
 
     def test_update_role_does_not_exist(self):
         self._put('/api/role/role2', {})
index 57521da7de4463564cc4e7345b3fd4fa7b9232b3..7af3442d422f0667863923c1df386a2eee88d5c7 100644 (file)
@@ -29,6 +29,7 @@ class UserTest(DashboardTestCase):
                           email='my@email.com',
                           roles=['administrator'])
         self.assertStatus(201)
+        user = self.jsonBody()
 
         self._get('/api/user/user1')
         self.assertStatus(200)
@@ -36,7 +37,8 @@ class UserTest(DashboardTestCase):
             'username': 'user1',
             'name': 'My Name',
             'email': 'my@email.com',
-            'roles': ['administrator']
+            'roles': ['administrator'],
+            'lastUpdate': user['lastUpdate']
         })
 
         self._put('/api/user/user1', {
@@ -45,11 +47,13 @@ class UserTest(DashboardTestCase):
             'roles': ['block-manager'],
         })
         self.assertStatus(200)
+        user = self.jsonBody()
         self.assertJsonBody({
             'username': 'user1',
             'name': 'My New Name',
             'email': 'mynew@email.com',
-            'roles': ['block-manager']
+            'roles': ['block-manager'],
+            'lastUpdate': user['lastUpdate']
         })
 
         self._delete('/api/user/user1')
@@ -58,11 +62,15 @@ class UserTest(DashboardTestCase):
     def test_list_users(self):
         self._get('/api/user')
         self.assertStatus(200)
+        user = self.jsonBody()
+        self.assertEqual(len(user), 1)
+        user = user[0]
         self.assertJsonBody([{
             'username': 'admin',
             'name': None,
             'email': None,
-            'roles': ['administrator']
+            'roles': ['administrator'],
+            'lastUpdate': user['lastUpdate']
         }])
 
     def test_create_user_already_exists(self):
index ed9c7c032f1091922c7d0c2c961ba31c0be5bd5f..5426ee47067a27b00d271beeeae8f81a4c67d57d 100644 (file)
@@ -22,11 +22,11 @@ import cherrypy
 from .. import logger
 from ..security import Scope, Permission
 from ..settings import Settings
-from ..tools import Session, wraps, getargspec, TaskManager
+from ..tools import wraps, getargspec, TaskManager
 from ..exceptions import ViewCacheNoDataException, DashboardException, \
                          ScopeNotValid, PermissionNotValid
 from ..services.exception import serialize_dashboard_exception
-from ..services.auth import AuthManager
+from ..services.auth import AuthManager, JwtManager
 
 
 class Controller(object):
@@ -57,9 +57,6 @@ class Controller(object):
         cls._security_scope = self.security_scope
 
         config = {
-            'tools.sessions.on': True,
-            'tools.sessions.name': Session.NAME,
-            'tools.session_expire_at_browser_close.on': True,
             'tools.dashboard_exception_handler.on': True,
             'tools.authenticate.on': self.secure,
         }
@@ -482,7 +479,7 @@ class BaseController(object):
         if scope is None:
             raise Exception("Cannot verify permissions without scope security"
                             " defined")
-        username = cherrypy.session.get(Session.USERNAME)
+        username = JwtManager.LOCAL_USER.username
         return AuthManager.authorize(username, scope, permissions)
 
     @classmethod
index 22a1675422564b436ea6e1fa8b7fb26e462c59eb..9c0effd48f7b5b897a67248647c80b1aa3464fb6 100644 (file)
@@ -1,40 +1,30 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
-import time
-
 import cherrypy
 
 from . import ApiController, RESTController
 from .. import logger
 from ..exceptions import DashboardException
-from ..services.auth import AuthManager
-from ..tools import Session
+from ..services.auth import AuthManager, JwtManager
 
 
 @ApiController('/auth', secure=False)
 class Auth(RESTController):
     """
-    Provide login and logout actions.
-
-    Supported config-keys:
-
-      | KEY             | DEFAULT | DESCR                                     |
-      ------------------------------------------------------------------------|
-      | session-expire  | 1200    | Session will expire after <expires>       |
-      |                           | seconds without activity                  |
+    Provide authenticates and returns JWT token.
     """
 
-    def create(self, username, password, stay_signed_in=False):
-        now = time.time()
+    def create(self, username, password):
         user_perms = AuthManager.authenticate(username, password)
         if user_perms is not None:
-            cherrypy.session.regenerate()
-            cherrypy.session[Session.USERNAME] = username
-            cherrypy.session[Session.TS] = now
-            cherrypy.session[Session.EXPIRE_AT_BROWSER_CLOSE] = not stay_signed_in
             logger.debug('Login successful')
+            token = JwtManager.gen_token(username)
+            token = token.decode('utf-8')
+            logger.debug("JWT Token: %s", token)
+            cherrypy.response.headers['Authorization'] = "Bearer: {}".format(token)
             return {
+                'token': token,
                 'username': username,
                 'permissions': user_perms
             }
@@ -45,6 +35,5 @@ class Auth(RESTController):
                                  component='auth')
 
     def bulk_delete(self):
-        logger.debug('Logout successful')
-        cherrypy.session[Session.USERNAME] = None
-        cherrypy.session[Session.TS] = None
+        token = JwtManager.get_token_from_header()
+        JwtManager.blacklist_token(token)
index 2998c0cbc63bce532f7ba291b60ad108effbd4ec..782a14f9abd9c21c6aa05ca025918dc55b7fffbf 100644 (file)
@@ -9,7 +9,7 @@ from . import Controller, BaseController, Endpoint, ENDPOINT_MAP
 from .. import logger, mgr
 
 
-@Controller('/docs')
+@Controller('/docs', secure=False)
 class Docs(BaseController):
 
     @classmethod
@@ -62,15 +62,10 @@ class Docs(BaseController):
             return None
 
         return {
-            'in': "body",
-            'name': "body",
-            'description': "",
-            'required': True,
-            'schema': {
-                'type': "object",
-                'required': required,
-                'properties': props
-            }
+            'title': '',
+            'type': "object",
+            'required': required,
+            'properties': props
         }
 
     @classmethod
@@ -111,7 +106,9 @@ class Docs(BaseController):
         res = {
             'name': param['name'],
             'in': ptype,
-            'type': cls._gen_type(param)
+            'schema': {
+                'type': cls._gen_type(param)
+            }
         }
         if param['required']:
             res['required'] = True
@@ -149,11 +146,6 @@ class Docs(BaseController):
                 params.extend([self._gen_param(p, 'query')
                                for p in endpoint.query_params])
 
-                if method.lower() in ['post', 'put']:
-                    body_params = self._gen_body_param(endpoint.body_params)
-                    if body_params:
-                        params.append(body_params)
-
                 methods[method.lower()] = {
                     'tags': [endpoint.group],
                     'summary': "",
@@ -164,10 +156,23 @@ class Docs(BaseController):
                         "application/json"
                     ],
                     'parameters': params,
-                    'responses': self._gen_responses_descriptions(method),
-                    "security": [""]
+                    'responses': self._gen_responses_descriptions(method)
                 }
 
+                if method.lower() in ['post', 'put']:
+                    body_params = self._gen_body_param(endpoint.body_params)
+                    if body_params:
+                        methods[method.lower()]['requestBody'] = {
+                            'content': {
+                                'application/json': {
+                                    'schema': body_params
+                                }
+                            }
+                        }
+
+                if endpoint.is_secure:
+                    methods[method.lower()]['security'] = [{'jwt': []}]
+
             if not skip:
                 paths[path[len(baseUrl):]] = methods
 
@@ -180,7 +185,7 @@ class Docs(BaseController):
             scheme = 'http'
 
         spec = {
-            'swagger': "2.0",
+            'openapi': "3.0.0",
             'info': {
                 'description': "Please note that this API is not an official "
                                "Ceph REST API to be used by third-party "
@@ -193,9 +198,19 @@ class Docs(BaseController):
             },
             'host': host,
             'basePath': baseUrl,
+            'servers': [{'url': "{}{}".format(cherrypy.request.base, baseUrl)}],
             'tags': self._gen_tags(all_endpoints),
             'schemes': [scheme],
-            'paths': paths
+            'paths': paths,
+            'components': {
+                'securitySchemes': {
+                    'jwt': {
+                        'type': 'http',
+                        'scheme': 'bearer',
+                        'bearerFormat': 'JWT'
+                    }
+                }
+            }
         }
 
         return spec
@@ -208,13 +223,28 @@ class Docs(BaseController):
     def api_all_json(self):
         return self._gen_spec(True, "/api")
 
-    @Endpoint(json_response=False)
-    def __call__(self, all_endpoints=False):
+    def _swagger_ui_page(self, all_endpoints=False, token=None):
         base = cherrypy.request.base
         if all_endpoints:
             spec_url = "{}/docs/api-all.json".format(base)
         else:
             spec_url = "{}/docs/api.json".format(base)
+
+        auth_header = cherrypy.request.headers.get('authorization')
+        jwt_token = ""
+        if auth_header is not None:
+            scheme, params = auth_header.split(' ', 1)
+            if scheme.lower() == 'bearer':
+                jwt_token = params
+        else:
+            if token is not None:
+                jwt_token = token
+
+        apiKeyCallback = """, onComplete: () => {{
+                        ui.preauthorizeApiKey('jwt', '{}');
+                    }}
+        """.format(jwt_token)
+
         page = """
         <!DOCTYPE html>
         <html>
@@ -261,12 +291,22 @@ class Docs(BaseController):
                         SwaggerUIBundle.presets.apis
                     ],
                     layout: "BaseLayout"
+                    {}
                 }})
                 window.ui = ui
             }}
         </script>
         </body>
         </html>
-        """.format(spec_url)
+        """.format(spec_url, apiKeyCallback)
 
         return page
+
+    @Endpoint(json_response=False)
+    def __call__(self, all_endpoints=False):
+        return self._swagger_ui_page(all_endpoints)
+
+    @Endpoint('POST', path="/", json_response=False,
+              query_params="{all_endpoints}")
+    def _with_token(self, token, all_endpoints=False):
+        return self._swagger_ui_page(all_endpoints, token)
index 3271442305185f06da5ce7476558d5f588287436..4e5bc596705e1589ed7e8dc87848cbd1669e8cc3 100644 (file)
@@ -83,6 +83,7 @@ class Role(RESTController):
         Role._validate_permissions(scopes_permissions)
         Role._set_permissions(role, scopes_permissions)
         role.description = description
+        ACCESS_CTRL_DB.update_users_with_roles(role)
         ACCESS_CTRL_DB.save()
         return Role._role_to_dict(role)
 
index a0671c5fe1cfd0bb3317dc1a4c078302c7561a58..d3cff95543aa95423770d82156c4267d7d253a38 100644 (file)
@@ -8,7 +8,7 @@ from ..exceptions import DashboardException, UserAlreadyExists, \
     UserDoesNotExist
 from ..security import Scope
 from ..services.access_control import ACCESS_CTRL_DB, SYSTEM_ROLES
-from ..tools import Session
+from ..services.auth import JwtManager
 
 
 @ApiController('/user', Scope.USER)
@@ -62,7 +62,7 @@ class User(RESTController):
         return User._user_to_dict(user)
 
     def delete(self, username):
-        session_username = cherrypy.session.get(Session.USERNAME)
+        session_username = JwtManager.get_username()
         if session_username == username:
             raise DashboardException(msg='Cannot delete current user',
                                      code='cannot_delete_current_user',
index 2140294e5a0fba56b44f0ae004ea6494bffd7cf5..9ba0a545dbe1ff966a4001a9578802aed3ae70cb 100644 (file)
@@ -59,9 +59,8 @@ if 'COVERAGE_ENABLED' in os.environ:
 # pylint: disable=wrong-import-position
 from . import logger, mgr
 from .controllers import generate_routes, json_error_page
-from .tools import SessionExpireAtBrowserCloseTool, NotificationQueue, \
-                   RequestLoggingTool, TaskManager
-from .services.auth import AuthManager, AuthManagerTool
+from .tools import NotificationQueue, RequestLoggingTool, TaskManager
+from .services.auth import AuthManager, AuthManagerTool, JwtManager
 from .services.access_control import ACCESS_CONTROL_COMMANDS, \
                                      handle_access_control_command
 from .services.exception import dashboard_exception_handler
@@ -133,7 +132,6 @@ class CherryPyConfig(object):
 
         # Initialize custom handlers.
         cherrypy.tools.authenticate = AuthManagerTool()
-        cherrypy.tools.session_expire_at_browser_close = SessionExpireAtBrowserCloseTool()
         cherrypy.tools.request_logging = RequestLoggingTool()
         cherrypy.tools.dashboard_exception_handler = HandlerWrapperTool(dashboard_exception_handler,
                                                                         priority=31)
@@ -220,11 +218,16 @@ class Module(MgrModule, CherryPyConfig):
 
     COMMANDS = [
         {
-            'cmd': 'dashboard set-session-expire '
+            'cmd': 'dashboard set-jwt-token-ttl '
                    'name=seconds,type=CephInt',
-            'desc': 'Set the session expire timeout',
+            'desc': 'Set the JWT token TTL in seconds',
             'perm': 'w'
         },
+        {
+            'cmd': 'dashboard get-jwt-token-ttl',
+            'desc': 'Get the JWT token TTL in seconds',
+            'perm': 'r'
+        },
         {
             "cmd": "dashboard create-self-signed-cert",
             "desc": "Create self signed certificate",
@@ -237,7 +240,7 @@ class Module(MgrModule, CherryPyConfig):
     OPTIONS = [
         {'name': 'server_addr'},
         {'name': 'server_port'},
-        {'name': 'session-expire'},
+        {'name': 'jwt_token_ttl'},
         {'name': 'password'},
         {'name': 'url_prefix'},
         {'name': 'username'},
@@ -328,9 +331,12 @@ class Module(MgrModule, CherryPyConfig):
         res = handle_access_control_command(cmd)
         if res[0] != -errno.ENOSYS:
             return res
-        elif cmd['prefix'] == 'dashboard set-session-expire':
-            self.set_config('session-expire', str(cmd['seconds']))
-            return 0, 'Session expiration timeout updated', ''
+        elif cmd['prefix'] == 'dashboard set-jwt-token-ttl':
+            self.set_config('jwt_token_ttl', str(cmd['seconds']))
+            return 0, 'JWT token TTL updated', ''
+        elif cmd['prefix'] == 'dashboard get-jwt-token-ttl':
+            ttl = self.get_config('jwt_token_ttl', JwtManager.JWT_TOKEN_TTL)
+            return 0, str(ttl), ''
         elif cmd['prefix'] == 'dashboard create-self-signed-cert':
             self.create_self_signed_cert()
             return 0, 'Self-signed certificate created', ''
index 73428a9d443a55872d6854b80ac5a1caea2cbc51..a256a3a0483729344a2fe912ea36e7ae0e9a089e 100644 (file)
@@ -19,6 +19,7 @@ portend==2.2
 py==1.5.2
 pycodestyle==2.3.1
 pycparser==2.18
+PyJWT==1.6.4
 pylint==1.8.2
 pyopenssl==17.5.0
 pytest==3.3.2
index 423df81c7a82fbbbf7f37044d0625c454fdc678e..43babfb399942d7ac6a26d1f811f4e4b0023c995 100644 (file)
@@ -6,6 +6,7 @@ from __future__ import absolute_import
 import errno
 import json
 import threading
+import time
 
 import bcrypt
 
@@ -152,7 +153,8 @@ SYSTEM_ROLES = {
 
 
 class User(object):
-    def __init__(self, username, password, name=None, email=None, roles=None):
+    def __init__(self, username, password, name=None, email=None, roles=None,
+                 lastUpdate=None):
         self.username = username
         self.password = password
         self.name = name
@@ -161,21 +163,32 @@ class User(object):
             self.roles = set()
         else:
             self.roles = roles
+        if lastUpdate is None:
+            self.refreshLastUpdate()
+        else:
+            self.lastUpdate = lastUpdate
+
+    def refreshLastUpdate(self):
+        self.lastUpdate = int(time.mktime(time.gmtime()))
 
     def set_password(self, password):
         self.password = password_hash(password)
+        self.refreshLastUpdate()
 
     def set_roles(self, roles):
         self.roles = set(roles)
+        self.refreshLastUpdate()
 
     def add_roles(self, roles):
         self.roles = self.roles.union(set(roles))
+        self.refreshLastUpdate()
 
     def del_roles(self, roles):
         for role in roles:
             if role not in self.roles:
                 raise RoleNotInUser(role.name, self.username)
         self.roles.difference_update(set(roles))
+        self.refreshLastUpdate()
 
     def authorize(self, scope, permissions):
         for role in self.roles:
@@ -201,13 +214,15 @@ class User(object):
             'password': self.password,
             'roles': sorted([r.name for r in self.roles]),
             'name': self.name,
-            'email': self.email
+            'email': self.email,
+            'lastUpdate': self.lastUpdate
         }
 
     @classmethod
     def from_dict(cls, u_dict, roles):
         return User(u_dict['username'], u_dict['password'], u_dict['name'],
-                    u_dict['email'], set([roles[r] for r in u_dict['roles']]))
+                    u_dict['email'], set([roles[r] for r in u_dict['roles']]),
+                    u_dict['lastUpdate'])
 
 
 class AccessControlDB(object):
@@ -268,6 +283,14 @@ class AccessControlDB(object):
                 raise UserDoesNotExist(username)
             del self.users[username]
 
+    def update_users_with_roles(self, role):
+        with self.lock:
+            if not role:
+                return
+            for _, user in self.users.items():
+                if role in user.roles:
+                    user.refreshLastUpdate()
+
     def save(self):
         with self.lock:
             db = {
@@ -305,7 +328,7 @@ class AccessControlDB(object):
     def load(cls):
         logger.info("AC: Loading user roles DB version=%s", cls.VERSION)
 
-        json_db = mgr.get_store(cls.accessdb_config_key(), None)
+        json_db = mgr.get_store(cls.accessdb_config_key())
         if json_db is None:
             logger.debug("AC: No DB v%s found, creating new...", cls.VERSION)
             db = cls(cls.VERSION, {}, {})
@@ -502,6 +525,7 @@ Username and password updated''', ''
             role = ACCESS_CTRL_DB.get_role(rolename)
             perms_array = [perm.strip() for perm in permissions]
             role.set_scope_permissions(scopename, perms_array)
+            ACCESS_CTRL_DB.update_users_with_roles(role)
             ACCESS_CTRL_DB.save()
             return 0, json.dumps(role.to_dict()), ''
         except RoleDoesNotExist as ex:
@@ -523,6 +547,7 @@ Username and password updated''', ''
         try:
             role = ACCESS_CTRL_DB.get_role(rolename)
             role.del_scope_permissions(scopename)
+            ACCESS_CTRL_DB.update_users_with_roles(role)
             ACCESS_CTRL_DB.save()
             return 0, json.dumps(role.to_dict()), ''
         except RoleDoesNotExist as ex:
@@ -670,6 +695,9 @@ class LocalAuthenticator(object):
     def __init__(self):
         load_access_control_db()
 
+    def get_user(self, username):
+        return ACCESS_CTRL_DB.get_user(username)
+
     def authenticate(self, username, password):
         try:
             user = ACCESS_CTRL_DB.get_user(username)
index d07a6c31efc6d50e23b03ecd5561e1fa54aad81d..61205a0eca982efd7d4be47251d5ebb8ef4e13ae 100644 (file)
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
+from base64 import b64encode
+import json
+import os
+import threading
 import time
+import uuid
 
 import cherrypy
+import jwt
 
-from .access_control import LocalAuthenticator
+from .access_control import LocalAuthenticator, UserDoesNotExist
 from .. import mgr, logger
-from ..tools import Session
+
+
+class JwtManager(object):
+    JWT_TOKEN_BLACKLIST_KEY = "jwt_token_black_list"
+    JWT_TOKEN_TTL = 28800  # default 8 hours
+    JWT_ALGORITHM = 'HS256'
+    _secret = None
+
+    LOCAL_USER = threading.local()
+
+    @staticmethod
+    def _gen_secret():
+        secret = os.urandom(16)
+        return b64encode(secret).decode('utf-8')
+
+    @classmethod
+    def init(cls):
+        # generate a new secret if it does not exist
+        secret = mgr.get_store('jwt_secret')
+        if secret is None:
+            secret = cls._gen_secret()
+            mgr.set_store('jwt_secret', secret)
+        cls._secret = secret
+
+    @classmethod
+    def gen_token(cls, username):
+        if not cls._secret:
+            cls.init()
+        ttl = mgr.get_config('jwt_token_ttl', cls.JWT_TOKEN_TTL)
+        ttl = int(ttl)
+        now = int(time.mktime(time.gmtime()))
+        payload = {
+            'iss': 'ceph-dashboard',
+            'jti': str(uuid.uuid4()),
+            'exp': now + ttl,
+            'iat': now,
+            'username': username
+        }
+        return jwt.encode(payload, cls._secret, algorithm=cls.JWT_ALGORITHM)
+
+    @classmethod
+    def decode_token(cls, token):
+        if not cls._secret:
+            cls.init()
+        return jwt.decode(token, cls._secret, algorithms=cls.JWT_ALGORITHM)
+
+    @classmethod
+    def get_token_from_header(cls):
+        auth_header = cherrypy.request.headers.get('authorization')
+        if auth_header is not None:
+            scheme, params = auth_header.split(' ', 1)
+            if scheme.lower() == 'bearer':
+                return params
+        return None
+
+    @classmethod
+    def set_user(cls, token):
+        cls.LOCAL_USER.username = token['username']
+
+    @classmethod
+    def reset_user(cls):
+        cls.set_user({'username': None, 'permissions': None})
+
+    @classmethod
+    def get_username(cls):
+        return getattr(cls.LOCAL_USER, 'username', None)
+
+    @classmethod
+    def blacklist_token(cls, token):
+        token = jwt.decode(token, verify=False)
+        blacklist_json = mgr.get_store(cls.JWT_TOKEN_BLACKLIST_KEY)
+        if not blacklist_json:
+            blacklist_json = "{}"
+        bl_dict = json.loads(blacklist_json)
+        now = time.time()
+
+        # remove expired tokens
+        to_delete = []
+        for jti, exp in bl_dict.items():
+            if exp < now:
+                to_delete.append(jti)
+        for jti in to_delete:
+            del bl_dict[jti]
+
+        bl_dict[token['jti']] = token['exp']
+        mgr.set_store(cls.JWT_TOKEN_BLACKLIST_KEY, json.dumps(bl_dict))
+
+    @classmethod
+    def is_blacklisted(cls, jti):
+        blacklist_json = mgr.get_store(cls.JWT_TOKEN_BLACKLIST_KEY)
+        if not blacklist_json:
+            blacklist_json = "{}"
+        bl_dict = json.loads(blacklist_json)
+        return jti in bl_dict
 
 
 class AuthManager(object):
@@ -17,6 +116,10 @@ class AuthManager(object):
     def initialize(cls):
         cls.AUTH_PROVIDER = LocalAuthenticator()
 
+    @classmethod
+    def get_user(cls, username):
+        return cls.AUTH_PROVIDER.get_user(username)
+
     @classmethod
     def authenticate(cls, username, password):
         return cls.AUTH_PROVIDER.authenticate(username, password)
@@ -32,41 +135,52 @@ class AuthManagerTool(cherrypy.Tool):
             'before_handler', self._check_authentication, priority=20)
 
     def _check_authentication(self):
-        username = cherrypy.session.get(Session.USERNAME)
-        if not username:
-            logger.debug('Unauthorized access to %s',
-                         cherrypy.url(relative='server'))
-            raise cherrypy.HTTPError(401, 'You are not authorized to access '
-                                          'that resource')
-        now = time.time()
-        expires = float(mgr.get_config(
-            'session-expire', Session.DEFAULT_EXPIRE))
-        if expires > 0:
-            username_ts = cherrypy.session.get(Session.TS, None)
-            if username_ts and float(username_ts) < (now - expires):
-                cherrypy.session[Session.USERNAME] = None
-                cherrypy.session[Session.TS] = None
-                logger.debug('Session expired')
-                raise cherrypy.HTTPError(401,
-                                         'Session expired. You are not '
-                                         'authorized to access that resource')
-        cherrypy.session[Session.TS] = now
-
-        self._check_authorization(username)
-
-    def _check_authorization(self, username):
+        JwtManager.reset_user()
+        token = JwtManager.get_token_from_header()
+        logger.debug("AMT: token: %s", token)
+        if token:
+            try:
+                token = JwtManager.decode_token(token)
+                if not JwtManager.is_blacklisted(token['jti']):
+                    user = AuthManager.get_user(token['username'])
+                    if user.lastUpdate <= token['iat']:
+                        self._check_authorization(token)
+                        return
+
+                    logger.debug("AMT: user info changed after token was"
+                                 " issued, iat=%s lastUpdate=%s",
+                                 token['iat'], user.lastUpdate)
+                else:
+                    logger.debug('AMT: Token is black-listed')
+            except jwt.exceptions.ExpiredSignatureError:
+                logger.debug("AMT: Token has expired")
+            except jwt.exceptions.InvalidTokenError:
+                logger.debug("AMT: Failed to decode token")
+            except UserDoesNotExist:
+                logger.debug("AMT: Invalid token: user %s does not exist",
+                             token['username'])
+
+        logger.debug('AMT: Unauthorized access to %s',
+                     cherrypy.url(relative='server'))
+        raise cherrypy.HTTPError(401, 'You are not authorized to access '
+                                      'that resource')
+
+    def _check_authorization(self, token):
         logger.debug("AMT: checking authorization...")
+        username = token['username']
         handler = cherrypy.request.handler.callable
         controller = handler.__self__
         sec_scope = getattr(controller, '_security_scope', None)
         sec_perms = getattr(handler, '_security_permissions', None)
-        logger.debug("AMT: checking %s access to '%s' scope", sec_perms,
-                     sec_scope)
+        JwtManager.set_user(token)
 
         if not sec_scope:
             # controller does not define any authorization restrictions
             return
 
+        logger.debug("AMT: checking '%s' access to '%s' scope", sec_perms,
+                     sec_scope)
+
         if not sec_perms:
             logger.debug("Fail to check permission on: %s:%s", controller,
                          handler)
index 23c0def78f14c725ba6524363057c805bdba91fd..1a8ea7a381bf0f2e67061f5bd7c875e65b5dce76 100644 (file)
@@ -14,7 +14,6 @@ from .. import logger
 from ..controllers import json_error_page, generate_controller_routes
 from ..services.auth import AuthManagerTool
 from ..services.exception import dashboard_exception_handler
-from ..tools import SessionExpireAtBrowserCloseTool
 
 
 class ControllerTestCase(helper.CPWebCase):
@@ -39,7 +38,6 @@ class ControllerTestCase(helper.CPWebCase):
 
     def __init__(self, *args, **kwargs):
         cherrypy.tools.authenticate = AuthManagerTool()
-        cherrypy.tools.session_expire_at_browser_close = SessionExpireAtBrowserCloseTool()
         cherrypy.tools.dashboard_exception_handler = HandlerWrapperTool(dashboard_exception_handler,
                                                                         priority=31)
         cherrypy.config.update({'error_page.default': json_error_page})
index ed9abc225b003763b03a08abd64ee08854319084..3592c741f0db45a84a0c5837fbdc20616482a93c 100644 (file)
@@ -4,6 +4,7 @@ from __future__ import absolute_import
 
 import errno
 import json
+import time
 import unittest
 
 from .. import mgr
@@ -28,7 +29,7 @@ class AccessControlTest(unittest.TestCase):
         cls.CONFIG_KEY_DICT[attr] = val
 
     @classmethod
-    def mock_get_config(cls, attr, default):
+    def mock_get_config(cls, attr, default=None):
         return cls.CONFIG_KEY_DICT.get(attr, default)
 
     @classmethod
@@ -98,7 +99,7 @@ class AccessControlTest(unittest.TestCase):
         self.assertNotIn(rolename, db['roles'])
 
     def validate_persistent_user(self, username, roles, password=None,
-                                 name=None, email=None):
+                                 name=None, email=None, lastUpdate=None):
         db = self.load_persistent_db()
         self.assertIn('users', db)
         self.assertIn(username, db['users'])
@@ -110,6 +111,8 @@ class AccessControlTest(unittest.TestCase):
             self.assertEqual(db['users'][username]['name'], name)
         if email:
             self.assertEqual(db['users'][username]['email'], email)
+        if lastUpdate:
+            self.assertEqual(db['users'][username]['lastUpdate'], lastUpdate)
 
     def validate_persistent_no_user(self, username):
         db = self.load_persistent_db()
@@ -306,13 +309,16 @@ class AccessControlTest(unittest.TestCase):
         self.assertDictEqual(user, {
             'username': username,
             'password': pass_hash,
+            'lastUpdate': user['lastUpdate'],
             'name': '{} User'.format(username),
             'email': '{}@user.com'.format(username),
             'roles': [rolename] if rolename else []
         })
         self.validate_persistent_user(username, [rolename] if rolename else [],
                                       pass_hash, '{} User'.format(username),
-                                      '{}@user.com'.format(username))
+                                      '{}@user.com'.format(username),
+                                      user['lastUpdate'])
+        return user
 
     def test_create_user_with_role(self):
         self.test_add_role_scope_perms()
@@ -386,7 +392,7 @@ class AccessControlTest(unittest.TestCase):
 
     def test_add_user_roles(self, username='admin',
                             roles=['pool-manager', 'block-manager']):
-        self.test_create_user(username)
+        user_orig = self.test_create_user(username)
         uroles = []
         for role in roles:
             uroles.append(role)
@@ -395,15 +401,17 @@ class AccessControlTest(unittest.TestCase):
                                  roles=[role])
             self.assertDictContainsSubset({'roles': uroles}, user)
         self.validate_persistent_user(username, uroles)
+        self.assertGreaterEqual(user['lastUpdate'], user_orig['lastUpdate'])
 
     def test_add_user_roles2(self):
-        self.test_create_user()
+        user_orig = self.test_create_user()
         user = self.exec_cmd('ac-user-add-roles', username="admin",
                              roles=['pool-manager', 'block-manager'])
         self.assertDictContainsSubset(
             {'roles': ['block-manager', 'pool-manager']}, user)
         self.validate_persistent_user('admin', ['block-manager',
                                                 'pool-manager'])
+        self.assertGreaterEqual(user['lastUpdate'], user_orig['lastUpdate'])
 
     def test_add_user_roles_not_existent_user(self):
         with self.assertRaises(CmdException) as ctx:
@@ -424,18 +432,20 @@ class AccessControlTest(unittest.TestCase):
                          "Role 'Invalid Role' does not exist")
 
     def test_set_user_roles(self):
-        self.test_create_user()
+        user_orig = self.test_create_user()
         user = self.exec_cmd('ac-user-add-roles', username="admin",
                              roles=['pool-manager'])
         self.assertDictContainsSubset(
             {'roles': ['pool-manager']}, user)
         self.validate_persistent_user('admin', ['pool-manager'])
-        user = self.exec_cmd('ac-user-set-roles', username="admin",
-                             roles=['rgw-manager', 'block-manager'])
+        self.assertGreaterEqual(user['lastUpdate'], user_orig['lastUpdate'])
+        user2 = self.exec_cmd('ac-user-set-roles', username="admin",
+                              roles=['rgw-manager', 'block-manager'])
         self.assertDictContainsSubset(
-            {'roles': ['block-manager', 'rgw-manager']}, user)
+            {'roles': ['block-manager', 'rgw-manager']}, user2)
         self.validate_persistent_user('admin', ['block-manager',
                                                 'rgw-manager'])
+        self.assertGreaterEqual(user2['lastUpdate'], user['lastUpdate'])
 
     def test_set_user_roles_not_existent_user(self):
         with self.assertRaises(CmdException) as ctx:
@@ -498,6 +508,7 @@ class AccessControlTest(unittest.TestCase):
         pass_hash = password_hash('admin', user['password'])
         self.assertDictEqual(user, {
             'username': 'admin',
+            'lastUpdate': user['lastUpdate'],
             'password': pass_hash,
             'name': 'admin User',
             'email': 'admin@user.com',
@@ -532,7 +543,7 @@ class AccessControlTest(unittest.TestCase):
                          "'guest'")
 
     def test_set_user_info(self):
-        self.test_create_user()
+        user_orig = self.test_create_user()
         user = self.exec_cmd('ac-user-set-info', username='admin',
                              name='Admin Name', email='admin@admin.com')
         pass_hash = password_hash('admin', user['password'])
@@ -541,10 +552,12 @@ class AccessControlTest(unittest.TestCase):
             'password': pass_hash,
             'name': 'Admin Name',
             'email': 'admin@admin.com',
+            'lastUpdate': user['lastUpdate'],
             'roles': []
         })
         self.validate_persistent_user('admin', [], pass_hash, 'Admin Name',
                                       'admin@admin.com')
+        self.assertEqual(user['lastUpdate'], user_orig['lastUpdate'])
 
     def test_set_user_info_nonexistent_user(self):
         with self.assertRaises(CmdException) as ctx:
@@ -555,7 +568,7 @@ class AccessControlTest(unittest.TestCase):
         self.assertEqual(str(ctx.exception), "User 'admin' does not exist")
 
     def test_set_user_password(self):
-        self.test_create_user()
+        user_orig = self.test_create_user()
         user = self.exec_cmd('ac-user-set-password', username='admin',
                              password='newpass')
         pass_hash = password_hash('newpass', user['password'])
@@ -564,10 +577,12 @@ class AccessControlTest(unittest.TestCase):
             'password': pass_hash,
             'name': 'admin User',
             'email': 'admin@user.com',
+            'lastUpdate': user['lastUpdate'],
             'roles': []
         })
         self.validate_persistent_user('admin', [], pass_hash, 'admin User',
                                       'admin@user.com')
+        self.assertGreaterEqual(user['lastUpdate'], user_orig['lastUpdate'])
 
     def test_set_user_password_nonexistent_user(self):
         with self.assertRaises(CmdException) as ctx:
@@ -587,6 +602,7 @@ class AccessControlTest(unittest.TestCase):
             'password': pass_hash,
             'name': None,
             'email': None,
+            'lastUpdate': user['lastUpdate'],
             'roles': ['administrator']
         })
         self.validate_persistent_user('admin', ['administrator'], pass_hash,
@@ -603,6 +619,7 @@ class AccessControlTest(unittest.TestCase):
             'password': pass_hash,
             'name': 'admin User',
             'email': 'admin@user.com',
+            'lastUpdate': user['lastUpdate'],
             'roles': ['read-only']
         })
         self.validate_persistent_user('admin', ['read-only'], pass_hash,
@@ -618,7 +635,8 @@ class AccessControlTest(unittest.TestCase):
                 "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
                         "roles": ["block-manager", "test_role"],
                         "name": "admin User",
-                        "email": "admin@user.com"
+                        "email": "admin@user.com",
+                        "lastUpdate": {}
                     }}
                 }},
                 "roles": {{
@@ -633,8 +651,8 @@ class AccessControlTest(unittest.TestCase):
                 }},
                 "version": 1
             }}
-        '''.format(Scope.ISCSI, Permission.READ, Permission.UPDATE,
-                   Scope.POOL, Permission.CREATE)
+        '''.format(int(round(time.time())), Scope.ISCSI, Permission.READ,
+                   Permission.UPDATE, Scope.POOL, Permission.CREATE)
 
         load_access_control_db()
         role = self.exec_cmd('ac-role-show', rolename="test_role")
@@ -649,6 +667,7 @@ class AccessControlTest(unittest.TestCase):
         user = self.exec_cmd('ac-user-show', username="admin")
         self.assertDictEqual(user, {
             'username': 'admin',
+            'lastUpdate': user['lastUpdate'],
             'password':
                 "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
             'name': 'admin User',
@@ -664,6 +683,7 @@ class AccessControlTest(unittest.TestCase):
         user = self.exec_cmd('ac-user-show', username="admin")
         self.assertDictEqual(user, {
             'username': 'admin',
+            'lastUpdate': user['lastUpdate'],
             'password':
                 "$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
             'name': None,
index c65b654b5a12f433e4edb4dc5c9b959aa3f834aa..662fc5cd955cdc8f4e862daabb6d0c584d0b1fb4 100644 (file)
@@ -17,6 +17,7 @@ import cherrypy
 
 from . import logger
 from .exceptions import ViewCacheNoDataException
+from .services.auth import JwtManager
 
 
 class RequestLoggingTool(cherrypy.Tool):
@@ -31,14 +32,9 @@ class RequestLoggingTool(cherrypy.Tool):
         cherrypy.request.hooks.attach('after_error_response', self.request_error,
                                       priority=5)
 
-    def _get_user(self):
-        if hasattr(cherrypy.serving, 'session'):
-            return cherrypy.session.get(Session.USERNAME)
-        return None
-
     def request_begin(self):
         req = cherrypy.request
-        user = self._get_user()
+        user = JwtManager.get_username()
         if user:
             logger.debug("[%s:%s] [%s] [%s] %s", req.remote.ip,
                          req.remote.port, req.method, user, req.path_info)
@@ -85,7 +81,7 @@ class RequestLoggingTool(cherrypy.Tool):
         req = cherrypy.request
         res = cherrypy.response
         lat = time.time() - res.time
-        user = self._get_user()
+        user = JwtManager.get_username()
         status = res.status[:3] if isinstance(res.status, str) else res.status
         if 'Content-Length' in res.headers:
             length = self._format_bytes(res.headers['Content-Length'])
@@ -219,48 +215,6 @@ class ViewCache(object):
         return wrapper
 
 
-class Session(object):
-    """
-    This class contains all relevant settings related to cherrypy.session.
-    """
-    NAME = 'session_id'
-
-    # The keys used to store the information in the cherrypy.session.
-    USERNAME = '_username'
-    TS = '_ts'
-    EXPIRE_AT_BROWSER_CLOSE = '_expire_at_browser_close'
-
-    # The default values.
-    DEFAULT_EXPIRE = 1200.0
-
-
-class SessionExpireAtBrowserCloseTool(cherrypy.Tool):
-    """
-    A CherryPi Tool which takes care that the cookie does not expire
-    at browser close if the 'Keep me logged in' checkbox was selected
-    on the login page.
-    """
-    def __init__(self):
-        cherrypy.Tool.__init__(self, 'before_finalize', self._callback)
-
-    def _callback(self):
-        # Shall the cookie expire at browser close?
-        expire_at_browser_close = cherrypy.session.get(
-            Session.EXPIRE_AT_BROWSER_CLOSE, True)
-        logger.debug("expire at browser close: %s", expire_at_browser_close)
-        if expire_at_browser_close:
-            # Get the cookie and its name.
-            cookie = cherrypy.response.cookie
-            name = cherrypy.request.config.get(
-                'tools.sessions.name', Session.NAME)
-            # Make the cookie a session cookie by purging the
-            # fields 'expires' and 'max-age'.
-            logger.debug("expire at browser close: removing 'expires' and 'max-age'")
-            if name in cookie:
-                del cookie[name]['expires']
-                del cookie[name]['max-age']
-
-
 class NotificationQueue(threading.Thread):
     _ALL_TYPES_ = '__ALL__'
     _listeners = collections.defaultdict(set)