Add policies for API access control to watcher project.

Change-Id: Ibdbe494c636dfaeca9cf2ef8724d0dade1f19c7f
blueprint: watcher-policies
This commit is contained in:
zte-hanrong 2016-06-25 19:43:42 +08:00
parent 6f2c82316c
commit bc06a7d419
22 changed files with 693 additions and 104 deletions

View File

@ -1,5 +1,37 @@
{
"admin_api": "role:admin or role:administrator",
"show_password": "!",
"default": "rule:admin_api"
"default": "rule:admin_api",
"action:detail": "rule:default",
"action:get": "rule:default",
"action:get_all": "rule:default",
"action_plan:delete": "rule:default",
"action_plan:detail": "rule:default",
"action_plan:get": "rule:default",
"action_plan:get_all": "rule:default",
"action_plan:update": "rule:default",
"audit:create": "rule:default",
"audit:delete": "rule:default",
"audit:detail": "rule:default",
"audit:get": "rule:default",
"audit:get_all": "rule:default",
"audit:update": "rule:default",
"audit_template:create": "rule:default",
"audit_template:delete": "rule:default",
"audit_template:detail": "rule:default",
"audit_template:get": "rule:default",
"audit_template:get_all": "rule:default",
"audit_template:update": "rule:default",
"goal:detail": "rule:default",
"goal:get": "rule:default",
"goal:get_all": "rule:default",
"strategy:detail": "rule:default",
"strategy:get": "rule:default",
"strategy:get_all": "rule:default"
}

View File

@ -70,6 +70,7 @@ from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception
from watcher.common import policy
from watcher import objects
@ -303,6 +304,10 @@ class ActionsController(rest.RestController):
:param audit_uuid: Optional UUID of an audit,
to get only actions for that audit.
"""
context = pecan.request.context
policy.enforce(context, 'action:get_all',
action='action:get_all')
if action_plan_uuid and audit_uuid:
raise exception.ActionFilterCombinationProhibited
@ -327,6 +332,10 @@ class ActionsController(rest.RestController):
:param audit_uuid: Optional UUID of an audit,
to get only actions for that audit.
"""
context = pecan.request.context
policy.enforce(context, 'action:detail',
action='action:detail')
# NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "actions":
@ -350,8 +359,10 @@ class ActionsController(rest.RestController):
if self.from_actions:
raise exception.OperationNotPermitted
action = objects.Action.get_by_uuid(pecan.request.context,
action_uuid)
context = pecan.request.context
action = api_utils.get_resource('Action', action_uuid)
policy.enforce(context, 'action:get', action, action='action:get')
return Action.convert_with_links(action)
@wsme_pecan.wsexpose(Action, body=Action, status_code=201)

View File

