samba-tool: Prepare to allow samba-tool user getpasswords to operate against a remote...
[bjacke/samba-autobuild/.git] / python / samba / netcmd / user / readpasswords / syncpasswords.py
1 # user management
2 #
3 # user syncpasswords command
4 #
5 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
6 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
7 #
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.
12 #
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.
17 #
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/>.
20 #
21
22 import base64
23 import errno
24 import fcntl
25 import os
26 import signal
27 import time
28 from subprocess import Popen, PIPE, STDOUT
29
30 import ldb
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
37
38 from .common import (
39     GetPasswordCommand,
40     gpg_decrypt,
41     decrypt_samba_gpg_help,
42     virtual_attributes_help
43 )
44
45
46 class cmd_user_syncpasswords(GetPasswordCommand):
47     """Sync the password of user accounts.
48
49 This syncs logon passwords for user accounts.
50
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.
54
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.
59
60 This command has three modes: "Cache Initialization", "Sync Loop Run" and
61 "Sync Loop Terminate".
62
63
64 Cache Initialization
65 ====================
66
67 The first time, this command needs to be called with
68 '--cache-ldb-initialize' in order to initialize its cache.
69
70 The cache initialization requires '--attributes' and allows the following
71 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
72 '-H/--URL'.
73
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):
80
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.
87
88    virtualClearTextUTF8:  As virtualClearTextUTF16, but converted to UTF-8
89                           (only from valid UTF-16-LE).
90
91    virtualSSHA:           As virtualClearTextUTF8, but a salted SHA-1
92                           checksum, useful for OpenLDAP's '{SSHA}' algorithm.
93
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'.
110
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'.
127
128    virtualWDigestNN:      The individual hash values stored in
129                           'Primary:WDigest' where NN is the hash number in
130                           the range 01 to 29.
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
134                           is incorrect.
135
136    virtualKerberosSalt:   This results the salt string that is used to compute
137                           Kerberos keys from a UTF-8 cleartext password.
138
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
143                           smb.conf.
144
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.
152
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.
158
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).
167
168 If no '--script' option is specified, the LDIF will be printed on STDOUT or
169 into the logfile.
170
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.
177
178
179 Sync Loop Run
180 =============
181
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:
187
188   unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
189   userPrincipalName and userAccountControl.
190
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".
196
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.
200
201 The optional '--daemon' option will put the command into the background.
202
203 You can stop the command without the '--daemon' option, also by hitting
204 strg+c.
205
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.
209
210 Sync Loop Terminate
211 ===================
212
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
215 to be specified.
216
217
218 Example1:
219 samba-tool user syncpasswords --cache-ldb-initialize \\
220     --attributes=virtualClearTextUTF8
221 samba-tool user syncpasswords
222
223 Example2:
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
233
234 """
235
236     synopsis = "%prog [--cache-ldb-initialize] [options]"
237
238     takes_optiongroups = {
239         "sambaopts": options.SambaOptions,
240         "versionopts": options.VersionOptions,
241     }
242
243     takes_options = [
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"),
271     ]
272
273     def run(self, cache_ldb_initialize=False, cache_ldb=None,
274             H=None, filter=None,
275             attributes=None, decrypt_samba_gpg=None,
276             script=None, nowait=None, logfile=None, daemon=None, terminate=None,
277             sambaopts=None, versionopts=None):
278
279         self.lp = sambaopts.get_loadparm()
280         self.logfile = None
281         self.samdb_url = None
282         self.samdb = None
283         self.cache = None
284
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")
294             if H is not None:
295                 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
296         else:
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")
305
306         if nowait is True:
307             if daemon is True:
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")
311
312         if terminate is True and daemon is True:
313             raise CommandError("--terminate is not allowed together with --daemon")
314
315         if daemon is True and logfile is None:
316             raise CommandError("--daemon is only allowed together with --logfile")
317
318         if terminate is True and logfile is None:
319             raise CommandError("--terminate is only allowed together with --logfile")
320
321         if script is not None:
322             if not os.path.exists(script):
323                 raise CommandError("script[%s] does not exist!" % script)
324
325             sync_command = "%s" % os.path.abspath(script)
326         else:
327             sync_command = None
328
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*))" + \
336                              ")"
337
338         dirsync_secret_attrs = [
339             "unicodePwd",
340             "dBCSPwd",
341             "supplementalCredentials",
342         ]
343
344         dirsync_attrs = dirsync_secret_attrs + [
345             "pwdLastSet",
346             "sAMAccountName",
347             "userPrincipalName",
348             "userAccountControl",
349             "isDeleted",
350             "isRecycled",
351         ]
352
353         password_attrs = None
354
355         if cache_ldb_initialize:
356             if H is None:
357                 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
358
359             if decrypt_samba_gpg and not gpg_decrypt:
360                 raise CommandError(decrypt_samba_gpg_help)
361
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]
368
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://"):
377                 pass
378             else:
379                 if not os.path.exists(cache_ldb):
380                     cache_ldb = self.lp.private_path(cache_ldb)
381         else:
382             cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
383
384         self.lockfile = "%s.pid" % cache_ldb
385
386         def log_msg(msg):
387             if self.logfile is not None:
388                 info = os.fstat(0)
389                 if info.st_nlink == 0:
390                     logfile = self.logfile
391                     self.logfile = None
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)
394                     os.dup2(logfd, 0)
395                     os.dup2(logfd, 1)
396                     os.dup2(logfd, 2)
397                     os.close(logfd)
398                     log_msg("Reopened logfile[%s]\n" % (logfile))
399                     self.logfile = logfile
400             msg = "%s: pid[%d]: %s" % (
401                     time.ctime(),
402                     os.getpid(),
403                     msg)
404             self.outf.write(msg)
405             return
406
407         def load_cache():
408             cache_attrs = [
409                 "samdbUrl",
410                 "dirsyncFilter",
411                 "dirsyncAttribute",
412                 "dirsyncControl",
413                 "passwordAttribute",
414                 "decryptSambaGPG",
415                 "syncCommand",
416                 "currentPid",
417             ]
418
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,
422                                     attrs=cache_attrs)
423             if len(res) == 1:
424                 try:
425                     self.samdb_url = str(res[0]["samdbUrl"][0])
426                 except KeyError as e:
427                     self.samdb_url = None
428             else:
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" % (
432                                    cache_ldb))
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" % (
435                                    cache_ldb))
436             if self.samdb_url is None:
437                 self.samdb_url = H
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"
453                 else:
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)
465             else:
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
478                 else:
479                     self.decrypt_samba_gpg = False
480                 if "syncCommand" in res[0]:
481                     self.sync_command = str(res[0]["syncCommand"][0])
482                 else:
483                     self.sync_command = None
484                 if "currentPid" in res[0]:
485                     self.current_pid = int(res[0]["currentPid"][0])
486                 else:
487                     self.current_pid = None
488                 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
489
490             return
491
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,
495                                    stdin=PIPE,
496                                    stdout=PIPE,
497                                    stderr=STDOUT)
498
499             res = sync_command_p.poll()
500             assert res is None
501
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()
507             if res is None:
508                 sync_command_p.terminate()
509             res = sync_command_p.wait()
510
511             if reply.startswith("DONE-EXIT: "):
512                 return
513
514             log_msg("RESULT: %s\n" % (res))
515             raise Exception("ERROR: %s - %s\n" % (res, reply))
516
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))
525                 return
526             for a in list(dirsync_obj.keys()):
527                 for h in dirsync_secret_attrs:
528                     if a.lower() == h.lower():
529                         del dirsync_obj[a]
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,
534                                               username="%s" % sid,
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))
544                 return
545             self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
546             run_sync_command(obj.dn, ldif)
547
548         def check_current_pid_conflict(terminate):
549             flags = os.O_RDWR
550             if not terminate:
551                 flags |= os.O_CREAT
552
553             try:
554                 self.lockfd = os.open(self.lockfile, flags, 0o600)
555             except IOError as e4:
556                 (err, msg) = e4.args
557                 if err == errno.ENOENT:
558                     if terminate:
559                         return False
560                 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
561                         (self.lockfile, msg, err))
562                 raise
563
564             got_exclusive = False
565             try:
566                 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
567                 got_exclusive = True
568             except IOError as e5:
569                 (err, msg) = e5.args
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))
573                     raise
574
575             if not got_exclusive:
576                 buf = os.read(self.lockfd, 64)
577                 self.current_pid = None
578                 try:
579                     self.current_pid = int(buf)
580                 except ValueError as e:
581                     pass
582                 if self.current_pid is not None:
583                     return True
584
585             if got_exclusive and terminate:
586                 try:
587                     os.ftruncate(self.lockfd, 0)
588                 except IOError as e2:
589                     (err, msg) = e2.args
590                     log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
591                             (self.lockfile, msg, err))
592                     raise
593                 os.close(self.lockfd)
594                 self.lockfd = -1
595                 return False
596
597             try:
598                 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
599             except IOError as e6:
600                 (err, msg) = e6.args
601                 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
602                         (self.lockfile, msg, err))
603
604             # We leave the function with the shared lock.
605             return False
606
607         def update_pid(pid):
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):
612                     try:
613                         fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
614                         got_exclusive = True
615                     except IOError as e:
616                         (err, msg) = e.args
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))
620                             raise
621                     if got_exclusive:
622                         break
623                     time.sleep(1)
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))
630
631                 if pid is not None:
632                     buf = "%d\n" % pid
633                 else:
634                     buf = None
635                 try:
636                     os.ftruncate(self.lockfd, 0)
637                     if buf is not None:
638                         os.write(self.lockfd, get_bytes(buf))
639                 except IOError as e3:
640                     (err, msg) = e3.args
641                     log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
642                             (self.lockfile, msg, err))
643                     raise
644             self.current_pid = pid
645             if self.current_pid is not None:
646                 log_msg("currentPid: %d\n" % self.current_pid)
647
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)
656             return
657
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)
665
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)
673             return
674
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"
678
679             binary_sid = dirsync_obj.dn.get_extended_component("SID")
680             sid = ndr_unpack(security.dom_sid, binary_sid)
681             dn = "KEY=%s" % sid
682             lastCookie = str(res_controls[0])
683
684             res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
685                                     expression="(lastCookie=%s)" % (
686                                         ldb.binary_encode(lastCookie)),
687                                     attrs=[])
688             if len(res) == 1:
689                 return True
690             return False
691
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"
695
696             binary_sid = dirsync_obj.dn.get_extended_component("SID")
697             sid = ndr_unpack(security.dom_sid, binary_sid)
698             dn = "KEY=%s" % sid
699             lastCookie = str(res_controls[0])
700
701             self.cache.transaction_start()
702             try:
703                 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
704                                         expression="(objectClass=*)",
705                                         attrs=["lastCookie"])
706                 if len(res) == 0:
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)
712                 else:
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()
723
724             return
725
726         def dirsync_loop():
727             while True:
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))
733                 ri = 0
734                 for r in res:
735                     done = check_object(r, res.controls)
736                     if not done:
737                         handle_object(ri, r)
738                         update_object(r, res.controls)
739                     ri += 1
740                 update_cache(res.controls)
741                 if len(res) == 0:
742                     break
743
744         def sync_loop(wait):
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,
749                                                        attrs=notify_attrs,
750                                                        controls=notify_controls,
751                                                        timeout=-1)
752
753             if wait is True:
754                 log_msg("Resuming monitoring\n")
755             else:
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)
760             dirsync_loop()
761
762             if wait is not True:
763                 return
764
765             for msg in notify_handle:
766                 if not isinstance(msg, ldb.Message):
767                     self.outf.write("referral: %s\n" % msg)
768                     continue
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))
773
774                 dirsync_loop()
775
776             res = notify_handle.result()
777
778         def daemonize():
779             self.samdb = None
780             self.cache = None
781             orig_pid = os.getpid()
782             pid = os.fork()
783             if pid == 0:
784                 os.setsid()
785                 pid = os.fork()
786                 if pid == 0:  # Actual daemon
787                     pid = os.getpid()
788                     log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
789                     load_cache()
790                     return
791             os._exit(0)
792
793         if cache_ldb_initialize:
794             self.samdb_url = H
795             self.samdb = self.connect_for_passwords(url=self.samdb_url,
796                                                     verbose=True)
797             load_cache()
798             return
799
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):
808                 if fd == logfd:
809                     continue
810                 try:
811                     os.close(fd)
812                 except OSError:
813                     pass
814             os.dup2(logfd, 0)
815             os.dup2(logfd, 1)
816             os.dup2(logfd, 2)
817             os.close(logfd)
818             log_msg("Attached to logfile[%s]\n" % (logfile))
819             self.logfile = logfile
820
821         load_cache()
822         conflict = check_current_pid_conflict(terminate)
823         if terminate:
824             if self.current_pid is None:
825                 log_msg("No process running.\n")
826                 return
827             if not conflict:
828                 log_msg("Process %d is not running anymore.\n" % (
829                         self.current_pid))
830                 update_pid(None)
831                 return
832             log_msg("Sending SIGTERM to process %d.\n" % (
833                     self.current_pid))
834             os.kill(self.current_pid, signal.SIGTERM)
835             return
836         if conflict:
837             raise CommandError("Exiting pid %d, command is already running as pid %d" % (
838                                os.getpid(), self.current_pid))
839
840         if daemon is True:
841             daemonize()
842         update_pid(os.getpid())
843
844         wait = True
845         while wait is True:
846             retry_sleep_min = 1
847             retry_sleep_max = 600
848             if nowait is True:
849                 wait = False
850                 retry_sleep = 0
851             else:
852                 retry_sleep = retry_sleep_min
853
854             while self.samdb is None:
855                 if retry_sleep != 0:
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)
862                 try:
863                     self.samdb = self.connect_for_passwords(url=self.samdb_url)
864                 except Exception as msg:
865                     self.samdb = None
866                     log_msg("Connect to samdb Exception => (%s)\n" % msg)
867                     if wait is not True:
868                         raise
869
870             try:
871                 sync_loop(wait)
872             except ldb.LdbError as e7:
873                 (enum, estr) = e7.args
874                 self.samdb = None
875                 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
876
877         update_pid(None)
878         return