Development of an internal social media platform with personalised dashboards for students
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.

rfc2849.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. """
  2. """
  3. # Created on 2013.12.08
  4. #
  5. # Author: Giovanni Cannata
  6. #
  7. # Copyright 2013 - 2018 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 base64 import b64encode
  25. from datetime import datetime
  26. from .. import STRING_TYPES
  27. from ..core.exceptions import LDAPLDIFError, LDAPExtensionError
  28. from ..protocol.persistentSearch import EntryChangeNotificationControl
  29. from ..utils.asn1 import decoder
  30. # LDIF converter RFC 2849 compliant
  31. LDIF_LINE_LENGTH = 78
  32. def safe_ldif_string(bytes_value):
  33. if not bytes_value:
  34. return True
  35. # check SAFE-INIT-CHAR: < 127, not NUL, LF, CR, SPACE, COLON, LESS-THAN
  36. if bytes_value[0] > 127 or bytes_value[0] in [0, 10, 13, 32, 58, 60]:
  37. return False
  38. # check SAFE-CHAR: < 127 not NUL, LF, CR
  39. if 0 in bytes_value or 10 in bytes_value or 13 in bytes_value:
  40. return False
  41. # check last char for SPACE
  42. if bytes_value[-1] == 32:
  43. return False
  44. for byte in bytes_value:
  45. if byte > 127:
  46. return False
  47. return True
  48. def _convert_to_ldif(descriptor, value, base64):
  49. if not value:
  50. value = ''
  51. if isinstance(value, STRING_TYPES):
  52. value = bytearray(value, encoding='utf-8')
  53. if base64 or not safe_ldif_string(value):
  54. try:
  55. encoded = b64encode(value)
  56. except TypeError:
  57. encoded = b64encode(str(value)) # patch for Python 2.6
  58. if not isinstance(encoded, str): # in Python 3 b64encode returns bytes in Python 2 returns str
  59. encoded = str(encoded, encoding='ascii') # Python 3
  60. line = descriptor + ':: ' + encoded
  61. else:
  62. if str is not bytes: # Python 3
  63. value = str(value, encoding='ascii')
  64. else: # Python 2
  65. value = str(value)
  66. line = descriptor + ': ' + value
  67. return line
  68. def add_controls(controls, all_base64):
  69. lines = []
  70. if controls:
  71. for control in controls:
  72. line = 'control: ' + control[0]
  73. line += ' ' + ('true' if control[1] else 'false')
  74. if control[2]:
  75. lines.append(_convert_to_ldif(line, control[2], all_base64))
  76. return lines
  77. def add_attributes(attributes, all_base64):
  78. lines = []
  79. oc_attr = None
  80. # objectclass first, even if this is not specified in the RFC
  81. for attr in attributes:
  82. if attr.lower() == 'objectclass':
  83. for val in attributes[attr]:
  84. lines.append(_convert_to_ldif(attr, val, all_base64))
  85. oc_attr = attr
  86. break
  87. # remaining attributes
  88. for attr in attributes:
  89. if attr != oc_attr:
  90. for val in attributes[attr]:
  91. lines.append(_convert_to_ldif(attr, val, all_base64))
  92. return lines
  93. def sort_ldif_lines(lines, sort_order):
  94. # sort lines as per custom sort_order
  95. # sort order is a list of descriptors, lines will be sorted following the same sequence
  96. return sorted(lines, key=lambda x: ldif_sort(x, sort_order)) if sort_order else lines
  97. def search_response_to_ldif(entries, all_base64, sort_order=None):
  98. lines = []
  99. for entry in entries:
  100. if 'dn' in entry:
  101. lines.append(_convert_to_ldif('dn', entry['dn'], all_base64))
  102. lines.extend(add_attributes(entry['raw_attributes'], all_base64))
  103. else:
  104. raise LDAPLDIFError('unable to convert to LDIF-CONTENT - missing DN')
  105. if sort_order:
  106. lines = sort_ldif_lines(lines, sort_order)
  107. lines.append('')
  108. if lines:
  109. lines.append('# total number of entries: ' + str(len(entries)))
  110. return lines
  111. def add_request_to_ldif(entry, all_base64, sort_order=None):
  112. lines = []
  113. if 'entry' in entry:
  114. lines.append(_convert_to_ldif('dn', entry['entry'], all_base64))
  115. lines.extend(add_controls(entry['controls'], all_base64))
  116. lines.append('changetype: add')
  117. lines.extend(add_attributes(entry['attributes'], all_base64))
  118. if sort_order:
  119. lines = sort_ldif_lines(lines, sort_order)
  120. else:
  121. raise LDAPLDIFError('unable to convert to LDIF-CHANGE-ADD - missing DN ')
  122. return lines
  123. def delete_request_to_ldif(entry, all_base64, sort_order=None):
  124. lines = []
  125. if 'entry' in entry:
  126. lines.append(_convert_to_ldif('dn', entry['entry'], all_base64))
  127. lines.append(add_controls(entry['controls'], all_base64))
  128. lines.append('changetype: delete')
  129. if sort_order:
  130. lines = sort_ldif_lines(lines, sort_order)
  131. else:
  132. raise LDAPLDIFError('unable to convert to LDIF-CHANGE-DELETE - missing DN ')
  133. return lines
  134. def modify_request_to_ldif(entry, all_base64, sort_order=None):
  135. lines = []
  136. if 'entry' in entry:
  137. lines.append(_convert_to_ldif('dn', entry['entry'], all_base64))
  138. lines.extend(add_controls(entry['controls'], all_base64))
  139. lines.append('changetype: modify')
  140. if 'changes' in entry:
  141. for change in entry['changes']:
  142. lines.append(['add', 'delete', 'replace', 'increment'][change['operation']] + ': ' + change['attribute']['type'])
  143. for value in change['attribute']['value']:
  144. lines.append(_convert_to_ldif(change['attribute']['type'], value, all_base64))
  145. lines.append('-')
  146. if sort_order:
  147. lines = sort_ldif_lines(lines, sort_order)
  148. return lines
  149. def modify_dn_request_to_ldif(entry, all_base64, sort_order=None):
  150. lines = []
  151. if 'entry' in entry:
  152. lines.append(_convert_to_ldif('dn', entry['entry'], all_base64))
  153. lines.extend(add_controls(entry['controls'], all_base64))
  154. lines.append('changetype: modrdn') if 'newSuperior' in entry and entry['newSuperior'] else lines.append('changetype: moddn')
  155. lines.append(_convert_to_ldif('newrdn', entry['newRdn'], all_base64))
  156. lines.append('deleteoldrdn: ' + ('1' if entry['deleteOldRdn'] else '0'))
  157. if 'newSuperior' in entry and entry['newSuperior']:
  158. lines.append(_convert_to_ldif('newsuperior', entry['newSuperior'], all_base64))
  159. if sort_order:
  160. lines = sort_ldif_lines(lines, sort_order)
  161. else:
  162. raise LDAPLDIFError('unable to convert to LDIF-CHANGE-MODDN - missing DN ')
  163. return lines
  164. def operation_to_ldif(operation_type, entries, all_base64=False, sort_order=None):
  165. if operation_type == 'searchResponse':
  166. lines = search_response_to_ldif(entries, all_base64, sort_order)
  167. elif operation_type == 'addRequest':
  168. lines = add_request_to_ldif(entries, all_base64, sort_order)
  169. elif operation_type == 'delRequest':
  170. lines = delete_request_to_ldif(entries, all_base64, sort_order)
  171. elif operation_type == 'modifyRequest':
  172. lines = modify_request_to_ldif(entries, all_base64, sort_order)
  173. elif operation_type == 'modDNRequest':
  174. lines = modify_dn_request_to_ldif(entries, all_base64, sort_order)
  175. else:
  176. lines = []
  177. ldif_record = []
  178. # check max line length and split as per note 2 of RFC 2849
  179. for line in lines:
  180. if line:
  181. ldif_record.append(line[0:LDIF_LINE_LENGTH])
  182. ldif_record.extend([' ' + line[i: i + LDIF_LINE_LENGTH - 1] for i in range(LDIF_LINE_LENGTH, len(line), LDIF_LINE_LENGTH - 1)] if len(line) > LDIF_LINE_LENGTH else [])
  183. else:
  184. ldif_record.append('')
  185. return ldif_record
  186. def add_ldif_header(ldif_lines):
  187. if ldif_lines:
  188. ldif_lines.insert(0, 'version: 1')
  189. return ldif_lines
  190. def ldif_sort(line, sort_order):
  191. for i, descriptor in enumerate(sort_order):
  192. if line and line.startswith(descriptor):
  193. return i
  194. return len(sort_order) + 1
  195. def decode_persistent_search_control(change):
  196. if 'controls' in change and '2.16.840.1.113730.3.4.7' in change['controls']:
  197. decoded = dict()
  198. decoded_control, unprocessed = decoder.decode(change['controls']['2.16.840.1.113730.3.4.7']['value'], asn1Spec=EntryChangeNotificationControl())
  199. if unprocessed:
  200. raise LDAPExtensionError('unprocessed value in EntryChangeNotificationControl')
  201. if decoded_control['changeType'] == 1: # add
  202. decoded['changeType'] = 'add'
  203. elif decoded_control['changeType'] == 2: # delete
  204. decoded['changeType'] = 'delete'
  205. elif decoded_control['changeType'] == 4: # modify
  206. decoded['changeType'] = 'modify'
  207. elif decoded_control['changeType'] == 8: # modify_dn
  208. decoded['changeType'] = 'modify dn'
  209. else:
  210. raise LDAPExtensionError('unknown Persistent Search changeType ' + str(decoded_control['changeType']))
  211. decoded['changeNumber'] = decoded_control['changeNumber'] if 'changeNumber' in decoded_control else None
  212. decoded['previousDN'] = decoded_control['previousDN'] if 'previousDN' in decoded_control else None
  213. return decoded
  214. return None
  215. def persistent_search_response_to_ldif(change):
  216. ldif_lines = ['# ' + datetime.now().isoformat()]
  217. control = decode_persistent_search_control(change)
  218. if control:
  219. if control['changeNumber']:
  220. ldif_lines.append('# change number: ' + str(control['changeNumber']))
  221. ldif_lines.append(control['changeType'])
  222. if control['previousDN']:
  223. ldif_lines.append('# previous dn: ' + str(control['previousDN']))
  224. ldif_lines += operation_to_ldif('searchResponse', [change])
  225. return ldif_lines[:-1] # removes "total number of entries"