Merge "api: Restrict additional query string arguments"

This commit is contained in:
Zuul
2026-02-28 19:22:07 +00:00
committed by Gerrit Code Review
58 changed files with 386 additions and 164 deletions

View File

@@ -283,7 +283,8 @@ REST_API_VERSION_HISTORY = """REST API Version History:
202 Accepted instead of HTTP 200 and a volumeAttachment response.
* 2.102 - Add support for filtering flavors by name. Remove the deprecated
``rxtx_factor`` and ``OS-FLV-DISABLED:disabled`` fields and
filters from various flavors APIs.
filters from various flavors APIs and restrict additional query
string parameters for all APIs.
"""
# The minimum and maximum versions of the API supported

View File

@@ -50,7 +50,8 @@ class AggregateController(wsgi.Controller):
self.conductor_tasks = conductor.ComputeTaskAPI()
@wsgi.expected_errors(())
@validation.query_schema(schema.index_query)
@validation.query_schema(schema.index_query, '2.1', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response, '2.1', '2.40')
@validation.response_body_schema(schema.index_response_v241, '2.41')
def index(self, req):
@@ -100,7 +101,8 @@ class AggregateController(wsgi.Controller):
return agg
@wsgi.expected_errors((400, 404))
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.1', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response, '2.1', '2.40')
@validation.response_body_schema(schema.show_response_v241, '2.41')
def show(self, req, id):

View File

@@ -65,7 +65,7 @@ class AssistedVolumeSnapshotsController(wsgi.Controller):
@wsgi.response(204)
@wsgi.expected_errors((400, 404))
@validation.query_schema(schema.delete_query, '2.0', '2.74')
@validation.query_schema(schema.delete_query_275, '2.75')
@validation.query_schema(schema.delete_query_v275, '2.75')
@validation.response_body_schema(schema.delete_response)
def delete(self, req, id):
"""Delete a snapshot."""

View File

@@ -65,7 +65,8 @@ class InterfaceAttachmentController(wsgi.Controller):
self.network_api = neutron.API()
@wsgi.expected_errors((404, 501))
@validation.query_schema(schema.index_query)
@validation.query_schema(schema.index_query, '2.1', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response, '2.1', '2.69')
@validation.response_body_schema(schema.index_response_v270, '2.70')
def index(self, req, server_id):
@@ -110,7 +111,8 @@ class InterfaceAttachmentController(wsgi.Controller):
return {'interfaceAttachments': results}
@wsgi.expected_errors((403, 404))
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.1', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response, '2.1', '2.69')
@validation.response_body_schema(schema.show_response_v270, '2.70')
def show(self, req, server_id, id):

View File

@@ -106,7 +106,8 @@ class AvailabilityZoneController(wsgi.Controller):
return {'availabilityZoneInfo': result}
@wsgi.expected_errors(())
@validation.query_schema(schema.index_query)
@validation.query_schema(schema.index_query, '2.1', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response)
def index(self, req):
"""Returns a summary list of availability zone."""
@@ -116,7 +117,8 @@ class AvailabilityZoneController(wsgi.Controller):
return self._describe_availability_zones(context)
@wsgi.expected_errors(())
@validation.query_schema(schema.detail_query)
@validation.query_schema(schema.detail_query, '2.1', '2.101')
@validation.query_schema(schema.detail_query_v2102, '2.102')
@validation.response_body_schema(schema.detail_response)
def detail(self, req):
"""Returns a detailed list of availability zone."""

View File

@@ -856,7 +856,8 @@ EXTENSION_LIST_LEGACY_V2_COMPATIBLE = sorted(
class ExtensionInfoController(wsgi.Controller):
@wsgi.expected_errors(())
@validation.query_schema(schema.index_query)
@validation.query_schema(schema.index_query, '2.1', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response)
def index(self, req):
context = req.environ['nova.context']
@@ -870,7 +871,8 @@ class ExtensionInfoController(wsgi.Controller):
return dict(extensions=EXTENSION_LIST)
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.1', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response)
def show(self, req, id):
context = req.environ['nova.context']

View File

@@ -42,7 +42,8 @@ class FlavorAccessController(wsgi.Controller):
"""The flavor access API controller for the OpenStack API."""
@wsgi.expected_errors(404)
@validation.query_schema(schema.index_query)
@validation.query_schema(schema.index_query, '2.1', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response)
def index(self, req, flavor_id):
context = req.environ['nova.context']

View File

@@ -171,7 +171,8 @@ class FlavorsController(wsgi.Controller):
req, limited_flavors, include_extra_specs=include_extra_specs)
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.0', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response, '2.0', '2.54')
@validation.response_body_schema(schema.show_response_v255, '2.55', '2.60')
@validation.response_body_schema(schema.show_response_v261, '2.61', '2.74')

