]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: configured security info for each controller
authorRicardo Dias <rdias@suse.com>
Fri, 27 Apr 2018 14:05:21 +0000 (15:05 +0100)
committerRicardo Dias <rdias@suse.com>
Tue, 26 Jun 2018 11:28:54 +0000 (12:28 +0100)
Signed-off-by: Ricardo Dias <rdias@suse.com>
16 files changed:
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/controllers/dashboard.py
src/pybind/mgr/dashboard/controllers/erasure_code_profile.py
src/pybind/mgr/dashboard/controllers/grafana.py
src/pybind/mgr/dashboard/controllers/host.py
src/pybind/mgr/dashboard/controllers/monitor.py
src/pybind/mgr/dashboard/controllers/osd.py
src/pybind/mgr/dashboard/controllers/perf_counters.py
src/pybind/mgr/dashboard/controllers/pool.py
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/controllers/summary.py
src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py
src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py

index 328eb271d892cf0d8b0b2dd78a51615f1b7473e1..23eb6d81eb42397cfc4154167b5224929597ea08 100644 (file)
@@ -14,16 +14,24 @@ import cherrypy
 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] != "/":
@@ -40,6 +48,7 @@ class Controller(object):
     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,
@@ -55,8 +64,9 @@ class Controller(object):
 
 
 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):
@@ -444,6 +454,22 @@ class BaseController(object):
         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:
@@ -544,6 +570,13 @@ class RESTController(BaseController):
     # 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}),
@@ -580,6 +613,8 @@ class RESTController(BaseController):
             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__]
@@ -593,6 +628,8 @@ class RESTController(BaseController):
 
                 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']:
@@ -602,6 +639,8 @@ class RESTController(BaseController):
                 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:
@@ -616,6 +655,8 @@ class RESTController(BaseController):
                 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
@@ -638,6 +679,8 @@ class RESTController(BaseController):
             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
@@ -686,3 +729,43 @@ class RESTController(BaseController):
             }
             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
index 90fa5c9c0b8c0a280270544271703f089babdf1c..df31d4a4f36eb2d9fb303c5167378159a24973fc 100644 (file)
@@ -8,11 +8,12 @@ import cherrypy
 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__()
index 87bf62089bdb6471364aed21fb54d05909702908..d859c85567d9acc6b9d0f784f0ca0f06a09f77de 100644 (file)
@@ -6,6 +6,7 @@ import json
 
 from . import ApiController, Endpoint, BaseController
 from .. import mgr
+from ..security import Permission, Scope
 from ..services.ceph_service import CephService
 from ..tools import NotificationQueue
 
@@ -45,32 +46,39 @@ class Dashboard(BaseController):
 
             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")
index 9c9f0d3f591f274871d29cb54c1fc275a7fe34cf..688fbf783547ff1d3b6250ed44c4746b37bd0631 100644 (file)
@@ -4,6 +4,7 @@ from __future__ import absolute_import
 from cherrypy import NotFound
 
 from . import ApiController, RESTController
+from ..security import Scope
 from ..services.ceph_service import CephService
 from .. import mgr
 
@@ -15,7 +16,7 @@ def _serialize_ecp(name, ecp):
     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
index e7ae55f3a02d49ef5a8ddbd067ffa4cb10967e79..03fdfbed4d36f7cda6f7e5bae1686abc55319087 100644 (file)
@@ -5,8 +5,9 @@ import cherrypy
 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
 
 
@@ -101,10 +102,11 @@ class GrafanaRestClient(object):
         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()
@@ -115,9 +117,10 @@ class Grafana(BaseController):
         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
index 76c2afea3b010c1f2b5b0689d100a15586d8e3ff..e8518a14c9218a69e669710ad3573d32fd8c0e38 100644 (file)
@@ -3,9 +3,10 @@ from __future__ import absolute_import
 
 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()
index be3a85fe24b959dad26e4b80b3c1f506fcb92a6d..d4512fcfe357fc7e1f1865a5fce8efc310e2b1a9 100644 (file)
@@ -3,13 +3,15 @@ from __future__ import absolute_import
 
 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 = [], []
 
index 0b462ef9c4008c55213f038be26951b88a8f637d..43cd6c8ffba390f6d9b742b530db2abbcce80c87 100644 (file)
@@ -1,14 +1,15 @@
 # -*- 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()
@@ -58,12 +59,13 @@ class Osd(RESTController):
         }
 
     @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():
