Policy in code

This patch introduces the implementation for registering
default policy rules in code. Default rules are defined under
cloudkitty.common.policies. Each API's policies are defined in a
sub-folder under that path and __init__.py contains all the
default policies in code which are registered in the ``init``
enforcer function in cloudkitty/common/policy.py.

This commit does the following:
 - Creates the ``policies`` module that contains all the default
   policies in code.
 - Adds the base policy rules into code (context_is_admin,
   admin_or_owner and default rules).
 - Add policies in code for current APIs
 - Add a tox env to generate default policy sample file
 - Delete policy.json from repo as policies in code will be used.

Change-Id: I257e8cefc2b699fc979c717531cd9ba77233d94b
Implements: blueprint policy-in-code
This commit is contained in:
Jeremy Liu 2017-09-05 17:52:45 +08:00 committed by Martin CAMEY
parent 43e1999e9d
commit 7eca672645
27 changed files with 611 additions and 57 deletions

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ cloudkitty.egg-info
# Configuration file
etc/cloudkitty/cloudkitty.conf.sample
etc/cloudkitty/policy.yaml.sample
# Installer logs
pip-log.txt

View File

@ -39,7 +39,7 @@ class MappingController(rest.RestController):
:param service: Name of the service to filter on.
"""
policy.enforce(pecan.request.context, 'collector:get_mapping', {})
policy.authorize(pecan.request.context, 'collector:get_mapping', {})
try:
mapping = self._db.get_mapping(service)
return collector_models.ServiceToCollectorMapping(
@ -55,7 +55,7 @@ class MappingController(rest.RestController):
:param collector: Filter on the collector name.
:return: Service to collector mappings collection.
"""
policy.enforce(pecan.request.context, 'collector:list_mappings', {})
policy.authorize(pecan.request.context, 'collector:list_mappings', {})
mappings = [collector_models.ServiceToCollectorMapping(
**mapping.as_dict())
for mapping in self._db.list_mappings(collector)]
@ -71,7 +71,7 @@ class MappingController(rest.RestController):
:param collector: Name of the collector to apply mapping on.
:param service: Name of the service to apply mapping on.
"""
policy.enforce(pecan.request.context, 'collector:manage_mapping', {})
policy.authorize(pecan.request.context, 'collector:manage_mapping', {})
new_mapping = self._db.set_mapping(service, collector)
return collector_models.ServiceToCollectorMapping(
service=new_mapping.service,
@ -85,7 +85,7 @@ class MappingController(rest.RestController):
:param service: Name of the service to filter on.
"""
policy.enforce(pecan.request.context, 'collector:manage_mapping', {})
policy.authorize(pecan.request.context, 'collector:manage_mapping', {})
try:
self._db.delete_mapping(service)
except db_api.NoSuchMapping as e:
@ -105,7 +105,7 @@ class CollectorStateController(rest.RestController):
:param name: Name of the collector.
:return: State of the collector.
"""
policy.enforce(pecan.request.context, 'collector:get_state', {})
policy.authorize(pecan.request.context, 'collector:get_state', {})
enabled = self._db.get_state('collector_{}'.format(name))
collector = collector_models.CollectorInfos(name=name,
enabled=enabled)
@ -121,7 +121,7 @@ class CollectorStateController(rest.RestController):
:param infos: New state informations of the collector.
:return: State of the collector.
"""
policy.enforce(pecan.request.context, 'collector:update_state', {})
policy.authorize(pecan.request.context, 'collector:update_state', {})
enabled = self._db.set_state('collector_{}'.format(name),
infos.enabled)
collector = collector_models.CollectorInfos(name=name,

View File

@ -44,7 +44,7 @@ class ServiceInfoController(rest.RestController):
:return: List of every services.
"""
policy.enforce(pecan.request.context, 'info:list_services_info', {})
policy.authorize(pecan.request.context, 'info:list_services_info', {})
services_info_list = []
for service, metadata in METADATA.items():
info = metadata.copy()
@ -60,7 +60,7 @@ class ServiceInfoController(rest.RestController):
:param service_name: name of the service.
"""
policy.enforce(pecan.request.context, 'info:get_service_info', {})
policy.authorize(pecan.request.context, 'info:get_service_info', {})
try:
info = METADATA[service_name].copy()
info['service_id'] = service_name
@ -81,7 +81,7 @@ class InfoController(rest.RestController):
})
def config(self):
"""Return current configuration."""
policy.enforce(pecan.request.context, 'info:get_config', {})
policy.authorize(pecan.request.context, 'info:get_config', {})
info = {}
info["collect"] = ck_utils.get_metrics_conf(CONF.collect.metrics_conf)
return info

