[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:
TommyLike 2017-09-23 11:05:31 +08:00
parent 53489819f5
commit 43a3152581
15 changed files with 451 additions and 87 deletions

View File

@ -25,7 +25,9 @@ from oslo_log import log as logging
from oslo_utils import timeutils
import six
from cinder import exception
from cinder.i18n import _
from cinder.objects import base as objects_base
from cinder import policy
context_opts = [
@ -94,7 +96,7 @@ class RequestContext(context.RequestContext):
# when policy.check_is_admin invokes request logging
# to make it loggable.
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:
self.roles.append('admin')
@ -145,6 +147,42 @@ class RequestContext(context.RequestContext):
user_domain=values.get('user_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):
policy = super(RequestContext, self).to_policy_values()

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

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

View File

@ -15,23 +15,52 @@
"""Policy Engine For Cinder"""
import sys
from oslo_config import cfg
from oslo_log import log as logging
from oslo_policy import opts as policy_opts
from oslo_policy import policy
from oslo_utils import excutils
from cinder import exception
from cinder import policies
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
policy_opts.set_defaults(cfg.CONF, 'policy.json')
_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
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):
@ -72,19 +101,100 @@ def enforce(context, action, target):
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.
"""
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.
target = {'project_id': ''}
if context is None:
credentials = {'roles': roles}
else:
credentials = context.to_dict()
return _ENFORCER.enforce('context_is_admin', target, credentials)
# the target is user-self
credentials = context.to_policy_values()
target = credentials
return _ENFORCER.authorize('context_is_admin', target, credentials)

View File

@ -313,8 +313,9 @@ class TestCase(testtools.TestCase):
def flags(self, **kw):
"""Override CONF variables for a test."""
group = kw.pop('group', None)
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):
host = host if host else uuid.uuid4().hex

View File

@ -1,5 +1,4 @@
{
"context_is_admin": "role:admin",
"admin_api": "is_admin:True",
"admin_or_owner": "is_admin:True or project_id:%(project_id)s",
@ -113,9 +112,6 @@
"backup:update": "rule:admin_or_owner",
"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:delete": "",

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

View File

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

View File

@ -43,6 +43,7 @@ from cinder import keymgr as key_manager
from cinder import objects
from cinder.objects import base as objects_base
from cinder.objects import fields
from cinder.policies import attachments as attachment_policy
import cinder.policy
from cinder import quota
from cinder import quota_utils
@ -1986,13 +1987,13 @@ class API(base.Base):
db_ref = self.db.volume_attach(ctxt.elevated(), values)
return objects.VolumeAttachment.get_by_id(ctxt, db_ref['id'])
@wrap_check_policy
def attachment_create(self,
ctxt,
volume_ref,
instance_uuid,
connector=None):
"""Create an attachment record for the specified volume."""
ctxt.authorize(attachment_policy.CREATE_POLICY, target_obj=volume_ref)
connection_info = {}
attachment_ref = self._attachment_reserve(ctxt,
volume_ref,
@ -2007,7 +2008,6 @@ class API(base.Base):
attachment_ref.save()
return attachment_ref
@wrap_check_policy
def attachment_update(self, ctxt, attachment_ref, connector):
"""Update an existing attachment record."""
# 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
# 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)
connection_info = (
self.volume_rpcapi.attachment_update(ctxt,
@ -2029,8 +2031,9 @@ class API(base.Base):
attachment_ref.save()
return attachment_ref
@wrap_check_policy
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)
if attachment.attach_status == 'reserved':
self.db.volume_detached(ctxt.elevated(), attachment.volume_id,

View 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

View File

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

View File

@ -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_from_image": "",
"volume:delete": "rule:admin_or_owner",
@ -105,10 +100,6 @@
"backup:update": "rule:admin_or_owner",
"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_manage": "rule:admin_api",
"snapshot_extension:snapshot_unmanage": "rule:admin_api",

View File

@ -51,6 +51,14 @@ oslo.config.opts =
cinder = cinder.opts:list_opts
oslo.config.opts.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 =
cinder-api = cinder.cmd.api:main
cinder-backup = cinder.cmd.backup:main

View File

@ -83,6 +83,10 @@ sitepackages = False
envdir = {toxworkdir}/pep8
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]
sitepackages = False
envdir = {toxworkdir}/pep8