View File

@@ -55,7 +55,8 @@ class FlavorExtraSpecsController(wsgi.Controller):
validators.validate(name, value)
@wsgi.expected_errors(404)
@validation.query_schema(schema.index_query)
@validation.query_schema(schema.index_query, '2.1', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response)
def index(self, req, flavor_id):
"""Returns the list of extra specs for a given flavor."""
@@ -108,7 +109,8 @@ class FlavorExtraSpecsController(wsgi.Controller):
return body
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.1', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response)
def show(self, req, flavor_id, id):
"""Return a single extra spec item."""

View File

@@ -139,7 +139,8 @@ class InstanceActionsController(wsgi.Controller):
return actions_dict
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.1', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response, "2.1", "2.50")
@validation.response_body_schema(schema.show_response_v251, "2.51", "2.57")
@validation.response_body_schema(schema.show_response_v258, "2.58", "2.61")

View File

@@ -35,7 +35,8 @@ class InstanceUsageAuditLogController(wsgi.Controller):
self.host_api = compute.HostAPI()
@wsgi.expected_errors(())
@validation.query_schema(schema.index_query)
@validation.query_schema(schema.index_query, '2.1', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response)
def index(self, req):
context = req.environ['nova.context']
@@ -44,7 +45,8 @@ class InstanceUsageAuditLogController(wsgi.Controller):
return {'instance_usage_audit_logs': task_log}
@wsgi.expected_errors(400)
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.1', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response)
def show(self, req, id):
context = req.environ['nova.context']

View File

@@ -35,7 +35,8 @@ class IPsController(wsgi.Controller):
self._compute_api = compute.API()
@wsgi.expected_errors(404)
@validation.query_schema(schema.index_query)
@validation.query_schema(schema.index_query, '2.1', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response)
def index(self, req, server_id):
context = req.environ["nova.context"]
@@ -46,7 +47,8 @@ class IPsController(wsgi.Controller):
return self._view_builder.index(req, networks)
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.1', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response)
def show(self, req, server_id, id):
context = req.environ["nova.context"]

View File

@@ -118,9 +118,9 @@ class KeypairController(wsgi.Controller):
@wsgi.response(202, '2.0', '2.1')
@wsgi.response(204, '2.2')
@validation.query_schema(schema.delete_query_schema_v20, '2.0', '2.9')
@validation.query_schema(schema.delete_query_schema_v210, '2.10', '2.74')
@validation.query_schema(schema.delete_query_schema_v275, '2.75')
@validation.query_schema(schema.delete_query_v20, '2.0', '2.9')
@validation.query_schema(schema.delete_query_v210, '2.10', '2.74')
@validation.query_schema(schema.delete_query_v275, '2.75')
@validation.response_body_schema(schema.delete_response)
@wsgi.expected_errors(404)
def delete(self, req, id):
@@ -143,9 +143,9 @@ class KeypairController(wsgi.Controller):
except exception.KeypairNotFound as exc:
raise webob.exc.HTTPNotFound(explanation=exc.format_message())
@validation.query_schema(schema.show_query_schema_v20, '2.0', '2.9')
@validation.query_schema(schema.show_query_schema_v210, '2.10', '2.74')
@validation.query_schema(schema.show_query_schema_v275, '2.75')
@validation.query_schema(schema.show_query_v20, '2.0', '2.9')
@validation.query_schema(schema.show_query_v210, '2.10', '2.74')
@validation.query_schema(schema.show_query_v275, '2.75')
@validation.response_body_schema(schema.show_response, '2.0', '2.1')
@validation.response_body_schema(schema.show_response_v22, '2.2')
@wsgi.expected_errors(404)
@@ -174,10 +174,10 @@ class KeypairController(wsgi.Controller):
raise webob.exc.HTTPNotFound(explanation=exc.format_message())
return self._view_builder.show(keypair, key_type=key_type)
@validation.query_schema(schema.index_query_schema_v20, '2.0', '2.9')
@validation.query_schema(schema.index_query_schema_v210, '2.10', '2.34')
@validation.query_schema(schema.index_query_schema_v235, '2.35', '2.74')
@validation.query_schema(schema.index_query_schema_v275, '2.75')
@validation.query_schema(schema.index_query_v20, '2.0', '2.9')
@validation.query_schema(schema.index_query_v210, '2.10', '2.34')
@validation.query_schema(schema.index_query_v235, '2.35', '2.74')
@validation.query_schema(schema.index_query_v275, '2.75')
@validation.response_body_schema(schema.index_response, '2.0', '2.1')
@validation.response_body_schema(schema.index_response_v22, '2.2', '2.34')
@validation.response_body_schema(schema.index_response_v235, '2.35')