index 68a82dc6a831b7d9ea6bf04cb1f89f092aa0e0a1..152d59b73e3c2472b892f91229f08684bd399991 100644 (file)
@@ -3,6 +3,7 @@ from __future__ import absolute_import
 
 from . import ApiController, RESTController
 from .. import mgr
+from ..security import Scope
 from ..services.ceph_service import CephService
 
 
@@ -38,32 +39,32 @@ class PerfCounter(RESTController):
         }
 
 
-@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'
 
index 09f7c75d1ff83f91ca560278b1a4a07a1080a24c..6dd3d3618207bd4cf432f6f11c2397deb6e04d4a 100644 (file)
@@ -3,14 +3,15 @@ from __future__ import absolute_import
 
 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
@@ -83,6 +84,7 @@ class Pool(RESTController):
             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):
index 3eca70a6ab1052bf45c7e0c1bac7c981ce141fda..d9b217fd6f75bf2acab2ba41d42e24610dcdb749 100644 (file)
@@ -11,8 +11,9 @@ import six
 
 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, \
@@ -112,7 +113,7 @@ def _sort_features(features, enable=True):
     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"
@@ -360,6 +361,7 @@ class Rbd(RESTController):
 
     @RbdTask('flatten', ['{pool_name}', '{image_name}'], 2.0)
     @RESTController.Resource('POST')
+    @UpdatePermission
     def flatten(self, pool_name, image_name):
 
         def _flatten(ioctx, image):
@@ -373,7 +375,7 @@ class Rbd(RESTController):
         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"
@@ -416,6 +418,7 @@ class RbdSnapshot(RESTController):
     @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)
index a1823ed5e7d6ae682a33a665c7704a91d0e102a2..40fa419b31821a3e8e6a57068581dbc5b53c0309 100644 (file)
@@ -8,8 +8,9 @@ from functools import partial
 
 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
@@ -154,7 +155,7 @@ def get_daemons_and_pools():  # pylint: disable=R0915
     }
 
 
-@ApiController('/rbdmirror')
+@ApiController('/rbdmirror', Scope.RBD_MIRRORING)
 class RbdMirror(BaseController):
 
     def __init__(self):
@@ -163,6 +164,7 @@ class RbdMirror(BaseController):
 
     @Endpoint()
     @handle_rbd_error()
+    @ReadPermission
     def __call__(self):
         status, content_data = self._get_content_data()
         return {'status': status, 'content_data': content_data}
index ee989a137fa2f15a3c643d2bcd0f4df8f7ec4bf7..53d6265595961ccf4d521f79acf8d062c9c23cb3 100644 (file)
@@ -4,18 +4,21 @@ from __future__ import absolute_import
 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:
@@ -35,7 +38,7 @@ class Rgw(BaseController):
         return status
 
 
-@ApiController('/rgw/daemon')
+@ApiController('/rgw/daemon', Scope.RGW)
 class RgwDaemon(RESTController):
 
     def list(self):
@@ -94,7 +97,7 @@ class RgwRESTController(RESTController):
             raise DashboardException(e, http_status_code=500, component='rgw')
 
 
-@ApiController('/rgw/bucket')
+@ApiController('/rgw/bucket', Scope.RGW)
 class RgwBucket(RgwRESTController):
 
     def list(self):
@@ -124,7 +127,7 @@ class RgwBucket(RgwRESTController):
         }, json_response=False)
 
 
-@ApiController('/rgw/user')
+@ApiController('/rgw/user', Scope.RGW)
 class RgwUser(RgwRESTController):
 
     def list(self):
index 59ff0f4015cf2fb472c772bb1dea001e163f2454..b73d0fb4ea63464ceb76618dcdfc7259f5b5435e 100644 (file)
@@ -6,8 +6,9 @@ import json
 
 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
 
 
@@ -43,12 +44,14 @@ class Summary(BaseController):
     @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
index 4043b0deeb3479b0dabf90daccd0a668206ab8f6..bcd3eb8bfd5593b8aa631e2164c9e8229e17f937 100644 (file)
@@ -3,12 +3,13 @@ from __future__ import absolute_import
 
 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
index 99ca8521e7f0a45cf9d13a25017f3f53bb1c58f4..d8ec2ee85b41f76e63d3695a85dc8f1c36cf7830 100644 (file)
@@ -78,9 +78,11 @@ class RbdMirroringControllerTest(ControllerTestCase):
         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)