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  import codecs 
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 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
407 - def _cbSetPassword_ExtendedOperation(self, msg):
408 assert isinstance(msg, pureldap.LDAPExtendedResponse) 409 assert msg.referral is None #TODO 410 if msg.resultCode!=ldaperrors.Success.resultCode: 411 raise ldaperrors.get(msg.resultCode, msg.errorMessage) 412 413 assert msg.matchedDN=='' 414 return self
415
416 - def setPassword_ExtendedOperation(self, newPasswd):
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):
509 return (True, None)
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 # failing 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 # end IEditableLDAPEntry 554 555 # start IConnectedLDAPEntry 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 #TODO 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 # search ended successfully 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 # returning None will stop the error 699 # from being propagated and logged. 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 # end IConnectedLDAPEntry 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 # API backwards compatibility 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