3 # user syncpasswords command
5 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
6 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
28 from subprocess import Popen, PIPE, STDOUT
31 import samba.getopt as options
32 from samba import Ldb, dsdb
33 from samba.dcerpc import misc, security
34 from samba.ndr import ndr_unpack
35 from samba.common import get_bytes
36 from samba.netcmd import CommandError, Option
41 decrypt_samba_gpg_help,
42 virtual_attributes_help
46 class cmd_user_syncpasswords(GetPasswordCommand):
47 """Sync the password of user accounts.
49 This syncs logon passwords for user accounts.
51 Note that this command should run on a single domain controller only
52 (typically the PDC-emulator). However the "password hash gpg key ids"
53 option should to be configured on all domain controllers.
55 The command must be run from the root user id or another authorized user id.
56 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
57 local path. By default, ldapi:// is used with the default path to the
58 privileged ldapi socket.
60 This command has three modes: "Cache Initialization", "Sync Loop Run" and
61 "Sync Loop Terminate".
67 The first time, this command needs to be called with
68 '--cache-ldb-initialize' in order to initialize its cache.
70 The cache initialization requires '--attributes' and allows the following
71 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
74 The '--attributes' parameter takes a comma separated list of attributes,
75 which will be printed or given to the script specified by '--script'. If a
76 specified attribute is not available on an object it will be silently omitted.
77 All attributes defined in the schema (e.g. the unicodePwd attribute holds
78 the NTHASH) and the following virtual attributes are possible (see '--help'
79 for supported virtual attributes in your environment):
81 virtualClearTextUTF16: The raw cleartext as stored in the
82 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
83 with '--decrypt-samba-gpg') buffer inside of the
84 supplementalCredentials attribute. This typically
85 contains valid UTF-16-LE, but may contain random
86 bytes, e.g. for computer accounts.
88 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
89 (only from valid UTF-16-LE).
91 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
92 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
94 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
95 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
96 with a $5$... salt, see crypt(3) on modern systems.
97 The number of rounds used to calculate the hash can
98 also be specified. By appending ";rounds=x" to the
99 attribute name i.e. virtualCryptSHA256;rounds=10000
100 will calculate a SHA256 hash with 10,000 rounds.
101 Non numeric values for rounds are silently ignored.
102 The value is calculated as follows:
103 1) If a value exists in 'Primary:userPassword' with
104 the specified number of rounds it is returned.
105 2) If 'Primary:CLEARTEXT', or 'Primary:SambaGPG' with
106 '--decrypt-samba-gpg'. Calculate a hash with
107 the specified number of rounds
108 3) Return the first CryptSHA256 value in
109 'Primary:userPassword'.
111 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
112 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
113 with a $6$... salt, see crypt(3) on modern systems.
114 The number of rounds used to calculate the hash can
115 also be specified. By appending ";rounds=x" to the
116 attribute name i.e. virtualCryptSHA512;rounds=10000
117 will calculate a SHA512 hash with 10,000 rounds.
118 Non numeric values for rounds are silently ignored.
119 The value is calculated as follows:
120 1) If a value exists in 'Primary:userPassword' with
121 the specified number of rounds it is returned.
122 2) If 'Primary:CLEARTEXT', or 'Primary:SambaGPG' with
123 '--decrypt-samba-gpg'. Calculate a hash with
124 the specified number of rounds.
125 3) Return the first CryptSHA512 value in
126 'Primary:userPassword'.
128 virtualWDigestNN: The individual hash values stored in
129 'Primary:WDigest' where NN is the hash number in
131 NOTE: As at 22-05-2017 the documentation:
132 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
133 https://msdn.microsoft.com/en-us/library/cc245680.aspx
136 virtualKerberosSalt: This results the salt string that is used to compute
137 Kerberos keys from a UTF-8 cleartext password.
139 virtualSambaGPG: The raw cleartext as stored in the
140 'Primary:SambaGPG' buffer inside of the
141 supplementalCredentials attribute.
142 See the 'password hash gpg key ids' option in
145 The '--decrypt-samba-gpg' option triggers decryption of the
146 Primary:SambaGPG buffer. Check with '--help' if this feature is available
147 in your environment or not (the python-gpgme package is required). Please
148 note that you might need to set the GNUPGHOME environment variable. If the
149 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
150 environment variable has been set correctly and the passphrase is already
151 known by the gpg-agent.
153 The '--script' option specifies a custom script that is called whenever any
154 of the dirsyncAttributes (see below) was changed. The script is called
155 without any arguments. It gets the LDIF for exactly one object on STDIN.
156 If the script processed the object successfully it has to respond with a
157 single line starting with 'DONE-EXIT: ' followed by an optional message.
159 Note that the script might be called without any password change, e.g. if
160 the account was disabled (a userAccountControl change) or the
161 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
162 are always returned as unique identifier of the account. It might be useful
163 to also ask for non-password attributes like: objectSid, sAMAccountName,
164 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
165 Depending on the object, some attributes may not be present/available,
166 but you always get the current state (and not a diff).
168 If no '--script' option is specified, the LDIF will be printed on STDOUT or
171 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
172 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
173 (!(sAMAccountName=krbtgt*)))
174 This means only normal (non-krbtgt) user
175 accounts are monitored. The '--filter' can modify that, e.g. if it's
176 required to also sync computer accounts.
182 This (default) mode runs in an endless loop waiting for password related
183 changes in the active directory database. It makes use of the
184 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
185 get changes in a reliable fashion. Objects are monitored for changes of the
186 following dirsyncAttributes:
188 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
189 userPrincipalName and userAccountControl.
191 It recovers from LDAP disconnects and updates the cache in conservative way
192 (in single steps after each successfully processed change). An error from
193 the script (specified by '--script') will result in fatal error and this
194 command will exit. But the cache state should be still valid and can be
195 resumed in the next "Sync Loop Run".
197 The '--logfile' option specifies an optional (required if '--daemon' is
198 specified) logfile that takes all output of the command. The logfile is
199 automatically reopened if fstat returns st_nlink == 0.
201 The optional '--daemon' option will put the command into the background.
203 You can stop the command without the '--daemon' option, also by hitting
206 If you specify the '--no-wait' option the command skips the
207 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
208 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
213 In order to terminate an already running command (likely as daemon) the
214 '--terminate' option can be used. This also requires the '--logfile' option
219 samba-tool user syncpasswords --cache-ldb-initialize \\
220 --attributes=virtualClearTextUTF8
221 samba-tool user syncpasswords
224 samba-tool user syncpasswords --cache-ldb-initialize \\
225 --attributes=objectGUID,objectSID,sAMAccountName,\\
226 userPrincipalName,userAccountControl,pwdLastSet,\\
227 msDS-KeyVersionNumber,virtualCryptSHA512 \\
228 --script=/path/to/my-custom-syncpasswords-script.py
229 samba-tool user syncpasswords --daemon \\
230 --logfile=/var/log/samba/user-syncpasswords.log
231 samba-tool user syncpasswords --terminate \\
232 --logfile=/var/log/samba/user-syncpasswords.log
236 synopsis = "%prog [--cache-ldb-initialize] [options]"
238 takes_optiongroups = {
239 "sambaopts": options.SambaOptions,
240 "versionopts": options.VersionOptions,
244 Option("--cache-ldb-initialize",
245 help="Initialize the cache for the first time",
246 dest="cache_ldb_initialize", action="store_true"),
247 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
248 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
249 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
250 metavar="URL", dest="H"),
251 Option("--filter", help="optional LDAP filter to set password on", type=str,
252 metavar="LDAP-SEARCH-FILTER", dest="filter"),
253 Option("--attributes", type=str,
254 help=virtual_attributes_help,
255 metavar="ATTRIBUTELIST", dest="attributes"),
256 Option("--decrypt-samba-gpg",
257 help=decrypt_samba_gpg_help,
258 action="store_true", default=False, dest="decrypt_samba_gpg"),
259 Option("--script", help="Script that is called for each password change", type=str,
260 metavar="/path/to/syncpasswords.script", dest="script"),
261 Option("--no-wait", help="Don't block waiting for changes",
262 action="store_true", default=False, dest="nowait"),
263 Option("--logfile", type=str,
264 help="The logfile to use (required in --daemon mode).",
265 metavar="/path/to/syncpasswords.log", dest="logfile"),
266 Option("--daemon", help="daemonize after initial setup",
267 action="store_true", default=False, dest="daemon"),
268 Option("--terminate",
269 help="Send a SIGTERM to an already running (daemon) process",
270 action="store_true", default=False, dest="terminate"),
273 def run(self, cache_ldb_initialize=False, cache_ldb=None,
275 attributes=None, decrypt_samba_gpg=None,
276 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
277 sambaopts=None, versionopts=None):
279 self.lp = sambaopts.get_loadparm()
281 self.samdb_url = None
285 if not cache_ldb_initialize:
286 if attributes is not None:
287 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
288 if decrypt_samba_gpg:
289 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
290 if script is not None:
291 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
292 if filter is not None:
293 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
295 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
297 if nowait is not False:
298 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
299 if logfile is not None:
300 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
301 if daemon is not False:
302 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
303 if terminate is not False:
304 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
308 raise CommandError("--daemon is not allowed together with --no-wait")
309 if terminate is not False:
310 raise CommandError("--terminate is not allowed together with --no-wait")
312 if terminate is True and daemon is True:
313 raise CommandError("--terminate is not allowed together with --daemon")
315 if daemon is True and logfile is None:
316 raise CommandError("--daemon is only allowed together with --logfile")
318 if terminate is True and logfile is None:
319 raise CommandError("--terminate is only allowed together with --logfile")
321 if script is not None:
322 if not os.path.exists(script):
323 raise CommandError("script[%s] does not exist!" % script)
325 sync_command = "%s" % os.path.abspath(script)
329 dirsync_filter = filter
330 if dirsync_filter is None:
331 dirsync_filter = "(&" + \
332 "(objectClass=user)" + \
333 "(userAccountControl:%s:=%u)" % (
334 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
335 "(!(sAMAccountName=krbtgt*))" + \
338 dirsync_secret_attrs = [
341 "supplementalCredentials",
344 dirsync_attrs = dirsync_secret_attrs + [
348 "userAccountControl",
353 password_attrs = None
355 if cache_ldb_initialize:
357 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
359 if decrypt_samba_gpg and not gpg_decrypt:
360 raise CommandError(decrypt_samba_gpg_help)
362 password_attrs = self.parse_attributes(attributes)
363 lower_attrs = [x.lower() for x in password_attrs]
364 # We always return these in order to track deletions
365 for a in ["objectGUID", "isDeleted", "isRecycled"]:
366 if a.lower() not in lower_attrs:
367 password_attrs += [a]
369 if cache_ldb is not None:
370 if cache_ldb.lower().startswith("ldapi://"):
371 raise CommandError("--cache_ldb ldapi:// is not supported")
372 elif cache_ldb.lower().startswith("ldap://"):
373 raise CommandError("--cache_ldb ldap:// is not supported")
374 elif cache_ldb.lower().startswith("ldaps://"):
375 raise CommandError("--cache_ldb ldaps:// is not supported")
376 elif cache_ldb.lower().startswith("tdb://"):
379 if not os.path.exists(cache_ldb):
380 cache_ldb = self.lp.private_path(cache_ldb)
382 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
384 self.lockfile = "%s.pid" % cache_ldb
387 if self.logfile is not None:
389 if info.st_nlink == 0:
390 logfile = self.logfile
392 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
393 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
398 log_msg("Reopened logfile[%s]\n" % (logfile))
399 self.logfile = logfile
400 msg = "%s: pid[%d]: %s" % (
419 self.cache = Ldb(cache_ldb)
420 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
421 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
425 self.samdb_url = str(res[0]["samdbUrl"][0])
426 except KeyError as e:
427 self.samdb_url = None
429 self.samdb_url = None
430 if self.samdb_url is None and not cache_ldb_initialize:
431 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
433 if self.samdb_url is not None and cache_ldb_initialize:
434 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
436 if self.samdb_url is None:
438 self.dirsync_filter = dirsync_filter
439 self.dirsync_attrs = dirsync_attrs
440 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"]
441 self.password_attrs = password_attrs
442 self.decrypt_samba_gpg = decrypt_samba_gpg
443 self.sync_command = sync_command
444 add_ldif = "dn: %s\n" % self.cache_dn +\
445 "objectClass: userSyncPasswords\n" +\
446 "samdbUrl:: %s\n" % base64.b64encode(get_bytes(self.samdb_url)).decode('utf8') +\
447 "dirsyncFilter:: %s\n" % base64.b64encode(get_bytes(self.dirsync_filter)).decode('utf8') +\
448 "".join("dirsyncAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.dirsync_attrs) +\
449 "dirsyncControl: %s\n" % self.dirsync_controls[0] +\
450 "".join("passwordAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.password_attrs)
451 if self.decrypt_samba_gpg:
452 add_ldif += "decryptSambaGPG: TRUE\n"
454 add_ldif += "decryptSambaGPG: FALSE\n"
455 if self.sync_command is not None:
456 add_ldif += "syncCommand: %s\n" % self.sync_command
457 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
458 self.cache.add_ldif(add_ldif)
459 self.current_pid = None
460 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
461 msgs = self.cache.parse_ldif(add_ldif)
462 changetype, msg = next(msgs)
463 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
464 self.outf.write("%s" % ldif)
466 self.dirsync_filter = str(res[0]["dirsyncFilter"][0])
467 self.dirsync_attrs = []
468 for a in res[0]["dirsyncAttribute"]:
469 self.dirsync_attrs.append(str(a))
470 self.dirsync_controls = [str(res[0]["dirsyncControl"][0]), "extended_dn:1:0"]
471 self.password_attrs = []
472 for a in res[0]["passwordAttribute"]:
473 self.password_attrs.append(str(a))
474 decrypt_string = str(res[0]["decryptSambaGPG"][0])
475 assert(decrypt_string in ["TRUE", "FALSE"])
476 if decrypt_string == "TRUE":
477 self.decrypt_samba_gpg = True
479 self.decrypt_samba_gpg = False
480 if "syncCommand" in res[0]:
481 self.sync_command = str(res[0]["syncCommand"][0])
483 self.sync_command = None
484 if "currentPid" in res[0]:
485 self.current_pid = int(res[0]["currentPid"][0])
487 self.current_pid = None
488 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
492 def run_sync_command(dn, ldif):
493 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
494 sync_command_p = Popen(self.sync_command,
499 res = sync_command_p.poll()
502 input = "%s" % (ldif)
503 reply = sync_command_p.communicate(
504 input.encode('utf-8'))[0].decode('utf-8')
505 log_msg("%s\n" % (reply))
506 res = sync_command_p.poll()
508 sync_command_p.terminate()
509 res = sync_command_p.wait()
511 if reply.startswith("DONE-EXIT: "):
514 log_msg("RESULT: %s\n" % (res))
515 raise Exception("ERROR: %s - %s\n" % (res, reply))
517 def handle_object(idx, dirsync_obj):
518 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
519 guid = ndr_unpack(misc.GUID, binary_guid)
520 binary_sid = dirsync_obj.dn.get_extended_component("SID")
521 sid = ndr_unpack(security.dom_sid, binary_sid)
522 domain_sid, rid = sid.split()
523 if rid == security.DOMAIN_RID_KRBTGT:
524 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
526 for a in list(dirsync_obj.keys()):
527 for h in dirsync_secret_attrs:
528 if a.lower() == h.lower():
530 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
531 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
532 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
533 obj = self.get_account_attributes(self.samdb,
535 basedn="<GUID=%s>" % guid,
536 filter="(objectClass=user)",
537 scope=ldb.SCOPE_BASE,
538 attrs=self.password_attrs,
539 decrypt=self.decrypt_samba_gpg)
540 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
541 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
542 if self.sync_command is None:
543 self.outf.write("%s" % (ldif))
545 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
546 run_sync_command(obj.dn, ldif)
548 def check_current_pid_conflict(terminate):
554 self.lockfd = os.open(self.lockfile, flags, 0o600)
555 except IOError as e4:
557 if err == errno.ENOENT:
560 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
561 (self.lockfile, msg, err))
564 got_exclusive = False
566 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
568 except IOError as e5:
570 if err != errno.EACCES and err != errno.EAGAIN:
571 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
572 (self.lockfile, msg, err))
575 if not got_exclusive:
576 buf = os.read(self.lockfd, 64)
577 self.current_pid = None
579 self.current_pid = int(buf)
580 except ValueError as e:
582 if self.current_pid is not None:
585 if got_exclusive and terminate:
587 os.ftruncate(self.lockfd, 0)
588 except IOError as e2:
590 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
591 (self.lockfile, msg, err))
593 os.close(self.lockfd)
598 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
599 except IOError as e6:
601 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
602 (self.lockfile, msg, err))
604 # We leave the function with the shared lock.
608 if self.lockfd != -1:
609 got_exclusive = False
610 # Try 5 times to get the exclusive lock.
611 for i in range(0, 5):
613 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
617 if err != errno.EACCES and err != errno.EAGAIN:
618 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
619 (pid, self.lockfile, msg, err))
624 if not got_exclusive:
625 log_msg("update_pid(%r): failed to get exclusive lock[%s]" %
626 (pid, self.lockfile))
627 raise CommandError("update_pid(%r): failed to get "
628 "exclusive lock[%s] after 5 seconds" %
629 (pid, self.lockfile))
636 os.ftruncate(self.lockfd, 0)
638 os.write(self.lockfd, get_bytes(buf))
639 except IOError as e3:
641 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
642 (self.lockfile, msg, err))
644 self.current_pid = pid
645 if self.current_pid is not None:
646 log_msg("currentPid: %d\n" % self.current_pid)
648 modify_ldif = "dn: %s\n" % (self.cache_dn) +\
649 "changetype: modify\n" +\
650 "replace: currentPid\n"
651 if self.current_pid is not None:
652 modify_ldif += "currentPid: %d\n" % (self.current_pid)
653 modify_ldif += "replace: currentTime\n" +\
654 "currentTime: %s\n" % ldb.timestring(int(time.time()))
655 self.cache.modify_ldif(modify_ldif)
658 def update_cache(res_controls):
659 assert len(res_controls) > 0
660 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
661 res_controls[0].critical = True
662 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
663 # This cookie can be extremely long
664 # log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
666 modify_ldif = "dn: %s\n" % (self.cache_dn) +\
667 "changetype: modify\n" +\
668 "replace: dirsyncControl\n" +\
669 "dirsyncControl: %s\n" % (self.dirsync_controls[0]) +\
670 "replace: currentTime\n" +\
671 "currentTime: %s\n" % ldb.timestring(int(time.time()))
672 self.cache.modify_ldif(modify_ldif)
675 def check_object(dirsync_obj, res_controls):
676 assert len(res_controls) > 0
677 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
679 binary_sid = dirsync_obj.dn.get_extended_component("SID")
680 sid = ndr_unpack(security.dom_sid, binary_sid)
682 lastCookie = str(res_controls[0])
684 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
685 expression="(lastCookie=%s)" % (
686 ldb.binary_encode(lastCookie)),
692 def update_object(dirsync_obj, res_controls):
693 assert len(res_controls) > 0
694 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
696 binary_sid = dirsync_obj.dn.get_extended_component("SID")
697 sid = ndr_unpack(security.dom_sid, binary_sid)
699 lastCookie = str(res_controls[0])
701 self.cache.transaction_start()
703 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
704 expression="(objectClass=*)",
705 attrs=["lastCookie"])
707 add_ldif = "dn: %s\n" % (dn) +\
708 "objectClass: userCookie\n" +\
709 "lastCookie: %s\n" % (lastCookie) +\
710 "currentTime: %s\n" % ldb.timestring(int(time.time()))
711 self.cache.add_ldif(add_ldif)
713 modify_ldif = "dn: %s\n" % (dn) +\
714 "changetype: modify\n" +\
715 "replace: lastCookie\n" +\
716 "lastCookie: %s\n" % (lastCookie) +\
717 "replace: currentTime\n" +\
718 "currentTime: %s\n" % ldb.timestring(int(time.time()))
719 self.cache.modify_ldif(modify_ldif)
720 self.cache.transaction_commit()
721 except Exception as e:
722 self.cache.transaction_cancel()
728 res = self.samdb.search(expression=str(self.dirsync_filter),
729 scope=ldb.SCOPE_SUBTREE,
730 attrs=self.dirsync_attrs,
731 controls=self.dirsync_controls)
732 log_msg("dirsync_loop(): results %d\n" % len(res))
735 done = check_object(r, res.controls)
738 update_object(r, res.controls)
740 update_cache(res.controls)
745 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
746 notify_controls = ["notification:1", "show_recycled:1"]
747 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
748 scope=ldb.SCOPE_SUBTREE,
750 controls=notify_controls,
754 log_msg("Resuming monitoring\n")
756 log_msg("Getting changes\n")
757 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
758 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
759 self.outf.write("syncCommand: %s\n" % self.sync_command)
765 for msg in notify_handle:
766 if not isinstance(msg, ldb.Message):
767 self.outf.write("referral: %s\n" % msg)
769 created = msg.get("uSNCreated")[0]
770 changed = msg.get("uSNChanged")[0]
771 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
772 (msg.dn, created, changed))
776 res = notify_handle.result()
781 orig_pid = os.getpid()
786 if pid == 0: # Actual daemon
788 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
793 if cache_ldb_initialize:
795 self.samdb = self.connect_for_passwords(url=self.samdb_url,
800 if logfile is not None:
801 import resource # Resource usage information.
802 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
803 if maxfd == resource.RLIM_INFINITY:
804 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
805 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
806 self.outf.write("Using logfile[%s]\n" % logfile)
807 for fd in range(0, maxfd):
818 log_msg("Attached to logfile[%s]\n" % (logfile))
819 self.logfile = logfile
822 conflict = check_current_pid_conflict(terminate)
824 if self.current_pid is None:
825 log_msg("No process running.\n")
828 log_msg("Process %d is not running anymore.\n" % (
832 log_msg("Sending SIGTERM to process %d.\n" % (
834 os.kill(self.current_pid, signal.SIGTERM)
837 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
838 os.getpid(), self.current_pid))
842 update_pid(os.getpid())
847 retry_sleep_max = 600
852 retry_sleep = retry_sleep_min
854 while self.samdb is None:
856 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
857 time.sleep(retry_sleep)
858 retry_sleep = retry_sleep * 2
859 if retry_sleep >= retry_sleep_max:
860 retry_sleep = retry_sleep_max
861 log_msg("Connecting to '%s'\n" % self.samdb_url)
863 self.samdb = self.connect_for_passwords(url=self.samdb_url)
864 except Exception as msg:
866 log_msg("Connect to samdb Exception => (%s)\n" % msg)
872 except ldb.LdbError as e7:
873 (enum, estr) = e7.args
875 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))