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

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. 

12 

13"""Internal implementation of request/response validating middleware.""" 

14 

15import re 

16 

17import jsonschema 

18from jsonschema import exceptions as jsonschema_exc 

19from oslo_utils import timeutils 

20from oslo_utils import uuidutils 

21import webob.exc 

22 

23from manila import exception 

24from manila.i18n import _ 

25from manila import utils 

26 

27 

28def _soft_validate_additional_properties( 

29 validator, 

30 additional_properties_value, 

31 param_value, 

32 schema, 

33): 

34 """Validator function. 

35 

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: 

39 

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 

57 

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) 

68 

69 if not extra_properties: 

70 return 

71 

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] 

85 

86 

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. 

96 

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 

107 

108 if mandatory and not value: 

109 msg = _("The '%s' can not be None.") % entity_name 

110 raise webob.exc.HTTPBadRequest(explanation=msg) 

111 

112 if remove_whitespaces: 

113 value = value.strip() 

114 

115 utils.check_string_length( 

116 value, entity_name, min_length=min_length, max_length=max_length 

117 ) 

118 

119 

120class FormatChecker(jsonschema.FormatChecker): 

121 """A FormatChecker can output the message from cause exception 

122 

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 """ 

127 

128 def check(self, param_value, format): 

129 """Check whether the param_value conforms to the given format. 

130 

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 """ 

136 

137 if format not in self.checkers: 

138 return 

139 

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 

145 

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) 

153 

154 

155_FORMAT_CHECKER = FormatChecker() 

156 

157 

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 

170 

171 

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 

178 

179 return uuidutils.is_uuid_like(instance) 

180 

181 

182class _SchemaValidator(object): 

183 """A validator class 

184 

185 This class is changed from Draft202012Validator to validate minimum/maximum 

186 value of a string number(e.g. '10'). 

187 

188 In addition, FormatCheckers are added for checking data formats which are 

189 common in the Manila API. 

190 """ 

191 

192 validator = None 

193 validator_org = jsonschema.Draft202012Validator 

194 

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 ) 

207 

208 validator_cls = jsonschema.validators.extend( 

209 self.validator_org, validators 

210 ) 

211 self.validator = validator_cls(schema, format_checker=_FORMAT_CHECKER) 

212 

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) 

254 

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 

264 

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 ) 

272 

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 )