diff --git a/etc/README-policy.yaml.txt b/etc/README-policy.yaml.txt new file mode 100644 index 00000000..f34c3a37 --- /dev/null +++ b/etc/README-policy.yaml.txt @@ -0,0 +1,4 @@ +To generate the sample policy.yaml file, run the following command from the top +level of the karbor directory: + + tox -egenpolicy diff --git a/etc/karbor-policy-generator.conf b/etc/karbor-policy-generator.conf new file mode 100644 index 00000000..821dcc37 --- /dev/null +++ b/etc/karbor-policy-generator.conf @@ -0,0 +1,3 @@ +[DEFAULT] +output_file = etc/policy.yaml.sample +namespace = karbor diff --git a/etc/policy.json b/etc/policy.json index 79ff71f9..3bb2a241 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -1,16 +1,4 @@ { - "context_is_admin": "role:admin", - "admin_or_owner": "is_admin:True or project_id:%(project_id)s", - "default": "rule:admin_or_owner", - - "admin_api": "is_admin:True", - - "plan:create": "rule:admin_or_owner", - "plan:update": "rule:admin_or_owner", - "plan:delete": "rule:admin_or_owner", - "plan:get": "rule:admin_or_owner", - "plan:get_all": "rule:admin_or_owner", - "restore:create": "rule:admin_or_owner", "restore:update": "rule:admin_or_owner", "restore:get": "rule:admin_or_owner", diff --git a/karbor/api/v1/plans.py b/karbor/api/v1/plans.py index f08789b0..14a32763 100644 --- a/karbor/api/v1/plans.py +++ b/karbor/api/v1/plans.py @@ -19,7 +19,6 @@ from oslo_utils import uuidutils from webob import exc -import karbor from karbor.api import common from karbor.api.openstack import wsgi from karbor.common import constants @@ -28,7 +27,7 @@ from karbor.i18n import _ from karbor import objects from karbor.objects import base as objects_base -import karbor.policy +from karbor.policies import plans as plan_policy from karbor.services.operationengine import api as operationengine_api from karbor.services.protection import api as protection_api from karbor import utils @@ -49,23 +48,6 @@ CONF.register_opt(query_plan_filters_opt) LOG = logging.getLogger(__name__) -def check_policy(context, action, target_obj=None): - target = { - 'project_id': context.project_id, - 'user_id': context.user_id, - } - - if isinstance(target_obj, objects_base.KarborObject): - # Turn object into dict so target.update can work - target.update( - target_obj.obj_to_primitive() or {}) - else: - target.update(target_obj or {}) - - _action = 'plan:%s' % action - karbor.policy.enforce(context, _action, target) - - class PlanViewBuilder(common.ViewBuilder): """Model a server API response as a python dictionary.""" @@ -170,7 +152,7 @@ class PlansController(wsgi.Controller): except exception.PlanNotFound as error: raise exc.HTTPNotFound(explanation=error.msg) - check_policy(context, 'delete', plan) + context.can(plan_policy.DELETE_POLICY, target_obj=plan) plan.destroy() LOG.info("Delete plan request issued successfully.", resource={'id': plan.id}) @@ -205,7 +187,7 @@ class PlansController(wsgi.Controller): def _get_all(self, context, marker=None, limit=None, sort_keys=None, sort_dirs=None, filters=None, offset=None): - check_policy(context, 'get_all') + context.can(plan_policy.GET_ALL_POLICY) if filters is None: filters = {} @@ -253,7 +235,7 @@ class PlansController(wsgi.Controller): LOG.debug('Create plan request body: %s', body) context = req.environ['karbor.context'] - check_policy(context, 'create') + context.can(plan_policy.CREATE_POLICY) plan = body['plan'] LOG.debug('Create plan request plan: %s', plan) @@ -347,8 +329,7 @@ class PlansController(wsgi.Controller): plan = self._plan_get(context, id) except exception.PlanNotFound as error: raise exc.HTTPNotFound(explanation=error.msg) - - check_policy(context, 'update', plan) + context.can(plan_policy.UPDATE_POLICY, target_obj=plan) self._plan_update(context, plan, update_dict) plan.update(update_dict) @@ -363,7 +344,7 @@ class PlansController(wsgi.Controller): plan = objects.Plan.get_by_id(context, plan_id) try: - check_policy(context, 'get', plan) + context.can(plan_policy.GET_POLICY, target_obj=plan) except exception.PolicyNotAuthorized: # raise PlanNotFound instead to make sure karbor behaves # as it used to diff --git a/karbor/context.py b/karbor/context.py index 0711c7d5..9adc9002 100644 --- a/karbor/context.py +++ b/karbor/context.py @@ -21,7 +21,9 @@ from oslo_context import context from oslo_utils import timeutils import six +from karbor import exception from karbor.i18n import _ +from karbor.objects import base as objects_base from karbor import policy CONF = cfg.CONF @@ -85,7 +87,7 @@ class RequestContext(context.RequestContext): # when policy.check_is_admin invokes request logging # to make it loggable. if self.is_admin is None: - self.is_admin = policy.check_is_admin(self.roles, self) + self.is_admin = policy.check_is_admin(self) elif self.is_admin and 'admin' not in self.roles: self.roles.append('admin') @@ -143,6 +145,42 @@ class RequestContext(context.RequestContext): kwargs = {k: values[k] for k in values if k in allowed_keys} return cls(**kwargs) + def can(self, action, target_obj=None, fatal=True): + """Verifies that the given action is valid on the target in this context. + + :param action: string representing the action to be checked. + :param target: dictionary representing the object of the action + for object creation this should be a dictionary representing the + location of the object e.g. ``{'project_id': context.project_id}``. + If None, then this default target will be considered: + {'project_id': self.project_id, 'user_id': self.user_id} + :param: target_obj: dictionary representing the object which will be + used to update target. + :param fatal: if False, will return False when an + exception.NotAuthorized occurs. + + :raises nova.exception.Forbidden: if verification fails and fatal is + True. + + :return: returns a non-False value (not necessarily "True") if + authorized and False if not authorized and fatal is False. + """ + target = {'project_id': self.project_id, + 'user_id': self.user_id} + if isinstance(target_obj, objects_base.KarborObject): + # Turn object into dict so target.update can work + target.update( + target_obj.obj_to_primitive()['karbor_object.data'] or {}) + else: + target.update(target_obj or {}) + + try: + return policy.authorize(self, action, target) + except exception.NotAuthorized: + if fatal: + raise + return False + def to_policy_values(self): policy = super(RequestContext, self).to_policy_values() diff --git a/karbor/policies/__init__.py b/karbor/policies/__init__.py new file mode 100644 index 00000000..0c970e9e --- /dev/null +++ b/karbor/policies/__init__.py @@ -0,0 +1,25 @@ +# 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 karbor.policies import base +from karbor.policies import plans + + +def list_rules(): + return itertools.chain( + base.list_rules(), + plans.list_rules() + ) diff --git a/karbor/policies/base.py b/karbor/policies/base.py new file mode 100644 index 00000000..8a8ba324 --- /dev/null +++ b/karbor/policies/base.py @@ -0,0 +1,34 @@ +# 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' +RULE_ADMIN_API = 'rule:admin_api' + +rules = [ + policy.RuleDefault('context_is_admin', 'role:admin'), + policy.RuleDefault('admin_or_owner', + 'is_admin:True or (role:admin and ' + 'is_admin_project:True) or project_id:%(project_id)s'), + policy.RuleDefault('default', + 'rule:admin_or_owner'), + policy.RuleDefault('admin_api', + 'is_admin:True or (role:admin and ' + 'is_admin_project:True)'), +] + + +def list_rules(): + return rules diff --git a/karbor/policies/plans.py b/karbor/policies/plans.py new file mode 100644 index 00000000..bba7c795 --- /dev/null +++ b/karbor/policies/plans.py @@ -0,0 +1,82 @@ +# Copyright (c) 2017 Huawei Technologies Co., Ltd. +# 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 karbor.policies import base + + +CREATE_POLICY = 'plan:create' +UPDATE_POLICY = 'plan:update' +DELETE_POLICY = 'plan:delete' +GET_POLICY = 'plan:get' +GET_ALL_POLICY = 'plan:get_all' + +plans_policies = [ + policy.DocumentedRuleDefault( + name=CREATE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="""Create a plan.""", + operations=[ + { + 'method': 'POST', + 'path': '/plans' + } + ]), + policy.DocumentedRuleDefault( + name=UPDATE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="""Update a plan.""", + operations=[ + { + 'method': 'PUT', + 'path': '/plans/{plan_id}' + } + ]), + policy.DocumentedRuleDefault( + name=DELETE_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="""Delete a plan.""", + operations=[ + { + 'method': 'DELETE', + 'path': '/plans/{plan_id}' + } + ]), + policy.DocumentedRuleDefault( + name=GET_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="""Get a plan.""", + operations=[ + { + 'method': 'GET', + 'path': '/plans/{plan_id}' + } + ]), + policy.DocumentedRuleDefault( + name=GET_ALL_POLICY, + check_str=base.RULE_ADMIN_OR_OWNER, + description="""Get plans.""", + operations=[ + { + 'method': 'GET', + 'path': '/plans' + } + ]), +] + + +def list_rules(): + return plans_policies diff --git a/karbor/policy.py b/karbor/policy.py index a050999c..7fbdfe77 100644 --- a/karbor/policy.py +++ b/karbor/policy.py @@ -13,25 +13,54 @@ # License for the specific language governing permissions and limitations # under the License. -"""Policy Engine For karbor""" +"""Policy Engine For Karbor""" +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 from karbor import exception +from karbor import policies CONF = cfg.CONF +LOG = logging.getLogger(__name__) policy_opts.set_defaults(cfg.CONF, 'policy.json') _ENFORCER = None -def init(): +def reset(): + global _ENFORCER + if _ENFORCER: + _ENFORCER.clear() + _ENFORCER = None + + +def init(policy_file=None, rules=None, default_rule=None, use_conf=True): + """Init an Enforcer class. + + :param policy_file: Custom policy file to use, if none is specified, + `CONF.policy_file` will be used. + :param rules: Default dictionary / Rules to use. It will be + considered just in the first instantiation. + :param default_rule: Default rule to use, CONF.default_rule will + be used if none is specified. + :param use_conf: Whether to load rules from config file. + """ + global _ENFORCER if not _ENFORCER: - _ENFORCER = policy.Enforcer(CONF) + _ENFORCER = policy.Enforcer(CONF, + policy_file=policy_file, + rules=rules, + default_rule=default_rule, + use_conf=use_conf) + register_rules(_ENFORCER) + _ENFORCER.load_rules() def enforce_action(context, action): @@ -55,9 +84,9 @@ def enforce(context, action, target): ``compute:attach_volume``, ``volume:attach_volume`` - :param target: dictionary representing the target of the action - for target creation this should be a dictionary representing the - location of the target e.g. ``{'project_id': context.project_id}`` + :param target: dictionary representing the object of the action + for object creation this should be a dictionary representing the + location of the object e.g. ``{'project_id': context.project_id}`` :raises PolicyNotAuthorized: if verification fails. @@ -72,19 +101,100 @@ def enforce(context, action, target): action=action) -def check_is_admin(roles, context=None): +def set_rules(rules, overwrite=True, use_conf=False): + """Set rules based on the provided dict of rules. + + :param rules: New rules to use. It should be an instance of dict. + :param overwrite: Whether to overwrite current rules or update them + with the new rules. + :param use_conf: Whether to reload rules from config file. + """ + + init(use_conf=False) + _ENFORCER.set_rules(rules, overwrite, use_conf) + + +def get_rules(): + if _ENFORCER: + return _ENFORCER.rules + + +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 Karbor 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='karbor') + init() + return _ENFORCER + + +def authorize(context, action, target, do_raise=True, exc=None): + """Verifies that the action is valid on the target in this context. + + :param context: karbor context + :param action: string representing the action to be checked + this should be colon separated for clarity. + i.e. ``compute:create_instance``, + ``plan:create``, + ``plan:get`` + :param target: dictionary representing the object of the action + for object creation this should be a dictionary representing the + location of the object e.g. ``{'project_id': context.project_id}`` + :param do_raise: if True (the default), raises PolicyNotAuthorized; + if False, returns False + :param exc: Class of the exception to raise if the check fails. + Any remaining arguments passed to :meth:`authorize` (both + positional and keyword arguments) will be passed to + the exception class. If not specified, + :class:`PolicyNotAuthorized` will be used. + + :raises karbor.exception.PolicyNotAuthorized: if verification fails + and do_raise is True. Or if 'exc' is specified it will raise an + exception of that type. + + :return: returns a non-False value (not necessarily "True") if + authorized, and the exact value False if not authorized and + do_raise is False. + """ + init() + credentials = context.to_policy_values() + if not exc: + exc = exception.PolicyNotAuthorized + try: + result = _ENFORCER.authorize(action, target, credentials, + do_raise=do_raise, exc=exc, 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.debug('Policy check for %(action)s failed with credentials ' + '%(credentials)s', + {'action': action, 'credentials': credentials}) + return result + + +def check_is_admin(context): """Whether or not user is admin according to policy setting. """ init() - - # include project_id on target to avoid KeyError if context_is_admin - # policy definition is missing, and default admin_or_owner rule - # attempts to apply. - target = {'project_id': ''} - if context is None: - credentials = {'roles': roles} - else: - credentials = context.to_dict() - - return _ENFORCER.enforce('context_is_admin', target, credentials) + # the target is user-self + credentials = context.to_policy_values() + target = credentials + return _ENFORCER.authorize('context_is_admin', target, credentials) diff --git a/karbor/tests/base.py b/karbor/tests/base.py index 5715e092..ad795721 100644 --- a/karbor/tests/base.py +++ b/karbor/tests/base.py @@ -14,6 +14,7 @@ import logging import os import fixtures +import mock from oslo_config import cfg from oslo_messaging import conffixture as messaging_conffixture from oslo_utils import timeutils @@ -73,6 +74,7 @@ class TestCase(base.BaseTestCase): self.messaging_conf.transport_driver = 'fake' self.messaging_conf.response_timeout = 15 self.useFixture(self.messaging_conf) + rpc.init(CONF) conf_fixture.set_defaults(CONF) @@ -112,3 +114,17 @@ class TestCase(base.BaseTestCase): """Override CONF variables for a test.""" for k, v in kw.items(): self.override_config(k, v) + + def mock_object(self, obj, attr_name, new_attr=None, **kwargs): + """Use python mock to mock an object attribute + + Mocks the specified objects attribute with the given value. + Automatically performs 'addCleanup' for the mock. + + """ + if not new_attr: + new_attr = mock.Mock() + patcher = mock.patch.object(obj, attr_name, new_attr, **kwargs) + patcher.start() + self.addCleanup(patcher.stop) + return new_attr diff --git a/karbor/tests/unit/api/v1/test_plans.py b/karbor/tests/unit/api/v1/test_plans.py index df38b1e0..d0eefc3d 100644 --- a/karbor/tests/unit/api/v1/test_plans.py +++ b/karbor/tests/unit/api/v1/test_plans.py @@ -38,6 +38,8 @@ class PlanApiTest(base.TestCase): super(PlanApiTest, self).setUp() self.controller = plans.PlansController() self.ctxt = context.RequestContext('demo', 'fakeproject', True) + self.mock_policy_check = self.mock_object( + context.RequestContext, 'can') @mock.patch( 'karbor.services.protection.rpcapi.ProtectionAPI.show_provider') @@ -50,6 +52,7 @@ class PlanApiTest(base.TestCase): mock_provider.return_value = fakes.PROVIDER_OS self.controller.create(req, body) self.assertTrue(mock_plan_create.called) + self.assertTrue(self.mock_policy_check.called) def test_plan_create_InvalidBody(self): plan = self._plan_in_request_body() @@ -206,12 +209,10 @@ class PlanApiTest(base.TestCase): exc.HTTPBadRequest, self.controller.delete, req, "1") - @mock.patch( - 'karbor.api.v1.plans.check_policy') @mock.patch( 'karbor.api.v1.plans.PlansController._plan_get') def test_plan_update_InvalidStatus( - self, mock_plan_get, mock_check_policy): + self, mock_plan_get): plan = self._plan_in_request_body( name=DEFAULT_NAME, description=DEFAULT_DESCRIPTION, diff --git a/karbor/tests/unit/api/v1/test_scheduled_operation.py b/karbor/tests/unit/api/v1/test_scheduled_operation.py index 57639c2e..70744c6e 100644 --- a/karbor/tests/unit/api/v1/test_scheduled_operation.py +++ b/karbor/tests/unit/api/v1/test_scheduled_operation.py @@ -187,9 +187,11 @@ class ScheduledOperationApiTest(base.TestCase): req = fakes.HTTPRequest.blank('/v1/triggers') return controller.create(req, create_trigger_param) + @mock.patch( + 'karbor.context.RequestContext.can') @mock.patch( 'karbor.services.protection.rpcapi.ProtectionAPI.show_provider') - def _create_plan(self, provider_id, mock_provider): + def _create_plan(self, provider_id, mock_provider, mock_policy): create_plan_param = { 'plan': { 'name': '123', diff --git a/karbor/tests/unit/test_policy.py b/karbor/tests/unit/test_policy.py new file mode 100644 index 00000000..1bc05360 --- /dev/null +++ b/karbor/tests/unit/test_policy.py @@ -0,0 +1,131 @@ +# Copyright (c) 2017 Huawei Technologies Co., Ltd. +# 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_policy import policy as oslo_policy + +from karbor import context +from karbor import exception +from karbor.tests import base +from karbor import utils + +from karbor import policy + +CONF = cfg.CONF + + +class PolicyFileTestCase(base.TestCase): + + def setUp(self): + super(PolicyFileTestCase, self).setUp() + self.context = context.get_admin_context() + self.target = {} + self.fixture = self.useFixture(config_fixture.Config(CONF)) + self.addCleanup(policy.reset) + + 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') + policy.reset() + policy.init() + rule = oslo_policy.RuleDefault('example:test', "") + 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(exception.PolicyNotAuthorized, + policy.authorize, + self.context, action, self.target) + + +class PolicyTestCase(base.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:my_file", + "role:compute_admin or " + "project_id:%(project_id)s"), + 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"), + ] + policy.reset() + policy.init() + # before a policy rule can be used, its default has to be registered. + policy._ENFORCER.register_defaults(rules) + self.context = context.RequestContext('fake', 'fake', roles=['member']) + self.target = {} + self.addCleanup(policy.reset) + + def test_authorize_nonexistent_action_throws(self): + action = "test:noexist" + self.assertRaises(oslo_policy.PolicyNotRegistered, policy.authorize, + self.context, action, self.target) + + def test_authorize_bad_action_throws(self): + action = "test:denied" + self.assertRaises(exception.PolicyNotAuthorized, policy.authorize, + self.context, action, self.target) + + def test_authorize_bad_action_noraise(self): + action = "test:denied" + result = policy.authorize(self.context, action, self.target, False) + self.assertFalse(result) + + def test_authorize_good_action(self): + action = "test:allowed" + result = policy.authorize(self.context, action, self.target) + self.assertTrue(result) + + def test_templatized_authorization(self): + target_mine = {'project_id': 'fake'} + target_not_mine = {'project_id': 'another'} + action = "test:my_file" + policy.authorize(self.context, action, target_mine) + self.assertRaises(exception.PolicyNotAuthorized, policy.authorize, + self.context, action, target_not_mine) + + def test_early_AND_authorization(self): + action = "test:early_and_fail" + self.assertRaises(exception.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) diff --git a/karbor/utils.py b/karbor/utils.py index 320e6c94..7a5a9faf 100644 --- a/karbor/utils.py +++ b/karbor/utils.py @@ -12,7 +12,11 @@ """Utilities and helper functions.""" import ast +import contextlib import os +import shutil +import six +import tempfile import webob.exc from keystoneclient import discover as ks_discover @@ -175,3 +179,16 @@ def walk_class_hierarchy(clazz, encountered=None): for subsubclass in walk_class_hierarchy(subclass, encountered): yield subsubclass yield subclass + + +@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)) diff --git a/setup.cfg b/setup.cfg index cacc36eb..4c76e418 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,14 @@ console_scripts = karbor-protection = karbor.cmd.protection:main oslo.config.opts = karbor.common.opts = karbor.common.opts:list_opts +oslo.policy.enforcer = + karbor = karbor.policy:get_enforcer +oslo.policy.policies = + # The sample policies will be ordered by entry point and then by list + # returned from that entry point. If more control is desired split out each + # list_rules method into a separate entry point rather than using the + # aggregate method. + karbor = karbor.policies:list_rules wsgi_scripts = karbor-wsgi = karbor.wsgi.wsgi:initialize_application karbor.database.migration_backend = diff --git a/tox.ini b/tox.ini index 1716010e..756b394b 100644 --- a/tox.ini +++ b/tox.ini @@ -58,6 +58,9 @@ commands = sphinx-build -a -E -d releasenotes/build/doctrees -b html releasenote [testenv:genconfig] commands = oslo-config-generator --config-file etc/oslo-config-generator/karbor.conf +[testenv:genpolicy] +commands = oslopolicy-sample-generator --config-file=etc/karbor-policy-generator.conf + [flake8] show-source = True ignore =