Package ldaptor :: Package protocols :: Package ldap :: Module ldapsyntax
[hide private]
[frames] | no frames]

Source Code for Module ldaptor.protocols.ldap.ldapsyntax

  1  """Pythonic API for LDAP operations.""" 
  2   
  3  from zope.interface import implements 
  4  from twisted.internet import defer 
  5  from twisted.python.failure import Failure 
  6  from ldaptor.protocols.ldap import ldapclient, ldif, distinguishedname, ldaperrors 
  7  from ldaptor.protocols import pureldap, pureber 
  8  from ldaptor.samba import smbpassword 
  9  from ldaptor import ldapfilter, interfaces, delta, attributeset, entry 
 10   
11 -class PasswordSetAggregateError(Exception):
12 """Some of the password plugins failed"""
13 - def __init__(self, errors):
14 Exception.__init__(self) 15 self.errors=errors
16
17 - def __str__(self):
18 return '%s: %s.' % ( 19 self.__doc__, 20 '; '.join([ '%s failed with %s' % (name, fail.getErrorMessage()) 21 for name, fail in self.errors]))
22
23 - def __repr__(self):
24 return '<'+self.__class__.__name__+' errors='+repr(self.errors)+'>'
25
26 -class PasswordSetAborted(Exception):
27 """Aborted""" 28
29 - def __str__(self):
30 return self.__doc__
31
32 -class DNNotPresentError(Exception):
33 """The requested DN cannot be found by the server.""" 34 pass
35
36 -class ObjectInBadStateError(Exception):
37 """The LDAP object in in a bad state.""" 38 pass
39
40 -class ObjectDeletedError(ObjectInBadStateError):
41 """The LDAP object has already been removed, unable to perform operations on it.""" 42 pass
43
44 -class ObjectDirtyError(ObjectInBadStateError):
45 """The LDAP object has a journal which needs to be committed or undone before this operation.""" 46 pass
47
48 -class NoContainingNamingContext(Exception):
49 """The server contains to LDAP naming context that would contain this object.""" 50 pass
51
52 -class CannotRemoveRDNError(Exception):
53 """The attribute to be removed is the RDN for the object and cannot be removed."""
54 - def __init__(self, key, val=None):
55 Exception.__init__(self) 56 self.key=key 57 self.val=val
58
59 - def __str__(self):
60 if self.val is None: 61 r=repr(self.key) 62 else: 63 r='%s=%s' % (repr(self.key), repr(self.val)) 64 return """The attribute to be removed, %s, is the RDN for the object and cannot be removed.""" % r
65
66 -class MatchNotImplemented(NotImplementedError):
67 """Match type not implemented"""
68 - def __init__(self, op):
69 Exception.__init__(self) 70 self.op=op
71
72 - def __str__(self):
73 return '%s: %r' % (self.__doc__, self.op)
74
75 -class JournaledLDAPAttributeSet(attributeset.LDAPAttributeSet):
76 - def __init__(self, ldapObject, *a, **kw):
77 self.ldapObject = ldapObject 78 super(JournaledLDAPAttributeSet, self).__init__(*a, **kw)
79
80 - def add(self, value):
81 self.ldapObject.journal(delta.Add(self.key, [value])) 82 super(JournaledLDAPAttributeSet, self).add(value)
83
84 - def update(self, sequence):
85 self.ldapObject.journal(delta.Add(self.key, sequence)) 86 super(JournaledLDAPAttributeSet, self).update(sequence)
87
88 - def remove(self, value):
89 if value not in self: 90 raise LookupError, value 91 self.ldapObject._canRemove(self.key, value) 92 self.ldapObject.journal(delta.Delete(self.key, [value])) 93 super(JournaledLDAPAttributeSet, self).remove(value)
94
95 - def clear(self):
96 self.ldapObject._canRemoveAll(self.key) 97 super(JournaledLDAPAttributeSet, self).clear() 98 self.ldapObject.journal(delta.Delete(self.key))
99
100 -class LDAPEntryWithClient(entry.EditableLDAPEntry):
101 implements(interfaces.ILDAPEntry, 102 interfaces.IEditableLDAPEntry, 103 interfaces.IConnectedLDAPEntry, 104 ) 105 106 _state = 'invalid' 107 """ 108 109 State of an LDAPEntry is one of: 110 111 invalid - object not initialized yet 112 113 ready - normal 114 115 deleted - object has been deleted 116 117 """ 118
119 - def __init__(self, client, dn, attributes={}, complete=0):
120 """ 121 122 Initialize the object. 123 124 @param client: The LDAP client connection this object belongs 125 to. 126 127 @param dn: Distinguished Name of the object, as a string. 128 129 @param attributes: Attributes of the object. A dictionary of 130 attribute types to list of attribute values. 131 132 """ 133 134 super(LDAPEntryWithClient, self).__init__(dn, attributes) 135 self.client=client 136 self.complete = complete 137 138 self._journal=[] 139 140 self._remoteData = entry.EditableLDAPEntry(dn, attributes) 141 self._state = 'ready'
142
143 - def buildAttributeSet(self, key, values):
144 return JournaledLDAPAttributeSet(self, key, values)
145
146 - def _canRemove(self, key, value):
147 """ 148 149 Called by JournaledLDAPAttributeSet when it is about to remove a value 150 of an attributeType. 151 152 """ 153 self._checkState() 154 for rdn in self.dn.split()[0].split(): 155 if rdn.attributeType == key and rdn.value == value: 156 raise CannotRemoveRDNError, (key, value)
157
158 - def _canRemoveAll(self, key):
159 """ 160 161 Called by JournaledLDAPAttributeSet when it is about to remove all values 162 of an attributeType. 163 164 """ 165 self._checkState() 166 import types 167 assert not isinstance(self.dn, types.StringType) 168 for keyval in self.dn.split()[0].split(): 169 if keyval.attributeType == key: 170 raise CannotRemoveRDNError, (key)
171 172 173
174 - def _checkState(self):
175 if self._state != 'ready': 176 if self._state == 'deleted': 177 raise ObjectDeletedError 178 else: 179 raise ObjectInBadStateError, \ 180 "State is %s while expecting %s" \ 181 % (repr(self._state), repr('ready'))
182
183 - def journal(self, journalOperation):
184 """ 185 186 Add a Modification into the list of modifications 187 that need to be flushed to the LDAP server. 188 189 Normal callers should not use this, they should use the 190 o['foo']=['bar', 'baz'] -style API that enforces schema, 191 handles errors and updates the cached data. 192 193 """ 194 self._journal.append(journalOperation)
195 196 197 # start ILDAPEntry
198 - def __getitem__(self, *a, **kw):
199 self._checkState() 200 return super(LDAPEntryWithClient, self).__getitem__(*a, **kw)
201
202 - def get(self, *a, **kw):
203 self._checkState() 204 return super(LDAPEntryWithClient, self).get(*a, **kw)
205
206 - def has_key(self, *a, **kw):
207 self._checkState() 208 return super(LDAPEntryWithClient, self).has_key(*a, **kw)
209
210 - def __contains__(self, key):
211 self._checkState() 212 return self.has_key(key)
213
214 - def keys(self):
215 self._checkState() 216 return super(LDAPEntryWithClient, self).keys()
217
218 - def items(self):
219 self._checkState() 220 return super(LDAPEntryWithClient, self).items()
221
222 - def __str__(self):
223 a=[] 224 225 objectClasses = list(self.get('objectClass', [])) 226 objectClasses.sort() 227 a.append(('objectClass', objectClasses)) 228 229 l=list(self.items()) 230 l.sort() 231 for key, values in l: 232 if key!='objectClass': 233 a.append((key, values)) 234 return ldif.asLDIF(self.dn, a)
235
236 - def __eq__(self, other):
237 if not isinstance(other, self.__class__): 238 return 0 239 if self.dn != other.dn: 240 return 0 241 242 my=self.keys() 243 my.sort() 244 its=other.keys() 245 its.sort() 246 if my!=its: 247 return 0 248 for key in my: 249 myAttr=self[key] 250 itsAttr=other[key] 251 if myAttr!=itsAttr: 252 return 0 253 return 1
254
255 - def __ne__(self, other):
256 return not self==other
257
258 - def __len__(self):
259 return len(self.keys())
260
261 - def __nonzero__(self):
262 return True
263
264 - def bind(self, password):
265 r=pureldap.LDAPBindRequest(dn=str(self.dn), auth=password) 266 d = self.client.send(r) 267 d.addCallback(self._handle_bind_msg) 268 return d
269
270 - def _handle_bind_msg(self, msg):
271 assert isinstance(msg, pureldap.LDAPBindResponse) 272 assert msg.referral is None #TODO 273 if msg.resultCode!=ldaperrors.Success.resultCode: 274 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 275 return self
276 277 278 # end ILDAPEntry 279 280 # start IEditableLDAPEntry
281 - def __setitem__(self, key, value):
282 self._checkState() 283 self._canRemoveAll(key) 284 285 new=JournaledLDAPAttributeSet(self, key, value) 286 super(LDAPEntryWithClient, self).__setitem__(key, new) 287 self.journal(delta.Replace(key, value))
288
289 - def __delitem__(self, key):
290 self._checkState() 291 self._canRemoveAll(key) 292 293 super(LDAPEntryWithClient, self).__delitem__(key) 294 self.journal(delta.Delete(key))
295
296 - def undo(self):
297 self._checkState() 298 self._attributes.clear() 299 for k, vs in self._remoteData.items(): 300 self._attributes[k] = self.buildAttributeSet(k, vs) 301 self._journal=[]
302
303 - def _commit_success(self, msg):
304 assert isinstance(msg, pureldap.LDAPModifyResponse) 305 assert msg.referral is None #TODO 306 if msg.resultCode!=ldaperrors.Success.resultCode: 307 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 308 309 assert msg.matchedDN=='' 310 311 self._remoteData = entry.EditableLDAPEntry(self.dn, self) 312 self._journal=[] 313 return self
314
315 - def commit(self):
316 self._checkState() 317 if not self._journal: 318 return defer.succeed(self) 319 320 op=pureldap.LDAPModifyRequest( 321 object=str(self.dn), 322 modification=[x.asLDAP() for x in self._journal]) 323 d = defer.maybeDeferred(self.client.send, op) 324 d.addCallback(self._commit_success) 325 return d
326
327 - def _cbMoveDone(self, msg, newDN):
328 assert isinstance(msg, pureldap.LDAPModifyDNResponse) 329 assert msg.referral is None #TODO 330 if msg.resultCode!=ldaperrors.Success.resultCode: 331 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 332 333 assert msg.matchedDN=='' 334 self.dn = newDN 335 return self
336
337 - def move(self, newDN):
338 self._checkState() 339 newDN = distinguishedname.DistinguishedName(newDN) 340 341 newrdn=newDN.split()[0] 342 newSuperior=distinguishedname.DistinguishedName(listOfRDNs=newDN.split()[1:]) 343 newDN = distinguishedname.DistinguishedName((newrdn,) + newSuperior.split()) 344 op = pureldap.LDAPModifyDNRequest(entry=str(self.dn), 345 newrdn=str(newrdn), 346 deleteoldrdn=1, 347 newSuperior=str(newSuperior)) 348 d = self.client.send(op) 349 d.addCallback(self._cbMoveDone, newDN) 350 return d
351
352 - def _cbDeleteDone(self, msg):
353 assert isinstance(msg, pureldap.LDAPResult) 354 if not isinstance(msg, pureldap.LDAPDelResponse): 355 raise ldaperrors.get(msg.resultCode, 356 msg.errorMessage) 357 assert msg.referral is None #TODO 358 if msg.resultCode!=ldaperrors.Success.resultCode: 359 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 360 361 assert msg.matchedDN=='' 362 return self
363
364 - def delete(self):
365 self._checkState() 366 367 op = pureldap.LDAPDelRequest(entry=str(self.dn)) 368 d = self.client.send(op) 369 d.addCallback(self._cbDeleteDone) 370 self._state = 'deleted' 371 return d
372
373 - def _cbAddDone(self, msg, dn):
374 assert isinstance(msg, pureldap.LDAPAddResponse), \ 375 "LDAPRequest response was not an LDAPAddResponse: %r" % msg 376 assert msg.referral is None #TODO 377 if msg.resultCode!=ldaperrors.Success.resultCode: 378 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 379 380 assert msg.matchedDN=='' 381 e = self.__class__(dn=dn, client=self.client) 382 return e
383
384 - def addChild(self, rdn, attributes):
385 self._checkState() 386 387 rdn = distinguishedname.RelativeDistinguishedName(rdn) 388 dn = distinguishedname.DistinguishedName( 389 listOfRDNs=(rdn,)+self.dn.split()) 390 391 ldapAttrs = [] 392 for attrType, values in attributes.items(): 393 ldapAttrType = pureldap.LDAPAttributeDescription(attrType) 394 l = [] 395 for value in values: 396 l.append(pureldap.LDAPAttributeValue(value)) 397 ldapValues = pureber.BERSet(l) 398 399 ldapAttrs.append((ldapAttrType, ldapValues)) 400 op=pureldap.LDAPAddRequest(entry=str(dn), 401 attributes=ldapAttrs) 402 d = self.client.send(op) 403 d.addCallback(self._cbAddDone, dn) 404 return d
405
406 - def _cbSetPassword_ExtendedOperation(self, msg):
407 assert isinstance(msg, pureldap.LDAPExtendedResponse) 408 assert msg.referral is None #TODO 409 if msg.resultCode!=ldaperrors.Success.resultCode: 410 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 411 412 assert msg.matchedDN=='' 413 return self
414
415 - def setPassword_ExtendedOperation(self, newPasswd):
416 """ 417 418 Set the password on this object. 419 420 @param newPasswd: A string containing the new password. 421 422 @return: A Deferred that will complete when the operation is 423 done. 424 425 """ 426 427 self._checkState() 428 429 op = pureldap.LDAPPasswordModifyRequest(userIdentity=str(self.dn), newPasswd=newPasswd) 430 d = self.client.send(op) 431 d.addCallback(self._cbSetPassword_ExtendedOperation) 432 return d
433 434 _setPasswordPriority_ExtendedOperation=0 435 setPasswordMaybe_ExtendedOperation = setPassword_ExtendedOperation 436
437 - def setPassword_Samba(self, newPasswd, style=None):
438 """ 439 440 Set the Samba password on this object. 441 442 @param newPasswd: A string containing the new password. 443 444 @param style: one of 'sambaSamAccount', 'sambaAccount' or 445 None. Specifies the style of samba accounts used. None is 446 default and is the same as 'sambaSamAccount'. 447 448 @return: A Deferred that will complete when the operation is 449 done. 450 451 """ 452 453 self._checkState() 454 455 nthash=smbpassword.nthash(newPasswd) 456 lmhash=smbpassword.lmhash(newPasswd) 457 458 if style is None: 459 style = 'sambaSamAccount' 460 if style == 'sambaSamAccount': 461 self['sambaNTPassword'] = [nthash] 462 self['sambaLMPassword'] = [lmhash] 463 elif style == 'sambaAccount': 464 self['ntPassword'] = [nthash] 465 self['lmPassword'] = [lmhash] 466 else: 467 raise RuntimeError, "Unknown samba password style %r" % style 468 return self.commit()
469 470 _setPasswordPriority_Samba=20
471 - def setPasswordMaybe_Samba(self, newPasswd):
472 """ 473 474 Set the Samba password on this object if it is a 475 sambaSamAccount or sambaAccount. 476 477 @param newPasswd: A string containing the new password. 478 479 @return: A Deferred that will complete when the operation is 480 done. 481 482 """ 483 if not self.complete and not self.has_key('objectClass'): 484 d=self.fetch('objectClass') 485 d.addCallback(lambda dummy, self=self, newPasswd=newPasswd: 486 self.setPasswordMaybe_Samba(newPasswd)) 487 else: 488 objectClasses = [s.upper() for s in self.get('objectClass', ())] 489 if 'sambaAccount'.upper() in objectClasses: 490 d = self.setPassword_Samba(newPasswd, style="sambaAccount") 491 elif 'sambaSamAccount'.upper() in objectClasses: 492 d = self.setPassword_Samba(newPasswd, style="sambaSamAccount") 493 else: 494 d = defer.succeed(self) 495 return d
496
497 - def _cbSetPassword(self, dl, names):
498 assert len(dl)==len(names) 499 l=[] 500 for name, (ok, x) in zip(names, dl): 501 if not ok: 502 l.append((name, x)) 503 if l: 504 raise PasswordSetAggregateError, l 505 return self
506
507 - def _cbSetPassword_one(self, result):
508 return (True, None)
509 - def _ebSetPassword_one(self, fail):
510 fail.trap(ldaperrors.LDAPException, 511 DNNotPresentError) 512 return (False, fail)
513 - def _setPasswordAll(self, results, newPasswd, prefix, names):
514 if not names: 515 return results 516 name, names = names[0], names[1:] 517 if results and not results[-1][0]: 518 # failing 519 fail = Failure(PasswordSetAborted()) 520 d = defer.succeed(results+[(None, fail)]) 521 else: 522 fn = getattr(self, prefix+name) 523 d = defer.maybeDeferred(fn, newPasswd) 524 d.addCallbacks(self._cbSetPassword_one, 525 self._ebSetPassword_one) 526 def cb((success, info)): 527 return results+[(success, info)]
528 d.addCallback(cb) 529 530 d.addCallback(self._setPasswordAll, 531 newPasswd, prefix, names) 532 return d
533
534 - def setPassword(self, newPasswd):
535 def _passwordChangerPriorityComparison(me, other): 536 mePri = getattr(self, '_setPasswordPriority_'+me) 537 otherPri = getattr(self, '_setPasswordPriority_'+other) 538 return cmp(mePri, otherPri)
539 540 prefix='setPasswordMaybe_' 541 names=[name[len(prefix):] for name in dir(self) if name.startswith(prefix)] 542 names.sort(_passwordChangerPriorityComparison) 543 544 d = defer.maybeDeferred(self._setPasswordAll, 545 [], 546 newPasswd, 547 prefix, 548 names) 549 d.addCallback(self._cbSetPassword, names) 550 return d 551 552 # end IEditableLDAPEntry 553 554 # start IConnectedLDAPEntry 555
556 - def _cbNamingContext_Entries(self, results):
557 for result in results: 558 for namingContext in result.get('namingContexts', ()): 559 dn = distinguishedname.DistinguishedName(namingContext) 560 if dn.contains(self.dn): 561 return LDAPEntry(self.client, dn) 562 raise NoContainingNamingContext, self.dn
563
564 - def namingContext(self):
565 o=LDAPEntry(client=self.client, dn='') 566 d=o.search(filterText='(objectClass=*)', 567 scope=pureldap.LDAP_SCOPE_baseObject, 568 attributes=['namingContexts']) 569 d.addCallback(self._cbNamingContext_Entries) 570 return d
571
572 - def _cbFetch(self, results, overWrite):
573 if len(results)!=1: 574 raise DNNotPresentError, self.dn 575 o=results[0] 576 577 assert not self._journal 578 579 if not overWrite: 580 for key in self._remoteData.keys(): 581 del self._remoteData[key] 582 overWrite=o.keys() 583 self.complete = 1 584 585 for k in overWrite: 586 vs=o.get(k) 587 if vs is not None: 588 self._remoteData[k] = vs 589 self.undo() 590 return self
591
592 - def fetch(self, *attributes):
593 self._checkState() 594 if self._journal: 595 raise ObjectDirtyError, 'cannot fetch attributes of %s, it is dirty' % repr(self) 596 597 d = self.search(scope=pureldap.LDAP_SCOPE_baseObject, 598 attributes=attributes) 599 d.addCallback(self._cbFetch, overWrite=attributes) 600 return d
601
602 - def _cbSearchEntry(self, callback, objectName, attributes, complete):
603 attrib={} 604 for key, values in attributes: 605 attrib[str(key)]=[str(x) for x in values] 606 o=LDAPEntry(client=self.client, 607 dn=objectName, 608 attributes=attrib, 609 complete=complete) 610 callback(o)
611
612 - def _cbSearchMsg(self, msg, d, callback, complete, sizeLimitIsNonFatal):
613 if isinstance(msg, pureldap.LDAPSearchResultDone): 614 assert msg.referral is None #TODO 615 e = ldaperrors.get(msg.resultCode, msg.errorMessage) 616 if not isinstance(e, ldaperrors.Success): 617 try: 618 raise e 619 except ldaperrors.LDAPSizeLimitExceeded, e: 620 if sizeLimitIsNonFatal: 621 pass 622 except: 623 d.errback(Failure()) 624 return True 625 626 # search ended successfully 627 assert msg.matchedDN=='' 628 d.callback(None) 629 return True 630 elif isinstance(msg, pureldap.LDAPSearchResultEntry): 631 self._cbSearchEntry(callback, msg.objectName, msg.attributes, 632 complete=complete) 633 return False 634 elif isinstance(msg, pureldap.LDAPSearchResultReference): 635 return False 636 else: 637 raise ldaperrors.LDAPProtocolError, \ 638 'bad search response: %r' % msg
639
640 - def search(self, 641 filterText=None, 642 filterObject=None, 643 attributes=(), 644 scope=None, 645 derefAliases=None, 646 sizeLimit=0, 647 sizeLimitIsNonFatal=False, 648 timeLimit=0, 649 typesOnly=0, 650 callback=None):
651 self._checkState() 652 d=defer.Deferred() 653 if filterObject is None and filterText is None: 654 filterObject=pureldap.LDAPFilterMatchAll 655 elif filterObject is None and filterText is not None: 656 filterObject=ldapfilter.parseFilter(filterText) 657 elif filterObject is not None and filterText is None: 658 pass 659 elif filterObject is not None and filterText is not None: 660 f=ldapfilter.parseFilter(filterText) 661 filterObject=pureldap.LDAPFilter_and((f, filterObject)) 662 663 if scope is None: 664 scope = pureldap.LDAP_SCOPE_wholeSubtree 665 if derefAliases is None: 666 derefAliases = pureldap.LDAP_DEREF_neverDerefAliases 667 668 if attributes is None: 669 attributes = ['1.1'] 670 671 results=[] 672 if callback is None: 673 cb=results.append 674 else: 675 cb=callback 676 try: 677 op = pureldap.LDAPSearchRequest( 678 baseObject=str(self.dn), 679 scope=scope, 680 derefAliases=derefAliases, 681 sizeLimit=sizeLimit, 682 timeLimit=timeLimit, 683 typesOnly=typesOnly, 684 filter=filterObject, 685 attributes=attributes) 686 dsend = self.client.send_multiResponse( 687 op, self._cbSearchMsg, 688 d, cb, complete=not attributes, 689 sizeLimitIsNonFatal=sizeLimitIsNonFatal) 690 except ldapclient.LDAPClientConnectionLostException: 691 d.errback(Failure()) 692 else: 693 if callback is None: 694 d.addCallback(lambda dummy: results) 695 def rerouteerr(e): 696 d.errback(e)
697 # returning None will stop the error 698 # from being propagated and logged. 699 dsend.addErrback(rerouteerr) 700 return d 701
702 - def lookup(self, dn):
703 e = self.__class__(self.client, dn) 704 d = e.fetch('1.1') 705 return d
706 707 # end IConnectedLDAPEntry 708
709 - def __repr__(self):
710 x={} 711 for key in super(LDAPEntryWithClient, self).keys(): 712 x[key]=self[key] 713 keys=x.keys() 714 keys.sort() 715 a=[] 716 for key in keys: 717 a.append('%s: %s' % (repr(key), repr(self[key]))) 718 attributes=', '.join(a) 719 return '%s(dn=%s, attributes={%s})' % ( 720 self.__class__.__name__, 721 repr(str(self.dn)), 722 attributes)
723 724 # API backwards compatibility 725 LDAPEntry = LDAPEntryWithClient 726
727 -class LDAPEntryWithAutoFill(LDAPEntry):
728 - def __init__(self, *args, **kwargs):
729 LDAPEntry.__init__(self, *args, **kwargs) 730 self.autoFillers = []
731
732 - def _cb_addAutofiller(self, r, autoFiller):
733 self.autoFillers.append(autoFiller) 734 return r
735
736 - def addAutofiller(self, autoFiller):
737 d = defer.maybeDeferred(autoFiller.start, self) 738 d.addCallback(self._cb_addAutofiller, autoFiller) 739 return d
740
741 - def journal(self, journalOperation):
742 LDAPEntry.journal(self, journalOperation) 743 for autoFiller in self.autoFillers: 744 autoFiller.notify(self, journalOperation.key)
745