%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
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
%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
python (>= 2.7),
python-all-dev,
python-cherrypy3,
+ python-jwt,
python-nose,
python-pecan,
python-bcrypt,
Architecture: linux-any
Depends: ceph-base (= ${binary:Version}),
python-cherrypy3,
+ python-jwt,
python-jinja2,
python-openssl,
python-pecan,
CEPHFS = False
_session = None # type: requests.sessions.Session
+ _token = None
_resp = None # type: requests.models.Response
_loggedin = False
_base_uri = None
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
return execute
return wrapper
+ @classmethod
+ def set_jwt_token(cls, token):
+ cls._token = token
+
@classmethod
def setUpClass(cls):
super(DashboardTestCase, cls).setUpClass()
# wait for mds restart to complete...
cls.fs.wait_for_daemons()
+ cls._token = None
cls._session = requests.Session()
cls._resp = None
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:
import time
+import jwt
+
from .helper import 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'})
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")
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')
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', {})
email='my@email.com',
roles=['administrator'])
self.assertStatus(201)
+ user = self.jsonBody()
self._get('/api/user/user1')
self.assertStatus(200)
'username': 'user1',
'name': 'My Name',
'email': 'my@email.com',
- 'roles': ['administrator']
+ 'roles': ['administrator'],
+ 'lastUpdate': user['lastUpdate']
})
self._put('/api/user/user1', {
'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')
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):
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):
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,
}
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
# -*- 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
}
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)
from .. import logger, mgr
-@Controller('/docs')
+@Controller('/docs', secure=False)
class Docs(BaseController):
@classmethod
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
res = {
'name': param['name'],
'in': ptype,
- 'type': cls._gen_type(param)
+ 'schema': {
+ 'type': cls._gen_type(param)
+ }
}
if param['required']:
res['required'] = True
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': "",
"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
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 "
},
'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
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>
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)
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)
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)
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',
# 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
# 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)
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",
OPTIONS = [
{'name': 'server_addr'},
{'name': 'server_port'},
- {'name': 'session-expire'},
+ {'name': 'jwt_token_ttl'},
{'name': 'password'},
{'name': 'url_prefix'},
{'name': 'username'},
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', ''
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
import errno
import json
import threading
+import time
import bcrypt
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
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:
'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):
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 = {
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, {}, {})
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:
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:
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)
# -*- 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):
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)
'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)
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):
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})
import errno
import json
+import time
import unittest
from .. import mgr
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
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'])
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()
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()
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)
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:
"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:
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',
"'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'])
'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:
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'])
'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:
'password': pass_hash,
'name': None,
'email': None,
+ 'lastUpdate': user['lastUpdate'],
'roles': ['administrator']
})
self.validate_persistent_user('admin', ['administrator'], pass_hash,
'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,
"$2b$12$sd0Az7mm3FaJl8kN3b/xwOuztaN0sWUwC1SJqjM4wcDw/s5cmGbLK",
"roles": ["block-manager", "test_role"],
"name": "admin User",
- "email": "admin@user.com"
+ "email": "admin@user.com",
+ "lastUpdate": {}
}}
}},
"roles": {{
}},
"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")
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',
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,
from . import logger
from .exceptions import ViewCacheNoDataException
+from .services.auth import JwtManager
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)
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'])
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)