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