From b21c3d68a490041a8b8254c0f79a8be8940f36f6 Mon Sep 17 00:00:00 2001 From: zhongjun Date: Mon, 25 Sep 2017 18:41:24 +0800 Subject: [PATCH] [policy in code] Add support for share instance export location resource This is the basic patch which consits of the framework code for default policy in code feature as well as share instance export location resource. Partial-Implements: blueprint policy-in-code Change-Id: Iedde7a4a674a60e760b47d5eb2973f42d79226d8 --- etc/manila/manila-policy-generator.conf | 3 + etc/manila/policy.json | 8 - manila/context.py | 7 +- manila/policies/__init__.py | 27 ++++ manila/policies/base.py | 32 ++++ .../share_instance_export_location.py | 51 +++++++ manila/policy.py | 137 +++++++++++++++--- manila/tests/policy.json | 2 - manila/tests/test_policy.py | 129 ++++++----------- setup.cfg | 8 + tox.ini | 3 + 11 files changed, 296 insertions(+), 111 deletions(-) create mode 100644 etc/manila/manila-policy-generator.conf create mode 100644 manila/policies/__init__.py create mode 100644 manila/policies/base.py create mode 100644 manila/policies/share_instance_export_location.py diff --git a/etc/manila/manila-policy-generator.conf b/etc/manila/manila-policy-generator.conf new file mode 100644 index 0000000000..b2037f5451 --- /dev/null +++ b/etc/manila/manila-policy-generator.conf @@ -0,0 +1,3 @@ +[DEFAULT] +output_file = etc/manila/policy.yaml.sample +namespace = manila diff --git a/etc/manila/policy.json b/etc/manila/policy.json index dc91ebf172..801c6492d4 100644 --- a/etc/manila/policy.json +++ b/etc/manila/policy.json @@ -1,10 +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", - "availability_zone:index": "rule:default", "quota_set:update": "rule:admin_api", @@ -50,8 +44,6 @@ "share_instance:show": "rule:admin_api", "share_instance:force_delete": "rule:admin_api", "share_instance:reset_status": "rule:admin_api", - "share_instance_export_location:index": "rule:admin_api", - "share_instance_export_location:show": "rule:admin_api", "share:create_snapshot": "rule:default", "share:delete_snapshot": "rule:default", diff --git a/manila/context.py b/manila/context.py index ab046c5ba9..c822346c34 100644 --- a/manila/context.py +++ b/manila/context.py @@ -72,7 +72,7 @@ class RequestContext(context.RequestContext): self.project_id = self.tenant if self.is_admin is None: - self.is_admin = policy.check_is_admin(self.roles) + self.is_admin = policy.check_is_admin(self) elif self.is_admin and 'admin' not in self.roles: self.roles.append('admin') self.read_deleted = read_deleted @@ -135,6 +135,11 @@ class RequestContext(context.RequestContext): return ctx + def to_policy_values(self): + policy = super(RequestContext, self).to_policy_values() + policy['is_admin'] = self.is_admin + return policy + def get_admin_context(read_deleted="no"): return RequestContext(user_id=None, diff --git a/manila/policies/__init__.py b/manila/policies/__init__.py new file mode 100644 index 0000000000..248e2f52c8 --- /dev/null +++ b/manila/policies/__init__.py @@ -0,0 +1,27 @@ +# 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 itertools + +from manila.policies import base +from manila.policies import share_instance_export_location + + +def list_rules(): + return itertools.chain( + base.list_rules(), + share_instance_export_location.list_rules(), + ) diff --git a/manila/policies/base.py b/manila/policies/base.py new file mode 100644 index 0000000000..26a4a2a4ec --- /dev/null +++ b/manila/policies/base.py @@ -0,0 +1,32 @@ +# 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 + +RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner' +RULE_ADMIN_API = 'rule:admin_api' + +rules = [ + policy.RuleDefault(name='context_is_admin', check_str='role:admin'), + policy.RuleDefault( + name='admin_or_owner', + check_str='is_admin:True or project_id:%(project_id)s'), + policy.RuleDefault(name='default', check_str=RULE_ADMIN_OR_OWNER), + policy.RuleDefault(name='admin_api', check_str='is_admin:True'), +] + + +def list_rules(): + return rules diff --git a/manila/policies/share_instance_export_location.py b/manila/policies/share_instance_export_location.py new file mode 100644 index 0000000000..a6031570ec --- /dev/null +++ b/manila/policies/share_instance_export_location.py @@ -0,0 +1,51 @@ +# 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 manila.policies import base + + +BASE_POLICY_NAME = 'share_instance_export_location:%s' + + +share_export_location_policies = [ + policy.DocumentedRuleDefault( + name=BASE_POLICY_NAME % 'index', + check_str=base.RULE_ADMIN_API, + description='Return data about the requested export location.', + operations=[ + { + 'method': 'POST', + 'path': ('/share_instances/{share_instance_id}/' + 'export_locations'), + } + ]), + policy.DocumentedRuleDefault( + name=BASE_POLICY_NAME % 'show', + check_str=base.RULE_ADMIN_API, + description='Return data about the requested export location.', + operations=[ + { + 'method': 'GET', + 'path': ('/share_instances/{share_instance_id}/' + 'export_locations/{export_location_id}'), + } + ]), +] + + +def list_rules(): + return share_export_location_policies diff --git a/manila/policy.py b/manila/policy.py index 641768c5f3..d84698f352 100644 --- a/manila/policy.py +++ b/manila/policy.py @@ -16,13 +16,18 @@ """Policy Engine For Manila""" import functools +import sys from oslo_config import cfg +from oslo_log import log as logging from oslo_policy import policy +from oslo_utils import excutils from manila import exception +from manila import policies CONF = cfg.CONF +LOG = logging.getLogger(__name__) _ENFORCER = None @@ -33,13 +38,24 @@ def reset(): _ENFORCER = None -def init(policy_path=None): +def init(rules=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) - if policy_path: - _ENFORCER.policy_path = policy_path - _ENFORCER.load_rules() + _ENFORCER = policy.Enforcer(CONF, + rules=rules, + use_conf=use_conf) + register_rules(_ENFORCER) def enforce(context, action, target, do_raise=True): @@ -48,9 +64,7 @@ def enforce(context, action, target, do_raise=True): :param context: manila context :param action: string representing the action to be checked, this should be colon separated for clarity. - i.e. ``compute:create_instance``, - ``compute:attach_volume``, - ``volume:attach_volume`` + i.e. ``share:create``, :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}`` @@ -76,19 +90,101 @@ def enforce(context, action, target, do_raise=True): return _ENFORCER.enforce(action, target, context, **extra) -def check_is_admin(roles): - """Whether or not roles contain 'admin' role according to policy setting. +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 Manila 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='manila') + 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: manila context + :param action: string representing the action to be checked + this should be colon separated for clarity. + i.e. ``share:create``, + :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 manila.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. Since our credentials dict does not include a - # project_id, this target can never match as a generic rule. - target = {'project_id': ''} - credentials = {'roles': roles} - return _ENFORCER.enforce("context_is_admin", target, credentials) + credentials = context.to_policy_values() + target = credentials + return _ENFORCER.authorize('context_is_admin', target, credentials) def wrap_check_policy(resource): @@ -110,4 +206,9 @@ def check_policy(context, resource, action, target_obj=None): } target.update(target_obj or {}) _action = '%s:%s' % (resource, action) - enforce(context, _action, target) + # The else branch will be deleted after all policy in code patches + # be merged. + if resource in ('share_instance_export_location', ): + authorize(context, _action, target) + else: + enforce(context, _action, target) diff --git a/manila/tests/policy.json b/manila/tests/policy.json index 68eba1e552..1d5a0be257 100644 --- a/manila/tests/policy.json +++ b/manila/tests/policy.json @@ -59,8 +59,6 @@ "share_instance:show": "rule:admin_api", "share_instance:force_delete": "rule:admin_api", "share_instance:reset_status": "rule:admin_api", - "share_instance_export_location:index": "rule:admin_api", - "share_instance_export_location:show": "rule:admin_api", "share_snapshot:force_delete": "rule:admin_api", "share_snapshot:reset_status": "rule:admin_api", diff --git a/manila/tests/test_policy.py b/manila/tests/test_policy.py index 354b59351c..a0d8bb4569 100644 --- a/manila/tests/test_policy.py +++ b/manila/tests/test_policy.py @@ -15,8 +15,6 @@ """Test of Policy Engine For Manila.""" -import os.path - from oslo_config import cfg from oslo_policy import policy as common_policy @@ -24,113 +22,80 @@ from manila import context from manila import exception from manila import policy from manila import test -from manila import utils CONF = cfg.CONF -class PolicyFileTestCase(test.TestCase): - - def setUp(self): - super(PolicyFileTestCase, self).setUp() - # since is_admin is defined by policy, create context before reset - self.context = context.RequestContext('fake', 'fake') - policy.reset() - self.target = {} - - def test_modified_policy_reloads(self): - with utils.tempdir() as tmpdir: - tmpfilename = os.path.join(tmpdir, 'policy') - CONF.set_override('policy_file', tmpfilename, group='oslo_policy') - action = "example:test" - with open(tmpfilename, "w") as policyfile: - policyfile.write("""{"example:test": []}""") - policy.init(tmpfilename) - policy.enforce(self.context, action, self.target) - with open(tmpfilename, "w") as policyfile: - policyfile.write("""{"example:test": ["false:false"]}""") - # NOTE(vish): reset stored policy cache so we don't have to - # sleep(1) - policy._ENFORCER.load_rules(True) - self.assertRaises( - exception.PolicyNotAuthorized, - policy.enforce, - self.context, - action, - self.target, - ) - - class PolicyTestCase(test.TestCase): def setUp(self): super(PolicyTestCase, self).setUp() + rules = [ + common_policy.RuleDefault("true", '@'), + common_policy.RuleDefault("test:allowed", '@'), + common_policy.RuleDefault("test:denied", "!"), + common_policy.RuleDefault("test:my_file", + "role:compute_admin or " + "project_id:%(project_id)s"), + common_policy.RuleDefault("test:early_and_fail", "! and @"), + common_policy.RuleDefault("test:early_or_success", "@ or !"), + common_policy.RuleDefault("test:lowercase_admin", + "role:admin"), + common_policy.RuleDefault("test:uppercase_admin", + "role:ADMIN"), + ] policy.reset() policy.init() - self.rules = { - "true": [], - "example:allowed": [], - "example:denied": [["false:false"]], - "example:get_http": [["http:http://www.example.com"]], - "example:my_file": [["role:compute_admin"], - ["project_id:%(project_id)s"]], - "example:early_and_fail": [["false:false", "rule:true"]], - "example:early_or_success": [["rule:true"], ["false:false"]], - "example:lowercase_admin": [["role:admin"], ["role:sysadmin"]], - "example:uppercase_admin": [["role:ADMIN"], ["role:sysadmin"]], - } - self._set_rules() + # 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 tearDown(self): - policy.reset() - super(PolicyTestCase, self).tearDown() - - def _set_rules(self): - these_rules = common_policy.Rules.from_dict(self.rules) - policy._ENFORCER.set_rules(these_rules) - - def test_enforce_nonexistent_action_throws(self): - action = "example:noexist" - self.assertRaises(exception.PolicyNotAuthorized, policy.enforce, + def test_authorize_nonexistent_action_throws(self): + action = "test:noexist" + self.assertRaises(common_policy.PolicyNotRegistered, policy.authorize, self.context, action, self.target) - def test_enforce_bad_action_throws(self): - action = "example:denied" - self.assertRaises(exception.PolicyNotAuthorized, policy.enforce, + def test_authorize_bad_action_throws(self): + action = "test:denied" + self.assertRaises(exception.PolicyNotAuthorized, policy.authorize, self.context, action, self.target) - def test_enforce_good_action(self): - action = "example:allowed" - policy.enforce(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_templatized_enforcement(self): + 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 = "example:my_file" - policy.enforce(self.context, action, target_mine) - self.assertRaises(exception.PolicyNotAuthorized, policy.enforce, + 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_enforcement(self): - action = "example:early_and_fail" - self.assertRaises(exception.PolicyNotAuthorized, policy.enforce, + 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_enforcement(self): - action = "example:early_or_success" - policy.enforce(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 = "example:lowercase_admin" - uppercase_action = "example:uppercase_admin" - # NOTE(dprince) we mix case in the Admin role here to ensure - # case is ignored + lowercase_action = "test:lowercase_admin" + uppercase_action = "test:uppercase_admin" admin_context = context.RequestContext('admin', 'fake', roles=['AdMiN']) - policy.enforce(admin_context, lowercase_action, self.target) - policy.enforce(admin_context, uppercase_action, self.target) + policy.authorize(admin_context, lowercase_action, self.target) + policy.authorize(admin_context, uppercase_action, self.target) class DefaultPolicyTestCase(test.TestCase): @@ -214,6 +179,6 @@ class ContextIsAdminPolicyTestCase(test.TestCase): } self._set_rules(rules, CONF.oslo_policy.policy_default_rule) ctx = context.RequestContext('fake', 'fake') - self.assertFalse(ctx.is_admin) + self.assertTrue(ctx.is_admin) ctx = context.RequestContext('fake', 'fake', roles=['admin']) self.assertTrue(ctx.is_admin) diff --git a/setup.cfg b/setup.cfg index 6c72c2a8d7..96ac4a120f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -71,6 +71,14 @@ oslo.config.opts = manila = manila.opts:list_opts oslo.config.opts.defaults = manila = manila.common.config:set_middleware_defaults +oslo.policy.enforcer = + manila = manila.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. + manila = manila.policies:list_rules manila.share.drivers.dell_emc.plugins = vnx = manila.share.drivers.dell_emc.plugins.vnx.connection:VNXStorageConnection unity = manila.share.drivers.dell_emc.plugins.unity.connection:UnityStorageConnection diff --git a/tox.ini b/tox.ini index d8c0f65de3..4c43824e18 100644 --- a/tox.ini +++ b/tox.ini @@ -58,6 +58,9 @@ whitelist_externals = bash commands = oslo-config-generator --config-file etc/oslo-config-generator/manila.conf +[testenv:genpolicy] +commands = oslopolicy-sample-generator --config-file=etc/manila/manila-policy-generator.conf + [testenv:venv] commands = {posargs}