[policy in code] Add support for attachment resource
This is the basic patch which consits of the framework code for default policy in code feature as well as support in attachment resource. Change-Id: Ie3ff068e61ea8e0e8fff78deb732e183e036a10c Partial-Implements: blueprint policy-in-code
This commit is contained in:
parent
53489819f5
commit
43a3152581
@ -25,7 +25,9 @@ from oslo_log import log as logging
|
|||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
|
from cinder.objects import base as objects_base
|
||||||
from cinder import policy
|
from cinder import policy
|
||||||
|
|
||||||
context_opts = [
|
context_opts = [
|
||||||
@ -94,7 +96,7 @@ class RequestContext(context.RequestContext):
|
|||||||
# when policy.check_is_admin invokes request logging
|
# when policy.check_is_admin invokes request logging
|
||||||
# to make it loggable.
|
# to make it loggable.
|
||||||
if self.is_admin is None:
|
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:
|
elif self.is_admin and 'admin' not in self.roles:
|
||||||
self.roles.append('admin')
|
self.roles.append('admin')
|
||||||
|
|
||||||
@ -145,6 +147,42 @@ class RequestContext(context.RequestContext):
|
|||||||
user_domain=values.get('user_domain'),
|
user_domain=values.get('user_domain'),
|
||||||
project_domain=values.get('project_domain'))
|
project_domain=values.get('project_domain'))
|
||||||
|
|
||||||
|
def authorize(self, action, target=None, 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 cinder.exception.NotAuthorized: 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.
|
||||||
|
"""
|
||||||
|
if target is None:
|
||||||
|
target = {'project_id': self.project_id,
|
||||||
|
'user_id': self.user_id}
|
||||||
|
if isinstance(target_obj, objects_base.CinderObject):
|
||||||
|
# Turn object into dict so target.update can work
|
||||||
|
target.update(
|
||||||
|
target_obj.obj_to_primitive()['versioned_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):
|
def to_policy_values(self):
|
||||||
policy = super(RequestContext, self).to_policy_values()
|
policy = super(RequestContext, self).to_policy_values()
|
||||||
|
|
||||||
|
26
cinder/policies/__init__.py
Normal file
26
cinder/policies/__init__.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# 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 cinder.policies import attachments
|
||||||
|
from cinder.policies import base
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return itertools.chain(
|
||||||
|
base.list_rules(),
|
||||||
|
attachments.list_rules()
|
||||||
|
)
|
60
cinder/policies/attachments.py
Normal file
60
cinder/policies/attachments.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# 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 cinder.policies import base
|
||||||
|
|
||||||
|
|
||||||
|
CREATE_POLICY = 'volume:attachment_create'
|
||||||
|
UPDATE_POLICY = 'volume:attachment_update'
|
||||||
|
DELETE_POLICY = 'volume:attachment_delete'
|
||||||
|
|
||||||
|
attachments_policies = [
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=CREATE_POLICY,
|
||||||
|
check_str="",
|
||||||
|
description="""Create attachment.""",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'POST',
|
||||||
|
'path': '/attachments'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=UPDATE_POLICY,
|
||||||
|
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||||
|
description="""Update attachment.""",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'PUT',
|
||||||
|
'path': '/attachments/{attachment_id}'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=DELETE_POLICY,
|
||||||
|
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||||
|
description="""Delete attachment.""",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'DELETE',
|
||||||
|
'path': '/attachments/{attachment_id}'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def list_rules():
|
||||||
|
return attachments_policies
|
35
cinder/policies/base.py
Normal file
35
cinder/policies/base.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# 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('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
|
138
cinder/policy.py
138
cinder/policy.py
@ -15,23 +15,52 @@
|
|||||||
|
|
||||||
"""Policy Engine For Cinder"""
|
"""Policy Engine For Cinder"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
from oslo_policy import opts as policy_opts
|
from oslo_policy import opts as policy_opts
|
||||||
from oslo_policy import policy
|
from oslo_policy import policy
|
||||||
|
from oslo_utils import excutils
|
||||||
|
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
|
from cinder import policies
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
policy_opts.set_defaults(cfg.CONF, 'policy.json')
|
policy_opts.set_defaults(cfg.CONF, 'policy.json')
|
||||||
|
|
||||||
_ENFORCER = None
|
_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
|
global _ENFORCER
|
||||||
if not _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):
|
def enforce_action(context, action):
|
||||||
@ -72,19 +101,100 @@ def enforce(context, action, target):
|
|||||||
action=action)
|
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 Cinder 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='cinder')
|
||||||
|
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: cinder 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``
|
||||||
|
: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 cinder.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.error('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.
|
"""Whether or not user is admin according to policy setting.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
init()
|
init()
|
||||||
|
# the target is user-self
|
||||||
# include project_id on target to avoid KeyError if context_is_admin
|
credentials = context.to_policy_values()
|
||||||
# policy definition is missing, and default admin_or_owner rule
|
target = credentials
|
||||||
# attempts to apply.
|
return _ENFORCER.authorize('context_is_admin', target, credentials)
|
||||||
target = {'project_id': ''}
|
|
||||||
if context is None:
|
|
||||||
credentials = {'roles': roles}
|
|
||||||
else:
|
|
||||||
credentials = context.to_dict()
|
|
||||||
|
|
||||||
return _ENFORCER.enforce('context_is_admin', target, credentials)
|
|
||||||
|
@ -313,8 +313,9 @@ class TestCase(testtools.TestCase):
|
|||||||
|
|
||||||
def flags(self, **kw):
|
def flags(self, **kw):
|
||||||
"""Override CONF variables for a test."""
|
"""Override CONF variables for a test."""
|
||||||
|
group = kw.pop('group', None)
|
||||||
for k, v in kw.items():
|
for k, v in kw.items():
|
||||||
self.override_config(k, v)
|
CONF.set_override(k, v, group)
|
||||||
|
|
||||||
def start_service(self, name, host=None, **kwargs):
|
def start_service(self, name, host=None, **kwargs):
|
||||||
host = host if host else uuid.uuid4().hex
|
host = host if host else uuid.uuid4().hex
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"context_is_admin": "role:admin",
|
|
||||||
"admin_api": "is_admin:True",
|
"admin_api": "is_admin:True",
|
||||||
"admin_or_owner": "is_admin:True or project_id:%(project_id)s",
|
"admin_or_owner": "is_admin:True or project_id:%(project_id)s",
|
||||||
|
|
||||||
@ -113,9 +112,6 @@
|
|||||||
"backup:update": "rule:admin_or_owner",
|
"backup:update": "rule:admin_or_owner",
|
||||||
"backup:backup_project_attribute": "rule:admin_api",
|
"backup:backup_project_attribute": "rule:admin_api",
|
||||||
|
|
||||||
"volume:attachment_create": "",
|
|
||||||
"volume:attachment_update": "rule:admin_or_owner",
|
|
||||||
"volume:attachment_delete": "rule:admin_or_owner",
|
|
||||||
|
|
||||||
"consistencygroup:create" : "",
|
"consistencygroup:create" : "",
|
||||||
"consistencygroup:delete": "",
|
"consistencygroup:delete": "",
|
||||||
|
131
cinder/tests/unit/test_policy.py
Normal file
131
cinder/tests/unit/test_policy.py
Normal file
@ -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 cinder import context
|
||||||
|
from cinder import exception
|
||||||
|
from cinder import test
|
||||||
|
from cinder import utils
|
||||||
|
|
||||||
|
from cinder import policy
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyFileTestCase(test.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(test.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)
|
@ -1,55 +0,0 @@
|
|||||||
# Copyright 2010 United States Government as represented by the
|
|
||||||
# Administrator of the National Aeronautics and Space Administration.
|
|
||||||
# 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.
|
|
||||||
"""Tests for volume policy."""
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from cinder import context
|
|
||||||
from cinder import test
|
|
||||||
|
|
||||||
import cinder.policy
|
|
||||||
|
|
||||||
|
|
||||||
class VolumePolicyTestCase(test.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(VolumePolicyTestCase, self).setUp()
|
|
||||||
|
|
||||||
cinder.policy.init()
|
|
||||||
|
|
||||||
self.context = context.get_admin_context()
|
|
||||||
|
|
||||||
def test_check_policy(self):
|
|
||||||
target = {
|
|
||||||
'project_id': self.context.project_id,
|
|
||||||
'user_id': self.context.user_id,
|
|
||||||
}
|
|
||||||
with mock.patch.object(cinder.policy, 'enforce') as mock_enforce:
|
|
||||||
cinder.volume.api.check_policy(self.context, 'attach')
|
|
||||||
mock_enforce.assert_called_once_with(self.context,
|
|
||||||
'volume:attach',
|
|
||||||
target)
|
|
||||||
|
|
||||||
def test_check_policy_with_target(self):
|
|
||||||
target = {
|
|
||||||
'project_id': self.context.project_id,
|
|
||||||
'user_id': self.context.user_id,
|
|
||||||
'id': 2,
|
|
||||||
}
|
|
||||||
with mock.patch.object(cinder.policy, 'enforce') as mock_enforce:
|
|
||||||
cinder.volume.api.check_policy(self.context, 'attach', {'id': 2})
|
|
||||||
mock_enforce.assert_called_once_with(self.context,
|
|
||||||
'volume:attach',
|
|
||||||
target)
|
|
@ -43,6 +43,7 @@ from cinder import keymgr as key_manager
|
|||||||
from cinder import objects
|
from cinder import objects
|
||||||
from cinder.objects import base as objects_base
|
from cinder.objects import base as objects_base
|
||||||
from cinder.objects import fields
|
from cinder.objects import fields
|
||||||
|
from cinder.policies import attachments as attachment_policy
|
||||||
import cinder.policy
|
import cinder.policy
|
||||||
from cinder import quota
|
from cinder import quota
|
||||||
from cinder import quota_utils
|
from cinder import quota_utils
|
||||||
@ -1986,13 +1987,13 @@ class API(base.Base):
|
|||||||
db_ref = self.db.volume_attach(ctxt.elevated(), values)
|
db_ref = self.db.volume_attach(ctxt.elevated(), values)
|
||||||
return objects.VolumeAttachment.get_by_id(ctxt, db_ref['id'])
|
return objects.VolumeAttachment.get_by_id(ctxt, db_ref['id'])
|
||||||
|
|
||||||
@wrap_check_policy
|
|
||||||
def attachment_create(self,
|
def attachment_create(self,
|
||||||
ctxt,
|
ctxt,
|
||||||
volume_ref,
|
volume_ref,
|
||||||
instance_uuid,
|
instance_uuid,
|
||||||
connector=None):
|
connector=None):
|
||||||
"""Create an attachment record for the specified volume."""
|
"""Create an attachment record for the specified volume."""
|
||||||
|
ctxt.authorize(attachment_policy.CREATE_POLICY, target_obj=volume_ref)
|
||||||
connection_info = {}
|
connection_info = {}
|
||||||
attachment_ref = self._attachment_reserve(ctxt,
|
attachment_ref = self._attachment_reserve(ctxt,
|
||||||
volume_ref,
|
volume_ref,
|
||||||
@ -2007,7 +2008,6 @@ class API(base.Base):
|
|||||||
attachment_ref.save()
|
attachment_ref.save()
|
||||||
return attachment_ref
|
return attachment_ref
|
||||||
|
|
||||||
@wrap_check_policy
|
|
||||||
def attachment_update(self, ctxt, attachment_ref, connector):
|
def attachment_update(self, ctxt, attachment_ref, connector):
|
||||||
"""Update an existing attachment record."""
|
"""Update an existing attachment record."""
|
||||||
# Valid items to update (connector includes mode and mountpoint):
|
# Valid items to update (connector includes mode and mountpoint):
|
||||||
@ -2019,6 +2019,8 @@ class API(base.Base):
|
|||||||
# We fetch the volume object and pass it to the rpc call because we
|
# We fetch the volume object and pass it to the rpc call because we
|
||||||
# need to direct this to the correct host/backend
|
# need to direct this to the correct host/backend
|
||||||
|
|
||||||
|
ctxt.authorize(attachment_policy.UPDATE_POLICY,
|
||||||
|
target_obj=attachment_ref)
|
||||||
volume_ref = objects.Volume.get_by_id(ctxt, attachment_ref.volume_id)
|
volume_ref = objects.Volume.get_by_id(ctxt, attachment_ref.volume_id)
|
||||||
connection_info = (
|
connection_info = (
|
||||||
self.volume_rpcapi.attachment_update(ctxt,
|
self.volume_rpcapi.attachment_update(ctxt,
|
||||||
@ -2029,8 +2031,9 @@ class API(base.Base):
|
|||||||
attachment_ref.save()
|
attachment_ref.save()
|
||||||
return attachment_ref
|
return attachment_ref
|
||||||
|
|
||||||
@wrap_check_policy
|
|
||||||
def attachment_delete(self, ctxt, attachment):
|
def attachment_delete(self, ctxt, attachment):
|
||||||
|
ctxt.authorize(attachment_policy.DELETE_POLICY,
|
||||||
|
target_obj=attachment)
|
||||||
volume = objects.Volume.get_by_id(ctxt, attachment.volume_id)
|
volume = objects.Volume.get_by_id(ctxt, attachment.volume_id)
|
||||||
if attachment.attach_status == 'reserved':
|
if attachment.attach_status == 'reserved':
|
||||||
self.db.volume_detached(ctxt.elevated(), attachment.volume_id,
|
self.db.volume_detached(ctxt.elevated(), attachment.volume_id,
|
||||||
|
13
etc/cinder/README-policy.generate.md
Normal file
13
etc/cinder/README-policy.generate.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Generate policy file
|
||||||
|
To generate the sample policy yaml file, run the following command from the top
|
||||||
|
level of the cinder directory:
|
||||||
|
|
||||||
|
tox -egenpolicy
|
||||||
|
|
||||||
|
# Use generated policy file
|
||||||
|
Cinder recognizes ``/etc/cinder/policy.yaml`` as the default policy file.
|
||||||
|
To specify your own policy file in order to overwrite the default policy value,
|
||||||
|
add this in Cinder config file:
|
||||||
|
|
||||||
|
[oslo_policy]
|
||||||
|
policy_file = path/to/policy/file
|
3
etc/cinder/cinder-policy-generator.conf
Normal file
3
etc/cinder/cinder-policy-generator.conf
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
output_file = etc/cinder/policy.yaml.sample
|
||||||
|
namespace = cinder
|
@ -1,9 +1,4 @@
|
|||||||
{
|
{
|
||||||
"admin_or_owner": "is_admin:True or (role:admin and is_admin_project:True) or project_id:%(project_id)s",
|
|
||||||
"default": "rule:admin_or_owner",
|
|
||||||
|
|
||||||
"admin_api": "is_admin:True or (role:admin and is_admin_project:True)",
|
|
||||||
|
|
||||||
"volume:create": "",
|
"volume:create": "",
|
||||||
"volume:create_from_image": "",
|
"volume:create_from_image": "",
|
||||||
"volume:delete": "rule:admin_or_owner",
|
"volume:delete": "rule:admin_or_owner",
|
||||||
@ -105,10 +100,6 @@
|
|||||||
"backup:update": "rule:admin_or_owner",
|
"backup:update": "rule:admin_or_owner",
|
||||||
"backup:backup_project_attribute": "rule:admin_api",
|
"backup:backup_project_attribute": "rule:admin_api",
|
||||||
|
|
||||||
"volume:attachment_create": "",
|
|
||||||
"volume:attachment_update": "rule:admin_or_owner",
|
|
||||||
"volume:attachment_delete": "rule:admin_or_owner",
|
|
||||||
|
|
||||||
"snapshot_extension:snapshot_actions:update_snapshot_status": "",
|
"snapshot_extension:snapshot_actions:update_snapshot_status": "",
|
||||||
"snapshot_extension:snapshot_manage": "rule:admin_api",
|
"snapshot_extension:snapshot_manage": "rule:admin_api",
|
||||||
"snapshot_extension:snapshot_unmanage": "rule:admin_api",
|
"snapshot_extension:snapshot_unmanage": "rule:admin_api",
|
||||||
|
@ -51,6 +51,14 @@ oslo.config.opts =
|
|||||||
cinder = cinder.opts:list_opts
|
cinder = cinder.opts:list_opts
|
||||||
oslo.config.opts.defaults =
|
oslo.config.opts.defaults =
|
||||||
cinder = cinder.common.config:set_middleware_defaults
|
cinder = cinder.common.config:set_middleware_defaults
|
||||||
|
oslo.policy.enforcer =
|
||||||
|
cinder = cinder.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.
|
||||||
|
cinder = cinder.policies:list_rules
|
||||||
console_scripts =
|
console_scripts =
|
||||||
cinder-api = cinder.cmd.api:main
|
cinder-api = cinder.cmd.api:main
|
||||||
cinder-backup = cinder.cmd.backup:main
|
cinder-backup = cinder.cmd.backup:main
|
||||||
|
4
tox.ini
4
tox.ini
@ -83,6 +83,10 @@ sitepackages = False
|
|||||||
envdir = {toxworkdir}/pep8
|
envdir = {toxworkdir}/pep8
|
||||||
commands = oslo-config-generator --config-file=tools/config/cinder-config-generator.conf
|
commands = oslo-config-generator --config-file=tools/config/cinder-config-generator.conf
|
||||||
|
|
||||||
|
|
||||||
|
[testenv:genpolicy]
|
||||||
|
commands = oslopolicy-sample-generator --config-file=etc/cinder/cinder-policy-generator.conf
|
||||||
|
|
||||||
[testenv:genopts]
|
[testenv:genopts]
|
||||||
sitepackages = False
|
sitepackages = False
|
||||||
envdir = {toxworkdir}/pep8
|
envdir = {toxworkdir}/pep8
|
||||||
|
Loading…
x
Reference in New Issue
Block a user