[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:
zhongjun 2017-09-25 18:41:24 +08:00 committed by zhongjun
parent 647147ba10
commit b21c3d68a4
11 changed files with 296 additions and 111 deletions

View File

@ -0,0 +1,3 @@
[DEFAULT]
output_file = etc/manila/policy.yaml.sample
namespace = manila

View File

@ -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",

View File

@ -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,

View 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
View 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

View 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

View File

@ -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)

View File

@ -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",

View File

@ -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)

View File

@ -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

View File

@ -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}