From 407434154edc1496c794b7f330f53a6d436e4321 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 29 Jul 2025 16:24:05 +0100 Subject: [PATCH] api: Add response body schemas for quota sets API This exposes a minor issue in our policy checks. We reportedly assert that a token scoped for one project cannot fetch quotas for another project, but we weren't actually checking this. If we were, it wouldn't have worked since our tests attempt to fetch quotas for the project specified in the token. We add negative tests cases and update the comments to clarify this. Change-Id: I93c60a6bb110ab70f8821d0fbd6e8c5f87453582 Signed-off-by: Stephen Finucane --- nova/api/openstack/compute/quota_sets.py | 95 +++--- .../compute/schemas/quota_classes.py | 2 +- .../openstack/compute/schemas/quota_sets.py | 251 +++++++++++++--- .../{test_quotas.py => test_quota_sets.py} | 272 ++++++++---------- nova/tests/unit/policies/base.py | 1 + nova/tests/unit/policies/test_quota_sets.py | 94 +++--- 6 files changed, 448 insertions(+), 267 deletions(-) rename nova/tests/unit/api/openstack/compute/{test_quotas.py => test_quota_sets.py} (84%) diff --git a/nova/api/openstack/compute/quota_sets.py b/nova/api/openstack/compute/quota_sets.py index 7777fc1cf8..616486c3ae 100644 --- a/nova/api/openstack/compute/quota_sets.py +++ b/nova/api/openstack/compute/quota_sets.py @@ -19,7 +19,7 @@ from oslo_utils import strutils import webob from nova.api.openstack import api_version_request -from nova.api.openstack.compute.schemas import quota_sets +from nova.api.openstack.compute.schemas import quota_sets as schema from nova.api.openstack import identity from nova.api.openstack import wsgi from nova.api import validation @@ -31,32 +31,35 @@ from nova import objects from nova.policies import quota_sets as qs_policies from nova import quota - CONF = nova.conf.CONF QUOTAS = quota.QUOTAS -FILTERED_QUOTAS_2_36 = ["fixed_ips", "floating_ips", - "security_group_rules", "security_groups"] +FILTERED_QUOTAS_v236 = [ + 'fixed_ips', 'floating_ips', 'security_group_rules', 'security_groups' +] -FILTERED_QUOTAS_2_57 = list(FILTERED_QUOTAS_2_36) -FILTERED_QUOTAS_2_57.extend(['injected_files', 'injected_file_content_bytes', - 'injected_file_path_bytes']) +FILTERED_QUOTAS_v257 = list(FILTERED_QUOTAS_v236) +FILTERED_QUOTAS_v257.extend([ + 'injected_files', + 'injected_file_content_bytes', + 'injected_file_path_bytes' +]) +@validation.validated class QuotaSetsController(wsgi.Controller): def _format_quota_set(self, project_id, quota_set, filtered_quotas): """Convert the quota object to a result dict.""" + result = {} if project_id: - result = dict(id=str(project_id)) - else: - result = {} + result['id'] = str(project_id) for resource in QUOTAS.resources: - if (resource not in filtered_quotas and - resource in quota_set): + if resource not in filtered_quotas and resource in quota_set: result[resource] = quota_set[resource] - return dict(quota_set=result) + + return {'quota_set': result} def _validate_quota_limit(self, resource, limit, minimum, maximum): def conv_inf(value): @@ -68,6 +71,7 @@ class QuotaSetsController(wsgi.Controller): "reserved %(minimum)s.") % {'limit': limit, 'resource': resource, 'minimum': minimum}) raise webob.exc.HTTPBadRequest(explanation=msg) + if conv_inf(limit) > conv_inf(maximum): msg = (_("Quota limit %(limit)s for %(resource)s must be " "less than or equal to %(maximum)s.") % @@ -104,60 +108,59 @@ class QuotaSetsController(wsgi.Controller): def _get_filtered_quotas(self, req): if api_version_request.is_supported(req, '2.57'): - return FILTERED_QUOTAS_2_57 + return FILTERED_QUOTAS_v257 elif api_version_request.is_supported(req, '2.36'): - return FILTERED_QUOTAS_2_36 + return FILTERED_QUOTAS_v236 else: return [] @wsgi.expected_errors(400) - @validation.query_schema(quota_sets.show_query, '2.0', '2.74') - @validation.query_schema(quota_sets.show_query_v275, '2.75') + @validation.query_schema(schema.show_query, '2.0', '2.74') + @validation.query_schema(schema.show_query_v275, '2.75') + @validation.response_body_schema(schema.show_response, '2.0', '2.35') + @validation.response_body_schema(schema.show_response_v236, '2.36', '2.56') + @validation.response_body_schema(schema.show_response_v257, '2.57') def show(self, req, id): - filtered_quotas = self._get_filtered_quotas(req) - return self._show(req, id, filtered_quotas) - - def _show(self, req, id, filtered_quotas): context = req.environ['nova.context'] context.can(qs_policies.POLICY_ROOT % 'show', {'project_id': id}) identity.verify_project_id(context, id) params = urlparse.parse_qs(req.environ.get('QUERY_STRING', '')) user_id = params.get('user_id', [None])[0] + filtered_quotas = self._get_filtered_quotas(req) return self._format_quota_set( id, self._get_quotas(context, id, user_id=user_id), filtered_quotas=filtered_quotas) @wsgi.expected_errors(400) - @validation.query_schema(quota_sets.show_query, '2.0', '2.74') - @validation.query_schema(quota_sets.show_query_v275, '2.75') + @validation.query_schema(schema.detail_query, '2.0', '2.74') + @validation.query_schema(schema.detail_query_v275, '2.75') + @validation.response_body_schema(schema.detail_response, '2.0', '2.35') + @validation.response_body_schema(schema.detail_response_v236, '2.36', '2.56') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v257, '2.57') def detail(self, req, id): - filtered_quotas = self._get_filtered_quotas(req) - return self._detail(req, id, filtered_quotas) - - def _detail(self, req, id, filtered_quotas): context = req.environ['nova.context'] context.can(qs_policies.POLICY_ROOT % 'detail', {'project_id': id}) identity.verify_project_id(context, id) user_id = req.GET.get('user_id', None) + filtered_quotas = self._get_filtered_quotas(req) return self._format_quota_set( id, self._get_quotas(context, id, user_id=user_id, usages=True), filtered_quotas=filtered_quotas) @wsgi.expected_errors(400) - @validation.schema(quota_sets.update, '2.0', '2.35') - @validation.schema(quota_sets.update_v236, '2.36', '2.56') - @validation.schema(quota_sets.update_v257, '2.57') - @validation.query_schema(quota_sets.show_query, '2.0', '2.74') - @validation.query_schema(quota_sets.show_query_v275, '2.75') + @validation.schema(schema.update, '2.0', '2.35') + @validation.schema(schema.update_v236, '2.36', '2.56') + @validation.schema(schema.update_v257, '2.57') + @validation.query_schema(schema.update_query, '2.0', '2.74') + @validation.query_schema(schema.update_query_v275, '2.75') + @validation.response_body_schema(schema.update_response, '2.0', '2.35') + @validation.response_body_schema(schema.update_response_v236, '2.36', '2.56') # noqa: E501 + @validation.response_body_schema(schema.update_response_v257, '2.57') def update(self, req, id, body): - filtered_quotas = self._get_filtered_quotas(req) - return self._update(req, id, body, filtered_quotas) - - def _update(self, req, id, body, filtered_quotas): context = req.environ['nova.context'] context.can(qs_policies.POLICY_ROOT % 'update', {'project_id': id}) identity.verify_project_id(context, id) @@ -165,6 +168,7 @@ class QuotaSetsController(wsgi.Controller): project_id = id params = urlparse.parse_qs(req.environ.get('QUERY_STRING', '')) user_id = params.get('user_id', [None])[0] + filtered_quotas = self._get_filtered_quotas(req) quota_set = body['quota_set'] @@ -221,27 +225,28 @@ class QuotaSetsController(wsgi.Controller): @wsgi.api_version('2.0') @wsgi.expected_errors(400) - @validation.query_schema(quota_sets.defaults_query) + @validation.query_schema(schema.defaults_query) + @validation.response_body_schema(schema.defaults_response, '2.0', '2.35') + @validation.response_body_schema(schema.defaults_response_v236, '2.36', '2.56') # noqa: E501 + @validation.response_body_schema(schema.defaults_response_v257, '2.57') def defaults(self, req, id): - filtered_quotas = self._get_filtered_quotas(req) - return self._defaults(req, id, filtered_quotas) - - def _defaults(self, req, id, filtered_quotas): context = req.environ['nova.context'] context.can(qs_policies.POLICY_ROOT % 'defaults', {'project_id': id}) identity.verify_project_id(context, id) values = QUOTAS.get_defaults(context) - return self._format_quota_set(id, values, - filtered_quotas=filtered_quotas) + filtered_quotas = self._get_filtered_quotas(req) + return self._format_quota_set( + id, values, filtered_quotas=filtered_quotas) # TODO(oomichi): Here should be 204(No Content) instead of 202 by v2.1 # +microversions because the resource quota-set has been deleted completely # when returning a response. @wsgi.expected_errors(()) - @validation.query_schema(quota_sets.show_query_v275, '2.75') - @validation.query_schema(quota_sets.show_query, '2.0', '2.74') @wsgi.response(202) + @validation.query_schema(schema.delete_query, '2.0', '2.74') + @validation.query_schema(schema.delete_query_v275, '2.75') + @validation.response_body_schema(schema.delete_response) def delete(self, req, id): context = req.environ['nova.context'] context.can(qs_policies.POLICY_ROOT % 'delete', {'project_id': id}) diff --git a/nova/api/openstack/compute/schemas/quota_classes.py b/nova/api/openstack/compute/schemas/quota_classes.py index ce8e30be18..7410a95a2c 100644 --- a/nova/api/openstack/compute/schemas/quota_classes.py +++ b/nova/api/openstack/compute/schemas/quota_classes.py @@ -20,7 +20,7 @@ update = { 'properties': { 'quota_class_set': { 'type': 'object', - 'properties': quota_sets.quota_resources, + 'properties': quota_sets._quota_resources, 'additionalProperties': False, }, }, diff --git a/nova/api/openstack/compute/schemas/quota_sets.py b/nova/api/openstack/compute/schemas/quota_sets.py index 0a84120f94..6dca6c8fb3 100644 --- a/nova/api/openstack/compute/schemas/quota_sets.py +++ b/nova/api/openstack/compute/schemas/quota_sets.py @@ -16,7 +16,7 @@ import copy from nova.api.validation import parameter_types -common_quota = { +_common_quota = { 'type': ['integer', 'string'], 'pattern': '^-?[0-9]+$', # -1 is a flag value for unlimited @@ -26,42 +26,47 @@ common_quota = { 'maximum': 0x7FFFFFFF } -quota_resources = { - 'instances': common_quota, - 'cores': common_quota, - 'ram': common_quota, - 'floating_ips': common_quota, - 'fixed_ips': common_quota, - 'metadata_items': common_quota, - 'key_pairs': common_quota, - 'security_groups': common_quota, - 'security_group_rules': common_quota, - 'injected_files': common_quota, - 'injected_file_content_bytes': common_quota, - 'injected_file_path_bytes': common_quota, - 'server_groups': common_quota, - 'server_group_members': common_quota, +_quota_resources = { + 'instances': _common_quota, + 'cores': _common_quota, + 'ram': _common_quota, + 'floating_ips': _common_quota, + 'fixed_ips': _common_quota, + 'metadata_items': _common_quota, + 'key_pairs': _common_quota, + 'security_groups': _common_quota, + 'security_group_rules': _common_quota, + 'injected_files': _common_quota, + 'injected_file_content_bytes': _common_quota, + 'injected_file_path_bytes': _common_quota, + 'server_groups': _common_quota, + 'server_group_members': _common_quota, # NOTE(stephenfin): This will always be rejected since it was nova-network # only, but we need to allow users to submit it at a minimum - 'networks': common_quota + 'networks': _common_quota } -update_quota_set = copy.deepcopy(quota_resources) -update_quota_set.update({'force': parameter_types.boolean}) +_update_quota_set = copy.deepcopy(_quota_resources) +_update_quota_set.update({'force': parameter_types.boolean}) -update_quota_set_v236 = copy.deepcopy(update_quota_set) -del update_quota_set_v236['fixed_ips'] -del update_quota_set_v236['floating_ips'] -del update_quota_set_v236['security_groups'] -del update_quota_set_v236['security_group_rules'] -del update_quota_set_v236['networks'] +_update_quota_set_v236 = copy.deepcopy(_update_quota_set) +del _update_quota_set_v236['fixed_ips'] +del _update_quota_set_v236['floating_ips'] +del _update_quota_set_v236['security_groups'] +del _update_quota_set_v236['security_group_rules'] +del _update_quota_set_v236['networks'] + +_update_quota_set_v257 = copy.deepcopy(_update_quota_set_v236) +del _update_quota_set_v257['injected_files'] +del _update_quota_set_v257['injected_file_content_bytes'] +del _update_quota_set_v257['injected_file_path_bytes'] update = { 'type': 'object', 'properties': { 'quota_set': { 'type': 'object', - 'properties': update_quota_set, + 'properties': _update_quota_set, 'additionalProperties': False, }, }, @@ -70,15 +75,10 @@ update = { } update_v236 = copy.deepcopy(update) -update_v236['properties']['quota_set']['properties'] = update_quota_set_v236 +update_v236['properties']['quota_set']['properties'] = _update_quota_set_v236 -# 2.57 builds on 2.36 and removes injected_file* quotas. -update_quota_set_v257 = copy.deepcopy(update_quota_set_v236) -del update_quota_set_v257['injected_files'] -del update_quota_set_v257['injected_file_content_bytes'] -del update_quota_set_v257['injected_file_path_bytes'] update_v257 = copy.deepcopy(update_v236) -update_v257['properties']['quota_set']['properties'] = update_quota_set_v257 +update_v257['properties']['quota_set']['properties'] = _update_quota_set_v257 show_query = { 'type': 'object', @@ -95,9 +95,194 @@ show_query = { show_query_v275 = copy.deepcopy(show_query) show_query_v275['additionalProperties'] = False +detail_query = copy.deepcopy(show_query) +detail_query_v275 = copy.deepcopy(show_query_v275) + +update_query = copy.deepcopy(show_query) +update_query_v275 = copy.deepcopy(show_query_v275) + # TODO(stephenfin): Remove additionalProperties in a future API version defaults_query = { 'type': 'object', 'properties': {}, 'additionalProperties': True, } + +delete_query = copy.deepcopy(show_query) +delete_query_v275 = copy.deepcopy(show_query_v275) + +_quota_response = { + 'type': 'object', + 'properties': { + 'cores': {'type': 'integer', 'minimum': -1}, + 'fixed_ips': {'type': 'integer', 'minimum': -1}, + 'floating_ips': {'type': 'integer', 'minimum': -1}, + 'injected_file_content_bytes': {'type': 'integer', 'minimum': -1}, + 'injected_file_path_bytes': {'type': 'integer', 'minimum': -1}, + 'injected_files': {'type': 'integer', 'minimum': -1}, + 'instances': {'type': 'integer', 'minimum': -1}, + 'key_pairs': {'type': 'integer', 'minimum': -1}, + 'metadata_items': {'type': 'integer', 'minimum': -1}, + 'networks': {'type': 'integer', 'minimum': -1}, + 'ram': {'type': 'integer', 'minimum': -1}, + 'security_groups': {'type': 'integer', 'minimum': -1}, + 'security_group_rules': {'type': 'integer', 'minimum': -1}, + 'server_groups': {'type': 'integer', 'minimum': -1}, + 'server_group_members': {'type': 'integer', 'minimum': -1}, + }, + 'required': [ + # only networks is optional (it only appears under nova-network) + 'cores', + 'fixed_ips', + 'floating_ips', + 'injected_file_content_bytes', + 'injected_file_path_bytes', + 'injected_files', + 'instances', + 'key_pairs', + 'metadata_items', + 'ram', + 'security_groups', + 'security_group_rules', + 'server_groups', + 'server_group_members', + ], + 'additionalProperties': False, +} + +_quota_response_v236 = copy.deepcopy(_quota_response) +for field in { + 'fixed_ips', 'floating_ips', 'security_group_rules', 'security_groups' +}: + del _quota_response_v236['properties'][field] + _quota_response_v236['required'].pop( + _quota_response_v236['required'].index(field) + ) + +_quota_response_v257 = copy.deepcopy(_quota_response_v236) +for field in { + 'injected_files', 'injected_file_content_bytes', 'injected_file_path_bytes' +}: + del _quota_response_v257['properties'][field] + _quota_response_v257['required'].pop( + _quota_response_v257['required'].index(field) + ) + +show_response = { + 'type': 'object', + 'properties': { + 'quota_set': copy.deepcopy(_quota_response), + }, + 'required': ['quota_set'], + 'additionalProperties': False, +} +show_response['properties']['quota_set']['properties'].update({ + 'id': {'type': 'string'}, +}) +show_response['properties']['quota_set']['required'].append('id') + +show_response_v236 = copy.deepcopy(show_response) +show_response_v236['properties']['quota_set'] = copy.deepcopy( + _quota_response_v236 +) +show_response_v236['properties']['quota_set']['properties'].update({ + 'id': {'type': 'string'}, +}) +show_response_v236['properties']['quota_set']['required'].append('id') + +show_response_v257 = copy.deepcopy(show_response_v236) +show_response_v257['properties']['quota_set'] = copy.deepcopy( + _quota_response_v257 +) +show_response_v257['properties']['quota_set']['properties'].update({ + 'id': {'type': 'string'}, +}) +show_response_v257['properties']['quota_set']['required'].append('id') + +_detail_quota = { + 'type': 'object', + 'properties': { + 'in_use': {'type': 'integer', 'minimum': -1}, + 'limit': {'type': 'integer', 'minimum': -1}, + 'reserved': {'type': 'integer', 'minimum': -1}, + }, + 'required': ['in_use', 'limit', 'reserved'], + 'additionalProperties': False, +} + +_detail_quota_response = copy.deepcopy(_quota_response) +for field in _detail_quota_response['properties']: + if field == 'id': + continue + + _detail_quota_response['properties'][field] = _detail_quota + +_detail_quota_response_v236 = copy.deepcopy(_detail_quota_response) +for field in { + 'fixed_ips', 'floating_ips', 'security_group_rules', 'security_groups' +}: + del _detail_quota_response_v236['properties'][field] + _detail_quota_response_v236['required'].pop( + _detail_quota_response_v236['required'].index(field) + ) + +_detail_quota_response_v257 = copy.deepcopy(_detail_quota_response_v236) +for field in { + 'injected_files', 'injected_file_content_bytes', 'injected_file_path_bytes' +}: + del _detail_quota_response_v257['properties'][field] + _detail_quota_response_v257['required'].pop( + _detail_quota_response_v257['required'].index(field) + ) + +detail_response = { + 'type': 'object', + 'properties': { + 'quota_set': copy.deepcopy(_detail_quota_response), + }, + 'required': ['quota_set'], + 'additionalProperties': False, +} +detail_response['properties']['quota_set']['properties'].update({ + 'id': {'type': 'string'}, +}) +detail_response['properties']['quota_set']['required'].append('id') + +detail_response_v236 = copy.deepcopy(detail_response) +detail_response_v236['properties']['quota_set'] = copy.deepcopy( + _detail_quota_response_v236 +) +detail_response_v236['properties']['quota_set']['properties'].update({ + 'id': {'type': 'string'}, +}) +detail_response_v236['properties']['quota_set']['required'].append('id') + +detail_response_v257 = copy.deepcopy(detail_response_v236) +detail_response_v257['properties']['quota_set'] = copy.deepcopy( + _detail_quota_response_v257 +) +detail_response_v257['properties']['quota_set']['properties'].update({ + 'id': {'type': 'string'}, +}) +detail_response_v257['properties']['quota_set']['required'].append('id') + +update_response = { + 'type': 'object', + 'properties': { + 'quota_set': _quota_response, + }, + 'required': ['quota_set'], + 'additionalProperties': False, +} + +update_response_v236 = copy.deepcopy(update_response) +update_response_v236['properties']['quota_set'] = _quota_response_v236 + +update_response_v257 = copy.deepcopy(update_response_v236) +update_response_v257['properties']['quota_set'] = _quota_response_v257 + +defaults_response = copy.deepcopy(show_response) +defaults_response_v236 = copy.deepcopy(show_response_v236) +defaults_response_v257 = copy.deepcopy(show_response_v257) + +delete_response = {'type': 'null'} diff --git a/nova/tests/unit/api/openstack/compute/test_quotas.py b/nova/tests/unit/api/openstack/compute/test_quota_sets.py similarity index 84% rename from nova/tests/unit/api/openstack/compute/test_quotas.py rename to nova/tests/unit/api/openstack/compute/test_quota_sets.py index 17813dda1a..bbfdaa9af2 100644 --- a/nova/tests/unit/api/openstack/compute/test_quotas.py +++ b/nova/tests/unit/api/openstack/compute/test_quota_sets.py @@ -20,7 +20,7 @@ from oslo_limit import fixture as limit_fixture from oslo_utils.fixture import uuidsentinel as uuids import webob -from nova.api.openstack.compute import quota_sets as quotas_v21 +from nova.api.openstack.compute import quota_sets from nova.db import constants as db_const from nova import exception from nova.limit import local as local_limit @@ -32,12 +32,14 @@ from nova.tests.unit.api.openstack import fakes def quota_set(id, include_server_group_quotas=True): - res = {'quota_set': {'id': id, 'metadata_items': 128, - 'ram': 51200, 'floating_ips': -1, 'fixed_ips': -1, - 'instances': 10, 'injected_files': 5, 'cores': 20, - 'injected_file_content_bytes': 10240, - 'security_groups': -1, 'security_group_rules': -1, - 'key_pairs': 100, 'injected_file_path_bytes': 255}} + res = { + 'quota_set': { + 'id': id, 'metadata_items': 128, + 'ram': 51200, 'floating_ips': -1, 'fixed_ips': -1, + 'instances': 10, 'injected_files': 5, 'cores': 20, + 'injected_file_content_bytes': 10240, + 'security_groups': -1, 'security_group_rules': -1, + 'key_pairs': 100, 'injected_file_path_bytes': 255}} if include_server_group_quotas: res['quota_set']['server_groups'] = 10 res['quota_set']['server_group_members'] = 10 @@ -47,7 +49,10 @@ def quota_set(id, include_server_group_quotas=True): class BaseQuotaSetsTest(test.TestCase): def setUp(self): - super(BaseQuotaSetsTest, self).setUp() + super().setUp() + + self.controller = quota_sets.QuotaSetsController() + # We need to stub out verify_project_id so that it doesn't # generate an EndpointNotFound exception and result in a # server error. @@ -56,13 +61,11 @@ class BaseQuotaSetsTest(test.TestCase): class QuotaSetsTestV21(BaseQuotaSetsTest): - plugin = quotas_v21 validation_error = exception.ValidationError include_server_group_quotas = True def setUp(self): super(QuotaSetsTestV21, self).setUp() - self._setup_controller() self.default_quotas = { 'instances': 10, 'cores': 20, @@ -81,19 +84,15 @@ class QuotaSetsTestV21(BaseQuotaSetsTest): self.default_quotas['server_groups'] = 10 self.default_quotas['server_group_members'] = 10 - def _setup_controller(self): - self.controller = self.plugin.QuotaSetsController() - def _get_http_request(self, url=''): return fakes.HTTPRequest.blank(url) def test_format_quota_set(self): - quota_set = self.controller._format_quota_set('1234', - self.default_quotas, - []) + quota_set = self.controller._format_quota_set( + uuids.project_id, self.default_quotas, []) qs = quota_set['quota_set'] - self.assertEqual(qs['id'], '1234') + self.assertEqual(qs['id'], uuids.project_id) self.assertEqual(qs['instances'], 10) self.assertEqual(qs['cores'], 20) self.assertEqual(qs['ram'], 51200) @@ -171,9 +170,11 @@ class QuotaSetsTestV21(BaseQuotaSetsTest): def test_quotas_show(self): req = self._get_http_request() - res_dict = self.controller.show(req, 1234) + res_dict = self.controller.show(req, uuids.project_id) - ref_quota_set = quota_set('1234', self.include_server_group_quotas) + ref_quota_set = quota_set( + uuids.project_id, self.include_server_group_quotas + ) self.assertEqual(res_dict, ref_quota_set) def test_quotas_update(self): @@ -197,8 +198,9 @@ class QuotaSetsTestV21(BaseQuotaSetsTest): @mock.patch('nova.api.validation.validators._SchemaValidator.validate') @mock.patch('nova.objects.Quotas.create_limit') - def test_quotas_update_with_bad_data(self, mock_createlimit, - mock_validate): + def test_quotas_update_with_bad_data( + self, mock_createlimit, mock_validate, + ): self.default_quotas.update({ 'instances': 50, 'cores': -50 @@ -278,69 +280,69 @@ class QuotaSetsTestV21(BaseQuotaSetsTest): @mock.patch('nova.objects.Quotas.destroy_all_by_project') def test_quotas_delete(self, mock_destroy_all_by_project): req = self._get_http_request() - self.controller.delete(req, 1234) + self.controller.delete(req, uuids.project_id) self.assertEqual(202, self.controller.delete.wsgi_codes(req)) mock_destroy_all_by_project.assert_called_once_with( - req.environ['nova.context'], 1234) + req.environ['nova.context'], uuids.project_id) def test_duplicate_quota_filter(self): query_string = 'user_id=1&user_id=2' req = fakes.HTTPRequest.blank('', query_string=query_string) - self.controller.show(req, 1234) - self.controller.update(req, 1234, body={'quota_set': {}}) - self.controller.detail(req, 1234) - self.controller.delete(req, 1234) + self.controller.show(req, uuids.project_id) + self.controller.update(req, uuids.project_id, body={'quota_set': {}}) + self.controller.detail(req, uuids.project_id) + self.controller.delete(req, uuids.project_id) def test_quota_filter_negative_int_as_string(self): req = fakes.HTTPRequest.blank('', query_string='user_id=-1') - self.controller.show(req, 1234) - self.controller.update(req, 1234, body={'quota_set': {}}) - self.controller.detail(req, 1234) - self.controller.delete(req, 1234) + self.controller.show(req, uuids.project_id) + self.controller.update(req, uuids.project_id, body={'quota_set': {}}) + self.controller.detail(req, uuids.project_id) + self.controller.delete(req, uuids.project_id) def test_quota_filter_int_as_string(self): req = fakes.HTTPRequest.blank('', query_string='user_id=123') - self.controller.show(req, 1234) - self.controller.update(req, 1234, body={'quota_set': {}}) - self.controller.detail(req, 1234) - self.controller.delete(req, 1234) + self.controller.show(req, uuids.project_id) + self.controller.update(req, uuids.project_id, body={'quota_set': {}}) + self.controller.detail(req, uuids.project_id) + self.controller.delete(req, uuids.project_id) def test_unknown_quota_filter(self): query_string = 'unknown_filter=abc' req = fakes.HTTPRequest.blank('', query_string=query_string) - self.controller.show(req, 1234) - self.controller.update(req, 1234, body={'quota_set': {}}) - self.controller.detail(req, 1234) - self.controller.delete(req, 1234) + self.controller.show(req, uuids.project_id) + self.controller.update(req, uuids.project_id, body={'quota_set': {}}) + self.controller.detail(req, uuids.project_id) + self.controller.delete(req, uuids.project_id) def test_quota_additional_filter(self): query_string = 'user_id=1&additional_filter=2' req = fakes.HTTPRequest.blank('', query_string=query_string) - self.controller.show(req, 1234) - self.controller.update(req, 1234, body={'quota_set': {}}) - self.controller.detail(req, 1234) - self.controller.delete(req, 1234) + self.controller.show(req, uuids.project_id) + self.controller.update(req, uuids.project_id, body={'quota_set': {}}) + self.controller.detail(req, uuids.project_id) + self.controller.delete(req, uuids.project_id) class ExtendedQuotasTestV21(BaseQuotaSetsTest): - plugin = quotas_v21 - - def setUp(self): - super(ExtendedQuotasTestV21, self).setUp() - self._setup_controller() - - fake_quotas = {'ram': {'limit': 51200, - 'in_use': 12800, - 'reserved': 12800}, - 'cores': {'limit': 20, - 'in_use': 10, - 'reserved': 5}, - 'instances': {'limit': 100, - 'in_use': 0, - 'reserved': 0}} - - def _setup_controller(self): - self.controller = self.plugin.QuotaSetsController() + fake_quotas = { + 'cores': {'limit': 20, 'in_use': 10, 'reserved': 5}, + 'fixed_ips': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'floating_ips': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'injected_file_content_bytes': { + 'limit': -1, 'in_use': 0, 'reserved': -1 + }, + 'injected_file_path_bytes': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'injected_files': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'instances': {'limit': 100, 'in_use': 0, 'reserved': 0}, + 'key_pairs': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'metadata_items': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'ram': {'limit': 51200, 'in_use': 12800, 'reserved': 12800}, + 'security_groups': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'security_group_rules': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'server_groups': {'limit': -1, 'in_use': 0, 'reserved': -1}, + 'server_group_members': {'limit': -1, 'in_use': 0, 'reserved': -1}, + } def fake_get_quotas(self, context, id, user_id=None, usages=False): if usages: @@ -350,15 +352,10 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest): def fake_get_settable_quotas(self, context, project_id, user_id=None): return { - 'ram': {'minimum': self.fake_quotas['ram']['in_use'] + - self.fake_quotas['ram']['reserved'], - 'maximum': -1}, - 'cores': {'minimum': self.fake_quotas['cores']['in_use'] + - self.fake_quotas['cores']['reserved'], - 'maximum': -1}, - 'instances': {'minimum': self.fake_quotas['instances']['in_use'] + - self.fake_quotas['instances']['reserved'], - 'maximum': -1}, + k: { + 'minimum': v['in_use'] + v['reserved'], + 'maximum': -1, + } for k, v in self.fake_quotas.items() } def _get_http_request(self, url=''): @@ -375,7 +372,7 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest): @mock.patch.object(quota.QUOTAS, 'get_settable_quotas') def test_quotas_force_update_exceed_in_used(self, get_settable_quotas): - with mock.patch.object(self.plugin.QuotaSetsController, + with mock.patch.object(quota_sets.QuotaSetsController, '_get_quotas') as _get_quotas: body = {'quota_set': {'cores': 10, 'force': 'True'}} @@ -413,19 +410,12 @@ class ExtendedQuotasTestV21(BaseQuotaSetsTest): class UserQuotasTestV21(BaseQuotaSetsTest): - plugin = quotas_v21 + plugin = quota_sets include_server_group_quotas = True - def setUp(self): - super(UserQuotasTestV21, self).setUp() - self._setup_controller() - def _get_http_request(self, url=''): return fakes.HTTPRequest.blank(url) - def _setup_controller(self): - self.controller = self.plugin.QuotaSetsController() - def test_user_quotas_show(self): req = self._get_http_request( '/v2.1/os-quota-sets/%s?user_id=1' % fakes.FAKE_PROJECT_ID) @@ -496,19 +486,15 @@ class UserQuotasTestV21(BaseQuotaSetsTest): len(mock_createlimit.mock_calls)) -class QuotaSetsTestV236(test.NoDBTestCase): +class QuotaSetsTestV236(BaseQuotaSetsTest): microversion = '2.36' def setUp(self): super(QuotaSetsTestV236, self).setUp() - # We need to stub out verify_project_id so that it doesn't - # generate an EndpointNotFound exception and result in a - # server error. - self.stub_out('nova.api.openstack.identity.verify_project_id', - lambda ctx, project_id: True) self.old_req = fakes.HTTPRequest.blank('', version='2.1') - self.filtered_quotas = ['fixed_ips', 'floating_ips', + self.filtered_quotas = [ + 'fixed_ips', 'floating_ips', 'security_group_rules', 'security_groups'] self.quotas = { 'cores': {'limit': 20}, @@ -542,58 +528,57 @@ class QuotaSetsTestV236(test.NoDBTestCase): 'server_group_members': 10, 'server_groups': 10 } - self.controller = quotas_v21.QuotaSetsController() + self.controller = quota_sets.QuotaSetsController() self.req = fakes.HTTPRequest.blank('', version=self.microversion) - def _ensure_filtered_quotas_existed_in_old_api(self): - res_dict = self.controller.show(self.old_req, 1234) + def test_quotas_show_filtered(self): + res_dict = self.controller.show(self.old_req, uuids.project_id) for filtered in self.filtered_quotas: self.assertIn(filtered, res_dict['quota_set']) - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_quotas_show_filtered(self, mock_quotas): - mock_quotas.return_value = self.quotas - self._ensure_filtered_quotas_existed_in_old_api() - res_dict = self.controller.show(self.req, 1234) + res_dict = self.controller.show(self.req, uuids.project_id) for filtered in self.filtered_quotas: self.assertNotIn(filtered, res_dict['quota_set']) - @mock.patch('nova.quota.QUOTAS.get_defaults') - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_quotas_default_filtered(self, mock_quotas, mock_defaults): - mock_quotas.return_value = self.quotas - self._ensure_filtered_quotas_existed_in_old_api() - res_dict = self.controller.defaults(self.req, 1234) + def test_quotas_default_filtered(self): + res_dict = self.controller.defaults(self.old_req, uuids.project_id) + for filtered in self.filtered_quotas: + self.assertIn(filtered, res_dict['quota_set']) + + res_dict = self.controller.defaults(self.req, uuids.project_id) for filtered in self.filtered_quotas: self.assertNotIn(filtered, res_dict['quota_set']) - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_quotas_detail_filtered(self, mock_quotas): - mock_quotas.return_value = self.quotas - self._ensure_filtered_quotas_existed_in_old_api() - res_dict = self.controller.detail(self.req, 1234) + def test_quotas_detail_filtered(self): + res_dict = self.controller.detail(self.old_req, uuids.project_id) + for filtered in self.filtered_quotas: + self.assertIn(filtered, res_dict['quota_set']) + + res_dict = self.controller.detail(self.req, uuids.project_id) for filtered in self.filtered_quotas: self.assertNotIn(filtered, res_dict['quota_set']) - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_quotas_update_input_filtered(self, mock_quotas): - mock_quotas.return_value = self.quotas - self._ensure_filtered_quotas_existed_in_old_api() + def test_quotas_update_input_filtered(self): + self.controller.update( + self.old_req, uuids.project_id, + body={'quota_set': {k: 100 for k in self.filtered_quotas}}) + for filtered in self.filtered_quotas: - self.assertRaises(exception.ValidationError, - self.controller.update, self.req, 1234, + self.assertRaises( + exception.ValidationError, + self.controller.update, self.req, uuids.project_id, body={'quota_set': {filtered: 100}}) - @mock.patch('nova.objects.Quotas.create_limit') - @mock.patch('nova.quota.QUOTAS.get_settable_quotas') - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_quotas_update_output_filtered(self, mock_quotas, mock_settable, - mock_create_limit): - mock_quotas.return_value = self.quotas - mock_settable.return_value = {'cores': {'maximum': -1, 'minimum': 0}} - self._ensure_filtered_quotas_existed_in_old_api() - res_dict = self.controller.update(self.req, 1234, - body={'quota_set': {'cores': 100}}) + def test_quotas_update_output_filtered(self): + res_dict = self.controller.update( + self.old_req, uuids.project_id, + body={'quota_set': {'cores': 100}}) + for filtered in self.filtered_quotas: + self.assertIn(filtered, res_dict['quota_set']) + + res_dict = self.controller.update( + self.req, uuids.project_id, + body={'quota_set': {'cores': 101}}) for filtered in self.filtered_quotas: self.assertNotIn(filtered, res_dict['quota_set']) @@ -603,29 +588,20 @@ class QuotaSetsTestV257(QuotaSetsTestV236): def setUp(self): super(QuotaSetsTestV257, self).setUp() - self.filtered_quotas.extend(quotas_v21.FILTERED_QUOTAS_2_57) + self.filtered_quotas.extend(quota_sets.FILTERED_QUOTAS_v257) class QuotaSetsTestV275(QuotaSetsTestV257): microversion = '2.75' - @mock.patch('nova.objects.Quotas.destroy_all_by_project') - @mock.patch('nova.objects.Quotas.create_limit') - @mock.patch('nova.quota.QUOTAS.get_settable_quotas') - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_quota_additional_filter_older_version(self, mock_quotas, - mock_settable, - mock_create_limit, - mock_destroy): - mock_quotas.return_value = self.quotas - mock_settable.return_value = {'cores': {'maximum': -1, 'minimum': 0}} + def test_quota_additional_filter_older_version(self): query_string = 'additional_filter=2' req = fakes.HTTPRequest.blank('', version='2.74', query_string=query_string) - self.controller.show(req, 1234) - self.controller.update(req, 1234, body={'quota_set': {}}) - self.controller.detail(req, 1234) - self.controller.delete(req, 1234) + self.controller.show(req, uuids.project_id) + self.controller.update(req, uuids.project_id, body={'quota_set': {}}) + self.controller.detail(req, uuids.project_id) + self.controller.delete(req, uuids.project_id) def test_quota_update_additional_filter(self): query_string = 'user_id=1&additional_filter=2' @@ -639,33 +615,31 @@ class QuotaSetsTestV275(QuotaSetsTestV257): req = fakes.HTTPRequest.blank('', version=self.microversion, query_string=query_string) self.assertRaises(exception.ValidationError, self.controller.show, - req, 1234) + req, uuids.project_id) def test_quota_detail_additional_filter(self): query_string = 'user_id=1&additional_filter=2' req = fakes.HTTPRequest.blank('', version=self.microversion, query_string=query_string) self.assertRaises(exception.ValidationError, self.controller.detail, - req, 1234) + req, uuids.project_id) def test_quota_delete_additional_filter(self): query_string = 'user_id=1&additional_filter=2' req = fakes.HTTPRequest.blank('', version=self.microversion, query_string=query_string) self.assertRaises(exception.ValidationError, self.controller.delete, - req, 1234) + req, uuids.project_id) -class NoopQuotaSetsTest(test.NoDBTestCase): +class NoopQuotaSetsTest(BaseQuotaSetsTest): quota_driver = "nova.quota.NoopQuotaDriver" expected_detail = {'in_use': -1, 'limit': -1, 'reserved': -1} def setUp(self): super(NoopQuotaSetsTest, self).setUp() self.flags(driver=self.quota_driver, group="quota") - self.controller = quotas_v21.QuotaSetsController() - self.stub_out('nova.api.openstack.identity.verify_project_id', - lambda ctx, project_id: True) + self.controller = quota_sets.QuotaSetsController() def test_show_v21(self): req = fakes.HTTPRequest.blank("") @@ -845,16 +819,16 @@ class NoopQuotaSetsTest(test.NoDBTestCase): @mock.patch('nova.objects.Quotas.destroy_all_by_project') def test_quotas_delete(self, mock_destroy_all_by_project): req = fakes.HTTPRequest.blank("") - self.controller.delete(req, "1234") + self.controller.delete(req, uuids.project_id) mock_destroy_all_by_project.assert_called_once_with( - req.environ['nova.context'], "1234") + req.environ['nova.context'], uuids.project_id) @mock.patch('nova.objects.Quotas.destroy_all_by_project_and_user') def test_user_quotas_delete(self, mock_destroy_all_by_user): req = fakes.HTTPRequest.blank("?user_id=42") - self.controller.delete(req, "1234") + self.controller.delete(req, uuids.project_id) mock_destroy_all_by_user.assert_called_once_with( - req.environ['nova.context'], "1234", "42") + req.environ['nova.context'], uuids.project_id, "42") class UnifiedLimitsQuotaSetsTest(NoopQuotaSetsTest): @@ -1116,13 +1090,13 @@ class UnifiedLimitsQuotaSetsTest(NoopQuotaSetsTest): @mock.patch('nova.objects.Quotas.destroy_all_by_project') def test_quotas_delete(self, mock_destroy_all_by_project): req = fakes.HTTPRequest.blank("") - self.controller.delete(req, "1234") + self.controller.delete(req, uuids.project_id) # Ensure destroy isn't called for unified limits self.assertEqual(0, mock_destroy_all_by_project.call_count) @mock.patch('nova.objects.Quotas.destroy_all_by_project_and_user') def test_user_quotas_delete(self, mock_destroy_all_by_user): req = fakes.HTTPRequest.blank("?user_id=42") - self.controller.delete(req, "1234") + self.controller.delete(req, uuids.project_id) # Ensure destroy isn't called for unified limits self.assertEqual(0, mock_destroy_all_by_user.call_count) diff --git a/nova/tests/unit/policies/base.py b/nova/tests/unit/policies/base.py index 8d4aa03142..a725bb43eb 100644 --- a/nova/tests/unit/policies/base.py +++ b/nova/tests/unit/policies/base.py @@ -74,6 +74,7 @@ class BasePolicyTest(test.TestCase): self.admin_project_id = uuids.admin_project_id self.project_id = uuids.project_id self.project_id_other = uuids.project_id_other + self.project_id_unused = uuids.project_id_random # all context are with implied roles. self.legacy_admin_context = nova_context.RequestContext( diff --git a/nova/tests/unit/policies/test_quota_sets.py b/nova/tests/unit/policies/test_quota_sets.py index 84a3cf0b1b..6aa85003f6 100644 --- a/nova/tests/unit/policies/test_quota_sets.py +++ b/nova/tests/unit/policies/test_quota_sets.py @@ -10,11 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from unittest import mock - from nova.api.openstack.compute import quota_sets from nova import exception from nova.policies import quota_sets as policies +from nova.tests import fixtures as nova_fixtures from nova.tests.unit.api.openstack import fakes from nova.tests.unit.policies import base @@ -30,10 +29,11 @@ class QuotaSetsPolicyTest(base.BasePolicyTest): def setUp(self): super(QuotaSetsPolicyTest, self).setUp() self.controller = quota_sets.QuotaSetsController() - self.controller._validate_quota_limit = mock.MagicMock() self.req = fakes.HTTPRequest.blank('') self.project_id = self.req.environ['nova.context'].project_id + self.useFixture(nova_fixtures.NoopQuotaDriverFixture()) + # With legacy rule all admin is able to update or revert their quota # to default or get other project quota. self.project_admin_authorized_contexts = set([ @@ -42,9 +42,7 @@ class QuotaSetsPolicyTest(base.BasePolicyTest): # With legacy rule, everyone is able to get their own quota. self.project_reader_authorized_contexts = set([ self.legacy_admin_context, self.system_admin_context, - self.project_admin_context, - self.system_member_context, self.system_reader_context, - self.system_foo_context, self.project_manager_context, + self.project_admin_context, self.project_manager_context, self.project_member_context, self.project_reader_context, self.project_foo_context, self.other_project_manager_context, @@ -63,20 +61,17 @@ class QuotaSetsPolicyTest(base.BasePolicyTest): self.other_project_member_context, self.other_project_reader_context, self.service_context]) - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - @mock.patch('nova.quota.QUOTAS.get_settable_quotas') - def test_update_quota_sets_policy(self, mock_update, mock_get): + def test_update_quota_sets_policy(self): rule_name = policies.POLICY_ROOT % 'update' - body = {'quota_set': { - 'instances': 50, - 'cores': 50} - } + body = {'quota_set': {'instances': 50, 'cores': 50}} + for cxtx in self.project_admin_authorized_contexts: req = fakes.HTTPRequest.blank('') req.environ['nova.context'] = cxtx self.controller.update(req, cxtx.project_id, body=body) - for cxtx in (self.all_contexts - - set(self.project_admin_authorized_contexts)): + for cxtx in ( + self.all_contexts - set(self.project_admin_authorized_contexts) + ): req = fakes.HTTPRequest.blank('') req.environ['nova.context'] = cxtx exc = self.assertRaises( @@ -86,15 +81,15 @@ class QuotaSetsPolicyTest(base.BasePolicyTest): "Policy doesn't allow %s to be performed." % rule_name, exc.format_message()) - @mock.patch('nova.objects.Quotas.destroy_all_by_project') - def test_delete_quota_sets_policy(self, mock_delete): + def test_delete_quota_sets_policy(self): rule_name = policies.POLICY_ROOT % 'delete' for cxtx in self.project_admin_authorized_contexts: req = fakes.HTTPRequest.blank('') req.environ['nova.context'] = cxtx self.controller.delete(req, cxtx.project_id) - for cxtx in (self.all_contexts - - set(self.project_admin_authorized_contexts)): + for cxtx in ( + self.all_contexts - set(self.project_admin_authorized_contexts) + ): req = fakes.HTTPRequest.blank('') req.environ['nova.context'] = cxtx exc = self.assertRaises( @@ -104,41 +99,62 @@ class QuotaSetsPolicyTest(base.BasePolicyTest): "Policy doesn't allow %s to be performed." % rule_name, exc.format_message()) - @mock.patch('nova.quota.QUOTAS.get_defaults') - def test_default_quota_sets_policy(self, mock_default): + def test_default_quota_sets_policy(self): rule_name = policies.POLICY_ROOT % 'defaults' self.common_policy_auth(self.everyone_authorized_contexts, rule_name, self.controller.defaults, - self.req, self.project_id) + self.req, self.project_id_unused) - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_detail_quota_sets_policy(self, mock_get): + def test_detail_quota_sets_policy(self): rule_name = policies.POLICY_ROOT % 'detail' self.common_policy_auth(self.project_admin_authorized_contexts, rule_name, self.controller.detail, - self.req, 'try-other-project') + self.req, self.project_id_unused) # Check if project reader or higher roles are able to get # their own quota for cxtx in self.project_reader_authorized_contexts: req = fakes.HTTPRequest.blank('') req.environ['nova.context'] = cxtx - self.controller.detail(req, cxtx.project_id) + self.controller.detail(req, cxtx.project_id or self.project_id) + for cxtx in ( + self.all_contexts - self.project_reader_authorized_contexts + ): + req = fakes.HTTPRequest.blank('') + req.environ['nova.context'] = cxtx + exc = self.assertRaises( + exception.PolicyNotAuthorized, self.controller.detail, + req, cxtx.project_id or self.project_id) + self.assertEqual( + "Policy doesn't allow %s to be performed." % rule_name, + exc.format_message()) - @mock.patch('nova.quota.QUOTAS.get_project_quotas') - def test_show_quota_sets_policy(self, mock_get): + def test_show_quota_sets_policy(self): rule_name = policies.POLICY_ROOT % 'show' self.common_policy_auth(self.project_admin_authorized_contexts, rule_name, self.controller.show, - self.req, 'try-other-project') + self.req, self.project_id_unused) # Check if project reader or higher roles are able to get # their own quota for cxtx in self.project_reader_authorized_contexts: req = fakes.HTTPRequest.blank('') req.environ['nova.context'] = cxtx - self.controller.show(req, cxtx.project_id) + self.controller.show(req, cxtx.project_id or self.project_id) + cnt = 0 + for cxtx in ( + self.all_contexts - self.project_reader_authorized_contexts + ): + cnt += 1 + req = fakes.HTTPRequest.blank('') + req.environ['nova.context'] = cxtx + exc = self.assertRaises( + exception.PolicyNotAuthorized, self.controller.show, + req, cxtx.project_id or self.project_id) + self.assertEqual( + "Policy doesn't allow %s to be performed." % rule_name, + exc.format_message()) class QuotaSetsNoLegacyNoScopePolicyTest(QuotaSetsPolicyTest): @@ -154,17 +170,17 @@ class QuotaSetsNoLegacyNoScopePolicyTest(QuotaSetsPolicyTest): # Even with no legacy rule, because any admin requesting # update/revert quota for their own project will be allowed. # And any admin will be able to get other project quota. - self.project_admin_authorized_contexts = [ + self.project_admin_authorized_contexts = set([ self.legacy_admin_context, self.system_admin_context, - self.project_admin_context] - # With no legacy rule, other project and foo role will not be - # able to get the quota. - self.project_reader_authorized_contexts = [ + self.project_admin_context]) + # With no legacy rule, foo role will not be able to get the quota. + self.project_reader_authorized_contexts = set([ self.legacy_admin_context, self.system_admin_context, - self.project_admin_context, - self.system_member_context, self.system_reader_context, - self.project_manager_context, self.project_member_context, - self.project_reader_context] + self.project_admin_context, self.project_manager_context, + self.project_member_context, self.project_reader_context, + self.other_project_manager_context, + self.other_project_member_context, + self.other_project_reader_context]) class QuotaSetsScopeTypePolicyTest(QuotaSetsPolicyTest):