From c68cb6a1d9d366ac3e564245ecca34348a4f1aa2 Mon Sep 17 00:00:00 2001 From: Stefan Metzmacher Date: Fri, 22 Jan 2016 21:52:26 +0100 Subject: [PATCH] samba-tool: add 'user syncpasswords' command This provides an easy way to keep passwords in sync with another account database, e.g. an OpenLDAP server. It provides a functionality like the "passwd program" for the "unix password sync" feature of a standalone, member and classic (NT4) server, but for an active directory domain controller. The provided script is called for each account/password related change. Like the 'user getpassword' command it allows virtual attributes like: virtualClearTextUTF16, virtualClearTextUTF8, virtualCryptSHA256, virtualCryptSHA512, virtualSSHA Note that this command should just run on a single domain controller (typically the PDC-emulator). Signed-off-by: Stefan Metzmacher Reviewed-by: Alexander Bokovoy --- python/samba/netcmd/user.py | 760 ++++++++++++++++++++++++++++++++++++ 1 file changed, 760 insertions(+) diff --git a/python/samba/netcmd/user.py b/python/samba/netcmd/user.py index 7c3d93a04a1..092618d7de0 100644 --- a/python/samba/netcmd/user.py +++ b/python/samba/netcmd/user.py @@ -22,9 +22,13 @@ import ldb import pwd import os import sys +import fcntl +import signal import errno +import time import base64 import binascii +from subprocess import Popen, PIPE, STDOUT from getpass import getpass from samba.auth import system_session from samba.samdb import SamDB @@ -37,6 +41,7 @@ from samba import ( dsdb, gensec, generate_random_password, + Ldb, ) from samba.net import Net @@ -1081,6 +1086,760 @@ samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS- self.outf.write("%s" % ldif) self.outf.write("Got password OK\n") +class cmd_user_syncpasswords(GetPasswordCommand): + """Sync the password of user accounts. + +This syncs logon passwords for user accounts. + +Note that this command should run on a single domain controller only +(typically the PDC-emulator). + +The command must be run from the root user id or another authorized user id. +The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the +local path. By default, ldapi:// is used with the default path to the +privileged ldapi socket. + +This command has three modes: "Cache Initialization", "Sync Loop Run" and +"Sync Loop Terminate". + + +Cache Initialization +==================== + +The first time, this command needs to be called with +'--cache-ldb-initialize' in order to initialize its cache. + +The cache initialization requires '--attributes' and allows the following +optional options: '--script', '--filter' or +'-H/--URL'. + +The '--attributes' parameter takes a comma separated list of attributes, +which will be printed or given to the script specified by '--script'. If a +specified attribute is not available on an object it will be silently omitted. +All attributes defined in the schema (e.g. the unicodePwd attribute holds +the NTHASH) and the following virtual attributes are possible (see '--help' +for supported virtual attributes in your environment): + + virtualClearTextUTF16: The raw cleartext as stored in the + 'Primary:CLEARTEXT' buffer inside of the + supplementalCredentials attribute. This typically + contains valid UTF-16-LE, but may contain random + bytes, e.g. for computer accounts. + + virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8 + (only from valid UTF-16-LE) + + virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1 + checksum, useful for OpenLDAP's '{SSHA}' algorithm. + + virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256 + checksum, useful for OpenLDAP's '{CRYPT}' algorithm, + with a $5$... salt, see crypt(3) on modern systems. + + virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512 + checksum, useful for OpenLDAP's '{CRYPT}' algorithm, + with a $6$... salt, see crypt(3) on modern systems. + +The '--script' option specifies a custom script that is called whenever any +of the dirsyncAttributes (see below) was changed. The script is called +without any arguments. It gets the LDIF for exactly one object on STDIN. +If the script processed the object successfully it has to respond with a +single line starting with 'DONE-EXIT: ' followed by an optional message. + +Note that the script might be called without any password change, e.g. if +the account was disabled (an userAccountControl change) or the +sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes +are always returned as unique identifier of the account. It might be useful +to also ask for non-password attributes like: objectSid, sAMAccountName, +userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber. +Depending on the object, some attributes may not be present/available, +but you always get the current state (and not a diff). + +If no '--script' option is specified, the LDIF will be printed on STDOUT or +into the logfile. + +The default filter for the LDAP_SERVER_DIRSYNC_OID search is: +(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\ + (!(sAMAccountName=krbtgt*))) +This means only normal (non-krbtgt) user +accounts are monitored. The '--filter' can modify that, e.g. if it's +required to also sync computer accounts. + + +Sync Loop Run +============= + +This (default) mode runs in an endless loop waiting for password related +changes in the active directory database. It makes use of the +LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order +get changes in a reliable fashion. Objects are monitored for changes of the +following dirsyncAttributes: + + unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName, + userPrincipalName and userAccountControl. + +It recovers from LDAP disconnects and updates the cache in conservative way +(in single steps after each succesfully processed change). An error from +the script (specified by '--script') will result in fatal error and this +command will exit. But the cache state should be still valid and can be +resumed in the next "Sync Loop Run". + +The '--logfile' option specifies an optional (required if '--daemon' is +specified) logfile that takes all output of the command. The logfile is +automatically reopened if fstat returns st_nlink == 0. + +The optional '--daemon' option will put the command into the background. + +You can stop the command without the '--daemon' option, also by hitting +strg+c. + +If you specify the '--no-wait' option the command skips the +LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once +all LDAP_SERVER_DIRSYNC_OID changes are consumed. + +Sync Loop Terminate +=================== + +In order to terminate an already running command (likely as daemon) the +'--terminate' option can be used. This also requires the '--logfile' option +to be specified. + + +Example1: +samba-tool user syncpasswords --cache-ldb-initialize \\ + --attributes=virtualClearTextUTF8 +samba-tool user syncpasswords + +Example2: +samba-tool user syncpasswords --cache-ldb-initialize \\ + --attributes=objectGUID,objectSID,sAMAccountName,\\ + userPrincipalName,userAccountControl,pwdLastSet,\\ + msDS-KeyVersionNumber,virtualCryptSHA512 \\ + --script=/path/to/my-custom-syncpasswords-script.py +samba-tool user syncpasswords --daemon \\ + --logfile=/var/log/samba/user-syncpasswords.log +samba-tool user syncpasswords --terminate \\ + --logfile=/var/log/samba/user-syncpasswords.log + +""" + def __init__(self): + super(cmd_user_syncpasswords, self).__init__() + + synopsis = "%prog [--cache-ldb-initialize] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("--cache-ldb-initialize", + help="Initialize the cache for the first time", + dest="cache_ldb_initialize", action="store_true"), + Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str, + metavar="CACHE-LDB-PATH", dest="cache_ldb"), + Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str, + metavar="URL", dest="H"), + Option("--filter", help="optional LDAP filter to set password on", type=str, + metavar="LDAP-SEARCH-FILTER", dest="filter"), + Option("--attributes", type=str, + help=virtual_attributes_help, + metavar="ATTRIBUTELIST", dest="attributes"), + Option("--script", help="Script that is called for each password change", type=str, + metavar="/path/to/syncpasswords.script", dest="script"), + Option("--no-wait", help="Don't block waiting for changes", + action="store_true", default=False, dest="nowait"), + Option("--logfile", type=str, + help="The logfile to use (required in --daemon mode).", + metavar="/path/to/syncpasswords.log", dest="logfile"), + Option("--daemon", help="daemonize after initial setup", + action="store_true", default=False, dest="daemon"), + Option("--terminate", + help="Send a SIGTERM to an already running (daemon) process", + action="store_true", default=False, dest="terminate"), + ] + + def run(self, cache_ldb_initialize=False, cache_ldb=None, + H=None, filter=None, + attributes=None, + script=None, nowait=None, logfile=None, daemon=None, terminate=None, + sambaopts=None, versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.logfile = None + self.samdb_url = None + self.samdb = None + self.cache = None + + if not cache_ldb_initialize: + if attributes is not None: + raise CommandError("--attributes is only allowed together with --cache-ldb-initialize") + if script is not None: + raise CommandError("--script is only allowed together with --cache-ldb-initialize") + if filter is not None: + raise CommandError("--filter is only allowed together with --cache-ldb-initialize") + if H is not None: + raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize") + else: + if nowait is not False: + raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize") + if logfile is not None: + raise CommandError("--logfile is not allowed together with --cache-ldb-initialize") + if daemon is not False: + raise CommandError("--daemon is not allowed together with --cache-ldb-initialize") + if terminate is not False: + raise CommandError("--terminate is not allowed together with --cache-ldb-initialize") + + if nowait is True: + if daemon is True: + raise CommandError("--daemon is not allowed together with --no-wait") + if terminate is not False: + raise CommandError("--terminate is not allowed together with --no-wait") + + if terminate is True and daemon is True: + raise CommandError("--terminate is not allowed together with --daemon") + + if daemon is True and logfile is None: + raise CommandError("--daemon is only allowed together with --logfile") + + if terminate is True and logfile is None: + raise CommandError("--terminate is only allowed together with --logfile") + + if script is not None: + if not os.path.exists(script): + raise CommandError("script[%s] does not exist!" % script) + + sync_command = "%s" % os.path.abspath(script) + else: + sync_command = None + + dirsync_filter = filter + if dirsync_filter is None: + dirsync_filter = "(&" + \ + "(objectClass=user)" + \ + "(userAccountControl:%s:=%u)" % ( + ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \ + "(!(sAMAccountName=krbtgt*))" + \ + ")" + + dirsync_secret_attrs = [ + "unicodePwd", + "dBCSPwd", + "supplementalCredentials", + ] + + dirsync_attrs = dirsync_secret_attrs + [ + "pwdLastSet", + "sAMAccountName", + "userPrincipalName", + "userAccountControl", + "isDeleted", + "isRecycled", + ] + + password_attrs = None + + if cache_ldb_initialize: + if H is None: + H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi")) + + password_attrs = self.parse_attributes(attributes) + lower_attrs = [x.lower() for x in password_attrs] + # We always return these in order to track deletions + for a in ["objectGUID", "isDeleted", "isRecycled"]: + if a.lower() not in lower_attrs: + password_attrs += [a] + + if cache_ldb is not None: + if cache_ldb.lower().startswith("ldapi://"): + raise CommandError("--cache_ldb ldapi:// is not supported") + elif cache_ldb.lower().startswith("ldap://"): + raise CommandError("--cache_ldb ldap:// is not supported") + elif cache_ldb.lower().startswith("ldaps://"): + raise CommandError("--cache_ldb ldaps:// is not supported") + elif cache_ldb.lower().startswith("tdb://"): + pass + else: + if not os.path.exists(cache_ldb): + cache_ldb = self.lp.private_path(cache_ldb) + else: + cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb") + + self.lockfile = "%s.pid" % cache_ldb + + def log_msg(msg): + if self.logfile is not None: + info = os.fstat(0) + if info.st_nlink == 0: + logfile = self.logfile + self.logfile = None + log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile)) + logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600) + os.dup2(logfd, 0) + os.dup2(logfd, 1) + os.dup2(logfd, 2) + os.close(logfd) + log_msg("Reopened logfile[%s]\n" % (logfile)) + self.logfile = logfile + msg = "%s: pid[%d]: %s" % ( + time.ctime(), + os.getpid(), + msg) + self.outf.write(msg) + return + + def load_cache(): + cache_attrs = [ + "samdbUrl", + "dirsyncFilter", + "dirsyncAttribute", + "dirsyncControl", + "passwordAttribute", + "syncCommand", + "currentPid", + ] + + self.cache = Ldb(cache_ldb) + self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS") + res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE, + attrs=cache_attrs) + if len(res) == 1: + try: + self.samdb_url = res[0]["samdbUrl"][0] + except KeyError as e: + self.samdb_url = None + else: + self.samdb_url = None + if self.samdb_url is None and not cache_ldb_initialize: + raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % ( + cache_ldb)) + if self.samdb_url is not None and cache_ldb_initialize: + raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % ( + cache_ldb)) + if self.samdb_url is None: + self.samdb_url = H + self.dirsync_filter = dirsync_filter + self.dirsync_attrs = dirsync_attrs + self.dirsync_controls = ["dirsync:1:0:0","extended_dn:1:0"]; + self.password_attrs = password_attrs + self.sync_command = sync_command + add_ldif = "dn: %s\n" % self.cache_dn + add_ldif += "objectClass: userSyncPasswords\n" + add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url) + add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter) + for a in self.dirsync_attrs: + add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a) + add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0] + for a in self.password_attrs: + add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a) + if self.sync_command is not None: + add_ldif += "syncCommand: %s\n" % self.sync_command + add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time())) + self.cache.add_ldif(add_ldif) + self.current_pid = None + self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb)) + msgs = self.cache.parse_ldif(add_ldif) + changetype,msg = msgs.next() + ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE) + self.outf.write("%s" % ldif) + else: + self.dirsync_filter = res[0]["dirsyncFilter"][0] + self.dirsync_attrs = [] + for a in res[0]["dirsyncAttribute"]: + self.dirsync_attrs.append(a) + self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"] + self.password_attrs = [] + for a in res[0]["passwordAttribute"]: + self.password_attrs.append(a) + if "syncCommand" in res[0]: + self.sync_command = res[0]["syncCommand"][0] + else: + self.sync_command = None + if "currentPid" in res[0]: + self.current_pid = int(res[0]["currentPid"][0]) + else: + self.current_pid = None + log_msg("Using cache_ldb[%s]\n" % (cache_ldb)) + + return + + def run_sync_command(dn, ldif): + log_msg("Call Popen[%s] for %s\n" % (dn, self.sync_command)) + sync_command_p = Popen(self.sync_command, + stdin=PIPE, + stdout=PIPE, + stderr=STDOUT) + + res = sync_command_p.poll() + assert res is None + + input = "%s" % (ldif) + reply = sync_command_p.communicate(input)[0] + log_msg("%s\n" % (reply)) + res = sync_command_p.poll() + if res is None: + sync_command_p.terminate() + res = sync_command_p.wait() + + if reply.startswith("DONE-EXIT: "): + return + + log_msg("RESULT: %s\n" % (res)) + raise Exception("ERROR: %s - %s\n" % (res, reply)) + + def handle_object(idx, dirsync_obj): + binary_guid = dirsync_obj.dn.get_extended_component("GUID") + guid = ndr_unpack(misc.GUID, binary_guid) + binary_sid = dirsync_obj.dn.get_extended_component("SID") + sid = ndr_unpack(security.dom_sid, binary_sid) + domain_sid, rid = sid.split() + if rid == security.DOMAIN_RID_KRBTGT: + log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx)) + return + for a in list(dirsync_obj.keys()): + for h in dirsync_secret_attrs: + if a.lower() == h.lower(): + del dirsync_obj[a] + dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"] + dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE) + log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif)) + obj = self.get_account_attributes(self.samdb, + username="%s" % sid, + basedn="" % guid, + filter="(objectClass=user)", + scope=ldb.SCOPE_BASE, + attrs=self.password_attrs) + ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE) + log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid)) + if self.sync_command is None: + self.outf.write("%s" % (ldif)) + return + self.outf.write("# attrs=%s\n" % (sorted(obj.keys()))) + run_sync_command(obj.dn, ldif) + + def check_current_pid_conflict(terminate): + flags = os.O_RDWR + if not terminate: + flags |= os.O_CREAT + + try: + self.lockfd = os.open(self.lockfile, flags, 0600) + except IOError as (err, msg): + if err == errno.ENOENT: + if terminate: + return False + log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" % + (self.lockfile, msg, err)) + raise + + got_exclusive = False + try: + fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB) + got_exclusive = True + except IOError as (err, msg): + if err != errno.EACCES and err != errno.EAGAIN: + log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" % + (self.lockfile, msg, err)) + raise + + if not got_exclusive: + buf = os.read(self.lockfd, 64) + self.current_pid = None + try: + self.current_pid = int(buf) + except ValueError as e: + pass + if self.current_pid is not None: + return True + + if got_exclusive and terminate: + try: + os.ftruncate(self.lockfd, 0) + except IOError as (err, msg): + log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" % + (self.lockfile, msg, err)) + raise + os.close(self.lockfd) + self.lockfd = -1 + return False + + try: + fcntl.lockf(self.lockfd, fcntl.LOCK_SH) + except IOError as (err, msg): + log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" % + (self.lockfile, msg, err)) + + # We leave the function with the shared lock. + return False + + def update_pid(pid): + if self.lockfd != -1: + got_exclusive = False + # Try 5 times to get the exclusiv lock. + for i in xrange(0, 5): + try: + fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB) + got_exclusive = True + except IOError as (err, msg): + if err != errno.EACCES and err != errno.EAGAIN: + log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" % + (pid, self.lockfile, msg, err)) + raise + if got_exclusive: + break + time.sleep(1) + if not got_exclusive: + log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" % + (pid, self.lockfile)) + raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" % + (pid, self.lockfile)) + + if pid is not None: + buf = "%d\n" % pid + else: + buf = None + try: + os.ftruncate(self.lockfd, 0) + if buf is not None: + os.write(self.lockfd, buf) + except IOError as (err, msg): + log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" % + (self.lockfile, msg, err)) + raise + self.current_pid = pid + if self.current_pid is not None: + log_msg("currentPid: %d\n" % self.current_pid) + + modify_ldif = "dn: %s\n" % (self.cache_dn) + modify_ldif += "changetype: modify\n" + modify_ldif += "replace: currentPid\n" + if self.current_pid is not None: + modify_ldif += "currentPid: %d\n" % (self.current_pid) + modify_ldif += "replace: currentTime\n" + modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time())) + self.cache.modify_ldif(modify_ldif) + return + + def update_cache(res_controls): + assert len(res_controls) > 0 + assert res_controls[0].oid == "1.2.840.113556.1.4.841" + res_controls[0].critical = True + self.dirsync_controls = [str(res_controls[0]),"extended_dn:1:0"] + log_msg("dirsyncControls: %r\n" % self.dirsync_controls) + + modify_ldif = "dn: %s\n" % (self.cache_dn) + modify_ldif += "changetype: modify\n" + modify_ldif += "replace: dirsyncControl\n" + modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0]) + modify_ldif += "replace: currentTime\n" + modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time())) + self.cache.modify_ldif(modify_ldif) + return + + def check_object(dirsync_obj, res_controls): + assert len(res_controls) > 0 + assert res_controls[0].oid == "1.2.840.113556.1.4.841" + + binary_sid = dirsync_obj.dn.get_extended_component("SID") + sid = ndr_unpack(security.dom_sid, binary_sid) + dn = "KEY=%s" % sid + lastCookie = str(res_controls[0]) + + res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE, + expression="(lastCookie=%s)" % ( + ldb.binary_encode(lastCookie)), + attrs=[]) + if len(res) == 1: + return True + return False + + def update_object(dirsync_obj, res_controls): + assert len(res_controls) > 0 + assert res_controls[0].oid == "1.2.840.113556.1.4.841" + + binary_sid = dirsync_obj.dn.get_extended_component("SID") + sid = ndr_unpack(security.dom_sid, binary_sid) + dn = "KEY=%s" % sid + lastCookie = str(res_controls[0]) + + self.cache.transaction_start() + try: + res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE, + expression="(objectClass=*)", + attrs=["lastCookie"]) + if len(res) == 0: + add_ldif = "dn: %s\n" % (dn) + add_ldif += "objectClass: userCookie\n" + add_ldif += "lastCookie: %s\n" % (lastCookie) + add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time())) + self.cache.add_ldif(add_ldif) + else: + modify_ldif = "dn: %s\n" % (dn) + modify_ldif += "changetype: modify\n" + modify_ldif += "replace: lastCookie\n" + modify_ldif += "lastCookie: %s\n" % (lastCookie) + modify_ldif += "replace: currentTime\n" + modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time())) + self.cache.modify_ldif(modify_ldif) + self.cache.transaction_commit() + except Exception as e: + self.cache.transaction_cancel() + + return + + def dirsync_loop(): + while True: + res = self.samdb.search(expression=self.dirsync_filter, + scope=ldb.SCOPE_SUBTREE, + attrs=self.dirsync_attrs, + controls=self.dirsync_controls) + log_msg("dirsync_loop(): results %d\n" % len(res)) + ri = 0 + for r in res: + done = check_object(r, res.controls) + if not done: + handle_object(ri, r) + update_object(r, res.controls) + ri += 1 + update_cache(res.controls) + if len(res) == 0: + break + + def sync_loop(wait): + notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"] + notify_controls = ["notification:1"] + notify_handle = self.samdb.search_iterator(expression="objectClass=*", + scope=ldb.SCOPE_SUBTREE, + attrs=notify_attrs, + controls=notify_controls, + timeout=-1) + + if wait is True: + log_msg("Resuming monitoring\n") + else: + log_msg("Getting changes\n") + self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter) + self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls) + self.outf.write("syncCommand: %s\n" % self.sync_command) + dirsync_loop() + + if wait is not True: + return + + for msg in notify_handle: + if not isinstance(msg, ldb.Message): + self.outf.write("referal: %s\n" % msg) + continue + created = msg.get("uSNCreated")[0] + changed = msg.get("uSNChanged")[0] + log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" % + (msg.dn, created, changed)) + + dirsync_loop() + + res = notify_handle.result() + + def daemonize(): + self.samdb = None + self.cache = None + orig_pid = os.getpid() + pid = os.fork() + if pid == 0: + os.setsid() + pid = os.fork() + if pid == 0: # Actual daemon + pid = os.getpid() + log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid)) + load_cache() + return + os._exit(0) + + if cache_ldb_initialize: + self.samdb_url = H + self.samdb = self.connect_system_samdb(url=self.samdb_url, + verbose=True) + load_cache() + return + + if logfile is not None: + import resource # Resource usage information. + maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] + if maxfd == resource.RLIM_INFINITY: + maxfd = 1024 # Rough guess at maximum number of open file descriptors. + logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600) + self.outf.write("Using logfile[%s]\n" % logfile) + for fd in range(0, maxfd): + if fd == logfd: + continue + try: + os.close(fd) + except OSError: + pass + os.dup2(logfd, 0) + os.dup2(logfd, 1) + os.dup2(logfd, 2) + os.close(logfd) + log_msg("Attached to logfile[%s]\n" % (logfile)) + self.logfile = logfile + + load_cache() + conflict = check_current_pid_conflict(terminate) + if terminate: + if self.current_pid is None: + log_msg("No process running.\n") + return + if not conflict: + log_msg("Proccess %d is not running anymore.\n" % ( + self.current_pid)) + update_pid(None) + return + log_msg("Sending SIGTERM to proccess %d.\n" % ( + self.current_pid)) + os.kill(self.current_pid, signal.SIGTERM) + return + if conflict: + raise CommandError("Exiting pid %d, command is already running as pid %d" % ( + os.getpid(), self.current_pid)) + + if daemon is True: + daemonize() + update_pid(os.getpid()) + + wait = True + while wait is True: + retry_sleep_min = 1 + retry_sleep_max = 600 + if nowait is True: + wait = False + retry_sleep = 0 + else: + retry_sleep = retry_sleep_min + + while self.samdb is None: + if retry_sleep != 0: + log_msg("Wait before connect - sleep(%d)\n" % retry_sleep) + time.sleep(retry_sleep) + retry_sleep = retry_sleep * 2 + if retry_sleep >= retry_sleep_max: + retry_sleep = retry_sleep_max + log_msg("Connecting to '%s'\n" % self.samdb_url) + try: + self.samdb = self.connect_system_samdb(url=self.samdb_url) + except Exception as msg: + self.samdb = None + log_msg("Connect to samdb Exception => (%s)\n" % msg) + if wait is not True: + raise + + try: + sync_loop(wait) + except ldb.LdbError as (enum, estr): + self.samdb = None + log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr)) + + update_pid(None) + return + class cmd_user(SuperCommand): """User management.""" @@ -1095,3 +1854,4 @@ class cmd_user(SuperCommand): subcommands["password"] = cmd_user_password() subcommands["setpassword"] = cmd_user_setpassword() subcommands["getpassword"] = cmd_user_getpassword() + subcommands["syncpasswords"] = cmd_user_syncpasswords() -- 2.34.1