Merge "Add default policy in code for the plan resource"

This commit is contained in:
Jenkins 2017-09-27 06:35:55 +00:00 committed by Gerrit Code Review
commit 3f3df9b761
16 changed files with 503 additions and 60 deletions

View File

@ -0,0 +1,4 @@
To generate the sample policy.yaml file, run the following command from the top
level of the karbor directory:
tox -egenpolicy

View File

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

View File

@ -1,16 +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",
"plan:create": "rule:admin_or_owner",
"plan:update": "rule:admin_or_owner",
"plan:delete": "rule:admin_or_owner",
"plan:get": "rule:admin_or_owner",
"plan:get_all": "rule:admin_or_owner",
"restore:create": "rule:admin_or_owner",
"restore:update": "rule:admin_or_owner",
"restore:get": "rule:admin_or_owner",

View File

@ -19,7 +19,6 @@ from oslo_utils import uuidutils
from webob import exc
import karbor
from karbor.api import common
from karbor.api.openstack import wsgi
from karbor.common import constants
@ -28,7 +27,7 @@ from karbor.i18n import _
from karbor import objects
from karbor.objects import base as objects_base
import karbor.policy
from karbor.policies import plans as plan_policy
from karbor.services.operationengine import api as operationengine_api
from karbor.services.protection import api as protection_api
from karbor import utils
@ -49,23 +48,6 @@ CONF.register_opt(query_plan_filters_opt)
LOG = logging.getLogger(__name__)
def check_policy(context, action, target_obj=None):
target = {
'project_id': context.project_id,
'user_id': context.user_id,
}
if isinstance(target_obj, objects_base.KarborObject):
# Turn object into dict so target.update can work
target.update(
target_obj.obj_to_primitive() or {})
else:
target.update(target_obj or {})
_action = 'plan:%s' % action
karbor.policy.enforce(context, _action, target)
class PlanViewBuilder(common.ViewBuilder):
"""Model a server API response as a python dictionary."""
@ -170,7 +152,7 @@ class PlansController(wsgi.Controller):
except exception.PlanNotFound as error:
raise exc.HTTPNotFound(explanation=error.msg)
check_policy(context, 'delete', plan)
context.can(plan_policy.DELETE_POLICY, target_obj=plan)
plan.destroy()
LOG.info("Delete plan request issued successfully.",
resource={'id': plan.id})
@ -205,7 +187,7 @@ class PlansController(wsgi.Controller):
def _get_all(self, context, marker=None, limit=None, sort_keys=None,
sort_dirs=None, filters=None, offset=None):
check_policy(context, 'get_all')
context.can(plan_policy.GET_ALL_POLICY)
if filters is None:
filters = {}
@ -253,7 +235,7 @@ class PlansController(wsgi.Controller):
LOG.debug('Create plan request body: %s', body)
context = req.environ['karbor.context']
check_policy(context, 'create')
context.can(plan_policy.CREATE_POLICY)
plan = body['plan']
LOG.debug('Create plan request plan: %s', plan)
@ -347,8 +329,7 @@ class PlansController(wsgi.Controller):
plan = self._plan_get(context, id)
except exception.PlanNotFound as error:
raise exc.HTTPNotFound(explanation=error.msg)
check_policy(context, 'update', plan)
context.can(plan_policy.UPDATE_POLICY, target_obj=plan)
self._plan_update(context, plan, update_dict)
plan.update(update_dict)
@ -363,7 +344,7 @@ class PlansController(wsgi.Controller):
plan = objects.Plan.get_by_id(context, plan_id)
try:
check_policy(context, 'get', plan)
context.can(plan_policy.GET_POLICY, target_obj=plan)
except exception.PolicyNotAuthorized:
# raise PlanNotFound instead to make sure karbor behaves
# as it used to

View File

