from six import add_metaclass
from .. import logger
+from ..security import Scope, Permission
from ..settings import Settings
from ..tools import Session, wraps, getargspec, TaskManager
-from ..exceptions import ViewCacheNoDataException, DashboardException
+from ..exceptions import ViewCacheNoDataException, DashboardException, \
+ ScopeNotValid, PermissionNotValid
from ..services.exception import serialize_dashboard_exception
+from ..services.auth import AuthManager
class Controller(object):
- def __init__(self, path, base_url=None, secure=True):
+ def __init__(self, path, base_url=None, security_scope=None, secure=True):
+ if security_scope and not Scope.valid_scope(security_scope):
+ logger.debug("Invalid security scope name: %s\n Possible values: "
+ "%s", security_scope, Scope.all_scopes())
+ raise ScopeNotValid(security_scope)
self.path = path
self.base_url = base_url
+ self.security_scope = security_scope
self.secure = secure
if self.path and self.path[0] != "/":
def __call__(self, cls):
cls._cp_controller_ = True
cls._cp_path_ = "{}{}".format(self.base_url, self.path)
+ cls._security_scope = self.security_scope
config = {
'tools.sessions.on': True,
class ApiController(Controller):
- def __init__(self, path, secure=True):
+ def __init__(self, path, security_scope=None, secure=True):
super(ApiController, self).__init__(path, base_url="/api",
+ security_scope=security_scope,
secure=secure)
def __call__(self, cls):
logger.info('Initializing controller: %s -> %s',
self.__class__.__name__, self._cp_path_)
+ def _has_permissions(self, permissions, scope=None):
+ if not self._cp_config['tools.authenticate.on']:
+ raise Exception("Cannot verify permission in non secured "
+ "controllers")
+
+ if not isinstance(permissions, list):
+ permissions = [permissions]
+
+ if scope is None:
+ scope = getattr(self, '_security_scope', None)
+ if scope is None:
+ raise Exception("Cannot verify permissions without scope security"
+ " defined")
+ username = cherrypy.session.get(Session.USERNAME)
+ return AuthManager.authorize(username, scope, permissions)
+
@classmethod
def get_path_param_names(cls, path_extension=None):
if path_extension is None:
# of the resourse ID.
RESOURCE_ID = None
+ _permission_map = {
+ 'GET': Permission.READ,
+ 'POST': Permission.CREATE,
+ 'PUT': Permission.UPDATE,
+ 'DELETE': Permission.DELETE
+ }
+
_method_mapping = collections.OrderedDict([
('list', {'method': 'GET', 'resource': False, 'status': 200}),
('create', {'method': 'POST', 'resource': False, 'status': 201}),
method = None
query_params = None
path = ""
+ sec_permissions = hasattr(func, '_security_permissions')
+ permission = None
if func.__name__ in cls._method_mapping:
meth = cls._method_mapping[func.__name__]
status = meth['status']
method = meth['method']
+ if not sec_permissions:
+ permission = cls._permission_map[method]
elif hasattr(func, "_collection_method_"):
if func._collection_method_['path']:
status = func._collection_method_['status']
method = func._collection_method_['method']
query_params = func._collection_method_['query_params']
+ if not sec_permissions:
+ permission = cls._permission_map[method]
elif hasattr(func, "_resource_method_"):
if not res_id_params:
status = func._resource_method_['status']
method = func._resource_method_['method']
query_params = func._resource_method_['query_params']
+ if not sec_permissions:
+ permission = cls._permission_map[method]
else:
continue
func = cls._status_code_wrapper(func, status)
endp_func = Endpoint(method, path=path,
query_params=query_params)(func)
+ if permission:
+ _set_func_permissions(endp_func, [permission])
result.append(cls.Endpoint(cls, endp_func))
return result
}
return func
return _wrapper
+
+
+# Role-based access permissions decorators
+
+def _set_func_permissions(func, permissions):
+ if not isinstance(permissions, list):
+ permissions = [permissions]
+
+ for perm in permissions:
+ if not Permission.valid_permission(perm):
+ logger.debug("Invalid security permission: %s\n "
+ "Possible values: %s", perm,
+ Permission.all_permissions())
+ raise PermissionNotValid(perm)
+
+ if not hasattr(func, '_security_permissions'):
+ func._security_permissions = permissions
+ else:
+ permissions.extend(func._security_permissions)
+ func._security_permissions = list(set(permissions))
+
+
+def ReadPermission(func):
+ _set_func_permissions(func, Permission.READ)
+ return func
+
+
+def CreatePermission(func):
+ _set_func_permissions(func, Permission.CREATE)
+ return func
+
+
+def DeletePermission(func):
+ _set_func_permissions(func, Permission.DELETE)
+ return func
+
+
+def UpdatePermission(func):
+ _set_func_permissions(func, Permission.UPDATE)
+ return func
from . import ApiController, RESTController
from .. import mgr
from ..exceptions import DashboardException
+from ..security import Scope
from ..services.ceph_service import CephService
from ..tools import ViewCache
-@ApiController('/cephfs')
+@ApiController('/cephfs', Scope.CEPHFS)
class CephFS(RESTController):
def __init__(self):
super(CephFS, self).__init__()
from . import ApiController, Endpoint, BaseController
from .. import mgr
+from ..security import Permission, Scope
from ..services.ceph_service import CephService
from ..tools import NotificationQueue
NotificationQueue.register(self.append_log, 'clog')
- # Fuse osdmap with pg_summary to get description of pools
- # including their PG states
+ result = {
+ "health": self.health_data(),
+ }
- osd_map = self.osd_map()
+ if self._has_permissions(Permission.READ, Scope.LOG):
+ result['clog'] = list(self.log_buffer)
+ result['audit_log'] = list(self.audit_buffer)
- pools = CephService.get_pool_list_with_stats()
+ if self._has_permissions(Permission.READ, Scope.MONITOR):
+ result['mon_status'] = self.mon_status()
- # Not needed, skip the effort of transmitting this
- # to UI
- del osd_map['pg_temp']
+ if self._has_permissions(Permission.READ, Scope.CEPHFS):
+ result['fs_map'] = mgr.get('fs_map')
- df = mgr.get("df")
- df['stats']['total_objects'] = sum(
- [p['stats']['objects'] for p in df['pools']])
+ if self._has_permissions(Permission.READ, Scope.OSD):
+ osd_map = self.osd_map()
+ # Not needed, skip the effort of transmitting this to UI
+ del osd_map['pg_temp']
+ result['osd_map'] = osd_map
- return {
- "health": self.health_data(),
- "mon_status": self.mon_status(),
- "fs_map": mgr.get('fs_map'),
- "osd_map": osd_map,
- "clog": list(self.log_buffer),
- "audit_log": list(self.audit_buffer),
- "pools": pools,
- "mgr_map": mgr.get("mgr_map"),
- "df": df
- }
+ if self._has_permissions(Permission.READ, Scope.MANAGER):
+ result['mgr_map'] = mgr.get("mgr_map")
+
+ if self._has_permissions(Permission.READ, Scope.POOL):
+ pools = CephService.get_pool_list_with_stats()
+ result['pools'] = pools
+
+ df = mgr.get("df")
+ df['stats']['total_objects'] = sum(
+ [p['stats']['objects'] for p in df['pools']])
+ result['df'] = df
+
+ return result
def mon_status(self):
mon_status_data = mgr.get("mon_status")
from cherrypy import NotFound
from . import ApiController, RESTController
+from ..security import Scope
from ..services.ceph_service import CephService
from .. import mgr
return ecp
-@ApiController('/erasure_code_profile')
+@ApiController('/erasure_code_profile', Scope.POOL)
class ErasureCodeProfile(RESTController):
"""
create() supports additional key-value arguments that are passed to the
import requests
from six.moves.urllib.parse import urlparse # pylint: disable=import-error
-from . import ApiController, BaseController, Proxy, Endpoint
+from . import ApiController, BaseController, Proxy, Endpoint, ReadPermission
from .. import logger
+from ..security import Scope
from ..settings import Settings
return True, ''
-@ApiController('/grafana')
+@ApiController('/grafana', Scope.GRAFANA)
class Grafana(BaseController):
@Endpoint()
+ @ReadPermission
def status(self):
grafana = GrafanaRestClient.instance()
available, msg = grafana.is_service_online()
return response
-@ApiController('/grafana/proxy')
+@ApiController('/grafana/proxy', Scope.GRAFANA)
class GrafanaProxy(BaseController):
@Proxy()
+ @ReadPermission
def __call__(self, path, **params):
grafana = GrafanaRestClient.instance()
method = cherrypy.request.method
from . import ApiController, RESTController
from .. import mgr
+from ..security import Scope
-@ApiController('/host')
+@ApiController('/host', Scope.HOSTS)
class Host(RESTController):
def list(self):
return mgr.list_servers()
import json
-from . import ApiController, Endpoint, BaseController
+from . import ApiController, Endpoint, BaseController, ReadPermission
from .. import mgr
+from ..security import Scope
-@ApiController('/monitor')
+@ApiController('/monitor', Scope.MONITOR)
class Monitor(BaseController):
@Endpoint()
+ @ReadPermission
def __call__(self):
in_quorum, out_quorum = [], []
# -*- coding: utf-8 -*-
from __future__ import absolute_import
-from . import ApiController, RESTController
+from . import ApiController, RESTController, UpdatePermission
from .. import mgr, logger
+from ..security import Scope
from ..services.ceph_service import CephService
from ..services.exception import handle_send_command_error
from ..tools import str_to_bool
-@ApiController('/osd')
+@ApiController('/osd', Scope.OSD)
class Osd(RESTController):
def list(self):
osds = self.get_osd_map()
}
@RESTController.Resource('POST', query_params=['deep'])
+ @UpdatePermission
def scrub(self, svc_id, deep=False):
api_scrub = "osd deep-scrub" if str_to_bool(deep) else "osd scrub"
CephService.send_command("mon", api_scrub, who=svc_id)
-@ApiController('/osd/flags')
+@ApiController('/osd/flags', Scope.OSD)
class OsdFlagsController(RESTController):
@staticmethod
def _osd_flags():
from . import ApiController, RESTController
from .. import mgr
+from ..security import Scope
from ..services.ceph_service import CephService
}
-@ApiController('perf_counters/mds')
+@ApiController('perf_counters/mds', Scope.CEPHFS)
class MdsPerfCounter(PerfCounter):
service_type = 'mds'
-@ApiController('perf_counters/mon')
+@ApiController('perf_counters/mon', Scope.MONITOR)
class MonPerfCounter(PerfCounter):
service_type = 'mon'
-@ApiController('perf_counters/osd')
+@ApiController('perf_counters/osd', Scope.OSD)
class OsdPerfCounter(PerfCounter):
service_type = 'osd'
-@ApiController('perf_counters/rgw')
+@ApiController('perf_counters/rgw', Scope.RGW)
class RgwPerfCounter(PerfCounter):
service_type = 'rgw'
-@ApiController('perf_counters/rbd-mirror')
+@ApiController('perf_counters/rbd-mirror', Scope.RBD_MIRRORING)
class RbdMirrorPerfCounter(PerfCounter):
service_type = 'rbd-mirror'
-@ApiController('perf_counters/mgr')
+@ApiController('perf_counters/mgr', Scope.MANAGER)
class MgrPerfCounter(PerfCounter):
service_type = 'mgr'
import cherrypy
-from . import ApiController, RESTController, Endpoint
+from . import ApiController, RESTController, Endpoint, ReadPermission
from .. import mgr
+from ..security import Scope
from ..services.ceph_service import CephService
from ..services.exception import handle_send_command_error
from ..tools import str_to_bool
-@ApiController('/pool')
+@ApiController('/pool', Scope.POOL)
class Pool(RESTController):
@classmethod
CephService.send_command('mon', 'osd pool set', pool=pool, var=key, val=value)
@Endpoint()
+ @ReadPermission
def _info(self):
"""Used by the create-pool dialog"""
def rules(pool_type):
import rbd
-from . import ApiController, RESTController, Task
+from . import ApiController, RESTController, Task, UpdatePermission
from .. import mgr
+from ..security import Scope
from ..services.ceph_service import CephService
from ..tools import ViewCache
from ..services.exception import handle_rados_error, handle_rbd_error, \
features.sort(key=key_func, reverse=not enable)
-@ApiController('/block/image')
+@ApiController('/block/image', Scope.RBD_IMAGE)
class Rbd(RESTController):
RESOURCE_ID = "pool_name/image_name"
@RbdTask('flatten', ['{pool_name}', '{image_name}'], 2.0)
@RESTController.Resource('POST')
+ @UpdatePermission
def flatten(self, pool_name, image_name):
def _flatten(ioctx, image):
return _format_bitmask(int(rbd_default_features))
-@ApiController('/block/image/:pool_name/:image_name/snap')
+@ApiController('/block/image/{pool_name}/{image_name}/snap', Scope.RBD_IMAGE)
class RbdSnapshot(RESTController):
RESOURCE_ID = "snapshot_name"
@RbdTask('snap/rollback',
['{pool_name}', '{image_name}', '{snapshot_name}'], 5.0)
@RESTController.Resource('POST')
+ @UpdatePermission
def rollback(self, pool_name, image_name, snapshot_name):
def _rollback(ioctx, img, snapshot_name):
img.rollback_to_snap(snapshot_name)
import rbd
-from . import ApiController, Endpoint, BaseController
+from . import ApiController, Endpoint, BaseController, ReadPermission
from .. import logger, mgr
+from ..security import Scope
from ..services.ceph_service import CephService
from ..tools import ViewCache
from ..services.exception import handle_rbd_error
}
-@ApiController('/rbdmirror')
+@ApiController('/rbdmirror', Scope.RBD_MIRRORING)
class RbdMirror(BaseController):
def __init__(self):
@Endpoint()
@handle_rbd_error()
+ @ReadPermission
def __call__(self):
status, content_data = self._get_content_data()
return {'status': status, 'content_data': content_data}
import json
import cherrypy
-from . import ApiController, BaseController, RESTController, Endpoint
+from . import ApiController, BaseController, RESTController, Endpoint, \
+ ReadPermission
from .. import logger
+from ..security import Scope
from ..services.ceph_service import CephService
from ..services.rgw_client import RgwClient
from ..rest_client import RequestException
from ..exceptions import DashboardException
-@ApiController('/rgw')
+@ApiController('/rgw', Scope.RGW)
class Rgw(BaseController):
@Endpoint()
+ @ReadPermission
def status(self):
status = {'available': False, 'message': None}
try:
return status
-@ApiController('/rgw/daemon')
+@ApiController('/rgw/daemon', Scope.RGW)
class RgwDaemon(RESTController):
def list(self):
raise DashboardException(e, http_status_code=500, component='rgw')
-@ApiController('/rgw/bucket')
+@ApiController('/rgw/bucket', Scope.RGW)
class RgwBucket(RgwRESTController):
def list(self):
}, json_response=False)
-@ApiController('/rgw/user')
+@ApiController('/rgw/user', Scope.RGW)
class RgwUser(RgwRESTController):
def list(self):
from . import ApiController, Endpoint, BaseController
from .. import mgr
+from ..security import Permission, Scope
from ..controllers.rbd_mirroring import get_daemons_and_pools
-from ..tools import ViewCacheNoDataException
+from ..exceptions import ViewCacheNoDataException
from ..tools import TaskManager
@Endpoint()
def __call__(self):
executing_t, finished_t = TaskManager.list_serializable()
- return {
+ result = {
'health_status': self._health_status(),
- 'rbd_mirroring': self._rbd_mirroring(),
'mgr_id': mgr.get_mgr_id(),
'have_mon_connection': mgr.have_mon_connection(),
'executing_tasks': executing_t,
'finished_tasks': finished_t,
'version': mgr.version
}
+ if self._has_permissions(Permission.READ, Scope.RBD_MIRRORING):
+ result['rbd_mirroring'] = self._rbd_mirroring()
+ return result
from . import ApiController, RESTController
from .. import mgr
+from ..security import Scope
from ..services.ceph_service import CephService
SERVICE_TYPE = 'tcmu-runner'
-@ApiController('/tcmuiscsi')
+@ApiController('/tcmuiscsi', Scope.ISCSI)
class TcmuIscsi(RESTController):
# pylint: disable=too-many-nested-blocks
def list(self): # pylint: disable=unused-argument
for k in ['daemons', 'pools', 'image_error', 'image_syncing', 'image_ready']:
self.assertIn(k, result['content_data'])
+ @mock.patch('dashboard.controllers.BaseController._has_permissions')
@mock.patch('dashboard.controllers.rbd_mirroring.rbd')
- def test_summary(self, rbd_mock): # pylint: disable=W0613
+ def test_summary(self, rbd_mock, has_perms_mock): # pylint: disable=W0613
"""We're also testing `summary`, as it also uses code from `rbd_mirroring.py`"""
+ has_perms_mock.return_value = True
self._get('/test/api/summary')
self.assertStatus(200)