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
|
# Copy api-paste file over to the trove conf dir
|
||||||
cp $TROVE_LOCAL_API_PASTE_INI $TROVE_API_PASTE_INI
|
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
|
# (Re)create trove conf files
|
||||||
rm -f $TROVE_CONF
|
rm -f $TROVE_CONF
|
||||||
rm -f $TROVE_TASKMANAGER_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_CONDUCTOR_CONF=${TROVE_CONDUCTOR_CONF:-${TROVE_CONF_DIR}/trove-conductor.conf}
|
||||||
TROVE_GUESTAGENT_CONF=${TROVE_GUESTAGENT_CONF:-${TROVE_CONF_DIR}/trove-guestagent.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_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_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_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_AUTH_CACHE_DIR=${TROVE_AUTH_CACHE_DIR:-/var/cache/trove}
|
||||||
TROVE_DATASTORE_TYPE=${TROVE_DATASTORE_TYPE:-"mysql"}
|
TROVE_DATASTORE_TYPE=${TROVE_DATASTORE_TYPE:-"mysql"}
|
||||||
TROVE_DATASTORE_VERSION=${TROVE_DATASTORE_VERSION:-"5.6"}
|
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
|
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
|
||||||
xmltodict>=0.10.1 # MIT
|
xmltodict>=0.10.1 # MIT
|
||||||
pycrypto>=2.6 # Public Domain
|
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 import notification
|
||||||
from trove.common.notification import StartNotification
|
from trove.common.notification import StartNotification
|
||||||
from trove.common import pagination
|
from trove.common import pagination
|
||||||
|
from trove.common import policy
|
||||||
from trove.common import wsgi
|
from trove.common import wsgi
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@ -40,6 +41,7 @@ class BackupController(wsgi.Controller):
|
|||||||
LOG.debug("Listing backups for tenant %s" % tenant_id)
|
LOG.debug("Listing backups for tenant %s" % tenant_id)
|
||||||
datastore = req.GET.get('datastore')
|
datastore = req.GET.get('datastore')
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
policy.authorize_on_tenant(context, 'backup:index')
|
||||||
backups, marker = Backup.list(context, datastore)
|
backups, marker = Backup.list(context, datastore)
|
||||||
view = views.BackupViews(backups)
|
view = views.BackupViews(backups)
|
||||||
paged = pagination.SimplePaginatedDataView(req.url, 'backups', view,
|
paged = pagination.SimplePaginatedDataView(req.url, 'backups', view,
|
||||||
@ -52,11 +54,14 @@ class BackupController(wsgi.Controller):
|
|||||||
% (tenant_id, id))
|
% (tenant_id, id))
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
backup = Backup.get_by_id(context, id)
|
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)
|
return wsgi.Result(views.BackupView(backup).data(), 200)
|
||||||
|
|
||||||
def create(self, req, body, tenant_id):
|
def create(self, req, body, tenant_id):
|
||||||
LOG.info(_("Creating a backup for tenant %s"), tenant_id)
|
LOG.info(_("Creating a backup for tenant %s"), tenant_id)
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
policy.authorize_on_tenant(context, 'backup:create')
|
||||||
data = body['backup']
|
data = body['backup']
|
||||||
instance = data['instance']
|
instance = data['instance']
|
||||||
name = data['name']
|
name = data['name']
|
||||||
@ -76,6 +81,9 @@ class BackupController(wsgi.Controller):
|
|||||||
'ID: %(backup_id)s') %
|
'ID: %(backup_id)s') %
|
||||||
{'tenant_id': tenant_id, 'backup_id': id})
|
{'tenant_id': tenant_id, 'backup_id': id})
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
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,
|
context.notification = notification.DBaaSBackupDelete(context,
|
||||||
request=req)
|
request=req)
|
||||||
with StartNotification(context, backup_id=id):
|
with StartNotification(context, backup_id=id):
|
||||||
|
@ -25,6 +25,7 @@ from trove.common.i18n import _
|
|||||||
from trove.common import notification
|
from trove.common import notification
|
||||||
from trove.common.notification import StartNotification
|
from trove.common.notification import StartNotification
|
||||||
from trove.common import pagination
|
from trove.common import pagination
|
||||||
|
from trove.common import policy
|
||||||
from trove.common import utils
|
from trove.common import utils
|
||||||
from trove.common import wsgi
|
from trove.common import wsgi
|
||||||
from trove.datastore import models as datastore_models
|
from trove.datastore import models as datastore_models
|
||||||
@ -39,6 +40,11 @@ class ClusterController(wsgi.Controller):
|
|||||||
"""Controller for cluster functionality."""
|
"""Controller for cluster functionality."""
|
||||||
schemas = apischema.cluster.copy()
|
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
|
@classmethod
|
||||||
def get_action_schema(cls, body, action_schema):
|
def get_action_schema(cls, body, action_schema):
|
||||||
action_type = list(body.keys())[0]
|
action_type = list(body.keys())[0]
|
||||||
@ -58,15 +64,25 @@ class ClusterController(wsgi.Controller):
|
|||||||
{"req": req, "id": id, "tenant_id": tenant_id})
|
{"req": req, "id": id, "tenant_id": tenant_id})
|
||||||
if not body:
|
if not body:
|
||||||
raise exception.BadRequest(_("Invalid request body."))
|
raise exception.BadRequest(_("Invalid request body."))
|
||||||
|
|
||||||
if len(body) != 1:
|
if len(body) != 1:
|
||||||
raise exception.BadRequest(_("Action request should have exactly"
|
raise exception.BadRequest(_("Action request should have exactly"
|
||||||
" one action specified in body"))
|
" one action specified in body"))
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
cluster = models.Cluster.load(context, id)
|
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())))
|
cluster.action(context, req, *next(iter(body.items())))
|
||||||
|
|
||||||
view = views.load_view(cluster, req=req, load_servers=False)
|
view = views.load_view(cluster, req=req, load_servers=False)
|
||||||
wsgi_result = wsgi.Result(view.data(), 202)
|
wsgi_result = wsgi.Result(view.data(), 202)
|
||||||
|
|
||||||
return wsgi_result
|
return wsgi_result
|
||||||
|
|
||||||
def show(self, req, tenant_id, id):
|
def show(self, req, tenant_id, id):
|
||||||
@ -77,6 +93,7 @@ class ClusterController(wsgi.Controller):
|
|||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
cluster = models.Cluster.load(context, id)
|
cluster = models.Cluster.load(context, id)
|
||||||
|
self.authorize_cluster_action(context, 'show', cluster)
|
||||||
return wsgi.Result(views.load_view(cluster, req=req).data(), 200)
|
return wsgi.Result(views.load_view(cluster, req=req).data(), 200)
|
||||||
|
|
||||||
def show_instance(self, req, tenant_id, cluster_id, instance_id):
|
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]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
cluster = models.Cluster.load(context, cluster_id)
|
cluster = models.Cluster.load(context, cluster_id)
|
||||||
|
self.authorize_cluster_action(context, 'show_instance', cluster)
|
||||||
instance = models.Cluster.load_instance(context, cluster.id,
|
instance = models.Cluster.load_instance(context, cluster.id,
|
||||||
instance_id)
|
instance_id)
|
||||||
return wsgi.Result(views.ClusterInstanceDetailView(
|
return wsgi.Result(views.ClusterInstanceDetailView(
|
||||||
@ -105,6 +123,7 @@ class ClusterController(wsgi.Controller):
|
|||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
cluster = models.Cluster.load(context, id)
|
cluster = models.Cluster.load(context, id)
|
||||||
|
self.authorize_cluster_action(context, 'delete', cluster)
|
||||||
context.notification = notification.DBaaSClusterDelete(context,
|
context.notification = notification.DBaaSClusterDelete(context,
|
||||||
request=req)
|
request=req)
|
||||||
with StartNotification(context, cluster_id=id):
|
with StartNotification(context, cluster_id=id):
|
||||||
@ -118,9 +137,19 @@ class ClusterController(wsgi.Controller):
|
|||||||
"tenant_id": tenant_id})
|
"tenant_id": tenant_id})
|
||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
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:
|
if not context.is_admin and context.tenant != tenant_id:
|
||||||
raise exception.TroveOperationAuthError(tenant_id=context.tenant)
|
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
|
# load all clusters and instances for the tenant
|
||||||
clusters, marker = models.Cluster.load_all(context, tenant_id)
|
clusters, marker = models.Cluster.load_all(context, tenant_id)
|
||||||
view = views.ClustersView(clusters, req=req)
|
view = views.ClustersView(clusters, req=req)
|
||||||
@ -134,6 +163,8 @@ class ClusterController(wsgi.Controller):
|
|||||||
{"tenant_id": tenant_id, "req": req, "body": body})
|
{"tenant_id": tenant_id, "req": req, "body": body})
|
||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
policy.authorize_on_tenant(context, 'cluster:create')
|
||||||
|
|
||||||
name = body['cluster']['name']
|
name = body['cluster']['name']
|
||||||
datastore_args = body['cluster'].get('datastore', {})
|
datastore_args = body['cluster'].get('datastore', {})
|
||||||
datastore, datastore_version = (
|
datastore, datastore_version = (
|
||||||
|
@ -236,11 +236,6 @@ class UnprocessableEntity(TroveError):
|
|||||||
message = _("Unable to process the contained request.")
|
message = _("Unable to process the contained request.")
|
||||||
|
|
||||||
|
|
||||||
class UnauthorizedRequest(TroveError):
|
|
||||||
|
|
||||||
message = _("Unauthorized request.")
|
|
||||||
|
|
||||||
|
|
||||||
class CannotResizeToSameSize(TroveError):
|
class CannotResizeToSameSize(TroveError):
|
||||||
|
|
||||||
message = _("No change was requested in the size of the instance.")
|
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.")
|
message = _("User does not have admin privileges.")
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyNotAuthorized(Forbidden):
|
||||||
|
|
||||||
|
message = _("Policy doesn't allow %(action)s to be performed.")
|
||||||
|
|
||||||
|
|
||||||
class InvalidModelError(TroveError):
|
class InvalidModelError(TroveError):
|
||||||
|
|
||||||
message = _("The following values are invalid: %(errors)s.")
|
message = _("The following values are invalid: %(errors)s.")
|
||||||
@ -538,6 +538,10 @@ class ModuleInvalid(Forbidden):
|
|||||||
message = _("The module is invalid: %(reason)s")
|
message = _("The module is invalid: %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceNotFound(NotFound):
|
||||||
|
message = _("Instance '%(instance)s' cannot be found.")
|
||||||
|
|
||||||
|
|
||||||
class ClusterNotFound(NotFound):
|
class ClusterNotFound(NotFound):
|
||||||
message = _("Cluster '%(cluster)s' cannot be found.")
|
message = _("Cluster '%(cluster)s' cannot be found.")
|
||||||
|
|
||||||
@ -622,3 +626,8 @@ class ImageNotFound(NotFound):
|
|||||||
class DatastoreVersionAlreadyExists(BadRequest):
|
class DatastoreVersionAlreadyExists(BadRequest):
|
||||||
|
|
||||||
message = _("A datastore version with the name '%(name)s' already exists.")
|
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.BackupTooLarge,
|
||||||
exception.ModuleAccessForbidden,
|
exception.ModuleAccessForbidden,
|
||||||
exception.ModuleAppliedToInstance,
|
exception.ModuleAppliedToInstance,
|
||||||
|
exception.PolicyNotAuthorized,
|
||||||
|
exception.LogAccessForbidden,
|
||||||
],
|
],
|
||||||
webob.exc.HTTPBadRequest: [
|
webob.exc.HTTPBadRequest: [
|
||||||
exception.InvalidModelError,
|
exception.InvalidModelError,
|
||||||
@ -548,7 +550,8 @@ class ContextMiddleware(base_wsgi.Middleware):
|
|||||||
is_admin=is_admin,
|
is_admin=is_admin,
|
||||||
limit=limits.get('limit'),
|
limit=limits.get('limit'),
|
||||||
marker=limits.get('marker'),
|
marker=limits.get('marker'),
|
||||||
service_catalog=service_catalog)
|
service_catalog=service_catalog,
|
||||||
|
roles=roles)
|
||||||
request.environ[CONTEXT_KEY] = context
|
request.environ[CONTEXT_KEY] = context
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -25,6 +25,7 @@ from trove.common.i18n import _
|
|||||||
from trove.common import notification
|
from trove.common import notification
|
||||||
from trove.common.notification import StartNotification, EndNotification
|
from trove.common.notification import StartNotification, EndNotification
|
||||||
from trove.common import pagination
|
from trove.common import pagination
|
||||||
|
from trove.common import policy
|
||||||
from trove.common import wsgi
|
from trove.common import wsgi
|
||||||
from trove.configuration import models
|
from trove.configuration import models
|
||||||
from trove.configuration.models import DBConfigurationParameter
|
from trove.configuration.models import DBConfigurationParameter
|
||||||
@ -41,9 +42,16 @@ class ConfigurationsController(wsgi.Controller):
|
|||||||
|
|
||||||
schemas = apischema.configuration
|
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):
|
def index(self, req, tenant_id):
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
configs, marker = models.Configurations.load(context)
|
configs, marker = models.Configurations.load(context)
|
||||||
|
policy.authorize_on_tenant(context, 'configuration:index')
|
||||||
view = views.ConfigurationsView(configs)
|
view = views.ConfigurationsView(configs)
|
||||||
paged = pagination.SimplePaginatedDataView(req.url, 'configurations',
|
paged = pagination.SimplePaginatedDataView(req.url, 'configurations',
|
||||||
view, marker)
|
view, marker)
|
||||||
@ -54,6 +62,7 @@ class ConfigurationsController(wsgi.Controller):
|
|||||||
% {"tenant": tenant_id, "id": id})
|
% {"tenant": tenant_id, "id": id})
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
configuration = models.Configuration.load(context, id)
|
configuration = models.Configuration.load(context, id)
|
||||||
|
self.authorize_config_action(context, 'show', configuration)
|
||||||
configuration_items = models.Configuration.load_items(context, id)
|
configuration_items = models.Configuration.load_items(context, id)
|
||||||
|
|
||||||
configuration.instance_count = instances_models.DBInstance.find_all(
|
configuration.instance_count = instances_models.DBInstance.find_all(
|
||||||
@ -68,6 +77,7 @@ class ConfigurationsController(wsgi.Controller):
|
|||||||
def instances(self, req, tenant_id, id):
|
def instances(self, req, tenant_id, id):
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
configuration = models.Configuration.load(context, id)
|
configuration = models.Configuration.load(context, id)
|
||||||
|
self.authorize_config_action(context, 'instances', configuration)
|
||||||
instances = instances_models.DBInstance.find_all(
|
instances = instances_models.DBInstance.find_all(
|
||||||
tenant_id=context.tenant,
|
tenant_id=context.tenant,
|
||||||
configuration_id=configuration.id,
|
configuration_id=configuration.id,
|
||||||
@ -89,6 +99,7 @@ class ConfigurationsController(wsgi.Controller):
|
|||||||
LOG.debug("body : '%s'\n\n" % req)
|
LOG.debug("body : '%s'\n\n" % req)
|
||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
policy.authorize_on_tenant(context, 'configuration:create')
|
||||||
context.notification = notification.DBaaSConfigurationCreate(
|
context.notification = notification.DBaaSConfigurationCreate(
|
||||||
context, request=req)
|
context, request=req)
|
||||||
name = body['configuration']['name']
|
name = body['configuration']['name']
|
||||||
@ -137,10 +148,11 @@ class ConfigurationsController(wsgi.Controller):
|
|||||||
LOG.info(msg % {"tenant_id": tenant_id, "cfg_id": id})
|
LOG.info(msg % {"tenant_id": tenant_id, "cfg_id": id})
|
||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
group = models.Configuration.load(context, id)
|
||||||
|
self.authorize_config_action(context, 'delete', group)
|
||||||
context.notification = notification.DBaaSConfigurationDelete(
|
context.notification = notification.DBaaSConfigurationDelete(
|
||||||
context, request=req)
|
context, request=req)
|
||||||
with StartNotification(context, configuration_id=id):
|
with StartNotification(context, configuration_id=id):
|
||||||
group = models.Configuration.load(context, id)
|
|
||||||
instances = instances_models.DBInstance.find_all(
|
instances = instances_models.DBInstance.find_all(
|
||||||
tenant_id=context.tenant,
|
tenant_id=context.tenant,
|
||||||
configuration_id=id,
|
configuration_id=id,
|
||||||
@ -157,6 +169,15 @@ class ConfigurationsController(wsgi.Controller):
|
|||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
group = models.Configuration.load(context, id)
|
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
|
# if name/description are provided in the request body, update the
|
||||||
# model with these values as well.
|
# model with these values as well.
|
||||||
@ -181,10 +202,11 @@ class ConfigurationsController(wsgi.Controller):
|
|||||||
|
|
||||||
def edit(self, req, body, tenant_id, id):
|
def edit(self, req, body, tenant_id, id):
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
group = models.Configuration.load(context, id)
|
||||||
|
self.authorize_config_action(context, 'edit', group)
|
||||||
context.notification = notification.DBaaSConfigurationEdit(
|
context.notification = notification.DBaaSConfigurationEdit(
|
||||||
context, request=req)
|
context, request=req)
|
||||||
with StartNotification(context, configuration_id=id):
|
with StartNotification(context, configuration_id=id):
|
||||||
group = models.Configuration.load(context, id)
|
|
||||||
items = self._configuration_items_list(group,
|
items = self._configuration_items_list(group,
|
||||||
body['configuration'])
|
body['configuration'])
|
||||||
models.Configuration.save(group, items)
|
models.Configuration.save(group, items)
|
||||||
@ -329,7 +351,18 @@ class ConfigurationsController(wsgi.Controller):
|
|||||||
|
|
||||||
class ParametersController(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):
|
def index(self, req, tenant_id, datastore, id):
|
||||||
|
self.authorize_request(req, 'index')
|
||||||
ds, ds_version = ds_models.get_datastore_version(
|
ds, ds_version = ds_models.get_datastore_version(
|
||||||
type=datastore, version=id)
|
type=datastore, version=id)
|
||||||
rules = models.DatastoreConfigurationParameters.load_parameters(
|
rules = models.DatastoreConfigurationParameters.load_parameters(
|
||||||
@ -338,6 +371,7 @@ class ParametersController(wsgi.Controller):
|
|||||||
200)
|
200)
|
||||||
|
|
||||||
def show(self, req, tenant_id, datastore, id, name):
|
def show(self, req, tenant_id, datastore, id, name):
|
||||||
|
self.authorize_request(req, 'show')
|
||||||
ds, ds_version = ds_models.get_datastore_version(
|
ds, ds_version = ds_models.get_datastore_version(
|
||||||
type=datastore, version=id)
|
type=datastore, version=id)
|
||||||
rule = models.DatastoreConfigurationParameters.load_parameter_by_name(
|
rule = models.DatastoreConfigurationParameters.load_parameter_by_name(
|
||||||
@ -345,6 +379,7 @@ class ParametersController(wsgi.Controller):
|
|||||||
return wsgi.Result(views.ConfigurationParameterView(rule).data(), 200)
|
return wsgi.Result(views.ConfigurationParameterView(rule).data(), 200)
|
||||||
|
|
||||||
def index_by_version(self, req, tenant_id, version):
|
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)
|
ds_version = ds_models.DatastoreVersion.load_by_uuid(version)
|
||||||
rules = models.DatastoreConfigurationParameters.load_parameters(
|
rules = models.DatastoreConfigurationParameters.load_parameters(
|
||||||
ds_version.id)
|
ds_version.id)
|
||||||
@ -352,6 +387,7 @@ class ParametersController(wsgi.Controller):
|
|||||||
200)
|
200)
|
||||||
|
|
||||||
def show_by_version(self, req, tenant_id, version, name):
|
def show_by_version(self, req, tenant_id, version, name):
|
||||||
|
self.authorize_request(req, 'show_by_version')
|
||||||
ds_models.DatastoreVersion.load_by_uuid(version)
|
ds_models.DatastoreVersion.load_by_uuid(version)
|
||||||
rule = models.DatastoreConfigurationParameters.load_parameter_by_name(
|
rule = models.DatastoreConfigurationParameters.load_parameter_by_name(
|
||||||
version, name)
|
version, name)
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from trove.common import policy
|
||||||
from trove.common import wsgi
|
from trove.common import wsgi
|
||||||
from trove.datastore import models, views
|
from trove.datastore import models, views
|
||||||
from trove.flavor import views as flavor_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):
|
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):
|
def show(self, req, tenant_id, id):
|
||||||
|
self.authorize_request(req, 'show')
|
||||||
datastore = models.Datastore.load(id)
|
datastore = models.Datastore.load(id)
|
||||||
datastore_versions = (models.DatastoreVersions.load(datastore.id))
|
datastore_versions = (models.DatastoreVersions.load(datastore.id))
|
||||||
return wsgi.Result(views.
|
return wsgi.Result(views.
|
||||||
@ -31,6 +41,7 @@ class DatastoreController(wsgi.Controller):
|
|||||||
req).data(), 200)
|
req).data(), 200)
|
||||||
|
|
||||||
def index(self, req, tenant_id):
|
def index(self, req, tenant_id):
|
||||||
|
self.authorize_request(req, 'index')
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
only_active = True
|
only_active = True
|
||||||
if context.is_admin:
|
if context.is_admin:
|
||||||
@ -42,17 +53,20 @@ class DatastoreController(wsgi.Controller):
|
|||||||
req).data(), 200)
|
req).data(), 200)
|
||||||
|
|
||||||
def version_show(self, req, tenant_id, datastore, id):
|
def version_show(self, req, tenant_id, datastore, id):
|
||||||
|
self.authorize_request(req, 'version_show')
|
||||||
datastore = models.Datastore.load(datastore)
|
datastore = models.Datastore.load(datastore)
|
||||||
datastore_version = models.DatastoreVersion.load(datastore, id)
|
datastore_version = models.DatastoreVersion.load(datastore, id)
|
||||||
return wsgi.Result(views.DatastoreVersionView(datastore_version,
|
return wsgi.Result(views.DatastoreVersionView(datastore_version,
|
||||||
req).data(), 200)
|
req).data(), 200)
|
||||||
|
|
||||||
def version_show_by_uuid(self, req, tenant_id, uuid):
|
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)
|
datastore_version = models.DatastoreVersion.load_by_uuid(uuid)
|
||||||
return wsgi.Result(views.DatastoreVersionView(datastore_version,
|
return wsgi.Result(views.DatastoreVersionView(datastore_version,
|
||||||
req).data(), 200)
|
req).data(), 200)
|
||||||
|
|
||||||
def version_index(self, req, tenant_id, datastore):
|
def version_index(self, req, tenant_id, datastore):
|
||||||
|
self.authorize_request(req, 'version_index')
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
only_active = True
|
only_active = True
|
||||||
if context.is_admin:
|
if context.is_admin:
|
||||||
@ -70,6 +84,7 @@ class DatastoreController(wsgi.Controller):
|
|||||||
one or more entries are found in datastore_version_metadata,
|
one or more entries are found in datastore_version_metadata,
|
||||||
in which case only those are returned.
|
in which case only those are returned.
|
||||||
"""
|
"""
|
||||||
|
self.authorize_request(req, 'list_associated_flavors')
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
flavors = (models.DatastoreVersionMetadata.
|
flavors = (models.DatastoreVersionMetadata.
|
||||||
list_datastore_version_flavor_associations(
|
list_datastore_version_flavor_associations(
|
||||||
|
@ -21,14 +21,17 @@ from oslo_log import log as logging
|
|||||||
from oslo_utils import importutils
|
from oslo_utils import importutils
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from trove.cluster import models as cluster_models
|
||||||
from trove.cluster.models import DBCluster
|
from trove.cluster.models import DBCluster
|
||||||
from trove.common import cfg
|
from trove.common import cfg
|
||||||
from trove.common import exception
|
from trove.common import exception
|
||||||
from trove.common.i18n import _LI
|
from trove.common.i18n import _LI
|
||||||
|
from trove.common import policy
|
||||||
from trove.common import wsgi
|
from trove.common import wsgi
|
||||||
from trove.datastore import models as datastore_models
|
from trove.datastore import models as datastore_models
|
||||||
from trove.extensions.common import models
|
from trove.extensions.common import models
|
||||||
from trove.extensions.common import views
|
from trove.extensions.common import views
|
||||||
|
from trove.instance import models as instance_models
|
||||||
from trove.instance.models import DBInstance
|
from trove.instance.models import DBInstance
|
||||||
|
|
||||||
|
|
||||||
@ -37,8 +40,30 @@ import_class = importutils.import_class
|
|||||||
CONF = cfg.CONF
|
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)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class BaseDatastoreRootController(wsgi.Controller):
|
class BaseDatastoreRootController(ExtensionController):
|
||||||
"""Base class that defines the contract for root controllers."""
|
"""Base class that defines the contract for root controllers."""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@ -174,13 +199,16 @@ class ClusterRootController(DefaultRootController):
|
|||||||
return single_instance_id, instance_ids
|
return single_instance_id, instance_ids
|
||||||
|
|
||||||
|
|
||||||
class RootController(wsgi.Controller):
|
class RootController(ExtensionController):
|
||||||
"""Controller for instance functionality."""
|
"""Controller for instance functionality."""
|
||||||
|
|
||||||
def index(self, req, tenant_id, instance_id):
|
def index(self, req, tenant_id, instance_id):
|
||||||
"""Returns True if root is enabled; False otherwise."""
|
"""Returns True if root is enabled; False otherwise."""
|
||||||
datastore_manager, is_cluster = self._get_datastore(tenant_id,
|
datastore_manager, is_cluster = self._get_datastore(tenant_id,
|
||||||
instance_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)
|
root_controller = self.load_root_controller(datastore_manager)
|
||||||
return root_controller.root_index(req, tenant_id, instance_id,
|
return root_controller.root_index(req, tenant_id, instance_id,
|
||||||
is_cluster)
|
is_cluster)
|
||||||
@ -189,6 +217,9 @@ class RootController(wsgi.Controller):
|
|||||||
"""Enable the root user for the db instance."""
|
"""Enable the root user for the db instance."""
|
||||||
datastore_manager, is_cluster = self._get_datastore(tenant_id,
|
datastore_manager, is_cluster = self._get_datastore(tenant_id,
|
||||||
instance_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)
|
root_controller = self.load_root_controller(datastore_manager)
|
||||||
if root_controller is not None:
|
if root_controller is not None:
|
||||||
return root_controller.root_create(req, body, tenant_id,
|
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):
|
def delete(self, req, tenant_id, instance_id):
|
||||||
datastore_manager, is_cluster = self._get_datastore(tenant_id,
|
datastore_manager, is_cluster = self._get_datastore(tenant_id,
|
||||||
instance_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)
|
root_controller = self.load_root_controller(datastore_manager)
|
||||||
if root_controller is not None:
|
if root_controller is not None:
|
||||||
return root_controller.root_delete(req, tenant_id,
|
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.utils import correct_id_with_req
|
||||||
from trove.common import wsgi
|
from trove.common import wsgi
|
||||||
from trove.extensions.common.service import DefaultRootController
|
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_users
|
||||||
from trove.extensions.mysql.common import populate_validated_databases
|
from trove.extensions.mysql.common import populate_validated_databases
|
||||||
from trove.extensions.mysql.common import unquote_user_host
|
from trove.extensions.mysql.common import unquote_user_host
|
||||||
@ -42,7 +43,7 @@ import_class = importutils.import_class
|
|||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
class UserController(wsgi.Controller):
|
class UserController(ExtensionController):
|
||||||
"""Controller for instance functionality."""
|
"""Controller for instance functionality."""
|
||||||
schemas = apischema.user
|
schemas = apischema.user
|
||||||
|
|
||||||
@ -60,6 +61,7 @@ class UserController(wsgi.Controller):
|
|||||||
"req : '%(req)s'\n\n") %
|
"req : '%(req)s'\n\n") %
|
||||||
{"id": instance_id, "req": req})
|
{"id": instance_id, "req": req})
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
self.authorize_target_action(context, 'user:index', instance_id)
|
||||||
users, next_marker = models.Users.load(context, instance_id)
|
users, next_marker = models.Users.load(context, instance_id)
|
||||||
view = views.UsersView(users)
|
view = views.UsersView(users)
|
||||||
paged = pagination.SimplePaginatedDataView(req.url, 'users', view,
|
paged = pagination.SimplePaginatedDataView(req.url, 'users', view,
|
||||||
@ -75,6 +77,7 @@ class UserController(wsgi.Controller):
|
|||||||
"req": strutils.mask_password(req),
|
"req": strutils.mask_password(req),
|
||||||
"body": strutils.mask_password(body)})
|
"body": strutils.mask_password(body)})
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
self.authorize_target_action(context, 'user:create', instance_id)
|
||||||
context.notification = notification.DBaaSUserCreate(context,
|
context.notification = notification.DBaaSUserCreate(context,
|
||||||
request=req)
|
request=req)
|
||||||
users = body['users']
|
users = body['users']
|
||||||
@ -94,6 +97,7 @@ class UserController(wsgi.Controller):
|
|||||||
"req : '%(req)s'\n\n") %
|
"req : '%(req)s'\n\n") %
|
||||||
{"id": instance_id, "req": req})
|
{"id": instance_id, "req": req})
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
self.authorize_target_action(context, 'user:delete', instance_id)
|
||||||
id = correct_id_with_req(id, req)
|
id = correct_id_with_req(id, req)
|
||||||
username, host = unquote_user_host(id)
|
username, host = unquote_user_host(id)
|
||||||
user = None
|
user = None
|
||||||
@ -122,6 +126,7 @@ class UserController(wsgi.Controller):
|
|||||||
"req : '%(req)s'\n\n") %
|
"req : '%(req)s'\n\n") %
|
||||||
{"id": instance_id, "req": req})
|
{"id": instance_id, "req": req})
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
self.authorize_target_action(context, 'user:show', instance_id)
|
||||||
id = correct_id_with_req(id, req)
|
id = correct_id_with_req(id, req)
|
||||||
username, host = unquote_user_host(id)
|
username, host = unquote_user_host(id)
|
||||||
user = None
|
user = None
|
||||||
@ -141,6 +146,7 @@ class UserController(wsgi.Controller):
|
|||||||
"req : '%(req)s'\n\n") %
|
"req : '%(req)s'\n\n") %
|
||||||
{"id": instance_id, "req": strutils.mask_password(req)})
|
{"id": instance_id, "req": strutils.mask_password(req)})
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
self.authorize_target_action(context, 'user:update', instance_id)
|
||||||
id = correct_id_with_req(id, req)
|
id = correct_id_with_req(id, req)
|
||||||
username, hostname = unquote_user_host(id)
|
username, hostname = unquote_user_host(id)
|
||||||
user = None
|
user = None
|
||||||
@ -171,6 +177,7 @@ class UserController(wsgi.Controller):
|
|||||||
"req : '%(req)s'\n\n") %
|
"req : '%(req)s'\n\n") %
|
||||||
{"id": instance_id, "req": strutils.mask_password(req)})
|
{"id": instance_id, "req": strutils.mask_password(req)})
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
self.authorize_target_action(context, 'user:update_all', instance_id)
|
||||||
context.notification = notification.DBaaSUserChangePassword(
|
context.notification = notification.DBaaSUserChangePassword(
|
||||||
context, request=req)
|
context, request=req)
|
||||||
users = body['users']
|
users = body['users']
|
||||||
@ -203,7 +210,7 @@ class UserController(wsgi.Controller):
|
|||||||
return wsgi.Result(None, 202)
|
return wsgi.Result(None, 202)
|
||||||
|
|
||||||
|
|
||||||
class UserAccessController(wsgi.Controller):
|
class UserAccessController(ExtensionController):
|
||||||
"""Controller for adding and removing database access for a user."""
|
"""Controller for adding and removing database access for a user."""
|
||||||
schemas = apischema.user
|
schemas = apischema.user
|
||||||
|
|
||||||
@ -232,6 +239,8 @@ class UserAccessController(wsgi.Controller):
|
|||||||
{"id": instance_id, "req": req})
|
{"id": instance_id, "req": req})
|
||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
self.authorize_target_action(
|
||||||
|
context, 'user_access:index', instance_id)
|
||||||
# Make sure this user exists.
|
# Make sure this user exists.
|
||||||
user_id = correct_id_with_req(user_id, req)
|
user_id = correct_id_with_req(user_id, req)
|
||||||
user = self._get_user(context, instance_id, user_id)
|
user = self._get_user(context, instance_id, user_id)
|
||||||
@ -249,6 +258,8 @@ class UserAccessController(wsgi.Controller):
|
|||||||
"req : '%(req)s'\n\n") %
|
"req : '%(req)s'\n\n") %
|
||||||
{"id": instance_id, "req": req})
|
{"id": instance_id, "req": req})
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
self.authorize_target_action(
|
||||||
|
context, 'user_access:update', instance_id)
|
||||||
context.notification = notification.DBaaSUserGrant(
|
context.notification = notification.DBaaSUserGrant(
|
||||||
context, request=req)
|
context, request=req)
|
||||||
user_id = correct_id_with_req(user_id, req)
|
user_id = correct_id_with_req(user_id, req)
|
||||||
@ -270,6 +281,8 @@ class UserAccessController(wsgi.Controller):
|
|||||||
"req : '%(req)s'\n\n") %
|
"req : '%(req)s'\n\n") %
|
||||||
{"id": instance_id, "req": req})
|
{"id": instance_id, "req": req})
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
self.authorize_target_action(
|
||||||
|
context, 'user_access:delete', instance_id)
|
||||||
context.notification = notification.DBaaSUserRevoke(
|
context.notification = notification.DBaaSUserRevoke(
|
||||||
context, request=req)
|
context, request=req)
|
||||||
user_id = correct_id_with_req(user_id, req)
|
user_id = correct_id_with_req(user_id, req)
|
||||||
@ -288,7 +301,7 @@ class UserAccessController(wsgi.Controller):
|
|||||||
return wsgi.Result(None, 202)
|
return wsgi.Result(None, 202)
|
||||||
|
|
||||||
|
|
||||||
class SchemaController(wsgi.Controller):
|
class SchemaController(ExtensionController):
|
||||||
"""Controller for instance functionality."""
|
"""Controller for instance functionality."""
|
||||||
schemas = apischema.dbschema
|
schemas = apischema.dbschema
|
||||||
|
|
||||||
@ -299,6 +312,8 @@ class SchemaController(wsgi.Controller):
|
|||||||
{"id": instance_id, "req": req})
|
{"id": instance_id, "req": req})
|
||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
self.authorize_target_action(
|
||||||
|
context, 'database:index', instance_id)
|
||||||
schemas, next_marker = models.Schemas.load(context, instance_id)
|
schemas, next_marker = models.Schemas.load(context, instance_id)
|
||||||
view = views.SchemasView(schemas)
|
view = views.SchemasView(schemas)
|
||||||
paged = pagination.SimplePaginatedDataView(req.url, 'databases', view,
|
paged = pagination.SimplePaginatedDataView(req.url, 'databases', view,
|
||||||
@ -315,6 +330,8 @@ class SchemaController(wsgi.Controller):
|
|||||||
"body": body})
|
"body": body})
|
||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
self.authorize_target_action(
|
||||||
|
context, 'database:create', instance_id)
|
||||||
schemas = body['databases']
|
schemas = body['databases']
|
||||||
context.notification = notification.DBaaSDatabaseCreate(context,
|
context.notification = notification.DBaaSDatabaseCreate(context,
|
||||||
request=req)
|
request=req)
|
||||||
@ -334,6 +351,8 @@ class SchemaController(wsgi.Controller):
|
|||||||
"req : '%(req)s'\n\n") %
|
"req : '%(req)s'\n\n") %
|
||||||
{"id": instance_id, "req": req})
|
{"id": instance_id, "req": req})
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
self.authorize_target_action(
|
||||||
|
context, 'database:delete', instance_id)
|
||||||
context.notification = notification.DBaaSDatabaseDelete(
|
context.notification = notification.DBaaSDatabaseDelete(
|
||||||
context, request=req)
|
context, request=req)
|
||||||
with StartNotification(context, instance_id=instance_id, dbname=id):
|
with StartNotification(context, instance_id=instance_id, dbname=id):
|
||||||
@ -349,6 +368,9 @@ class SchemaController(wsgi.Controller):
|
|||||||
return wsgi.Result(None, 202)
|
return wsgi.Result(None, 202)
|
||||||
|
|
||||||
def show(self, req, tenant_id, instance_id, id):
|
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()
|
raise webob.exc.HTTPNotImplemented()
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import six
|
import six
|
||||||
|
|
||||||
from trove.common import exception
|
from trove.common import exception
|
||||||
|
from trove.common import policy
|
||||||
from trove.common import wsgi
|
from trove.common import wsgi
|
||||||
from trove.flavor import models
|
from trove.flavor import models
|
||||||
from trove.flavor import views
|
from trove.flavor import views
|
||||||
@ -30,12 +31,16 @@ class FlavorController(wsgi.Controller):
|
|||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
self._validate_flavor_id(id)
|
self._validate_flavor_id(id)
|
||||||
flavor = models.Flavor(context=context, 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.
|
# Pass in the request to build accurate links.
|
||||||
return wsgi.Result(views.FlavorView(flavor, req).data(), 200)
|
return wsgi.Result(views.FlavorView(flavor, req).data(), 200)
|
||||||
|
|
||||||
def index(self, req, tenant_id):
|
def index(self, req, tenant_id):
|
||||||
"""Return all flavors."""
|
"""Return all flavors."""
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
policy.authorize_on_tenant(context, 'flavor:index')
|
||||||
flavors = models.Flavors(context=context)
|
flavors = models.Flavors(context=context)
|
||||||
return wsgi.Result(views.FlavorsView(flavors, req).data(), 200)
|
return wsgi.Result(views.FlavorsView(flavors, req).data(), 200)
|
||||||
|
|
||||||
|
@ -209,8 +209,7 @@ class GuestLog(object):
|
|||||||
'metafile': self._metafile_name()
|
'metafile': self._metafile_name()
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise exception.UnauthorizedRequest(_(
|
raise exception.LogAccessForbidden(action='show', log=self._name)
|
||||||
"Not authorized to show log '%s'.") % self._name)
|
|
||||||
|
|
||||||
def _refresh_details(self):
|
def _refresh_details(self):
|
||||||
|
|
||||||
@ -310,16 +309,16 @@ class GuestLog(object):
|
|||||||
self._file)
|
self._file)
|
||||||
return self.show()
|
return self.show()
|
||||||
else:
|
else:
|
||||||
raise exception.UnauthorizedRequest(_(
|
raise exception.LogAccessForbidden(
|
||||||
"Not authorized to publish log '%s'.") % self._name)
|
action='publish', log=self._name)
|
||||||
|
|
||||||
def discard_log(self):
|
def discard_log(self):
|
||||||
if self.exposed:
|
if self.exposed:
|
||||||
self._delete_log_components()
|
self._delete_log_components()
|
||||||
return self.show()
|
return self.show()
|
||||||
else:
|
else:
|
||||||
raise exception.UnauthorizedRequest(_(
|
raise exception.LogAccessForbidden(
|
||||||
"Not authorized to discard log '%s'.") % self._name)
|
action='discard', log=self._name)
|
||||||
|
|
||||||
def _delete_log_components(self):
|
def _delete_log_components(self):
|
||||||
container_name = self.get_container_name(force=True)
|
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 import notification
|
||||||
from trove.common.notification import StartNotification
|
from trove.common.notification import StartNotification
|
||||||
from trove.common import pagination
|
from trove.common import pagination
|
||||||
|
from trove.common import policy
|
||||||
from trove.common.remote import create_guest_client
|
from trove.common.remote import create_guest_client
|
||||||
from trove.common import utils
|
from trove.common import utils
|
||||||
from trove.common import wsgi
|
from trove.common import wsgi
|
||||||
@ -47,6 +48,11 @@ class InstanceController(wsgi.Controller):
|
|||||||
"""Controller for instance functionality."""
|
"""Controller for instance functionality."""
|
||||||
schemas = apischema.instance.copy()
|
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
|
@classmethod
|
||||||
def get_action_schema(cls, body, action_schema):
|
def get_action_schema(cls, body, action_schema):
|
||||||
action_type = list(body.keys())[0]
|
action_type = list(body.keys())[0]
|
||||||
@ -106,6 +112,7 @@ class InstanceController(wsgi.Controller):
|
|||||||
def _action_restart(self, context, req, instance, body):
|
def _action_restart(self, context, req, instance, body):
|
||||||
context.notification = notification.DBaaSInstanceRestart(context,
|
context.notification = notification.DBaaSInstanceRestart(context,
|
||||||
request=req)
|
request=req)
|
||||||
|
self.authorize_instance_action(context, 'restart', instance)
|
||||||
with StartNotification(context, instance_id=instance.id):
|
with StartNotification(context, instance_id=instance.id):
|
||||||
instance.restart()
|
instance.restart()
|
||||||
return wsgi.Result(None, 202)
|
return wsgi.Result(None, 202)
|
||||||
@ -136,6 +143,8 @@ class InstanceController(wsgi.Controller):
|
|||||||
def _action_resize_volume(self, context, req, instance, volume):
|
def _action_resize_volume(self, context, req, instance, volume):
|
||||||
context.notification = notification.DBaaSInstanceResizeVolume(
|
context.notification = notification.DBaaSInstanceResizeVolume(
|
||||||
context, request=req)
|
context, request=req)
|
||||||
|
self.authorize_instance_action(context, 'resize_volume', instance)
|
||||||
|
|
||||||
with StartNotification(context, instance_id=instance.id,
|
with StartNotification(context, instance_id=instance.id,
|
||||||
new_size=volume['size']):
|
new_size=volume['size']):
|
||||||
instance.resize_volume(volume['size'])
|
instance.resize_volume(volume['size'])
|
||||||
@ -144,6 +153,8 @@ class InstanceController(wsgi.Controller):
|
|||||||
def _action_resize_flavor(self, context, req, instance, flavorRef):
|
def _action_resize_flavor(self, context, req, instance, flavorRef):
|
||||||
context.notification = notification.DBaaSInstanceResizeInstance(
|
context.notification = notification.DBaaSInstanceResizeInstance(
|
||||||
context, request=req)
|
context, request=req)
|
||||||
|
self.authorize_instance_action(context, 'resize_flavor', instance)
|
||||||
|
|
||||||
new_flavor_id = utils.get_id_from_href(flavorRef)
|
new_flavor_id = utils.get_id_from_href(flavorRef)
|
||||||
with StartNotification(context, instance_id=instance.id,
|
with StartNotification(context, instance_id=instance.id,
|
||||||
new_flavor_id=new_flavor_id):
|
new_flavor_id=new_flavor_id):
|
||||||
@ -154,6 +165,8 @@ class InstanceController(wsgi.Controller):
|
|||||||
raise webob.exc.HTTPNotImplemented()
|
raise webob.exc.HTTPNotImplemented()
|
||||||
|
|
||||||
def _action_promote_to_replica_source(self, context, req, instance, body):
|
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,
|
context.notification = notification.DBaaSInstanceEject(context,
|
||||||
request=req)
|
request=req)
|
||||||
with StartNotification(context, instance_id=instance.id):
|
with StartNotification(context, instance_id=instance.id):
|
||||||
@ -161,6 +174,8 @@ class InstanceController(wsgi.Controller):
|
|||||||
return wsgi.Result(None, 202)
|
return wsgi.Result(None, 202)
|
||||||
|
|
||||||
def _action_eject_replica_source(self, context, req, instance, body):
|
def _action_eject_replica_source(self, context, req, instance, body):
|
||||||
|
self.authorize_instance_action(
|
||||||
|
context, 'eject_replica_source', instance)
|
||||||
context.notification = notification.DBaaSInstancePromote(context,
|
context.notification = notification.DBaaSInstancePromote(context,
|
||||||
request=req)
|
request=req)
|
||||||
with StartNotification(context, instance_id=instance.id):
|
with StartNotification(context, instance_id=instance.id):
|
||||||
@ -168,6 +183,11 @@ class InstanceController(wsgi.Controller):
|
|||||||
return wsgi.Result(None, 202)
|
return wsgi.Result(None, 202)
|
||||||
|
|
||||||
def _action_reset_status(self, context, req, instance, body):
|
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.notification = notification.DBaaSInstanceResetStatus(
|
||||||
context, request=req)
|
context, request=req)
|
||||||
with StartNotification(context, instance_id=instance.id):
|
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.info(_LI("Listing database instances for tenant '%s'"), tenant_id)
|
||||||
LOG.debug("req : '%s'\n\n", req)
|
LOG.debug("req : '%s'\n\n", req)
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
policy.authorize_on_tenant(context, 'instance:index')
|
||||||
clustered_q = req.GET.get('include_clustered', '').lower()
|
clustered_q = req.GET.get('include_clustered', '').lower()
|
||||||
include_clustered = clustered_q == 'true'
|
include_clustered = clustered_q == 'true'
|
||||||
servers, marker = models.Instances.load(context, include_clustered)
|
servers, marker = models.Instances.load(context, include_clustered)
|
||||||
@ -197,6 +218,10 @@ class InstanceController(wsgi.Controller):
|
|||||||
id)
|
id)
|
||||||
LOG.debug("req : '%s'\n\n", req)
|
LOG.debug("req : '%s'\n\n", req)
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
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)
|
backups, marker = backup_model.list_for_instance(context, id)
|
||||||
view = backup_views.BackupViews(backups)
|
view = backup_views.BackupViews(backups)
|
||||||
paged = pagination.SimplePaginatedDataView(req.url, 'backups', view,
|
paged = pagination.SimplePaginatedDataView(req.url, 'backups', view,
|
||||||
@ -213,6 +238,7 @@ class InstanceController(wsgi.Controller):
|
|||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
server = models.load_instance_with_info(models.DetailInstance,
|
server = models.load_instance_with_info(models.DetailInstance,
|
||||||
context, id)
|
context, id)
|
||||||
|
self.authorize_instance_action(context, 'show', server)
|
||||||
return wsgi.Result(views.InstanceDetailView(server,
|
return wsgi.Result(views.InstanceDetailView(server,
|
||||||
req=req).data(), 200)
|
req=req).data(), 200)
|
||||||
|
|
||||||
@ -224,6 +250,7 @@ class InstanceController(wsgi.Controller):
|
|||||||
LOG.debug("req : '%s'\n\n", req)
|
LOG.debug("req : '%s'\n\n", req)
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
instance = models.load_any_instance(context, id)
|
instance = models.load_any_instance(context, id)
|
||||||
|
self.authorize_instance_action(context, 'delete', instance)
|
||||||
context.notification = notification.DBaaSInstanceDelete(
|
context.notification = notification.DBaaSInstanceDelete(
|
||||||
context, request=req)
|
context, request=req)
|
||||||
with StartNotification(context, instance_id=instance.id):
|
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("req : '%s'\n\n", strutils.mask_password(req))
|
||||||
LOG.debug("body : '%s'\n\n", strutils.mask_password(body))
|
LOG.debug("body : '%s'\n\n", strutils.mask_password(body))
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
policy.authorize_on_tenant(context, 'instance:create')
|
||||||
context.notification = notification.DBaaSInstanceCreate(context,
|
context.notification = notification.DBaaSInstanceCreate(context,
|
||||||
request=req)
|
request=req)
|
||||||
datastore_args = body['instance'].get('datastore', {})
|
datastore_args = body['instance'].get('datastore', {})
|
||||||
@ -268,6 +296,25 @@ class InstanceController(wsgi.Controller):
|
|||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
raise exception.BadRequest(msg=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']:
|
if 'volume' in body['instance']:
|
||||||
volume_info = body['instance']['volume']
|
volume_info = body['instance']['volume']
|
||||||
volume_size = int(volume_info['size'])
|
volume_size = int(volume_info['size'])
|
||||||
@ -289,7 +336,6 @@ class InstanceController(wsgi.Controller):
|
|||||||
# also check for older name
|
# also check for older name
|
||||||
body['instance'].get('slave_of'))
|
body['instance'].get('slave_of'))
|
||||||
replica_count = body['instance'].get('replica_count')
|
replica_count = body['instance'].get('replica_count')
|
||||||
modules = body['instance'].get('modules')
|
|
||||||
locality = body['instance'].get('locality')
|
locality = body['instance'].get('locality')
|
||||||
if locality:
|
if locality:
|
||||||
locality_domain = ['affinity', 'anti-affinity']
|
locality_domain = ['affinity', 'anti-affinity']
|
||||||
@ -371,6 +417,7 @@ class InstanceController(wsgi.Controller):
|
|||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
|
||||||
instance = models.Instance.load(context, id)
|
instance = models.Instance.load(context, id)
|
||||||
|
self.authorize_instance_action(context, 'update', instance)
|
||||||
|
|
||||||
# Make sure args contains a 'configuration_id' argument,
|
# Make sure args contains a 'configuration_id' argument,
|
||||||
args = {}
|
args = {}
|
||||||
@ -388,6 +435,7 @@ class InstanceController(wsgi.Controller):
|
|||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
|
||||||
instance = models.Instance.load(context, id)
|
instance = models.Instance.load(context, id)
|
||||||
|
self.authorize_instance_action(context, 'edit', instance)
|
||||||
|
|
||||||
args = {}
|
args = {}
|
||||||
args['detach_replica'] = ('replica_of' in body['instance'] or
|
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)
|
LOG.info(_LI("Getting default configuration for instance %s"), id)
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
instance = models.Instance.load(context, id)
|
instance = models.Instance.load(context, id)
|
||||||
|
self.authorize_instance_action(context, 'configuration', instance)
|
||||||
|
|
||||||
LOG.debug("Server: %s", instance)
|
LOG.debug("Server: %s", instance)
|
||||||
config = instance.get_default_configuration_template()
|
config = instance.get_default_configuration_template()
|
||||||
LOG.debug("Default config for instance %(instance_id)s is %(config)s",
|
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)
|
instance = models.Instance.load(context, id)
|
||||||
if not instance:
|
if not instance:
|
||||||
raise exception.NotFound(uuid=id)
|
raise exception.NotFound(uuid=id)
|
||||||
|
self.authorize_instance_action(context, 'guest_log_list', instance)
|
||||||
client = create_guest_client(context, id)
|
client = create_guest_client(context, id)
|
||||||
guest_log_list = client.guest_log_list()
|
guest_log_list = client.guest_log_list()
|
||||||
return wsgi.Result({'logs': guest_log_list}, 200)
|
return wsgi.Result({'logs': guest_log_list}, 200)
|
||||||
@ -454,6 +505,7 @@ class InstanceController(wsgi.Controller):
|
|||||||
instance = models.Instance.load(context, id)
|
instance = models.Instance.load(context, id)
|
||||||
if not instance:
|
if not instance:
|
||||||
raise exception.NotFound(uuid=id)
|
raise exception.NotFound(uuid=id)
|
||||||
|
self.authorize_instance_action(context, 'module_list', instance)
|
||||||
from_guest = bool(req.GET.get('from_guest', '').lower())
|
from_guest = bool(req.GET.get('from_guest', '').lower())
|
||||||
include_contents = bool(req.GET.get('include_contents', '').lower())
|
include_contents = bool(req.GET.get('include_contents', '').lower())
|
||||||
if from_guest:
|
if from_guest:
|
||||||
@ -481,6 +533,7 @@ class InstanceController(wsgi.Controller):
|
|||||||
instance = models.Instance.load(context, id)
|
instance = models.Instance.load(context, id)
|
||||||
if not instance:
|
if not instance:
|
||||||
raise exception.NotFound(uuid=id)
|
raise exception.NotFound(uuid=id)
|
||||||
|
self.authorize_instance_action(context, 'module_apply', instance)
|
||||||
module_ids = [mod['id'] for mod in body.get('modules', [])]
|
module_ids = [mod['id'] for mod in body.get('modules', [])]
|
||||||
modules = module_models.Modules.load_by_ids(context, module_ids)
|
modules = module_models.Modules.load_by_ids(context, module_ids)
|
||||||
module_list = []
|
module_list = []
|
||||||
@ -501,6 +554,7 @@ class InstanceController(wsgi.Controller):
|
|||||||
instance = models.Instance.load(context, id)
|
instance = models.Instance.load(context, id)
|
||||||
if not instance:
|
if not instance:
|
||||||
raise exception.NotFound(uuid=id)
|
raise exception.NotFound(uuid=id)
|
||||||
|
self.authorize_instance_action(context, 'module_remove', instance)
|
||||||
module = module_models.Module.load(context, module_id)
|
module = module_models.Module.load(context, module_id)
|
||||||
module_info = module_views.DetailedModuleView(module).data()
|
module_info = module_views.DetailedModuleView(module).data()
|
||||||
client = create_guest_client(context, id)
|
client = create_guest_client(context, id)
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from trove.common import policy
|
||||||
from trove.common import wsgi
|
from trove.common import wsgi
|
||||||
from trove.limits import views
|
from trove.limits import views
|
||||||
from trove.quota.quota import QUOTAS
|
from trove.quota.quota import QUOTAS
|
||||||
@ -27,6 +28,8 @@ class LimitsController(wsgi.Controller):
|
|||||||
"""
|
"""
|
||||||
Return all absolute and rate limit information.
|
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)
|
quotas = QUOTAS.get_all_quotas_by_tenant(tenant_id)
|
||||||
abs_limits = {k: v['hard_limit'] for k, v in quotas.items()}
|
abs_limits = {k: v['hard_limit'] for k, v in quotas.items()}
|
||||||
rate_limits = req.environ.get("trove.limits", [])
|
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 import exception
|
||||||
from trove.common.i18n import _
|
from trove.common.i18n import _
|
||||||
from trove.common import pagination
|
from trove.common import pagination
|
||||||
|
from trove.common import policy
|
||||||
from trove.common import wsgi
|
from trove.common import wsgi
|
||||||
from trove.datastore import models as datastore_models
|
from trove.datastore import models as datastore_models
|
||||||
from trove.instance import models as instance_models
|
from trove.instance import models as instance_models
|
||||||
@ -37,8 +38,20 @@ class ModuleController(wsgi.Controller):
|
|||||||
|
|
||||||
schemas = apischema.module
|
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):
|
def index(self, req, tenant_id):
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
policy.authorize_on_tenant(context, 'module:index')
|
||||||
datastore = req.GET.get('datastore', '')
|
datastore = req.GET.get('datastore', '')
|
||||||
if datastore and datastore.lower() != models.Modules.MATCH_ALL_NAME:
|
if datastore and datastore.lower() != models.Modules.MATCH_ALL_NAME:
|
||||||
ds, ds_ver = datastore_models.get_datastore_version(
|
ds, ds_ver = datastore_models.get_datastore_version(
|
||||||
@ -53,6 +66,7 @@ class ModuleController(wsgi.Controller):
|
|||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
module = models.Module.load(context, id)
|
module = models.Module.load(context, id)
|
||||||
|
self.authorize_module_action(context, 'show', module)
|
||||||
module.instance_count = len(models.InstanceModules.load(
|
module.instance_count = len(models.InstanceModules.load(
|
||||||
context, module_id=module.id, md5=module.md5))
|
context, module_id=module.id, md5=module.md5))
|
||||||
|
|
||||||
@ -65,6 +79,7 @@ class ModuleController(wsgi.Controller):
|
|||||||
LOG.info(_("Creating module '%s'") % name)
|
LOG.info(_("Creating module '%s'") % name)
|
||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
|
policy.authorize_on_tenant(context, 'module:create')
|
||||||
module_type = body['module']['module_type']
|
module_type = body['module']['module_type']
|
||||||
contents = body['module']['contents']
|
contents = body['module']['contents']
|
||||||
|
|
||||||
@ -89,6 +104,7 @@ class ModuleController(wsgi.Controller):
|
|||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
module = models.Module.load(context, id)
|
module = models.Module.load(context, id)
|
||||||
|
self.authorize_module_action(context, 'delete', module)
|
||||||
models.Module.delete(context, module)
|
models.Module.delete(context, module)
|
||||||
return wsgi.Result(None, 200)
|
return wsgi.Result(None, 200)
|
||||||
|
|
||||||
@ -97,6 +113,7 @@ class ModuleController(wsgi.Controller):
|
|||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
context = req.environ[wsgi.CONTEXT_KEY]
|
||||||
module = models.Module.load(context, id)
|
module = models.Module.load(context, id)
|
||||||
|
self.authorize_module_action(context, 'update', module)
|
||||||
original_module = copy.deepcopy(module)
|
original_module = copy.deepcopy(module)
|
||||||
if 'name' in body['module']:
|
if 'name' in body['module']:
|
||||||
module.name = body['module']['name']
|
module.name = body['module']['name']
|
||||||
@ -146,6 +163,10 @@ class ModuleController(wsgi.Controller):
|
|||||||
LOG.info(_("Getting instances for module %s") % id)
|
LOG.info(_("Getting instances for module %s") % id)
|
||||||
|
|
||||||
context = req.environ[wsgi.CONTEXT_KEY]
|
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(
|
instance_modules, marker = models.InstanceModules.load(
|
||||||
context, module_id=id)
|
context, module_id=id)
|
||||||
if instance_modules:
|
if instance_modules:
|
||||||
|
@ -18,6 +18,7 @@ from proboscis import after_class
|
|||||||
from proboscis.asserts import assert_equal
|
from proboscis.asserts import assert_equal
|
||||||
from proboscis.asserts import assert_raises
|
from proboscis.asserts import assert_raises
|
||||||
from proboscis import before_class
|
from proboscis import before_class
|
||||||
|
from proboscis import SkipTest
|
||||||
from proboscis import test
|
from proboscis import test
|
||||||
|
|
||||||
from trove.backup import models as backup_models
|
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 import models as imodels
|
||||||
from trove.instance.models import DBInstance
|
from trove.instance.models import DBInstance
|
||||||
from trove.instance.tasks import InstanceTasks
|
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 create_dbaas_client
|
||||||
from trove.tests.util import test_config
|
from trove.tests.util import test_config
|
||||||
from trove.tests.util.users import Requirements
|
from trove.tests.util.users import Requirements
|
||||||
@ -79,6 +81,7 @@ class MgmtInstanceBase(object):
|
|||||||
|
|
||||||
@test(groups=[GROUP])
|
@test(groups=[GROUP])
|
||||||
class RestartTaskStatusTests(MgmtInstanceBase):
|
class RestartTaskStatusTests(MgmtInstanceBase):
|
||||||
|
|
||||||
@before_class
|
@before_class
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RestartTaskStatusTests, self).setUp()
|
super(RestartTaskStatusTests, self).setUp()
|
||||||
@ -137,6 +140,9 @@ class RestartTaskStatusTests(MgmtInstanceBase):
|
|||||||
|
|
||||||
@test
|
@test
|
||||||
def mgmt_reset_task_status_clears_backups(self):
|
def mgmt_reset_task_status_clears_backups(self):
|
||||||
|
if CONFIG.fake_mode:
|
||||||
|
raise SkipTest("Test requires an instance.")
|
||||||
|
|
||||||
self.reset_task_status()
|
self.reset_task_status()
|
||||||
self._reload_db_info()
|
self._reload_db_info()
|
||||||
assert_equal(self.db_info.task_status, InstanceTasks.NONE)
|
assert_equal(self.db_info.task_status, InstanceTasks.NONE)
|
||||||
@ -201,5 +207,6 @@ class RestartTaskStatusTests(MgmtInstanceBase):
|
|||||||
found_backup.delete()
|
found_backup.delete()
|
||||||
admin = test_config.users.find_user(Requirements(is_admin=True))
|
admin = test_config.users.find_user(Requirements(is_admin=True))
|
||||||
admin_dbaas = create_dbaas_client(admin)
|
admin_dbaas = create_dbaas_client(admin)
|
||||||
result = admin_dbaas.instances.backups(self.db_info.id)
|
if not CONFIG.fake_mode:
|
||||||
assert_equal(0, len(result))
|
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):
|
def run_delete_build_instance(self, expected_http_code=202):
|
||||||
if self.build_inst_id:
|
if self.build_inst_id:
|
||||||
self.auth_client.instances.force_delete(self.build_inst_id)
|
self.admin_client.instances.force_delete(self.build_inst_id)
|
||||||
self.assert_client_code(expected_http_code)
|
self.assert_client_code(expected_http_code,
|
||||||
|
client=self.admin_client)
|
||||||
|
|
||||||
def run_wait_for_force_delete(self):
|
def run_wait_for_force_delete(self):
|
||||||
if self.build_inst_id:
|
if self.build_inst_id:
|
||||||
|
@ -45,6 +45,7 @@ class BaseLimitTestSuite(trove_testtools.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(BaseLimitTestSuite, self).setUp()
|
super(BaseLimitTestSuite, self).setUp()
|
||||||
|
self.context = trove_testtools.TroveTestContext(self)
|
||||||
self.absolute_limits = {"max_instances": 55,
|
self.absolute_limits = {"max_instances": 55,
|
||||||
"max_volumes": 100,
|
"max_volumes": 100,
|
||||||
"max_backups": 40}
|
"max_backups": 40}
|
||||||
@ -60,7 +61,7 @@ class LimitsControllerTest(BaseLimitTestSuite):
|
|||||||
limit_controller = LimitsController()
|
limit_controller = LimitsController()
|
||||||
|
|
||||||
req = MagicMock()
|
req = MagicMock()
|
||||||
req.environ = {}
|
req.environ = {'trove.context': self.context}
|
||||||
|
|
||||||
view = limit_controller.index(req, "test_tenant_id")
|
view = limit_controller.index(req, "test_tenant_id")
|
||||||
expected = {'limits': [{'verb': 'ABSOLUTE'}]}
|
expected = {'limits': [{'verb': 'ABSOLUTE'}]}
|
||||||
@ -122,7 +123,7 @@ class LimitsControllerTest(BaseLimitTestSuite):
|
|||||||
hard_limit=55)}
|
hard_limit=55)}
|
||||||
|
|
||||||
req = MagicMock()
|
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',
|
with patch.object(QUOTAS, 'get_all_quotas_by_tenant',
|
||||||
return_value=abs_limits):
|
return_value=abs_limits):
|
||||||
|
@ -18,7 +18,6 @@ import jsonschema
|
|||||||
from mock import MagicMock
|
from mock import MagicMock
|
||||||
from mock import Mock
|
from mock import Mock
|
||||||
from mock import patch
|
from mock import patch
|
||||||
from testtools import TestCase
|
|
||||||
from testtools.matchers import Is, Equals
|
from testtools.matchers import Is, Equals
|
||||||
from trove.cluster import models
|
from trove.cluster import models
|
||||||
from trove.cluster.models import Cluster, DBCluster
|
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
|
from trove.tests.unittests import trove_testtools
|
||||||
|
|
||||||
|
|
||||||
class TestClusterController(TestCase):
|
class TestClusterController(trove_testtools.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestClusterController, self).setUp()
|
super(TestClusterController, self).setUp()
|
||||||
self.controller = ClusterController()
|
self.controller = ClusterController()
|
||||||
@ -248,7 +248,8 @@ class TestClusterController(TestCase):
|
|||||||
cluster.delete.assert_called_with()
|
cluster.delete.assert_called_with()
|
||||||
|
|
||||||
|
|
||||||
class TestClusterControllerWithStrategy(TestCase):
|
class TestClusterControllerWithStrategy(trove_testtools.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestClusterControllerWithStrategy, self).setUp()
|
super(TestClusterControllerWithStrategy, self).setUp()
|
||||||
self.controller = ClusterController()
|
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 ClusterRootController
|
||||||
from trove.extensions.common.service import DefaultRootController
|
from trove.extensions.common.service import DefaultRootController
|
||||||
from trove.extensions.common.service import RootController
|
from trove.extensions.common.service import RootController
|
||||||
|
from trove.instance import models as instance_models
|
||||||
from trove.instance.models import DBInstance
|
from trove.instance.models import DBInstance
|
||||||
from trove.tests.unittests import trove_testtools
|
from trove.tests.unittests import trove_testtools
|
||||||
|
|
||||||
@ -90,16 +91,20 @@ class TestRootController(trove_testtools.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestRootController, self).setUp()
|
super(TestRootController, self).setUp()
|
||||||
|
self.context = trove_testtools.TroveTestContext(self)
|
||||||
self.controller = RootController()
|
self.controller = RootController()
|
||||||
|
|
||||||
|
@patch.object(instance_models.Instance, "load")
|
||||||
@patch.object(RootController, "load_root_controller")
|
@patch.object(RootController, "load_root_controller")
|
||||||
@patch.object(RootController, "_get_datastore")
|
@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 = Mock()
|
||||||
|
req.environ = {'trove.context': self.context}
|
||||||
tenant_id = Mock()
|
tenant_id = Mock()
|
||||||
uuid = utils.generate_uuid()
|
uuid = utils.generate_uuid()
|
||||||
ds_manager = Mock()
|
ds_manager = Mock()
|
||||||
is_cluster = Mock()
|
is_cluster = False
|
||||||
service_get_datastore.return_value = (ds_manager, is_cluster)
|
service_get_datastore.return_value = (ds_manager, is_cluster)
|
||||||
root_controller = Mock()
|
root_controller = Mock()
|
||||||
ret = Mock()
|
ret = Mock()
|
||||||
@ -112,15 +117,18 @@ class TestRootController(trove_testtools.TestCase):
|
|||||||
root_controller.root_index.assert_called_with(
|
root_controller.root_index.assert_called_with(
|
||||||
req, tenant_id, uuid, is_cluster)
|
req, tenant_id, uuid, is_cluster)
|
||||||
|
|
||||||
|
@patch.object(instance_models.Instance, "load")
|
||||||
@patch.object(RootController, "load_root_controller")
|
@patch.object(RootController, "load_root_controller")
|
||||||
@patch.object(RootController, "_get_datastore")
|
@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 = Mock()
|
||||||
|
req.environ = {'trove.context': self.context}
|
||||||
body = Mock()
|
body = Mock()
|
||||||
tenant_id = Mock()
|
tenant_id = Mock()
|
||||||
uuid = utils.generate_uuid()
|
uuid = utils.generate_uuid()
|
||||||
ds_manager = Mock()
|
ds_manager = Mock()
|
||||||
is_cluster = Mock()
|
is_cluster = False
|
||||||
service_get_datastore.return_value = (ds_manager, is_cluster)
|
service_get_datastore.return_value = (ds_manager, is_cluster)
|
||||||
root_controller = Mock()
|
root_controller = Mock()
|
||||||
ret = Mock()
|
ret = Mock()
|
||||||
@ -134,17 +142,20 @@ class TestRootController(trove_testtools.TestCase):
|
|||||||
root_controller.root_create.assert_called_with(
|
root_controller.root_create.assert_called_with(
|
||||||
req, body, tenant_id, uuid, is_cluster)
|
req, body, tenant_id, uuid, is_cluster)
|
||||||
|
|
||||||
|
@patch.object(instance_models.Instance, "load")
|
||||||
@patch.object(RootController, "load_root_controller")
|
@patch.object(RootController, "load_root_controller")
|
||||||
@patch.object(RootController, "_get_datastore")
|
@patch.object(RootController, "_get_datastore")
|
||||||
def test_create_with_no_root_controller(self,
|
def test_create_with_no_root_controller(self,
|
||||||
service_get_datastore,
|
service_get_datastore,
|
||||||
service_load_root_controller):
|
service_load_root_controller,
|
||||||
|
service_load_instance):
|
||||||
req = Mock()
|
req = Mock()
|
||||||
|
req.environ = {'trove.context': self.context}
|
||||||
body = Mock()
|
body = Mock()
|
||||||
tenant_id = Mock()
|
tenant_id = Mock()
|
||||||
uuid = utils.generate_uuid()
|
uuid = utils.generate_uuid()
|
||||||
ds_manager = Mock()
|
ds_manager = Mock()
|
||||||
is_cluster = Mock()
|
is_cluster = False
|
||||||
service_get_datastore.return_value = (ds_manager, is_cluster)
|
service_get_datastore.return_value = (ds_manager, is_cluster)
|
||||||
service_load_root_controller.return_value = None
|
service_load_root_controller.return_value = None
|
||||||
|
|
||||||
@ -160,6 +171,7 @@ class TestClusterRootController(trove_testtools.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestClusterRootController, self).setUp()
|
super(TestClusterRootController, self).setUp()
|
||||||
|
self.context = trove_testtools.TroveTestContext(self)
|
||||||
self.controller = ClusterRootController()
|
self.controller = ClusterRootController()
|
||||||
|
|
||||||
@patch.object(ClusterRootController, "cluster_root_index")
|
@patch.object(ClusterRootController, "cluster_root_index")
|
||||||
@ -204,22 +216,18 @@ class TestClusterRootController(trove_testtools.TestCase):
|
|||||||
|
|
||||||
@patch.object(models.ClusterRoot, "load")
|
@patch.object(models.ClusterRoot, "load")
|
||||||
def test_instance_root_index(self, mock_cluster_root_load):
|
def test_instance_root_index(self, mock_cluster_root_load):
|
||||||
context = Mock()
|
|
||||||
req = Mock()
|
req = Mock()
|
||||||
req.environ = Mock()
|
req.environ = {'trove.context': self.context}
|
||||||
req.environ.__getitem__ = Mock(return_value=context)
|
|
||||||
tenant_id = Mock()
|
tenant_id = Mock()
|
||||||
instance_id = utils.generate_uuid()
|
instance_id = utils.generate_uuid()
|
||||||
self.controller.instance_root_index(req, tenant_id, instance_id)
|
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",
|
@patch.object(models.ClusterRoot, "load",
|
||||||
side_effect=exception.UnprocessableEntity())
|
side_effect=exception.UnprocessableEntity())
|
||||||
def test_instance_root_index_exception(self, mock_cluster_root_load):
|
def test_instance_root_index_exception(self, mock_cluster_root_load):
|
||||||
context = Mock()
|
|
||||||
req = Mock()
|
req = Mock()
|
||||||
req.environ = Mock()
|
req.environ = {'trove.context': self.context}
|
||||||
req.environ.__getitem__ = Mock(return_value=context)
|
|
||||||
tenant_id = Mock()
|
tenant_id = Mock()
|
||||||
instance_id = utils.generate_uuid()
|
instance_id = utils.generate_uuid()
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
@ -227,7 +235,7 @@ class TestClusterRootController(trove_testtools.TestCase):
|
|||||||
self.controller.instance_root_index,
|
self.controller.instance_root_index,
|
||||||
req, tenant_id, instance_id
|
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, "instance_root_index")
|
||||||
@patch.object(ClusterRootController, "_get_cluster_instance_id")
|
@patch.object(ClusterRootController, "_get_cluster_instance_id")
|
||||||
@ -278,12 +286,10 @@ class TestClusterRootController(trove_testtools.TestCase):
|
|||||||
@patch.object(models.ClusterRoot, "create")
|
@patch.object(models.ClusterRoot, "create")
|
||||||
def test_instance_root_create(self, mock_cluster_root_create):
|
def test_instance_root_create(self, mock_cluster_root_create):
|
||||||
user = Mock()
|
user = Mock()
|
||||||
context = Mock()
|
self.context.user = Mock()
|
||||||
context.user = Mock()
|
self.context.user.__getitem__ = Mock(return_value=user)
|
||||||
context.user.__getitem__ = Mock(return_value=user)
|
|
||||||
req = Mock()
|
req = Mock()
|
||||||
req.environ = Mock()
|
req.environ = {'trove.context': self.context}
|
||||||
req.environ.__getitem__ = Mock(return_value=context)
|
|
||||||
password = Mock()
|
password = Mock()
|
||||||
body = {'password': password}
|
body = {'password': password}
|
||||||
instance_id = utils.generate_uuid()
|
instance_id = utils.generate_uuid()
|
||||||
@ -291,17 +297,16 @@ class TestClusterRootController(trove_testtools.TestCase):
|
|||||||
self.controller.instance_root_create(
|
self.controller.instance_root_create(
|
||||||
req, body, instance_id, cluster_instances)
|
req, body, instance_id, cluster_instances)
|
||||||
mock_cluster_root_create.assert_called_with(
|
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")
|
@patch.object(models.ClusterRoot, "create")
|
||||||
def test_instance_root_create_no_body(self, mock_cluster_root_create):
|
def test_instance_root_create_no_body(self, mock_cluster_root_create):
|
||||||
user = Mock()
|
user = Mock()
|
||||||
context = Mock()
|
self.context.user = Mock()
|
||||||
context.user = Mock()
|
self.context.user.__getitem__ = Mock(return_value=user)
|
||||||
context.user.__getitem__ = Mock(return_value=user)
|
|
||||||
req = Mock()
|
req = Mock()
|
||||||
req.environ = Mock()
|
req.environ = {'trove.context': self.context}
|
||||||
req.environ.__getitem__ = Mock(return_value=context)
|
|
||||||
password = None
|
password = None
|
||||||
body = None
|
body = None
|
||||||
instance_id = utils.generate_uuid()
|
instance_id = utils.generate_uuid()
|
||||||
@ -309,4 +314,5 @@ class TestClusterRootController(trove_testtools.TestCase):
|
|||||||
self.controller.instance_root_create(
|
self.controller.instance_root_create(
|
||||||
req, body, instance_id, cluster_instances)
|
req, body, instance_id, cluster_instances)
|
||||||
mock_cluster_root_create.assert_called_with(
|
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 import cfg
|
||||||
from trove.common.context import TroveContext
|
from trove.common.context import TroveContext
|
||||||
from trove.common.notification import DBaaSAPINotification
|
from trove.common.notification import DBaaSAPINotification
|
||||||
|
from trove.common import policy
|
||||||
from trove.tests import root_logger
|
from trove.tests import root_logger
|
||||||
|
|
||||||
|
|
||||||
@ -101,6 +102,11 @@ class TestCase(testtools.TestCase):
|
|||||||
# Default manager used by all unittsest unless explicitly overridden.
|
# Default manager used by all unittsest unless explicitly overridden.
|
||||||
self.patch_datastore_manager('mysql')
|
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):
|
def tearDown(self):
|
||||||
# yes, this is gross and not thread aware.
|
# yes, this is gross and not thread aware.
|
||||||
# but the only way to make it thread aware would require that
|
# but the only way to make it thread aware would require that
|
||||||
|
Loading…
Reference in New Issue
Block a user