diff --git a/etc/watcher/policy.json b/etc/watcher/policy.json index f7726778e..fb2f57afd 100644 --- a/etc/watcher/policy.json +++ b/etc/watcher/policy.json @@ -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" } diff --git a/watcher/api/controllers/v1/action.py b/watcher/api/controllers/v1/action.py index 535637fda..d1fbe387f 100644 --- a/watcher/api/controllers/v1/action.py +++ b/watcher/api/controllers/v1/action.py @@ -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) diff --git a/watcher/api/controllers/v1/action_plan.py b/watcher/api/controllers/v1/action_plan.py index 1e3c03a01..7165e0f42 100644 --- a/watcher/api/controllers/v1/action_plan.py +++ b/watcher/api/controllers/v1/action_plan.py @@ -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( diff --git a/watcher/api/controllers/v1/audit.py b/watcher/api/controllers/v1/audit.py index 19cf30209..72462cb83 100644 --- a/watcher/api/controllers/v1/audit.py +++ b/watcher/api/controllers/v1/audit.py @@ -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() diff --git a/watcher/api/controllers/v1/audit_template.py b/watcher/api/controllers/v1/audit_template.py index a1a2f27c0..d004e6138 100644 --- a/watcher/api/controllers/v1/audit_template.py +++ b/watcher/api/controllers/v1/audit_template.py @@ -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() diff --git a/watcher/api/controllers/v1/goal.py b/watcher/api/controllers/v1/goal.py index 06f284595..90271bd3b 100644 --- a/watcher/api/controllers/v1/goal.py +++ b/watcher/api/controllers/v1/goal.py @@ -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) diff --git a/watcher/api/controllers/v1/strategy.py b/watcher/api/controllers/v1/strategy.py index adc861ba9..c640b1e4e 100644 --- a/watcher/api/controllers/v1/strategy.py +++ b/watcher/api/controllers/v1/strategy.py @@ -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) diff --git a/watcher/api/controllers/v1/utils.py b/watcher/api/controllers/v1/utils.py index a69695114..2eaaa4aa7 100644 --- a/watcher/api/controllers/v1/utils.py +++ b/watcher/api/controllers/v1/utils.py @@ -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) diff --git a/watcher/api/hooks.py b/watcher/api/hooks.py index b9b67755b..c76ad2d6e 100644 --- a/watcher/api/hooks.py +++ b/watcher/api/hooks.py @@ -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): diff --git a/watcher/common/context.py b/watcher/common/context.py index 7bd5a1a76..b11c99506 100644 --- a/watcher/common/context.py +++ b/watcher/common/context.py @@ -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): diff --git a/watcher/common/exception.py b/watcher/common/exception.py index 8c517150f..892358498 100644 --- a/watcher/common/exception.py +++ b/watcher/common/exception.py @@ -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") diff --git a/watcher/common/policy.py b/watcher/common/policy.py index e3baf20f2..30caafdda 100644 --- a/watcher/common/policy.py +++ b/watcher/common/policy.py @@ -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) diff --git a/watcher/tests/api/base.py b/watcher/tests/api/base.py index d2fe347ee..4e4698aa6 100644 --- a/watcher/tests/api/base.py +++ b/watcher/tests/api/base.py @@ -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) diff --git a/watcher/tests/api/v1/test_actions.py b/watcher/tests/api/v1/test_actions.py index 6fc93c988..c79823da1 100644 --- a/watcher/tests/api/v1/test_actions.py +++ b/watcher/tests/api/v1/test_actions.py @@ -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"}) diff --git a/watcher/tests/api/v1/test_actions_plans.py b/watcher/tests/api/v1/test_actions_plans.py index a82158575..880019d7a 100644 --- a/watcher/tests/api/v1/test_actions_plans.py +++ b/watcher/tests/api/v1/test_actions_plans.py @@ -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"}) diff --git a/watcher/tests/api/v1/test_audit_templates.py b/watcher/tests/api/v1/test_audit_templates.py index 60109561f..bbfd0d0a1 100644 --- a/watcher/tests/api/v1/test_audit_templates.py +++ b/watcher/tests/api/v1/test_audit_templates.py @@ -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"}) diff --git a/watcher/tests/api/v1/test_audits.py b/watcher/tests/api/v1/test_audits.py index e733659e4..962fd35da 100644 --- a/watcher/tests/api/v1/test_audits.py +++ b/watcher/tests/api/v1/test_audits.py @@ -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"}) diff --git a/watcher/tests/api/v1/test_goals.py b/watcher/tests/api/v1/test_goals.py index 611b35cc5..e3968be61 100644 --- a/watcher/tests/api/v1/test_goals.py +++ b/watcher/tests/api/v1/test_goals.py @@ -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"}) diff --git a/watcher/tests/api/v1/test_strategies.py b/watcher/tests/api/v1/test_strategies.py index 4a7a8c4ed..1203535a9 100644 --- a/watcher/tests/api/v1/test_strategies.py +++ b/watcher/tests/api/v1/test_strategies.py @@ -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"}) diff --git a/watcher/tests/base.py b/watcher/tests/base.py index e6e632266..3a031675a 100644 --- a/watcher/tests/base.py +++ b/watcher/tests/base.py @@ -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'): diff --git a/watcher/tests/fake_policy.py b/watcher/tests/fake_policy.py index 611e4a28a..ec2533ffa 100644 --- a/watcher/tests/fake_policy.py +++ b/watcher/tests/fake_policy.py @@ -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": "" } """ diff --git a/watcher/tests/policy_fixture.py b/watcher/tests/policy_fixture.py index eb380f65a..8a5b4e876 100644 --- a/watcher/tests/policy_fixture.py +++ b/watcher/tests/policy_fixture.py @@ -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()})