ldapurl.py 11 KB

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