Coverage for nova/api/validation/__init__.py: 94%
98 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
1# Copyright 2013 NEC Corporation. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14"""
15Request Body validating middleware.
17"""
19import functools
20import re
21import typing as ty
23from oslo_log import log as logging
24from oslo_serialization import jsonutils
25import webob
27from nova.api.openstack import api_version_request as api_version
28from nova.api.openstack import wsgi
29from nova.api.validation import validators
30import nova.conf
31from nova import exception
32from nova.i18n import _
34CONF = nova.conf.CONF
35LOG = logging.getLogger(__name__)
38def _schema_validation_helper(schema, target, min_version, max_version,
39 args, kwargs, is_body=True):
40 """A helper method to execute JSON-Schema Validation.
42 This method checks the request version whether matches the specified max
43 version and min_version. It also takes a care of legacy v2 request.
45 If the version range matches the request, we validate the schema against
46 the target and a failure will result in a ValidationError being raised.
48 :param schema: A dict, the JSON-Schema is used to validate the target.
49 :param target: A dict, the target is validated by the JSON-Schema.
50 :param min_version: A string of two numerals. X.Y indicating the minimum
51 version of the JSON-Schema to validate against.
52 :param max_version: A string of two numerals. X.Y indicating the maximum
53 version of the JSON-Schema to validate against.
54 :param args: Positional arguments which passed into original method.
55 :param kwargs: Keyword arguments which passed into original method.
56 :param is_body: A boolean. Indicating whether the target is HTTP request
57 body or not.
58 :returns: A boolean. `True` if and only if the version range matches the
59 request AND the schema is successfully validated. `False` if the
60 version range does not match the request and no validation is
61 performed.
62 :raises: ValidationError, when the validation fails.
63 """
64 min_ver = api_version.APIVersionRequest(min_version)
65 max_ver = api_version.APIVersionRequest(max_version)
67 # The request object is always the second argument.
68 # However numerous unittests pass in the request object
69 # via kwargs instead so we handle that as well.
70 # TODO(cyeoh): cleanup unittests so we don't have to
71 # to do this
72 if 'req' in kwargs:
73 ver = kwargs['req'].api_version_request
74 legacy_v2 = kwargs['req'].is_legacy_v2()
75 else:
76 ver = args[1].api_version_request
77 legacy_v2 = args[1].is_legacy_v2()
79 if legacy_v2:
80 # NOTE: For v2.0 compatible API, here should work like
81 # client | schema min_version | schema
82 # -----------+--------------------+--------
83 # legacy_v2 | None | work
84 # legacy_v2 | 2.0 | work
85 # legacy_v2 | 2.1+ | don't
86 if min_version is None or min_version == '2.0':
87 schema_validator = validators._SchemaValidator(
88 schema, legacy_v2, is_body)
89 schema_validator.validate(target)
90 return True
91 elif ver.matches(min_ver, max_ver):
92 # Only validate against the schema if it lies within
93 # the version range specified. Note that if both min
94 # and max are not specified the validator will always
95 # be run.
96 schema_validator = validators._SchemaValidator(
97 schema, legacy_v2, is_body)
98 schema_validator.validate(target)
99 return True
101 return False
104# TODO(stephenfin): This decorator should take the five schemas we validate:
105# request body, request query string, request headers, response body, and
106# response headers. As things stand, we're going to need five separate
107# decorators.
108def schema(
109 request_body_schema: ty.Dict[str, ty.Any],
110 min_version: ty.Optional[str] = None,
111 max_version: ty.Optional[str] = None,
112) -> ty.Dict[str, ty.Any]:
113 """Register a schema to validate request body.
115 Registered schema will be used for validating request body just before
116 API method executing.
118 :param dict request_body_schema: a schema to validate request body
119 :param dict response_body_schema: a schema to validate response body
120 :param str min_version: Minimum API microversion that the schema applies to
121 :param str max_version: Maximum API microversion that the schema applies to
122 """
124 def add_validator(func):
125 @functools.wraps(func)
126 def wrapper(*args, **kwargs):
127 _schema_validation_helper(
128 request_body_schema,
129 kwargs['body'],
130 min_version,
131 max_version,
132 args,
133 kwargs
134 )
135 return func(*args, **kwargs)
137 wrapper._request_schema = request_body_schema
139 return wrapper
141 return add_validator
144def response_body_schema(
145 response_body_schema: ty.Dict[str, ty.Any],
146 min_version: ty.Optional[str] = None,
147 max_version: ty.Optional[str] = None,
148):
149 """Register a schema to validate response body.
151 Registered schema will be used for validating response body just after
152 API method executing.
154 :param dict response_body_schema: a schema to validate response body
155 :param str min_version: Minimum API microversion that the schema applies to
156 :param str max_version: Maximum API microversion that the schema applies to
157 """
159 def add_validator(func):
160 @functools.wraps(func)
161 def wrapper(*args, **kwargs):
162 response = func(*args, **kwargs)
164 if CONF.api.response_validation == 'ignore': 164 ↛ 167line 164 didn't jump to line 167 because the condition on line 164 was never true
165 # don't waste our time checking anything if we're ignoring
166 # schema errors
167 return response
169 # NOTE(stephenfin): If our response is an object, we need to
170 # serializer and deserialize to convert e.g. date-time to strings
171 if isinstance(response, wsgi.ResponseObject):
172 serializer = wsgi.JSONDictSerializer()
173 _body = serializer.serialize(response.obj)
174 # TODO(stephenfin): We should replace all instances of this with
175 # wsgi.ResponseObject
176 elif isinstance(response, webob.Response):
177 _body = response.body
178 else:
179 serializer = wsgi.JSONDictSerializer()
180 _body = serializer.serialize(response)
182 if _body == b'':
183 body = None
184 else:
185 body = jsonutils.loads(_body)
187 try:
188 _schema_validation_helper(
189 response_body_schema,
190 body,
191 min_version,
192 max_version,
193 args,
194 kwargs
195 )
196 except exception.ValidationError:
197 if CONF.api.response_validation == 'warn':
198 LOG.exception('Schema failed to validate')
199 else:
200 raise
201 return response
203 wrapper._response_schema = response_body_schema
205 return wrapper
207 return add_validator
210def _strip_additional_query_parameters(schema, req):
211 """Strip the additional properties from the req.GET.
213 This helper method assumes the JSON-Schema is only described as a dict
214 without nesting. This method should be called after query parameters pass
215 the JSON-Schema validation. It also means this method only can be called
216 after _schema_validation_helper return `True`.
217 """
218 additional_properties = schema.get('additionalProperties', True)
219 pattern_regexes = []
221 patterns = schema.get('patternProperties', None)
222 if patterns:
223 for regex in patterns:
224 pattern_regexes.append(re.compile(regex))
226 if additional_properties:
227 # `req.GET.keys` will return duplicated keys for multiple values
228 # parameters. To get rid of duplicated keys, convert it to set.
229 for param in set(req.GET.keys()):
230 if param not in schema['properties'].keys():
231 # keys that can match the patternProperties will be
232 # retained and handle latter.
233 if not (list(regex for regex in pattern_regexes if
234 regex.match(param))):
235 del req.GET[param]
238def query_schema(query_params_schema, min_version=None,
239 max_version=None):
240 """Register a schema to validate request query parameters.
242 Registered schema will be used for validating request query params just
243 before API method executing.
245 :param query_params_schema: A dict, the JSON-Schema for validating the
246 query parameters.
247 :param min_version: A string of two numerals. X.Y indicating the minimum
248 version of the JSON-Schema to validate against.
249 :param max_version: A string of two numerals. X.Y indicating the maximum
250 version of the JSON-Schema against to.
251 """
253 def add_validator(func):
254 @functools.wraps(func)
255 def wrapper(*args, **kwargs):
256 # The request object is always the second argument.
257 # However numerous unittests pass in the request object
258 # via kwargs instead so we handle that as well.
259 # TODO(cyeoh): cleanup unittests so we don't have to
260 # to do this
261 if 'req' in kwargs:
262 req = kwargs['req']
263 else:
264 req = args[1]
266 # NOTE(Kevin_Zheng): The webob package throws UnicodeError when
267 # param cannot be decoded. Catch this and raise HTTP 400.
269 try:
270 query_dict = req.GET.dict_of_lists()
271 except UnicodeDecodeError:
272 msg = _('Query string is not UTF-8 encoded')
273 raise exception.ValidationError(msg)
275 if _schema_validation_helper(query_params_schema,
276 query_dict,
277 min_version, max_version,
278 args, kwargs, is_body=False):
279 # NOTE(alex_xu): The additional query parameters were stripped
280 # out when `additionalProperties=True`. This is for backward
281 # compatible with v2.1 API and legacy v2 API. But it makes the
282 # system more safe for no more unexpected parameters pass down
283 # to the system. In microversion 2.75, we have blocked all of
284 # those additional parameters.
285 _strip_additional_query_parameters(query_params_schema, req)
286 return func(*args, **kwargs)
288 wrapper._query_schema = query_params_schema
290 return wrapper
292 return add_validator