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 import codecs
12 """Some of the password plugins failed"""
14 Exception.__init__(self)
15 self.errors=errors
16
18 return '%s: %s.' % (
19 self.__doc__,
20 '; '.join([ '%s failed with %s' % (name, fail.getErrorMessage())
21 for name, fail in self.errors]))
22
24 return '<'+self.__class__.__name__+' errors='+repr(self.errors)+'>'
25
31
33 """The requested DN cannot be found by the server."""
34 pass
35
37 """The LDAP object in in a bad state."""
38 pass
39
41 """The LDAP object has already been removed, unable to perform operations on it."""
42 pass
43
45 """The LDAP object has a journal which needs to be committed or undone before this operation."""
46 pass
47
49 """The server contains to LDAP naming context that would contain this object."""
50 pass
51
53 """The attribute to be removed is the RDN for the object and cannot be removed."""
55 Exception.__init__(self)
56 self.key=key
57 self.val=val
58
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
67 """Match type not implemented"""
69 Exception.__init__(self)
70 self.op=op
71
73 return '%s: %r' % (self.__doc__, self.op)
74
76 - def __init__(self, ldapObject, *a, **kw):
79
80 - def add(self, value):
83
87
94
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
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
215 self._checkState()
216 return super(LDAPEntryWithClient, self).keys()
217
219 self._checkState()
220 return super(LDAPEntryWithClient, self).items()
221
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
259 return len(self.keys())
260
261 - def __nonzero__(self):
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
273 if msg.resultCode!=ldaperrors.Success.resultCode:
274 raise ldaperrors.get(msg.resultCode, msg.errorMessage)
275 return self
276
277
278
279
280
281 - def __setitem__(self, 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
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
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
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
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):
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
358 if msg.resultCode!=ldaperrors.Success.resultCode:
359 raise ldaperrors.get(msg.resultCode, msg.errorMessage)
360
361 assert msg.matchedDN==''
362 return self
363
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
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 if (isinstance(value, unicode)):
397 value = value.encode('utf-8')
398 l.append(pureldap.LDAPAttributeValue(value))
399 ldapValues = pureber.BERSet(l)
400 ldapAttrs.append((ldapAttrType, ldapValues))
401 op=pureldap.LDAPAddRequest(entry=str(dn),
402 attributes=ldapAttrs)
403 d = self.client.send(op)
404 d.addCallback(self._cbAddDone, dn)
405 return d
406
408 assert isinstance(msg, pureldap.LDAPExtendedResponse)
409 assert msg.referral is None
410 if msg.resultCode!=ldaperrors.Success.resultCode:
411 raise ldaperrors.get(msg.resultCode, msg.errorMessage)
412
413 assert msg.matchedDN==''
414 return self
415
417 """
418
419 Set the password on this object.
420
421 @param newPasswd: A string containing the new password.
422
423 @return: A Deferred that will complete when the operation is
424 done.
425
426 """
427
428 self._checkState()
429
430 op = pureldap.LDAPPasswordModifyRequest(userIdentity=str(self.dn), newPasswd=newPasswd)
431 d = self.client.send(op)
432 d.addCallback(self._cbSetPassword_ExtendedOperation)
433 return d
434
435 _setPasswordPriority_ExtendedOperation=0
436 setPasswordMaybe_ExtendedOperation = setPassword_ExtendedOperation
437
438 - def setPassword_Samba(self, newPasswd, style=None):
439 """
440
441 Set the Samba password on this object.
442
443 @param newPasswd: A string containing the new password.
444
445 @param style: one of 'sambaSamAccount', 'sambaAccount' or
446 None. Specifies the style of samba accounts used. None is
447 default and is the same as 'sambaSamAccount'.
448
449 @return: A Deferred that will complete when the operation is
450 done.
451
452 """
453
454 self._checkState()
455
456 nthash=smbpassword.nthash(newPasswd)
457 lmhash=smbpassword.lmhash(newPasswd)
458
459 if style is None:
460 style = 'sambaSamAccount'
461 if style == 'sambaSamAccount':
462 self['sambaNTPassword'] = [nthash]
463 self['sambaLMPassword'] = [lmhash]
464 elif style == 'sambaAccount':
465 self['ntPassword'] = [nthash]
466 self['lmPassword'] = [lmhash]
467 else:
468 raise RuntimeError, "Unknown samba password style %r" % style
469 return self.commit()
470
471 _setPasswordPriority_Samba=20
472 - def setPasswordMaybe_Samba(self, newPasswd):
473 """
474
475 Set the Samba password on this object if it is a
476 sambaSamAccount or sambaAccount.
477
478 @param newPasswd: A string containing the new password.
479
480 @return: A Deferred that will complete when the operation is
481 done.
482
483 """
484 if not self.complete and not self.has_key('objectClass'):
485 d=self.fetch('objectClass')
486 d.addCallback(lambda dummy, self=self, newPasswd=newPasswd:
487 self.setPasswordMaybe_Samba(newPasswd))
488 else:
489 objectClasses = [s.upper() for s in self.get('objectClass', ())]
490 if 'sambaAccount'.upper() in objectClasses:
491 d = self.setPassword_Samba(newPasswd, style="sambaAccount")
492 elif 'sambaSamAccount'.upper() in objectClasses:
493 d = self.setPassword_Samba(newPasswd, style="sambaSamAccount")
494 else:
495 d = defer.succeed(self)
496 return d
497
498 - def _cbSetPassword(self, dl, names):
499 assert len(dl)==len(names)
500 l=[]
501 for name, (ok, x) in zip(names, dl):
502 if not ok:
503 l.append((name, x))
504 if l:
505 raise PasswordSetAggregateError, l
506 return self
507
508 - def _cbSetPassword_one(self, result):
510 - def _ebSetPassword_one(self, fail):
511 fail.trap(ldaperrors.LDAPException,
512 DNNotPresentError)
513 return (False, fail)
514 - def _setPasswordAll(self, results, newPasswd, prefix, names):
515 if not names:
516 return results
517 name, names = names[0], names[1:]
518 if results and not results[-1][0]:
519
520 fail = Failure(PasswordSetAborted())
521 d = defer.succeed(results+[(None, fail)])
522 else:
523 fn = getattr(self, prefix+name)
524 d = defer.maybeDeferred(fn, newPasswd)
525 d.addCallbacks(self._cbSetPassword_one,
526 self._ebSetPassword_one)
527 def cb((success, info)):
528 return results+[(success, info)]
529 d.addCallback(cb)
530
531 d.addCallback(self._setPasswordAll,
532 newPasswd, prefix, names)
533 return d
534
535 - def setPassword(self, newPasswd):
536 def _passwordChangerPriorityComparison(me, other):
537 mePri = getattr(self, '_setPasswordPriority_'+me)
538 otherPri = getattr(self, '_setPasswordPriority_'+other)
539 return cmp(mePri, otherPri)
540
541 prefix='setPasswordMaybe_'
542 names=[name[len(prefix):] for name in dir(self) if name.startswith(prefix)]
543 names.sort(_passwordChangerPriorityComparison)
544
545 d = defer.maybeDeferred(self._setPasswordAll,
546 [],
547 newPasswd,
548 prefix,
549 names)
550 d.addCallback(self._cbSetPassword, names)
551 return d
552
553
554
555
556
557 - def _cbNamingContext_Entries(self, results):
558 for result in results:
559 for namingContext in result.get('namingContexts', ()):
560 dn = distinguishedname.DistinguishedName(namingContext)
561 if dn.contains(self.dn):
562 return LDAPEntry(self.client, dn)
563 raise NoContainingNamingContext, self.dn
564
565 - def namingContext(self):
566 o=LDAPEntry(client=self.client, dn='')
567 d=o.search(filterText='(objectClass=*)',
568 scope=pureldap.LDAP_SCOPE_baseObject,
569 attributes=['namingContexts'])
570 d.addCallback(self._cbNamingContext_Entries)
571 return d
572
573 - def _cbFetch(self, results, overWrite):
574 if len(results)!=1:
575 raise DNNotPresentError, self.dn
576 o=results[0]
577
578 assert not self._journal
579
580 if not overWrite:
581 for key in self._remoteData.keys():
582 del self._remoteData[key]
583 overWrite=o.keys()
584 self.complete = 1
585
586 for k in overWrite:
587 vs=o.get(k)
588 if vs is not None:
589 self._remoteData[k] = vs
590 self.undo()
591 return self
592
593 - def fetch(self, *attributes):
594 self._checkState()
595 if self._journal:
596 raise ObjectDirtyError, 'cannot fetch attributes of %s, it is dirty' % repr(self)
597
598 d = self.search(scope=pureldap.LDAP_SCOPE_baseObject,
599 attributes=attributes)
600 d.addCallback(self._cbFetch, overWrite=attributes)
601 return d
602
603 - def _cbSearchEntry(self, callback, objectName, attributes, complete):
604 attrib={}
605 for key, values in attributes:
606 attrib[str(key)]=[str(x) for x in values]
607 o=LDAPEntry(client=self.client,
608 dn=objectName,
609 attributes=attrib,
610 complete=complete)
611 callback(o)
612
613 - def _cbSearchMsg(self, msg, d, callback, complete, sizeLimitIsNonFatal):
614 if isinstance(msg, pureldap.LDAPSearchResultDone):
615 assert msg.referral is None
616 e = ldaperrors.get(msg.resultCode, msg.errorMessage)
617 if not isinstance(e, ldaperrors.Success):
618 try:
619 raise e
620 except ldaperrors.LDAPSizeLimitExceeded, e:
621 if sizeLimitIsNonFatal:
622 pass
623 except:
624 d.errback(Failure())
625 return True
626
627
628 assert msg.matchedDN==''
629 d.callback(None)
630 return True
631 elif isinstance(msg, pureldap.LDAPSearchResultEntry):
632 self._cbSearchEntry(callback, msg.objectName, msg.attributes,
633 complete=complete)
634 return False
635 elif isinstance(msg, pureldap.LDAPSearchResultReference):
636 return False
637 else:
638 raise ldaperrors.LDAPProtocolError, \
639 'bad search response: %r' % msg
640
641 - def search(self,
642 filterText=None,
643 filterObject=None,
644 attributes=(),
645 scope=None,
646 derefAliases=None,
647 sizeLimit=0,
648 sizeLimitIsNonFatal=False,
649 timeLimit=0,
650 typesOnly=0,
651 callback=None):
652 self._checkState()
653 d=defer.Deferred()
654 if filterObject is None and filterText is None:
655 filterObject=pureldap.LDAPFilterMatchAll
656 elif filterObject is None and filterText is not None:
657 filterObject=ldapfilter.parseFilter(filterText)
658 elif filterObject is not None and filterText is None:
659 pass
660 elif filterObject is not None and filterText is not None:
661 f=ldapfilter.parseFilter(filterText)
662 filterObject=pureldap.LDAPFilter_and((f, filterObject))
663
664 if scope is None:
665 scope = pureldap.LDAP_SCOPE_wholeSubtree
666 if derefAliases is None:
667 derefAliases = pureldap.LDAP_DEREF_neverDerefAliases
668
669 if attributes is None:
670 attributes = ['1.1']
671
672 results=[]
673 if callback is None:
674 cb=results.append
675 else:
676 cb=callback
677 try:
678 op = pureldap.LDAPSearchRequest(
679 baseObject=str(self.dn),
680 scope=scope,
681 derefAliases=derefAliases,
682 sizeLimit=sizeLimit,
683 timeLimit=timeLimit,
684 typesOnly=typesOnly,
685 filter=filterObject,
686 attributes=attributes)
687 dsend = self.client.send_multiResponse(
688 op, self._cbSearchMsg,
689 d, cb, complete=not attributes,
690 sizeLimitIsNonFatal=sizeLimitIsNonFatal)
691 except ldapclient.LDAPClientConnectionLostException:
692 d.errback(Failure())
693 else:
694 if callback is None:
695 d.addCallback(lambda dummy: results)
696 def rerouteerr(e):
697 d.errback(e)
698
699
700 dsend.addErrback(rerouteerr)
701 return d
702
703 - def lookup(self, dn):
704 e = self.__class__(self.client, dn)
705 d = e.fetch('1.1')
706 return d
707
708
709
710 - def __repr__(self):
711 x={}
712 for key in super(LDAPEntryWithClient, self).keys():
713 x[key]=self[key]
714 keys=x.keys()
715 keys.sort()
716 a=[]
717 for key in keys:
718 a.append('%s: %s' % (repr(key), repr(self[key])))
719 attributes=', '.join(a)
720 return '%s(dn=%s, attributes={%s})' % (
721 self.__class__.__name__,
722 repr(str(self.dn)),
723 attributes)
724
725
726 LDAPEntry = LDAPEntryWithClient
727
728 -class LDAPEntryWithAutoFill(LDAPEntry):
729 - def __init__(self, *args, **kwargs):
730 LDAPEntry.__init__(self, *args, **kwargs)
731 self.autoFillers = []
732
733 - def _cb_addAutofiller(self, r, autoFiller):
734 self.autoFillers.append(autoFiller)
735 return r
736
737 - def addAutofiller(self, autoFiller):
738 d = defer.maybeDeferred(autoFiller.start, self)
739 d.addCallback(self._cb_addAutofiller, autoFiller)
740 return d
741
742 - def journal(self, journalOperation):
743 LDAPEntry.journal(self, journalOperation)
744 for autoFiller in self.autoFillers:
745 autoFiller.notify(self, journalOperation.key)
746