@ -21,7 +21,9 @@ from oslo_context import context
from oslo_utils import timeutils
import six
from karbor import exception
from karbor.i18n import _
from karbor.objects import base as objects_base
from karbor import policy
CONF = cfg.CONF
@ -85,7 +87,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')
@ -143,6 +145,42 @@ class RequestContext(context.RequestContext):
kwargs = {k: values[k] for k in values if k in allowed_keys}
return cls(**kwargs)
def can(self, action, 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 nova.exception.Forbidden: 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.
"""
target = {'project_id': self.project_id,
'user_id': self.user_id}
if isinstance(target_obj, objects_base.KarborObject):
# Turn object into dict so target.update can work
target.update(
target_obj.obj_to_primitive()['karbor_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,25 @@
# 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 karbor.policies import base
from karbor.policies import plans
def list_rules():
return itertools.chain(
base.list_rules(),
plans.list_rules()
)

34
karbor/policies/base.py Normal file
View File

@ -0,0 +1,34 @@
# 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

82
karbor/policies/plans.py Normal file
View File

@ -0,0 +1,82 @@
# 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 karbor.policies import base
CREATE_POLICY = 'plan:create'
UPDATE_POLICY = 'plan:update'
DELETE_POLICY = 'plan:delete'
GET_POLICY = 'plan:get'
GET_ALL_POLICY = 'plan:get_all'
plans_policies = [
policy.DocumentedRuleDefault(
name=CREATE_POLICY,
check_str=base.RULE_ADMIN_OR_OWNER,
description="""Create a plan.""",
operations=[
{
'method': 'POST',
'path': '/plans'
}
]),
policy.DocumentedRuleDefault(
name=UPDATE_POLICY,
check_str=base.RULE_ADMIN_OR_OWNER,
description="""Update a plan.""",
operations=[
{
'method': 'PUT',
'path': '/plans/{plan_id}'
}
]),
policy.DocumentedRuleDefault(
name=DELETE_POLICY,
check_str=base.RULE_ADMIN_OR_OWNER,
description="""Delete a plan.""",
operations=[
{
'method': 'DELETE',
'path': '/plans/{plan_id}'
}
]),
policy.DocumentedRuleDefault(
name=GET_POLICY,
check_str=base.RULE_ADMIN_OR_OWNER,
description="""Get a plan.""",
operations=[
{
'method': 'GET',
'path': '/plans/{plan_id}'
}
]),
policy.DocumentedRuleDefault(
name=GET_ALL_POLICY,
check_str=base.RULE_ADMIN_OR_OWNER,
description="""Get plans.""",
operations=[
{
'method': 'GET',
'path': '/plans'
}
]),
]
def list_rules():
return plans_policies

View File

@ -13,25 +13,54 @@
# License for the specific language governing permissions and limitations
# under the License.
"""Policy Engine For karbor"""
"""Policy Engine For Karbor"""
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 karbor import exception
from karbor 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):
@ -55,9 +84,9 @@ def enforce(context, action, target):
``compute:attach_volume``,
``volume:attach_volume``
:param target: dictionary representing the target of the action
for target creation this should be a dictionary representing the
location of the target e.g. ``{'project_id': context.project_id}``
: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}``
:raises PolicyNotAuthorized: if verification fails.
@ -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 Karbor 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='karbor')
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: karbor context
:param action: string representing the action to be checked
this should be colon separated for clarity.
i.e. ``compute:create_instance``,
``plan:create``,
``plan:get``
: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 karbor.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.
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

@ -14,6 +14,7 @@ import logging
import os
import fixtures
import mock
from oslo_config import cfg
from oslo_messaging import conffixture as messaging_conffixture
from oslo_utils import timeutils
@ -73,6 +74,7 @@ class TestCase(base.BaseTestCase):
self.messaging_conf.transport_driver = 'fake'
self.messaging_conf.response_timeout = 15
self.useFixture(self.messaging_conf)
rpc.init(CONF)
conf_fixture.set_defaults(CONF)
@ -112,3 +114,17 @@ class TestCase(base.BaseTestCase):
"""Override CONF variables for a test."""
for k, v in kw.items():
self.override_config(k, v)
def mock_object(self, obj, attr_name, new_attr=None, **kwargs):
"""Use python mock to mock an object attribute
Mocks the specified objects attribute with the given value.
Automatically performs 'addCleanup' for the mock.
"""
if not new_attr:
new_attr = mock.Mock()
patcher = mock.patch.object(obj, attr_name, new_attr, **kwargs)
patcher.start()
self.addCleanup(patcher.stop)
return new_attr

View File

@ -38,6 +38,8 @@ class PlanApiTest(base.TestCase):
super(PlanApiTest, self).setUp()
self.controller = plans.PlansController()
self.ctxt = context.RequestContext('demo', 'fakeproject', True)
self.mock_policy_check = self.mock_object(
context.RequestContext, 'can')
@mock.patch(
'karbor.services.protection.rpcapi.ProtectionAPI.show_provider')
@ -50,6 +52,7 @@ class PlanApiTest(base.TestCase):
mock_provider.return_value = fakes.PROVIDER_OS
self.controller.create(req, body)
self.assertTrue(mock_plan_create.called)
self.assertTrue(self.mock_policy_check.called)
def test_plan_create_InvalidBody(self):
plan = self._plan_in_request_body()
@ -206,12 +209,10 @@ class PlanApiTest(base.TestCase):
exc.HTTPBadRequest, self.controller.delete,
req, "1")
@mock.patch(
'karbor.api.v1.plans.check_policy')
@mock.patch(
'karbor.api.v1.plans.PlansController._plan_get')
def test_plan_update_InvalidStatus(
self, mock_plan_get, mock_check_policy):
self, mock_plan_get):
plan = self._plan_in_request_body(
name=DEFAULT_NAME,
description=DEFAULT_DESCRIPTION,

View File

@ -187,9 +187,11 @@ class ScheduledOperationApiTest(base.TestCase):
req = fakes.HTTPRequest.blank('/v1/triggers')
return controller.create(req, create_trigger_param)
@mock.patch(
'karbor.context.RequestContext.can')
@mock.patch(
'karbor.services.protection.rpcapi.ProtectionAPI.show_provider')
def _create_plan(self, provider_id, mock_provider):
def _create_plan(self, provider_id, mock_provider, mock_policy):
create_plan_param = {
'plan': {
'name': '123',

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 karbor import context
from karbor import exception
from karbor.tests import base
from karbor import utils
from karbor import policy
CONF = cfg.CONF
class PolicyFileTestCase(base.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(base.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

@ -12,7 +12,11 @@
"""Utilities and helper functions."""
import ast
import contextlib
import os
import shutil
import six
import tempfile
import webob.exc
from keystoneclient import discover as ks_discover
@ -175,3 +179,16 @@ def walk_class_hierarchy(clazz, encountered=None):
for subsubclass in walk_class_hierarchy(subclass, encountered):
yield subsubclass
yield subclass
@contextlib.contextmanager
def tempdir(**kwargs):
tmpdir = tempfile.mkdtemp(**kwargs)
try:
yield tmpdir
finally:
try:
shutil.rmtree(tmpdir)
except OSError as e:
LOG.debug('Could not remove tmpdir: %s',
six.text_type(e))

View File

@ -30,6 +30,14 @@ console_scripts =
karbor-protection = karbor.cmd.protection:main
oslo.config.opts =
karbor.common.opts = karbor.common.opts:list_opts
oslo.policy.enforcer =
karbor = karbor.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.
karbor = karbor.policies:list_rules
wsgi_scripts =
karbor-wsgi = karbor.wsgi.wsgi:initialize_application
karbor.database.migration_backend =

View File

@ -60,6 +60,9 @@ commands = sphinx-build -a -E -d releasenotes/build/doctrees -b html releasenote
[testenv:genconfig]
commands = oslo-config-generator --config-file etc/oslo-config-generator/karbor.conf
[testenv:genpolicy]
commands = oslopolicy-sample-generator --config-file=etc/karbor-policy-generator.conf
[flake8]
show-source = True
ignore =