Merge "Add resource_type-specific policies"

This commit is contained in:
Jenkins 2015-08-29 16:18:53 +00:00 committed by Gerrit Code Review
commit 6fb5000ae2
10 changed files with 157 additions and 5 deletions

View File

@ -73,5 +73,7 @@
"software_deployments:delete": "rule:deny_stack_user",
"software_deployments:metadata": "",
"service:index": "rule:context_is_admin"
"service:index": "rule:context_is_admin",
"resource_types:OS::Nova::Flavor": "rule:context_is_admin"
}

View File

@ -107,7 +107,10 @@ class NotAuthenticated(HeatException):
class Forbidden(HeatException):
msg_fmt = _("You are not authorized to complete this action.")
msg_fmt = _("You are not authorized to use %(action)s.")
def __init__(self, action='this action'):
super(Forbidden, self).__init__(action=action)
# NOTE(bcwaldon): here for backwards-compatibility, need to deprecate.

View File

@ -18,14 +18,18 @@
"""Policy Engine For Heat"""
from oslo_config import cfg
from oslo_log import log as logging
from oslo_policy import policy
import six
from heat.common import exception
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
DEFAULT_RULES = policy.Rules.from_dict({'default': '!'})
DEFAULT_RESOURCE_RULES = policy.Rules.from_dict({'default': '@'})
class Enforcer(object):
@ -82,3 +86,30 @@ class Enforcer(object):
:returns: A non-False value if the user is admin according to policy
"""
return self._check(context, 'context_is_admin', target={}, exc=None)
class ResourceEnforcer(Enforcer):
def __init__(self, default_rule=DEFAULT_RESOURCE_RULES['default'],
**kwargs):
super(ResourceEnforcer, self).__init__(
default_rule=default_rule, **kwargs)
def enforce(self, context, res_type, scope=None, target=None):
# NOTE(pas-ha): try/except just to log the exception
try:
result = super(ResourceEnforcer, self).enforce(
context, res_type,
scope=scope or 'resource_types',
target=target)
except self.exc as ex:
LOG.info(six.text_type(ex))
raise
if not result:
if self.exc:
raise self.exc(action=res_type)
else:
return result
def enforce_stack(self, stack, scope=None, target=None):
for res in stack.resources.values():
self.enforce(stack.context, res.type(), scope=scope, target=target)

View File

@ -28,6 +28,7 @@ from heat.common.i18n import _
from heat.common.i18n import _LE
from heat.common.i18n import _LI
from heat.common.i18n import _LW
from heat.common import policy
from heat.engine import support
LOG = log.getLogger(__name__)
@ -458,10 +459,23 @@ class ResourceRegistry(object):
def not_hidden_matches(cls):
return cls.get_class().support_status.status != support.HIDDEN
def is_allowed(enforcer, name):
if cnxt is None:
return True
try:
enforcer.enforce(cnxt, name)
except enforcer.exc:
return False
else:
return True
enforcer = policy.ResourceEnforcer()
return [name for name, cls in six.iteritems(self._registry)
if (is_resource(name) and
status_matches(cls) and
is_available(cls) and
is_allowed(enforcer, name) and
not_hidden_matches(cls))]

View File

@ -180,7 +180,7 @@ class Resource(object):
service_name=ResourceClass.default_client_name,
resource_type=definition.resource_type
)
LOG.error(six.text_type(ex))
LOG.info(six.text_type(ex))
raise ex

View File

@ -39,6 +39,7 @@ from heat.common.i18n import _LI
from heat.common.i18n import _LW
from heat.common import identifier
from heat.common import messaging as rpc_messaging
from heat.common import policy
from heat.common import service_utils
from heat.engine import api
from heat.engine import attributes
@ -293,6 +294,7 @@ class EngineService(service.Service):
self.manage_thread_grp = None
self._rpc_server = None
self.software_config = service_software_config.SoftwareConfigService()
self.resource_enforcer = policy.ResourceEnforcer()
if cfg.CONF.trusts_delegated_roles:
warnings.warn('The default value of "trusts_delegated_roles" '
@ -641,6 +643,7 @@ class EngineService(service.Service):
args,
convergence=conv_eng)
self.resource_enforcer.enforce_stack(stack)
return api.format_stack_preview(stack)
@context.request_context
@ -700,6 +703,7 @@ class EngineService(service.Service):
nested_depth, user_creds_id, stack_user_project_id, convergence,
parent_resource_name)
self.resource_enforcer.enforce_stack(stack)
stack.store()
_create_stack_user(stack)
if convergence:
@ -734,6 +738,7 @@ class EngineService(service.Service):
LOG.info(_LI('Updating stack %s'), db_stack.name)
current_stack = parser.Stack.load(cnxt, stack=db_stack)
self.resource_enforcer.enforce_stack(current_stack)
if current_stack.action == current_stack.SUSPEND:
msg = _('Updating a stack when it is suspended')
@ -778,6 +783,7 @@ class EngineService(service.Service):
current_kwargs.update(common_params)
updated_stack = parser.Stack(cnxt, stack_name, tmpl,
**current_kwargs)
self.resource_enforcer.enforce_stack(updated_stack)
updated_stack.parameters.set_stack_id(current_stack.identifier())
self._validate_deferred_auth_context(cnxt, updated_stack)
@ -957,6 +963,7 @@ class EngineService(service.Service):
st = self._get_stack(cnxt, stack_identity)
LOG.info(_LI('Deleting stack %s'), st.name)
stack = parser.Stack.load(cnxt, stack=st)
self.resource_enforcer.enforce_stack(stack)
if stack.convergence:
template = templatem.Template.create_empty_template()
@ -1067,6 +1074,7 @@ class EngineService(service.Service):
:param cnxt: RPC context.
:param type_name: Name of the resource type to obtain the schema of.
"""
self.resource_enforcer.enforce(cnxt, type_name)
try:
resource_class = resources.global_env().get_class(type_name)
except (exception.InvalidResourceType,
@ -1112,6 +1120,7 @@ class EngineService(service.Service):
:param type_name: Name of the resource type to generate a template for.
:param template_type: the template type to generate, cfn or hot.
"""
self.resource_enforcer.enforce(cnxt, type_name)
try:
resource_class = resources.global_env().get_class(type_name)
if resource_class.support_status.status == support.HIDDEN:
@ -1317,6 +1326,7 @@ class EngineService(service.Service):
s = self._get_stack(cnxt, stack_identity)
stack = parser.Stack.load(cnxt, stack=s)
self.resource_enforcer.enforce_stack(stack)
self.thread_group_mgr.start_with_lock(cnxt, stack, self.engine_id,
_stack_suspend, stack)
@ -1332,6 +1342,7 @@ class EngineService(service.Service):
s = self._get_stack(cnxt, stack_identity)
stack = parser.Stack.load(cnxt, stack=s)
self.resource_enforcer.enforce_stack(stack)
self.thread_group_mgr.start_with_lock(cnxt, stack, self.engine_id,
_stack_resume, stack)
@ -1418,8 +1429,11 @@ class EngineService(service.Service):
s = self._get_stack(cnxt, stack_identity)
stack = parser.Stack.load(cnxt, stack=s)
self.resource_enforcer.enforce_stack(stack)
snapshot = snapshot_object.Snapshot.get_snapshot_by_stack(
cnxt, snapshot_id, s)
# FIXME(pas-ha) has to be ammended to deny restoring stacks
# that have disallowed for current user
self.thread_group_mgr.start_with_lock(cnxt, stack, self.engine_id,
_stack_restore, stack, snapshot)

View File

@ -25,6 +25,7 @@ import testtools
from heat.common import context
from heat.common import messaging
from heat.common import policy
from heat.engine.clients.os import cinder
from heat.engine.clients.os import glance
from heat.engine.clients.os import keystone
@ -78,7 +79,8 @@ class FakeLogMixin(object):
class HeatTestCase(testscenarios.WithScenarios,
testtools.TestCase, FakeLogMixin):
def setUp(self, mock_keystone=True, quieten_logging=True):
def setUp(self, mock_keystone=True, mock_resource_policy=True,
quieten_logging=True):
super(HeatTestCase, self).setUp()
self.m = mox.Mox()
self.addCleanup(self.m.UnsetStubs)
@ -124,6 +126,9 @@ class HeatTestCase(testscenarios.WithScenarios,
if mock_keystone:
self.stub_keystoneclient()
if mock_resource_policy:
self.mock_resource_policy = self.patchobject(
policy.ResourceEnforcer, 'enforce')
utils.setup_dummy_db()
self.register_test_resources()
self.addCleanup(utils.reset_dummy_db)

View File

@ -0,0 +1,6 @@
{
"context_is_admin": "role:admin",
"resource_types:OS::Test::AdminOnly": "rule:context_is_admin"
}

View File

@ -40,7 +40,7 @@ class TestPolicyEnforcer(common.HeatTestCase):
"PutMetricAlarm", "PutMetricData", "SetAlarmState")
def setUp(self):
super(TestPolicyEnforcer, self).setUp()
super(TestPolicyEnforcer, self).setUp(mock_resource_policy=False)
opts = [
cfg.StrOpt('config_dir', default=policy_path),
cfg.StrOpt('config_file', default='foo'),
@ -183,3 +183,47 @@ class TestPolicyEnforcer(common.HeatTestCase):
False, exc=None).AndReturn(True)
self.m.ReplayAll()
self.assertTrue(enforcer.check_is_admin(ctx))
def test_resource_default_rule(self):
context = utils.dummy_context(roles=['non-admin'])
enforcer = policy.ResourceEnforcer(
policy_file=self.get_policy_file('resources.json'))
res_type = "OS::Test::NotInPolicy"
self.assertIsNone(enforcer.enforce(context, res_type))
def test_resource_enforce_success(self):
context = utils.dummy_context(roles=['admin'])
enforcer = policy.ResourceEnforcer(
policy_file=self.get_policy_file('resources.json'))
res_type = "OS::Test::AdminOnly"
self.assertIsNone(enforcer.enforce(context, res_type))
def test_resource_enforce_fail(self):
context = utils.dummy_context(roles=['non-admin'])
enforcer = policy.ResourceEnforcer(
policy_file=self.get_policy_file('resources.json'))
res_type = "OS::Test::AdminOnly"
ex = self.assertRaises(exception.Forbidden,
enforcer.enforce,
context, res_type)
self.assertIn(res_type, ex.message)
def test_resource_enforce_returns_false(self):
context = utils.dummy_context(roles=['non-admin'])
enforcer = policy.ResourceEnforcer(
policy_file=self.get_policy_file('resources.json'),
exc=None)
res_type = "OS::Test::AdminOnly"
self.assertFalse(enforcer.enforce(context, res_type))
def test_resource_enforce_exc_on_false(self):
context = utils.dummy_context(roles=['non-admin'])
enforcer = policy.ResourceEnforcer(
policy_file=self.get_policy_file('resources.json'))
res_type = "OS::Test::AdminOnly"
self.patchobject(base_policy.Enforcer, 'enforce',
return_value=False)
ex = self.assertRaises(exception.Forbidden,
enforcer.enforce,
context, res_type)
self.assertIn(res_type, ex.message)

View File

@ -66,3 +66,36 @@ resources:
template=self.unavailable_template)
self.assertIn('ResourceTypeUnavailable', ex.message)
self.assertIn('OS::Sahara::NodeGroupTemplate', ex.message)
class RoleBasedExposureTest(functional_base.FunctionalTestsBase):
forbidden_resource_type = "OS::Nova::Flavor"
fl_tmpl = """
heat_template_version: 2015-10-15
resources:
not4everyone:
type: OS::Nova::Flavor
properties:
ram: 20000
vcpus: 10
"""
def test_non_admin_forbidden_create_flavors(self):
"""Fail to create Flavor resource w/o admin role
Integration tests job runs as normal OpenStack user,
and OS::Nova:Flavor is configured to require
admin role in default policy file of Heat.
"""
stack_name = self._stack_rand_name()
ex = self.assertRaises(exc.Forbidden,
self.client.stacks.create,
stack_name=stack_name,
template=self.fl_tmpl)
self.assertIn(self.forbidden_resource_type, ex.message)
def test_forbidden_resource_not_listed(self):
resources = self.client.resource_types.list()
self.assertNotIn(self.forbidden_resource_type,
(r.resource_type for r in resources))