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", "admin_api": "role:admin or role:administrator",
"show_password": "!", "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 types
from watcher.api.controllers.v1 import utils as api_utils from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception from watcher.common import exception
from watcher.common import policy
from watcher import objects from watcher import objects
@@ -303,6 +304,10 @@ class ActionsController(rest.RestController):
:param audit_uuid: Optional UUID of an audit, :param audit_uuid: Optional UUID of an audit,
to get only actions for that 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: if action_plan_uuid and audit_uuid:
raise exception.ActionFilterCombinationProhibited raise exception.ActionFilterCombinationProhibited
@@ -327,6 +332,10 @@ class ActionsController(rest.RestController):
:param audit_uuid: Optional UUID of an audit, :param audit_uuid: Optional UUID of an audit,
to get only actions for that 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 # NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1] parent = pecan.request.path.split('/')[:-1][-1]
if parent != "actions": if parent != "actions":
@@ -350,8 +359,10 @@ class ActionsController(rest.RestController):
if self.from_actions: if self.from_actions:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
action = objects.Action.get_by_uuid(pecan.request.context, context = pecan.request.context
action_uuid) action = api_utils.get_resource('Action', action_uuid)
policy.enforce(context, 'action:get', action, action='action:get')
return Action.convert_with_links(action) return Action.convert_with_links(action)
@wsme_pecan.wsexpose(Action, body=Action, status_code=201) @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.api.controllers.v1 import utils as api_utils
from watcher.applier import rpcapi from watcher.applier import rpcapi
from watcher.common import exception from watcher.common import exception
from watcher.common import policy
from watcher import objects from watcher import objects
from watcher.objects import action_plan as ap_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 :param audit_uuid: Optional UUID of an audit, to get only actions
for that audit. 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( return self._get_action_plans_collection(
marker, limit, sort_key, sort_dir, audit_uuid=audit_uuid) 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 :param audit_uuid: Optional UUID of an audit, to get only actions
for that audit. 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 # NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1] parent = pecan.request.path.split('/')[:-1][-1]
if parent != "action_plans": if parent != "action_plans":
@@ -395,8 +404,11 @@ class ActionPlansController(rest.RestController):
if self.from_actionsPlans: if self.from_actionsPlans:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
action_plan = objects.ActionPlan.get_by_uuid( context = pecan.request.context
pecan.request.context, action_plan_uuid) 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) return ActionPlan.convert_with_links(action_plan)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204) @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. :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( action_plan.soft_delete()
pecan.request.context,
action_plan_uuid)
action_plan_to_delete.soft_delete()
@wsme.validate(types.uuid, [ActionPlanPatchType]) @wsme.validate(types.uuid, [ActionPlanPatchType])
@wsme_pecan.wsexpose(ActionPlan, types.uuid, @wsme_pecan.wsexpose(ActionPlan, types.uuid,
@@ -424,9 +437,12 @@ class ActionPlansController(rest.RestController):
if self.from_actionsPlans: if self.from_actionsPlans:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
action_plan_to_update = objects.ActionPlan.get_by_uuid( context = pecan.request.context
pecan.request.context, action_plan_to_update = api_utils.get_resource('ActionPlan',
action_plan_uuid) action_plan_uuid)
policy.enforce(context, 'action_plan:update', action_plan_to_update,
action='action_plan:update')
try: try:
action_plan_dict = action_plan_to_update.as_dict() action_plan_dict = action_plan_to_update.as_dict()
action_plan = ActionPlan(**api_utils.apply_jsonpatch( 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 types
from watcher.api.controllers.v1 import utils as api_utils from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception from watcher.common import exception
from watcher.common import policy
from watcher.common import utils from watcher.common import utils
from watcher.decision_engine import rpcapi from watcher.decision_engine import rpcapi
from watcher import objects from watcher import objects
@@ -308,6 +309,9 @@ class AuditsController(rest.RestController):
:param audit_template: Optional UUID or name of an audit :param audit_template: Optional UUID or name of an audit
template, to get only audits for that audit template. 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, return self._get_audits_collection(marker, limit, sort_key,
sort_dir, sort_dir,
audit_template=audit_template) audit_template=audit_template)
@@ -323,6 +327,9 @@ class AuditsController(rest.RestController):
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :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 # NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1] parent = pecan.request.path.split('/')[:-1][-1]
if parent != "audits": if parent != "audits":
@@ -343,8 +350,10 @@ class AuditsController(rest.RestController):
if self.from_audits: if self.from_audits:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
rpc_audit = objects.Audit.get_by_uuid(pecan.request.context, context = pecan.request.context
audit_uuid) 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) return Audit.convert_with_links(rpc_audit)
@wsme_pecan.wsexpose(Audit, body=AuditPostType, status_code=201) @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. :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() audit = audit_p.as_audit()
if self.from_audits: if self.from_audits:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
@@ -388,6 +401,12 @@ class AuditsController(rest.RestController):
if self.from_audits: if self.from_audits:
raise exception.OperationNotPermitted 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_to_update = objects.Audit.get_by_uuid(pecan.request.context,
audit_uuid) audit_uuid)
try: try:
@@ -417,8 +436,9 @@ class AuditsController(rest.RestController):
:param audit_uuid: UUID of a audit. :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() 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.api.controllers.v1 import utils as api_utils
from watcher.common import context as context_utils from watcher.common import context as context_utils
from watcher.common import exception from watcher.common import exception
from watcher.common import policy
from watcher.common import utils as common_utils from watcher.common import utils as common_utils
from watcher import objects from watcher import objects
@@ -493,6 +494,9 @@ class AuditTemplatesController(rest.RestController):
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :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 = {} filters = {}
if goal: if goal:
if common_utils.is_uuid_like(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_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :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 # NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1] parent = pecan.request.path.split('/')[:-1][-1]
if parent != "audit_templates": if parent != "audit_templates":
@@ -555,14 +563,11 @@ class AuditTemplatesController(rest.RestController):
if self.from_audit_templates: if self.from_audit_templates:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
if common_utils.is_uuid_like(audit_template): context = pecan.request.context
rpc_audit_template = objects.AuditTemplate.get_by_uuid( rpc_audit_template = api_utils.get_resource('AuditTemplate',
pecan.request.context, audit_template)
audit_template) policy.enforce(context, 'audit_template:get', rpc_audit_template,
else: action='audit_template:get')
rpc_audit_template = objects.AuditTemplate.get_by_name(
pecan.request.context,
audit_template)
return AuditTemplate.convert_with_links(rpc_audit_template) return AuditTemplate.convert_with_links(rpc_audit_template)
@@ -578,6 +583,10 @@ class AuditTemplatesController(rest.RestController):
if self.from_audit_templates: if self.from_audit_templates:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
context = pecan.request.context
policy.enforce(context, 'audit_template:create',
action='audit_template:create')
context = pecan.request.context context = pecan.request.context
audit_template = audit_template_postdata.as_audit_template() audit_template = audit_template_postdata.as_audit_template()
audit_template_dict = audit_template.as_dict() audit_template_dict = audit_template.as_dict()
@@ -602,6 +611,13 @@ class AuditTemplatesController(rest.RestController):
if self.from_audit_templates: if self.from_audit_templates:
raise exception.OperationNotPermitted 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): if common_utils.is_uuid_like(audit_template):
audit_template_to_update = objects.AuditTemplate.get_by_uuid( audit_template_to_update = objects.AuditTemplate.get_by_uuid(
pecan.request.context, pecan.request.context,
@@ -639,14 +655,11 @@ class AuditTemplatesController(rest.RestController):
:param audit template_uuid: UUID or name of an audit template. :param audit template_uuid: UUID or name of an audit template.
""" """
context = pecan.request.context
if common_utils.is_uuid_like(audit_template): audit_template_to_delete = api_utils.get_resource('AuditTemplate',
audit_template_to_delete = objects.AuditTemplate.get_by_uuid( audit_template)
pecan.request.context, policy.enforce(context, 'audit_template:update',
audit_template) audit_template_to_delete,
else: action='audit_template:update')
audit_template_to_delete = objects.AuditTemplate.get_by_name(
pecan.request.context,
audit_template)
audit_template_to_delete.soft_delete() 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 types
from watcher.api.controllers.v1 import utils as api_utils from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception from watcher.common import exception
from watcher.common import utils as common_utils from watcher.common import policy
from watcher import objects from watcher import objects
CONF = cfg.CONF CONF = cfg.CONF
@@ -201,6 +201,9 @@ class GoalsController(rest.RestController):
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :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) return self._get_goals_collection(marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(GoalCollection, wtypes.text, int, @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_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :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 # NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1] parent = pecan.request.path.split('/')[:-1][-1]
if parent != "goals": if parent != "goals":
@@ -231,11 +237,8 @@ class GoalsController(rest.RestController):
if self.from_goals: if self.from_goals:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
if common_utils.is_uuid_like(goal): context = pecan.request.context
get_goal_func = objects.Goal.get_by_uuid rpc_goal = api_utils.get_resource('Goal', goal)
else: policy.enforce(context, 'goal:get', rpc_goal, action='goal:get')
get_goal_func = objects.Goal.get_by_name
rpc_goal = get_goal_func(pecan.request.context, goal)
return Goal.convert_with_links(rpc_goal) 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 types
from watcher.api.controllers.v1 import utils as api_utils from watcher.api.controllers.v1 import utils as api_utils
from watcher.common import exception from watcher.common import exception
from watcher.common import policy
from watcher.common import utils as common_utils from watcher.common import utils as common_utils
from watcher import objects from watcher import objects
@@ -223,6 +224,9 @@ class StrategiesController(rest.RestController):
:param sort_key: column to sort results by. Default: id. :param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :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 = {} filters = {}
if goal: if goal:
if common_utils.is_uuid_like(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_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc. :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 # NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1] parent = pecan.request.path.split('/')[:-1][-1]
if parent != "strategies": if parent != "strategies":
@@ -271,11 +278,9 @@ class StrategiesController(rest.RestController):
if self.from_strategies: if self.from_strategies:
raise exception.OperationNotPermitted raise exception.OperationNotPermitted
if common_utils.is_uuid_like(strategy): context = pecan.request.context
get_strategy_func = objects.Strategy.get_by_uuid rpc_strategy = api_utils.get_resource('Strategy', strategy)
else: policy.enforce(context, 'strategy:get', rpc_strategy,
get_strategy_func = objects.Strategy.get_by_name action='strategy:get')
rpc_strategy = get_strategy_func(pecan.request.context, strategy)
return Strategy.convert_with_links(rpc_strategy) return Strategy.convert_with_links(rpc_strategy)

View File

@@ -15,9 +15,12 @@
import jsonpatch import jsonpatch
from oslo_config import cfg from oslo_config import cfg
from oslo_utils import uuidutils
import pecan
import wsme import wsme
from watcher._i18n import _ from watcher._i18n import _
from watcher import objects
CONF = cfg.CONF CONF = cfg.CONF
@@ -75,3 +78,19 @@ def as_filters_dict(**filters):
filters_dict[filter_name] = filter_value filters_dict[filter_name] = filter_value
return filters_dict 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) auth_token = headers.get('X-Auth-Token', auth_token)
show_deleted = headers.get('X-Show-Deleted') show_deleted = headers.get('X-Show-Deleted')
auth_token_info = state.request.environ.get('keystone.token_info') 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') auth_url = headers.get('X-Auth-Url')
if auth_url is None: if auth_url is None:
@@ -72,7 +74,8 @@ class ContextHook(hooks.PecanHook):
project_id=project_id, project_id=project_id,
domain_id=domain_id, domain_id=domain_id,
domain_name=domain_name, domain_name=domain_name,
show_deleted=show_deleted) show_deleted=show_deleted,
roles=roles)
class NoExceptionTracebackHook(hooks.PecanHook): class NoExceptionTracebackHook(hooks.PecanHook):

View File

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

View File

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

View File

@@ -15,55 +15,80 @@
"""Policy Engine For Watcher.""" """Policy Engine For Watcher."""
from oslo_concurrency import lockutils
from oslo_config import cfg from oslo_config import cfg
from oslo_policy import policy from oslo_policy import policy
from watcher.common import exception
_ENFORCER = None _ENFORCER = None
CONF = cfg.CONF CONF = cfg.CONF
@lockutils.synchronized('policy_enforcer', 'watcher-') # we can get a policy enforcer by this init.
def init_enforcer(policy_file=None, rules=None, # oslo policy support change policy rule dynamically.
default_rule=None, use_conf=True): # at present, policy.enforce will reload the policy rules when it checks
"""Synchronously initializes the policy enforcer # the policy files have been touched.
def init(policy_file=None, rules=None,
:param policy_file: Custom policy file to use, if none is specified, default_rule=None, use_conf=True, overwrite=True):
`CONF.policy_file` will be used. """Init an Enforcer class.
: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.
: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 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: 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 return _ENFORCER
def enforce(rule, target, creds, do_raise=False, exc=None, *args, **kwargs): def enforce(context, rule=None, target=None,
"""A shortcut for policy.Enforcer.enforce() 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() enforcer = init()
return enforcer.enforce(rule, target, creds, do_raise=do_raise, credentials = context.to_dict()
exc=exc, *args, **kwargs) 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. # https://bugs.launchpad.net/watcher/+bug/1255115.
# NOTE(deva): import auth_token so we can override a config option # 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 from oslo_config import cfg
import pecan import pecan
import pecan.testing import pecan.testing
from six.moves.urllib import parse as urlparse from six.moves.urllib import parse as urlparse
from watcher.api import hooks from watcher.api import hooks
from watcher.common import context as watcher_context
from watcher.tests.db import base from watcher.tests.db import base
PATH_PREFIX = '/v1' PATH_PREFIX = '/v1'
@@ -243,3 +246,41 @@ class FunctionalTest(base.DbTestCase):
return True return True
except Exception: except Exception:
return False 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. # limitations under the License.
import datetime import datetime
import json
import mock import mock
from oslo_config import cfg from oslo_config import cfg
from wsme import types as wtypes from wsme import types as wtypes
@@ -482,3 +483,49 @@ class TestDelete(api_base.FunctionalTest):
self.assertEqual(403, response.status_int) self.assertEqual(403, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) 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 datetime
import itertools import itertools
import json
import mock import mock
import pecan import pecan
@@ -575,3 +576,65 @@ class TestPatchStateTransitionOk(api_base.FunctionalTest):
self.assertEqual(self.new_state, updated_ap['state']) self.assertEqual(self.new_state, updated_ap['state'])
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(200, response.status_code) 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 datetime
import itertools import itertools
import json
import mock import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_utils import timeutils from oslo_utils import timeutils
from six.moves.urllib import parse as urlparse 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(404, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) 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. # limitations under the License.
import datetime import datetime
import json
import mock import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_utils import timeutils from oslo_utils import timeutils
from wsme import types as wtypes from wsme import types as wtypes
@@ -606,3 +607,74 @@ class TestDelete(api_base.FunctionalTest):
self.assertEqual(404, response.status_int) self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message']) 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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json
from oslo_config import cfg from oslo_config import cfg
from six.moves.urllib import parse as urlparse 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) cfg.CONF.set_override('max_limit', 3, 'api', enforce_type=True)
response = self.get_json('/goals') response = self.get_json('/goals')
self.assertEqual(3, len(response['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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json
from oslo_config import cfg from oslo_config import cfg
from six.moves.urllib import parse as urlparse from six.moves.urllib import parse as urlparse
@@ -187,3 +189,49 @@ class TestListStrategy(api_base.FunctionalTest):
self.assertEqual(2, len(strategies)) self.assertEqual(2, len(strategies))
for strategy in strategies: for strategy in strategies:
self.assertEqual(goal1['uuid'], strategy['goal_uuid']) 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.common import context as watcher_context
from watcher.objects import base as objects_base from watcher.objects import base as objects_base
from watcher.tests import conf_fixture from watcher.tests import conf_fixture
from watcher.tests import policy_fixture
CONF = cfg.CONF CONF = cfg.CONF
@@ -68,6 +69,8 @@ class TestCase(BaseTestCase):
project_id='fake_project', project_id='fake_project',
user_id='fake_user') user_id='fake_user')
self.policy = self.useFixture(policy_fixture.PolicyFixture())
def make_context(*args, **kwargs): def make_context(*args, **kwargs):
# If context hasn't been constructed with token_info # If context hasn't been constructed with token_info
if not kwargs.get('auth_token_info'): if not kwargs.get('auth_token_info'):

View File

@@ -16,9 +16,40 @@
policy_data = """ policy_data = """
{ {
"admin_api": "role:admin or role:administrator", "admin_api": "role:admin or role:administrator",
"public_api": "is_public_api:True", "show_password": "!",
"trusted_call": "rule:admin_api or rule:public_api", "default": "rule:admin_api",
"default": "rule:trusted_call",
"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 import fixtures
from oslo_config import cfg 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 from watcher.tests import fake_policy
CONF = cfg.CONF CONF = cfg.CONF
class PolicyFixture(fixtures.Fixture): class PolicyFixture(fixtures.Fixture):
def __init__(self, compat=None):
self.compat = compat
def setUp(self): def _setUp(self):
super(PolicyFixture, self).setUp()
self.policy_dir = self.useFixture(fixtures.TempDir()) self.policy_dir = self.useFixture(fixtures.TempDir())
self.policy_file_name = os.path.join(self.policy_dir.path, self.policy_file_name = os.path.join(self.policy_dir.path,
'policy.json') 'policy.json')
with open(self.policy_file_name, 'w') as policy_file: with open(self.policy_file_name, 'w') as policy_file:
policy_file.write(fake_policy.get_policy_data(self.compat)) policy_file.write(fake_policy.policy_data)
CONF.set_override('policy_file', self.policy_file_name, policy_opts.set_defaults(CONF)
enforce_type=True) CONF.set_override('policy_file', self.policy_file_name, 'oslo_policy')
w_policy._ENFORCER = None watcher_policy._ENFORCER = None
self.addCleanup(w_policy.get_enforcer().clear) 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()})