Add support for Oslo Policies to Trove
The Oslo Policy library provides support for RBAC policy enforcement across all OpenStack services. Update the devstack plugin to copy the default policy file over to /etc/trove in the gate environments. Note: Not adding a rule for 'reset-password' instance action as that API was discontinued years ago and is now just waiting for removal (Bug: 1645866). DocImpact Co-Authored-By: Ali Adil <aadil@tesora.com> Change-Id: Ic443a4c663301840406cad537159eab7b0b5ed1c Implements: blueprint trove-policy
This commit is contained in:
parent
77fd7014c0
commit
21250cf20c
@ -110,6 +110,9 @@ function configure_trove {
|
||||
# Copy api-paste file over to the trove conf dir
|
||||
cp $TROVE_LOCAL_API_PASTE_INI $TROVE_API_PASTE_INI
|
||||
|
||||
# Copy the default policy file over to the trove conf dir
|
||||
cp $TROVE_LOCAL_POLICY_JSON $TROVE_POLICY_JSON
|
||||
|
||||
# (Re)create trove conf files
|
||||
rm -f $TROVE_CONF
|
||||
rm -f $TROVE_TASKMANAGER_CONF
|
||||
|
@ -21,9 +21,11 @@ TROVE_TASKMANAGER_CONF=${TROVE_TASKMANAGER_CONF:-${TROVE_CONF_DIR}/trove-taskman
|
||||
TROVE_CONDUCTOR_CONF=${TROVE_CONDUCTOR_CONF:-${TROVE_CONF_DIR}/trove-conductor.conf}
|
||||
TROVE_GUESTAGENT_CONF=${TROVE_GUESTAGENT_CONF:-${TROVE_CONF_DIR}/trove-guestagent.conf}
|
||||
TROVE_API_PASTE_INI=${TROVE_API_PASTE_INI:-${TROVE_CONF_DIR}/api-paste.ini}
|
||||
TROVE_POLICY_JSON=${TROVE_POLICY_JSON:-${TROVE_CONF_DIR}/policy.json}
|
||||
|
||||
TROVE_LOCAL_CONF_DIR=${TROVE_LOCAL_CONF_DIR:-${TROVE_DIR}/etc/trove}
|
||||
TROVE_LOCAL_API_PASTE_INI=${TROVE_LOCAL_API_PASTE_INI:-${TROVE_LOCAL_CONF_DIR}/api-paste.ini}
|
||||
TROVE_LOCAL_POLICY_JSON=${TROVE_LOCAL_POLICY_JSON:-${TROVE_LOCAL_CONF_DIR}/policy.json}
|
||||
TROVE_AUTH_CACHE_DIR=${TROVE_AUTH_CACHE_DIR:-/var/cache/trove}
|
||||
TROVE_DATASTORE_TYPE=${TROVE_DATASTORE_TYPE:-"mysql"}
|
||||
TROVE_DATASTORE_VERSION=${TROVE_DATASTORE_VERSION:-"5.6"}
|
||||
|
96
etc/trove/policy.json
Normal file
96
etc/trove/policy.json
Normal file
@ -0,0 +1,96 @@
|
||||
{
|
||||
"admin": "role:admin or is_admin:True",
|
||||
"admin_or_owner": "rule:admin or tenant:%(tenant)s",
|
||||
"default": "rule:admin_or_owner",
|
||||
|
||||
"instance:create": "rule:admin_or_owner",
|
||||
"instance:delete": "rule:admin_or_owner",
|
||||
"instance:force_delete": "rule:admin_or_owner",
|
||||
"instance:index": "rule:admin_or_owner",
|
||||
"instance:show": "rule:admin_or_owner",
|
||||
"instance:update": "rule:admin_or_owner",
|
||||
"instance:edit": "rule:admin_or_owner",
|
||||
"instance:restart": "rule:admin_or_owner",
|
||||
"instance:resize_volume": "rule:admin_or_owner",
|
||||
"instance:resize_flavor": "rule:admin_or_owner",
|
||||
"instance:reset_status": "rule:admin",
|
||||
"instance:promote_to_replica_source": "rule:admin_or_owner",
|
||||
"instance:eject_replica_source": "rule:admin_or_owner",
|
||||
"instance:configuration": "rule:admin_or_owner",
|
||||
"instance:guest_log_list": "rule:admin_or_owner",
|
||||
"instance:backups": "rule:admin_or_owner",
|
||||
"instance:module_list": "rule:admin_or_owner",
|
||||
"instance:module_apply": "rule:admin_or_owner",
|
||||
"instance:module_remove": "rule:admin_or_owner",
|
||||
|
||||
"instance:extension:root:create": "rule:admin_or_owner",
|
||||
"instance:extension:root:delete": "rule:admin_or_owner",
|
||||
"instance:extension:root:index": "rule:admin_or_owner",
|
||||
|
||||
"instance:extension:user:create": "rule:admin_or_owner",
|
||||
"instance:extension:user:delete": "rule:admin_or_owner",
|
||||
"instance:extension:user:index": "rule:admin_or_owner",
|
||||
"instance:extension:user:show": "rule:admin_or_owner",
|
||||
"instance:extension:user:update": "rule:admin_or_owner",
|
||||
"instance:extension:user:update_all": "rule:admin_or_owner",
|
||||
|
||||
"instance:extension:user_access:update": "rule:admin_or_owner",
|
||||
"instance:extension:user_access:delete": "rule:admin_or_owner",
|
||||
"instance:extension:user_access:index": "rule:admin_or_owner",
|
||||
|
||||
"instance:extension:database:create": "rule:admin_or_owner",
|
||||
"instance:extension:database:delete": "rule:admin_or_owner",
|
||||
"instance:extension:database:index": "rule:admin_or_owner",
|
||||
"instance:extension:database:show": "rule:admin_or_owner",
|
||||
|
||||
"cluster:create": "rule:admin_or_owner",
|
||||
"cluster:delete": "rule:admin_or_owner",
|
||||
"cluster:force_delete": "rule:admin_or_owner",
|
||||
"cluster:index": "rule:admin_or_owner",
|
||||
"cluster:show": "rule:admin_or_owner",
|
||||
"cluster:show_instance": "rule:admin_or_owner",
|
||||
"cluster:action": "rule:admin_or_owner",
|
||||
"cluster:reset-status": "rule:admin",
|
||||
|
||||
"cluster:extension:root:create": "rule:admin_or_owner",
|
||||
"cluster:extension:root:delete": "rule:admin_or_owner",
|
||||
"cluster:extension:root:index": "rule:admin_or_owner",
|
||||
|
||||
"backup:create": "rule:admin_or_owner",
|
||||
"backup:delete": "rule:admin_or_owner",
|
||||
"backup:index": "rule:admin_or_owner",
|
||||
"backup:show": "rule:admin_or_owner",
|
||||
|
||||
"configuration:create": "rule:admin_or_owner",
|
||||
"configuration:delete": "rule:admin_or_owner",
|
||||
"configuration:index": "rule:admin_or_owner",
|
||||
"configuration:show": "rule:admin_or_owner",
|
||||
"configuration:instances": "rule:admin_or_owner",
|
||||
"configuration:update": "rule:admin_or_owner",
|
||||
"configuration:edit": "rule:admin_or_owner",
|
||||
|
||||
"configuration-parameter:index": "rule:admin_or_owner",
|
||||
"configuration-parameter:show": "rule:admin_or_owner",
|
||||
"configuration-parameter:index_by_version": "rule:admin_or_owner",
|
||||
"configuration-parameter:show_by_version": "rule:admin_or_owner",
|
||||
|
||||
"datastore:index": "",
|
||||
"datastore:show": "",
|
||||
"datastore:version_show": "",
|
||||
"datastore:version_show_by_uuid": "",
|
||||
"datastore:version_index": "",
|
||||
"datastore:list_associated_flavors": "",
|
||||
"datastore:list_associated_volume_types": "",
|
||||
|
||||
"flavor:index": "",
|
||||
"flavor:show": "",
|
||||
|
||||
"limits:index": "rule:admin_or_owner",
|
||||
|
||||
"module:create": "rule:admin_or_owner",
|
||||
"module:delete": "rule:admin_or_owner",
|
||||
"module:index": "rule:admin_or_owner",
|
||||
"module:show": "rule:admin_or_owner",
|
||||
"module:instances": "rule:admin_or_owner",
|
||||
"module:update": "rule:admin_or_owner"
|
||||
}
|
8
releasenotes/notes/use-oslo-policy-bbd1b911e6487c36.yaml
Normal file
8
releasenotes/notes/use-oslo-policy-bbd1b911e6487c36.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- Add RBAC (role-based access control)
|
||||
enforcement on all trove APIs.
|
||||
Allows to define a role-based access rule
|
||||
for every trove API call
|
||||
(rule definitions are available in
|
||||
/etc/trove/policy.json).
|
@ -47,3 +47,4 @@ oslo.db!=4.13.1,!=4.13.2,>=4.11.0 # Apache-2.0
|
||||
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
|
||||
xmltodict>=0.10.1 # MIT
|
||||
pycrypto>=2.6 # Public Domain
|
||||
oslo.policy>=1.17.0 # Apache-2.0
|
||||
|
@ -22,6 +22,7 @@ from trove.common.i18n import _
|
||||
from trove.common import notification
|
||||
from trove.common.notification import StartNotification
|
||||
from trove.common import pagination
|
||||
from trove.common import policy
|
||||
from trove.common import wsgi
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -40,6 +41,7 @@ class BackupController(wsgi.Controller):
|
||||
LOG.debug("Listing backups for tenant %s" % tenant_id)
|
||||
datastore = req.GET.get('datastore')
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
policy.authorize_on_tenant(context, 'backup:index')
|
||||
backups, marker = Backup.list(context, datastore)
|
||||
view = views.BackupViews(backups)
|
||||
paged = pagination.SimplePaginatedDataView(req.url, 'backups', view,
|
||||
@ -52,11 +54,14 @@ class BackupController(wsgi.Controller):
|
||||
% (tenant_id, id))
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
backup = Backup.get_by_id(context, id)
|
||||
policy.authorize_on_target(context, 'backup:show',
|
||||
{'tenant': backup.tenant_id})
|
||||
return wsgi.Result(views.BackupView(backup).data(), 200)
|
||||
|
||||
def create(self, req, body, tenant_id):
|
||||
LOG.info(_("Creating a backup for tenant %s"), tenant_id)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
policy.authorize_on_tenant(context, 'backup:create')
|
||||
data = body['backup']
|
||||
instance = data['instance']
|
||||
name = data['name']
|
||||
@ -76,6 +81,9 @@ class BackupController(wsgi.Controller):
|
||||
'ID: %(backup_id)s') %
|
||||
{'tenant_id': tenant_id, 'backup_id': id})
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
backup = Backup.get_by_id(context, id)
|
||||
policy.authorize_on_target(context, 'backup:delete',
|
||||
{'tenant': backup.tenant_id})
|
||||
context.notification = notification.DBaaSBackupDelete(context,
|
||||
request=req)
|
||||
with StartNotification(context, backup_id=id):
|
||||
|
@ -25,6 +25,7 @@ from trove.common.i18n import _
|
||||
from trove.common import notification
|
||||
from trove.common.notification import StartNotification
|
||||
from trove.common import pagination
|
||||
from trove.common import policy
|
||||
from trove.common import utils
|
||||
from trove.common import wsgi
|
||||
from trove.datastore import models as datastore_models
|
||||
@ -39,6 +40,11 @@ class ClusterController(wsgi.Controller):
|
||||
"""Controller for cluster functionality."""
|
||||
schemas = apischema.cluster.copy()
|
||||
|
||||
@classmethod
|
||||
def authorize_cluster_action(cls, context, cluster_rule_name, cluster):
|
||||
policy.authorize_on_target(context, 'cluster:%s' % cluster_rule_name,
|
||||
{'tenant': cluster.tenant_id})
|
||||
|
||||
@classmethod
|
||||
def get_action_schema(cls, body, action_schema):
|
||||
action_type = list(body.keys())[0]
|
||||
@ -58,15 +64,25 @@ class ClusterController(wsgi.Controller):
|
||||
{"req": req, "id": id, "tenant_id": tenant_id})
|
||||
if not body:
|
||||
raise exception.BadRequest(_("Invalid request body."))
|
||||
|
||||
if len(body) != 1:
|
||||
raise exception.BadRequest(_("Action request should have exactly"
|
||||
" one action specified in body"))
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
cluster = models.Cluster.load(context, id)
|
||||
if ('reset-status' in body and
|
||||
'force_delete' not in body['reset-status']):
|
||||
self.authorize_cluster_action(context, 'reset-status', cluster)
|
||||
elif ('reset-status' in body and
|
||||
'force_delete' in body['reset-status']):
|
||||
self.authorize_cluster_action(context, 'force_delete', cluster)
|
||||
else:
|
||||
self.authorize_cluster_action(context, 'action', cluster)
|
||||
cluster.action(context, req, *next(iter(body.items())))
|
||||
|
||||
view = views.load_view(cluster, req=req, load_servers=False)
|
||||
wsgi_result = wsgi.Result(view.data(), 202)
|
||||
|
||||
return wsgi_result
|
||||
|
||||
def show(self, req, tenant_id, id):
|
||||
@ -77,6 +93,7 @@ class ClusterController(wsgi.Controller):
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
cluster = models.Cluster.load(context, id)
|
||||
self.authorize_cluster_action(context, 'show', cluster)
|
||||
return wsgi.Result(views.load_view(cluster, req=req).data(), 200)
|
||||
|
||||
def show_instance(self, req, tenant_id, cluster_id, instance_id):
|
||||
@ -92,6 +109,7 @@ class ClusterController(wsgi.Controller):
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
cluster = models.Cluster.load(context, cluster_id)
|
||||
self.authorize_cluster_action(context, 'show_instance', cluster)
|
||||
instance = models.Cluster.load_instance(context, cluster.id,
|
||||
instance_id)
|
||||
return wsgi.Result(views.ClusterInstanceDetailView(
|
||||
@ -105,6 +123,7 @@ class ClusterController(wsgi.Controller):
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
cluster = models.Cluster.load(context, id)
|
||||
self.authorize_cluster_action(context, 'delete', cluster)
|
||||
context.notification = notification.DBaaSClusterDelete(context,
|
||||
request=req)
|
||||
with StartNotification(context, cluster_id=id):
|
||||
@ -118,9 +137,19 @@ class ClusterController(wsgi.Controller):
|
||||
"tenant_id": tenant_id})
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
|
||||
# This theoretically allows the Admin tenant list clusters for
|
||||
# only one particular tenant as opposed to listing all clusters for
|
||||
# for all tenants.
|
||||
# * As far as I can tell this is the only call which actually uses the
|
||||
# passed-in 'tenant_id' for anything.
|
||||
if not context.is_admin and context.tenant != tenant_id:
|
||||
raise exception.TroveOperationAuthError(tenant_id=context.tenant)
|
||||
|
||||
# The rule checks that the currently authenticated tenant can perform
|
||||
# the 'cluster-list' action.
|
||||
policy.authorize_on_tenant(context, 'cluster:index')
|
||||
|
||||
# load all clusters and instances for the tenant
|
||||
clusters, marker = models.Cluster.load_all(context, tenant_id)
|
||||
view = views.ClustersView(clusters, req=req)
|
||||
@ -134,6 +163,8 @@ class ClusterController(wsgi.Controller):
|
||||
{"tenant_id": tenant_id, "req": req, "body": body})
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
policy.authorize_on_tenant(context, 'cluster:create')
|
||||
|
||||
name = body['cluster']['name']
|
||||
datastore_args = body['cluster'].get('datastore', {})
|
||||
datastore, datastore_version = (
|
||||
|
@ -236,11 +236,6 @@ class UnprocessableEntity(TroveError):
|
||||
message = _("Unable to process the contained request.")
|
||||
|
||||
|
||||
class UnauthorizedRequest(TroveError):
|
||||
|
||||
message = _("Unauthorized request.")
|
||||
|
||||
|
||||
class CannotResizeToSameSize(TroveError):
|
||||
|
||||
message = _("No change was requested in the size of the instance.")
|
||||
@ -309,6 +304,11 @@ class Forbidden(TroveError):
|
||||
message = _("User does not have admin privileges.")
|
||||
|
||||
|
||||
class PolicyNotAuthorized(Forbidden):
|
||||
|
||||
message = _("Policy doesn't allow %(action)s to be performed.")
|
||||
|
||||
|
||||
class InvalidModelError(TroveError):
|
||||
|
||||
message = _("The following values are invalid: %(errors)s.")
|
||||
@ -538,6 +538,10 @@ class ModuleInvalid(Forbidden):
|
||||
message = _("The module is invalid: %(reason)s")
|
||||
|
||||
|
||||
class InstanceNotFound(NotFound):
|
||||
message = _("Instance '%(instance)s' cannot be found.")
|
||||
|
||||
|
||||
class ClusterNotFound(NotFound):
|
||||
message = _("Cluster '%(cluster)s' cannot be found.")
|
||||
|
||||
@ -622,3 +626,8 @@ class ImageNotFound(NotFound):
|
||||
class DatastoreVersionAlreadyExists(BadRequest):
|
||||
|
||||
message = _("A datastore version with the name '%(name)s' already exists.")
|
||||
|
||||
|
||||
class LogAccessForbidden(Forbidden):
|
||||
|
||||
message = _("You must be admin to %(action)s log '%(log)s'.")
|
||||
|
260
trove/common/policy.py
Normal file
260
trove/common/policy.py
Normal file
@ -0,0 +1,260 @@
|
||||
# Copyright 2016 Tesora Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_policy import policy
|
||||
|
||||
from trove.common import exception as trove_exceptions
|
||||
|
||||
CONF = cfg.CONF
|
||||
_ENFORCER = None
|
||||
|
||||
|
||||
base_rules = [
|
||||
policy.RuleDefault(
|
||||
'admin',
|
||||
'role:admin or is_admin:True',
|
||||
description='Must be an administrator.'),
|
||||
policy.RuleDefault(
|
||||
'admin_or_owner',
|
||||
'rule:admin or tenant:%(tenant)s',
|
||||
description='Must be an administrator or owner of the object.'),
|
||||
policy.RuleDefault(
|
||||
'default',
|
||||
'rule:admin_or_owner',
|
||||
description='Must be an administrator or owner of the object.')
|
||||
]
|
||||
|
||||
instance_rules = [
|
||||
policy.RuleDefault(
|
||||
'instance:create', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:delete', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:force_delete', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:index', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:show', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:update', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:edit', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:restart', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:resize_volume', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:resize_flavor', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:reset_status', 'rule:admin'),
|
||||
policy.RuleDefault(
|
||||
'instance:promote_to_replica_source', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:eject_replica_source', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:configuration', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:guest_log_list', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:backups', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:module_list', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:module_apply', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:module_remove', 'rule:admin_or_owner'),
|
||||
|
||||
policy.RuleDefault(
|
||||
'instance:extension:root:create', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:extension:root:delete', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:extension:root:index', 'rule:admin_or_owner'),
|
||||
|
||||
policy.RuleDefault(
|
||||
'instance:extension:user:create', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:extension:user:delete', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:extension:user:index', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:extension:user:show', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:extension:user:update', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:extension:user:update_all', 'rule:admin_or_owner'),
|
||||
|
||||
policy.RuleDefault(
|
||||
'instance:extension:user_access:update', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:extension:user_access:delete', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:extension:user_access:index', 'rule:admin_or_owner'),
|
||||
|
||||
policy.RuleDefault(
|
||||
'instance:extension:database:create', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:extension:database:delete', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:extension:database:index', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'instance:extension:database:show', 'rule:admin_or_owner'),
|
||||
|
||||
policy.RuleDefault(
|
||||
'cluster:create', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'cluster:delete', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'cluster:force_delete', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'cluster:index', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'cluster:show', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'cluster:show_instance', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'cluster:action', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'cluster:reset-status', 'rule:admin'),
|
||||
|
||||
policy.RuleDefault(
|
||||
'cluster:extension:root:create', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'cluster:extension:root:delete', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'cluster:extension:root:index', 'rule:admin_or_owner'),
|
||||
|
||||
policy.RuleDefault(
|
||||
'backup:create', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'backup:delete', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'backup:index', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'backup:show', 'rule:admin_or_owner'),
|
||||
|
||||
policy.RuleDefault(
|
||||
'configuration:create', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'configuration:delete', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'configuration:index', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'configuration:show', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'configuration:instances', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'configuration:update', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'configuration:edit', 'rule:admin_or_owner'),
|
||||
|
||||
policy.RuleDefault(
|
||||
'configuration-parameter:index', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'configuration-parameter:show', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'configuration-parameter:index_by_version', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'configuration-parameter:show_by_version', 'rule:admin_or_owner'),
|
||||
|
||||
policy.RuleDefault(
|
||||
'datastore:index', ''),
|
||||
policy.RuleDefault(
|
||||
'datastore:show', ''),
|
||||
policy.RuleDefault(
|
||||
'datastore:version_show', ''),
|
||||
policy.RuleDefault(
|
||||
'datastore:version_show_by_uuid', ''),
|
||||
policy.RuleDefault(
|
||||
'datastore:version_index', ''),
|
||||
policy.RuleDefault(
|
||||
'datastore:list_associated_flavors', ''),
|
||||
policy.RuleDefault(
|
||||
'datastore:list_associated_volume_types', ''),
|
||||
|
||||
policy.RuleDefault(
|
||||
'flavor:index', ''),
|
||||
policy.RuleDefault(
|
||||
'flavor:show', ''),
|
||||
|
||||
policy.RuleDefault(
|
||||
'limits:index', 'rule:admin_or_owner'),
|
||||
|
||||
policy.RuleDefault(
|
||||
'module:create', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'module:delete', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'module:index', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'module:show', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'module:instances', 'rule:admin_or_owner'),
|
||||
policy.RuleDefault(
|
||||
'module:update', 'rule:admin_or_owner'),
|
||||
]
|
||||
|
||||
|
||||
def get_enforcer():
|
||||
global _ENFORCER
|
||||
if not _ENFORCER:
|
||||
_ENFORCER = policy.Enforcer(CONF)
|
||||
_ENFORCER.register_defaults(base_rules)
|
||||
_ENFORCER.register_defaults(instance_rules)
|
||||
return _ENFORCER
|
||||
|
||||
|
||||
def authorize_on_tenant(context, rule):
|
||||
return __authorize(context, rule, target=None)
|
||||
|
||||
|
||||
def authorize_on_target(context, rule, target):
|
||||
if target:
|
||||
return __authorize(context, rule, target=target)
|
||||
raise trove_exceptions.TroveError(
|
||||
"BUG: Target must not evaluate to False.")
|
||||
|
||||
|
||||
def __authorize(context, rule, target=None):
|
||||
"""Checks authorization of a rule against the target in this context.
|
||||
|
||||
* This function is not to be called directly.
|
||||
Calling the function with a target that evaluates to None may
|
||||
result in policy bypass.
|
||||
Use 'authorize_on_*' calls instead.
|
||||
|
||||
:param context Trove context.
|
||||
:type context Context.
|
||||
|
||||
:param rule: The rule to evaluate.
|
||||
e.g. ``instance:create_instance``,
|
||||
``instance:resize_volume``
|
||||
|
||||
:param target As much information about the object being operated on
|
||||
as possible.
|
||||
For object creation (target=None) this should be a
|
||||
dictionary representing the location of the object
|
||||
e.g. ``{'project_id': context.project_id}``
|
||||
:type target dict
|
||||
|
||||
:raises: :class:`PolicyNotAuthorized` if verification fails.
|
||||
|
||||
"""
|
||||
target = target or {'tenant': context.tenant}
|
||||
return get_enforcer().authorize(
|
||||
rule, target, context.to_dict(), do_raise=True,
|
||||
exc=trove_exceptions.PolicyNotAuthorized, action=rule)
|
@ -322,6 +322,8 @@ class Controller(object):
|
||||
exception.BackupTooLarge,
|
||||
exception.ModuleAccessForbidden,
|
||||
exception.ModuleAppliedToInstance,
|
||||
exception.PolicyNotAuthorized,
|
||||
exception.LogAccessForbidden,
|
||||
],
|
||||
webob.exc.HTTPBadRequest: [
|
||||
exception.InvalidModelError,
|
||||
@ -548,7 +550,8 @@ class ContextMiddleware(base_wsgi.Middleware):
|
||||
is_admin=is_admin,
|
||||
limit=limits.get('limit'),
|
||||
marker=limits.get('marker'),
|
||||
service_catalog=service_catalog)
|
||||
service_catalog=service_catalog,
|
||||
roles=roles)
|
||||
request.environ[CONTEXT_KEY] = context
|
||||
|
||||
@classmethod
|
||||
|
@ -25,6 +25,7 @@ from trove.common.i18n import _
|
||||
from trove.common import notification
|
||||
from trove.common.notification import StartNotification, EndNotification
|
||||
from trove.common import pagination
|
||||
from trove.common import policy
|
||||
from trove.common import wsgi
|
||||
from trove.configuration import models
|
||||
from trove.configuration.models import DBConfigurationParameter
|
||||
@ -41,9 +42,16 @@ class ConfigurationsController(wsgi.Controller):
|
||||
|
||||
schemas = apischema.configuration
|
||||
|
||||
@classmethod
|
||||
def authorize_config_action(cls, context, config_rule_name, config):
|
||||
policy.authorize_on_target(
|
||||
context, 'configuration:%s' % config_rule_name,
|
||||
{'tenant': config.tenant_id})
|
||||
|
||||
def index(self, req, tenant_id):
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
configs, marker = models.Configurations.load(context)
|
||||
policy.authorize_on_tenant(context, 'configuration:index')
|
||||
view = views.ConfigurationsView(configs)
|
||||
paged = pagination.SimplePaginatedDataView(req.url, 'configurations',
|
||||
view, marker)
|
||||
@ -54,6 +62,7 @@ class ConfigurationsController(wsgi.Controller):
|
||||
% {"tenant": tenant_id, "id": id})
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
configuration = models.Configuration.load(context, id)
|
||||
self.authorize_config_action(context, 'show', configuration)
|
||||
configuration_items = models.Configuration.load_items(context, id)
|
||||
|
||||
configuration.instance_count = instances_models.DBInstance.find_all(
|
||||
@ -68,6 +77,7 @@ class ConfigurationsController(wsgi.Controller):
|
||||
def instances(self, req, tenant_id, id):
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
configuration = models.Configuration.load(context, id)
|
||||
self.authorize_config_action(context, 'instances', configuration)
|
||||
instances = instances_models.DBInstance.find_all(
|
||||
tenant_id=context.tenant,
|
||||
configuration_id=configuration.id,
|
||||
@ -89,6 +99,7 @@ class ConfigurationsController(wsgi.Controller):
|
||||
LOG.debug("body : '%s'\n\n" % req)
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
policy.authorize_on_tenant(context, 'configuration:create')
|
||||
context.notification = notification.DBaaSConfigurationCreate(
|
||||
context, request=req)
|
||||
name = body['configuration']['name']
|
||||
@ -137,10 +148,11 @@ class ConfigurationsController(wsgi.Controller):
|
||||
LOG.info(msg % {"tenant_id": tenant_id, "cfg_id": id})
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
group = models.Configuration.load(context, id)
|
||||
self.authorize_config_action(context, 'delete', group)
|
||||
context.notification = notification.DBaaSConfigurationDelete(
|
||||
context, request=req)
|
||||
with StartNotification(context, configuration_id=id):
|
||||
group = models.Configuration.load(context, id)
|
||||
instances = instances_models.DBInstance.find_all(
|
||||
tenant_id=context.tenant,
|
||||
configuration_id=id,
|
||||
@ -157,6 +169,15 @@ class ConfigurationsController(wsgi.Controller):
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
group = models.Configuration.load(context, id)
|
||||
# Note that changing the configuration group will also
|
||||
# indirectly affect all the instances which attach it.
|
||||
#
|
||||
# The Trove instance itself won't be changed (the same group is still
|
||||
# attached) but the configuration values will.
|
||||
#
|
||||
# The operator needs to keep this in mind when defining the related
|
||||
# policies.
|
||||
self.authorize_config_action(context, 'update', group)
|
||||
|
||||
# if name/description are provided in the request body, update the
|
||||
# model with these values as well.
|
||||
@ -181,10 +202,11 @@ class ConfigurationsController(wsgi.Controller):
|
||||
|
||||
def edit(self, req, body, tenant_id, id):
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
group = models.Configuration.load(context, id)
|
||||
self.authorize_config_action(context, 'edit', group)
|
||||
context.notification = notification.DBaaSConfigurationEdit(
|
||||
context, request=req)
|
||||
with StartNotification(context, configuration_id=id):
|
||||
group = models.Configuration.load(context, id)
|
||||
items = self._configuration_items_list(group,
|
||||
body['configuration'])
|
||||
models.Configuration.save(group, items)
|
||||
@ -329,7 +351,18 @@ class ConfigurationsController(wsgi.Controller):
|
||||
|
||||
class ParametersController(wsgi.Controller):
|
||||
|
||||
@classmethod
|
||||
def authorize_request(cls, req, rule_name):
|
||||
"""Parameters (configuration templates) bind to a datastore.
|
||||
Datastores are not owned by any particular tenant so we only check
|
||||
the current tenant is allowed to perform the action.
|
||||
"""
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
policy.authorize_on_tenant(context, 'configuration-parameter:%s'
|
||||
% rule_name)
|
||||
|
||||
def index(self, req, tenant_id, datastore, id):
|
||||
self.authorize_request(req, 'index')
|
||||
ds, ds_version = ds_models.get_datastore_version(
|
||||
type=datastore, version=id)
|
||||
rules = models.DatastoreConfigurationParameters.load_parameters(
|
||||
@ -338,6 +371,7 @@ class ParametersController(wsgi.Controller):
|
||||
200)
|
||||
|
||||
def show(self, req, tenant_id, datastore, id, name):
|
||||
self.authorize_request(req, 'show')
|
||||
ds, ds_version = ds_models.get_datastore_version(
|
||||
type=datastore, version=id)
|
||||
rule = models.DatastoreConfigurationParameters.load_parameter_by_name(
|
||||
@ -345,6 +379,7 @@ class ParametersController(wsgi.Controller):
|
||||
return wsgi.Result(views.ConfigurationParameterView(rule).data(), 200)
|
||||
|
||||
def index_by_version(self, req, tenant_id, version):
|
||||
self.authorize_request(req, 'index_by_version')
|
||||
ds_version = ds_models.DatastoreVersion.load_by_uuid(version)
|
||||
rules = models.DatastoreConfigurationParameters.load_parameters(
|
||||
ds_version.id)
|
||||
@ -352,6 +387,7 @@ class ParametersController(wsgi.Controller):
|
||||
200)
|
||||
|
||||
def show_by_version(self, req, tenant_id, version, name):
|
||||
self.authorize_request(req, 'show_by_version')
|
||||
ds_models.DatastoreVersion.load_by_uuid(version)
|
||||
rule = models.DatastoreConfigurationParameters.load_parameter_by_name(
|
||||
version, name)
|
||||
|
@ -16,6 +16,7 @@
|
||||
# under the License.
|
||||
#
|
||||
|
||||
from trove.common import policy
|
||||
from trove.common import wsgi
|
||||
from trove.datastore import models, views
|
||||
from trove.flavor import views as flavor_views
|
||||
@ -23,7 +24,16 @@ from trove.flavor import views as flavor_views
|
||||
|
||||
class DatastoreController(wsgi.Controller):
|
||||
|
||||
@classmethod
|
||||
def authorize_request(cls, req, rule_name):
|
||||
"""Datastores are not owned by any particular tenant so we only check
|
||||
the current tenant is allowed to perform the action.
|
||||
"""
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
policy.authorize_on_tenant(context, 'datastore:%s' % rule_name)
|
||||
|
||||
def show(self, req, tenant_id, id):
|
||||
self.authorize_request(req, 'show')
|
||||
datastore = models.Datastore.load(id)
|
||||
datastore_versions = (models.DatastoreVersions.load(datastore.id))
|
||||
return wsgi.Result(views.
|
||||
@ -31,6 +41,7 @@ class DatastoreController(wsgi.Controller):
|
||||
req).data(), 200)
|
||||
|
||||
def index(self, req, tenant_id):
|
||||
self.authorize_request(req, 'index')
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
only_active = True
|
||||
if context.is_admin:
|
||||
@ -42,17 +53,20 @@ class DatastoreController(wsgi.Controller):
|
||||
req).data(), 200)
|
||||
|
||||
def version_show(self, req, tenant_id, datastore, id):
|
||||
self.authorize_request(req, 'version_show')
|
||||
datastore = models.Datastore.load(datastore)
|
||||
datastore_version = models.DatastoreVersion.load(datastore, id)
|
||||
return wsgi.Result(views.DatastoreVersionView(datastore_version,
|
||||
req).data(), 200)
|
||||
|
||||
def version_show_by_uuid(self, req, tenant_id, uuid):
|
||||
self.authorize_request(req, 'version_show_by_uuid')
|
||||
datastore_version = models.DatastoreVersion.load_by_uuid(uuid)
|
||||
return wsgi.Result(views.DatastoreVersionView(datastore_version,
|
||||
req).data(), 200)
|
||||
|
||||
def version_index(self, req, tenant_id, datastore):
|
||||
self.authorize_request(req, 'version_index')
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
only_active = True
|
||||
if context.is_admin:
|
||||
@ -70,6 +84,7 @@ class DatastoreController(wsgi.Controller):
|
||||
one or more entries are found in datastore_version_metadata,
|
||||
in which case only those are returned.
|
||||
"""
|
||||
self.authorize_request(req, 'list_associated_flavors')
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
flavors = (models.DatastoreVersionMetadata.
|
||||
list_datastore_version_flavor_associations(
|
||||
|
@ -21,14 +21,17 @@ from oslo_log import log as logging
|
||||
from oslo_utils import importutils
|
||||
import six
|
||||
|
||||
from trove.cluster import models as cluster_models
|
||||
from trove.cluster.models import DBCluster
|
||||
from trove.common import cfg
|
||||
from trove.common import exception
|
||||
from trove.common.i18n import _LI
|
||||
from trove.common import policy
|
||||
from trove.common import wsgi
|
||||
from trove.datastore import models as datastore_models
|
||||
from trove.extensions.common import models
|
||||
from trove.extensions.common import views
|
||||
from trove.instance import models as instance_models
|
||||
from trove.instance.models import DBInstance
|
||||
|
||||
|
||||
@ -37,8 +40,30 @@ import_class = importutils.import_class
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class ExtensionController(wsgi.Controller):
|
||||
|
||||
@classmethod
|
||||
def authorize_target_action(cls, context, target_rule_name,
|
||||
target_id, is_cluster=False):
|
||||
target = None
|
||||
if is_cluster:
|
||||
target = cluster_models.Cluster.load(context, target_id)
|
||||
else:
|
||||
target = instance_models.Instance.load(context, target_id)
|
||||
|
||||
if not target:
|
||||
if is_cluster:
|
||||
raise exception.ClusterNotFound(cluster=target_id)
|
||||
raise exception.InstanceNotFound(instance=target_id)
|
||||
|
||||
target_type = 'cluster' if is_cluster else 'instance'
|
||||
policy.authorize_on_target(
|
||||
context, '%s:extension:%s' % (target_type, target_rule_name),
|
||||
{'tenant': target.tenant_id})
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseDatastoreRootController(wsgi.Controller):
|
||||
class BaseDatastoreRootController(ExtensionController):
|
||||
"""Base class that defines the contract for root controllers."""
|
||||
|
||||
@abc.abstractmethod
|
||||
@ -174,13 +199,16 @@ class ClusterRootController(DefaultRootController):
|
||||
return single_instance_id, instance_ids
|
||||
|
||||
|
||||
class RootController(wsgi.Controller):
|
||||
class RootController(ExtensionController):
|
||||
"""Controller for instance functionality."""
|
||||
|
||||
def index(self, req, tenant_id, instance_id):
|
||||
"""Returns True if root is enabled; False otherwise."""
|
||||
datastore_manager, is_cluster = self._get_datastore(tenant_id,
|
||||
instance_id)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(context, 'root:index', instance_id,
|
||||
is_cluster=is_cluster)
|
||||
root_controller = self.load_root_controller(datastore_manager)
|
||||
return root_controller.root_index(req, tenant_id, instance_id,
|
||||
is_cluster)
|
||||
@ -189,6 +217,9 @@ class RootController(wsgi.Controller):
|
||||
"""Enable the root user for the db instance."""
|
||||
datastore_manager, is_cluster = self._get_datastore(tenant_id,
|
||||
instance_id)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(context, 'root:create', instance_id,
|
||||
is_cluster=is_cluster)
|
||||
root_controller = self.load_root_controller(datastore_manager)
|
||||
if root_controller is not None:
|
||||
return root_controller.root_create(req, body, tenant_id,
|
||||
@ -199,6 +230,9 @@ class RootController(wsgi.Controller):
|
||||
def delete(self, req, tenant_id, instance_id):
|
||||
datastore_manager, is_cluster = self._get_datastore(tenant_id,
|
||||
instance_id)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(context, 'root:delete', instance_id,
|
||||
is_cluster=is_cluster)
|
||||
root_controller = self.load_root_controller(datastore_manager)
|
||||
if root_controller is not None:
|
||||
return root_controller.root_delete(req, tenant_id,
|
||||
|
@ -30,6 +30,7 @@ from trove.common import pagination
|
||||
from trove.common.utils import correct_id_with_req
|
||||
from trove.common import wsgi
|
||||
from trove.extensions.common.service import DefaultRootController
|
||||
from trove.extensions.common.service import ExtensionController
|
||||
from trove.extensions.mysql.common import populate_users
|
||||
from trove.extensions.mysql.common import populate_validated_databases
|
||||
from trove.extensions.mysql.common import unquote_user_host
|
||||
@ -42,7 +43,7 @@ import_class = importutils.import_class
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class UserController(wsgi.Controller):
|
||||
class UserController(ExtensionController):
|
||||
"""Controller for instance functionality."""
|
||||
schemas = apischema.user
|
||||
|
||||
@ -60,6 +61,7 @@ class UserController(wsgi.Controller):
|
||||
"req : '%(req)s'\n\n") %
|
||||
{"id": instance_id, "req": req})
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(context, 'user:index', instance_id)
|
||||
users, next_marker = models.Users.load(context, instance_id)
|
||||
view = views.UsersView(users)
|
||||
paged = pagination.SimplePaginatedDataView(req.url, 'users', view,
|
||||
@ -75,6 +77,7 @@ class UserController(wsgi.Controller):
|
||||
"req": strutils.mask_password(req),
|
||||
"body": strutils.mask_password(body)})
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(context, 'user:create', instance_id)
|
||||
context.notification = notification.DBaaSUserCreate(context,
|
||||
request=req)
|
||||
users = body['users']
|
||||
@ -94,6 +97,7 @@ class UserController(wsgi.Controller):
|
||||
"req : '%(req)s'\n\n") %
|
||||
{"id": instance_id, "req": req})
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(context, 'user:delete', instance_id)
|
||||
id = correct_id_with_req(id, req)
|
||||
username, host = unquote_user_host(id)
|
||||
user = None
|
||||
@ -122,6 +126,7 @@ class UserController(wsgi.Controller):
|
||||
"req : '%(req)s'\n\n") %
|
||||
{"id": instance_id, "req": req})
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(context, 'user:show', instance_id)
|
||||
id = correct_id_with_req(id, req)
|
||||
username, host = unquote_user_host(id)
|
||||
user = None
|
||||
@ -141,6 +146,7 @@ class UserController(wsgi.Controller):
|
||||
"req : '%(req)s'\n\n") %
|
||||
{"id": instance_id, "req": strutils.mask_password(req)})
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(context, 'user:update', instance_id)
|
||||
id = correct_id_with_req(id, req)
|
||||
username, hostname = unquote_user_host(id)
|
||||
user = None
|
||||
@ -171,6 +177,7 @@ class UserController(wsgi.Controller):
|
||||
"req : '%(req)s'\n\n") %
|
||||
{"id": instance_id, "req": strutils.mask_password(req)})
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(context, 'user:update_all', instance_id)
|
||||
context.notification = notification.DBaaSUserChangePassword(
|
||||
context, request=req)
|
||||
users = body['users']
|
||||
@ -203,7 +210,7 @@ class UserController(wsgi.Controller):
|
||||
return wsgi.Result(None, 202)
|
||||
|
||||
|
||||
class UserAccessController(wsgi.Controller):
|
||||
class UserAccessController(ExtensionController):
|
||||
"""Controller for adding and removing database access for a user."""
|
||||
schemas = apischema.user
|
||||
|
||||
@ -232,6 +239,8 @@ class UserAccessController(wsgi.Controller):
|
||||
{"id": instance_id, "req": req})
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(
|
||||
context, 'user_access:index', instance_id)
|
||||
# Make sure this user exists.
|
||||
user_id = correct_id_with_req(user_id, req)
|
||||
user = self._get_user(context, instance_id, user_id)
|
||||
@ -249,6 +258,8 @@ class UserAccessController(wsgi.Controller):
|
||||
"req : '%(req)s'\n\n") %
|
||||
{"id": instance_id, "req": req})
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(
|
||||
context, 'user_access:update', instance_id)
|
||||
context.notification = notification.DBaaSUserGrant(
|
||||
context, request=req)
|
||||
user_id = correct_id_with_req(user_id, req)
|
||||
@ -270,6 +281,8 @@ class UserAccessController(wsgi.Controller):
|
||||
"req : '%(req)s'\n\n") %
|
||||
{"id": instance_id, "req": req})
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(
|
||||
context, 'user_access:delete', instance_id)
|
||||
context.notification = notification.DBaaSUserRevoke(
|
||||
context, request=req)
|
||||
user_id = correct_id_with_req(user_id, req)
|
||||
@ -288,7 +301,7 @@ class UserAccessController(wsgi.Controller):
|
||||
return wsgi.Result(None, 202)
|
||||
|
||||
|
||||
class SchemaController(wsgi.Controller):
|
||||
class SchemaController(ExtensionController):
|
||||
"""Controller for instance functionality."""
|
||||
schemas = apischema.dbschema
|
||||
|
||||
@ -299,6 +312,8 @@ class SchemaController(wsgi.Controller):
|
||||
{"id": instance_id, "req": req})
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(
|
||||
context, 'database:index', instance_id)
|
||||
schemas, next_marker = models.Schemas.load(context, instance_id)
|
||||
view = views.SchemasView(schemas)
|
||||
paged = pagination.SimplePaginatedDataView(req.url, 'databases', view,
|
||||
@ -315,6 +330,8 @@ class SchemaController(wsgi.Controller):
|
||||
"body": body})
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(
|
||||
context, 'database:create', instance_id)
|
||||
schemas = body['databases']
|
||||
context.notification = notification.DBaaSDatabaseCreate(context,
|
||||
request=req)
|
||||
@ -334,6 +351,8 @@ class SchemaController(wsgi.Controller):
|
||||
"req : '%(req)s'\n\n") %
|
||||
{"id": instance_id, "req": req})
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(
|
||||
context, 'database:delete', instance_id)
|
||||
context.notification = notification.DBaaSDatabaseDelete(
|
||||
context, request=req)
|
||||
with StartNotification(context, instance_id=instance_id, dbname=id):
|
||||
@ -349,6 +368,9 @@ class SchemaController(wsgi.Controller):
|
||||
return wsgi.Result(None, 202)
|
||||
|
||||
def show(self, req, tenant_id, instance_id, id):
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self.authorize_target_action(
|
||||
context, 'database:show', instance_id)
|
||||
raise webob.exc.HTTPNotImplemented()
|
||||
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
import six
|
||||
|
||||
from trove.common import exception
|
||||
from trove.common import policy
|
||||
from trove.common import wsgi
|
||||
from trove.flavor import models
|
||||
from trove.flavor import views
|
||||
@ -30,12 +31,16 @@ class FlavorController(wsgi.Controller):
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
self._validate_flavor_id(id)
|
||||
flavor = models.Flavor(context=context, flavor_id=id)
|
||||
# Flavors do not bind to a particular tenant.
|
||||
# Only authorize the current tenant.
|
||||
policy.authorize_on_tenant(context, 'flavor:show')
|
||||
# Pass in the request to build accurate links.
|
||||
return wsgi.Result(views.FlavorView(flavor, req).data(), 200)
|
||||
|
||||
def index(self, req, tenant_id):
|
||||
"""Return all flavors."""
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
policy.authorize_on_tenant(context, 'flavor:index')
|
||||
flavors = models.Flavors(context=context)
|
||||
return wsgi.Result(views.FlavorsView(flavors, req).data(), 200)
|
||||
|
||||
|
@ -209,8 +209,7 @@ class GuestLog(object):
|
||||
'metafile': self._metafile_name()
|
||||
}
|
||||
else:
|
||||
raise exception.UnauthorizedRequest(_(
|
||||
"Not authorized to show log '%s'.") % self._name)
|
||||
raise exception.LogAccessForbidden(action='show', log=self._name)
|
||||
|
||||
def _refresh_details(self):
|
||||
|
||||
@ -310,16 +309,16 @@ class GuestLog(object):
|
||||
self._file)
|
||||
return self.show()
|
||||
else:
|
||||
raise exception.UnauthorizedRequest(_(
|
||||
"Not authorized to publish log '%s'.") % self._name)
|
||||
raise exception.LogAccessForbidden(
|
||||
action='publish', log=self._name)
|
||||
|
||||
def discard_log(self):
|
||||
if self.exposed:
|
||||
self._delete_log_components()
|
||||
return self.show()
|
||||
else:
|
||||
raise exception.UnauthorizedRequest(_(
|
||||
"Not authorized to discard log '%s'.") % self._name)
|
||||
raise exception.LogAccessForbidden(
|
||||
action='discard', log=self._name)
|
||||
|
||||
def _delete_log_components(self):
|
||||
container_name = self.get_container_name(force=True)
|
||||
|
@ -27,6 +27,7 @@ from trove.common.i18n import _LI
|
||||
from trove.common import notification
|
||||
from trove.common.notification import StartNotification
|
||||
from trove.common import pagination
|
||||
from trove.common import policy
|
||||
from trove.common.remote import create_guest_client
|
||||
from trove.common import utils
|
||||
from trove.common import wsgi
|
||||
@ -47,6 +48,11 @@ class InstanceController(wsgi.Controller):
|
||||
"""Controller for instance functionality."""
|
||||
schemas = apischema.instance.copy()
|
||||
|
||||
@classmethod
|
||||
def authorize_instance_action(cls, context, instance_rule_name, instance):
|
||||
policy.authorize_on_target(context, 'instance:%s' % instance_rule_name,
|
||||
{'tenant': instance.tenant_id})
|
||||
|
||||
@classmethod
|
||||
def get_action_schema(cls, body, action_schema):
|
||||
action_type = list(body.keys())[0]
|
||||
@ -106,6 +112,7 @@ class InstanceController(wsgi.Controller):
|
||||
def _action_restart(self, context, req, instance, body):
|
||||
context.notification = notification.DBaaSInstanceRestart(context,
|
||||
request=req)
|
||||
self.authorize_instance_action(context, 'restart', instance)
|
||||
with StartNotification(context, instance_id=instance.id):
|
||||
instance.restart()
|
||||
return wsgi.Result(None, 202)
|
||||
@ -136,6 +143,8 @@ class InstanceController(wsgi.Controller):
|
||||
def _action_resize_volume(self, context, req, instance, volume):
|
||||
context.notification = notification.DBaaSInstanceResizeVolume(
|
||||
context, request=req)
|
||||
self.authorize_instance_action(context, 'resize_volume', instance)
|
||||
|
||||
with StartNotification(context, instance_id=instance.id,
|
||||
new_size=volume['size']):
|
||||
instance.resize_volume(volume['size'])
|
||||
@ -144,6 +153,8 @@ class InstanceController(wsgi.Controller):
|
||||
def _action_resize_flavor(self, context, req, instance, flavorRef):
|
||||
context.notification = notification.DBaaSInstanceResizeInstance(
|
||||
context, request=req)
|
||||
self.authorize_instance_action(context, 'resize_flavor', instance)
|
||||
|
||||
new_flavor_id = utils.get_id_from_href(flavorRef)
|
||||
with StartNotification(context, instance_id=instance.id,
|
||||
new_flavor_id=new_flavor_id):
|
||||
@ -154,6 +165,8 @@ class InstanceController(wsgi.Controller):
|
||||
raise webob.exc.HTTPNotImplemented()
|
||||
|
||||
def _action_promote_to_replica_source(self, context, req, instance, body):
|
||||
self.authorize_instance_action(
|
||||
context, 'promote_to_replica_source', instance)
|
||||
context.notification = notification.DBaaSInstanceEject(context,
|
||||
request=req)
|
||||
with StartNotification(context, instance_id=instance.id):
|
||||
@ -161,6 +174,8 @@ class InstanceController(wsgi.Controller):
|
||||
return wsgi.Result(None, 202)
|
||||
|
||||
def _action_eject_replica_source(self, context, req, instance, body):
|
||||
self.authorize_instance_action(
|
||||
context, 'eject_replica_source', instance)
|
||||
context.notification = notification.DBaaSInstancePromote(context,
|
||||
request=req)
|
||||
with StartNotification(context, instance_id=instance.id):
|
||||
@ -168,6 +183,11 @@ class InstanceController(wsgi.Controller):
|
||||
return wsgi.Result(None, 202)
|
||||
|
||||
def _action_reset_status(self, context, req, instance, body):
|
||||
if 'force_delete' in body['reset_status']:
|
||||
self.authorize_instance_action(context, 'force_delete', instance)
|
||||
else:
|
||||
self.authorize_instance_action(
|
||||
context, 'reset_status', instance)
|
||||
context.notification = notification.DBaaSInstanceResetStatus(
|
||||
context, request=req)
|
||||
with StartNotification(context, instance_id=instance.id):
|
||||
@ -183,6 +203,7 @@ class InstanceController(wsgi.Controller):
|
||||
LOG.info(_LI("Listing database instances for tenant '%s'"), tenant_id)
|
||||
LOG.debug("req : '%s'\n\n", req)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
policy.authorize_on_tenant(context, 'instance:index')
|
||||
clustered_q = req.GET.get('include_clustered', '').lower()
|
||||
include_clustered = clustered_q == 'true'
|
||||
servers, marker = models.Instances.load(context, include_clustered)
|
||||
@ -197,6 +218,10 @@ class InstanceController(wsgi.Controller):
|
||||
id)
|
||||
LOG.debug("req : '%s'\n\n", req)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
|
||||
instance = models.Instance.load(context, id)
|
||||
self.authorize_instance_action(context, 'backups', instance)
|
||||
|
||||
backups, marker = backup_model.list_for_instance(context, id)
|
||||
view = backup_views.BackupViews(backups)
|
||||
paged = pagination.SimplePaginatedDataView(req.url, 'backups', view,
|
||||
@ -213,6 +238,7 @@ class InstanceController(wsgi.Controller):
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
server = models.load_instance_with_info(models.DetailInstance,
|
||||
context, id)
|
||||
self.authorize_instance_action(context, 'show', server)
|
||||
return wsgi.Result(views.InstanceDetailView(server,
|
||||
req=req).data(), 200)
|
||||
|
||||
@ -224,6 +250,7 @@ class InstanceController(wsgi.Controller):
|
||||
LOG.debug("req : '%s'\n\n", req)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
instance = models.load_any_instance(context, id)
|
||||
self.authorize_instance_action(context, 'delete', instance)
|
||||
context.notification = notification.DBaaSInstanceDelete(
|
||||
context, request=req)
|
||||
with StartNotification(context, instance_id=instance.id):
|
||||
@ -247,6 +274,7 @@ class InstanceController(wsgi.Controller):
|
||||
LOG.debug("req : '%s'\n\n", strutils.mask_password(req))
|
||||
LOG.debug("body : '%s'\n\n", strutils.mask_password(body))
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
policy.authorize_on_tenant(context, 'instance:create')
|
||||
context.notification = notification.DBaaSInstanceCreate(context,
|
||||
request=req)
|
||||
datastore_args = body['instance'].get('datastore', {})
|
||||
@ -268,6 +296,25 @@ class InstanceController(wsgi.Controller):
|
||||
except ValueError as ve:
|
||||
raise exception.BadRequest(msg=ve)
|
||||
|
||||
modules = body['instance'].get('modules')
|
||||
|
||||
# The following operations have their own API calls.
|
||||
# We need to make sure the same policies are enforced when
|
||||
# creating an instance.
|
||||
# i.e. if attaching configuration group to an existing instance is not
|
||||
# allowed, it should not be possible to create a new instance with the
|
||||
# group attached either
|
||||
if configuration:
|
||||
policy.authorize_on_tenant(context, 'instance:update')
|
||||
if modules:
|
||||
policy.authorize_on_tenant(context, 'instance:module_apply')
|
||||
if users:
|
||||
policy.authorize_on_tenant(
|
||||
context, 'instance:extension:user:create')
|
||||
if databases:
|
||||
policy.authorize_on_tenant(
|
||||
context, 'instance:extension:database:create')
|
||||
|
||||
if 'volume' in body['instance']:
|
||||
volume_info = body['instance']['volume']
|
||||
volume_size = int(volume_info['size'])
|
||||
@ -289,7 +336,6 @@ class InstanceController(wsgi.Controller):
|
||||
# also check for older name
|
||||
body['instance'].get('slave_of'))
|
||||
replica_count = body['instance'].get('replica_count')
|
||||
modules = body['instance'].get('modules')
|
||||
locality = body['instance'].get('locality')
|
||||
if locality:
|
||||
locality_domain = ['affinity', 'anti-affinity']
|
||||
@ -371,6 +417,7 @@ class InstanceController(wsgi.Controller):
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
|
||||
instance = models.Instance.load(context, id)
|
||||
self.authorize_instance_action(context, 'update', instance)
|
||||
|
||||
# Make sure args contains a 'configuration_id' argument,
|
||||
args = {}
|
||||
@ -388,6 +435,7 @@ class InstanceController(wsgi.Controller):
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
|
||||
instance = models.Instance.load(context, id)
|
||||
self.authorize_instance_action(context, 'edit', instance)
|
||||
|
||||
args = {}
|
||||
args['detach_replica'] = ('replica_of' in body['instance'] or
|
||||
@ -411,6 +459,8 @@ class InstanceController(wsgi.Controller):
|
||||
LOG.info(_LI("Getting default configuration for instance %s"), id)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
instance = models.Instance.load(context, id)
|
||||
self.authorize_instance_action(context, 'configuration', instance)
|
||||
|
||||
LOG.debug("Server: %s", instance)
|
||||
config = instance.get_default_configuration_template()
|
||||
LOG.debug("Default config for instance %(instance_id)s is %(config)s",
|
||||
@ -425,6 +475,7 @@ class InstanceController(wsgi.Controller):
|
||||
instance = models.Instance.load(context, id)
|
||||
if not instance:
|
||||
raise exception.NotFound(uuid=id)
|
||||
self.authorize_instance_action(context, 'guest_log_list', instance)
|
||||
client = create_guest_client(context, id)
|
||||
guest_log_list = client.guest_log_list()
|
||||
return wsgi.Result({'logs': guest_log_list}, 200)
|
||||
@ -454,6 +505,7 @@ class InstanceController(wsgi.Controller):
|
||||
instance = models.Instance.load(context, id)
|
||||
if not instance:
|
||||
raise exception.NotFound(uuid=id)
|
||||
self.authorize_instance_action(context, 'module_list', instance)
|
||||
from_guest = bool(req.GET.get('from_guest', '').lower())
|
||||
include_contents = bool(req.GET.get('include_contents', '').lower())
|
||||
if from_guest:
|
||||
@ -481,6 +533,7 @@ class InstanceController(wsgi.Controller):
|
||||
instance = models.Instance.load(context, id)
|
||||
if not instance:
|
||||
raise exception.NotFound(uuid=id)
|
||||
self.authorize_instance_action(context, 'module_apply', instance)
|
||||
module_ids = [mod['id'] for mod in body.get('modules', [])]
|
||||
modules = module_models.Modules.load_by_ids(context, module_ids)
|
||||
module_list = []
|
||||
@ -501,6 +554,7 @@ class InstanceController(wsgi.Controller):
|
||||
instance = models.Instance.load(context, id)
|
||||
if not instance:
|
||||
raise exception.NotFound(uuid=id)
|
||||
self.authorize_instance_action(context, 'module_remove', instance)
|
||||
module = module_models.Module.load(context, module_id)
|
||||
module_info = module_views.DetailedModuleView(module).data()
|
||||
client = create_guest_client(context, id)
|
||||
|
@ -13,6 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from trove.common import policy
|
||||
from trove.common import wsgi
|
||||
from trove.limits import views
|
||||
from trove.quota.quota import QUOTAS
|
||||
@ -27,6 +28,8 @@ class LimitsController(wsgi.Controller):
|
||||
"""
|
||||
Return all absolute and rate limit information.
|
||||
"""
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
policy.authorize_on_tenant(context, 'limits:index')
|
||||
quotas = QUOTAS.get_all_quotas_by_tenant(tenant_id)
|
||||
abs_limits = {k: v['hard_limit'] for k, v in quotas.items()}
|
||||
rate_limits = req.environ.get("trove.limits", [])
|
||||
|
@ -22,6 +22,7 @@ import trove.common.apischema as apischema
|
||||
from trove.common import exception
|
||||
from trove.common.i18n import _
|
||||
from trove.common import pagination
|
||||
from trove.common import policy
|
||||
from trove.common import wsgi
|
||||
from trove.datastore import models as datastore_models
|
||||
from trove.instance import models as instance_models
|
||||
@ -37,8 +38,20 @@ class ModuleController(wsgi.Controller):
|
||||
|
||||
schemas = apischema.module
|
||||
|
||||
@classmethod
|
||||
def authorize_module_action(cls, context, module_rule_name, module):
|
||||
"""If a modules in not owned by any particular tenant just check
|
||||
the current tenant is allowed to perform the action.
|
||||
"""
|
||||
if module.tenant_id is not None:
|
||||
policy.authorize_on_target(context, 'module:%s' % module_rule_name,
|
||||
{'tenant': module.tenant_id})
|
||||
else:
|
||||
policy.authorize_on_tenant(context, 'module:%s' % module_rule_name)
|
||||
|
||||
def index(self, req, tenant_id):
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
policy.authorize_on_tenant(context, 'module:index')
|
||||
datastore = req.GET.get('datastore', '')
|
||||
if datastore and datastore.lower() != models.Modules.MATCH_ALL_NAME:
|
||||
ds, ds_ver = datastore_models.get_datastore_version(
|
||||
@ -53,6 +66,7 @@ class ModuleController(wsgi.Controller):
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
module = models.Module.load(context, id)
|
||||
self.authorize_module_action(context, 'show', module)
|
||||
module.instance_count = len(models.InstanceModules.load(
|
||||
context, module_id=module.id, md5=module.md5))
|
||||
|
||||
@ -65,6 +79,7 @@ class ModuleController(wsgi.Controller):
|
||||
LOG.info(_("Creating module '%s'") % name)
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
policy.authorize_on_tenant(context, 'module:create')
|
||||
module_type = body['module']['module_type']
|
||||
contents = body['module']['contents']
|
||||
|
||||
@ -89,6 +104,7 @@ class ModuleController(wsgi.Controller):
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
module = models.Module.load(context, id)
|
||||
self.authorize_module_action(context, 'delete', module)
|
||||
models.Module.delete(context, module)
|
||||
return wsgi.Result(None, 200)
|
||||
|
||||
@ -97,6 +113,7 @@ class ModuleController(wsgi.Controller):
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
module = models.Module.load(context, id)
|
||||
self.authorize_module_action(context, 'update', module)
|
||||
original_module = copy.deepcopy(module)
|
||||
if 'name' in body['module']:
|
||||
module.name = body['module']['name']
|
||||
@ -146,6 +163,10 @@ class ModuleController(wsgi.Controller):
|
||||
LOG.info(_("Getting instances for module %s") % id)
|
||||
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
|
||||
module = models.Module.load(context, id)
|
||||
self.authorize_module_action(context, 'instances', module)
|
||||
|
||||
instance_modules, marker = models.InstanceModules.load(
|
||||
context, module_id=id)
|
||||
if instance_modules:
|
||||
|
@ -18,6 +18,7 @@ from proboscis import after_class
|
||||
from proboscis.asserts import assert_equal
|
||||
from proboscis.asserts import assert_raises
|
||||
from proboscis import before_class
|
||||
from proboscis import SkipTest
|
||||
from proboscis import test
|
||||
|
||||
from trove.backup import models as backup_models
|
||||
@ -30,6 +31,7 @@ from trove.extensions.mgmt.instances.service import MgmtInstanceController
|
||||
from trove.instance import models as imodels
|
||||
from trove.instance.models import DBInstance
|
||||
from trove.instance.tasks import InstanceTasks
|
||||
from trove.tests.config import CONFIG
|
||||
from trove.tests.util import create_dbaas_client
|
||||
from trove.tests.util import test_config
|
||||
from trove.tests.util.users import Requirements
|
||||
@ -79,6 +81,7 @@ class MgmtInstanceBase(object):
|
||||
|
||||
@test(groups=[GROUP])
|
||||
class RestartTaskStatusTests(MgmtInstanceBase):
|
||||
|
||||
@before_class
|
||||
def setUp(self):
|
||||
super(RestartTaskStatusTests, self).setUp()
|
||||
@ -137,6 +140,9 @@ class RestartTaskStatusTests(MgmtInstanceBase):
|
||||
|
||||
@test
|
||||
def mgmt_reset_task_status_clears_backups(self):
|
||||
if CONFIG.fake_mode:
|
||||
raise SkipTest("Test requires an instance.")
|
||||
|
||||
self.reset_task_status()
|
||||
self._reload_db_info()
|
||||
assert_equal(self.db_info.task_status, InstanceTasks.NONE)
|
||||
@ -201,5 +207,6 @@ class RestartTaskStatusTests(MgmtInstanceBase):
|
||||
found_backup.delete()
|
||||
admin = test_config.users.find_user(Requirements(is_admin=True))
|
||||
admin_dbaas = create_dbaas_client(admin)
|
||||
result = admin_dbaas.instances.backups(self.db_info.id)
|
||||
assert_equal(0, len(result))
|
||||
if not CONFIG.fake_mode:
|
||||
result = admin_dbaas.instances.backups(self.db_info.id)
|
||||
assert_equal(0, len(result))
|
||||
|
@ -46,8 +46,9 @@ class InstanceForceDeleteRunner(TestRunner):
|
||||
|
||||
def run_delete_build_instance(self, expected_http_code=202):
|
||||
if self.build_inst_id:
|
||||
self.auth_client.instances.force_delete(self.build_inst_id)
|
||||
self.assert_client_code(expected_http_code)
|
||||
self.admin_client.instances.force_delete(self.build_inst_id)
|
||||
self.assert_client_code(expected_http_code,
|
||||
client=self.admin_client)
|
||||
|
||||
def run_wait_for_force_delete(self):
|
||||
if self.build_inst_id:
|
||||
|
@ -45,6 +45,7 @@ class BaseLimitTestSuite(trove_testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(BaseLimitTestSuite, self).setUp()
|
||||
self.context = trove_testtools.TroveTestContext(self)
|
||||
self.absolute_limits = {"max_instances": 55,
|
||||
"max_volumes": 100,
|
||||
"max_backups": 40}
|
||||
@ -60,7 +61,7 @@ class LimitsControllerTest(BaseLimitTestSuite):
|
||||
limit_controller = LimitsController()
|
||||
|
||||
req = MagicMock()
|
||||
req.environ = {}
|
||||
req.environ = {'trove.context': self.context}
|
||||
|
||||
view = limit_controller.index(req, "test_tenant_id")
|
||||
expected = {'limits': [{'verb': 'ABSOLUTE'}]}
|
||||
@ -122,7 +123,7 @@ class LimitsControllerTest(BaseLimitTestSuite):
|
||||
hard_limit=55)}
|
||||
|
||||
req = MagicMock()
|
||||
req.environ = {"trove.limits": limits}
|
||||
req.environ = {"trove.limits": limits, 'trove.context': self.context}
|
||||
|
||||
with patch.object(QUOTAS, 'get_all_quotas_by_tenant',
|
||||
return_value=abs_limits):
|
||||
|
@ -18,7 +18,6 @@ import jsonschema
|
||||
from mock import MagicMock
|
||||
from mock import Mock
|
||||
from mock import patch
|
||||
from testtools import TestCase
|
||||
from testtools.matchers import Is, Equals
|
||||
from trove.cluster import models
|
||||
from trove.cluster.models import Cluster, DBCluster
|
||||
@ -33,7 +32,8 @@ from trove.datastore import models as datastore_models
|
||||
from trove.tests.unittests import trove_testtools
|
||||
|
||||
|
||||
class TestClusterController(TestCase):
|
||||
class TestClusterController(trove_testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestClusterController, self).setUp()
|
||||
self.controller = ClusterController()
|
||||
@ -248,7 +248,8 @@ class TestClusterController(TestCase):
|
||||
cluster.delete.assert_called_with()
|
||||
|
||||
|
||||
class TestClusterControllerWithStrategy(TestCase):
|
||||
class TestClusterControllerWithStrategy(trove_testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestClusterControllerWithStrategy, self).setUp()
|
||||
self.controller = ClusterController()
|
||||
|
@ -24,6 +24,7 @@ from trove.extensions.common import models
|
||||
from trove.extensions.common.service import ClusterRootController
|
||||
from trove.extensions.common.service import DefaultRootController
|
||||
from trove.extensions.common.service import RootController
|
||||
from trove.instance import models as instance_models
|
||||
from trove.instance.models import DBInstance
|
||||
from trove.tests.unittests import trove_testtools
|
||||
|
||||
@ -90,16 +91,20 @@ class TestRootController(trove_testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRootController, self).setUp()
|
||||
self.context = trove_testtools.TroveTestContext(self)
|
||||
self.controller = RootController()
|
||||
|
||||
@patch.object(instance_models.Instance, "load")
|
||||
@patch.object(RootController, "load_root_controller")
|
||||
@patch.object(RootController, "_get_datastore")
|
||||
def test_index(self, service_get_datastore, service_load_root_controller):
|
||||
def test_index(self, service_get_datastore, service_load_root_controller,
|
||||
service_load_instance):
|
||||
req = Mock()
|
||||
req.environ = {'trove.context': self.context}
|
||||
tenant_id = Mock()
|
||||
uuid = utils.generate_uuid()
|
||||
ds_manager = Mock()
|
||||
is_cluster = Mock()
|
||||
is_cluster = False
|
||||
service_get_datastore.return_value = (ds_manager, is_cluster)
|
||||
root_controller = Mock()
|
||||
ret = Mock()
|
||||
@ -112,15 +117,18 @@ class TestRootController(trove_testtools.TestCase):
|
||||
root_controller.root_index.assert_called_with(
|
||||
req, tenant_id, uuid, is_cluster)
|
||||
|
||||
@patch.object(instance_models.Instance, "load")
|
||||
@patch.object(RootController, "load_root_controller")
|
||||
@patch.object(RootController, "_get_datastore")
|
||||
def test_create(self, service_get_datastore, service_load_root_controller):
|
||||
def test_create(self, service_get_datastore, service_load_root_controller,
|
||||
service_load_instance):
|
||||
req = Mock()
|
||||
req.environ = {'trove.context': self.context}
|
||||
body = Mock()
|
||||
tenant_id = Mock()
|
||||
uuid = utils.generate_uuid()
|
||||
ds_manager = Mock()
|
||||
is_cluster = Mock()
|
||||
is_cluster = False
|
||||
service_get_datastore.return_value = (ds_manager, is_cluster)
|
||||
root_controller = Mock()
|
||||
ret = Mock()
|
||||
@ -134,17 +142,20 @@ class TestRootController(trove_testtools.TestCase):
|
||||
root_controller.root_create.assert_called_with(
|
||||
req, body, tenant_id, uuid, is_cluster)
|
||||
|
||||
@patch.object(instance_models.Instance, "load")
|
||||
@patch.object(RootController, "load_root_controller")
|
||||
@patch.object(RootController, "_get_datastore")
|
||||
def test_create_with_no_root_controller(self,
|
||||
service_get_datastore,
|
||||
service_load_root_controller):
|
||||
service_load_root_controller,
|
||||
service_load_instance):
|
||||
req = Mock()
|
||||
req.environ = {'trove.context': self.context}
|
||||
body = Mock()
|
||||
tenant_id = Mock()
|
||||
uuid = utils.generate_uuid()
|
||||
ds_manager = Mock()
|
||||
is_cluster = Mock()
|
||||
is_cluster = False
|
||||
service_get_datastore.return_value = (ds_manager, is_cluster)
|
||||
service_load_root_controller.return_value = None
|
||||
|
||||
@ -160,6 +171,7 @@ class TestClusterRootController(trove_testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestClusterRootController, self).setUp()
|
||||
self.context = trove_testtools.TroveTestContext(self)
|
||||
self.controller = ClusterRootController()
|
||||
|
||||
@patch.object(ClusterRootController, "cluster_root_index")
|
||||
@ -204,22 +216,18 @@ class TestClusterRootController(trove_testtools.TestCase):
|
||||
|
||||
@patch.object(models.ClusterRoot, "load")
|
||||
def test_instance_root_index(self, mock_cluster_root_load):
|
||||
context = Mock()
|
||||
req = Mock()
|
||||
req.environ = Mock()
|
||||
req.environ.__getitem__ = Mock(return_value=context)
|
||||
req.environ = {'trove.context': self.context}
|
||||
tenant_id = Mock()
|
||||
instance_id = utils.generate_uuid()
|
||||
self.controller.instance_root_index(req, tenant_id, instance_id)
|
||||
mock_cluster_root_load.assert_called_with(context, instance_id)
|
||||
mock_cluster_root_load.assert_called_with(self.context, instance_id)
|
||||
|
||||
@patch.object(models.ClusterRoot, "load",
|
||||
side_effect=exception.UnprocessableEntity())
|
||||
def test_instance_root_index_exception(self, mock_cluster_root_load):
|
||||
context = Mock()
|
||||
req = Mock()
|
||||
req.environ = Mock()
|
||||
req.environ.__getitem__ = Mock(return_value=context)
|
||||
req.environ = {'trove.context': self.context}
|
||||
tenant_id = Mock()
|
||||
instance_id = utils.generate_uuid()
|
||||
self.assertRaises(
|
||||
@ -227,7 +235,7 @@ class TestClusterRootController(trove_testtools.TestCase):
|
||||
self.controller.instance_root_index,
|
||||
req, tenant_id, instance_id
|
||||
)
|
||||
mock_cluster_root_load.assert_called_with(context, instance_id)
|
||||
mock_cluster_root_load.assert_called_with(self.context, instance_id)
|
||||
|
||||
@patch.object(ClusterRootController, "instance_root_index")
|
||||
@patch.object(ClusterRootController, "_get_cluster_instance_id")
|
||||
@ -278,12 +286,10 @@ class TestClusterRootController(trove_testtools.TestCase):
|
||||
@patch.object(models.ClusterRoot, "create")
|
||||
def test_instance_root_create(self, mock_cluster_root_create):
|
||||
user = Mock()
|
||||
context = Mock()
|
||||
context.user = Mock()
|
||||
context.user.__getitem__ = Mock(return_value=user)
|
||||
self.context.user = Mock()
|
||||
self.context.user.__getitem__ = Mock(return_value=user)
|
||||
req = Mock()
|
||||
req.environ = Mock()
|
||||
req.environ.__getitem__ = Mock(return_value=context)
|
||||
req.environ = {'trove.context': self.context}
|
||||
password = Mock()
|
||||
body = {'password': password}
|
||||
instance_id = utils.generate_uuid()
|
||||
@ -291,17 +297,16 @@ class TestClusterRootController(trove_testtools.TestCase):
|
||||
self.controller.instance_root_create(
|
||||
req, body, instance_id, cluster_instances)
|
||||
mock_cluster_root_create.assert_called_with(
|
||||
context, instance_id, context.user, password, cluster_instances)
|
||||
self.context, instance_id, self.context.user, password,
|
||||
cluster_instances)
|
||||
|
||||
@patch.object(models.ClusterRoot, "create")
|
||||
def test_instance_root_create_no_body(self, mock_cluster_root_create):
|
||||
user = Mock()
|
||||
context = Mock()
|
||||
context.user = Mock()
|
||||
context.user.__getitem__ = Mock(return_value=user)
|
||||
self.context.user = Mock()
|
||||
self.context.user.__getitem__ = Mock(return_value=user)
|
||||
req = Mock()
|
||||
req.environ = Mock()
|
||||
req.environ.__getitem__ = Mock(return_value=context)
|
||||
req.environ = {'trove.context': self.context}
|
||||
password = None
|
||||
body = None
|
||||
instance_id = utils.generate_uuid()
|
||||
@ -309,4 +314,5 @@ class TestClusterRootController(trove_testtools.TestCase):
|
||||
self.controller.instance_root_create(
|
||||
req, body, instance_id, cluster_instances)
|
||||
mock_cluster_root_create.assert_called_with(
|
||||
context, instance_id, context.user, password, cluster_instances)
|
||||
self.context, instance_id, self.context.user, password,
|
||||
cluster_instances)
|
||||
|
53
trove/tests/unittests/common/test_policy.py
Normal file
53
trove/tests/unittests/common/test_policy.py
Normal file
@ -0,0 +1,53 @@
|
||||
# Copyright 2016 Tesora Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from mock import MagicMock
|
||||
from mock import NonCallableMock
|
||||
from mock import patch
|
||||
|
||||
from trove.common import exception as trove_exceptions
|
||||
from trove.common import policy as trove_policy
|
||||
from trove.tests.unittests import trove_testtools
|
||||
|
||||
|
||||
class TestPolicy(trove_testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPolicy, self).setUp()
|
||||
self.context = trove_testtools.TroveTestContext(self)
|
||||
self.mock_enforcer = MagicMock()
|
||||
get_enforcer_patch = patch.object(trove_policy, 'get_enforcer',
|
||||
return_value=self.mock_enforcer)
|
||||
self.addCleanup(get_enforcer_patch.stop)
|
||||
self.mock_get_enforcer = get_enforcer_patch.start()
|
||||
|
||||
def test_authorize_on_tenant(self):
|
||||
test_rule = NonCallableMock()
|
||||
trove_policy.authorize_on_tenant(self.context, test_rule)
|
||||
self.mock_get_enforcer.assert_called_once_with()
|
||||
self.mock_enforcer.authorize.assert_called_once_with(
|
||||
test_rule, {'tenant': self.context.tenant}, self.context.to_dict(),
|
||||
do_raise=True, exc=trove_exceptions.PolicyNotAuthorized,
|
||||
action=test_rule)
|
||||
|
||||
def test_authorize_on_target(self):
|
||||
test_rule = NonCallableMock()
|
||||
test_target = NonCallableMock()
|
||||
trove_policy.authorize_on_target(self.context, test_rule, test_target)
|
||||
self.mock_get_enforcer.assert_called_once_with()
|
||||
self.mock_enforcer.authorize.assert_called_once_with(
|
||||
test_rule, test_target, self.context.to_dict(),
|
||||
do_raise=True, exc=trove_exceptions.PolicyNotAuthorized,
|
||||
action=test_rule)
|
@ -23,6 +23,7 @@ import testtools
|
||||
from trove.common import cfg
|
||||
from trove.common.context import TroveContext
|
||||
from trove.common.notification import DBaaSAPINotification
|
||||
from trove.common import policy
|
||||
from trove.tests import root_logger
|
||||
|
||||
|
||||
@ -101,6 +102,11 @@ class TestCase(testtools.TestCase):
|
||||
# Default manager used by all unittsest unless explicitly overridden.
|
||||
self.patch_datastore_manager('mysql')
|
||||
|
||||
policy_patcher = mock.patch.object(policy, 'get_enforcer',
|
||||
return_value=mock.MagicMock())
|
||||
self.addCleanup(policy_patcher.stop)
|
||||
policy_patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
# yes, this is gross and not thread aware.
|
||||
# but the only way to make it thread aware would require that
|
||||
|
Loading…
Reference in New Issue
Block a user