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.

ldapurl.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. """
  2. ldapurl - handling of LDAP URLs as described in RFC 4516
  3. See https://www.python-ldap.org/ for details.
  4. """
  5. __version__ = '3.1.0'
  6. __all__ = [
  7. # constants
  8. 'SEARCH_SCOPE','SEARCH_SCOPE_STR',
  9. 'LDAP_SCOPE_BASE','LDAP_SCOPE_ONELEVEL','LDAP_SCOPE_SUBTREE',
  10. # functions
  11. 'isLDAPUrl',
  12. # classes
  13. 'LDAPUrlExtension','LDAPUrlExtensions','LDAPUrl'
  14. ]
  15. from ldap.compat import UserDict, quote, unquote
  16. LDAP_SCOPE_BASE = 0
  17. LDAP_SCOPE_ONELEVEL = 1
  18. LDAP_SCOPE_SUBTREE = 2
  19. LDAP_SCOPE_SUBORDINATES = 3
  20. SEARCH_SCOPE_STR = {
  21. None:'',
  22. LDAP_SCOPE_BASE:'base',
  23. LDAP_SCOPE_ONELEVEL:'one',
  24. LDAP_SCOPE_SUBTREE:'sub',
  25. LDAP_SCOPE_SUBORDINATES:'subordinates',
  26. }
  27. SEARCH_SCOPE = {
  28. '':None,
  29. # the search scope strings defined in RFC 4516
  30. 'base':LDAP_SCOPE_BASE,
  31. 'one':LDAP_SCOPE_ONELEVEL,
  32. 'sub':LDAP_SCOPE_SUBTREE,
  33. # from draft-sermersheim-ldap-subordinate-scope
  34. 'subordinates':LDAP_SCOPE_SUBORDINATES,
  35. }
  36. # Some widely used types
  37. StringType = type('')
  38. TupleType=type(())
  39. def isLDAPUrl(s):
  40. """
  41. Returns 1 if s is a LDAP URL, 0 else
  42. """
  43. s_lower = s.lower()
  44. return \
  45. s_lower.startswith('ldap://') or \
  46. s_lower.startswith('ldaps://') or \
  47. s_lower.startswith('ldapi://')
  48. def ldapUrlEscape(s):
  49. """Returns URL encoding of string s"""
  50. return quote(s).replace(',','%2C').replace('/','%2F')
  51. class LDAPUrlExtension(object):
  52. """
  53. Class for parsing and unparsing LDAP URL extensions
  54. as described in RFC 4516.
  55. Usable class attributes:
  56. critical
  57. Boolean integer marking the extension as critical
  58. extype
  59. Type of extension
  60. exvalue
  61. Value of extension
  62. """
  63. def __init__(self,extensionStr=None,critical=0,extype=None,exvalue=None):
  64. self.critical = critical
  65. self.extype = extype
  66. self.exvalue = exvalue
  67. if extensionStr:
  68. self._parse(extensionStr)
  69. def _parse(self,extension):
  70. extension = extension.strip()
  71. if not extension:
  72. # Don't parse empty strings
  73. self.extype,self.exvalue = None,None
  74. return
  75. self.critical = extension[0]=='!'
  76. if extension[0]=='!':
  77. extension = extension[1:].strip()
  78. try:
  79. self.extype,self.exvalue = extension.split('=',1)
  80. except ValueError:
  81. # No value, just the extype
  82. self.extype,self.exvalue = extension,None
  83. else:
  84. self.exvalue = unquote(self.exvalue.strip())
  85. self.extype = self.extype.strip()
  86. def unparse(self):
  87. if self.exvalue is None:
  88. return '%s%s' % ('!'*(self.critical>0),self.extype)
  89. else:
  90. return '%s%s=%s' % (
  91. '!'*(self.critical>0),
  92. self.extype,quote(self.exvalue or '')
  93. )
  94. def __str__(self):
  95. return self.unparse()
  96. def __repr__(self):
  97. return '<%s.%s instance at %s: %s>' % (
  98. self.__class__.__module__,
  99. self.__class__.__name__,
  100. hex(id(self)),
  101. self.__dict__
  102. )
  103. def __eq__(self,other):
  104. return \
  105. (self.critical==other.critical) and \
  106. (self.extype==other.extype) and \
  107. (self.exvalue==other.exvalue)
  108. def __ne__(self,other):
  109. return not self.__eq__(other)
  110. class LDAPUrlExtensions(UserDict):
  111. """
  112. Models a collection of LDAP URL extensions as
  113. dictionary type
  114. """
  115. def __init__(self,default=None):
  116. UserDict.__init__(self)
  117. for k,v in (default or {}).items():
  118. self[k]=v
  119. def __setitem__(self,name,value):
  120. """
  121. value
  122. Either LDAPUrlExtension instance, (critical,exvalue)
  123. or string'ed exvalue
  124. """
  125. assert isinstance(value,LDAPUrlExtension)
  126. assert name==value.extype
  127. self.data[name] = value
  128. def values(self):
  129. return [
  130. self[k]
  131. for k in self.keys()
  132. ]
  133. def __str__(self):
  134. return ','.join(str(v) for v in self.values())
  135. def __repr__(self):
  136. return '<%s.%s instance at %s: %s>' % (
  137. self.__class__.__module__,
  138. self.__class__.__name__,
  139. hex(id(self)),
  140. self.data
  141. )
  142. def __eq__(self,other):
  143. assert isinstance(other,self.__class__),TypeError(
  144. "other has to be instance of %s" % (self.__class__)
  145. )
  146. return self.data==other.data
  147. def parse(self,extListStr):
  148. for extension_str in extListStr.strip().split(','):
  149. if extension_str:
  150. e = LDAPUrlExtension(extension_str)
  151. self[e.extype] = e
  152. def unparse(self):
  153. return ','.join([ v.unparse() for v in self.values() ])
  154. class LDAPUrl(object):
  155. """
  156. Class for parsing and unparsing LDAP URLs
  157. as described in RFC 4516.
  158. Usable class attributes:
  159. urlscheme
  160. URL scheme (either ldap, ldaps or ldapi)
  161. hostport
  162. LDAP host (default '')
  163. dn
  164. String holding distinguished name (default '')
  165. attrs
  166. list of attribute types (default None)
  167. scope
  168. integer search scope for ldap-module
  169. filterstr
  170. String representation of LDAP Search Filters
  171. (see RFC 4515)
  172. extensions
  173. Dictionary used as extensions store
  174. who
  175. Maps automagically to bindname LDAP URL extension
  176. cred
  177. Maps automagically to X-BINDPW LDAP URL extension
  178. """
  179. attr2extype = {'who':'bindname','cred':'X-BINDPW'}
  180. def __init__(
  181. self,
  182. ldapUrl=None,
  183. urlscheme='ldap',
  184. hostport='',dn='',attrs=None,scope=None,filterstr=None,
  185. extensions=None,
  186. who=None,cred=None
  187. ):
  188. self.urlscheme=urlscheme
  189. self.hostport=hostport
  190. self.dn=dn
  191. self.attrs=attrs
  192. self.scope=scope
  193. self.filterstr=filterstr
  194. self.extensions=(extensions or LDAPUrlExtensions({}))
  195. if ldapUrl!=None:
  196. self._parse(ldapUrl)
  197. if who!=None:
  198. self.who = who
  199. if cred!=None:
  200. self.cred = cred
  201. def __eq__(self,other):
  202. return \
  203. self.urlscheme==other.urlscheme and \
  204. self.hostport==other.hostport and \
  205. self.dn==other.dn and \
  206. self.attrs==other.attrs and \
  207. self.scope==other.scope and \
  208. self.filterstr==other.filterstr and \
  209. self.extensions==other.extensions
  210. def __ne__(self,other):
  211. return not self.__eq__(other)
  212. def _parse(self,ldap_url):
  213. """
  214. parse a LDAP URL and set the class attributes
  215. urlscheme,host,dn,attrs,scope,filterstr,extensions
  216. """
  217. if not isLDAPUrl(ldap_url):
  218. raise ValueError('Value %s for ldap_url does not seem to be a LDAP URL.' % (repr(ldap_url)))
  219. scheme,rest = ldap_url.split('://',1)
  220. self.urlscheme = scheme.strip()
  221. if not self.urlscheme in ['ldap','ldaps','ldapi']:
  222. raise ValueError('LDAP URL contains unsupported URL scheme %s.' % (self.urlscheme))
  223. slash_pos = rest.find('/')
  224. qemark_pos = rest.find('?')
  225. if (slash_pos==-1) and (qemark_pos==-1):
  226. # No / and ? found at all
  227. self.hostport = unquote(rest)
  228. self.dn = ''
  229. return
  230. else:
  231. if slash_pos!=-1 and (qemark_pos==-1 or (slash_pos<qemark_pos)):
  232. # Slash separates DN from hostport
  233. self.hostport = unquote(rest[:slash_pos])
  234. # Eat the slash from rest
  235. rest = rest[slash_pos+1:]
  236. elif qemark_pos!=1 and (slash_pos==-1 or (slash_pos>qemark_pos)):
  237. # Question mark separates hostport from rest, DN is assumed to be empty
  238. self.hostport = unquote(rest[:qemark_pos])
  239. # Do not eat question mark
  240. rest = rest[qemark_pos:]
  241. else:
  242. raise ValueError('Something completely weird happened!')
  243. paramlist=rest.split('?',4)
  244. paramlist_len = len(paramlist)
  245. if paramlist_len>=1:
  246. self.dn = unquote(paramlist[0]).strip()
  247. if (paramlist_len>=2) and (paramlist[1]):
  248. self.attrs = unquote(paramlist[1].strip()).split(',')
  249. if paramlist_len>=3:
  250. scope = paramlist[2].strip()
  251. try:
  252. self.scope = SEARCH_SCOPE[scope]
  253. except KeyError:
  254. raise ValueError('Invalid search scope %s' % (repr(scope)))
  255. if paramlist_len>=4:
  256. filterstr = paramlist[3].strip()
  257. if not filterstr:
  258. self.filterstr = None
  259. else:
  260. self.filterstr = unquote(filterstr)
  261. if paramlist_len>=5:
  262. if paramlist[4]:
  263. self.extensions = LDAPUrlExtensions()
  264. self.extensions.parse(paramlist[4])
  265. else:
  266. self.extensions = None
  267. return
  268. def applyDefaults(self,defaults):
  269. """
  270. Apply defaults to all class attributes which are None.
  271. defaults
  272. Dictionary containing a mapping from class attributes
  273. to default values
  274. """
  275. for k, value in defaults.items():
  276. if getattr(self,k) is None:
  277. setattr(self, k, value)
  278. def initializeUrl(self):
  279. """
  280. Returns LDAP URL suitable to be passed to ldap.initialize()
  281. """
  282. if self.urlscheme=='ldapi':
  283. # hostport part might contain slashes when ldapi:// is used
  284. hostport = ldapUrlEscape(self.hostport)
  285. else:
  286. hostport = self.hostport
  287. return '%s://%s' % (self.urlscheme,hostport)
  288. def unparse(self):
  289. """
  290. Returns LDAP URL depending on class attributes set.
  291. """
  292. if self.attrs is None:
  293. attrs_str = ''
  294. else:
  295. attrs_str = ','.join(self.attrs)
  296. scope_str = SEARCH_SCOPE_STR[self.scope]
  297. if self.filterstr is None:
  298. filterstr = ''
  299. else:
  300. filterstr = ldapUrlEscape(self.filterstr)
  301. dn = ldapUrlEscape(self.dn)
  302. if self.urlscheme=='ldapi':
  303. # hostport part might contain slashes when ldapi:// is used
  304. hostport = ldapUrlEscape(self.hostport)
  305. else:
  306. hostport = self.hostport
  307. ldap_url = '%s://%s/%s?%s?%s?%s' % (
  308. self.urlscheme,
  309. hostport,dn,attrs_str,scope_str,filterstr
  310. )
  311. if self.extensions:
  312. ldap_url = ldap_url+'?'+self.extensions.unparse()
  313. return ldap_url
  314. def htmlHREF(self,urlPrefix='',hrefText=None,hrefTarget=None):
  315. """
  316. Returns a string with HTML link for this LDAP URL.
  317. urlPrefix
  318. Prefix before LDAP URL (e.g. for addressing another web-based client)
  319. hrefText
  320. link text/description
  321. hrefTarget
  322. string added as link target attribute
  323. """
  324. assert type(urlPrefix)==StringType, "urlPrefix must be StringType"
  325. if hrefText is None:
  326. hrefText = self.unparse()
  327. assert type(hrefText)==StringType, "hrefText must be StringType"
  328. if hrefTarget is None:
  329. target = ''
  330. else:
  331. assert type(hrefTarget)==StringType, "hrefTarget must be StringType"
  332. target = ' target="%s"' % hrefTarget
  333. return '<a%s href="%s%s">%s</a>' % (
  334. target,urlPrefix,self.unparse(),hrefText
  335. )
  336. def __str__(self):
  337. return self.unparse()
  338. def __repr__(self):
  339. return '<%s.%s instance at %s: %s>' % (
  340. self.__class__.__module__,
  341. self.__class__.__name__,
  342. hex(id(self)),
  343. self.__dict__
  344. )
  345. def __getattr__(self,name):
  346. if name in self.attr2extype:
  347. extype = self.attr2extype[name]
  348. if self.extensions and \
  349. extype in self.extensions and \
  350. not self.extensions[extype].exvalue is None:
  351. result = unquote(self.extensions[extype].exvalue)
  352. else:
  353. return None
  354. else:
  355. raise AttributeError('%s has no attribute %s' % (
  356. self.__class__.__name__,name
  357. ))
  358. return result # __getattr__()
  359. def __setattr__(self,name,value):
  360. if name in self.attr2extype:
  361. extype = self.attr2extype[name]
  362. if value is None:
  363. # A value of None means that extension is deleted
  364. delattr(self,name)
  365. elif value!=None:
  366. # Add appropriate extension
  367. self.extensions[extype] = LDAPUrlExtension(
  368. extype=extype,exvalue=unquote(value)
  369. )
  370. else:
  371. self.__dict__[name] = value
  372. def __delattr__(self,name):
  373. if name in self.attr2extype:
  374. extype = self.attr2extype[name]
  375. if self.extensions:
  376. try:
  377. del self.extensions[extype]
  378. except KeyError:
  379. pass
  380. else:
  381. del self.__dict__[name]