View File

@ -63,7 +63,7 @@ class ModulesController(rest.RestController, RatingModulesMixin):
def route(self, *args):
route = args[0]
if route.startswith('/v1/module_config'):
policy.enforce(pecan.request.context, 'rating:module_config', {})
policy.authorize(pecan.request.context, 'rating:module_config', {})
super(ModulesController, self).route(*args)
@ -73,7 +73,7 @@ class ModulesController(rest.RestController, RatingModulesMixin):
:return: name of every loaded modules.
"""
policy.enforce(pecan.request.context, 'rating:list_modules', {})
policy.authorize(pecan.request.context, 'rating:list_modules', {})
modules_list = []
lock = lockutils.lock('rating-modules')
@ -92,7 +92,7 @@ class ModulesController(rest.RestController, RatingModulesMixin):
:return: CloudKittyModule
"""
policy.enforce(pecan.request.context, 'rating:get_module', {})
policy.authorize(pecan.request.context, 'rating:get_module', {})
try:
lock = lockutils.lock('rating-modules')
@ -114,7 +114,7 @@ class ModulesController(rest.RestController, RatingModulesMixin):
: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', {})
policy.authorize(pecan.request.context, 'rating:update_module', {})
try:
lock = lockutils.lock('rating-modules')
@ -194,7 +194,7 @@ class RatingController(rest.RestController):
:param res_data: List of resource descriptions.
:return: Total price for these descriptions.
"""
policy.enforce(pecan.request.context, 'rating:quote', {})
policy.authorize(pecan.request.context, 'rating:quote', {})
client = pecan.request.rpc_client.prepare(namespace='rating')
res_dict = {}
@ -212,7 +212,7 @@ class RatingController(rest.RestController):
"""Trigger a rating module list reload.
"""
policy.enforce(pecan.request.context, 'rating:module_config', {})
policy.authorize(pecan.request.context, 'rating:module_config', {})
self.modules.reload_extensions()
self.module_config.reload_extensions()
self.module_config.expose_modules()

View File

@ -46,7 +46,7 @@ class ReportController(rest.RestController):
"""Return the list of rated tenants.
"""
policy.enforce(pecan.request.context, 'report:list_tenants', {})
policy.authorize(pecan.request.context, 'report:list_tenants', {})
if not begin:
begin = ck_utils.get_month_start()
@ -78,7 +78,7 @@ class ReportController(rest.RestController):
else:
tenant_context = pecan.request.context.tenant
tenant_id = tenant_context if not tenant_id else tenant_id
policy.enforce(pecan.request.context, 'report:get_total',
policy.authorize(pecan.request.context, 'report:get_total',
{"tenant_id": tenant_id})
storage = pecan.request.storage_backend
@ -114,7 +114,7 @@ class ReportController(rest.RestController):
else:
tenant_context = pecan.request.context.tenant
tenant_id = tenant_context if not tenant_id else tenant_id
policy.enforce(pecan.request.context, 'report:get_summary',
policy.authorize(pecan.request.context, 'report:get_summary',
{"tenant_id": tenant_id})
storage = pecan.request.storage_backend

View File

@ -48,7 +48,7 @@ class DataFramesController(rest.RestController):
:return: Collection of DataFrame objects.
"""
policy.enforce(pecan.request.context, 'storage:list_data_frames', {})
policy.authorize(pecan.request.context, 'storage:list_data_frames', {})
if not begin:
begin = ck_utils.get_month_start()

