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
This commit is contained in:
Gauvain Pocentek 2015-05-09 19:05:04 +02:00
parent f072724991
commit 0f21a37d06
14 changed files with 128 additions and 14 deletions

@ -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)

@ -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)

@ -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:

@ -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

@ -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

@ -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)

@ -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)

@ -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.
"""

@ -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.
"""

@ -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.
"""

@ -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.
"""

@ -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.
"""

@ -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.
"""

@ -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"
}