summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorStefan Metzmacher <metze@samba.org>2016-01-22 21:52:26 +0100
committerStefan Metzmacher <metze@samba.org>2016-07-22 16:03:26 +0200
commitc68cb6a1d9d366ac3e564245ecca34348a4f1aa2 (patch)
treed586e9feb069763e556483b7d29d998f8658ba9a /python
parentc8fb61cadca367b53e8d7ee64a3d19ab5ebf75e4 (diff)
downloadsamba-c68cb6a1d9d366ac3e564245ecca34348a4f1aa2.tar.gz
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 <metze@samba.org> Reviewed-by: Alexander Bokovoy <ab@samba.org>
Diffstat (limited to 'python')
-rw-r--r--python/samba/netcmd/user.py760
1 files changed, 760 insertions, 0 deletions
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=%s>" % 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()