View File

@ -0,0 +1,34 @@
# Copyright 2017 GohighSec.
# All Rights Reserved.
#
# 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.
import itertools
from cloudkitty.common.policies import base
from cloudkitty.common.policies import collector
from cloudkitty.common.policies import info
from cloudkitty.common.policies import rating
from cloudkitty.common.policies import report
from cloudkitty.common.policies import storage
def list_rules():
return itertools.chain(
base.list_rules(),
collector.list_rules(),
info.list_rules(),
rating.list_rules(),
report.list_rules(),
storage.list_rules()
)

View File

@ -0,0 +1,36 @@
# Copyright 2017 GohighSec.
# All Rights Reserved.
#
# 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_policy import policy
RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner'
ROLE_ADMIN = 'role:admin'
UNPROTECTED = ''
rules = [
policy.RuleDefault(
name='context_is_admin',
check_str='role:admin'),
policy.RuleDefault(
name='admin_or_owner',
check_str='is_admin:True or tenant:%(tenant_id)s'),
policy.RuleDefault(
name='default',
check_str=UNPROTECTED)
]
def list_rules():
return rules

View File

@ -0,0 +1,57 @@
# Copyright 2017 GohighSec.
# All Rights Reserved.
#
# 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_policy import policy
from cloudkitty.common.policies import base
collector_policies = [
policy.DocumentedRuleDefault(
name='collector:list_mappings',
check_str=base.ROLE_ADMIN,
description='Return the list of every services mapped to a collector.',
operations=[{'path': '/v1/collector/mappings',
'method': 'LIST'}]),
policy.DocumentedRuleDefault(
name='collector:get_mapping',
check_str=base.ROLE_ADMIN,
description='Return a service to collector mapping.',
operations=[{'path': '/v1/collector/mappings/{service_id}',
'method': 'GET'}]),
policy.DocumentedRuleDefault(
name='collector:manage_mapping',
check_str=base.ROLE_ADMIN,
description='Manage a service to collector mapping.',
operations=[{'path': '/v1/collector/mappings',
'method': 'POST'},
{'path': '/v1/collector/mappings/{service_id}',
'method': 'DELETE'}]),
policy.DocumentedRuleDefault(
name='collector:get_state',
check_str=base.ROLE_ADMIN,
description='Query the enable state of a collector.',
operations=[{'path': '/v1/collector/states/{collector_id}',
'method': 'GET'}]),
policy.DocumentedRuleDefault(
name='collector:update_state',
check_str=base.ROLE_ADMIN,
description='Set the enable state of a collector.',
operations=[{'path': '/v1/collector/states/{collector_id}',
'method': 'PUT'}])
]
def list_rules():
return collector_policies

View File

@ -0,0 +1,43 @@
# Copyright 2017 GohighSec.
# All Rights Reserved.
#
# 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_policy import policy
from cloudkitty.common.policies import base
info_policies = [
policy.DocumentedRuleDefault(
name='info:list_services_info',
check_str=base.UNPROTECTED,
description='List available services information in Cloudkitty.',
operations=[{'path': '/v1/info/services',
'method': 'LIST'}]),
policy.DocumentedRuleDefault(
name='info:get_service_info',
check_str=base.UNPROTECTED,
description='Get specified service information.',
operations=[{'path': '/v1/info/services/{service_id}',
'method': 'GET'}]),
policy.DocumentedRuleDefault(
name='info:get_config',
check_str=base.UNPROTECTED,
description='Get current configuration in Cloudkitty.',
operations=[{'path': '/v1/info/config',
'method': 'GET'}])
]
def list_rules():
return info_policies

View File

