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:
parent
51657807ae
commit
8cdf982210
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue