Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

404 linhas
14 KiB

  1. """
  2. """
  3. # Created on 2014.09.08
  4. #
  5. # Author: Giovanni Cannata
  6. #
  7. # Copyright 2014 - 2019 Giovanni Cannata
  8. #
  9. # This file is part of ldap3.
  10. #
  11. # ldap3 is free software: you can redistribute it and/or modify
  12. # it under the terms of the GNU Lesser General Public License as published
  13. # by the Free Software Foundation, either version 3 of the License, or
  14. # (at your option) any later version.
  15. #
  16. # ldap3 is distributed in the hope that it will be useful,
  17. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. # GNU Lesser General Public License for more details.
  20. #
  21. # You should have received a copy of the GNU Lesser General Public License
  22. # along with ldap3 in the COPYING and COPYING.LESSER files.
  23. # If not, see <http://www.gnu.org/licenses/>.
  24. from string import hexdigits, ascii_letters, digits
  25. from .. import SEQUENCE_TYPES
  26. from ..core.exceptions import LDAPInvalidDnError
  27. STATE_ANY = 0
  28. STATE_ESCAPE = 1
  29. STATE_ESCAPE_HEX = 2
  30. def _add_ava(ava, decompose, remove_space, space_around_equal):
  31. if not ava:
  32. return ''
  33. space = ' ' if space_around_equal else ''
  34. attr_name, _, value = ava.partition('=')
  35. if decompose:
  36. if remove_space:
  37. component = (attr_name.strip(), value.strip())
  38. else:
  39. component = (attr_name, value)
  40. else:
  41. if remove_space:
  42. component = attr_name.strip() + space + '=' + space + value.strip()
  43. else:
  44. component = attr_name + space + '=' + space + value
  45. return component
  46. def to_dn(iterator, decompose=False, remove_space=False, space_around_equal=False, separate_rdn=False):
  47. """
  48. Convert an iterator to a list of dn parts
  49. if decompose=True return a list of tuple (one for each dn component) else return a list of strings
  50. if remove_space=True removes unneeded spaces
  51. if space_around_equal=True add spaces around equal in returned strings
  52. if separate_rdn=True consider multiple RDNs as different component of DN
  53. """
  54. dn = []
  55. component = ''
  56. escape_sequence = False
  57. for c in iterator:
  58. if c == '\\': # escape sequence
  59. escape_sequence = True
  60. elif escape_sequence and c != ' ':
  61. escape_sequence = False
  62. elif c == '+' and separate_rdn:
  63. dn.append(_add_ava(component, decompose, remove_space, space_around_equal))
  64. component = ''
  65. continue
  66. elif c == ',':
  67. if '=' in component:
  68. dn.append(_add_ava(component, decompose, remove_space, space_around_equal))
  69. component = ''
  70. continue
  71. component += c
  72. dn.append(_add_ava(component, decompose, remove_space, space_around_equal))
  73. return dn
  74. def _find_first_unescaped(dn, char, pos):
  75. while True:
  76. pos = dn.find(char, pos)
  77. if pos == -1:
  78. break # no char found
  79. if pos > 0 and dn[pos - 1] != '\\': # unescaped char
  80. break
  81. elif pos > 1 and dn[pos - 1] == '\\': # may be unescaped
  82. escaped = True
  83. for c in dn[pos - 2:0:-1]:
  84. if c == '\\':
  85. escaped = not escaped
  86. else:
  87. break
  88. if not escaped:
  89. break
  90. pos += 1
  91. return pos
  92. def _find_last_unescaped(dn, char, start, stop=0):
  93. while True:
  94. stop = dn.rfind(char, start, stop)
  95. if stop == -1:
  96. break
  97. if stop >= 0 and dn[stop - 1] != '\\':
  98. break
  99. elif stop > 1 and dn[stop - 1] == '\\': # may be unescaped
  100. escaped = True
  101. for c in dn[stop - 2:0:-1]:
  102. if c == '\\':
  103. escaped = not escaped
  104. else:
  105. break
  106. if not escaped:
  107. break
  108. if stop < start:
  109. stop = -1
  110. break
  111. return stop
  112. def _get_next_ava(dn):
  113. comma = _find_first_unescaped(dn, ',', 0)
  114. plus = _find_first_unescaped(dn, '+', 0)
  115. if plus > 0 and (plus < comma or comma == -1):
  116. equal = _find_first_unescaped(dn, '=', plus + 1)
  117. if equal > plus + 1:
  118. plus = _find_last_unescaped(dn, '+', plus, equal)
  119. return dn[:plus], '+'
  120. if comma > 0:
  121. equal = _find_first_unescaped(dn, '=', comma + 1)
  122. if equal > comma + 1:
  123. comma = _find_last_unescaped(dn, ',', comma, equal)
  124. return dn[:comma], ','
  125. return dn, ''
  126. def _split_ava(ava, escape=False, strip=True):
  127. equal = ava.find('=')
  128. while equal > 0: # not first character
  129. if ava[equal - 1] != '\\': # not an escaped equal so it must be an ava separator
  130. # attribute_type1 = ava[0:equal].strip() if strip else ava[0:equal]
  131. if strip:
  132. attribute_type = ava[0:equal].strip()
  133. attribute_value = _escape_attribute_value(ava[equal + 1:].strip()) if escape else ava[equal + 1:].strip()
  134. else:
  135. attribute_type = ava[0:equal]
  136. attribute_value = _escape_attribute_value(ava[equal + 1:]) if escape else ava[equal + 1:]
  137. return attribute_type, attribute_value
  138. equal = ava.find('=', equal + 1)
  139. return '', (ava.strip if strip else ava) # if no equal found return only value
  140. def _validate_attribute_type(attribute_type):
  141. if not attribute_type:
  142. raise LDAPInvalidDnError('attribute type not present')
  143. if attribute_type == '<GUID': # patch for AD DirSync
  144. return True
  145. for c in attribute_type:
  146. if not (c in ascii_letters or c in digits or c == '-'): # allowed uppercase and lowercase letters, digits and hyphen as per RFC 4512
  147. raise LDAPInvalidDnError('character \'' + c + '\' not allowed in attribute type')
  148. if attribute_type[0] in digits or attribute_type[0] == '-': # digits and hyphen not allowed as first character
  149. raise LDAPInvalidDnError('character \'' + attribute_type[0] + '\' not allowed as first character of attribute type')
  150. return True
  151. def _validate_attribute_value(attribute_value):
  152. if not attribute_value:
  153. return False
  154. if attribute_value[0] == '#': # only hex characters are valid
  155. for c in attribute_value:
  156. if c not in hexdigits: # allowed only hex digits as per RFC 4514
  157. raise LDAPInvalidDnError('character ' + c + ' not allowed in hex representation of attribute value')
  158. if len(attribute_value) % 2 == 0: # string must be # + HEX HEX (an odd number of chars)
  159. raise LDAPInvalidDnError('hex representation must be in the form of <HEX><HEX> pairs')
  160. if attribute_value[0] == ' ': # unescaped space cannot be used as leading or last character
  161. raise LDAPInvalidDnError('SPACE must be escaped as leading character of attribute value')
  162. if attribute_value.endswith(' ') and not attribute_value.endswith('\\ '):
  163. raise LDAPInvalidDnError('SPACE must be escaped as trailing character of attribute value')
  164. state = STATE_ANY
  165. for c in attribute_value:
  166. if state == STATE_ANY:
  167. if c == '\\':
  168. state = STATE_ESCAPE
  169. elif c in '"#+,;<=>\00':
  170. raise LDAPInvalidDnError('special character ' + c + ' must be escaped')
  171. elif state == STATE_ESCAPE:
  172. if c in hexdigits:
  173. state = STATE_ESCAPE_HEX
  174. elif c in ' "#+,;<=>\\\00':
  175. state = STATE_ANY
  176. else:
  177. raise LDAPInvalidDnError('invalid escaped character ' + c)
  178. elif state == STATE_ESCAPE_HEX:
  179. if c in hexdigits:
  180. state = STATE_ANY
  181. else:
  182. raise LDAPInvalidDnError('invalid escaped character ' + c)
  183. # final state
  184. if state != STATE_ANY:
  185. raise LDAPInvalidDnError('invalid final character')
  186. return True
  187. def _escape_attribute_value(attribute_value):
  188. if not attribute_value:
  189. return ''
  190. if attribute_value[0] == '#': # with leading SHARP only pairs of hex characters are valid
  191. valid_hex = True
  192. if len(attribute_value) % 2 == 0: # string must be # + HEX HEX (an odd number of chars)
  193. valid_hex = False
  194. if valid_hex:
  195. for c in attribute_value:
  196. if c not in hexdigits: # allowed only hex digits as per RFC 4514
  197. valid_hex = False
  198. break
  199. if valid_hex:
  200. return attribute_value
  201. state = STATE_ANY
  202. escaped = ''
  203. tmp_buffer = ''
  204. for c in attribute_value:
  205. if state == STATE_ANY:
  206. if c == '\\':
  207. state = STATE_ESCAPE
  208. elif c in '"#+,;<=>\00':
  209. escaped += '\\' + c
  210. else:
  211. escaped += c
  212. elif state == STATE_ESCAPE:
  213. if c in hexdigits:
  214. tmp_buffer = c
  215. state = STATE_ESCAPE_HEX
  216. elif c in ' "#+,;<=>\\\00':
  217. escaped += '\\' + c
  218. state = STATE_ANY
  219. else:
  220. escaped += '\\\\' + c
  221. elif state == STATE_ESCAPE_HEX:
  222. if c in hexdigits:
  223. escaped += '\\' + tmp_buffer + c
  224. else:
  225. escaped += '\\\\' + tmp_buffer + c
  226. tmp_buffer = ''
  227. state = STATE_ANY
  228. # final state
  229. if state == STATE_ESCAPE:
  230. escaped += '\\\\'
  231. elif state == STATE_ESCAPE_HEX:
  232. escaped += '\\\\' + tmp_buffer
  233. if escaped[0] == ' ': # leading SPACE must be escaped
  234. escaped = '\\' + escaped
  235. if escaped[-1] == ' ' and len(escaped) > 1 and escaped[-2] != '\\': # trailing SPACE must be escaped
  236. escaped = escaped[:-1] + '\\ '
  237. return escaped
  238. def parse_dn(dn, escape=False, strip=False):
  239. """
  240. Parses a DN into syntactic components
  241. :param dn:
  242. :param escape:
  243. :param strip:
  244. :return:
  245. a list of tripels representing `attributeTypeAndValue` elements
  246. containing `attributeType`, `attributeValue` and the following separator (`COMMA` or `PLUS`) if given, else an empty `str`.
  247. in their original representation, still containing escapes or encoded as hex.
  248. """
  249. rdns = []
  250. avas = []
  251. while dn:
  252. ava, separator = _get_next_ava(dn) # if returned ava doesn't containg any unescaped equal it'a appended to last ava in avas
  253. dn = dn[len(ava) + 1:]
  254. if _find_first_unescaped(ava, '=', 0) > 0 or len(avas) == 0:
  255. avas.append((ava, separator))
  256. else:
  257. avas[len(avas) - 1] = (avas[len(avas) - 1][0] + avas[len(avas) - 1][1] + ava, separator)
  258. for ava, separator in avas:
  259. attribute_type, attribute_value = _split_ava(ava, escape, strip)
  260. if not _validate_attribute_type(attribute_type):
  261. raise LDAPInvalidDnError('unable to validate attribute type in ' + ava)
  262. if not _validate_attribute_value(attribute_value):
  263. raise LDAPInvalidDnError('unable to validate attribute value in ' + ava)
  264. rdns.append((attribute_type, attribute_value, separator))
  265. dn = dn[len(ava) + 1:]
  266. if not rdns:
  267. raise LDAPInvalidDnError('empty dn')
  268. return rdns
  269. def safe_dn(dn, decompose=False, reverse=False):
  270. """
  271. normalize and escape a dn, if dn is a sequence it is joined.
  272. the reverse parameter changes the join direction of the sequence
  273. """
  274. if isinstance(dn, SEQUENCE_TYPES):
  275. components = [rdn for rdn in dn]
  276. if reverse:
  277. dn = ','.join(reversed(components))
  278. else:
  279. dn = ','.join(components)
  280. if decompose:
  281. escaped_dn = []
  282. else:
  283. escaped_dn = ''
  284. if dn.startswith('<GUID=') and dn.endswith('>'): # Active Directory allows looking up objects by putting its GUID in a specially-formatted DN (e.g. '<GUID=7b95f0d5-a3ed-486c-919c-077b8c9731f2>')
  285. escaped_dn = dn
  286. elif dn.startswith('<WKGUID=') and dn.endswith('>'): # Active Directory allows Binding to Well-Known Objects Using WKGUID in a specially-formatted DN (e.g. <WKGUID=a9d1ca15768811d1aded00c04fd8d5cd,dc=Fabrikam,dc=com>)
  287. escaped_dn = dn
  288. elif '@' not in dn: # active directory UPN (User Principal Name) consist of an account, the at sign (@) and a domain, or the domain level logn name domain\username
  289. for component in parse_dn(dn, escape=True):
  290. if decompose:
  291. escaped_dn.append((component[0], component[1], component[2]))
  292. else:
  293. escaped_dn += component[0] + '=' + component[1] + component[2]
  294. elif '@' in dn and '=' not in dn and len(dn.split('@')) != 2:
  295. raise LDAPInvalidDnError('Active Directory User Principal Name must consist of name@domain')
  296. elif '\\' in dn and '=' not in dn and len(dn.split('\\')) != 2:
  297. raise LDAPInvalidDnError('Active Directory Domain Level Logon Name must consist of name\\domain')
  298. else:
  299. escaped_dn = dn
  300. return escaped_dn
  301. def safe_rdn(dn, decompose=False):
  302. """Returns a list of rdn for the dn, usually there is only one rdn, but it can be more than one when the + sign is used"""
  303. escaped_rdn = []
  304. one_more = True
  305. for component in parse_dn(dn, escape=True):
  306. if component[2] == '+' or one_more:
  307. if decompose:
  308. escaped_rdn.append((component[0], component[1]))
  309. else:
  310. escaped_rdn.append(component[0] + '=' + component[1])
  311. if component[2] == '+':
  312. one_more = True
  313. else:
  314. one_more = False
  315. break
  316. if one_more:
  317. raise LDAPInvalidDnError('bad dn ' + str(dn))
  318. return escaped_rdn
  319. def escape_rdn(rdn):
  320. """
  321. Escape rdn characters to prevent injection according to RFC 4514.
  322. """
  323. # '/' must be handled first or the escape slashes will be escaped!
  324. for char in ['\\', ',', '+', '"', '<', '>', ';', '=', '\x00']:
  325. rdn = rdn.replace(char, '\\' + char)
  326. if rdn[0] == '#' or rdn[0] == ' ':
  327. rdn = ''.join(('\\', rdn))
  328. if rdn[-1] == ' ':
  329. rdn = ''.join((rdn[:-1], '\\ '))
  330. return rdn