View File

@@ -87,7 +87,8 @@ class QuotaClassSetsController(wsgi.Controller):
return []
@wsgi.expected_errors(())
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.1', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response, '2.1', '2.49')
@validation.response_body_schema(schema.show_response_v250, '2.50', '2.56') # noqa: E501
@validation.response_body_schema(schema.show_response_v257, '2.57')

View File

@@ -125,19 +125,21 @@ set_metadata = {
'additionalProperties': False,
}
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
# TODO(stephenfin): Remove additionalProperties in a future API version
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
_aggregate_response = {
'type': 'object',

View File

@@ -65,8 +65,8 @@ delete_query = {
'additionalProperties': True
}
delete_query_275 = copy.deepcopy(delete_query)
delete_query_275['additionalProperties'] = False
delete_query_v275 = copy.deepcopy(delete_query)
delete_query_v275['additionalProperties'] = False
create_response = {
'type': 'object',

View File

@@ -16,7 +16,6 @@ import copy
from nova.api.validation import parameter_types
create = {
'type': 'object',
'properties': {
@@ -50,20 +49,24 @@ create = {
create_v249 = copy.deepcopy(create)
create_v249['properties']['interfaceAttachment']['properties']['tag'] = parameter_types.tag # noqa: E501
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
_interface_attachment = {
'type': 'object',
'properties': {

View File

@@ -14,15 +14,23 @@
import copy
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
detail_query = index_query
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
detail_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
detail_query_v2102 = copy.deepcopy(detail_query)
detail_query_v2102['additionalProperties'] = False
index_response = {
'type': 'object',

View File

@@ -10,19 +10,23 @@
# License for the specific language governing permissions and limitations
# under the License.
# TODO(stephenfin): Remove additionalProperties in a future API version
import copy
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
# TODO(stephenfin): Remove additionalProperties in a future API version
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
_extension_obj = {
'type': 'object',

View File

@@ -16,7 +16,6 @@ import copy
from nova.api.validation import parameter_types
add_tenant_access = {
'type': 'object',
'properties': {
@@ -55,13 +54,15 @@ remove_tenant_access = {
'additionalProperties': False,
}
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
_common_response = {
'type': 'object',
'properties': {

View File

@@ -144,13 +144,15 @@ index_query_v2102['properties']['name'] = parameter_types.multi_params(
index_query_v2102['properties']['sort_key']['items']['enum'].remove(
'rxtx_factor')
# TODO(stephenfin): Remove additionalProperties in a future API version
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
_flavor_basic = {
'type': 'object',
'properties': {

View File

@@ -39,20 +39,24 @@ update.update({
'maxProperties': 1
})
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
index_response = {
'type': 'object',
'properties': {

View File

@@ -10,13 +10,17 @@
# License for the specific language governing permissions and limitations
# under the License.
# TODO(stephenfin): Remove additionalProperties in a future API version
import copy
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
index_response = {
'type': 'object',
'properties': {

View File

@@ -49,6 +49,9 @@ show_query = {
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
index_response = {
'type': 'object',
'properties': {

View File

@@ -10,18 +10,26 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
_instance_usage_audit_log_response = {
'type': 'object',
'properties': {

View File

@@ -12,20 +12,24 @@
import copy
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
_ip_address = {
'type': 'object',
'properties': {

View File

@@ -89,35 +89,33 @@ create_v292['properties']['keypair']['properties']['name'] = (
parameter_types.keypair_name_special_chars_v292)
create_v292['properties']['keypair']['required'] = ['name', 'public_key']
index_query_schema_v20 = {
index_query_v20 = {
'type': 'object',
'properties': {},
'additionalProperties': True
}
index_query_schema_v210 = {
index_query_v210 = {
'type': 'object',
'properties': {
'user_id': parameter_types.multi_params({'type': 'string'})
},
'additionalProperties': True
}
index_query_schema_v235 = copy.deepcopy(index_query_schema_v210)
index_query_schema_v235['properties'].update(
index_query_v235 = copy.deepcopy(index_query_v210)
index_query_v235['properties'].update(
parameter_types.pagination_parameters)
index_query_v275 = copy.deepcopy(index_query_v235)
index_query_v275['additionalProperties'] = False
show_query_schema_v20 = index_query_schema_v20
show_query_schema_v210 = index_query_schema_v210
delete_query_schema_v20 = index_query_schema_v20
delete_query_schema_v210 = index_query_schema_v210
show_query_v20 = index_query_v20
show_query_v210 = index_query_v210
show_query_v275 = copy.deepcopy(show_query_v210)
show_query_v275['additionalProperties'] = False
index_query_schema_v275 = copy.deepcopy(index_query_schema_v235)
index_query_schema_v275['additionalProperties'] = False
show_query_schema_v275 = copy.deepcopy(show_query_schema_v210)
show_query_schema_v275['additionalProperties'] = False
delete_query_schema_v275 = copy.deepcopy(delete_query_schema_v210)
delete_query_schema_v275['additionalProperties'] = False
delete_query_v20 = index_query_v20
delete_query_v210 = index_query_v210
delete_query_v275 = copy.deepcopy(delete_query_v210)
delete_query_v275['additionalProperties'] = False
create_response = {
'type': 'object',

View File

@@ -17,7 +17,6 @@ import copy
from nova.api.validation import parameter_types
from nova.api.validation import response_types
index_query_v20 = {
'type': 'object',
'properties': {

View File

@@ -11,6 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
from nova.api.openstack.compute.schemas import quota_sets
@@ -45,13 +46,15 @@ del update_v257['properties']['quota_class_set']['properties'][
del update_v257['properties']['quota_class_set']['properties'][
'injected_file_path_bytes']
# TODO(stephenfin): Remove additionalProperties in a future API version
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
_quota_response = {
'type': 'object',
'properties': {

View File

@@ -101,15 +101,17 @@ 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
delete_query = copy.deepcopy(show_query)
delete_query_v275 = copy.deepcopy(show_query_v275)
defaults_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
delete_query = copy.deepcopy(show_query)
delete_query_v275 = copy.deepcopy(show_query_v275)
defaults_query_v2102 = copy.deepcopy(defaults_query)
defaults_query_v2102['additionalProperties'] = False
_quota_response = {
'type': 'object',

View File

@@ -94,13 +94,15 @@ index_query = {
'additionalProperties': True
}
# TODO(stephenfin): Remove additionalProperties in a future API version
index_server_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
index_server_query_v2102 = copy.deepcopy(index_server_query)
index_server_query_v2102['additionalProperties'] = False
# TODO(stephenfin): Remove additionalProperties in a future API version
add_security_group = {
'type': 'object',

View File

@@ -10,12 +10,15 @@
# License for the specific language governing permissions and limitations
# under the License.
# TODO(stephenfin): Remove additionalProperties in a future API version
import copy
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
# NOTE(stephenfin): We could define all available response types for the
# various virt drivers, but we'd need to be able to do this (accurately) for

View File

@@ -99,6 +99,9 @@ show_query = {
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
_server_group_response = {
'type': 'object',
'properties': {

View File

@@ -51,20 +51,23 @@ update_all = {
'additionalProperties': False,
}
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
# TODO(stephenfin): Remove additionalProperties in a future API version
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
index_response = {
'type': 'object',
'properties': {

View File

@@ -17,6 +17,23 @@ import copy
from nova.api.validation import parameter_types
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
force_complete = {
'type': 'object',
@@ -29,20 +46,6 @@ force_complete = {
'additionalProperties': False,
}
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
# TODO(stephenfin): Remove additionalProperties in a future API version
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
force_complete_response = {
'type': 'null',
}

View File

@@ -10,13 +10,17 @@
# License for the specific language governing permissions and limitations
# under the License.
# TODO(stephenfin): Remove additionalProperties in a future API version
import copy
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
index_response = {
'type': 'object',
'properties': {

View File

@@ -10,6 +10,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from nova.api.validation import parameter_types
from nova.objects import instance
@@ -32,20 +34,24 @@ update = {
"type": "null",
}
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
# TODO(stephenfin): Remove additionalProperties in a future API version
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
show_response = {'type': 'null'}
index_response = {

View File

@@ -10,12 +10,15 @@
# License for the specific language governing permissions and limitations
# under the License.
# TODO(stephenfin): Remove additionalProperties in a future API version
query_params_v21 = {
import copy
index_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
index_query_v2102 = copy.deepcopy(index_query)
index_query_v2102['additionalProperties'] = False
index_response = {
'type': 'object',

View File

@@ -619,7 +619,7 @@ VALID_SORT_KEYS_V275['enum'] = list(
set(VALID_SORT_KEYS_V273["enum"]) - set(SERVER_LIST_IGNORE_SORT_KEY_V273)
)
query_params_v21 = {
index_query = {
'type': 'object',
'properties': {
'user_id': parameter_types.common_query_param,
@@ -692,38 +692,38 @@ query_params_v21 = {
# Update the joined-table fields to the list so it will not be
# stripped in later process, thus can be handled later in api
# to raise HTTP 400.
query_params_v21['properties'].update(
index_query['properties'].update(
JOINED_TABLE_QUERY_PARAMS_SERVERS)
query_params_v21['properties'].update(
index_query['properties'].update(
parameter_types.pagination_parameters)
query_params_v226 = copy.deepcopy(query_params_v21)
query_params_v226['properties'].update({
index_query_v226 = copy.deepcopy(index_query)
index_query_v226['properties'].update({
'tags': parameter_types.common_query_regex_param,
'tags-any': parameter_types.common_query_regex_param,
'not-tags': parameter_types.common_query_regex_param,
'not-tags-any': parameter_types.common_query_regex_param,
})
query_params_v266 = copy.deepcopy(query_params_v226)
query_params_v266['properties'].update({
index_query_v266 = copy.deepcopy(index_query_v226)
index_query_v266['properties'].update({
'changes-before': parameter_types.multi_params(
{'type': 'string', 'format': 'date-time'}
),
})
query_params_v273 = copy.deepcopy(query_params_v266)
query_params_v273['properties'].update({
index_query_v273 = copy.deepcopy(index_query_v266)
index_query_v273['properties'].update({
'sort_key': parameter_types.multi_params(VALID_SORT_KEYS_V273),
'locked': parameter_types.common_query_param,
})
query_params_v275 = copy.deepcopy(query_params_v273)
query_params_v275['properties'].update({
index_query_v275 = copy.deepcopy(index_query_v273)
index_query_v275['properties'].update({
'sort_key': parameter_types.multi_params(VALID_SORT_KEYS_V275),
})
query_params_v275['additionalProperties'] = False
index_query_v275['additionalProperties'] = False
show_query = {
'type': 'object',
@@ -731,6 +731,9 @@ show_query = {
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
_server_status = {
'type': 'string',
'enum': [

View File

@@ -68,7 +68,6 @@ update_v253 = {
'additionalProperties': False
}
index_query = {
'type': 'object',
'properties': {

View File

@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from nova.api.validation import parameter_types
create = {
@@ -52,6 +54,9 @@ show_query = {
'additionalProperties': True
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
_snapshot_response = {
'type': 'object',
'properties': {

View File

@@ -10,14 +10,15 @@
# License for the specific language governing permissions and limitations
# under the License.
# TODO(stephenfin): Remove additionalProperties in a future API version
# NOTE(stephenfin): We would like to change additionalProperties to false, but
# these APIs are unversioned so we can't
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
# TODO(stephenfin): Remove additionalProperties in a future API version
multi_query = {
'type': 'object',
'properties': {},

View File

@@ -14,6 +14,33 @@ import copy
from nova.api.validation import parameter_types
index_query = {
'type': 'object',
'properties': {
'limit': parameter_types.multi_params(
parameter_types.non_negative_integer),
'offset': parameter_types.multi_params(
parameter_types.non_negative_integer)
},
# NOTE(gmann): This is kept True to keep backward compatibility.
# As of now Schema validation stripped out the additional parameters and
# does not raise 400. In microversion 2.75, we have blocked the additional
# parameters.
'additionalProperties': True
}
index_query_v275 = copy.deepcopy(index_query)
index_query_v275['additionalProperties'] = False
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
create = {
'type': 'object',
'properties': {
@@ -76,31 +103,6 @@ update_v285 = {
'additionalProperties': False,
}
index_query = {
'type': 'object',
'properties': {
'limit': parameter_types.multi_params(
parameter_types.non_negative_integer),
'offset': parameter_types.multi_params(
parameter_types.non_negative_integer)
},
# NOTE(gmann): This is kept True to keep backward compatibility.
# As of now Schema validation stripped out the additional parameters and
# does not raise 400. In microversion 2.75, we have blocked the additional
# parameters.
'additionalProperties': True
}
index_query_v275 = copy.deepcopy(index_query)
index_query_v275['additionalProperties'] = False
# TODO(stephenfin): Remove additionalProperties in a future API version
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True,
}
_volume_attachment_response = {
'type': 'object',
'properties': {

View File

@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from nova.api.validation import parameter_types
from nova.api.validation import response_types
@@ -58,14 +60,23 @@ index_query = {
'additionalProperties': True
}
index_query_v275 = copy.deepcopy(index_query)
index_query_v275['additionalProperties'] = False
detail_query = index_query
detail_query_v2102 = copy.deepcopy(detail_query)
detail_query_v2102['additionalProperties'] = False
show_query = {
'type': 'object',
'properties': {},
'additionalProperties': True
}
show_query_v2102 = copy.deepcopy(show_query)
show_query_v2102['additionalProperties'] = False
_volume_response = {
'type': 'object',
'properties': {

View File

@@ -366,7 +366,8 @@ class ServerSecurityGroupController(
):
@wsgi.expected_errors(404)
@validation.query_schema(schema.index_server_query)
@validation.query_schema(schema.index_server_query, '2.1', '2.101')
@validation.query_schema(schema.index_server_query_v2102, '2.102')
@validation.response_body_schema(schema.index_server_response)
def index(self, req, server_id):
"""Returns a list of security groups for the given instance."""

View File

@@ -35,7 +35,8 @@ class ServerDiagnosticsController(wsgi.Controller):
self.compute_api = compute.API()
@wsgi.expected_errors((400, 404, 409, 501))
@validation.query_schema(schema.index_query)
@validation.query_schema(schema.index_query, '2.1', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response, '2.1', '2.47')
@validation.response_body_schema(schema.index_response_v248, '2.48')
def index(self, req, server_id):

View File

@@ -131,7 +131,8 @@ class ServerGroupController(wsgi.Controller):
return server_group
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.1', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response, '2.1', '2.12')
@validation.response_body_schema(schema.show_response_v213, '2.13', '2.14')
@validation.response_body_schema(schema.show_response_v215, '2.15', '2.63')

View File

@@ -49,7 +49,8 @@ class ServerMetadataController(wsgi.Controller):
return meta_dict
@wsgi.expected_errors(404)
@validation.query_schema(schema.index_query)
@validation.query_schema(schema.index_query, '2.1', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response)
def index(self, req, server_id):
"""Returns the list of metadata for a given instance."""
@@ -126,7 +127,8 @@ class ServerMetadataController(wsgi.Controller):
state_error, 'update metadata', server.uuid)
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.1', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response)
def show(self, req, server_id, id):
"""Return a single metadata item."""

View File

@@ -119,7 +119,8 @@ class ServerMigrationsController(wsgi.Controller):
@wsgi.api_version("2.23")
@wsgi.expected_errors(404)
@validation.query_schema(schema.index_query)
@validation.query_schema(schema.index_query, '2.23', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response_v223, '2.23', '2.58') # noqa: E501
@validation.response_body_schema(schema.index_response_v259, '2.59', '2.79') # noqa: E501
@validation.response_body_schema(schema.index_response_v280, '2.80')
@@ -148,7 +149,8 @@ class ServerMigrationsController(wsgi.Controller):
@wsgi.api_version("2.23")
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.23', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response_v223, '2.23', '2.58')
@validation.response_body_schema(schema.show_response_v259, '2.59', '2.79') # noqa: E501
@validation.response_body_schema(schema.show_response_v280, '2.80')

View File

@@ -33,7 +33,8 @@ class ServerPasswordController(wsgi.Controller):
self.compute_api = compute.API()
@wsgi.expected_errors(404)
@validation.query_schema(schema.index_query)
@validation.query_schema(schema.index_query, '2.1', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response)
def index(self, req, server_id):
context = req.environ['nova.context']

View File

@@ -28,7 +28,8 @@ class ServerTopologyController(wsgi.Controller):
@wsgi.api_version("2.78")
@wsgi.expected_errors(404)
@validation.query_schema(schema.query_params_v21)
@validation.query_schema(schema.index_query, '2.78', '2.101')
@validation.query_schema(schema.index_query_v2102, '2.102')
@validation.response_body_schema(schema.index_response)
def index(self, req, server_id):
context = req.environ["nova.context"]

View File

@@ -114,11 +114,11 @@ class ServersController(wsgi.Controller):
self.compute_api = compute.API()
@wsgi.expected_errors((400, 403))
@validation.query_schema(schema.query_params_v21, '2.1', '2.25')
@validation.query_schema(schema.query_params_v226, '2.26', '2.65')
@validation.query_schema(schema.query_params_v266, '2.66', '2.72')
@validation.query_schema(schema.query_params_v273, '2.73', '2.74')
@validation.query_schema(schema.query_params_v275, '2.75')
@validation.query_schema(schema.index_query, '2.1', '2.25')
@validation.query_schema(schema.index_query_v226, '2.26', '2.65')
@validation.query_schema(schema.index_query_v266, '2.66', '2.72')
@validation.query_schema(schema.index_query_v273, '2.73', '2.74')
@validation.query_schema(schema.index_query_v275, '2.75')
@validation.response_body_schema(schema.index_response, '2.1', '2.68')
@validation.response_body_schema(schema.index_response_v269, '2.69')
def index(self, req):
@@ -132,11 +132,11 @@ class ServersController(wsgi.Controller):
return servers
@wsgi.expected_errors((400, 403))
@validation.query_schema(schema.query_params_v21, '2.1', '2.25')
@validation.query_schema(schema.query_params_v226, '2.26', '2.65')
@validation.query_schema(schema.query_params_v266, '2.66', '2.72')
@validation.query_schema(schema.query_params_v273, '2.73', '2.74')
@validation.query_schema(schema.query_params_v275, '2.75')
@validation.query_schema(schema.index_query, '2.1', '2.25')
@validation.query_schema(schema.index_query_v226, '2.26', '2.65')
@validation.query_schema(schema.index_query_v266, '2.66', '2.72')
@validation.query_schema(schema.index_query_v273, '2.73', '2.74')
@validation.query_schema(schema.index_query_v275, '2.75')
@validation.response_body_schema(schema.detail_response, '2.1', '2.2')
@validation.response_body_schema(schema.detail_response_v23, '2.3', '2.8')
@validation.response_body_schema(schema.detail_response_v29, '2.9', '2.15')
@@ -474,7 +474,8 @@ class ServersController(wsgi.Controller):
return objects.NetworkRequestList(objects=networks)
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
@validation.query_schema(schema.show_query, '2.1', '2.101')
@validation.query_schema(schema.show_query_v2102, '2.102')
@validation.response_body_schema(schema.show_response, '2.0', '2.2')
@validation.response_body_schema(schema.show_response_v23, '2.3', '2.8')
@validation.response_body_schema(schema.show_response_v29, '2.9', '2.15')

View File

@@ -106,17 +106,14 @@ class AggregateTestCaseV21(test.NoDBTestCase):
set_metadata = 'self.controller._set_metadata'
bad_request = exception.ValidationError
def _set_up(self):
def setUp(self):
super().setUp()
self.controller = aggregates_v21.AggregateController()
self.req = fakes.HTTPRequest.blank('/v2.1/os-aggregates',
use_admin_context=True)
self.user_req = fakes.HTTPRequest.blank('/v2.1/os-aggregates')
self.context = self.req.environ['nova.context']
def setUp(self):
super(AggregateTestCaseV21, self).setUp()
self._set_up()
def test_index(self):
def _list_aggregates(context):
if context is None:
@@ -130,6 +127,15 @@ class AggregateTestCaseV21(test.NoDBTestCase):
self._assert_agg_data(AGGREGATE_LIST, _make_agg_list(result))
self.assertTrue(mock_list.called)
def test_index_invalid_query_params(self):
req = fakes.HTTPRequest.blank(
'/v2/os-aggregates?unknown=1',
use_admin_context=True,
version='2.102')
self.assertRaises(
exception.ValidationError, self.controller.index, req
)
def test_create(self):
with mock.patch.object(self.controller.api, 'create_aggregate',
return_value=AGGREGATE) as mock_create:
@@ -293,6 +299,15 @@ class AggregateTestCaseV21(test.NoDBTestCase):
self._assert_agg_data(AGGREGATE, _make_agg_obj(aggregate))
mock_get.assert_called_once_with(self.context, '1')
def test_show_invalid_query_params(self):
req = fakes.HTTPRequest.blank(
'/v2/os-aggregates/1?unknown=1',
use_admin_context=True,
version='2.102')
self.assertRaises(
exception.ValidationError, self.controller.show, req, '1'
)
def test_show_with_bad_aggregate(self):
side_effect = exception.AggregateNotFound(aggregate_id='2')
with mock.patch.object(self.controller.api, 'get_aggregate',

View File

@@ -184,6 +184,29 @@ class InterfaceAttachTestsV21(test.NoDBTestCase):
self.attachments.show, self.req, FAKE_UUID1,
FAKE_PORT_ID1)
def test_show_invalid_query_params(self):
req = fakes.HTTPRequest.blank(
f'/servers/{FAKE_UUID1}/os-interface/{FAKE_PORT_ID1}?invalid=1',
use_admin_context=True, version='2.102')
self.assertRaises(
exception.ValidationError,
self.attachments.show,
req,
FAKE_UUID1,
FAKE_PORT_ID1,
)
def test_index_invalid_query_params(self):
req = fakes.HTTPRequest.blank(
f'/servers/{FAKE_UUID1}/os-interface?invalid=1',
use_admin_context=True, version='2.102')
self.assertRaises(
exception.ValidationError,
self.attachments.index,
req,
FAKE_UUID1,
)
def test_delete(self):
self.stub_out('nova.compute.api.API.detach_interface',
fake_detach_interface)

View File

@@ -128,6 +128,14 @@ class AvailabilityZoneApiTestV21(test.NoDBTestCase):
self.assertFalse(zones[1]['zoneState']['available'])
self.assertIsNone(zones[1]['hosts'])
def test_availability_zone_index_invalid_query_params(self):
req = fakes.HTTPRequest.blank('?invalid=1', version='2.102')
self.assertRaises(
exception.ValidationError,
self.controller.index,
req,
)
def test_availability_zone_detail(self):
req = fakes.HTTPRequest.blank('')
resp_dict = self.controller.detail(req)
@@ -191,6 +199,14 @@ class AvailabilityZoneApiTestV21(test.NoDBTestCase):
self.assertThat(resp_dict,
matchers.DictMatches(expected_response))
def test_availability_zone_detail_invalid_query_params(self):
req = fakes.HTTPRequest.blank('?invalid=1', version='2.102')
self.assertRaises(
exception.ValidationError,
self.controller.index,
req,
)
class ServersControllerCreateTestV21(test.TestCase):
base_url = '/v2.1'

View File

@@ -15,6 +15,7 @@
import webob
from nova.api.openstack.compute import extension_info
from nova import exception
from nova import test
from nova.tests.unit.api.openstack import fakes
@@ -25,7 +26,24 @@ class ExtensionInfoV21Test(test.NoDBTestCase):
super(ExtensionInfoV21Test, self).setUp()
self.controller = extension_info.ExtensionInfoController()
def test_extension_info_show_servers_not_present(self):
def test_extension_info_index_invalid_query_params(self):
req = fakes.HTTPRequest.blank('?invalid=1', version='2.102')
self.assertRaises(
exception.ValidationError,
self.controller.index,
req,
)
def test_extension_info_show_extension_not_present(self):
req = fakes.HTTPRequest.blank('/extensions/servers')
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
req, 'servers')
def test_extension_info_show_invalid_query_params(self):
req = fakes.HTTPRequest.blank('?invalid=1', version='2.102')
self.assertRaises(
exception.ValidationError,
self.controller.show,
req,
'servers',
)

View File

@@ -839,8 +839,7 @@ class FlavorsTestV275(FlavorsTestV261):
@mock.patch('nova.objects.Flavor.get_by_flavor_id')
def test_show_flavor_default_swap_value_old_version(self, mock_get):
mock_get.return_value = self.FLAVOR_WITH_NO_SWAP
req = fakes.HTTPRequestV21.blank(
'/flavors/detail?limit=1', version='2.74')
req = fakes.HTTPRequestV21.blank('/flavors/1', version='2.74')
response = self.controller.show(req, 1)
response_list = response["flavor"]
self.assertEqual(response_list['swap'], "")
@@ -859,7 +858,7 @@ class FlavorsTestV275(FlavorsTestV261):
def test_show_flavor_default_swap_value(self, mock_get):
mock_get.return_value = self.FLAVOR_WITH_NO_SWAP
req = fakes.HTTPRequestV21.blank(
'/flavors/detail?limit=1', version=self.microversion)
'/flavors/1', version=self.microversion)
response = self.controller.show(req, 1)
response_list = response["flavor"]
self.assertEqual(response_list['swap'], 0)
@@ -943,6 +942,33 @@ class FlavorsTestV2102(FlavorsTestV275):
self.omit_legacy_fields = False
super().test_list_detail_flavors_with_additional_filter_old_version()
def test_list_flavor_invalid_query_params(self):
req = fakes.HTTPRequest.blank(
'/flavors?unknown=1',
use_admin_context=True,
version='2.102')
self.assertRaises(
exception.ValidationError, self.controller.index, req
)
def test_detail_flavor_invalid_query_params(self):
req = fakes.HTTPRequest.blank(
'/flavors/detail?unknown=1',
use_admin_context=True,
version='2.102')
self.assertRaises(
exception.ValidationError, self.controller.detail, req
)
def test_show_flavor_invalid_query_params(self):
req = fakes.HTTPRequest.blank(
'/flavors/123?unknown=1',
use_admin_context=True,
version='2.102')
self.assertRaises(
exception.ValidationError, self.controller.show, req, '123'
)
class DisabledFlavorsWithRealDBTestV21(test.TestCase):
"""Tests that disabled flavors should not be shown nor listed."""

View File

@@ -8,4 +8,6 @@ features:
In addition, the ``rxtx_factor`` and ``OS-FLV-DISABLED:disabled`` fields
have been removed from all flavors responses, while the ``rxtx_factor``
field can no longer be provided when creating a server.
field can no longer be provided when creating a server. Finally, all APIs
now reject unknown query string parameters with a HTTP 400 (Bad Request)
error, building upon work first started in microversion 2.75.