From 0f21a37d06f3b4d7b2477aff7ba34ef092f1ce4f Mon Sep 17 00:00:00 2001 From: Gauvain Pocentek <gauvain.pocentek@objectif-libre.com> Date: Sat, 9 May 2015 19:05:04 +0200 Subject: [PATCH] Enforce a default policy Provide a default policy which requires to be admin for every API call except rating/quote, report/total and storage/list_data_frames (which will require a bit more control to return only the current tenant data for non-admin users). Also provide a custom Context class to extract roles information from the API request. hashmap controllers now use a new base class which provides a custom _route method. This will avoid to define policies for core and external configuration APIs. Change-Id: Ie3feb4e926270b95ab813a5d24854d1df1758a5e --- cloudkitty/api/hooks.py | 3 +- cloudkitty/api/v1/controllers/collector.py | 5 ++ cloudkitty/api/v1/controllers/rating.py | 16 ++++++ cloudkitty/api/v1/controllers/report.py | 4 ++ cloudkitty/api/v1/controllers/storage.py | 3 ++ cloudkitty/common/context.py | 53 +++++++++++++++++++ cloudkitty/rating/__init__.py | 14 +++++ cloudkitty/rating/hash/controllers/field.py | 4 +- cloudkitty/rating/hash/controllers/group.py | 4 +- cloudkitty/rating/hash/controllers/mapping.py | 4 +- cloudkitty/rating/hash/controllers/root.py | 4 +- cloudkitty/rating/hash/controllers/service.py | 4 +- .../rating/hash/controllers/threshold.py | 4 +- etc/cloudkitty/policy.json | 20 ++++++- 14 files changed, 128 insertions(+), 14 deletions(-) create mode 100644 cloudkitty/common/context.py diff --git a/cloudkitty/api/hooks.py b/cloudkitty/api/hooks.py index 9b7a1618..56fa9bf7 100644 --- a/cloudkitty/api/hooks.py +++ b/cloudkitty/api/hooks.py @@ -15,9 +15,9 @@ # # @author: Stéphane Albert # -from oslo_context import context from pecan import hooks +from cloudkitty.common import context from cloudkitty.common import policy @@ -49,6 +49,7 @@ class ContextHook(hooks.PecanHook): 'tenant': headers.get('X-Tenant') or headers.get('X-Tenant-Id'), 'auth_token': headers.get('X-Auth-Token'), 'is_admin': is_admin, + 'roles': roles, } state.request.context = context.RequestContext(**creds) diff --git a/cloudkitty/api/v1/controllers/collector.py b/cloudkitty/api/v1/controllers/collector.py index e14e5509..0b9eb43f 100644 --- a/cloudkitty/api/v1/controllers/collector.py +++ b/cloudkitty/api/v1/controllers/collector.py @@ -21,6 +21,7 @@ from wsme import types as wtypes import wsmeext.pecan as wsme_pecan from cloudkitty.api.v1.datamodels import collector as collector_models +from cloudkitty.common import policy from cloudkitty.db import api as db_api @@ -36,6 +37,7 @@ class MappingController(rest.RestController): :return: List of every services mapped. """ + policy.enforce(pecan.request.context, 'collector:list_mappings', {}) return [mapping.service for mapping in self._db.list_services()] @wsme_pecan.wsexpose(collector_models.ServiceToCollectorMapping, @@ -45,6 +47,7 @@ class MappingController(rest.RestController): :param service: Name of the service to filter on. """ + policy.enforce(pecan.request.context, 'collector:get_mapping', {}) try: return self._db.get_mapping(service) except db_api.NoSuchMapping as e: @@ -70,6 +73,7 @@ class CollectorController(rest.RestController): :param collector: Name of the collector. :return: State of the collector. """ + policy.enforce(pecan.request.context, 'collector:get_state', {}) return self._db.get_state('collector_{}'.format(collector)) @wsme_pecan.wsexpose(bool, wtypes.text, body=bool) @@ -80,4 +84,5 @@ class CollectorController(rest.RestController): :param state: New state for the collector. :return: State of the collector. """ + policy.enforce(pecan.request.context, 'collector:update_state', {}) return self._db.set_state('collector_{}'.format(collector), state) diff --git a/cloudkitty/api/v1/controllers/rating.py b/cloudkitty/api/v1/controllers/rating.py index 773b5cf6..d3f6b697 100644 --- a/cloudkitty/api/v1/controllers/rating.py +++ b/cloudkitty/api/v1/controllers/rating.py @@ -22,6 +22,7 @@ from wsme import types as wtypes import wsmeext.pecan as wsme_pecan from cloudkitty.api.v1.datamodels import rating as rating_models +from cloudkitty.common import policy from cloudkitty.openstack.common import log as logging LOG = logging.getLogger(__name__) @@ -40,12 +41,21 @@ class ModulesController(rest.RestController): invoke_on_load=True ) + def route(self, *args): + route = args[0] + if route.startswith('/v1/module_config'): + policy.enforce(pecan.request.context, 'rating:module_config', {}) + + super(ModulesController, self).route(*args) + @wsme_pecan.wsexpose(rating_models.CloudkittyModuleCollection) def get_all(self): """return the list of loaded modules. :return: name of every loaded modules. """ + policy.enforce(pecan.request.context, 'rating:list_modules', {}) + modules_list = [] for module in self.extensions: infos = module.obj.module_info.copy() @@ -61,6 +71,8 @@ class ModulesController(rest.RestController): :return: CloudKittyModule """ + policy.enforce(pecan.request.context, 'rating:get_module', {}) + try: module = self.extensions[module_id] except KeyError: @@ -79,6 +91,8 @@ class ModulesController(rest.RestController): :param module_id: name of the module to modify :param module: CloudKittyModule object describing the new desired state """ + policy.enforce(pecan.request.context, 'rating:update_module', {}) + try: ext = self.extensions[module_id].obj except KeyError: @@ -153,6 +167,8 @@ class RatingController(rest.RestController): :param res_data: List of resource descriptions. :return: Total price for these descriptions. """ + policy.enforce(pecan.request.context, 'rating:quote', {}) + client = pecan.request.rpc_client.prepare(namespace='rating') res_dict = {} for res in res_data.resources: diff --git a/cloudkitty/api/v1/controllers/report.py b/cloudkitty/api/v1/controllers/report.py index 750c683a..96efb973 100644 --- a/cloudkitty/api/v1/controllers/report.py +++ b/cloudkitty/api/v1/controllers/report.py @@ -23,6 +23,8 @@ from pecan import rest from wsme import types as wtypes import wsmeext.pecan as wsme_pecan +from cloudkitty.common import policy + class ReportController(rest.RestController): """REST Controller managing the reporting. @@ -41,6 +43,7 @@ class ReportController(rest.RestController): """Return the list of rated tenants. """ + policy.enforce(pecan.request.context, 'report:list_tenants', {}) storage = pecan.request.storage_backend tenants = storage.get_tenants(begin, end) return tenants @@ -53,6 +56,7 @@ class ReportController(rest.RestController): """Return the amount to pay for a given period. """ + policy.enforce(pecan.request.context, 'report:get_total', {}) storage = pecan.request.storage_backend # FIXME(sheeprine): We should filter on user id. # Use keystone token information by default but make it overridable and diff --git a/cloudkitty/api/v1/controllers/storage.py b/cloudkitty/api/v1/controllers/storage.py index e6a6c94a..2aba6245 100644 --- a/cloudkitty/api/v1/controllers/storage.py +++ b/cloudkitty/api/v1/controllers/storage.py @@ -24,6 +24,7 @@ from wsme import types as wtypes import wsmeext.pecan as wsme_pecan from cloudkitty.api.v1.datamodels import storage as storage_models +from cloudkitty.common import policy from cloudkitty import storage as ck_storage from cloudkitty import utils as ck_utils @@ -46,6 +47,8 @@ class DataFramesController(rest.RestController): :return: Collection of DataFrame objects. """ + policy.enforce(pecan.request.context, 'storage:list_data_frames', {}) + begin_ts = ck_utils.dt2ts(begin) end_ts = ck_utils.dt2ts(end) backend = pecan.request.storage_backend diff --git a/cloudkitty/common/context.py b/cloudkitty/common/context.py new file mode 100644 index 00000000..0724c2ad --- /dev/null +++ b/cloudkitty/common/context.py @@ -0,0 +1,53 @@ +# -*- encoding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_context import context + + +class RequestContext(context.RequestContext): + """Extends security contexts from the OpenStack common library.""" + + def __init__(self, auth_token=None, user=None, tenant=None, domain=None, + user_domain=None, project_domain=None, is_admin=False, + read_only=False, show_deleted=False, request_id=None, + resource_uuid=None, overwrite=True, roles=None): + """Extra parameter: + + :param roles: List of user's roles if any. + + """ + self.roles = roles or [] + + super(RequestContext, self).__init__(auth_token=auth_token, + user=user, tenant=tenant, + domain=domain, + user_domain=user_domain, + project_domain=project_domain, + is_admin=is_admin, + read_only=read_only, + show_deleted=show_deleted, + request_id=request_id, + resource_uuid=resource_uuid, + overwrite=overwrite) + + def to_dict(self): + d = super(RequestContext, self).to_dict() + d['roles'] = self.roles + return d + + @classmethod + def from_dict(cls, values): + values.pop('user', None) + values.pop('tenant', None) + return cls(**values) diff --git a/cloudkitty/rating/__init__.py b/cloudkitty/rating/__init__.py index fb125e41..61cd4325 100644 --- a/cloudkitty/rating/__init__.py +++ b/cloudkitty/rating/__init__.py @@ -17,8 +17,11 @@ # import abc +import pecan +from pecan import rest import six +from cloudkitty.common import policy from cloudkitty.db import api as db_api from cloudkitty import rpc @@ -132,3 +135,14 @@ class RatingProcessorBase(object): def notify_reload(self): client = rpc.get_client().prepare(namespace='rating', fanout=True) client.cast({}, 'reload_module', name=self.module_name) + + +class RatingRestControllerBase(rest.RestController): + @pecan.expose() + def _route(self, args, request): + try: + policy.enforce(request.context, 'rating:module_config', {}) + except policy.PolicyNotAuthorized as e: + pecan.abort(403, str(e)) + + return super(RatingRestControllerBase, self)._route(args, request) diff --git a/cloudkitty/rating/hash/controllers/field.py b/cloudkitty/rating/hash/controllers/field.py index 2e7809c4..fa9a57fa 100644 --- a/cloudkitty/rating/hash/controllers/field.py +++ b/cloudkitty/rating/hash/controllers/field.py @@ -16,15 +16,15 @@ # @author: Stéphane Albert # import pecan -from pecan import rest import wsmeext.pecan as wsme_pecan from cloudkitty.api.v1 import types as ck_types +from cloudkitty import rating from cloudkitty.rating.hash.datamodels import field as field_models from cloudkitty.rating.hash.db import api as db_api -class HashMapFieldsController(rest.RestController): +class HashMapFieldsController(rating.RatingRestControllerBase): """Controller responsible of fields management. """ diff --git a/cloudkitty/rating/hash/controllers/group.py b/cloudkitty/rating/hash/controllers/group.py index 1dd22592..54a89f49 100644 --- a/cloudkitty/rating/hash/controllers/group.py +++ b/cloudkitty/rating/hash/controllers/group.py @@ -16,16 +16,16 @@ # @author: Stéphane Albert # import pecan -from pecan import rest import wsmeext.pecan as wsme_pecan from cloudkitty.api.v1 import types as ck_types +from cloudkitty import rating from cloudkitty.rating.hash.datamodels import group as group_models from cloudkitty.rating.hash.datamodels import mapping as mapping_models from cloudkitty.rating.hash.db import api as db_api -class HashMapGroupsController(rest.RestController): +class HashMapGroupsController(rating.RatingRestControllerBase): """Controller responsible of groups management. """ diff --git a/cloudkitty/rating/hash/controllers/mapping.py b/cloudkitty/rating/hash/controllers/mapping.py index d7f47d5e..f510b051 100644 --- a/cloudkitty/rating/hash/controllers/mapping.py +++ b/cloudkitty/rating/hash/controllers/mapping.py @@ -16,16 +16,16 @@ # @author: Stéphane Albert # import pecan -from pecan import rest import wsmeext.pecan as wsme_pecan from cloudkitty.api.v1 import types as ck_types +from cloudkitty import rating from cloudkitty.rating.hash.datamodels import group as group_models from cloudkitty.rating.hash.datamodels import mapping as mapping_models from cloudkitty.rating.hash.db import api as db_api -class HashMapMappingsController(rest.RestController): +class HashMapMappingsController(rating.RatingRestControllerBase): """Controller responsible of mappings management. """ diff --git a/cloudkitty/rating/hash/controllers/root.py b/cloudkitty/rating/hash/controllers/root.py index 267c8f2d..42b250dd 100644 --- a/cloudkitty/rating/hash/controllers/root.py +++ b/cloudkitty/rating/hash/controllers/root.py @@ -15,10 +15,10 @@ # # @author: Stéphane Albert # -from pecan import rest from wsme import types as wtypes import wsmeext.pecan as wsme_pecan +from cloudkitty import rating from cloudkitty.rating.hash.controllers import field as field_api from cloudkitty.rating.hash.controllers import group as group_api from cloudkitty.rating.hash.controllers import mapping as mapping_api @@ -27,7 +27,7 @@ from cloudkitty.rating.hash.controllers import threshold as threshold_api from cloudkitty.rating.hash.datamodels import mapping as mapping_models -class HashMapConfigController(rest.RestController): +class HashMapConfigController(rating.RatingRestControllerBase): """Controller exposing all management sub controllers. """ diff --git a/cloudkitty/rating/hash/controllers/service.py b/cloudkitty/rating/hash/controllers/service.py index 3d70870d..71d108c0 100644 --- a/cloudkitty/rating/hash/controllers/service.py +++ b/cloudkitty/rating/hash/controllers/service.py @@ -16,16 +16,16 @@ # @author: Stéphane Albert # import pecan -from pecan import rest import wsmeext.pecan as wsme_pecan from cloudkitty.api.v1 import types as ck_types +from cloudkitty import rating from cloudkitty.rating.hash.controllers import field as field_api from cloudkitty.rating.hash.datamodels import service as service_models from cloudkitty.rating.hash.db import api as db_api -class HashMapServicesController(rest.RestController): +class HashMapServicesController(rating.RatingRestControllerBase): """Controller responsible of services management. """ diff --git a/cloudkitty/rating/hash/controllers/threshold.py b/cloudkitty/rating/hash/controllers/threshold.py index 4f3030d2..390dc066 100644 --- a/cloudkitty/rating/hash/controllers/threshold.py +++ b/cloudkitty/rating/hash/controllers/threshold.py @@ -16,16 +16,16 @@ # @author: Stéphane Albert # import pecan -from pecan import rest import wsmeext.pecan as wsme_pecan from cloudkitty.api.v1 import types as ck_types +from cloudkitty import rating from cloudkitty.rating.hash.datamodels import group as group_models from cloudkitty.rating.hash.datamodels import threshold as threshold_models from cloudkitty.rating.hash.db import api as db_api -class HashMapThresholdsController(rest.RestController): +class HashMapThresholdsController(rating.RatingRestControllerBase): """Controller responsible of thresholds management. """ diff --git a/etc/cloudkitty/policy.json b/etc/cloudkitty/policy.json index 155cfdc3..4b77deac 100644 --- a/etc/cloudkitty/policy.json +++ b/etc/cloudkitty/policy.json @@ -1,3 +1,21 @@ { - "context_is_admin": "role:admin" + "context_is_admin": "role:admin", + "default": "", + + "rating:list_modules": "role:admin", + "rating:get_module": "role:admin", + "rating:update_module": "role:admin", + "rating:quote": "", + + "report:list_tenants": "role:admin", + "report:get_total": "", + + "collector:list_mappings": "role:admin", + "collector:get_mapping": "role:admin", + "collector:get_state": "role:admin", + "collector:update_state": "role:admin", + + "storage:list_data_frames": "", + + "rating:module_config": "role:admin" }