Adds the 'public network' concept to Quantum
Implements blueprint quantum-v2-public-networks This patch allows Quantum to handle public networks. It modifies the API adding a new attribute to the network resource ('shared') and enhances the policy engine in order to handle the behaviour of the service wrt shared networks. Policy.json specifies a default behaviour which can be changed by the administrator, even at runtime. Tests added to test_db_plugin validate 'obvious' behaviour - such as that only the ports belonging to a given tenant should be returned even when they are queried on a public network. Tests added to test_policy instead validate the changes added to the policy engine. Change-Id: I0087d449a677ed29357cd3cb4d8580bece940fdf
This commit is contained in:
parent
f01dafb010
commit
f3b64410af
@ -1,23 +1,37 @@
|
|||||||
{
|
{
|
||||||
"admin_or_owner": [["role:admin"], ["tenant_id:%(tenant_id)s"]],
|
"admin_or_owner": [["role:admin"], ["tenant_id:%(tenant_id)s"]],
|
||||||
|
"admin_or_network_owner": [["role:admin"], ["tenant_id:%(network_tenant_id)s"]],
|
||||||
|
"admin_only": [["role:admin"]],
|
||||||
|
"regular_user": [],
|
||||||
"default": [["rule:admin_or_owner"]],
|
"default": [["rule:admin_or_owner"]],
|
||||||
|
|
||||||
"admin_api": [["role:admin"]],
|
"extension:provider_network:view": [["rule:admin_only"]],
|
||||||
"extension:provider_network:view": [["rule:admin_api"]],
|
"extension:provider_network:set": [["rule:admin_only"]],
|
||||||
"extension:provider_network:set": [["rule:admin_api"]],
|
|
||||||
|
|
||||||
"create_subnet": [],
|
"networks:private:read": [["rule:admin_or_owner"]],
|
||||||
|
"networks:private:write": [["rule:admin_or_owner"]],
|
||||||
|
"networks:shared:read": [["rule:regular_user"]],
|
||||||
|
"networks:shared:write": [["rule:admin_only"]],
|
||||||
|
|
||||||
|
"create_subnet": [["rule:admin_or_network_owner"]],
|
||||||
"get_subnet": [["rule:admin_or_owner"]],
|
"get_subnet": [["rule:admin_or_owner"]],
|
||||||
"update_subnet": [["rule:admin_or_owner"]],
|
"update_subnet": [["rule:admin_or_network_owner"]],
|
||||||
"delete_subnet": [["rule:admin_or_owner"]],
|
"delete_subnet": [["rule:admin_or_network_owner"]],
|
||||||
|
|
||||||
"create_network": [],
|
"create_network": [],
|
||||||
"get_network": [["rule:admin_or_owner"]],
|
"get_network": [],
|
||||||
"update_network": [["rule:admin_or_owner"]],
|
"create_network:shared": [["rule:admin_only"]],
|
||||||
"delete_network": [["rule:admin_or_owner"]],
|
"update_network": [],
|
||||||
|
"update_network:shared": [["rule:admin_only"]],
|
||||||
|
"delete_network": [],
|
||||||
|
|
||||||
"create_port": [],
|
"create_port": [],
|
||||||
|
"create_port:mac_address": [["rule:admin_or_network_owner"]],
|
||||||
|
"create_port:host_routes": [["rule:admin_or_network_owner"]],
|
||||||
|
"create_port:fixed_ips": [["rule:admin_or_network_owner"]],
|
||||||
"get_port": [["rule:admin_or_owner"]],
|
"get_port": [["rule:admin_or_owner"]],
|
||||||
"update_port": [["rule:admin_or_owner"]],
|
"update_port": [["rule:admin_or_owner"]],
|
||||||
|
"update_port:host_routes": [["rule:admin_or_network_owner"]],
|
||||||
|
"update_port:fixed_ips": [["rule:admin_or_network_owner"]],
|
||||||
"delete_port": [["rule:admin_or_owner"]]
|
"delete_port": [["rule:admin_or_owner"]]
|
||||||
}
|
}
|
||||||
|
@ -14,16 +14,8 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
ATTR_NOT_SPECIFIED = object()
|
ATTR_NOT_SPECIFIED = object()
|
||||||
|
# Defining a constant to avoid repeating string literal in several modules
|
||||||
# Note: a default of ATTR_NOT_SPECIFIED indicates that an
|
SHARED = 'shared'
|
||||||
# attribute is not required, but will be generated by the plugin
|
|
||||||
# if it is not specified. Particularly, a value of ATTR_NOT_SPECIFIED
|
|
||||||
# is different from an attribute that has been specified with a value of
|
|
||||||
# None. For example, if 'gateway_ip' is ommitted in a request to
|
|
||||||
# create a subnet, the plugin will receive ATTR_NOT_SPECIFIED
|
|
||||||
# and the default gateway_ip will be generated.
|
|
||||||
# However, if gateway_ip is specified as None, this means that
|
|
||||||
# the subnet does not have a gateway IP.
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import netaddr
|
import netaddr
|
||||||
@ -137,10 +129,20 @@ validators = {'type:boolean': _validate_boolean,
|
|||||||
# and the default gateway_ip will be generated.
|
# and the default gateway_ip will be generated.
|
||||||
# However, if gateway_ip is specified as None, this means that
|
# However, if gateway_ip is specified as None, this means that
|
||||||
# the subnet does not have a gateway IP.
|
# the subnet does not have a gateway IP.
|
||||||
# Some of the following attributes are used by the policy engine.
|
# The following is a short reference for understanding attribute info:
|
||||||
# They are explicitly marked with the required_by_policy flag to ensure
|
# default: default value of the attribute (if missing, the attribute
|
||||||
# they are always returned by a plugin for policy processing, even if
|
# becomes mandatory.
|
||||||
# they are not specified in the 'fields' query param
|
# allow_post: the attribute can be used on POST requests.
|
||||||
|
# allow_put: the attribute can be used on PUT requests.
|
||||||
|
# validate: specifies rules for validating data in the attribute.
|
||||||
|
# convert_to: transformation to apply to the value before it is returned
|
||||||
|
# is_visible: the attribute is returned in GET responses.
|
||||||
|
# required_by_policy: the attribute is required by the policy engine and
|
||||||
|
# should therefore be filled by the API layer even if not present in
|
||||||
|
# request body.
|
||||||
|
# enforce_policy: the attribute is actively part of the policy enforcing
|
||||||
|
# mechanism, ie: there might be rules which refer to this attribute.
|
||||||
|
|
||||||
RESOURCE_ATTRIBUTE_MAP = {
|
RESOURCE_ATTRIBUTE_MAP = {
|
||||||
'networks': {
|
'networks': {
|
||||||
'id': {'allow_post': False, 'allow_put': False,
|
'id': {'allow_post': False, 'allow_put': False,
|
||||||
@ -160,7 +162,15 @@ RESOURCE_ATTRIBUTE_MAP = {
|
|||||||
'is_visible': True},
|
'is_visible': True},
|
||||||
'tenant_id': {'allow_post': True, 'allow_put': False,
|
'tenant_id': {'allow_post': True, 'allow_put': False,
|
||||||
'required_by_policy': True,
|
'required_by_policy': True,
|
||||||
'is_visible': True}
|
'is_visible': True},
|
||||||
|
SHARED: {'allow_post': True,
|
||||||
|
'allow_put': True,
|
||||||
|
'default': False,
|
||||||
|
'convert_to': convert_to_boolean,
|
||||||
|
'validate': {'type:boolean': None},
|
||||||
|
'is_visible': True,
|
||||||
|
'required_by_policy': True,
|
||||||
|
'enforce_policy': True},
|
||||||
},
|
},
|
||||||
'ports': {
|
'ports': {
|
||||||
'id': {'allow_post': False, 'allow_put': False,
|
'id': {'allow_post': False, 'allow_put': False,
|
||||||
@ -179,12 +189,15 @@ RESOURCE_ATTRIBUTE_MAP = {
|
|||||||
'mac_address': {'allow_post': True, 'allow_put': False,
|
'mac_address': {'allow_post': True, 'allow_put': False,
|
||||||
'default': ATTR_NOT_SPECIFIED,
|
'default': ATTR_NOT_SPECIFIED,
|
||||||
'validate': {'type:mac_address': None},
|
'validate': {'type:mac_address': None},
|
||||||
|
'enforce_policy': True,
|
||||||
'is_visible': True},
|
'is_visible': True},
|
||||||
'fixed_ips': {'allow_post': True, 'allow_put': True,
|
'fixed_ips': {'allow_post': True, 'allow_put': True,
|
||||||
'default': ATTR_NOT_SPECIFIED,
|
'default': ATTR_NOT_SPECIFIED,
|
||||||
|
'enforce_policy': True,
|
||||||
'is_visible': True},
|
'is_visible': True},
|
||||||
'host_routes': {'allow_post': True, 'allow_put': True,
|
'host_routes': {'allow_post': True, 'allow_put': True,
|
||||||
'default': ATTR_NOT_SPECIFIED,
|
'default': ATTR_NOT_SPECIFIED,
|
||||||
|
'enforce_policy': True,
|
||||||
'is_visible': False},
|
'is_visible': False},
|
||||||
'device_id': {'allow_post': True, 'allow_put': True,
|
'device_id': {'allow_post': True, 'allow_put': True,
|
||||||
'default': '',
|
'default': '',
|
||||||
@ -235,3 +248,11 @@ RESOURCE_ATTRIBUTE_MAP = {
|
|||||||
'is_visible': True},
|
'is_visible': True},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Associates to each resource its own parent resource
|
||||||
|
# Resources without parents, such as networks, are not in this list
|
||||||
|
|
||||||
|
RESOURCE_HIERARCHY_MAP = {
|
||||||
|
'ports': {'parent': 'networks', 'identified_by': 'network_id'},
|
||||||
|
'subnets': {'parent': 'networks', 'identified_by': 'network_id'}
|
||||||
|
}
|
||||||
|
@ -40,6 +40,7 @@ FAULT_MAP = {exceptions.NotFound: webob.exc.HTTPNotFound,
|
|||||||
exceptions.OverlappingAllocationPools: webob.exc.HTTPConflict,
|
exceptions.OverlappingAllocationPools: webob.exc.HTTPConflict,
|
||||||
exceptions.OutOfBoundsAllocationPool: webob.exc.HTTPBadRequest,
|
exceptions.OutOfBoundsAllocationPool: webob.exc.HTTPBadRequest,
|
||||||
exceptions.InvalidAllocationPool: webob.exc.HTTPBadRequest,
|
exceptions.InvalidAllocationPool: webob.exc.HTTPBadRequest,
|
||||||
|
exceptions.InvalidSharedSetting: webob.exc.HTTPConflict,
|
||||||
}
|
}
|
||||||
|
|
||||||
QUOTAS = quota.QUOTAS
|
QUOTAS = quota.QUOTAS
|
||||||
@ -122,8 +123,7 @@ class Controller(object):
|
|||||||
self._resource = resource
|
self._resource = resource
|
||||||
self._attr_info = attr_info
|
self._attr_info = attr_info
|
||||||
self._policy_attrs = [name for (name, info) in self._attr_info.items()
|
self._policy_attrs = [name for (name, info) in self._attr_info.items()
|
||||||
if 'required_by_policy' in info
|
if info.get('required_by_policy')]
|
||||||
and info['required_by_policy']]
|
|
||||||
self._publisher_id = notifier_api.publisher_id('network')
|
self._publisher_id = notifier_api.publisher_id('network')
|
||||||
|
|
||||||
def _is_visible(self, attr):
|
def _is_visible(self, attr):
|
||||||
@ -160,33 +160,32 @@ class Controller(object):
|
|||||||
obj_list = obj_getter(request.context, **kwargs)
|
obj_list = obj_getter(request.context, **kwargs)
|
||||||
# Check authz
|
# Check authz
|
||||||
if do_authz:
|
if do_authz:
|
||||||
|
# FIXME(salvatore-orlando): obj_getter might return references to
|
||||||
|
# other resources. Must check authZ on them too.
|
||||||
# Omit items from list that should not be visible
|
# Omit items from list that should not be visible
|
||||||
obj_list = [obj for obj in obj_list
|
obj_list = [obj for obj in obj_list
|
||||||
if policy.check(request.context,
|
if policy.check(request.context,
|
||||||
"get_%s" % self._resource,
|
"get_%s" % self._resource,
|
||||||
obj)]
|
obj,
|
||||||
|
plugin=self._plugin)]
|
||||||
|
|
||||||
return {self._collection: [self._view(obj,
|
return {self._collection: [self._view(obj,
|
||||||
fields_to_strip=fields_to_add)
|
fields_to_strip=fields_to_add)
|
||||||
for obj in obj_list]}
|
for obj in obj_list]}
|
||||||
|
|
||||||
def _item(self, request, id, do_authz=False):
|
def _item(self, request, id, do_authz=False, field_list=None):
|
||||||
"""Retrieves and formats a single element of the requested entity"""
|
"""Retrieves and formats a single element of the requested entity"""
|
||||||
# NOTE(salvatore-orlando): The following ensures that fields which
|
|
||||||
# are needed for authZ policy validation are not stripped away by the
|
|
||||||
# plugin before returning.
|
|
||||||
field_list, added_fields = self._do_field_list(fields(request))
|
|
||||||
kwargs = {'verbose': verbose(request),
|
kwargs = {'verbose': verbose(request),
|
||||||
'fields': field_list}
|
'fields': field_list}
|
||||||
action = "get_%s" % self._resource
|
action = "get_%s" % self._resource
|
||||||
obj_getter = getattr(self._plugin, action)
|
obj_getter = getattr(self._plugin, action)
|
||||||
obj = obj_getter(request.context, id, **kwargs)
|
obj = obj_getter(request.context, id, **kwargs)
|
||||||
|
|
||||||
# Check authz
|
# Check authz
|
||||||
|
# FIXME(salvatore-orlando): obj_getter might return references to
|
||||||
|
# other resources. Must check authZ on them too.
|
||||||
if do_authz:
|
if do_authz:
|
||||||
policy.enforce(request.context, action, obj)
|
policy.enforce(request.context, action, obj, plugin=self._plugin)
|
||||||
|
return obj
|
||||||
return {self._resource: self._view(obj, fields_to_strip=added_fields)}
|
|
||||||
|
|
||||||
def index(self, request):
|
def index(self, request):
|
||||||
"""Returns a list of the requested entity"""
|
"""Returns a list of the requested entity"""
|
||||||
@ -195,7 +194,16 @@ class Controller(object):
|
|||||||
def show(self, request, id):
|
def show(self, request, id):
|
||||||
"""Returns detailed information about the requested entity"""
|
"""Returns detailed information about the requested entity"""
|
||||||
try:
|
try:
|
||||||
return self._item(request, id, True)
|
# NOTE(salvatore-orlando): The following ensures that fields
|
||||||
|
# which are needed for authZ policy validation are not stripped
|
||||||
|
# away by the plugin before returning.
|
||||||
|
field_list, added_fields = self._do_field_list(fields(request))
|
||||||
|
return {self._resource:
|
||||||
|
self._view(self._item(request,
|
||||||
|
id,
|
||||||
|
do_authz=True,
|
||||||
|
field_list=field_list),
|
||||||
|
fields_to_strip=added_fields)}
|
||||||
except exceptions.PolicyNotAuthorized:
|
except exceptions.PolicyNotAuthorized:
|
||||||
# To avoid giving away information, pretend that it
|
# To avoid giving away information, pretend that it
|
||||||
# doesn't exist
|
# doesn't exist
|
||||||
@ -221,11 +229,10 @@ class Controller(object):
|
|||||||
request,
|
request,
|
||||||
item[self._resource],
|
item[self._resource],
|
||||||
)
|
)
|
||||||
policy.enforce(
|
policy.enforce(request.context,
|
||||||
request.context,
|
|
||||||
action,
|
action,
|
||||||
item[self._resource],
|
item[self._resource],
|
||||||
)
|
plugin=self._plugin)
|
||||||
count = QUOTAS.count(request.context, self._resource,
|
count = QUOTAS.count(request.context, self._resource,
|
||||||
self._plugin, self._collection,
|
self._plugin, self._collection,
|
||||||
item[self._resource]['tenant_id'])
|
item[self._resource]['tenant_id'])
|
||||||
@ -236,13 +243,17 @@ class Controller(object):
|
|||||||
request,
|
request,
|
||||||
body[self._resource]
|
body[self._resource]
|
||||||
)
|
)
|
||||||
policy.enforce(request.context, action, body[self._resource])
|
policy.enforce(request.context,
|
||||||
|
action,
|
||||||
|
body[self._resource],
|
||||||
|
plugin=self._plugin)
|
||||||
count = QUOTAS.count(request.context, self._resource,
|
count = QUOTAS.count(request.context, self._resource,
|
||||||
self._plugin, self._collection,
|
self._plugin, self._collection,
|
||||||
body[self._resource]['tenant_id'])
|
body[self._resource]['tenant_id'])
|
||||||
kwargs = {self._resource: count + 1}
|
kwargs = {self._resource: count + 1}
|
||||||
QUOTAS.limit_check(request.context, **kwargs)
|
QUOTAS.limit_check(request.context, **kwargs)
|
||||||
except exceptions.PolicyNotAuthorized:
|
except exceptions.PolicyNotAuthorized:
|
||||||
|
LOG.exception("Create operation not authorized")
|
||||||
raise webob.exc.HTTPForbidden()
|
raise webob.exc.HTTPForbidden()
|
||||||
|
|
||||||
obj_creator = getattr(self._plugin, action)
|
obj_creator = getattr(self._plugin, action)
|
||||||
@ -266,9 +277,12 @@ class Controller(object):
|
|||||||
action = "delete_%s" % self._resource
|
action = "delete_%s" % self._resource
|
||||||
|
|
||||||
# Check authz
|
# Check authz
|
||||||
obj = self._item(request, id)[self._resource]
|
obj = self._item(request, id)
|
||||||
try:
|
try:
|
||||||
policy.enforce(request.context, action, obj)
|
policy.enforce(request.context,
|
||||||
|
action,
|
||||||
|
obj,
|
||||||
|
plugin=self._plugin)
|
||||||
except exceptions.PolicyNotAuthorized:
|
except exceptions.PolicyNotAuthorized:
|
||||||
# To avoid giving away information, pretend that it
|
# To avoid giving away information, pretend that it
|
||||||
# doesn't exist
|
# doesn't exist
|
||||||
@ -293,11 +307,21 @@ class Controller(object):
|
|||||||
payload)
|
payload)
|
||||||
body = self._prepare_request_body(request.context, body, False)
|
body = self._prepare_request_body(request.context, body, False)
|
||||||
action = "update_%s" % self._resource
|
action = "update_%s" % self._resource
|
||||||
|
# Load object to check authz
|
||||||
|
# but pass only attributes in the original body and required
|
||||||
|
# by the policy engine to the policy 'brain'
|
||||||
|
field_list = [name for (name, value) in self._attr_info.iteritems()
|
||||||
|
if ('required_by_policy' in value and
|
||||||
|
value['required_by_policy'] or
|
||||||
|
not 'default' in value)]
|
||||||
|
orig_obj = self._item(request, id, field_list=field_list)
|
||||||
|
orig_obj.update(body)
|
||||||
|
|
||||||
# Check authz
|
|
||||||
orig_obj = self._item(request, id)[self._resource]
|
|
||||||
try:
|
try:
|
||||||
policy.enforce(request.context, action, orig_obj)
|
policy.enforce(request.context,
|
||||||
|
action,
|
||||||
|
orig_obj,
|
||||||
|
plugin=self._plugin)
|
||||||
except exceptions.PolicyNotAuthorized:
|
except exceptions.PolicyNotAuthorized:
|
||||||
# To avoid giving away information, pretend that it
|
# To avoid giving away information, pretend that it
|
||||||
# doesn't exist
|
# doesn't exist
|
||||||
@ -345,7 +369,6 @@ class Controller(object):
|
|||||||
raise webob.exc.HTTPBadRequest(_("Resource body required"))
|
raise webob.exc.HTTPBadRequest(_("Resource body required"))
|
||||||
|
|
||||||
body = body or {self._resource: {}}
|
body = body or {self._resource: {}}
|
||||||
|
|
||||||
if self._collection in body and allow_bulk:
|
if self._collection in body and allow_bulk:
|
||||||
bulk_body = [self._prepare_request_body(context,
|
bulk_body = [self._prepare_request_body(context,
|
||||||
{self._resource: b},
|
{self._resource: b},
|
||||||
@ -411,17 +434,21 @@ class Controller(object):
|
|||||||
msg = _("Invalid input for %(attr)s. "
|
msg = _("Invalid input for %(attr)s. "
|
||||||
"Reason: %(reason)s.") % msg_dict
|
"Reason: %(reason)s.") % msg_dict
|
||||||
raise webob.exc.HTTPUnprocessableEntity(msg)
|
raise webob.exc.HTTPUnprocessableEntity(msg)
|
||||||
|
|
||||||
return body
|
return body
|
||||||
|
|
||||||
def _validate_network_tenant_ownership(self, request, resource_item):
|
def _validate_network_tenant_ownership(self, request, resource_item):
|
||||||
|
# TODO(salvatore-orlando): consider whether this check can be folded
|
||||||
|
# in the policy engine
|
||||||
if self._resource not in ('port', 'subnet'):
|
if self._resource not in ('port', 'subnet'):
|
||||||
return
|
return
|
||||||
|
network = self._plugin.get_network(
|
||||||
network_owner = self._plugin.get_network(
|
|
||||||
request.context,
|
request.context,
|
||||||
resource_item['network_id'],
|
resource_item['network_id'])
|
||||||
)['tenant_id']
|
# do not perform the check on shared networks
|
||||||
|
if network.get('shared'):
|
||||||
|
return
|
||||||
|
|
||||||
|
network_owner = network['tenant_id']
|
||||||
|
|
||||||
if network_owner != resource_item['tenant_id']:
|
if network_owner != resource_item['tenant_id']:
|
||||||
msg = _("Tenant %(tenant_id)s not allowed to "
|
msg = _("Tenant %(tenant_id)s not allowed to "
|
||||||
|
@ -195,3 +195,8 @@ class OverQuota(QuantumException):
|
|||||||
class InvalidQuotaValue(QuantumException):
|
class InvalidQuotaValue(QuantumException):
|
||||||
message = _("Change would make usage less than 0 for the following "
|
message = _("Change would make usage less than 0 for the following "
|
||||||
"resources: %(unders)s")
|
"resources: %(unders)s")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSharedSetting(QuantumException):
|
||||||
|
message = _("Unable to reconfigure sharing settings for network"
|
||||||
|
"%(network). Multiple tenants are using it")
|
||||||
|
@ -65,9 +65,13 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
|
|||||||
query = context.session.query(model)
|
query = context.session.query(model)
|
||||||
|
|
||||||
# NOTE(jkoelker) non-admin queries are scoped to their tenant_id
|
# NOTE(jkoelker) non-admin queries are scoped to their tenant_id
|
||||||
|
# NOTE(salvatore-orlando): unless the model allows for shared objects
|
||||||
if not context.is_admin and hasattr(model, 'tenant_id'):
|
if not context.is_admin and hasattr(model, 'tenant_id'):
|
||||||
|
if hasattr(model, 'shared'):
|
||||||
|
query = query.filter((model.tenant_id == context.tenant_id) |
|
||||||
|
(model.shared))
|
||||||
|
else:
|
||||||
query = query.filter(model.tenant_id == context.tenant_id)
|
query = query.filter(model.tenant_id == context.tenant_id)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def _get_by_id(self, context, model, id, joins=(), verbose=None):
|
def _get_by_id(self, context, model, id, joins=(), verbose=None):
|
||||||
@ -610,12 +614,32 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
|
|||||||
subnet['cidr'])
|
subnet['cidr'])
|
||||||
return pools
|
return pools
|
||||||
|
|
||||||
|
def _validate_shared_update(self, context, id, original, updated):
|
||||||
|
# The only case that needs to be validated is when 'shared'
|
||||||
|
# goes from True to False
|
||||||
|
if updated['shared'] == original.shared or updated['shared']:
|
||||||
|
return
|
||||||
|
ports = self._model_query(
|
||||||
|
context, models_v2.Port).filter(
|
||||||
|
models_v2.Port.network_id == id).all()
|
||||||
|
subnets = self._model_query(
|
||||||
|
context, models_v2.Subnet).filter(
|
||||||
|
models_v2.Subnet.network_id == id).all()
|
||||||
|
tenant_ids = set([port['tenant_id'] for port in ports] +
|
||||||
|
[subnet['tenant_id'] for subnet in subnets])
|
||||||
|
# raise if multiple tenants found or if the only tenant found
|
||||||
|
# is not the owner of the network
|
||||||
|
if (len(tenant_ids) > 1 or len(tenant_ids) == 1 and
|
||||||
|
tenant_ids.pop() != original.tenant_id):
|
||||||
|
raise q_exc.InvalidSharedSetting(network=original.name)
|
||||||
|
|
||||||
def _make_network_dict(self, network, fields=None):
|
def _make_network_dict(self, network, fields=None):
|
||||||
res = {'id': network['id'],
|
res = {'id': network['id'],
|
||||||
'name': network['name'],
|
'name': network['name'],
|
||||||
'tenant_id': network['tenant_id'],
|
'tenant_id': network['tenant_id'],
|
||||||
'admin_state_up': network['admin_state_up'],
|
'admin_state_up': network['admin_state_up'],
|
||||||
'status': network['status'],
|
'status': network['status'],
|
||||||
|
'shared': network['shared'],
|
||||||
'subnets': [subnet['id']
|
'subnets': [subnet['id']
|
||||||
for subnet in network['subnets']]}
|
for subnet in network['subnets']]}
|
||||||
|
|
||||||
@ -659,6 +683,7 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
|
|||||||
id=n.get('id') or utils.str_uuid(),
|
id=n.get('id') or utils.str_uuid(),
|
||||||
name=n['name'],
|
name=n['name'],
|
||||||
admin_state_up=n['admin_state_up'],
|
admin_state_up=n['admin_state_up'],
|
||||||
|
shared=n['shared'],
|
||||||
status="ACTIVE")
|
status="ACTIVE")
|
||||||
context.session.add(network)
|
context.session.add(network)
|
||||||
return self._make_network_dict(network)
|
return self._make_network_dict(network)
|
||||||
@ -667,6 +692,9 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
|
|||||||
n = network['network']
|
n = network['network']
|
||||||
with context.session.begin():
|
with context.session.begin():
|
||||||
network = self._get_network(context, id)
|
network = self._get_network(context, id)
|
||||||
|
# validate 'shared' parameter
|
||||||
|
if 'shared' in n:
|
||||||
|
self._validate_shared_update(context, id, network, n)
|
||||||
network.update(n)
|
network.update(n)
|
||||||
return self._make_network_dict(network)
|
return self._make_network_dict(network)
|
||||||
|
|
||||||
|
@ -126,3 +126,4 @@ class Network(model_base.BASEV2, HasId, HasTenant):
|
|||||||
subnets = orm.relationship(Subnet, backref='networks')
|
subnets = orm.relationship(Subnet, backref='networks')
|
||||||
status = sa.Column(sa.String(16))
|
status = sa.Column(sa.String(16))
|
||||||
admin_state_up = sa.Column(sa.Boolean)
|
admin_state_up = sa.Column(sa.Boolean)
|
||||||
|
shared = sa.Column(sa.Boolean)
|
||||||
|
@ -18,9 +18,7 @@
|
|||||||
"""
|
"""
|
||||||
Policy engine for quantum. Largely copied from nova.
|
Policy engine for quantum. Largely copied from nova.
|
||||||
"""
|
"""
|
||||||
|
from quantum.api.v2 import attributes
|
||||||
import os.path
|
|
||||||
|
|
||||||
from quantum.common import exceptions
|
from quantum.common import exceptions
|
||||||
from quantum.openstack.common import cfg
|
from quantum.openstack.common import cfg
|
||||||
import quantum.common.utils as utils
|
import quantum.common.utils as utils
|
||||||
@ -56,41 +54,139 @@ def _set_brain(data):
|
|||||||
policy.set_brain(policy.Brain.load_json(data, default_rule))
|
policy.set_brain(policy.Brain.load_json(data, default_rule))
|
||||||
|
|
||||||
|
|
||||||
def check(context, action, target):
|
def _get_resource_and_action(action):
|
||||||
|
data = action.split(':', 1)[0].split('_', 1)
|
||||||
|
return ("%ss" % data[-1], data[0] != 'get')
|
||||||
|
|
||||||
|
|
||||||
|
def _is_attribute_explicitly_set(attribute_name, resource, target):
|
||||||
|
"""Verify that an attribute is present and has a non-default value"""
|
||||||
|
if ('default' in resource[attribute_name] and
|
||||||
|
target.get(attribute_name, attributes.ATTR_NOT_SPECIFIED) !=
|
||||||
|
attributes.ATTR_NOT_SPECIFIED):
|
||||||
|
if (target[attribute_name] != resource[attribute_name]['default']):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _build_target(action, original_target, plugin, context):
|
||||||
|
"""Augment dictionary of target attributes for policy engine.
|
||||||
|
|
||||||
|
This routine adds to the dictionary attributes belonging to the
|
||||||
|
"parent" resource of the targeted one.
|
||||||
|
"""
|
||||||
|
target = original_target.copy()
|
||||||
|
resource, _w = _get_resource_and_action(action)
|
||||||
|
hierarchy_info = attributes.RESOURCE_HIERARCHY_MAP.get(resource, None)
|
||||||
|
if hierarchy_info and plugin:
|
||||||
|
# use the 'singular' version of the resource name
|
||||||
|
parent_resource = hierarchy_info['parent'][:-1]
|
||||||
|
parent_id = hierarchy_info['identified_by']
|
||||||
|
f = getattr(plugin, 'get_%s' % parent_resource)
|
||||||
|
# f *must* exist, if not found it is better to let quantum explode
|
||||||
|
# Note: we do not use admin context
|
||||||
|
data = f(context, target[parent_id], fields=['tenant_id'])
|
||||||
|
target['%s_tenant_id' % parent_resource] = data['tenant_id']
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def _create_access_rule_match(resource, is_write, shared):
|
||||||
|
if shared == resource[attributes.SHARED]:
|
||||||
|
return ('rule:%s:%s:%s' % (resource,
|
||||||
|
shared and 'shared' or 'private',
|
||||||
|
is_write and 'write' or 'read'), )
|
||||||
|
|
||||||
|
|
||||||
|
def _build_perm_match(action, target):
|
||||||
|
"""Create the permission rule match.
|
||||||
|
|
||||||
|
Given the current access right on a network (shared/private), and
|
||||||
|
the type of the current operation (read/write), builds a match
|
||||||
|
rule of the type <resource>:<sharing_mode>:<operation_type>
|
||||||
|
"""
|
||||||
|
resource, is_write = _get_resource_and_action(action)
|
||||||
|
res_map = attributes.RESOURCE_ATTRIBUTE_MAP
|
||||||
|
if (resource in res_map and
|
||||||
|
attributes.SHARED in res_map[resource] and
|
||||||
|
attributes.SHARED in target):
|
||||||
|
return ('rule:%s:%s:%s' % (resource,
|
||||||
|
target[attributes.SHARED]
|
||||||
|
and 'shared' or 'private',
|
||||||
|
is_write and 'write' or 'read'), )
|
||||||
|
|
||||||
|
|
||||||
|
def _build_match_list(action, target):
|
||||||
|
"""Create the list of rules to match for a given action.
|
||||||
|
|
||||||
|
The list of policy rules to be matched is built in the following way:
|
||||||
|
1) add entries for matching permission on objects
|
||||||
|
2) add an entry for the specific action (e.g.: create_network)
|
||||||
|
3) add an entry for attributes of a resource for which the action
|
||||||
|
is being executed (e.g.: create_network:shared)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
match_list = ('rule:%s' % action,)
|
||||||
|
resource, is_write = _get_resource_and_action(action)
|
||||||
|
# assigning to variable with short name for improving readability
|
||||||
|
res_map = attributes.RESOURCE_ATTRIBUTE_MAP
|
||||||
|
if resource in res_map:
|
||||||
|
for attribute_name in res_map[resource]:
|
||||||
|
if _is_attribute_explicitly_set(attribute_name,
|
||||||
|
res_map[resource],
|
||||||
|
target):
|
||||||
|
attribute = res_map[resource][attribute_name]
|
||||||
|
if 'enforce_policy' in attribute and is_write:
|
||||||
|
match_list += ('rule:%s:%s' % (action,
|
||||||
|
attribute_name),)
|
||||||
|
# add permission-based rule (for shared resources)
|
||||||
|
perm_match = _build_perm_match(action, target)
|
||||||
|
if perm_match:
|
||||||
|
match_list += perm_match
|
||||||
|
# the policy engine must AND between all the rules
|
||||||
|
return [match_list]
|
||||||
|
|
||||||
|
|
||||||
|
def check(context, action, target, plugin=None):
|
||||||
"""Verifies that the action is valid on the target in this context.
|
"""Verifies that the action is valid on the target in this context.
|
||||||
|
|
||||||
:param context: quantum context
|
:param context: quantum context
|
||||||
:param action: string representing the action to be checked
|
:param action: string representing the action to be checked
|
||||||
this should be colon separated for clarity.
|
this should be colon separated for clarity.
|
||||||
:param object: dictionary representing the object of the action
|
:param target: dictionary representing the object of the action
|
||||||
for object creation this should be a dictionary representing the
|
for object creation this should be a dictionary representing the
|
||||||
location of the object e.g. ``{'project_id': context.project_id}``
|
location of the object e.g. ``{'project_id': context.project_id}``
|
||||||
|
:param plugin: quantum plugin used to retrieve information required
|
||||||
|
for augmenting the target
|
||||||
|
|
||||||
:return: Returns True if access is permitted else False.
|
:return: Returns True if access is permitted else False.
|
||||||
"""
|
"""
|
||||||
init()
|
init()
|
||||||
match_list = ('rule:%s' % action,)
|
real_target = _build_target(action, target, plugin, context)
|
||||||
|
match_list = _build_match_list(action, real_target)
|
||||||
credentials = context.to_dict()
|
credentials = context.to_dict()
|
||||||
return policy.enforce(match_list, target, credentials)
|
return policy.enforce(match_list, real_target, credentials)
|
||||||
|
|
||||||
|
|
||||||
def enforce(context, action, target):
|
def enforce(context, action, target, plugin=None):
|
||||||
"""Verifies that the action is valid on the target in this context.
|
"""Verifies that the action is valid on the target in this context.
|
||||||
|
|
||||||
:param context: quantum context
|
:param context: quantum context
|
||||||
:param action: string representing the action to be checked
|
:param action: string representing the action to be checked
|
||||||
this should be colon separated for clarity.
|
this should be colon separated for clarity.
|
||||||
:param object: dictionary representing the object of the action
|
:param target: dictionary representing the object of the action
|
||||||
for object creation this should be a dictionary representing the
|
for object creation this should be a dictionary representing the
|
||||||
location of the object e.g. ``{'project_id': context.project_id}``
|
location of the object e.g. ``{'project_id': context.project_id}``
|
||||||
|
:param plugin: quantum plugin used to retrieve information required
|
||||||
|
for augmenting the target
|
||||||
|
|
||||||
:raises quantum.exceptions.PolicyNotAllowed: if verification fails.
|
:raises quantum.exceptions.PolicyNotAllowed: if verification fails.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
init()
|
init()
|
||||||
|
|
||||||
match_list = ('rule:%s' % action,)
|
real_target = _build_target(action, target, plugin, context)
|
||||||
|
match_list = _build_match_list(action, real_target)
|
||||||
credentials = context.to_dict()
|
credentials = context.to_dict()
|
||||||
|
policy.enforce(match_list, real_target, credentials,
|
||||||
policy.enforce(match_list, target, credentials,
|
|
||||||
exceptions.PolicyNotAuthorized, action=action)
|
exceptions.PolicyNotAuthorized, action=action)
|
||||||
|
@ -233,37 +233,45 @@ class APIv2TestCase(APIv2TestBase):
|
|||||||
fields=mock.ANY,
|
fields=mock.ANY,
|
||||||
verbose=True)
|
verbose=True)
|
||||||
|
|
||||||
|
def _do_field_list(self, resource, base_fields):
|
||||||
|
attr_info = attributes.RESOURCE_ATTRIBUTE_MAP[resource]
|
||||||
|
policy_attrs = [name for (name, info) in attr_info.items()
|
||||||
|
if info.get('required_by_policy')]
|
||||||
|
fields = base_fields
|
||||||
|
fields.extend(policy_attrs)
|
||||||
|
return fields
|
||||||
|
|
||||||
def test_fields(self):
|
def test_fields(self):
|
||||||
instance = self.plugin.return_value
|
instance = self.plugin.return_value
|
||||||
instance.get_networks.return_value = []
|
instance.get_networks.return_value = []
|
||||||
|
|
||||||
self.api.get(_get_path('networks'), {'fields': 'foo'})
|
self.api.get(_get_path('networks'), {'fields': 'foo'})
|
||||||
|
fields = self._do_field_list('networks', ['foo'])
|
||||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||||
filters=mock.ANY,
|
filters=mock.ANY,
|
||||||
fields=['foo',
|
fields=fields,
|
||||||
'tenant_id'],
|
|
||||||
verbose=mock.ANY)
|
verbose=mock.ANY)
|
||||||
|
|
||||||
def test_fields_multiple(self):
|
def test_fields_multiple(self):
|
||||||
instance = self.plugin.return_value
|
instance = self.plugin.return_value
|
||||||
instance.get_networks.return_value = []
|
instance.get_networks.return_value = []
|
||||||
|
|
||||||
|
fields = self._do_field_list('networks', ['foo', 'bar'])
|
||||||
self.api.get(_get_path('networks'), {'fields': ['foo', 'bar']})
|
self.api.get(_get_path('networks'), {'fields': ['foo', 'bar']})
|
||||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||||
filters=mock.ANY,
|
filters=mock.ANY,
|
||||||
fields=['foo', 'bar',
|
fields=fields,
|
||||||
'tenant_id'],
|
|
||||||
verbose=mock.ANY)
|
verbose=mock.ANY)
|
||||||
|
|
||||||
def test_fields_multiple_with_empty(self):
|
def test_fields_multiple_with_empty(self):
|
||||||
instance = self.plugin.return_value
|
instance = self.plugin.return_value
|
||||||
instance.get_networks.return_value = []
|
instance.get_networks.return_value = []
|
||||||
|
|
||||||
|
fields = self._do_field_list('networks', ['foo'])
|
||||||
self.api.get(_get_path('networks'), {'fields': ['foo', '']})
|
self.api.get(_get_path('networks'), {'fields': ['foo', '']})
|
||||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||||
filters=mock.ANY,
|
filters=mock.ANY,
|
||||||
fields=['foo',
|
fields=fields,
|
||||||
'tenant_id'],
|
|
||||||
verbose=mock.ANY)
|
verbose=mock.ANY)
|
||||||
|
|
||||||
def test_fields_empty(self):
|
def test_fields_empty(self):
|
||||||
@ -359,10 +367,10 @@ class APIv2TestCase(APIv2TestBase):
|
|||||||
|
|
||||||
self.api.get(_get_path('networks'), {'foo': 'bar', 'fields': 'foo'})
|
self.api.get(_get_path('networks'), {'foo': 'bar', 'fields': 'foo'})
|
||||||
filters = {'foo': ['bar']}
|
filters = {'foo': ['bar']}
|
||||||
|
fields = self._do_field_list('networks', ['foo'])
|
||||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
fields=['foo',
|
fields=fields,
|
||||||
'tenant_id'],
|
|
||||||
verbose=mock.ANY)
|
verbose=mock.ANY)
|
||||||
|
|
||||||
def test_filters_with_verbose(self):
|
def test_filters_with_verbose(self):
|
||||||
@ -385,10 +393,10 @@ class APIv2TestCase(APIv2TestBase):
|
|||||||
'fields': 'foo',
|
'fields': 'foo',
|
||||||
'verbose': 'true'})
|
'verbose': 'true'})
|
||||||
filters = {'foo': ['bar']}
|
filters = {'foo': ['bar']}
|
||||||
|
fields = self._do_field_list('networks', ['foo'])
|
||||||
instance.get_networks.assert_called_once_with(mock.ANY,
|
instance.get_networks.assert_called_once_with(mock.ANY,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
fields=['foo',
|
fields=fields,
|
||||||
'tenant_id'],
|
|
||||||
verbose=True)
|
verbose=True)
|
||||||
|
|
||||||
|
|
||||||
@ -405,6 +413,7 @@ class JSONV2TestCase(APIv2TestBase):
|
|||||||
'admin_state_up': True,
|
'admin_state_up': True,
|
||||||
'status': "ACTIVE",
|
'status': "ACTIVE",
|
||||||
'tenant_id': real_tenant_id,
|
'tenant_id': real_tenant_id,
|
||||||
|
'shared': False,
|
||||||
'subnets': []}
|
'subnets': []}
|
||||||
return_value = [input_dict]
|
return_value = [input_dict]
|
||||||
instance = self.plugin.return_value
|
instance = self.plugin.return_value
|
||||||
@ -416,6 +425,7 @@ class JSONV2TestCase(APIv2TestBase):
|
|||||||
# expect full list returned
|
# expect full list returned
|
||||||
self.assertEqual(len(res.json['networks']), 1)
|
self.assertEqual(len(res.json['networks']), 1)
|
||||||
output_dict = res.json['networks'][0]
|
output_dict = res.json['networks'][0]
|
||||||
|
input_dict['shared'] = False
|
||||||
self.assertEqual(len(input_dict), len(output_dict))
|
self.assertEqual(len(input_dict), len(output_dict))
|
||||||
for k, v in input_dict.iteritems():
|
for k, v in input_dict.iteritems():
|
||||||
self.assertEqual(v, output_dict[k])
|
self.assertEqual(v, output_dict[k])
|
||||||
@ -456,7 +466,9 @@ class JSONV2TestCase(APIv2TestBase):
|
|||||||
def test_create_use_defaults(self):
|
def test_create_use_defaults(self):
|
||||||
net_id = _uuid()
|
net_id = _uuid()
|
||||||
initial_input = {'network': {'name': 'net1', 'tenant_id': _uuid()}}
|
initial_input = {'network': {'name': 'net1', 'tenant_id': _uuid()}}
|
||||||
full_input = {'network': {'admin_state_up': True, 'subnets': []}}
|
full_input = {'network': {'admin_state_up': True,
|
||||||
|
'shared': False,
|
||||||
|
'subnets': []}}
|
||||||
full_input['network'].update(initial_input['network'])
|
full_input['network'].update(initial_input['network'])
|
||||||
|
|
||||||
return_value = {'id': net_id, 'status': "ACTIVE"}
|
return_value = {'id': net_id, 'status': "ACTIVE"}
|
||||||
@ -489,7 +501,7 @@ class JSONV2TestCase(APIv2TestBase):
|
|||||||
# tenant_id should be fetched from env
|
# tenant_id should be fetched from env
|
||||||
initial_input = {'network': {'name': 'net1'}}
|
initial_input = {'network': {'name': 'net1'}}
|
||||||
full_input = {'network': {'admin_state_up': True, 'subnets': [],
|
full_input = {'network': {'admin_state_up': True, 'subnets': [],
|
||||||
'tenant_id': tenant_id}}
|
'shared': False, 'tenant_id': tenant_id}}
|
||||||
full_input['network'].update(initial_input['network'])
|
full_input['network'].update(initial_input['network'])
|
||||||
|
|
||||||
return_value = {'id': net_id, 'status': "ACTIVE"}
|
return_value = {'id': net_id, 'status': "ACTIVE"}
|
||||||
@ -643,7 +655,8 @@ class JSONV2TestCase(APIv2TestBase):
|
|||||||
if req_tenant_id:
|
if req_tenant_id:
|
||||||
env = {'quantum.context': context.Context('', req_tenant_id)}
|
env = {'quantum.context': context.Context('', req_tenant_id)}
|
||||||
instance = self.plugin.return_value
|
instance = self.plugin.return_value
|
||||||
instance.get_network.return_value = {'tenant_id': real_tenant_id}
|
instance.get_network.return_value = {'tenant_id': real_tenant_id,
|
||||||
|
'shared': False}
|
||||||
instance.delete_network.return_value = None
|
instance.delete_network.return_value = None
|
||||||
|
|
||||||
res = self.api.delete(_get_path('networks', id=str(uuid.uuid4())),
|
res = self.api.delete(_get_path('networks', id=str(uuid.uuid4())),
|
||||||
@ -665,9 +678,14 @@ class JSONV2TestCase(APIv2TestBase):
|
|||||||
def _test_get(self, req_tenant_id, real_tenant_id, expected_code,
|
def _test_get(self, req_tenant_id, real_tenant_id, expected_code,
|
||||||
expect_errors=False):
|
expect_errors=False):
|
||||||
env = {}
|
env = {}
|
||||||
|
shared = False
|
||||||
if req_tenant_id:
|
if req_tenant_id:
|
||||||
env = {'quantum.context': context.Context('', req_tenant_id)}
|
env = {'quantum.context': context.Context('', req_tenant_id)}
|
||||||
data = {'tenant_id': real_tenant_id}
|
if req_tenant_id.endswith('another'):
|
||||||
|
shared = True
|
||||||
|
env['quantum.context'].roles = ['tenant_admin']
|
||||||
|
|
||||||
|
data = {'tenant_id': real_tenant_id, 'shared': shared}
|
||||||
instance = self.plugin.return_value
|
instance = self.plugin.return_value
|
||||||
instance.get_network.return_value = data
|
instance.get_network.return_value = data
|
||||||
|
|
||||||
@ -689,6 +707,10 @@ class JSONV2TestCase(APIv2TestBase):
|
|||||||
self._test_get(tenant_id + "bad", tenant_id,
|
self._test_get(tenant_id + "bad", tenant_id,
|
||||||
exc.HTTPNotFound.code, expect_errors=True)
|
exc.HTTPNotFound.code, expect_errors=True)
|
||||||
|
|
||||||
|
def test_get_keystone_shared_network(self):
|
||||||
|
tenant_id = _uuid()
|
||||||
|
self._test_get(tenant_id + "another", tenant_id, 200)
|
||||||
|
|
||||||
def _test_update(self, req_tenant_id, real_tenant_id, expected_code,
|
def _test_update(self, req_tenant_id, real_tenant_id, expected_code,
|
||||||
expect_errors=False):
|
expect_errors=False):
|
||||||
env = {}
|
env = {}
|
||||||
@ -700,7 +722,8 @@ class JSONV2TestCase(APIv2TestBase):
|
|||||||
return_value.update(data['network'].copy())
|
return_value.update(data['network'].copy())
|
||||||
|
|
||||||
instance = self.plugin.return_value
|
instance = self.plugin.return_value
|
||||||
instance.get_network.return_value = {'tenant_id': real_tenant_id}
|
instance.get_network.return_value = {'tenant_id': real_tenant_id,
|
||||||
|
'shared': False}
|
||||||
instance.update_network.return_value = return_value
|
instance.update_network.return_value = return_value
|
||||||
|
|
||||||
res = self.api.put_json(_get_path('networks',
|
res = self.api.put_json(_get_path('networks',
|
||||||
@ -887,9 +910,12 @@ class ExtensionTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
def test_extended_create(self):
|
def test_extended_create(self):
|
||||||
net_id = _uuid()
|
net_id = _uuid()
|
||||||
data = {'network': {'name': 'net1', 'admin_state_up': True,
|
initial_input = {'network': {'name': 'net1', 'tenant_id': _uuid(),
|
||||||
'tenant_id': _uuid(), 'subnets': [],
|
|
||||||
'v2attrs:something_else': "abc"}}
|
'v2attrs:something_else': "abc"}}
|
||||||
|
data = {'network': {'admin_state_up': True, 'subnets': [],
|
||||||
|
'shared': False}}
|
||||||
|
data['network'].update(initial_input['network'])
|
||||||
|
|
||||||
return_value = {'subnets': [], 'status': "ACTIVE",
|
return_value = {'subnets': [], 'status': "ACTIVE",
|
||||||
'id': net_id,
|
'id': net_id,
|
||||||
'v2attrs:something': "123"}
|
'v2attrs:something': "123"}
|
||||||
@ -898,7 +924,7 @@ class ExtensionTestCase(unittest.TestCase):
|
|||||||
instance = self.plugin.return_value
|
instance = self.plugin.return_value
|
||||||
instance.create_network.return_value = return_value
|
instance.create_network.return_value = return_value
|
||||||
|
|
||||||
res = self.api.post_json(_get_path('networks'), data)
|
res = self.api.post_json(_get_path('networks'), initial_input)
|
||||||
|
|
||||||
instance.create_network.assert_called_with(mock.ANY,
|
instance.create_network.assert_called_with(mock.ANY,
|
||||||
network=data)
|
network=data)
|
||||||
|
@ -122,56 +122,87 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase):
|
|||||||
data = {'network': {'name': name,
|
data = {'network': {'name': name,
|
||||||
'admin_state_up': admin_status_up,
|
'admin_state_up': admin_status_up,
|
||||||
'tenant_id': self._tenant_id}}
|
'tenant_id': self._tenant_id}}
|
||||||
for arg in ('admin_state_up', 'tenant_id', 'public'):
|
for arg in ('admin_state_up', 'tenant_id', 'shared'):
|
||||||
# Arg must be present and not empty
|
# Arg must be present and not empty
|
||||||
if arg in kwargs and kwargs[arg]:
|
if arg in kwargs and kwargs[arg]:
|
||||||
data['network'][arg] = kwargs[arg]
|
data['network'][arg] = kwargs[arg]
|
||||||
network_req = self.new_create_request('networks', data, fmt)
|
network_req = self.new_create_request('networks', data, fmt)
|
||||||
if ('set_context' in kwargs and
|
if (kwargs.get('set_context') and 'tenant_id' in kwargs):
|
||||||
kwargs['set_context'] is True and
|
|
||||||
'tenant_id' in kwargs):
|
|
||||||
# create a specific auth context for this request
|
# create a specific auth context for this request
|
||||||
network_req.environ['quantum.context'] = context.Context(
|
network_req.environ['quantum.context'] = context.Context(
|
||||||
'', kwargs['tenant_id'])
|
'', kwargs['tenant_id'])
|
||||||
|
|
||||||
return network_req.get_response(self.api)
|
return network_req.get_response(self.api)
|
||||||
|
|
||||||
def _create_subnet(self, fmt, tenant_id, net_id, gateway_ip, cidr,
|
def _create_subnet(self, fmt, net_id, cidr,
|
||||||
allocation_pools=None, ip_version=4, enable_dhcp=True):
|
|
||||||
data = {'subnet': {'tenant_id': tenant_id,
|
|
||||||
'network_id': net_id,
|
|
||||||
'cidr': cidr,
|
|
||||||
'ip_version': ip_version,
|
|
||||||
'tenant_id': self._tenant_id,
|
|
||||||
'enable_dhcp': enable_dhcp}}
|
|
||||||
if gateway_ip:
|
|
||||||
data['subnet']['gateway_ip'] = gateway_ip
|
|
||||||
if allocation_pools:
|
|
||||||
data['subnet']['allocation_pools'] = allocation_pools
|
|
||||||
subnet_req = self.new_create_request('subnets', data, fmt)
|
|
||||||
return subnet_req.get_response(self.api)
|
|
||||||
|
|
||||||
def _create_port(self, fmt, net_id, custom_req_body=None,
|
|
||||||
expected_res_status=None, **kwargs):
|
expected_res_status=None, **kwargs):
|
||||||
|
data = {'subnet': {'network_id': net_id,
|
||||||
|
'cidr': cidr,
|
||||||
|
'ip_version': 4,
|
||||||
|
'tenant_id': self._tenant_id}}
|
||||||
|
for arg in ('gateway_ip', 'allocation_pools',
|
||||||
|
'ip_version', 'tenant_id',
|
||||||
|
'enable_dhcp'):
|
||||||
|
# Arg must be present and not null (but can be false)
|
||||||
|
if arg in kwargs and kwargs[arg] is not None:
|
||||||
|
data['subnet'][arg] = kwargs[arg]
|
||||||
|
subnet_req = self.new_create_request('subnets', data, fmt)
|
||||||
|
if (kwargs.get('set_context') and 'tenant_id' in kwargs):
|
||||||
|
# create a specific auth context for this request
|
||||||
|
subnet_req.environ['quantum.context'] = context.Context(
|
||||||
|
'', kwargs['tenant_id'])
|
||||||
|
|
||||||
|
subnet_res = subnet_req.get_response(self.api)
|
||||||
|
if expected_res_status:
|
||||||
|
self.assertEqual(subnet_res.status_int, expected_res_status)
|
||||||
|
return subnet_res
|
||||||
|
|
||||||
|
def _create_port(self, fmt, net_id, expected_res_status=None, **kwargs):
|
||||||
content_type = 'application/' + fmt
|
content_type = 'application/' + fmt
|
||||||
data = {'port': {'network_id': net_id,
|
data = {'port': {'network_id': net_id,
|
||||||
'tenant_id': self._tenant_id}}
|
'tenant_id': self._tenant_id}}
|
||||||
for arg in ('admin_state_up', 'device_id', 'mac_address',
|
for arg in ('admin_state_up', 'device_id',
|
||||||
'name', 'fixed_ips'):
|
'mac_address', 'fixed_ips',
|
||||||
|
'name', 'tenant_id'):
|
||||||
# Arg must be present and not empty
|
# Arg must be present and not empty
|
||||||
if arg in kwargs and kwargs[arg]:
|
if arg in kwargs and kwargs[arg]:
|
||||||
data['port'][arg] = kwargs[arg]
|
data['port'][arg] = kwargs[arg]
|
||||||
|
|
||||||
port_req = self.new_create_request('ports', data, fmt)
|
port_req = self.new_create_request('ports', data, fmt)
|
||||||
return port_req.get_response(self.api)
|
if (kwargs.get('set_context') and 'tenant_id' in kwargs):
|
||||||
|
# create a specific auth context for this request
|
||||||
|
port_req.environ['quantum.context'] = context.Context(
|
||||||
|
'', kwargs['tenant_id'])
|
||||||
|
|
||||||
|
port_res = port_req.get_response(self.api)
|
||||||
|
if expected_res_status:
|
||||||
|
self.assertEqual(port_res.status_int, expected_res_status)
|
||||||
|
return port_res
|
||||||
|
|
||||||
|
def _list_ports(self, fmt, expected_res_status=None,
|
||||||
|
net_id=None, **kwargs):
|
||||||
|
query_params = None
|
||||||
|
if net_id:
|
||||||
|
query_params = "network_id=%s" % net_id
|
||||||
|
port_req = self.new_list_request('ports', fmt, query_params)
|
||||||
|
if ('set_context' in kwargs and
|
||||||
|
kwargs['set_context'] is True and
|
||||||
|
'tenant_id' in kwargs):
|
||||||
|
# create a specific auth context for this request
|
||||||
|
port_req.environ['quantum.context'] = context.Context(
|
||||||
|
'', kwargs['tenant_id'])
|
||||||
|
|
||||||
|
port_res = port_req.get_response(self.api)
|
||||||
|
if expected_res_status:
|
||||||
|
self.assertEqual(port_res.status_int, expected_res_status)
|
||||||
|
return port_res
|
||||||
|
|
||||||
def _make_subnet(self, fmt, network, gateway, cidr,
|
def _make_subnet(self, fmt, network, gateway, cidr,
|
||||||
allocation_pools=None, ip_version=4, enable_dhcp=True):
|
allocation_pools=None, ip_version=4, enable_dhcp=True):
|
||||||
res = self._create_subnet(fmt,
|
res = self._create_subnet(fmt,
|
||||||
network['network']['tenant_id'],
|
net_id=network['network']['id'],
|
||||||
network['network']['id'],
|
cidr=cidr,
|
||||||
gateway,
|
gateway_ip=gateway,
|
||||||
cidr,
|
tenant_id=network['network']['tenant_id'],
|
||||||
allocation_pools=allocation_pools,
|
allocation_pools=allocation_pools,
|
||||||
ip_version=ip_version,
|
ip_version=ip_version,
|
||||||
enable_dhcp=enable_dhcp)
|
enable_dhcp=enable_dhcp)
|
||||||
@ -190,9 +221,21 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase):
|
|||||||
req.get_response(self.api)
|
req.get_response(self.api)
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def network(self, name='net1', admin_status_up=True, fmt='json'):
|
def network(self, name='net1',
|
||||||
res = self._create_network(fmt, name, admin_status_up)
|
admin_status_up=True,
|
||||||
|
fmt='json',
|
||||||
|
**kwargs):
|
||||||
|
res = self._create_network(fmt,
|
||||||
|
name,
|
||||||
|
admin_status_up,
|
||||||
|
**kwargs)
|
||||||
network = self.deserialize(fmt, res)
|
network = self.deserialize(fmt, res)
|
||||||
|
# TODO(salvatore-orlando): do exception handling in this test module
|
||||||
|
# in a uniform way (we do it differently for ports, subnets, and nets
|
||||||
|
# Things can go wrong - raise HTTP exc with res code only
|
||||||
|
# so it can be caught by unit tests
|
||||||
|
if res.status_int >= 400:
|
||||||
|
raise webob.exc.HTTPClientError(code=res.status_int)
|
||||||
yield network
|
yield network
|
||||||
self._delete('networks', network['network']['id'])
|
self._delete('networks', network['network']['id'])
|
||||||
|
|
||||||
@ -373,6 +416,19 @@ class TestPortsV2(QuantumDbPluginV2TestCase):
|
|||||||
res = port_req.get_response(self.api)
|
res = port_req.get_response(self.api)
|
||||||
self.assertEquals(res.status_int, 403)
|
self.assertEquals(res.status_int, 403)
|
||||||
|
|
||||||
|
def test_create_port_public_network(self):
|
||||||
|
keys = [('admin_state_up', True), ('status', 'ACTIVE')]
|
||||||
|
with self.network(shared=True) as network:
|
||||||
|
port_res = self._create_port('json',
|
||||||
|
network['network']['id'],
|
||||||
|
201,
|
||||||
|
tenant_id='another_tenant',
|
||||||
|
set_context=True)
|
||||||
|
port = self.deserialize('json', port_res)
|
||||||
|
for k, v in keys:
|
||||||
|
self.assertEquals(port['port'][k], v)
|
||||||
|
self.assertTrue('mac_address' in port['port'])
|
||||||
|
|
||||||
def test_list_ports(self):
|
def test_list_ports(self):
|
||||||
with contextlib.nested(self.port(), self.port()) as (port1, port2):
|
with contextlib.nested(self.port(), self.port()) as (port1, port2):
|
||||||
req = self.new_list_request('ports', 'json')
|
req = self.new_list_request('ports', 'json')
|
||||||
@ -382,6 +438,41 @@ class TestPortsV2(QuantumDbPluginV2TestCase):
|
|||||||
self.assertTrue(port1['port']['id'] in ids)
|
self.assertTrue(port1['port']['id'] in ids)
|
||||||
self.assertTrue(port2['port']['id'] in ids)
|
self.assertTrue(port2['port']['id'] in ids)
|
||||||
|
|
||||||
|
def test_list_ports_public_network(self):
|
||||||
|
with self.network(shared=True) as network:
|
||||||
|
portres_1 = self._create_port('json',
|
||||||
|
network['network']['id'],
|
||||||
|
201,
|
||||||
|
tenant_id='tenant_1',
|
||||||
|
set_context=True)
|
||||||
|
portres_2 = self._create_port('json',
|
||||||
|
network['network']['id'],
|
||||||
|
201,
|
||||||
|
tenant_id='tenant_2',
|
||||||
|
set_context=True)
|
||||||
|
port1 = self.deserialize('json', portres_1)
|
||||||
|
port2 = self.deserialize('json', portres_2)
|
||||||
|
|
||||||
|
def _list_and_test_ports(expected_len, ports, tenant_id=None):
|
||||||
|
set_context = tenant_id is not None
|
||||||
|
port_res = self._list_ports('json',
|
||||||
|
200,
|
||||||
|
network['network']['id'],
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
set_context=set_context)
|
||||||
|
port_list = self.deserialize('json', port_res)
|
||||||
|
self.assertEqual(len(port_list['ports']), expected_len)
|
||||||
|
ids = [p['id'] for p in port_list['ports']]
|
||||||
|
for port in ports:
|
||||||
|
self.assertIn(port['port']['id'], ids)
|
||||||
|
|
||||||
|
# Admin request - must return both ports
|
||||||
|
_list_and_test_ports(2, [port1, port2])
|
||||||
|
# Tenant_1 request - must return single port
|
||||||
|
_list_and_test_ports(1, [port1], tenant_id='tenant_1')
|
||||||
|
# Tenant_2 request - must return single port
|
||||||
|
_list_and_test_ports(1, [port2], tenant_id='tenant_2')
|
||||||
|
|
||||||
def test_show_port(self):
|
def test_show_port(self):
|
||||||
with self.port() as port:
|
with self.port() as port:
|
||||||
req = self.new_show_request('ports', port['port']['id'], 'json')
|
req = self.new_show_request('ports', port['port']['id'], 'json')
|
||||||
@ -396,6 +487,20 @@ class TestPortsV2(QuantumDbPluginV2TestCase):
|
|||||||
res = req.get_response(self.api)
|
res = req.get_response(self.api)
|
||||||
self.assertEquals(res.status_int, 404)
|
self.assertEquals(res.status_int, 404)
|
||||||
|
|
||||||
|
def test_delete_port_public_network(self):
|
||||||
|
with self.network(shared=True) as network:
|
||||||
|
port_res = self._create_port('json',
|
||||||
|
network['network']['id'],
|
||||||
|
201,
|
||||||
|
tenant_id='another_tenant',
|
||||||
|
set_context=True)
|
||||||
|
|
||||||
|
port = self.deserialize('json', port_res)
|
||||||
|
port_id = port['port']['id']
|
||||||
|
# delete the port
|
||||||
|
self._delete('ports', port['port']['id'])
|
||||||
|
# Todo: verify!!!
|
||||||
|
|
||||||
def test_update_port(self):
|
def test_update_port(self):
|
||||||
with self.port() as port:
|
with self.port() as port:
|
||||||
data = {'port': {'admin_state_up': False}}
|
data = {'port': {'admin_state_up': False}}
|
||||||
@ -615,9 +720,12 @@ class TestPortsV2(QuantumDbPluginV2TestCase):
|
|||||||
# Get a IPv4 and IPv6 address
|
# Get a IPv4 and IPv6 address
|
||||||
tenant_id = subnet['subnet']['tenant_id']
|
tenant_id = subnet['subnet']['tenant_id']
|
||||||
net_id = subnet['subnet']['network_id']
|
net_id = subnet['subnet']['network_id']
|
||||||
res = self._create_subnet(fmt, tenant_id, net_id=net_id,
|
res = self._create_subnet(fmt,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
net_id=net_id,
|
||||||
cidr='2607:f0d0:1002:51::0/124',
|
cidr='2607:f0d0:1002:51::0/124',
|
||||||
ip_version=6, gateway_ip=None)
|
ip_version=6,
|
||||||
|
gateway_ip=None)
|
||||||
subnet2 = self.deserialize(fmt, res)
|
subnet2 = self.deserialize(fmt, res)
|
||||||
kwargs = {"fixed_ips":
|
kwargs = {"fixed_ips":
|
||||||
[{'subnet_id': subnet['subnet']['id']},
|
[{'subnet_id': subnet['subnet']['id']},
|
||||||
@ -834,11 +942,125 @@ class TestNetworksV2(QuantumDbPluginV2TestCase):
|
|||||||
def test_create_network(self):
|
def test_create_network(self):
|
||||||
name = 'net1'
|
name = 'net1'
|
||||||
keys = [('subnets', []), ('name', name), ('admin_state_up', True),
|
keys = [('subnets', []), ('name', name), ('admin_state_up', True),
|
||||||
('status', 'ACTIVE')]
|
('status', 'ACTIVE'), ('shared', False)]
|
||||||
with self.network(name=name) as net:
|
with self.network(name=name) as net:
|
||||||
for k, v in keys:
|
for k, v in keys:
|
||||||
self.assertEquals(net['network'][k], v)
|
self.assertEquals(net['network'][k], v)
|
||||||
|
|
||||||
|
def test_create_public_network(self):
|
||||||
|
name = 'public_net'
|
||||||
|
keys = [('subnets', []), ('name', name), ('admin_state_up', True),
|
||||||
|
('status', 'ACTIVE'), ('shared', True)]
|
||||||
|
with self.network(name=name, shared=True) as net:
|
||||||
|
for k, v in keys:
|
||||||
|
self.assertEquals(net['network'][k], v)
|
||||||
|
|
||||||
|
def test_create_public_network_no_admin_tenant(self):
|
||||||
|
name = 'public_net'
|
||||||
|
keys = [('subnets', []), ('name', name), ('admin_state_up', True),
|
||||||
|
('status', 'ACTIVE'), ('shared', True)]
|
||||||
|
with self.assertRaises(webob.exc.HTTPClientError) as ctx_manager:
|
||||||
|
with self.network(name=name,
|
||||||
|
shared=True,
|
||||||
|
tenant_id="another_tenant",
|
||||||
|
set_context=True):
|
||||||
|
pass
|
||||||
|
self.assertEquals(ctx_manager.exception.code, 403)
|
||||||
|
|
||||||
|
def test_update_network(self):
|
||||||
|
with self.network() as network:
|
||||||
|
data = {'network': {'name': 'a_brand_new_name'}}
|
||||||
|
req = self.new_update_request('networks',
|
||||||
|
data,
|
||||||
|
network['network']['id'])
|
||||||
|
res = self.deserialize('json', req.get_response(self.api))
|
||||||
|
self.assertEqual(res['network']['name'],
|
||||||
|
data['network']['name'])
|
||||||
|
|
||||||
|
def test_update_shared_network_noadmin_returns_403(self):
|
||||||
|
with self.network(shared=True) as network:
|
||||||
|
data = {'network': {'name': 'a_brand_new_name'}}
|
||||||
|
req = self.new_update_request('networks',
|
||||||
|
data,
|
||||||
|
network['network']['id'])
|
||||||
|
req.environ['quantum.context'] = context.Context('', 'somebody')
|
||||||
|
res = req.get_response(self.api)
|
||||||
|
# The API layer always returns 404 on updates in place of 403
|
||||||
|
self.assertEqual(res.status_int, 404)
|
||||||
|
|
||||||
|
def test_update_network_set_shared(self):
|
||||||
|
with self.network(shared=False) as network:
|
||||||
|
data = {'network': {'shared': True}}
|
||||||
|
req = self.new_update_request('networks',
|
||||||
|
data,
|
||||||
|
network['network']['id'])
|
||||||
|
res = self.deserialize('json', req.get_response(self.api))
|
||||||
|
self.assertTrue(res['network']['shared'])
|
||||||
|
|
||||||
|
def test_update_network_set_not_shared_single_tenant(self):
|
||||||
|
with self.network(shared=True) as network:
|
||||||
|
self._create_port('json',
|
||||||
|
network['network']['id'],
|
||||||
|
201,
|
||||||
|
tenant_id=network['network']['tenant_id'],
|
||||||
|
set_context=True)
|
||||||
|
data = {'network': {'shared': False}}
|
||||||
|
req = self.new_update_request('networks',
|
||||||
|
data,
|
||||||
|
network['network']['id'])
|
||||||
|
res = self.deserialize('json', req.get_response(self.api))
|
||||||
|
self.assertFalse(res['network']['shared'])
|
||||||
|
|
||||||
|
def test_update_network_set_not_shared_other_tenant_returns_409(self):
|
||||||
|
with self.network(shared=True) as network:
|
||||||
|
self._create_port('json',
|
||||||
|
network['network']['id'],
|
||||||
|
201,
|
||||||
|
tenant_id='somebody_else',
|
||||||
|
set_context=True)
|
||||||
|
data = {'network': {'shared': False}}
|
||||||
|
req = self.new_update_request('networks',
|
||||||
|
data,
|
||||||
|
network['network']['id'])
|
||||||
|
self.assertEqual(req.get_response(self.api).status_int, 409)
|
||||||
|
|
||||||
|
def test_update_network_set_not_shared_multi_tenants_returns_409(self):
|
||||||
|
with self.network(shared=True) as network:
|
||||||
|
self._create_port('json',
|
||||||
|
network['network']['id'],
|
||||||
|
201,
|
||||||
|
tenant_id='somebody_else',
|
||||||
|
set_context=True)
|
||||||
|
self._create_port('json',
|
||||||
|
network['network']['id'],
|
||||||
|
201,
|
||||||
|
tenant_id=network['network']['tenant_id'],
|
||||||
|
set_context=True)
|
||||||
|
data = {'network': {'shared': False}}
|
||||||
|
req = self.new_update_request('networks',
|
||||||
|
data,
|
||||||
|
network['network']['id'])
|
||||||
|
self.assertEqual(req.get_response(self.api).status_int, 409)
|
||||||
|
|
||||||
|
def test_update_network_set_not_shared_multi_tenants2_returns_409(self):
|
||||||
|
with self.network(shared=True) as network:
|
||||||
|
self._create_port('json',
|
||||||
|
network['network']['id'],
|
||||||
|
201,
|
||||||
|
tenant_id='somebody_else',
|
||||||
|
set_context=True)
|
||||||
|
self._create_subnet('json',
|
||||||
|
network['network']['id'],
|
||||||
|
'10.0.0.0/24',
|
||||||
|
201,
|
||||||
|
tenant_id=network['network']['tenant_id'],
|
||||||
|
set_context=True)
|
||||||
|
data = {'network': {'shared': False}}
|
||||||
|
req = self.new_update_request('networks',
|
||||||
|
data,
|
||||||
|
network['network']['id'])
|
||||||
|
self.assertEqual(req.get_response(self.api).status_int, 409)
|
||||||
|
|
||||||
def test_list_networks(self):
|
def test_list_networks(self):
|
||||||
with self.network(name='net1') as net1:
|
with self.network(name='net1') as net1:
|
||||||
with self.network(name='net2') as net2:
|
with self.network(name='net2') as net2:
|
||||||
|
@ -29,6 +29,7 @@ import quantum
|
|||||||
from quantum.common import exceptions
|
from quantum.common import exceptions
|
||||||
from quantum.common import utils
|
from quantum.common import utils
|
||||||
from quantum import context
|
from quantum import context
|
||||||
|
from quantum.openstack.common import importutils
|
||||||
from quantum.openstack.common import policy as common_policy
|
from quantum.openstack.common import policy as common_policy
|
||||||
from quantum import policy
|
from quantum import policy
|
||||||
|
|
||||||
@ -206,3 +207,113 @@ class DefaultPolicyTestCase(unittest.TestCase):
|
|||||||
self._set_brain("default_noexist")
|
self._set_brain("default_noexist")
|
||||||
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
||||||
self.context, "example:noexist", {})
|
self.context, "example:noexist", {})
|
||||||
|
|
||||||
|
|
||||||
|
class QuantumPolicyTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(QuantumPolicyTestCase, self).setUp()
|
||||||
|
policy.reset()
|
||||||
|
policy.init()
|
||||||
|
self.rules = {
|
||||||
|
"admin_or_network_owner": [["role:admin"],
|
||||||
|
["tenant_id:%(network_tenant_id)s"]],
|
||||||
|
"admin_only": [["role:admin"]],
|
||||||
|
"regular_user": [["role:user"]],
|
||||||
|
"default": [],
|
||||||
|
|
||||||
|
"networks:private:read": [["rule:admin_only"]],
|
||||||
|
"networks:private:write": [["rule:admin_only"]],
|
||||||
|
"networks:shared:read": [["rule:regular_user"]],
|
||||||
|
"networks:shared:write": [["rule:admin_only"]],
|
||||||
|
|
||||||
|
"create_network": [],
|
||||||
|
"create_network:shared": [["rule:admin_only"]],
|
||||||
|
"update_network": [],
|
||||||
|
"update_network:shared": [["rule:admin_only"]],
|
||||||
|
|
||||||
|
"get_network": [],
|
||||||
|
"create_port:mac": [["rule:admin_or_network_owner"]],
|
||||||
|
}
|
||||||
|
|
||||||
|
def fakepolicyinit():
|
||||||
|
common_policy.set_brain(common_policy.Brain(self.rules))
|
||||||
|
|
||||||
|
self.patcher = mock.patch.object(quantum.policy,
|
||||||
|
'init',
|
||||||
|
new=fakepolicyinit)
|
||||||
|
self.patcher.start()
|
||||||
|
self.context = context.Context('fake', 'fake', roles=['user'])
|
||||||
|
plugin_klass = importutils.import_class(
|
||||||
|
"quantum.db.db_base_plugin_v2.QuantumDbPluginV2")
|
||||||
|
self.plugin = plugin_klass()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.patcher.stop()
|
||||||
|
|
||||||
|
def test_nonadmin_write_on_private_returns_403(self):
|
||||||
|
action = "update_network"
|
||||||
|
user_context = context.Context('', "user", roles=['user'])
|
||||||
|
# 384 is the int value of the bitmask for rw------
|
||||||
|
target = {'tenant_id': 'the_owner', 'shared': False}
|
||||||
|
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
||||||
|
user_context, action, target, None)
|
||||||
|
|
||||||
|
def test_nonadmin_read_on_private_returns_403(self):
|
||||||
|
action = "get_network"
|
||||||
|
user_context = context.Context('', "user", roles=['user'])
|
||||||
|
# 384 is the int value of the bitmask for rw------
|
||||||
|
target = {'tenant_id': 'the_owner', 'shared': False}
|
||||||
|
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
||||||
|
user_context, action, target, None)
|
||||||
|
|
||||||
|
def test_nonadmin_write_on_shared_returns_403(self):
|
||||||
|
action = "update_network"
|
||||||
|
user_context = context.Context('', "user", roles=['user'])
|
||||||
|
# 384 is the int value of the bitmask for rw-r--r--
|
||||||
|
target = {'tenant_id': 'the_owner', 'shared': True}
|
||||||
|
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
||||||
|
user_context, action, target, None)
|
||||||
|
|
||||||
|
def test_nonadmin_read_on_shared_returns_200(self):
|
||||||
|
action = "get_network"
|
||||||
|
user_context = context.Context('', "user", roles=['user'])
|
||||||
|
# 420 is the int value of the bitmask for rw-r--r--
|
||||||
|
target = {'tenant_id': 'the_owner', 'shared': True}
|
||||||
|
result = policy.enforce(user_context, action, target, None)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
def _test_enforce_adminonly_attribute(self, action):
|
||||||
|
admin_context = context.get_admin_context()
|
||||||
|
target = {'shared': True}
|
||||||
|
result = policy.enforce(admin_context, action, target, None)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
def test_enforce_adminonly_attribute_create(self):
|
||||||
|
self._test_enforce_adminonly_attribute('create_network')
|
||||||
|
|
||||||
|
def test_enforce_adminonly_attribute_update(self):
|
||||||
|
self._test_enforce_adminonly_attribute('update_network')
|
||||||
|
|
||||||
|
def test_enforce_adminoly_attribute_nonadminctx_returns_403(self):
|
||||||
|
action = "create_network"
|
||||||
|
target = {'shared': True}
|
||||||
|
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
||||||
|
self.context, action, target, None)
|
||||||
|
|
||||||
|
def test_enforce_regularuser_on_read(self):
|
||||||
|
action = "get_network"
|
||||||
|
target = {'shared': True, 'tenant_id': 'somebody_else'}
|
||||||
|
result = policy.enforce(self.context, action, target, None)
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_enforce_parentresource_owner(self):
|
||||||
|
|
||||||
|
def fakegetnetwork(*args, **kwargs):
|
||||||
|
return {'tenant_id': 'fake'}
|
||||||
|
|
||||||
|
action = "create_port:mac"
|
||||||
|
with mock.patch.object(self.plugin, 'get_network', new=fakegetnetwork):
|
||||||
|
target = {'network_id': 'whatever'}
|
||||||
|
result = policy.enforce(self.context, action, target, self.plugin)
|
||||||
|
self.assertIsNone(result)
|
||||||
|
@ -17,7 +17,8 @@ from quantum import wsgi
|
|||||||
from quantum.wsgi import Serializer
|
from quantum.wsgi import Serializer
|
||||||
|
|
||||||
|
|
||||||
def create_request(path, body, content_type, method='GET', query_string=None):
|
def create_request(path, body, content_type, method='GET',
|
||||||
|
query_string=None, context=None):
|
||||||
if query_string:
|
if query_string:
|
||||||
url = "%s?%s" % (path, query_string)
|
url = "%s?%s" % (path, query_string)
|
||||||
else:
|
else:
|
||||||
@ -27,4 +28,6 @@ def create_request(path, body, content_type, method='GET', query_string=None):
|
|||||||
req.headers = {}
|
req.headers = {}
|
||||||
req.headers['Accept'] = content_type
|
req.headers['Accept'] = content_type
|
||||||
req.body = body
|
req.body = body
|
||||||
|
if context:
|
||||||
|
req.environ['quantum.context'] = context
|
||||||
return req
|
return req
|
||||||
|
Loading…
Reference in New Issue
Block a user