@ -72,6 +72,7 @@ from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils
from watcher.applier import rpcapi
from watcher.common import exception
from watcher.common import policy
from watcher import objects
from watcher.objects import action_plan as ap_objects
@ -358,6 +359,10 @@ class ActionPlansController(rest.RestController):
:param audit_uuid: Optional UUID of an audit, to get only actions
for that audit.
"""
context = pecan.request.context
policy.enforce(context, 'action_plan:get_all',
action='action_plan:get_all')
return self._get_action_plans_collection(
marker, limit, sort_key, sort_dir, audit_uuid=audit_uuid)
@ -374,6 +379,10 @@ class ActionPlansController(rest.RestController):
:param audit_uuid: Optional UUID of an audit, to get only actions
for that audit.
"""
context = pecan.request.context
policy.enforce(context, 'action_plan:detail',
action='action_plan:detail')
# NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "action_plans":
@ -395,8 +404,11 @@ class ActionPlansController(rest.RestController):
if self.from_actionsPlans:
raise exception.OperationNotPermitted
action_plan = objects.ActionPlan.get_by_uuid(
pecan.request.context, action_plan_uuid)
context = pecan.request.context
action_plan = api_utils.get_resource('ActionPlan', action_plan_uuid)
policy.enforce(
context, 'action_plan:get', action_plan, action='action_plan:get')
return ActionPlan.convert_with_links(action_plan)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
@ -405,11 +417,12 @@ class ActionPlansController(rest.RestController):
:param action_plan_uuid: UUID of a action.
"""
context = pecan.request.context
action_plan = api_utils.get_resource('ActionPlan', action_plan_uuid)
policy.enforce(context, 'action_plan:delete', action_plan,
action='action_plan:delete')
action_plan_to_delete = objects.ActionPlan.get_by_uuid(
pecan.request.context,
action_plan_uuid)
action_plan_to_delete.soft_delete()
action_plan.soft_delete()
@wsme.validate(types.uuid, [ActionPlanPatchType])
@wsme_pecan.wsexpose(ActionPlan, types.uuid,
@ -424,9 +437,12 @@ class ActionPlansController(rest.RestController):
if self.from_actionsPlans:
raise exception.OperationNotPermitted
action_plan_to_update = objects.ActionPlan.get_by_uuid(
pecan.request.context,
action_plan_uuid)
context = pecan.request.context
action_plan_to_update = api_utils.get_resource('ActionPlan',
action_plan_uuid)
policy.enforce(context, 'action_plan:update', action_plan_to_update,
action='action_plan:update')
try:
action_plan_dict = action_plan_to_update.as_dict()
action_plan = ActionPlan(**api_utils.apply_jsonpatch(

View File

@ -44,6 +44,7 @@ from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception
from watcher.common import policy
from watcher.common import utils
from watcher.decision_engine import rpcapi
from watcher import objects
@ -308,6 +309,9 @@ class AuditsController(rest.RestController):
:param audit_template: Optional UUID or name of an audit
template, to get only audits for that audit template.
"""
context = pecan.request.context
policy.enforce(context, 'audit:get_all',
action='audit:get_all')
return self._get_audits_collection(marker, limit, sort_key,
sort_dir,
audit_template=audit_template)
@ -323,6 +327,9 @@ class AuditsController(rest.RestController):
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'audit:detail',
action='audit:detail')
# NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "audits":
@ -343,8 +350,10 @@ class AuditsController(rest.RestController):
if self.from_audits:
raise exception.OperationNotPermitted
rpc_audit = objects.Audit.get_by_uuid(pecan.request.context,
audit_uuid)
context = pecan.request.context
rpc_audit = api_utils.get_resource('Audit', audit_uuid)
policy.enforce(context, 'audit:get', rpc_audit, action='audit:get')
return Audit.convert_with_links(rpc_audit)
@wsme_pecan.wsexpose(Audit, body=AuditPostType, status_code=201)
@ -353,6 +362,10 @@ class AuditsController(rest.RestController):
:param audit_p: a audit within the request body.
"""
context = pecan.request.context
policy.enforce(context, 'audit:create',
action='audit:create')
audit = audit_p.as_audit()
if self.from_audits:
raise exception.OperationNotPermitted
@ -388,6 +401,12 @@ class AuditsController(rest.RestController):
if self.from_audits:
raise exception.OperationNotPermitted
context = pecan.request.context
audit_to_update = api_utils.get_resource('Audit',
audit_uuid)
policy.enforce(context, 'audit:update', audit_to_update,
action='audit:update')
audit_to_update = objects.Audit.get_by_uuid(pecan.request.context,
audit_uuid)
try:
@ -417,8 +436,9 @@ class AuditsController(rest.RestController):
:param audit_uuid: UUID of a audit.
"""
context = pecan.request.context
audit_to_delete = api_utils.get_resource('Audit', audit_uuid)
policy.enforce(context, 'audit:update', audit_to_delete,
action='audit:update')
audit_to_delete = objects.Audit.get_by_uuid(
pecan.request.context,
audit_uuid)
audit_to_delete.soft_delete()

View File