@ -0,0 +1,56 @@
# Copyright 2017 GohighSec.
# All Rights Reserved.
#
# 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_policy import policy
from cloudkitty.common.policies import base
rating_policies = [
policy.DocumentedRuleDefault(
name='rating:list_modules',
check_str=base.ROLE_ADMIN,
description='Reture the list of loaded modules in Cloudkitty.',
operations=[{'path': '/v1/rating/modules',
'method': 'LIST'}]),
policy.DocumentedRuleDefault(
name='rating:get_module',
check_str=base.ROLE_ADMIN,
description='Get specified module.',
operations=[{'path': '/v1/rating/modules/{module_id}',
'method': 'GET'}]),
policy.DocumentedRuleDefault(
name='rating:update_module',
check_str=base.ROLE_ADMIN,
description='Change the state and priority of a module.',
operations=[{'path': '/v1/rating/modules/{module_id}',
'method': 'PUT'}]),
policy.DocumentedRuleDefault(
name='rating:quote',
check_str=base.UNPROTECTED,
description='Get an instant quote based on multiple resource '
'descriptions.',
operations=[{'path': '/v1/rating/quote',
'method': 'POST'}]),
policy.DocumentedRuleDefault(
name='rating:module_config',
check_str=base.ROLE_ADMIN,
description='Trigger a rating module list reload.',
operations=[{'path': '/v1/rating/reload_modules',
'method': 'GET'}])
]
def list_rules():
return rating_policies

View File

@ -0,0 +1,43 @@
# Copyright 2017 GohighSec.
# All Rights Reserved.
#
# 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_policy import policy
from cloudkitty.common.policies import base
report_policies = [
policy.DocumentedRuleDefault(
name='report:list_tenants',
check_str=base.ROLE_ADMIN,
description='Return the list of rated tenants.',
operations=[{'path': '/v1/report/tenants',
'method': 'GET'}]),
policy.DocumentedRuleDefault(
name='report:get_summary',
check_str=base.RULE_ADMIN_OR_OWNER,
description='Return the summary to pay for a given period.',
operations=[{'path': '/v1/report/summary',
'method': 'GET'}]),
policy.DocumentedRuleDefault(
name='report:get_total',
check_str=base.RULE_ADMIN_OR_OWNER,
description='Return the amount to pay for a given period.',
operations=[{'path': '/v1/report/total',
'method': 'GET'}])
]
def list_rules():
return report_policies

View File

@ -0,0 +1,32 @@
# Copyright 2017 GohighSec.
# All Rights Reserved.
#
# 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_policy import policy
from cloudkitty.common.policies import base
storage_policies = [
policy.DocumentedRuleDefault(
name='storage:list_data_frames',
check_str=base.UNPROTECTED,
description='Return a list of rated resources for a time period '
'and a tenant.',
operations=[{'path': '/v1/storage/dataframes',
'method': 'GET'}])
]
def list_rules():
return storage_policies

View File

