mirror of
https://github.com/ceph/ceph
synced 2025-02-20 17:37:29 +00:00
mgr/dashboard: backend: JWT based authentication
Signed-off-by: Ricardo Dias <rdias@suse.com>
This commit is contained in:
parent
5727f9b455
commit
2f5e7c3392
@ -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
|
||||
|
2
debian/control
vendored
2
debian/control
vendored
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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', {})
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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', ''
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -1,13 +1,112 @@
|
||||
# -*- 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
|
||||
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
|
||||
|
||||
self._check_authorization(username)
|
||||
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'])
|
||||
|
||||
def _check_authorization(self, 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)
|
||||
|
@ -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})
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user