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}