Coverage for manila/api/validation/validators.py: 54%
110 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
13"""Internal implementation of request/response validating middleware."""
15import re
17import jsonschema
18from jsonschema import exceptions as jsonschema_exc
19from oslo_utils import timeutils
20from oslo_utils import uuidutils
21import webob.exc
23from manila import exception
24from manila.i18n import _
25from manila import utils
28def _soft_validate_additional_properties(
29 validator,
30 additional_properties_value,
31 param_value,
32 schema,
33):
34 """Validator function.
36 If there are not any properties on the param_value that are not specified
37 in the schema, this will return without any effect. If there are any such
38 extra properties, they will be handled as follows:
40 - if the validator passed to the method is not of type "object", this
41 method will return without any effect.
42 - if the 'additional_properties_value' parameter is True, this method will
43 return without any effect.
44 - if the schema has an additionalProperties value of True, the extra
45 properties on the param_value will not be touched.
46 - if the schema has an additionalProperties value of False and there
47 aren't patternProperties specified, the extra properties will be stripped
48 from the param_value.
49 - if the schema has an additionalProperties value of False and there
50 are patternProperties specified, the extra properties will not be
51 touched and raise validation error if pattern doesn't match.
52 """
53 if not (
54 validator.is_type(param_value, "object") or additional_properties_value
55 ):
56 return
58 properties = schema.get('properties', {})
59 patterns = '|'.join(schema.get('patternProperties', {}))
60 extra_properties = set()
61 for prop in param_value:
62 if prop not in properties:
63 if patterns:
64 if not re.search(patterns, prop):
65 extra_properties.add(prop)
66 else:
67 extra_properties.add(prop)
69 if not extra_properties:
70 return
72 if patterns:
73 error = 'Additional properties are not allowed (%s %s unexpected)'
74 if len(extra_properties) == 1:
75 verb = 'was'
76 else:
77 verb = 'were'
78 yield jsonschema_exc.ValidationError(
79 error
80 % (', '.join(repr(extra) for extra in extra_properties), verb)
81 )
82 else:
83 for prop in extra_properties:
84 del param_value[prop]
87def _validate_string_length(
88 value,
89 entity_name,
90 mandatory=False,
91 min_length=0,
92 max_length=None,
93 remove_whitespaces=False,
94):
95 """Check the length of specified string.
97 :param value: the value of the string
98 :param entity_name: the name of the string
99 :mandatory: string is mandatory or not
100 :param min_length: the min_length of the string
101 :param max_length: the max_length of the string
102 :param remove_whitespaces: True if trimming whitespaces is needed else
103 False
104 """
105 if not mandatory and not value:
106 return True
108 if mandatory and not value:
109 msg = _("The '%s' can not be None.") % entity_name
110 raise webob.exc.HTTPBadRequest(explanation=msg)
112 if remove_whitespaces:
113 value = value.strip()
115 utils.check_string_length(
116 value, entity_name, min_length=min_length, max_length=max_length
117 )
120class FormatChecker(jsonschema.FormatChecker):
121 """A FormatChecker can output the message from cause exception
123 We need understandable validation errors messages for users. When a
124 custom checker has an exception, the FormatChecker will output a
125 readable message provided by the checker.
126 """
128 def check(self, param_value, format):
129 """Check whether the param_value conforms to the given format.
131 :param param_value: the param_value to check
132 :type: any primitive type (str, number, bool)
133 :param str format: the format that param_value should conform to
134 :raises: :exc:`FormatError` if param_value does not conform to format
135 """
137 if format not in self.checkers:
138 return
140 # For safety reasons custom checkers can be registered with
141 # allowed exception types. Anything else will fall into the
142 # default formatter.
143 func, raises = self.checkers[format]
144 result, cause = None, None
146 try:
147 result = func(param_value)
148 except raises as e:
149 cause = e
150 if not result:
151 msg = '%r is not a %r' % (param_value, format)
152 raise jsonschema_exc.FormatError(msg, cause=cause)
155_FORMAT_CHECKER = FormatChecker()
158@_FORMAT_CHECKER.checks('date-time')
159def _validate_datetime_format(instance: object) -> bool:
160 # format checks constrain to the relevant primitive type
161 # https://github.com/OAI/OpenAPI-Specification/issues/3148
162 if not isinstance(instance, str):
163 return True
164 try:
165 timeutils.parse_isotime(instance)
166 except ValueError:
167 return False
168 else:
169 return True
172@_FORMAT_CHECKER.checks('uuid')
173def _validate_uuid_format(instance: object) -> bool:
174 # format checks constrain to the relevant primitive type
175 # https://github.com/OAI/OpenAPI-Specification/issues/3148
176 if not isinstance(instance, str): 176 ↛ 177line 176 didn't jump to line 177 because the condition on line 176 was never true
177 return True
179 return uuidutils.is_uuid_like(instance)
182class _SchemaValidator(object):
183 """A validator class
185 This class is changed from Draft202012Validator to validate minimum/maximum
186 value of a string number(e.g. '10').
188 In addition, FormatCheckers are added for checking data formats which are
189 common in the Manila API.
190 """
192 validator = None
193 validator_org = jsonschema.Draft202012Validator
195 def __init__(
196 self, schema, relax_additional_properties=False, is_body=True
197 ):
198 self.is_body = is_body
199 validators = {
200 'minimum': self._validate_minimum,
201 'maximum': self._validate_maximum,
202 }
203 if relax_additional_properties: 203 ↛ 204line 203 didn't jump to line 204 because the condition on line 203 was never true
204 validators['additionalProperties'] = (
205 _soft_validate_additional_properties
206 )
208 validator_cls = jsonschema.validators.extend(
209 self.validator_org, validators
210 )
211 self.validator = validator_cls(schema, format_checker=_FORMAT_CHECKER)
213 def validate(self, *args, **kwargs):
214 try:
215 self.validator.validate(*args, **kwargs)
216 except jsonschema.ValidationError as ex:
217 if len(ex.path) > 0:
218 if self.is_body: 218 ↛ 238line 218 didn't jump to line 238 because the condition on line 218 was always true
219 # NOTE: For consistency across OpenStack services, this
220 # error message has been written in a similar format as
221 # WSME errors.
222 detail = _(
223 'Invalid input for field/attribute %(path)s. '
224 'Value: %(value)s. %(message)s'
225 ) % {
226 'path': ex.path.pop(),
227 'value': ex.instance,
228 'message': ex.message,
229 }
230 else:
231 # NOTE: We use 'ex.path.popleft()' instead of
232 # 'ex.path.pop()'. This is due to the structure of query
233 # parameters which is a dict with key as name and value is
234 # list. As such, the first item in the 'ex.path' is the key
235 # and second item is the index of list in the value. We
236 # need the key as the parameter name in the error message
237 # so we pop the first value out of 'ex.path'.
238 detail = _(
239 'Invalid input for query parameters %(path)s. '
240 'Value: %(value)s. %(message)s'
241 ) % {
242 'path': ex.path.popleft(),
243 'value': ex.instance,
244 'message': ex.message,
245 }
246 else:
247 detail = ex.message
248 raise exception.ValidationError(detail=detail)
249 except TypeError as ex:
250 # NOTE: If passing non string value to patternProperties parameter,
251 # TypeError happens. Here is for catching the TypeError.
252 detail = str(ex)
253 raise exception.ValidationError(detail=detail)
255 def _number_from_str(self, param_value):
256 try:
257 value = int(param_value)
258 except (ValueError, TypeError):
259 try:
260 value = float(param_value)
261 except (ValueError, TypeError):
262 return None
263 return value
265 def _validate_minimum(self, validator, minimum, param_value, schema):
266 param_value = self._number_from_str(param_value)
267 if param_value is None: 267 ↛ 268line 267 didn't jump to line 268 because the condition on line 267 was never true
268 return
269 return self.validator_org.VALIDATORS['minimum'](
270 validator, minimum, param_value, schema
271 )
273 def _validate_maximum(self, validator, maximum, param_value, schema):
274 param_value = self._number_from_str(param_value)
275 if param_value is None: 275 ↛ 276line 275 didn't jump to line 276 because the condition on line 275 was never true
276 return
277 return self.validator_org.VALIDATORS['maximum'](
278 validator, maximum, param_value, schema
279 )