@ -64,6 +64,7 @@ from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import context as context_utils
from watcher.common import exception
from watcher.common import policy
from watcher.common import utils as common_utils
from watcher import objects
@ -493,6 +494,9 @@ class AuditTemplatesController(rest.RestController):
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'audit_template:get_all',
action='audit_template:get_all')
filters = {}
if goal:
if common_utils.is_uuid_like(goal):
@ -522,6 +526,10 @@ class AuditTemplatesController(rest.RestController):
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'audit_template:detail',
action='audit_template:detail')
# NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "audit_templates":
@ -555,14 +563,11 @@ class AuditTemplatesController(rest.RestController):
if self.from_audit_templates:
raise exception.OperationNotPermitted
if common_utils.is_uuid_like(audit_template):
rpc_audit_template = objects.AuditTemplate.get_by_uuid(
pecan.request.context,
audit_template)
else:
rpc_audit_template = objects.AuditTemplate.get_by_name(
pecan.request.context,
audit_template)
context = pecan.request.context
rpc_audit_template = api_utils.get_resource('AuditTemplate',
audit_template)
policy.enforce(context, 'audit_template:get', rpc_audit_template,
action='audit_template:get')
return AuditTemplate.convert_with_links(rpc_audit_template)
@ -578,6 +583,10 @@ class AuditTemplatesController(rest.RestController):
if self.from_audit_templates:
raise exception.OperationNotPermitted
context = pecan.request.context
policy.enforce(context, 'audit_template:create',
action='audit_template:create')
context = pecan.request.context
audit_template = audit_template_postdata.as_audit_template()
audit_template_dict = audit_template.as_dict()
@ -602,6 +611,13 @@ class AuditTemplatesController(rest.RestController):
if self.from_audit_templates:
raise exception.OperationNotPermitted
context = pecan.request.context
audit_template_to_update = api_utils.get_resource('AuditTemplate',
audit_template)
policy.enforce(context, 'audit_template:update',
audit_template_to_update,
action='audit_template:update')
if common_utils.is_uuid_like(audit_template):
audit_template_to_update = objects.AuditTemplate.get_by_uuid(
pecan.request.context,
@ -639,14 +655,11 @@ class AuditTemplatesController(rest.RestController):
:param audit template_uuid: UUID or name of an audit template.
"""
if common_utils.is_uuid_like(audit_template):
audit_template_to_delete = objects.AuditTemplate.get_by_uuid(
pecan.request.context,
audit_template)
else:
audit_template_to_delete = objects.AuditTemplate.get_by_name(
pecan.request.context,
audit_template)
context = pecan.request.context
audit_template_to_delete = api_utils.get_resource('AuditTemplate',
audit_template)
policy.enforce(context, 'audit_template:update',
audit_template_to_delete,
action='audit_template:update')
audit_template_to_delete.soft_delete()

View File

@ -46,7 +46,7 @@ from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception
from watcher.common import utils as common_utils
from watcher.common import policy
from watcher import objects
CONF = cfg.CONF
@ -201,6 +201,9 @@ class GoalsController(rest.RestController):
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'goal:get_all',
action='goal:get_all')
return self._get_goals_collection(marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(GoalCollection, wtypes.text, int,
@ -213,6 +216,9 @@ class GoalsController(rest.RestController):
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'goal:detail',
action='goal:detail')
# NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "goals":
@ -231,11 +237,8 @@ class GoalsController(rest.RestController):
if self.from_goals:
raise exception.OperationNotPermitted
if common_utils.is_uuid_like(goal):
get_goal_func = objects.Goal.get_by_uuid
else:
get_goal_func = objects.Goal.get_by_name
rpc_goal = get_goal_func(pecan.request.context, goal)
context = pecan.request.context
rpc_goal = api_utils.get_resource('Goal', goal)
policy.enforce(context, 'goal:get', rpc_goal, action='goal:get')
return Goal.convert_with_links(rpc_goal)

View File

@ -41,6 +41,7 @@ from watcher.api.controllers.v1 import collection
from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception
from watcher.common import policy
from watcher.common import utils as common_utils
from watcher import objects
@ -223,6 +224,9 @@ class StrategiesController(rest.RestController):
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'strategy:get_all',
action='strategy:get_all')
filters = {}
if goal:
if common_utils.is_uuid_like(goal):
@ -245,6 +249,9 @@ class StrategiesController(rest.RestController):
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
context = pecan.request.context
policy.enforce(context, 'strategy:detail',
action='strategy:detail')
# NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "strategies":
@ -271,11 +278,9 @@ class StrategiesController(rest.RestController):
if self.from_strategies:
raise exception.OperationNotPermitted
if common_utils.is_uuid_like(strategy):
get_strategy_func = objects.Strategy.get_by_uuid
else:
get_strategy_func = objects.Strategy.get_by_name
rpc_strategy = get_strategy_func(pecan.request.context, strategy)
context = pecan.request.context
rpc_strategy = api_utils.get_resource('Strategy', strategy)
policy.enforce(context, 'strategy:get', rpc_strategy,
action='strategy:get')
return Strategy.convert_with_links(rpc_strategy)

View File

@ -15,9 +15,12 @@
import jsonpatch
from oslo_config import cfg
from oslo_utils import uuidutils
import pecan
import wsme
from watcher._i18n import _
from watcher import objects
CONF = cfg.CONF
@ -75,3 +78,19 @@ def as_filters_dict(**filters):
filters_dict[filter_name] = filter_value
return filters_dict
def get_resource(resource, resource_ident):
"""Get the resource from the uuid or logical name.
:param resource: the resource type.
:param resource_ident: the UUID or logical name of the resource.
:returns: The resource.
"""
resource = getattr(objects, resource)
if uuidutils.is_uuid_like(resource_ident):
return resource.get_by_uuid(pecan.request.context, resource_ident)
return resource.get_by_name(pecan.request.context, resource_ident)

View File

@ -56,6 +56,8 @@ class ContextHook(hooks.PecanHook):
auth_token = headers.get('X-Auth-Token', auth_token)
show_deleted = headers.get('X-Show-Deleted')
auth_token_info = state.request.environ.get('keystone.token_info')
roles = (headers.get('X-Roles', None) and
headers.get('X-Roles').split(','))
auth_url = headers.get('X-Auth-Url')
if auth_url is None:
@ -72,7 +74,8 @@ class ContextHook(hooks.PecanHook):
project_id=project_id,
domain_id=domain_id,
domain_name=domain_name,
show_deleted=show_deleted)
show_deleted=show_deleted,
roles=roles)
class NoExceptionTracebackHook(hooks.PecanHook):

View File

@ -20,7 +20,7 @@ class RequestContext(context.RequestContext):
domain_name=None, user=None, user_id=None, project=None,
project_id=None, is_admin=False, is_public_api=False,
read_only=False, show_deleted=False, request_id=None,
trust_id=None, auth_token_info=None):
trust_id=None, auth_token_info=None, roles=None):
"""Stores several additional request parameters:
:param domain_id: The ID of the domain.
@ -44,7 +44,8 @@ class RequestContext(context.RequestContext):
is_admin=is_admin,
read_only=read_only,
show_deleted=show_deleted,
request_id=request_id)
request_id=request_id,
roles=roles)
def to_dict(self):
return {'auth_token': self.auth_token,
@ -61,7 +62,8 @@ class RequestContext(context.RequestContext):
'show_deleted': self.show_deleted,
'request_id': self.request_id,
'trust_id': self.trust_id,
'auth_token_info': self.auth_token_info}
'auth_token_info': self.auth_token_info,
'roles': self.roles}
@classmethod
def from_dict(cls, values):