@ -15,13 +15,28 @@
# Borrowed from cinder (cinder/policy.py)
import copy
import sys
from oslo_config import cfg
from oslo_log import log as logging
from oslo_policy import opts as policy_opts
from oslo_policy import policy
from oslo_utils import excutils
import six
from cloudkitty.common import policies
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
policy_opts.set_defaults(cfg.CONF, 'policy.json')
_ENFORCER = None
# oslo_policy will read the policy configuration file again when the file
# is changed in runtime so the old policy rules will be saved to
# saved_file_rules and used to compare with new rules to determine the
# rules whether were updated.
saved_file_rules = []
# TODO(gpocentek): provide a proper parent class to handle such exceptions
@ -37,13 +52,37 @@ class PolicyNotAuthorized(Exception):
return six.text_type(self.msg)
def reset():
global _ENFORCER
if _ENFORCER:
_ENFORCER.clear()
_ENFORCER = None
def init():
global _ENFORCER
global saved_file_rules
if not _ENFORCER:
_ENFORCER = policy.Enforcer(CONF)
register_rules(_ENFORCER)
# Only the rules which are loaded from file may be changed.
current_file_rules = _ENFORCER.file_rules
current_file_rules = _serialize_rules(current_file_rules)
# Checks whether the rules are updated in the runtime
if saved_file_rules != current_file_rules:
saved_file_rules = copy.deepcopy(current_file_rules)
def enforce(context, action, target):
def _serialize_rules(rules):
"""Serialize all the Rule object as string."""
result = [(rule_name, str(rule))
for rule_name, rule in rules.items()]
return sorted(result, key=lambda rule: rule[0])
def authorize(context, action, target):
"""Verifies that the action is valid on the target in this context.
:param context: cloudkitty context
@ -65,11 +104,21 @@ def enforce(context, action, target):
init()
return _ENFORCER.enforce(action, target, context.to_dict(),
try:
return _ENFORCER.authorize(action, target, context.to_dict(),
do_raise=True,
exc=PolicyNotAuthorized,
action=action)
except policy.PolicyNotRegistered:
with excutils.save_and_reraise_exception():
LOG.exception('Policy not registered')
except Exception:
with excutils.save_and_reraise_exception():
LOG.error('Policy check for %(action)s failed with credentials '
'%(credentials)s',
{'action': action, 'credentials': context.to_dict()})
def check_is_admin(roles):
"""Whether or not roles contains 'admin' role according to policy setting.
@ -87,4 +136,28 @@ def check_is_admin(roles):
target = {'project_id': ''}
credentials = {'roles': roles}
return _ENFORCER.enforce('context_is_admin', target, credentials)
return _ENFORCER.authorize('context_is_admin', target, credentials)
def register_rules(enforcer):
enforcer.register_defaults(policies.list_rules())
def get_enforcer():
# This method is for use by oslopolicy CLI scripts. Those scripts need the
# 'output-file' and 'namespace' options, but having those in sys.argv means
# loading the Cloudkitty config options will fail as those are not expected
# to be present. So we pass in an arg list with those stripped out.
conf_args = []
# Start at 1 because cfg.CONF expects the equivalent of sys.argv[1:]
i = 1
while i < len(sys.argv):
if sys.argv[i].strip('-') in ['namespace', 'output-file']:
i += 2
continue
conf_args.append(sys.argv[i])
i += 1
cfg.CONF(conf_args, project='cloudkitty')
init()
return _ENFORCER

View File

@ -142,7 +142,7 @@ class RatingRestControllerBase(rest.RestController):
@pecan.expose()
def _route(self, args, request):
try:
policy.enforce(request.context, 'rating:module_config', {})
policy.authorize(request.context, 'rating:module_config', {})
except policy.PolicyNotAuthorized as e:
pecan.abort(403, six.text_type(e))

View File

