1# Copyright (C) 2010-2020 by the Free Software Foundation, Inc. 2# 3# This file is part of GNU Mailman. 4# 5# GNU Mailman is free software: you can redistribute it and/or modify it under 6# the terms of the GNU General Public License as published by the Free 7# Software Foundation, either version 3 of the License, or (at your option) 8# any later version. 9# 10# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT 11# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 13# more details. 14# 15# You should have received a copy of the GNU General Public License along with 16# GNU Mailman. If not, see <https://www.gnu.org/licenses/>. 17 18"""REST web form validation.""" 19 20import re 21 22from mailman.interfaces.address import IEmailValidator 23from mailman.interfaces.errors import MailmanError 24from mailman.interfaces.languages import ILanguageManager 25from mailman.rest.helpers import get_request_params 26from public import public 27from zope.component import getUtility 28 29 30COMMASPACE = ', ' 31 32 33@public 34class RESTError(MailmanError): 35 """Base class for REST API errors.""" 36 37 38@public 39class UnknownPATCHRequestError(RESTError): 40 """A PATCH request contained an unknown attribute.""" 41 42 def __init__(self, attribute): 43 self.attribute = attribute 44 45 46@public 47class ReadOnlyPATCHRequestError(RESTError): 48 """A PATCH request contained a read-only attribute.""" 49 50 def __init__(self, attribute): 51 self.attribute = attribute 52 53 54@public 55class enum_validator: 56 """Convert an enum value name into an enum value.""" 57 58 def __init__(self, enum_class, *, allow_blank=False): 59 self._enum_class = enum_class 60 self._allow_blank = allow_blank 61 62 def __call__(self, enum_value): 63 # This will raise a KeyError if the enum value is unknown. The 64 # Validator API requires turning this into a ValueError. 65 if not enum_value and self._allow_blank: 66 return None 67 try: 68 return self._enum_class[enum_value] 69 except KeyError: 70 # Retain the error message. 71 err_msg = 'Accepted Values are: {}'.format(self._accepted_values) 72 raise ValueError(err_msg) 73 74 @property 75 def _accepted_values(self): 76 """Joined comma separated self._enum_class values""" 77 return ', '.join(item._name_ for item in self._enum_class) 78 79 80@public 81def subscriber_validator(api): 82 """Convert an email-or-(int|hex) to an email-or-UUID.""" 83 def _inner(subscriber): 84 try: 85 return api.to_uuid(subscriber) 86 except ValueError: 87 # It must be an email address. 88 if getUtility(IEmailValidator).is_valid(subscriber): 89 return subscriber 90 raise ValueError 91 return _inner 92 93 94@public 95def language_validator(code): 96 """Convert a language code to a Language object.""" 97 return getUtility(ILanguageManager)[code] 98 99 100@public 101def list_of_strings_validator(values): 102 """Turn a list of things, or a single thing, into a list of unicodes.""" 103 # There is no good way to pass around an empty list through HTTP API, so, 104 # we consider an empty string as an empty list, which can easily be passed 105 # around. This is a contract between Core and Postorius. This also fixes a 106 # bug where an empty string ('') would be interpreted as a valid value [''] 107 # to create a singleton list, instead of empty list, which in later stages 108 # would create other problems. 109 if values is '': # noqa: F632 110 return [] 111 if not isinstance(values, (list, tuple)): 112 values = [values] 113 for value in values: 114 if not isinstance(value, str): 115 raise ValueError('Expected str, got {!r}'.format(value)) 116 return values 117 118 119@public 120def list_of_emails_validator(values): 121 """Turn a list of things, or a single thing, into a list of emails.""" 122 if not isinstance(values, (list, tuple)): 123 if getUtility(IEmailValidator).is_valid(values): 124 return [values] 125 raise ValueError('Bad email address format: {}'.format(values)) 126 for value in values: 127 if not getUtility(IEmailValidator).is_valid(value): 128 raise ValueError('Expected email address, got {!r}'.format(value)) 129 return values 130 131 132@public 133def integer_ge_zero_validator(value): 134 """Validate that the value is a non-negative integer.""" 135 value = int(value) 136 if value < 0: 137 raise ValueError('Expected a non-negative integer: {}'.format(value)) 138 return value 139 140 141@public 142def regexp_validator(value): # pragma: missed 143 """Validate that the value is a valid regexp.""" 144 # This code is covered as proven by the fact that the tests 145 # test_add_bad_regexp and test_patch_bad_regexp in 146 # mailman/rest/tests/test_header_matches.py fail with AssertionError: 147 # HTTPError not raised if the code is bypassed, but coverage says it's 148 # not covered so work around it for now. 149 try: 150 re.compile(value) 151 except re.error: 152 raise ValueError('Expected a valid regexp, got {}'.format(value)) 153 return value 154 155 156@public 157def email_or_regexp_validator(value): 158 """ Email or regular expression validator 159 160 Validate that the value is not null and is a valid regular expression or 161 email. 162 """ 163 if not value: 164 raise ValueError( 165 'Expected a valid email address or regular expression, got empty') 166 valid = True 167 # A string starts with ^ will be regarded as regex. 168 if value.startswith('^'): 169 try: 170 regexp_validator(value) 171 except ValueError: 172 valid = False 173 else: 174 valid = getUtility(IEmailValidator).is_valid(value) 175 176 if valid: 177 return value 178 else: 179 raise ValueError( 180 'Expected a valid email address or regular expression,' 181 ' got {}'.format(value)) 182 183 184@public 185def email_validator(value): 186 """Validate the value is a valid email.""" 187 if not getUtility(IEmailValidator).is_valid(value): 188 raise ValueError( 189 'Expected a valid email address, got {}'.format(value)) 190 return value 191 192 193@public 194class Validator: 195 """A validator of parameter input.""" 196 197 def __init__(self, **kws): 198 if '_optional' in kws: 199 self._optional = set(kws.pop('_optional')) 200 else: 201 self._optional = set() 202 self._converters = kws.copy() 203 204 def __call__(self, request): 205 values = {} 206 extras = set() 207 cannot_convert = set() 208 form_data = {} 209 # All keys which show up only once in the form data get a scalar value 210 # in the pre-converted dictionary. All keys which show up more than 211 # once get a list value. 212 missing = object() 213 # Parse the items from request depending on the content type. 214 items = get_request_params(request) 215 216 for key, new_value in items.items(): 217 old_value = form_data.get(key, missing) 218 if old_value is missing: 219 form_data[key] = new_value 220 elif isinstance(old_value, list): 221 old_value.append(new_value) 222 else: 223 form_data[key] = [old_value, new_value] 224 # Now do all the conversions. 225 for key, value in form_data.items(): 226 try: 227 values[key] = self._converters[key](value) 228 except KeyError: 229 extras.add(key) 230 except (TypeError, ValueError) as e: 231 cannot_convert.add((key, str(e))) 232 # Make sure there are no unexpected values. 233 if len(extras) != 0: 234 extras = COMMASPACE.join(sorted(extras)) 235 raise ValueError('Unexpected parameters: {}'.format(extras)) 236 # raise BadRequestError( 237 # description='Unexpected parameters: {}'.format(extras)) 238 # Make sure everything could be converted. 239 if len(cannot_convert) != 0: 240 invalid_msg = [] 241 for param in sorted(cannot_convert): 242 invalid_msg.append( 243 'Invalid Parameter "{0}": {1}.'.format(*param)) 244 raise ValueError(' '.join(invalid_msg)) 245 # raise InvalidParamError(param_name=bad, msg=invalid_msg) 246 # Make sure nothing's missing. 247 value_keys = set(values) 248 required_keys = set(self._converters) - self._optional 249 if value_keys & required_keys != required_keys: 250 missing = COMMASPACE.join(sorted(required_keys - value_keys)) 251 raise ValueError('Missing Parameter: {}'.format(missing)) 252 # raise MissingParamError(param_name=missing) 253 return values 254 255 def update(self, obj, request): 256 """Update the object with the values in the request. 257 258 This first validates and converts the attributes in the request, then 259 updates the given object with the newly converted values. 260 261 :param obj: The object to update. 262 :type obj: object 263 :param request: The HTTP request. 264 :raises ValueError: if conversion failed for some attribute, including 265 if the API version mismatches. 266 """ 267 for key, value in self.__call__(request).items(): 268 self._converters[key].put(obj, key, value) 269 270 271@public 272class PatchValidator(Validator): 273 """Create a special validator for PATCH requests. 274 275 PATCH is different than PUT because with the latter, you're changing the 276 entire resource, so all expected attributes must exist. With the former, 277 you're only changing a subset of the attributes, so you only validate the 278 ones that exist in the request. 279 """ 280 281 def __init__(self, request, converters): 282 """Create a validator for the PATCH request. 283 284 :param request: The request object, which must have a .PATCH 285 attribute. 286 :param converters: A mapping of attribute names to the converter for 287 that attribute's type. Generally, this will be a GetterSetter 288 instance, but it might be something more specific for custom data 289 types (e.g. non-basic types like unicodes). 290 :raises UnknownPATCHRequestError: if the request contains an unknown 291 attribute, i.e. one that is not in the `attributes` mapping. 292 :raises ReadOnlyPATCHRequest: if the requests contains an attribute 293 that is defined as read-only. 294 """ 295 validationators = {} 296 # Parse the items from request depending on the content type. 297 items = get_request_params(request) 298 for attribute in items: 299 if attribute not in converters: 300 raise UnknownPATCHRequestError(attribute) 301 if converters[attribute].decoder is None: 302 raise ReadOnlyPATCHRequestError(attribute) 303 validationators[attribute] = converters[attribute] 304 super().__init__(**validationators) 305