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.

subentry.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. """
  2. ldap.schema.subentry - subschema subentry handling
  3. See https://www.python-ldap.org/ for details.
  4. """
  5. import copy
  6. import ldap.cidict,ldap.schema
  7. from ldap.compat import urlopen
  8. from ldap.schema.models import *
  9. import ldapurl
  10. import ldif
  11. SCHEMA_CLASS_MAPPING = ldap.cidict.cidict()
  12. SCHEMA_ATTR_MAPPING = {}
  13. for o in list(vars().values()):
  14. if hasattr(o,'schema_attribute'):
  15. SCHEMA_CLASS_MAPPING[o.schema_attribute] = o
  16. SCHEMA_ATTR_MAPPING[o] = o.schema_attribute
  17. SCHEMA_ATTRS = SCHEMA_CLASS_MAPPING.keys()
  18. class SubschemaError(ValueError):
  19. pass
  20. class OIDNotUnique(SubschemaError):
  21. def __init__(self,desc):
  22. self.desc = desc
  23. def __str__(self):
  24. return 'OID not unique for %s' % (self.desc)
  25. class NameNotUnique(SubschemaError):
  26. def __init__(self,desc):
  27. self.desc = desc
  28. def __str__(self):
  29. return 'NAME not unique for %s' % (self.desc)
  30. class SubSchema:
  31. """
  32. Arguments:
  33. sub_schema_sub_entry
  34. Dictionary usually returned by LDAP search or the LDIF parser
  35. containing the sub schema sub entry
  36. check_uniqueness
  37. Defines whether uniqueness of OIDs and NAME is checked.
  38. 0
  39. no check
  40. 1
  41. check but add schema description with work-around
  42. 2
  43. check and raise exception if non-unique OID or NAME is found
  44. Class attributes:
  45. sed
  46. Dictionary holding the subschema information as pre-parsed
  47. SchemaElement objects (do not access directly!)
  48. name2oid
  49. Dictionary holding the mapping from NAMEs to OIDs
  50. (do not access directly!)
  51. non_unique_oids
  52. List of OIDs used at least twice in the subschema
  53. non_unique_names
  54. List of NAMEs used at least twice in the subschema for the same schema element
  55. """
  56. def __init__(self,sub_schema_sub_entry,check_uniqueness=1):
  57. # Initialize all dictionaries
  58. self.name2oid = {}
  59. self.sed = {}
  60. self.non_unique_oids = {}
  61. self.non_unique_names = {}
  62. for c in SCHEMA_CLASS_MAPPING.values():
  63. self.name2oid[c] = ldap.cidict.cidict()
  64. self.sed[c] = {}
  65. self.non_unique_names[c] = ldap.cidict.cidict()
  66. # Transform entry dict to case-insensitive dict
  67. e = ldap.cidict.cidict(sub_schema_sub_entry)
  68. # Build the schema registry in dictionaries
  69. for attr_type in SCHEMA_ATTRS:
  70. for attr_value in filter(None,e.get(attr_type,[])):
  71. se_class = SCHEMA_CLASS_MAPPING[attr_type]
  72. se_instance = se_class(attr_value)
  73. se_id = se_instance.get_id()
  74. if check_uniqueness and se_id in self.sed[se_class]:
  75. self.non_unique_oids[se_id] = None
  76. if check_uniqueness==1:
  77. # Add to subschema by adding suffix to ID
  78. suffix_counter = 1
  79. new_se_id = se_id
  80. while new_se_id in self.sed[se_class]:
  81. new_se_id = ';'.join((se_id,str(suffix_counter)))
  82. suffix_counter += 1
  83. else:
  84. se_id = new_se_id
  85. elif check_uniqueness>=2:
  86. raise OIDNotUnique(attr_value)
  87. # Store the schema element instance in the central registry
  88. self.sed[se_class][se_id] = se_instance
  89. if hasattr(se_instance,'names'):
  90. for name in ldap.cidict.cidict({}.fromkeys(se_instance.names)).keys():
  91. if check_uniqueness and name in self.name2oid[se_class]:
  92. self.non_unique_names[se_class][se_id] = None
  93. raise NameNotUnique(attr_value)
  94. else:
  95. self.name2oid[se_class][name] = se_id
  96. # Turn dict into list maybe more handy for applications
  97. self.non_unique_oids = self.non_unique_oids.keys()
  98. return # subSchema.__init__()
  99. def ldap_entry(self):
  100. """
  101. Returns a dictionary containing the sub schema sub entry
  102. """
  103. # Initialize the dictionary with empty lists
  104. entry = {}
  105. # Collect the schema elements and store them in
  106. # entry's attributes
  107. for se_class, elements in self.sed.items():
  108. for se in elements.values():
  109. se_str = str(se)
  110. try:
  111. entry[SCHEMA_ATTR_MAPPING[se_class]].append(se_str)
  112. except KeyError:
  113. entry[SCHEMA_ATTR_MAPPING[se_class]] = [ se_str ]
  114. return entry
  115. def listall(self,schema_element_class,schema_element_filters=None):
  116. """
  117. Returns a list of OIDs of all available schema
  118. elements of a given schema element class.
  119. """
  120. avail_se = self.sed[schema_element_class]
  121. if schema_element_filters:
  122. result = []
  123. for se_key, se in avail_se.items():
  124. for fk,fv in schema_element_filters:
  125. try:
  126. if getattr(se,fk) in fv:
  127. result.append(se_key)
  128. except AttributeError:
  129. pass
  130. else:
  131. result = avail_se.keys()
  132. return result
  133. def tree(self,schema_element_class,schema_element_filters=None):
  134. """
  135. Returns a ldap.cidict.cidict dictionary representing the
  136. tree structure of the schema elements.
  137. """
  138. assert schema_element_class in [ObjectClass,AttributeType]
  139. avail_se = self.listall(schema_element_class,schema_element_filters)
  140. top_node = '_'
  141. tree = ldap.cidict.cidict({top_node:[]})
  142. # 1. Pass: Register all nodes
  143. for se in avail_se:
  144. tree[se] = []
  145. # 2. Pass: Register all sup references
  146. for se_oid in avail_se:
  147. se_obj = self.get_obj(schema_element_class,se_oid,None)
  148. if se_obj.__class__!=schema_element_class:
  149. # Ignore schema elements not matching schema_element_class.
  150. # This helps with falsely assigned OIDs.
  151. continue
  152. assert se_obj.__class__==schema_element_class, \
  153. "Schema element referenced by %s must be of class %s but was %s" % (
  154. se_oid,schema_element_class.__name__,se_obj.__class__
  155. )
  156. for s in se_obj.sup or ('_',):
  157. sup_oid = self.getoid(schema_element_class,s)
  158. try:
  159. tree[sup_oid].append(se_oid)
  160. except:
  161. pass
  162. return tree
  163. def getoid(self,se_class,nameoroid,raise_keyerror=0):
  164. """
  165. Get an OID by name or OID
  166. """
  167. nameoroid_stripped = nameoroid.split(';')[0].strip()
  168. if nameoroid_stripped in self.sed[se_class]:
  169. # name_or_oid is already a registered OID
  170. return nameoroid_stripped
  171. else:
  172. try:
  173. result_oid = self.name2oid[se_class][nameoroid_stripped]
  174. except KeyError:
  175. if raise_keyerror:
  176. raise KeyError('No registered %s-OID for nameoroid %s' % (se_class.__name__,repr(nameoroid_stripped)))
  177. else:
  178. result_oid = nameoroid_stripped
  179. return result_oid
  180. def get_inheritedattr(self,se_class,nameoroid,name):
  181. """
  182. Get a possibly inherited attribute specified by name
  183. of a schema element specified by nameoroid.
  184. Returns None if class attribute is not set at all.
  185. Raises KeyError if no schema element is found by nameoroid.
  186. """
  187. se = self.sed[se_class][self.getoid(se_class,nameoroid)]
  188. try:
  189. result = getattr(se,name)
  190. except AttributeError:
  191. result = None
  192. if result is None and se.sup:
  193. result = self.get_inheritedattr(se_class,se.sup[0],name)
  194. return result
  195. def get_obj(self,se_class,nameoroid,default=None,raise_keyerror=0):
  196. """
  197. Get a schema element by name or OID
  198. """
  199. se_oid = self.getoid(se_class,nameoroid)
  200. try:
  201. se_obj = self.sed[se_class][se_oid]
  202. except KeyError:
  203. if raise_keyerror:
  204. raise KeyError('No ldap.schema.%s instance with nameoroid %s and se_oid %s' % (
  205. se_class.__name__,repr(nameoroid),repr(se_oid))
  206. )
  207. else:
  208. se_obj = default
  209. return se_obj
  210. def get_inheritedobj(self,se_class,nameoroid,inherited=None):
  211. """
  212. Get a schema element by name or OID with all class attributes
  213. set including inherited class attributes
  214. """
  215. inherited = inherited or []
  216. se = copy.copy(self.sed[se_class].get(self.getoid(se_class,nameoroid)))
  217. if se and hasattr(se,'sup'):
  218. for class_attr_name in inherited:
  219. setattr(se,class_attr_name,self.get_inheritedattr(se_class,nameoroid,class_attr_name))
  220. return se
  221. def get_syntax(self,nameoroid):
  222. """
  223. Get the syntax of an attribute type specified by name or OID
  224. """
  225. at_oid = self.getoid(AttributeType,nameoroid)
  226. try:
  227. at_obj = self.get_inheritedobj(AttributeType,at_oid)
  228. except KeyError:
  229. return None
  230. else:
  231. return at_obj.syntax
  232. def get_structural_oc(self,oc_list):
  233. """
  234. Returns OID of structural object class in oc_list
  235. if any is present. Returns None else.
  236. """
  237. # Get tree of all STRUCTURAL object classes
  238. oc_tree = self.tree(ObjectClass,[('kind',[0])])
  239. # Filter all STRUCTURAL object classes
  240. struct_ocs = {}
  241. for oc_nameoroid in oc_list:
  242. oc_se = self.get_obj(ObjectClass,oc_nameoroid,None)
  243. if oc_se and oc_se.kind==0:
  244. struct_ocs[oc_se.oid] = None
  245. result = None
  246. # Build a copy of the oid list, to be cleaned as we go.
  247. struct_oc_list = list(struct_ocs)
  248. while struct_oc_list:
  249. oid = struct_oc_list.pop()
  250. for child_oid in oc_tree[oid]:
  251. if self.getoid(ObjectClass,child_oid) in struct_ocs:
  252. break
  253. else:
  254. result = oid
  255. return result
  256. def get_applicable_aux_classes(self,nameoroid):
  257. """
  258. Return a list of the applicable AUXILIARY object classes
  259. for a STRUCTURAL object class specified by 'nameoroid'
  260. if the object class is governed by a DIT content rule.
  261. If there's no DIT content rule all available AUXILIARY
  262. object classes are returned.
  263. """
  264. content_rule = self.get_obj(DITContentRule,nameoroid)
  265. if content_rule:
  266. # Return AUXILIARY object classes from DITContentRule instance
  267. return content_rule.aux
  268. else:
  269. # list all AUXILIARY object classes
  270. return self.listall(ObjectClass,[('kind',[2])])
  271. def attribute_types(
  272. self,object_class_list,attr_type_filter=None,raise_keyerror=1,ignore_dit_content_rule=0
  273. ):
  274. """
  275. Returns a 2-tuple of all must and may attributes including
  276. all inherited attributes of superior object classes
  277. by walking up classes along the SUP attribute.
  278. The attributes are stored in a ldap.cidict.cidict dictionary.
  279. object_class_list
  280. list of strings specifying object class names or OIDs
  281. attr_type_filter
  282. list of 2-tuples containing lists of class attributes
  283. which has to be matched
  284. raise_keyerror
  285. All KeyError exceptions for non-existent schema elements
  286. are ignored
  287. ignore_dit_content_rule
  288. A DIT content rule governing the structural object class
  289. is ignored
  290. """
  291. AttributeType = ldap.schema.AttributeType
  292. ObjectClass = ldap.schema.ObjectClass
  293. # Map object_class_list to object_class_oids (list of OIDs)
  294. object_class_oids = [
  295. self.getoid(ObjectClass,o)
  296. for o in object_class_list
  297. ]
  298. # Initialize
  299. oid_cache = {}
  300. r_must,r_may = ldap.cidict.cidict(),ldap.cidict.cidict()
  301. if '1.3.6.1.4.1.1466.101.120.111' in object_class_oids:
  302. # Object class 'extensibleObject' MAY carry every attribute type
  303. for at_obj in self.sed[AttributeType].values():
  304. r_may[at_obj.oid] = at_obj
  305. # Loop over OIDs of all given object classes
  306. while object_class_oids:
  307. object_class_oid = object_class_oids.pop(0)
  308. # Check whether the objectClass with this OID
  309. # has already been processed
  310. if object_class_oid in oid_cache:
  311. continue
  312. # Cache this OID as already being processed
  313. oid_cache[object_class_oid] = None
  314. try:
  315. object_class = self.sed[ObjectClass][object_class_oid]
  316. except KeyError:
  317. if raise_keyerror:
  318. raise
  319. # Ignore this object class
  320. continue
  321. assert isinstance(object_class,ObjectClass)
  322. assert hasattr(object_class,'must'),ValueError(object_class_oid)
  323. assert hasattr(object_class,'may'),ValueError(object_class_oid)
  324. for a in object_class.must:
  325. se_oid = self.getoid(AttributeType,a,raise_keyerror=raise_keyerror)
  326. r_must[se_oid] = self.get_obj(AttributeType,se_oid,raise_keyerror=raise_keyerror)
  327. for a in object_class.may:
  328. se_oid = self.getoid(AttributeType,a,raise_keyerror=raise_keyerror)
  329. r_may[se_oid] = self.get_obj(AttributeType,se_oid,raise_keyerror=raise_keyerror)
  330. object_class_oids.extend([
  331. self.getoid(ObjectClass,o)
  332. for o in object_class.sup
  333. ])
  334. # Process DIT content rules
  335. if not ignore_dit_content_rule:
  336. structural_oc = self.get_structural_oc(object_class_list)
  337. if structural_oc:
  338. # Process applicable DIT content rule
  339. try:
  340. dit_content_rule = self.get_obj(DITContentRule,structural_oc,raise_keyerror=1)
  341. except KeyError:
  342. # Not DIT content rule found for structural objectclass
  343. pass
  344. else:
  345. for a in dit_content_rule.must:
  346. se_oid = self.getoid(AttributeType,a,raise_keyerror=raise_keyerror)
  347. r_must[se_oid] = self.get_obj(AttributeType,se_oid,raise_keyerror=raise_keyerror)
  348. for a in dit_content_rule.may:
  349. se_oid = self.getoid(AttributeType,a,raise_keyerror=raise_keyerror)
  350. r_may[se_oid] = self.get_obj(AttributeType,se_oid,raise_keyerror=raise_keyerror)
  351. for a in dit_content_rule.nots:
  352. a_oid = self.getoid(AttributeType,a,raise_keyerror=raise_keyerror)
  353. try:
  354. del r_may[a_oid]
  355. except KeyError:
  356. pass
  357. # Remove all mandantory attribute types from
  358. # optional attribute type list
  359. for a in list(r_may.keys()):
  360. if a in r_must:
  361. del r_may[a]
  362. # Apply attr_type_filter to results
  363. if attr_type_filter:
  364. for l in [r_must,r_may]:
  365. for a in list(l.keys()):
  366. for afk,afv in attr_type_filter:
  367. try:
  368. schema_attr_type = self.sed[AttributeType][a]
  369. except KeyError:
  370. if raise_keyerror:
  371. raise KeyError('No attribute type found in sub schema by name %s' % (a))
  372. # If there's no schema element for this attribute type
  373. # but still KeyError is to be ignored we filter it away
  374. del l[a]
  375. break
  376. else:
  377. if not getattr(schema_attr_type,afk) in afv:
  378. del l[a]
  379. break
  380. return r_must,r_may # attribute_types()
  381. def urlfetch(uri,trace_level=0):
  382. """
  383. Fetches a parsed schema entry by uri.
  384. If uri is a LDAP URL the LDAP server is queried directly.
  385. Otherwise uri is assumed to point to a LDIF file which
  386. is loaded with urllib.
  387. """
  388. uri = uri.strip()
  389. if uri.startswith(('ldap:', 'ldaps:', 'ldapi:')):
  390. ldap_url = ldapurl.LDAPUrl(uri)
  391. l=ldap.initialize(ldap_url.initializeUrl(),trace_level)
  392. l.protocol_version = ldap.VERSION3
  393. l.simple_bind_s(ldap_url.who or u'', ldap_url.cred or u'')
  394. subschemasubentry_dn = l.search_subschemasubentry_s(ldap_url.dn)
  395. if subschemasubentry_dn is None:
  396. s_temp = None
  397. else:
  398. if ldap_url.attrs is None:
  399. schema_attrs = SCHEMA_ATTRS
  400. else:
  401. schema_attrs = ldap_url.attrs
  402. s_temp = l.read_subschemasubentry_s(
  403. subschemasubentry_dn,attrs=schema_attrs
  404. )
  405. l.unbind_s()
  406. del l
  407. else:
  408. ldif_file = urlopen(uri)
  409. ldif_parser = ldif.LDIFRecordList(ldif_file,max_entries=1)
  410. ldif_parser.parse()
  411. subschemasubentry_dn,s_temp = ldif_parser.all_records[0]
  412. # Work-around for mixed-cased attribute names
  413. subschemasubentry_entry = ldap.cidict.cidict()
  414. s_temp = s_temp or {}
  415. for at,av in s_temp.items():
  416. if at in SCHEMA_CLASS_MAPPING:
  417. try:
  418. subschemasubentry_entry[at].extend(av)
  419. except KeyError:
  420. subschemasubentry_entry[at] = av
  421. # Finally parse the schema
  422. if subschemasubentry_dn!=None:
  423. parsed_sub_schema = ldap.schema.SubSchema(subschemasubentry_entry)
  424. else:
  425. parsed_sub_schema = None
  426. return subschemasubentry_dn, parsed_sub_schema