@ -0,0 +1,122 @@
# Copyright (c) 2017 GohighSec.
# All Rights Reserved.
#
# 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.
import os.path
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_context import context
from oslo_policy import policy as oslo_policy
from cloudkitty.common import policy
from cloudkitty import tests
from cloudkitty import utils
CONF = cfg.CONF
class PolicyFileTestCase(tests.TestCase):
def setUp(self):
super(PolicyFileTestCase, self).setUp()
self.context = context.RequestContext('fake', 'fake', roles=['member'])
self.target = {}
self.fixture = self.useFixture(config_fixture.Config(CONF))
self.addCleanup(policy.reset)
CONF(args=[], project='cloudkitty', default_config_files=[])
def test_modified_policy_reloads(self):
with utils.tempdir() as tmpdir:
tmpfilename = os.path.join(tmpdir, 'policy')
self.fixture.config(policy_file=tmpfilename, group='oslo_policy')
rule = oslo_policy.RuleDefault('example:test', "")
policy.reset()
policy.init()
policy._ENFORCER.register_defaults([rule])
action = "example:test"
with open(tmpfilename, "w") as policyfile:
policyfile.write('{"example:test": ""}')
policy.authorize(self.context, action, self.target)
with open(tmpfilename, "w") as policyfile:
policyfile.write('{"example:test": "!"}')
policy._ENFORCER.load_rules(True)
self.assertRaises(policy.PolicyNotAuthorized,
policy.authorize,
self.context, action, self.target)
class PolicyTestCase(tests.TestCase):
def setUp(self):
super(PolicyTestCase, self).setUp()
rules = [
oslo_policy.RuleDefault("true", '@'),
oslo_policy.RuleDefault("test:allowed", '@'),
oslo_policy.RuleDefault("test:denied", "!"),
oslo_policy.RuleDefault("test:early_and_fail", "! and @"),
oslo_policy.RuleDefault("test:early_or_success", "@ or !"),
oslo_policy.RuleDefault("test:lowercase_admin",
"role:admin"),
oslo_policy.RuleDefault("test:uppercase_admin",
"role:ADMIN"),
]
CONF(args=[], project='cloudkitty', default_config_files=[])
# before a policy rule can be used, its default has to be registered.
policy.reset()
policy.init()
policy._ENFORCER.register_defaults(rules)
self.context = context.RequestContext('fake',
'fake',
roles=['member'])
self.target = {}
self.addCleanup(policy.reset)
def test_enforce_nonexistent_action_throws(self):
action = "test:noexist"
self.assertRaises(oslo_policy.PolicyNotRegistered, policy.authorize,
self.context, action, self.target)
def test_enforce_bad_action_throws(self):
action = "test:denied"
self.assertRaises(policy.PolicyNotAuthorized, policy.authorize,
self.context, action, self.target)
def test_enforce_bad_action_noraise(self):
action = "test:denied"
self.assertRaises(policy.PolicyNotAuthorized, policy.authorize,
self.context, action, self.target)
def test_enforce_good_action(self):
action = "test:allowed"
result = policy.authorize(self.context, action, self.target)
self.assertTrue(result)
def test_early_AND_authorization(self):
action = "test:early_and_fail"
self.assertRaises(policy.PolicyNotAuthorized, policy.authorize,
self.context, action, self.target)
def test_early_OR_authorization(self):
action = "test:early_or_success"
policy.authorize(self.context, action, self.target)
def test_ignore_case_role_check(self):
lowercase_action = "test:lowercase_admin"
uppercase_action = "test:uppercase_admin"
admin_context = context.RequestContext('admin',
'fake',
roles=['AdMiN'])
policy.authorize(admin_context, lowercase_action, self.target)
policy.authorize(admin_context, uppercase_action, self.target)

View File

