Add policy enforcement to ReST API

Currently the native API lacks support for enforcing policy via
policy.json, as is possible for the aws-compatible APIs.

So modify and rename the tenant_local decorator to also enforce
policy, and add tests to ensure the API controllers are all
using the decorator on their actions

Change-Id: Id80d576d5ff5e546da42dbf08ebd653005af14ff
blueprint: request-scoping-policy
This commit is contained in:
Steven Hardy 2013-12-19 18:08:03 +00:00
parent 51657807ae
commit 8cdf982210
11 changed files with 604 additions and 117 deletions

View File

@ -1,7 +1,7 @@
{
"context_is_admin": "role:admin",
"deny_stack_user": "not role:heat_stack_user",
"cloudformation:ListStacks": "rule:deny_stack_user",
"cloudformation:CreateStack": "rule:deny_stack_user",
"cloudformation:DescribeStacks": "rule:deny_stack_user",
@ -25,5 +25,26 @@
"cloudwatch:ListMetrics": "rule:deny_stack_user",
"cloudwatch:PutMetricAlarm": "rule:deny_stack_user",
"cloudwatch:PutMetricData": "",
"cloudwatch:SetAlarmState": "rule:deny_stack_user"
"cloudwatch:SetAlarmState": "rule:deny_stack_user",
"actions:action": "rule:deny_stack_user",
"build_info:build_info": "rule:deny_stack_user",
"events:index": "rule:deny_stack_user",
"events:show": "rule:deny_stack_user",
"resource:index": "rule:deny_stack_user",
"resource:metadata": "",
"resource:show": "rule:deny_stack_user",
"stacks:abandon": "rule:deny_stack_user",
"stacks:create": "rule:deny_stack_user",
"stacks:delete": "rule:deny_stack_user",
"stacks:detail": "rule:deny_stack_user",
"stacks:generate_template": "rule:deny_stack_user",
"stacks:index": "rule:deny_stack_user",
"stacks:list_resource_types": "rule:deny_stack_user",
"stacks:lookup": "rule:deny_stack_user",
"stacks:resource_schema": "rule:deny_stack_user",
"stacks:show": "rule:deny_stack_user",
"stacks:template": "rule:deny_stack_user",
"stacks:update": "rule:deny_stack_user",
"stacks:validate_template": "rule:deny_stack_user"
}

View File

