[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
This commit is contained in:
parent
647147ba10
commit
b21c3d68a4
3
etc/manila/manila-policy-generator.conf
Normal file
3
etc/manila/manila-policy-generator.conf
Normal file
@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
output_file = etc/manila/policy.yaml.sample
|
||||
namespace = manila
|
@ -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",
|
||||
|
@ -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,
|
||||
|
27
manila/policies/__init__.py
Normal file
27
manila/policies/__init__.py
Normal file
@ -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(),
|
||||
)
|
32
manila/policies/base.py
Normal file
32
manila/policies/base.py
Normal file
@ -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
|
51
manila/policies/share_instance_export_location.py
Normal file
51
manila/policies/share_instance_export_location.py
Normal file
@ -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
|
137
manila/policy.py
137
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)
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user