@ -22,12 +22,17 @@ We're mostly using oslo_utils for time calculations but we're encapsulating it
to ease maintenance in case of library modifications.
"""
import calendar
import contextlib
import datetime
import shutil
import six
import sys
import tempfile
import yaml
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import timeutils
from six import moves
from stevedore import extension
@ -257,3 +262,16 @@ def get_metrics_conf(conf_path):
LOG.error(exc)
return res
@contextlib.contextmanager
def tempdir(**kwargs):
tmpdir = tempfile.mkdtemp(**kwargs)
try:
yield tmpdir
finally:
try:
shutil.rmtree(tmpdir)
except OSError as e:
LOG.debug('Could not remove tmpdir: %s',
six.text_type(e))

View File

@ -132,7 +132,11 @@ function configure_cloudkitty {
touch $CLOUDKITTY_CONF
cp $CLOUDKITTY_DIR$CLOUDKITTY_CONF_DIR/policy.json $CLOUDKITTY_CONF_DIR
# generate policy sample file
oslopolicy-sample-generator --config-file $CLOUDKITTY_DIR/etc/oslo-policy-generator/cloudkitty.conf --output-file $CLOUDKITTY_DIR/etc/cloudkitty/policy.yaml.sample
cp $CLOUDKITTY_DIR/etc/cloudkitty/policy.yaml.sample "$CLOUDKITTY_CONF_DIR/policy.yaml"
iniset $CLOUDKITTY_CONF oslo_policy policy_file 'policy.yaml'
cp $CLOUDKITTY_DIR$CLOUDKITTY_CONF_DIR/api_paste.ini $CLOUDKITTY_CONF_DIR
cp $CLOUDKITTY_DIR$CLOUDKITTY_CONF_DIR/metrics.yml $CLOUDKITTY_CONF_DIR
iniset_rpc_backend cloudkitty $CLOUDKITTY_CONF DEFAULT

View File

@ -57,6 +57,8 @@ extensions = ['sphinx.ext.coverage',
'sphinxcontrib.pecanwsme.rest',
'sphinxcontrib.httpdomain',
'openstackdocstheme',
'oslo_policy.sphinxext',
'oslo_policy.sphinxpolicygen',
]
# openstackdocstheme options
@ -65,6 +67,9 @@ bug_project = 'cloudkitty'
bug_tag = ''
html_last_updated_fmt = '%Y-%m-%d %H:%M'
policy_generator_config_file = '../../etc/oslo-policy-generator/cloudkitty.conf'
sample_policy_basename = '_static/cloudkitty'
# Add any paths that contain templates here, relative to this directory.
# templates_path = []

View File

@ -0,0 +1,12 @@
====================
Policy configuration
====================
Configuration
~~~~~~~~~~~~~
The following is an overview of all available policies in Cloudkitty. For a sample
configuration file, refer to :doc:`samples/policy-yaml`.
.. show-policy::
:config-file: ../../etc/oslo-policy-generator/cloudkitty.conf

View File

@ -0,0 +1,11 @@
==========================
Sample configuration files
==========================
Configuration files can alter how cloudkitty behaves at runtime and by default
are located in ``/etc/cloudkitty/``. Links to sample configuration files can be
found below:
.. toctree::
policy-yaml.rst

View File

@ -0,0 +1,8 @@
===========
policy.yaml
===========
Use the ``policy.yaml`` file to define additional access controls that apply to
the Rating service:
.. literalinclude:: ../../_static/policy.yaml.sample

View File

@ -0,0 +1,15 @@
========================
Cloudkitty Sample Policy
========================
The following is a sample Cloudkitty policy file that has been auto-generated
from default policy values in code. If you're using the default policies, then
the maintenance of this file is not necessary, and it should not be copied into
a deployment. Doing so will result in duplicate policy definitions. It is here
to help explain which policy operations protect specific Cloudkitty APIs, but it
is not suggested to copy and paste into a deployment unless you're planning on
providing a different policy for an operation that is not the default.
The sample policy file can also be viewed in `file form <_static/policy.yaml.sample>`_.
.. literalinclude:: _static/policy.yaml.sample

View File

@ -1,28 +0,0 @@
{
"context_is_admin": "role:admin",
"admin_or_owner": "is_admin:True or tenant:%(tenant_id)s",
"default": "",
"info:list_services_info": "",
"info:get_service_info": "",
"info:get_config":"",
"rating:list_modules": "role:admin",
"rating:get_module": "role:admin",
"rating:update_module": "role:admin",
"rating:quote": "",
"report:list_tenants": "role:admin",
"report:get_summary": "rule:admin_or_owner",
"report:get_total": "rule:admin_or_owner",
"collector:list_mappings": "role:admin",
"collector:get_mapping": "role:admin",
"collector:manage_mapping": "role:admin",
"collector:get_state": "role:admin",
"collector:update_state": "role:admin",
"storage:list_data_frames": "",
"rating:module_config": "role:admin"
}

View File

@ -0,0 +1,3 @@
[DEFAULT]
output_file = etc/cloudkitty/policy.yaml.sample
namespace = cloudkitty

View File

@ -32,6 +32,12 @@ console_scripts =
wsgi_scripts =
cloudkitty-api = cloudkitty.api.app:build_wsgi_app
oslo.policy.enforcer =
cloudkitty = cloudkitty.common.policy:get_enforcer
oslo.policy.policies =
cloudkitty = cloudkitty.common.policies:list_rules
oslo.config.opts =
cloudkitty.common.config = cloudkitty.common.config:list_opts

View File

@ -34,6 +34,9 @@ commands =
commands =
oslo-config-generator --config-file etc/oslo-config-generator/cloudkitty.conf
[testenv:genpolicy]
commands = oslopolicy-sample-generator --config-file=etc/oslo-policy-generator/cloudkitty.conf
[testenv:docs]
commands = python setup.py build_sphinx