No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

validator.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. #-*- coding: utf-8 -*-
  2. import sys
  3. import os.path
  4. import re
  5. import socket
  6. import inspect
  7. import copy
  8. from lodel.context import LodelContext
  9. LodelContext.expose_modules(globals(), {
  10. 'lodel.mlnamedobject.mlnamedobject': ['MlNamedObject'],
  11. 'lodel.exceptions': ['LodelException', 'LodelExceptions',
  12. 'LodelFatalError', 'FieldValidationError']})
  13. # @package lodel.settings.validator Lodel2 settings validators/cast module
  14. #
  15. # Validator are registered in the Validator class.
  16. # @note to get a list of registered default validators just run
  17. # <pre>$ python scripts/settings_validator.py</pre>
  18. # @brief Exception class that should be raised when a validation fails
  19. class ValidationError(Exception):
  20. pass
  21. # @brief Handles settings validators
  22. #
  23. # Class instance are callable objects that takes a value argument (the value to validate). It raises
  24. # a ValidationError if validation fails, else it returns a properly
  25. # casted value.
  26. #@todo implement an IP validator and use it in multisite confspec
  27. class Validator(MlNamedObject):
  28. _validators = dict()
  29. _description = dict()
  30. # @brief Instanciate a validator
  31. #@param name str : validator name
  32. #@param none_is_valid bool : if True None will be validated
  33. #@param **kwargs : more arguement for the validator
  34. def __init__(self, name, none_is_valid=False, display_name=None, help_text=None, **kwargs):
  35. if name is not None and name not in self._validators:
  36. raise LodelFatalError("No validator named '%s'" % name)
  37. self.__none_is_valid = none_is_valid
  38. self.__name = name
  39. self._opt_args = kwargs
  40. if display_name is None:
  41. display_name = name
  42. super().__init__(display_name, help_text)
  43. # @brief Call the validator
  44. # @param value *
  45. # @return properly casted value
  46. # @throw ValidationError
  47. def __call__(self, value):
  48. if value is None:
  49. if self.__none_is_valid:
  50. return None
  51. else:
  52. raise ValidationError('None is not a valid value')
  53. if self.__none_is_valid and value is None:
  54. return None
  55. try:
  56. ret = self._validators[self.__name](value, **self._opt_args)
  57. return ret
  58. except Exception as exp:
  59. raise ValidationError(exp)
  60. # @brief Register a new validator
  61. # @param name str : validator name
  62. # @param callback callable : the function that will validate a value
  63. # @param description str
  64. @classmethod
  65. def register_validator(cls, name, callback, description=None):
  66. if name in cls._validators:
  67. raise NameError("A validator named '%s' allready exists" % name)
  68. # Broken test for callable
  69. if not inspect.isfunction(callback) and not inspect.ismethod(callback) and not hasattr(callback, '__call__'):
  70. raise TypeError("Callable expected but got %s" % type(callback))
  71. cls._validators[name] = callback
  72. cls._description[name] = description
  73. # @brief Get the validator list associated with description
  74. @classmethod
  75. def validators_list(cls):
  76. return copy.copy(cls._description)
  77. # @brief Create and register a list validator
  78. # @param elt_validator callable : The validator that will be used for validate each elt value
  79. # @param validator_name str
  80. # @param description None | str
  81. # @param separator str : The element separator
  82. # @return A Validator instance
  83. @classmethod
  84. def create_list_validator(cls, validator_name, elt_validator, description=None, separator=','):
  85. def list_validator(value):
  86. res = list()
  87. if value is None:
  88. return res
  89. errors = list()
  90. for elt in value.split(separator):
  91. elt = elt_validator(elt)
  92. if len(elt) > 0:
  93. res.append(elt)
  94. return res
  95. description = "Convert value to an array" if description is None else description
  96. cls.register_validator(validator_name, list_validator, description)
  97. return cls(validator_name)
  98. # @brief Create and register a list validator which reads an array and returns a string
  99. # @param elt_validator callable : The validator that will be used for validate each elt value
  100. # @param validator_name str
  101. # @param description None | str
  102. # @param separator str : The element separator
  103. # @return A Validator instance
  104. @classmethod
  105. def create_write_list_validator(cls, validator_name, elt_validator, description=None, separator=','):
  106. def write_list_validator(value):
  107. res = ''
  108. for elt in value:
  109. res += elt_validator(elt) + ','
  110. return res[:len(res) - 1]
  111. description = "Convert value to a string" if description is None else description
  112. cls.register_validator(validator_name, write_list_validator, description)
  113. return cls(validator_name)
  114. # @brief Create and register a regular expression validator
  115. # @param pattern str : regex pattern
  116. # @param validator_name str : The validator name
  117. # @param description str : Validator description
  118. # @return a Validator instance
  119. @classmethod
  120. def create_re_validator(cls, pattern, validator_name, description=None):
  121. def re_validator(value):
  122. if not re.match(pattern, value):
  123. raise ValidationError(
  124. "The value '%s' doesn't match the following pattern '%s'"
  125. % pattern)
  126. return value
  127. # registering the validator
  128. cls.register_validator(validator_name, re_validator,
  129. ("Match value to '%s'" % pattern)
  130. if description is None else description)
  131. return cls(validator_name)
  132. #  @return a list of registered validators
  133. @classmethod
  134. def validators_list_str(cls):
  135. result = ''
  136. for name in sorted(cls._validators.keys()):
  137. result += "\t%016s" % name
  138. if name in cls._description and cls._description[name] is not None:
  139. result += ": %s" % cls._description[name]
  140. result += "\n"
  141. return result
  142. # @brief Integer value validator callback
  143. def int_val(value):
  144. return int(value)
  145. # @brief Output file validator callback
  146. # @return A file object (if filename is '-' return sys.stderr)
  147. def file_err_output(value):
  148. if not isinstance(value, str):
  149. raise ValidationError("A string was expected but got '%s' " % value)
  150. if value == '-':
  151. return None
  152. return value
  153. # @brief Boolean value validator callback
  154. def boolean_val(value):
  155. if isinstance(value, bool):
  156. return value
  157. if value.strip().lower() == 'true' or value.strip() == '1':
  158. value = True
  159. elif value.strip().lower() == 'false' or value.strip() == '0':
  160. value = False
  161. else:
  162. raise ValidationError("A boolean was expected but got '%s' " % value)
  163. return bool(value)
  164. # @brief Validate a directory path
  165. def directory_val(value):
  166. res = Validator('strip')(value)
  167. if not os.path.isdir(res):
  168. raise ValidationError("Following path don't exists or is not a directory : '%s'" % res)
  169. return res
  170. # @brief Validate a loglevel value
  171. def loglevel_val(value):
  172. valids = ['DEBUG', 'INFO', 'WARNING', 'SECURITY', 'ERROR', 'CRITICAL']
  173. if value.upper() not in valids:
  174. raise ValidationError(
  175. "The value '%s' is not a valid loglevel" % value)
  176. return value.upper()
  177. # @brief Validate a path
  178. def path_val(value):
  179. if value is None or not os.path.exists(value):
  180. raise ValidationError(
  181. "path '%s' doesn't exists" % value)
  182. return value
  183. # @brief Validate None
  184. def none_val(value):
  185. if value is None:
  186. return None
  187. raise ValidationError("This settings cannot be set in configuration file")
  188. # @brief Validate a string
  189. def str_val(value):
  190. try:
  191. return str(value)
  192. except Exception as exp:
  193. raise ValidationError("Can't to convert value to string: " + str(exp))
  194. # @brief Validate using a regex
  195. def regex_val(value, pattern):
  196. if re.match(pattern, value) is None:
  197. raise ValidationError("The value '%s' is not validated by : \
  198. r\"%s\"" % (value, pattern))
  199. return value
  200. # @brief Validate a hostname (ipv4 or ipv6)
  201. def host_val(value):
  202. if value == 'localhost':
  203. return value
  204. ok = False
  205. try:
  206. socket.inet_aton(value)
  207. return value
  208. except (TypeError, OSError):
  209. pass
  210. try:
  211. socket.inet_pton(socket.AF_INET6, value)
  212. return value
  213. except (TypeError, OSError):
  214. pass
  215. try:
  216. socket.getaddrinfo(value, 80)
  217. return value
  218. except (TypeError, socket.gaierror):
  219. msg = "The value '%s' is not a valid host"
  220. raise ValidationError(msg % value)
  221. def custom_list_validator(value, validator_name, validator_kwargs=None):
  222. validator_kwargs = dict() if validator_kwargs is None else validator_kwargs
  223. validator = Validator(validator_name, **validator_kwargs)
  224. for item in value.split():
  225. validator(item)
  226. return value.split()
  227. #
  228. # Default validators registration
  229. #
  230. Validator.register_validator('custom_list', custom_list_validator,
  231. 'A list validator that takes a "validator_name" as argument')
  232. Validator.register_validator('dummy', lambda value: value, 'Validate anything')
  233. Validator.register_validator('none', none_val, 'Validate None')
  234. Validator.register_validator('string', str_val, 'Validate string values')
  235. Validator.register_validator('strip', str.strip, 'String trim')
  236. Validator.register_validator('int', int_val, 'Integer value validator')
  237. Validator.register_validator('bool', boolean_val, 'Boolean value validator')
  238. Validator.register_validator('errfile', file_err_output,
  239. 'Error output file validator (return stderr if filename is "-")')
  240. Validator.register_validator('directory', directory_val,
  241. 'Directory path validator')
  242. Validator.register_validator('loglevel', loglevel_val, 'Loglevel validator')
  243. Validator.register_validator('path', path_val, 'path validator')
  244. Validator.register_validator('host', host_val, 'host validator')
  245. Validator.register_validator('regex', regex_val,
  246. 'RegEx name validator (take re as argument)')
  247. Validator.create_list_validator('list', Validator('strip'), description="Simple list validator. Validate a list of values separated by ','",
  248. separator=',')
  249. Validator.create_list_validator(
  250. 'directory_list',
  251. Validator('directory'),
  252. description="Validator for a list of directory path separated with ','",
  253. separator=',')
  254. Validator.create_write_list_validator(
  255. 'write_list',
  256. Validator('directory'),
  257. description="Validator for an array of values \
  258. which will be set in a string, separated by ','",
  259. separator=',')
  260. Validator.create_re_validator(
  261. r'^https?://[^\./]+.[^\./]+/?.*$',
  262. 'http_url',
  263. 'Url validator')
  264. # @brief Validator for Editorial model component
  265. #
  266. # Designed to validate a conf that indicate a class.field in an EM
  267. #@todo modified the hardcoded dyncode import (it's a warning)
  268. def emfield_val(value):
  269. LodelContext.expose_modules(globals(),
  270. {'lodel.plugin.hooks': ['LodelHook']})
  271. spl = value.split('.')
  272. if len(spl) != 2:
  273. msg = "Expected a value in the form CLASSNAME.FIELDNAME but got : %s"
  274. raise ValidationError(msg % value)
  275. value = tuple(spl)
  276. # Late validation hook
  277. @LodelHook('lodel2_dyncode_bootstraped')
  278. def emfield_conf_check(hookname, caller, payload):
  279. import leapi_dyncode as dyncode # <-- dirty & quick
  280. classnames = {cls.__name__.lower(): cls for cls in dyncode.dynclasses}
  281. if value[0].lower() not in classnames:
  282. msg = "Following dynamic class do not exists in current EM : %s"
  283. raise ValidationError(msg % value[0])
  284. ccls = classnames[value[0].lower()]
  285. if value[1].lower() not in ccls.fieldnames(True):
  286. msg = "Following field not found in class %s : %s"
  287. raise ValidationError(msg % value)
  288. return value
  289. # @brief Validator for plugin name & optionnaly type
  290. #
  291. # Able to check that the value is a plugin and if it is of a specific type
  292. def plugin_validator(value, ptype=None):
  293. if value:
  294. LodelContext.expose_modules(globals(), {
  295. 'lodel.plugin.hooks': ['LodelHook']})
  296. value = copy.copy(value)
  297. @LodelHook('lodel2_dyncode_bootstraped')
  298. def plugin_type_checker(hookname, caller, payload):
  299. LodelContext.expose_modules(globals(), {
  300. 'lodel.plugin.plugins': ['Plugin'],
  301. 'lodel.plugin.exceptions': ['PluginError']})
  302. if value is None:
  303. return
  304. try:
  305. plugin = Plugin.get(value)
  306. except PluginError:
  307. msg = "No plugin named %s found"
  308. msg %= value
  309. raise ValidationError(msg)
  310. if plugin._type_conf_name.lower() != ptype.lower():
  311. msg = "A plugin of type '%s' was expected but found a plugin \
  312. named '%s' that is a '%s' plugin"
  313. msg %= (ptype, value, plugin._type_conf_name)
  314. raise ValidationError(msg)
  315. return value
  316. return None
  317. Validator.register_validator(
  318. 'plugin',
  319. plugin_validator,
  320. 'plugin name & type validator')
  321. Validator.register_validator(
  322. 'emfield',
  323. emfield_val,
  324. 'EmField name validator')
  325. #
  326. # Lodel 2 configuration specification
  327. #
  328. # @brief Append a piece of confspec
  329. #@note orig is modified during the process
  330. #@param orig dict : the confspec to update
  331. #@param section str : section name
  332. #@param key str
  333. #@param validator Validator : the validator to use to check this configuration key's value
  334. #@param default
  335. #@return new confspec
  336. def confspec_append(orig, section, key, validator, default):
  337. if section not in orig:
  338. orig[section] = dict()
  339. if key not in orig[section]:
  340. orig[section][key] = (default, validator)
  341. return orig
  342. # @brief Global specifications for lodel2 settings
  343. LODEL2_CONF_SPECS = {
  344. 'lodel2': {
  345. 'debug': (True, Validator('bool')),
  346. 'sitename': ('noname', Validator('strip')),
  347. 'runtest': (False, Validator('bool')),
  348. },
  349. 'lodel2.logging.*': {
  350. 'level': ('ERROR', Validator('loglevel')),
  351. 'context': (False, Validator('bool')),
  352. 'filename': ("-", Validator('errfile', none_is_valid=False)),
  353. 'backupcount': (5, Validator('int', none_is_valid=False)),
  354. 'maxbytes': (1024 * 10, Validator('int', none_is_valid=False)),
  355. },
  356. 'lodel2.editorialmodel': {
  357. 'emfile': ('em.pickle', Validator('strip')),
  358. 'emtranslator': ('picklefile', Validator('strip')),
  359. 'dyncode': ('leapi_dyncode.py', Validator('strip')),
  360. 'groups': ('', Validator('list')),
  361. 'editormode': (False, Validator('bool')),
  362. },
  363. 'lodel2.datasources.*': {
  364. 'read_only': (False, Validator('bool')),
  365. 'identifier': (None, Validator('string')),
  366. },
  367. 'lodel2.auth': {
  368. 'login_classfield': ('user.login', Validator('emfield')),
  369. 'pass_classfield': ('user.password', Validator('emfield')),
  370. },
  371. }