@ -25,6 +25,8 @@ class ActionController(object):
WSGI controller for Actions in Heat v1 API
Implements the API for stack actions
"""
# Define request scope (must match what is in policy.json)
REQUEST_SCOPE = 'actions'
ACTIONS = (SUSPEND, RESUME) = ('suspend', 'resume')

View File

@ -25,12 +25,14 @@ class BuildInfoController(object):
WSGI controller for BuildInfo in Heat v1 API
Returns build information for current app
"""
# Define request scope (must match what is in policy.json)
REQUEST_SCOPE = 'build_info'
def __init__(self, options):
self.options = options
self.engine = rpc_client.EngineClient()
@util.tenant_local
@util.policy_enforce
def build_info(self, req):
engine_revision = self.engine.get_revision(req.context)
build_info = {

View File

@ -73,6 +73,8 @@ class EventController(object):
WSGI controller for Events in Heat v1 API
Implements the API actions
"""
# Define request scope (must match what is in policy.json)
REQUEST_SCOPE = 'events'
def __init__(self, options):
self.options = options

View File

@ -60,6 +60,8 @@ class ResourceController(object):
WSGI controller for Resources in Heat v1 API
Implements the API actions
"""
# Define request scope (must match what is in policy.json)
REQUEST_SCOPE = 'resource'
def __init__(self, options):
self.options = options

View File

@ -142,6 +142,8 @@ class StackController(object):
WSGI controller for stacks resource in Heat v1 API
Implements the API actions
"""
# Define request scope (must match what is in policy.json)
REQUEST_SCOPE = 'stacks'
def __init__(self, options):
self.options = options
@ -150,7 +152,7 @@ class StackController(object):
def default(self, req, **args):
raise exc.HTTPNotFound()
@util.tenant_local
@util.policy_enforce
def index(self, req):
"""
Lists summary information for all stacks
@ -184,7 +186,7 @@ class StackController(object):
return stacks_view.collection(req, stacks=stacks, count=count)
@util.tenant_local
@util.policy_enforce
def detail(self, req):
"""
Lists detailed information for all stacks
@ -193,12 +195,11 @@ class StackController(object):
return {'stacks': [stacks_view.format_stack(req, s) for s in stacks]}
@util.tenant_local
@util.policy_enforce
def create(self, req, body):
"""
Create a new stack
"""
data = InstantiationData(body)
result = self.engine.create_stack(req.context,
@ -214,7 +215,7 @@ class StackController(object):
)
return {'stack': formatted_stack}
@util.tenant_local
@util.policy_enforce
def lookup(self, req, stack_name, path='', body=None):
"""
Redirect to the canonical URL for a stack
@ -302,7 +303,7 @@ class StackController(object):
return self.engine.abandon_stack(req.context,
identity)
@util.tenant_local
@util.policy_enforce
def validate_template(self, req, body):
"""
Implements the ValidateTemplate API action
@ -319,21 +320,21 @@ class StackController(object):
return result
@util.tenant_local
@util.policy_enforce
def list_resource_types(self, req):
"""
Returns a list of valid resource types that may be used in a template.
"""
return {'resource_types': self.engine.list_resource_types(req.context)}
@util.tenant_local
@util.policy_enforce
def resource_schema(self, req, type_name):
"""
Returns the schema of the given resource type.
"""
return self.engine.resource_schema(req.context, type_name)
@util.tenant_local
@util.policy_enforce
def generate_template(self, req, type_name):
"""
Generates a template based on the specified type.

View File

@ -19,15 +19,20 @@ from functools import wraps
from heat.common import identifier
def tenant_local(handler):
def policy_enforce(handler):
'''
Decorator for a handler method that checks the path matches the
request context.
request context and enforce policy defined in policy.json
'''
@wraps(handler)
def handle_stack_method(controller, req, tenant_id, **kwargs):
if req.context.tenant_id != tenant_id:
raise exc.HTTPForbidden()
allowed = req.context.policy.enforce(context=req.context,
action=handler.__name__,
scope=controller.REQUEST_SCOPE)
if not allowed:
raise exc.HTTPForbidden()
return handler(controller, req, **kwargs)
return handle_stack_method
@ -38,7 +43,7 @@ def identified_stack(handler):
Decorator for a handler method that passes a stack identifier in place of
the various path components.
'''
@tenant_local
@policy_enforce
@wraps(handler)
def handle_stack_method(controller, req, stack_name, stack_id, **kwargs):
stack_identity = identifier.HeatIdentifier(req.context.tenant_id,

View File

@ -72,7 +72,7 @@ class Enforcer(object):
return self.enforcer.enforce(rule, target, credentials,
do_raise, exc=exc, *args, **kwargs)
def enforce(self, context, action, target=None):
def enforce(self, context, action, scope=None, target=None):
"""Verifies that the action is valid on the target in this context.
:param context: Heat request context
@ -81,7 +81,7 @@ class Enforcer(object):
:raises: self.exc (defaults to heat.common.exception.Forbidden)
:returns: A non-False value if access is allowed.
"""
_action = '%s:%s' % (self.scope, action)
_action = '%s:%s' % (scope or self.scope, action)
_target = target or {}
return self._check(context, _action, _target, self.exc, action=action)

View File

@ -77,7 +77,7 @@ class WatchControllerTest(HeatTestCase):
dummy_req.context.roles = ['heat_stack_user']
self.m.StubOutWithMock(policy.Enforcer, 'enforce')
policy.Enforcer.enforce(dummy_req.context, 'ListMetrics', {}
policy.Enforcer.enforce(dummy_req.context, 'ListMetrics'
).AndRaise(AttributeError)
self.m.ReplayAll()

File diff suppressed because it is too large Load Diff

View File

@ -12,10 +12,13 @@
# License for the specific language governing permissions and limitations
# under the License.
import mock
from webob import exc
from heat.api.openstack.v1 import util
from heat.common import context
from heat.common import policy
from heat.common.wsgi import Request
from heat.tests.common import HeatTestCase
@ -82,20 +85,37 @@ class TestGetAllowedParams(HeatTestCase):
self.assertNotIn('foo', result)
class TestTenantLocal(HeatTestCase):
class TestPolicyEnforce(HeatTestCase):
def setUp(self):
super(TestTenantLocal, self).setUp()
super(TestPolicyEnforce, self).setUp()
self.req = Request({})
self.req.context = context.RequestContext(tenant_id='foo',
is_admin=False)
def test_tenant_local(self):
@util.tenant_local
def an_action(controller, req):
return 'woot'
class DummyController(object):
REQUEST_SCOPE = 'test'
@util.policy_enforce
def an_action(self, req):
return 'woot'
self.controller = DummyController()
@mock.patch.object(policy.Enforcer, 'enforce')
def test_policy_enforce_tenant_mismatch(self, mock_enforce):
mock_enforce.return_value = True
self.assertEqual('woot',
an_action(None, self.req, tenant_id='foo'))
self.controller.an_action(self.req, 'foo'))
self.assertRaises(exc.HTTPForbidden,
an_action, None, self.req, tenant_id='bar')
self.controller.an_action,
self.req, tenant_id='bar')
@mock.patch.object(policy.Enforcer, 'enforce')
def test_policy_enforce_policy_deny(self, mock_enforce):
mock_enforce.return_value = False
self.assertRaises(exc.HTTPForbidden,
self.controller.an_action,
self.req, tenant_id='foo')