Merge "api: Only apply "soft" additionalProperties validation to requests"

This commit is contained in:
Zuul
2025-08-27 06:04:36 +00:00
committed by Gerrit Code Review
4 changed files with 56 additions and 23 deletions

View File

@@ -13,10 +13,14 @@
# under the License.
import re
import typing as ty
from nova import exception
from nova.i18n import _
if ty.TYPE_CHECKING:
from nova.api.openstack import wsgi
# Define the minimum and maximum version of the API across all of the
# REST API. The format of the version is:
# X.Y where:
@@ -306,7 +310,7 @@ def max_api_version():
return APIVersionRequest(_MAX_API_VERSION)
def is_supported(req, version):
def is_supported(req: 'wsgi.Request', version: str) -> bool:
"""Check if API request version satisfies version restrictions.
:param req: request object
@@ -316,6 +320,10 @@ def is_supported(req, version):
:returns: True if request satisfies minimal and maximum API version
requirements. False in other case.
"""
# TODO(stephenfin): This should be an error since it highlights bugs in
# either our middleware or (more likely) an incomplete test
if req.is_legacy_v2(): # legacy requests will not pass microversion info
return False
return req.api_version_request >= APIVersionRequest(version)

View File

@@ -102,8 +102,17 @@ class Schemas:
return None
def _schema_validation_helper(schema, target, min_version, max_version,
args, kwargs, is_body=True):
def _schema_validation_helper(
schema,
target,
min_version,
max_version,
args,
kwargs,
*,
relax_additional_properties=False,
is_body=True,
):
"""A helper method to execute JSON-Schema Validation.
This method checks the request version whether matches the specified max
@@ -115,17 +124,20 @@ def _schema_validation_helper(schema, target, min_version, max_version,
:param schema: A dict, the JSON-Schema is used to validate the target.
:param target: A dict, the target is validated by the JSON-Schema.
:param min_version: A string of two numerals. X.Y indicating the minimum
version of the JSON-Schema to validate against.
version of the JSON-Schema to validate against.
:param max_version: A string of two numerals. X.Y indicating the maximum
version of the JSON-Schema to validate against.
version of the JSON-Schema to validate against.
:param args: Positional arguments which passed into original method.
:param kwargs: Keyword arguments which passed into original method.
:param relax_additional_properties: Whether to enable soft
additionalProperties validation. This is only enabled for request
validation.
:param is_body: A boolean. Indicating whether the target is HTTP request
body or not.
body or not.
:returns: A boolean. `True` if and only if the version range matches the
request AND the schema is successfully validated. `False` if the
version range does not match the request and no validation is
performed.
request AND the schema is successfully validated. `False` if the
version range does not match the request and no validation is
performed.
:raises: ValidationError, when the validation fails.
"""
min_ver = api_version_request.APIVersionRequest(min_version)
@@ -144,6 +156,8 @@ def _schema_validation_helper(schema, target, min_version, max_version,
legacy_v2 = args[1].is_legacy_v2()
if legacy_v2:
relax_additional_properties = relax_additional_properties and legacy_v2
# NOTE: For v2.0 compatible API, here should work like
# client | schema min_version | schema
# -----------+--------------------+--------
@@ -152,7 +166,8 @@ def _schema_validation_helper(schema, target, min_version, max_version,
# legacy_v2 | 2.1+ | don't
if min_version is None or min_version == '2.0':
schema_validator = validators._SchemaValidator(
schema, legacy_v2, is_body)
schema, relax_additional_properties, is_body
)
schema_validator.validate(target)
return True
elif ver.matches(min_ver, max_ver):
@@ -160,8 +175,7 @@ def _schema_validation_helper(schema, target, min_version, max_version,
# the version range specified. Note that if both min
# and max are not specified the validator will always
# be run.
schema_validator = validators._SchemaValidator(
schema, legacy_v2, is_body)
schema_validator = validators._SchemaValidator(schema, False, is_body)
schema_validator.validate(target)
return True
@@ -197,7 +211,8 @@ def schema(
min_version,
max_version,
args,
kwargs
kwargs,
relax_additional_properties=True,
)
return func(*args, **kwargs)
@@ -349,10 +364,16 @@ def query_schema(request_query_schema, min_version=None,
msg = _('Query string is not UTF-8 encoded')
raise exception.ValidationError(msg)
if _schema_validation_helper(request_query_schema,
query_dict,
min_version, max_version,
args, kwargs, is_body=False):
if _schema_validation_helper(
request_query_schema,
query_dict,
min_version,
max_version,
args,
kwargs,
relax_additional_properties=True,
is_body=False,
):
# NOTE(alex_xu): The additional query parameters were stripped
# out when `additionalProperties=True`. This is for backward
# compatible with v2.1 API and legacy v2 API. But it makes the

View File

@@ -193,6 +193,10 @@ def _validate_az_name(instance):
raise exception.InvalidName(reason=regex.reason)
# NOTE(stephenfin): Be careful with this method: it doesn't work with oneOf. If
# you have multiple schemas, this method will delete properties that are not
# allowed against earlier subschemas even if they're allowed (or even required)
# by later subschemas.
def _soft_validate_additional_properties(validator,
additional_properties_value,
instance,

View File

@@ -21,7 +21,6 @@ from unittest import mock
from oslo_utils.fixture import uuidsentinel
from webob import exc
from nova.api.openstack import api_version_request
from nova.api.openstack.compute import aggregates as aggregates_v21
from nova.compute import api as compute_api
from nova import context
@@ -731,15 +730,16 @@ class AggregateTestCaseV21(test.NoDBTestCase):
# top-level dict, so we need to put availability_zone at the top also
agg['availability_zone'] = 'nova'
avr_v240 = api_version_request.APIVersionRequest("2.40")
avr_v241 = api_version_request.APIVersionRequest("2.41")
req = mock.MagicMock(api_version_request=avr_v241)
req = fakes.HTTPRequest.blank('/v2/os-aggregates',
use_admin_context=True,
version='2.41')
marshalled_agg = self.controller._marshall_aggregate(req, agg_obj)
self.assertEqual(agg, marshalled_agg['aggregate'])
req = mock.MagicMock(api_version_request=avr_v240)
req = fakes.HTTPRequest.blank('/v2/os-aggregates',
use_admin_context=True,
version='2.40')
marshalled_agg = self.controller._marshall_aggregate(req, agg_obj)
# UUID isn't in microversion 2.40 and before