From 83f97e4c41001a3e48dec46b438f790b5caba952 Mon Sep 17 00:00:00 2001 From: chenying Date: Tue, 26 Sep 2017 09:42:05 +0800 Subject: [PATCH] Add default policy in code for the plan resource This adds the basic framework for registering and using default policy rules. Rules should be defined and returned from a module in karbor/policies/, and then added to the list in karbor/policies/__init__.py. The sample file about default policy will be generated as yaml using cmd 'tox -e genpolicy' in this patch. A new context.can() method has been added for policy enforcement of registered rules. It has the same parameters as the enforce() method currently being used. The patch add default policy in code for plan resource in karbor. Partial-Implements: blueprint policy-in-code Change-Id: I88ce31ee7cff9263055cfb51f6b5da5c333c50f2 --- etc/README-policy.yaml.txt | 4 + etc/karbor-policy-generator.conf | 3 + etc/policy.json | 12 -- karbor/api/v1/plans.py | 31 +--- karbor/context.py | 40 ++++- karbor/policies/__init__.py | 25 +++ karbor/policies/base.py | 34 ++++ karbor/policies/plans.py | 82 ++++++++++ karbor/policy.py | 146 +++++++++++++++--- karbor/tests/base.py | 16 ++ karbor/tests/unit/api/v1/test_plans.py | 7 +- .../unit/api/v1/test_scheduled_operation.py | 4 +- karbor/tests/unit/test_policy.py | 131 ++++++++++++++++ karbor/utils.py | 17 ++ setup.cfg | 8 + tox.ini | 3 + 16 files changed, 503 insertions(+), 60 deletions(-) create mode 100644 etc/README-policy.yaml.txt create mode 100644 etc/karbor-policy-generator.conf create mode 100644 karbor/policies/__init__.py create mode 100644 karbor/policies/base.py create mode 100644 karbor/policies/plans.py create mode 100644 karbor/tests/unit/test_policy.py 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 =