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
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 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
407 assert isinstance(msg, pureldap.LDAPExtendedResponse)
408 assert msg.referral is None
409 if msg.resultCode!=ldaperrors.Success.resultCode:
410 raise ldaperrors.get(msg.resultCode, msg.errorMessage)
411
412 assert msg.matchedDN==''
413 return self
414
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):
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
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
553
554
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
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
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
698
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
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
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