View File

@ -123,6 +123,10 @@ class NotAuthorized(WatcherException):
code = 403
class PolicyNotAuthorized(NotAuthorized):
msg_fmt = _("Policy doesn't allow %(action)s to be performed.")
class OperationNotPermitted(NotAuthorized):
msg_fmt = _("Operation not permitted")

View File

@ -15,55 +15,80 @@
"""Policy Engine For Watcher."""
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_policy import policy
from watcher.common import exception
_ENFORCER = None
CONF = cfg.CONF
@lockutils.synchronized('policy_enforcer', 'watcher-')
def init_enforcer(policy_file=None, rules=None,
default_rule=None, use_conf=True):
"""Synchronously initializes the policy enforcer
: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.
# we can get a policy enforcer by this init.
# oslo policy support change policy rule dynamically.
# at present, policy.enforce will reload the policy rules when it checks
# the policy files have been touched.
def init(policy_file=None, rules=None,
default_rule=None, use_conf=True, overwrite=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. If
:meth:`load_rules` with ``force_reload=True``,
:meth:`clear` or :meth:`set_rules` with
``overwrite=True`` is called this will be overwritten.
: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 cache or config file.
:param overwrite: Whether to overwrite existing rules when reload rules
from config file.
"""
global _ENFORCER
if _ENFORCER:
return
_ENFORCER = policy.Enforcer(policy_file=policy_file,
rules=rules,
default_rule=default_rule,
use_conf=use_conf)
def get_enforcer():
"""Provides access to the single instance of Policy enforcer."""
if not _ENFORCER:
init_enforcer()
# http://docs.openstack.org/developer/oslo.policy/usage.html
_ENFORCER = policy.Enforcer(CONF,
policy_file=policy_file,
rules=rules,
default_rule=default_rule,
use_conf=use_conf,
overwrite=overwrite)
return _ENFORCER
def enforce(rule, target, creds, do_raise=False, exc=None, *args, **kwargs):
"""A shortcut for policy.Enforcer.enforce()
def enforce(context, rule=None, target=None,
do_raise=True, exc=None, *args, **kwargs):
Checks authorization of a rule against the target and credentials.
"""Checks authorization of a rule against the target and credentials.
:param dict context: As much information about the user performing the
action as possible.
:param rule: The rule to evaluate.
:param dict target: As much information about the object being operated
on as possible.
:param do_raise: Whether to raise an exception or not if check
fails.
:param exc: Class of the exception to raise if the check fails.
Any remaining arguments passed to :meth:`enforce` (both
positional and keyword arguments) will be passed to
the exception class. If not specified,
:class:`PolicyNotAuthorized` will be used.
:return: ``False`` if the policy does not allow the action and `exc` is
not provided; otherwise, returns a value that evaluates to
``True``. Note: for rules using the "case" expression, this
``True`` value will be the specified string from the
expression.
"""
enforcer = get_enforcer()
return enforcer.enforce(rule, target, creds, do_raise=do_raise,
exc=exc, *args, **kwargs)
enforcer = init()
credentials = context.to_dict()
if not exc:
exc = exception.PolicyNotAuthorized
if target is None:
target = {'project_id': context.project_id,
'user_id': context.user_id}
return enforcer.enforce(rule, target, credentials,
do_raise=do_raise, exc=exc, *args, **kwargs)

View File

@ -21,14 +21,17 @@
# https://bugs.launchpad.net/watcher/+bug/1255115.
# NOTE(deva): import auth_token so we can override a config option
from keystonemiddleware import auth_token # noqa
# import mock
import copy
import mock
from oslo_config import cfg
import pecan
import pecan.testing
from six.moves.urllib import parse as urlparse
from watcher.api import hooks
from watcher.common import context as watcher_context
from watcher.tests.db import base
PATH_PREFIX = '/v1'
@ -243,3 +246,41 @@ class FunctionalTest(base.DbTestCase):
return True
except Exception:
return False
class AdminRoleTest(base.DbTestCase):
def setUp(self):
super(AdminRoleTest, self).setUp()
token_info = {
'token': {
'project': {
'id': 'admin'
},
'user': {
'id': 'admin'
}
}
}
self.context = watcher_context.RequestContext(
auth_token_info=token_info,
project_id='admin',
user_id='admin')
def make_context(*args, **kwargs):
# If context hasn't been constructed with token_info
if not kwargs.get('auth_token_info'):
kwargs['auth_token_info'] = copy.deepcopy(token_info)
if not kwargs.get('project_id'):
kwargs['project_id'] = 'admin'
if not kwargs.get('user_id'):
kwargs['user_id'] = 'admin'
if not kwargs.get('roles'):
kwargs['roles'] = ['admin']
context = watcher_context.RequestContext(*args, **kwargs)
return watcher_context.RequestContext.from_dict(context.to_dict())
p = mock.patch.object(watcher_context, 'make_context',
side_effect=make_context)
self.mock_make_context = p.start()
self.addCleanup(p.stop)

View File

@ -11,8 +11,9 @@
# limitations under the License.
import datetime
import json
import mock
from oslo_config import cfg
from wsme import types as wtypes
@ -482,3 +483,49 @@ class TestDelete(api_base.FunctionalTest):
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
class TestActionPolicyEnforcement(api_base.FunctionalTest):
def _common_policy_check(self, rule, func, *arg, **kwarg):
self.policy.set_rules({
"admin_api": "(role:admin or role:administrator)",
"default": "rule:admin_api",
rule: "rule:defaut"})
response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,
json.loads(response.json['error_message'])['faultstring'])
def test_policy_disallow_get_all(self):
self._common_policy_check(
"action:get_all", self.get_json, '/actions',
expect_errors=True)
def test_policy_disallow_get_one(self):
action = obj_utils.create_test_action(self.context)
self._common_policy_check(
"action:get", self.get_json,
'/actions/%s' % action.uuid,
expect_errors=True)
def test_policy_disallow_detail(self):
self._common_policy_check(
"action:detail", self.get_json,
'/actions/detail',
expect_errors=True)
class TestActionPolicyEnforcementWithAdminContext(TestListAction,
api_base.AdminRoleTest):
def setUp(self):
super(TestActionPolicyEnforcementWithAdminContext, self).setUp()
self.policy.set_rules({
"admin_api": "(role:admin or role:administrator)",
"default": "rule:admin_api",
"action:detail": "rule:default",
"action:get": "rule:default",
"action:get_all": "rule:default"})

View File

@ -12,6 +12,7 @@
import datetime
import itertools
import json
import mock
import pecan
@ -575,3 +576,65 @@ class TestPatchStateTransitionOk(api_base.FunctionalTest):
self.assertEqual(self.new_state, updated_ap['state'])
self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code)
class TestActionPlanPolicyEnforcement(api_base.FunctionalTest):
def _common_policy_check(self, rule, func, *arg, **kwarg):
self.policy.set_rules({
"admin_api": "(role:admin or role:administrator)",
"default": "rule:admin_api",
rule: "rule:defaut"})
response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,
json.loads(response.json['error_message'])['faultstring'])
def test_policy_disallow_get_all(self):
self._common_policy_check(
"action_plan:get_all", self.get_json, '/action_plans',
expect_errors=True)
def test_policy_disallow_get_one(self):
action_plan = obj_utils.create_test_action_plan(self.context)
self._common_policy_check(
"action_plan:get", self.get_json,
'/action_plans/%s' % action_plan.uuid,
expect_errors=True)
def test_policy_disallow_detail(self):
self._common_policy_check(
"action_plan:detail", self.get_json,
'/action_plans/detail',
expect_errors=True)
def test_policy_disallow_update(self):
action_plan = obj_utils.create_test_action_plan(self.context)
self._common_policy_check(
"action_plan:update", self.patch_json,
'/action_plans/%s' % action_plan.uuid,
[{'path': '/state', 'value': 'DELETED', 'op': 'replace'}],
expect_errors=True)
def test_policy_disallow_delete(self):
action_plan = obj_utils.create_test_action_plan(self.context)
self._common_policy_check(
"action_plan:delete", self.delete,
'/action_plans/%s' % action_plan.uuid, expect_errors=True)
class TestActionPlanPolicyEnforcementWithAdminContext(TestListActionPlan,
api_base.AdminRoleTest):
def setUp(self):
super(TestActionPlanPolicyEnforcementWithAdminContext, self).setUp()
self.policy.set_rules({
"admin_api": "(role:admin or role:administrator)",
"default": "rule:admin_api",
"action_plan:delete": "rule:default",
"action_plan:detail": "rule:default",
"action_plan:get": "rule:default",
"action_plan:get_all": "rule:default",
"action_plan:update": "rule:default"})

View File

@ -12,8 +12,9 @@
import datetime
import itertools
import json
import mock
from oslo_config import cfg
from oslo_utils import timeutils
from six.moves.urllib import parse as urlparse
@ -654,3 +655,81 @@ class TestDelete(api_base.FunctionalTest):
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
class TestAuaditTemplatePolicyEnforcement(api_base.FunctionalTest):
def _common_policy_check(self, rule, func, *arg, **kwarg):
self.policy.set_rules({
"admin_api": "(role:admin or role:administrator)",
"default": "rule:admin_api",
rule: "rule:defaut"})
response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,
json.loads(response.json['error_message'])['faultstring'])
def test_policy_disallow_get_all(self):
self._common_policy_check(
"audit_template:get_all", self.get_json, '/audit_templates',
expect_errors=True)
def test_policy_disallow_get_one(self):
audit_template = obj_utils.create_test_audit_template(self.context)
self._common_policy_check(
"audit_template:get", self.get_json,
'/audit_templates/%s' % audit_template.uuid,
expect_errors=True)
def test_policy_disallow_detail(self):
self._common_policy_check(
"audit_template:detail", self.get_json,
'/audit_templates/detail',
expect_errors=True)
def test_policy_disallow_update(self):
audit_template = obj_utils.create_test_audit_template(self.context)
self._common_policy_check(
"audit_template:update", self.patch_json,
'/audit_templates/%s' % audit_template.uuid,
[{'path': '/state', 'value': 'SUBMITTED', 'op': 'replace'}],
expect_errors=True)
def test_policy_disallow_create(self):
fake_goal1 = obj_utils.get_test_goal(
self.context, id=1, uuid=utils.generate_uuid(), name="dummy_1")
fake_goal1.create()
fake_strategy1 = obj_utils.get_test_strategy(
self.context, id=1, uuid=utils.generate_uuid(), name="strategy_1",
goal_id=fake_goal1.id)
fake_strategy1.create()
audit_template_dict = post_get_test_audit_template(
goal=fake_goal1.uuid,
strategy=fake_strategy1.uuid)
self._common_policy_check(
"audit_template:create", self.post_json, '/audit_templates',
audit_template_dict, expect_errors=True)
def test_policy_disallow_delete(self):
audit_template = obj_utils.create_test_audit_template(self.context)
self._common_policy_check(
"audit_template:delete", self.delete,
'/audit_templates/%s' % audit_template.uuid, expect_errors=True)
class TestAuditTemplatePolicyWithAdminContext(TestListAuditTemplate,
api_base.AdminRoleTest):
def setUp(self):
super(TestAuditTemplatePolicyWithAdminContext, self).setUp()
self.policy.set_rules({
"admin_api": "(role:admin or role:administrator)",
"default": "rule:admin_api",
"audit_template:create": "rule:default",
"audit_template:delete": "rule:default",
"audit_template:detail": "rule:default",
"audit_template:get": "rule:default",
"audit_template:get_all": "rule:default",
"audit_template:update": "rule:default"})

View File

@ -11,8 +11,9 @@
# limitations under the License.
import datetime
import json
import mock
from oslo_config import cfg
from oslo_utils import timeutils
from wsme import types as wtypes
@ -606,3 +607,74 @@ class TestDelete(api_base.FunctionalTest):
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
class TestAuaditPolicyEnforcement(api_base.FunctionalTest):
def _common_policy_check(self, rule, func, *arg, **kwarg):
self.policy.set_rules({
"admin_api": "(role:admin or role:administrator)",
"default": "rule:admin_api",
rule: "rule:defaut"})
response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,
json.loads(response.json['error_message'])['faultstring'])
def test_policy_disallow_get_all(self):
self._common_policy_check(
"audit:get_all", self.get_json, '/audits',
expect_errors=True)
def test_policy_disallow_get_one(self):
audit = obj_utils.create_test_audit(self.context)
self._common_policy_check(
"audit:get", self.get_json,
'/audits/%s' % audit.uuid,
expect_errors=True)
def test_policy_disallow_detail(self):
self._common_policy_check(
"audit:detail", self.get_json,
'/audits/detail',
expect_errors=True)
def test_policy_disallow_update(self):
audit = obj_utils.create_test_audit(self.context)
self._common_policy_check(
"audit:update", self.patch_json,
'/audits/%s' % audit.uuid,
[{'path': '/state', 'value': 'SUBMITTED', 'op': 'replace'}],
expect_errors=True)
def test_policy_disallow_create(self):
audit_dict = post_get_test_audit(state=objects.audit.State.PENDING)
del audit_dict['uuid']
del audit_dict['state']
self._common_policy_check(
"audit:create", self.post_json, '/audits', audit_dict,
expect_errors=True)
def test_policy_disallow_delete(self):
audit = obj_utils.create_test_audit(self.context)
self._common_policy_check(
"audit:delete", self.delete,
'/audits/%s' % audit.uuid, expect_errors=True)
class TestAuditEnforcementWithAdminContext(TestListAudit,
api_base.AdminRoleTest):
def setUp(self):
super(TestAuditEnforcementWithAdminContext, self).setUp()
self.policy.set_rules({
"admin_api": "(role:admin or role:administrator)",
"default": "rule:admin_api",
"audit:create": "rule:default",
"audit:delete": "rule:default",
"audit:detail": "rule:default",
"audit:get": "rule:default",
"audit:get_all": "rule:default",
"audit:update": "rule:default"})

View File

@ -10,6 +10,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from oslo_config import cfg
from six.moves.urllib import parse as urlparse
@ -118,3 +120,49 @@ class TestListGoal(api_base.FunctionalTest):
cfg.CONF.set_override('max_limit', 3, 'api', enforce_type=True)
response = self.get_json('/goals')
self.assertEqual(3, len(response['goals']))
class TestGoalPolicyEnforcement(api_base.FunctionalTest):
def _common_policy_check(self, rule, func, *arg, **kwarg):
self.policy.set_rules({
"admin_api": "(role:admin or role:administrator)",
"default": "rule:admin_api",
rule: "rule:defaut"})
response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,
json.loads(response.json['error_message'])['faultstring'])
def test_policy_disallow_get_all(self):
self._common_policy_check(
"goal:get_all", self.get_json, '/goals',
expect_errors=True)
def test_policy_disallow_get_one(self):
goal = obj_utils.create_test_goal(self.context)
self._common_policy_check(
"goal:get", self.get_json,
'/goals/%s' % goal.uuid,
expect_errors=True)
def test_policy_disallow_detail(self):
self._common_policy_check(
"goal:detail", self.get_json,
'/goals/detail',
expect_errors=True)
class TestGoalPolicyEnforcementWithAdminContext(TestListGoal,
api_base.AdminRoleTest):
def setUp(self):
super(TestGoalPolicyEnforcementWithAdminContext, self).setUp()
self.policy.set_rules({
"admin_api": "(role:admin or role:administrator)",
"default": "rule:admin_api",
"goal:detail": "rule:default",
"goal:get_all": "rule:default",
"goal:get_one": "rule:default"})

View File

@ -10,6 +10,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from oslo_config import cfg
from six.moves.urllib import parse as urlparse
@ -187,3 +189,49 @@ class TestListStrategy(api_base.FunctionalTest):
self.assertEqual(2, len(strategies))
for strategy in strategies:
self.assertEqual(goal1['uuid'], strategy['goal_uuid'])
class TestStrategyPolicyEnforcement(api_base.FunctionalTest):
def _common_policy_check(self, rule, func, *arg, **kwarg):
self.policy.set_rules({
"admin_api": "(role:admin or role:administrator)",
"default": "rule:admin_api",
rule: "rule:defaut"})
response = func(*arg, **kwarg)
self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(
"Policy doesn't allow %s to be performed." % rule,
json.loads(response.json['error_message'])['faultstring'])
def test_policy_disallow_get_all(self):
self._common_policy_check(
"strategy:get_all", self.get_json, '/strategies',
expect_errors=True)
def test_policy_disallow_get_one(self):
strategy = obj_utils.create_test_strategy(self.context)
self._common_policy_check(
"strategy:get", self.get_json,
'/strategies/%s' % strategy.uuid,
expect_errors=True)
def test_policy_disallow_detail(self):
self._common_policy_check(
"strategy:detail", self.get_json,
'/strategies/detail',
expect_errors=True)
class TestStrategyEnforcementWithAdminContext(TestListStrategy,
api_base.AdminRoleTest):
def setUp(self):
super(TestStrategyEnforcementWithAdminContext, self).setUp()
self.policy.set_rules({
"admin_api": "(role:admin or role:administrator)",
"default": "rule:admin_api",
"strategy:detail": "rule:default",
"strategy:get": "rule:default",
"strategy:get_all": "rule:default"})

View File

@ -29,6 +29,7 @@ import testscenarios
from watcher.common import context as watcher_context
from watcher.objects import base as objects_base
from watcher.tests import conf_fixture
from watcher.tests import policy_fixture
CONF = cfg.CONF
@ -68,6 +69,8 @@ class TestCase(BaseTestCase):
project_id='fake_project',
user_id='fake_user')
self.policy = self.useFixture(policy_fixture.PolicyFixture())
def make_context(*args, **kwargs):
# If context hasn't been constructed with token_info
if not kwargs.get('auth_token_info'):

View File

@ -16,9 +16,40 @@
policy_data = """
{
"admin_api": "role:admin or role:administrator",
"public_api": "is_public_api:True",
"trusted_call": "rule:admin_api or rule:public_api",
"default": "rule:trusted_call",
"show_password": "!",
"default": "rule:admin_api",
"action:detail": "",
"action:get": "",
"action:get_all": "",
"action_plan:delete": "",
"action_plan:detail": "",
"action_plan:get": "",
"action_plan:get_all": "",
"action_plan:update": "",
"audit:create": "",
"audit:delete": "",
"audit:detail": "",
"audit:get": "",
"audit:get_all": "",
"audit:update": "",
"audit_template:create": "",
"audit_template:delete": "",
"audit_template:detail": "",
"audit_template:get": "",
"audit_template:get_all": "",
"audit_template:update": "",
"goal:detail": "",
"goal:get": "",
"goal:get_all": "",
"strategy:detail": "",
"strategy:get": "",
"strategy:get_all": ""
}
"""

View File

@ -16,25 +16,29 @@ import os
import fixtures
from oslo_config import cfg
from oslo_policy import _parser
from oslo_policy import opts as policy_opts
from watcher.common import policy as w_policy
from watcher.common import policy as watcher_policy
from watcher.tests import fake_policy
CONF = cfg.CONF
class PolicyFixture(fixtures.Fixture):
def __init__(self, compat=None):
self.compat = compat
def setUp(self):
super(PolicyFixture, self).setUp()
def _setUp(self):
self.policy_dir = self.useFixture(fixtures.TempDir())
self.policy_file_name = os.path.join(self.policy_dir.path,
'policy.json')
with open(self.policy_file_name, 'w') as policy_file:
policy_file.write(fake_policy.get_policy_data(self.compat))
CONF.set_override('policy_file', self.policy_file_name,
enforce_type=True)
w_policy._ENFORCER = None
self.addCleanup(w_policy.get_enforcer().clear)
policy_file.write(fake_policy.policy_data)
policy_opts.set_defaults(CONF)
CONF.set_override('policy_file', self.policy_file_name, 'oslo_policy')
watcher_policy._ENFORCER = None
self.addCleanup(watcher_policy.init().clear)
def set_rules(self, rules):
policy = watcher_policy._ENFORCER
policy.set_rules({k: _parser.parse_rule(v)
for k, v in rules.items()})