Correct "specifiy" typos.
[samba.git] / python / samba / netcmd / user.py
1 # user management
2 #
3 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
4 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19
20 import samba.getopt as options
21 import ldb
22 import pwd
23 import os
24 import sys
25 import fcntl
26 import signal
27 import errno
28 import time
29 import base64
30 import binascii
31 from subprocess import Popen, PIPE, STDOUT
32 from getpass import getpass
33 from samba.auth import system_session
34 from samba.samdb import SamDB
35 from samba.dcerpc import misc
36 from samba.dcerpc import security
37 from samba.dcerpc import drsblobs
38 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
39 from samba import (
40     credentials,
41     dsdb,
42     gensec,
43     generate_random_password,
44     Ldb,
45     )
46 from samba.net import Net
47
48 from samba.netcmd import (
49     Command,
50     CommandError,
51     SuperCommand,
52     Option,
53     )
54
55
56 try:
57     import io
58     import gpgme
59     gpgme_support = True
60     decrypt_samba_gpg_help = "Decrypt the SambaGPG password as cleartext source"
61 except ImportError as e:
62     gpgme_support = False
63     decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
64             "python-gpgme required"
65
66 disabled_virtual_attributes = {
67     }
68
69 virtual_attributes = {
70     "virtualClearTextUTF8": {
71         "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
72         },
73     "virtualClearTextUTF16": {
74         "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
75         },
76     "virtualSambaGPG": {
77         "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
78         },
79     }
80
81 get_random_bytes_fn = None
82 if get_random_bytes_fn is None:
83     try:
84         import Crypto.Random
85         get_random_bytes_fn = Crypto.Random.get_random_bytes
86     except ImportError as e:
87         pass
88 if get_random_bytes_fn is None:
89     try:
90         import M2Crypto.Rand
91         get_random_bytes_fn = M2Crypto.Rand.rand_bytes
92     except ImportError as e:
93         pass
94
95 def check_random():
96     if get_random_bytes_fn is not None:
97         return None
98     return "Crypto.Random or M2Crypto.Rand required"
99
100 def get_random_bytes(num):
101     random_reason = check_random()
102     if random_reason is not None:
103         raise ImportError(random_reason)
104     return get_random_bytes_fn(num)
105
106 def get_crypt_value(alg, utf8pw):
107     algs = {
108         "5": {"length": 43},
109         "6": {"length": 86},
110     }
111     assert alg in algs
112     salt = get_random_bytes(16)
113     # The salt needs to be in [A-Za-z0-9./]
114     # base64 is close enough and as we had 16
115     # random bytes but only need 16 characters
116     # we can ignore the possible == at the end
117     # of the base64 string
118     # we just need to replace '+' by '.'
119     b64salt = base64.b64encode(salt)
120     crypt_salt = "$%s$%s$" % (alg, b64salt[0:16].replace('+', '.'))
121     crypt_value = crypt.crypt(utf8pw, crypt_salt)
122     if crypt_value is None:
123         raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
124     expected_len = len(crypt_salt) + algs[alg]["length"]
125     if len(crypt_value) != expected_len:
126         raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
127             crypt_salt, len(crypt_value), expected_len))
128     return crypt_value
129
130 try:
131     random_reason = check_random()
132     if random_reason is not None:
133         raise ImportError(random_reason)
134     import hashlib
135     h = hashlib.sha1()
136     h = None
137     virtual_attributes["virtualSSHA"] = {
138         }
139 except ImportError as e:
140     reason = "hashlib.sha1()"
141     if random_reason:
142         reason += " and " + random_reason
143     reason += " required"
144     disabled_virtual_attributes["virtualSSHA"] = {
145         "reason" : reason,
146         }
147
148 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
149     try:
150         random_reason = check_random()
151         if random_reason is not None:
152             raise ImportError(random_reason)
153         import crypt
154         v = get_crypt_value(alg, "")
155         v = None
156         virtual_attributes[attr] = {
157             }
158     except ImportError as e:
159         reason = "crypt"
160         if random_reason:
161             reason += " and " + random_reason
162         reason += " required"
163         disabled_virtual_attributes[attr] = {
164             "reason" : reason,
165             }
166     except NotImplementedError as e:
167         reason = "modern '$%s$' salt in crypt(3) required" % (alg)
168         disabled_virtual_attributes[attr] = {
169             "reason" : reason,
170             }
171
172 virtual_attributes_help  = "The attributes to display (comma separated). "
173 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
174 if len(disabled_virtual_attributes) != 0:
175     virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
176
177 class cmd_user_create(Command):
178     """Create a new user.
179
180 This command creates a new user account in the Active Directory domain.  The username specified on the command is the sAMaccountName.
181
182 User accounts may represent physical entities, such as people or may be used as service accounts for applications.  User accounts are also referred to as security principals and are assigned a security identifier (SID).
183
184 A user account enables a user to logon to a computer and domain with an identity that can be authenticated.  To maximize security, each user should have their own unique user account and password.  A user's access to domain resources is based on permissions assigned to the user account.
185
186 Unix (RFC2307) attributes may be added to the user account. Attributes taken from NSS are obtained on the local machine. Explicitly given values override values obtained from NSS. Configure 'idmap_ldb:use rfc2307 = Yes' to use these attributes for UID/GID mapping.
187
188 The command may be run from the root userid or another authorized userid.  The -H or --URL= option can be used to execute the command against a remote server.
189
190 Example1:
191 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
192
193 Example1 shows how to create a new user in the domain against a remote LDAP server.  The -H parameter is used to specify the remote target server.  The -U option is used to pass the userid and password authorized to issue the command remotely.
194
195 Example2:
196 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
197
198 Example2 shows how to create a new user in the domain against the local server.   sudo is used so a user may run the command as root.  In this example, after User2 is created, he/she will be forced to change their password when they logon.
199
200 Example3:
201 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
202
203 Example3 shows how to create a new user in the OrgUnit organizational unit.
204
205 Example4:
206 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
207
208 Example4 shows how to create a new user with Unix UID, GID and login-shell set from the local NSS and GECOS set to 'some text'.
209
210 Example5:
211 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
212            --uid-number=10005 --login-shell=/bin/false --gid-number=10000
213
214 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
215 --nis-domain is set, then the other four parameters are mandatory.
216
217 """
218     synopsis = "%prog <username> [<password>] [options]"
219
220     takes_options = [
221         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
222                 metavar="URL", dest="H"),
223         Option("--must-change-at-next-login",
224                 help="Force password to be changed on next login",
225                 action="store_true"),
226         Option("--random-password",
227                 help="Generate random password",
228                 action="store_true"),
229         Option("--smartcard-required",
230                 help="Require a smartcard for interactive logons",
231                 action="store_true"),
232         Option("--use-username-as-cn",
233                 help="Force use of username as user's CN",
234                 action="store_true"),
235         Option("--userou",
236                 help="DN of alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created. E. g. 'OU=<OU name>'",
237                 type=str),
238         Option("--surname", help="User's surname", type=str),
239         Option("--given-name", help="User's given name", type=str),
240         Option("--initials", help="User's initials", type=str),
241         Option("--profile-path", help="User's profile path", type=str),
242         Option("--script-path", help="User's logon script path", type=str),
243         Option("--home-drive", help="User's home drive letter", type=str),
244         Option("--home-directory", help="User's home directory path", type=str),
245         Option("--job-title", help="User's job title", type=str),
246         Option("--department", help="User's department", type=str),
247         Option("--company", help="User's company", type=str),
248         Option("--description", help="User's description", type=str),
249         Option("--mail-address", help="User's email address", type=str),
250         Option("--internet-address", help="User's home page", type=str),
251         Option("--telephone-number", help="User's phone number", type=str),
252         Option("--physical-delivery-office", help="User's office location", type=str),
253         Option("--rfc2307-from-nss",
254                 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
255                 action="store_true"),
256         Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
257         Option("--unix-home", help="User's Unix/RFC2307 home directory",
258                 type=str),
259         Option("--uid", help="User's Unix/RFC2307 username", type=str),
260         Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
261         Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
262         Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
263         Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
264     ]
265
266     takes_args = ["username", "password?"]
267
268     takes_optiongroups = {
269         "sambaopts": options.SambaOptions,
270         "credopts": options.CredentialsOptions,
271         "versionopts": options.VersionOptions,
272         }
273
274     def run(self, username, password=None, credopts=None, sambaopts=None,
275             versionopts=None, H=None, must_change_at_next_login=False,
276             random_password=False, use_username_as_cn=False, userou=None,
277             surname=None, given_name=None, initials=None, profile_path=None,
278             script_path=None, home_drive=None, home_directory=None,
279             job_title=None, department=None, company=None, description=None,
280             mail_address=None, internet_address=None, telephone_number=None,
281             physical_delivery_office=None, rfc2307_from_nss=False,
282             nis_domain=None, unix_home=None, uid=None, uid_number=None,
283             gid_number=None, gecos=None, login_shell=None,
284             smartcard_required=False):
285
286         if smartcard_required:
287             if password is not None and password is not '':
288                 raise CommandError('It is not allowed to specify '
289                                    '--newpassword '
290                                    'together with --smartcard-required.')
291             if must_change_at_next_login:
292                 raise CommandError('It is not allowed to specify '
293                                    '--must-change-at-next-login '
294                                    'together with --smartcard-required.')
295
296         if random_password and not smartcard_required:
297             password = generate_random_password(128, 255)
298
299         while True:
300             if smartcard_required:
301                 break
302             if password is not None and password is not '':
303                 break
304             password = getpass("New Password: ")
305             passwordverify = getpass("Retype Password: ")
306             if not password == passwordverify:
307                 password = None
308                 self.outf.write("Sorry, passwords do not match.\n")
309
310         if rfc2307_from_nss:
311                 pwent = pwd.getpwnam(username)
312                 if uid is None:
313                     uid = username
314                 if uid_number is None:
315                     uid_number = pwent[2]
316                 if gid_number is None:
317                     gid_number = pwent[3]
318                 if gecos is None:
319                     gecos = pwent[4]
320                 if login_shell is None:
321                     login_shell = pwent[6]
322
323         lp = sambaopts.get_loadparm()
324         creds = credopts.get_credentials(lp)
325
326         if uid_number or gid_number:
327             if not lp.get("idmap_ldb:use rfc2307"):
328                 self.outf.write("You are setting a Unix/RFC2307 UID or GID. You may want to set 'idmap_ldb:use rfc2307 = Yes' to use those attributes for XID/SID-mapping.\n")
329
330         if nis_domain is not None:
331             if None in (uid_number, login_shell, unix_home, gid_number):
332                 raise CommandError('Missing parameters. To enable NIS features, '
333                                    'the following options have to be given: '
334                                    '--nis-domain=, --uidNumber=, --login-shell='
335                                    ', --unix-home=, --gid-number= Operation '
336                                    'cancelled.')
337
338         try:
339             samdb = SamDB(url=H, session_info=system_session(),
340                           credentials=creds, lp=lp)
341             samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
342                           useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
343                           profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
344                           jobtitle=job_title, department=department, company=company, description=description,
345                           mailaddress=mail_address, internetaddress=internet_address,
346                           telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
347                           nisdomain=nis_domain, unixhome=unix_home, uid=uid,
348                           uidnumber=uid_number, gidnumber=gid_number,
349                           gecos=gecos, loginshell=login_shell,
350                           smartcard_required=smartcard_required)
351         except Exception, e:
352             raise CommandError("Failed to add user '%s': " % username, e)
353
354         self.outf.write("User '%s' created successfully\n" % username)
355
356
357 class cmd_user_add(cmd_user_create):
358     __doc__ = cmd_user_create.__doc__
359     # take this print out after the add subcommand is removed.
360     # the add subcommand is deprecated but left in for now to allow people to
361     # migrate to create
362
363     def run(self, *args, **kwargs):
364         self.outf.write(
365             "Note: samba-tool user add is deprecated.  "
366             "Please use samba-tool user create for the same function.\n")
367         return super(cmd_user_add, self).run(*args, **kwargs)
368
369
370 class cmd_user_delete(Command):
371     """Delete a user.
372
373 This command deletes a user account from the Active Directory domain.  The username specified on the command is the sAMAccountName.
374
375 Once the account is deleted, all permissions and memberships associated with that account are deleted.  If a new user account is added with the same name as a previously deleted account name, the new user does not have the previous permissions.  The new account user will be assigned a new security identifier (SID) and permissions and memberships will have to be added.
376
377 The command may be run from the root userid or another authorized userid.  The -H or --URL= option can be used to execute the command against a remote server.
378
379 Example1:
380 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
381
382 Example1 shows how to delete a user in the domain against a remote LDAP server.  The -H parameter is used to specify the remote target server.  The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to issue the command on that server.
383
384 Example2:
385 sudo samba-tool user delete User2
386
387 Example2 shows how to delete a user in the domain against the local server.   sudo is used so a user may run the command as root.
388
389 """
390     synopsis = "%prog <username> [options]"
391
392     takes_options = [
393         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
394                metavar="URL", dest="H"),
395     ]
396
397     takes_args = ["username"]
398     takes_optiongroups = {
399         "sambaopts": options.SambaOptions,
400         "credopts": options.CredentialsOptions,
401         "versionopts": options.VersionOptions,
402         }
403
404     def run(self, username, credopts=None, sambaopts=None, versionopts=None,
405             H=None):
406         lp = sambaopts.get_loadparm()
407         creds = credopts.get_credentials(lp, fallback_machine=True)
408
409         samdb = SamDB(url=H, session_info=system_session(),
410                       credentials=creds, lp=lp)
411
412         filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
413                    username)
414
415         try:
416             res = samdb.search(base=samdb.domain_dn(),
417                                scope=ldb.SCOPE_SUBTREE,
418                                expression=filter,
419                                attrs=["dn"])
420             user_dn = res[0].dn
421         except IndexError:
422             raise CommandError('Unable to find user "%s"' % (username))
423
424         try:
425             samdb.delete(user_dn)
426         except Exception, e:
427             raise CommandError('Failed to remove user "%s"' % username, e)
428         self.outf.write("Deleted user %s\n" % username)
429
430
431 class cmd_user_list(Command):
432     """List all users."""
433
434     synopsis = "%prog [options]"
435
436     takes_options = [
437         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
438                metavar="URL", dest="H"),
439         ]
440
441     takes_optiongroups = {
442         "sambaopts": options.SambaOptions,
443         "credopts": options.CredentialsOptions,
444         "versionopts": options.VersionOptions,
445         }
446
447     def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
448         lp = sambaopts.get_loadparm()
449         creds = credopts.get_credentials(lp, fallback_machine=True)
450
451         samdb = SamDB(url=H, session_info=system_session(),
452             credentials=creds, lp=lp)
453
454         domain_dn = samdb.domain_dn()
455         res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
456                     expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
457                     % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
458                     attrs=["samaccountname"])
459         if (len(res) == 0):
460             return
461
462         for msg in res:
463             self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
464
465
466 class cmd_user_enable(Command):
467     """Enable an user.
468
469 This command enables a user account for logon to an Active Directory domain.  The username specified on the command is the sAMAccountName.  The username may also be specified using the --filter option.
470
471 There are many reasons why an account may become disabled.  These include:
472 - If a user exceeds the account policy for logon attempts
473 - If an administrator disables the account
474 - If the account expires
475
476 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
477
478 Additionally, the enable function allows an administrator to have a set of created user accounts defined and setup with default permissions that can be easily enabled for use.
479
480 The command may be run from the root userid or another authorized userid.  The -H or --URL= option can be used to execute the command against a remote server.
481
482 Example1:
483 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
484
485 Example1 shows how to enable a user in the domain against a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server.
486
487 Example2:
488 su samba-tool user enable Testuser2
489
490 Example2 shows how to enable user Testuser2 for use in the domain on the local server. sudo is used so a user may run the command as root.
491
492 Example3:
493 samba-tool user enable --filter=samaccountname=Testuser3
494
495 Example3 shows how to enable a user in the domain against a local LDAP server.  It uses the --filter=samaccountname to specify the username.
496
497 """
498     synopsis = "%prog (<username>|--filter <filter>) [options]"
499
500
501     takes_optiongroups = {
502         "sambaopts": options.SambaOptions,
503         "versionopts": options.VersionOptions,
504         "credopts": options.CredentialsOptions,
505     }
506
507     takes_options = [
508         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
509                metavar="URL", dest="H"),
510         Option("--filter", help="LDAP Filter to set password on", type=str),
511         ]
512
513     takes_args = ["username?"]
514
515     def run(self, username=None, sambaopts=None, credopts=None,
516             versionopts=None, filter=None, H=None):
517         if username is None and filter is None:
518             raise CommandError("Either the username or '--filter' must be specified!")
519
520         if filter is None:
521             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
522
523         lp = sambaopts.get_loadparm()
524         creds = credopts.get_credentials(lp, fallback_machine=True)
525
526         samdb = SamDB(url=H, session_info=system_session(),
527             credentials=creds, lp=lp)
528         try:
529             samdb.enable_account(filter)
530         except Exception, msg:
531             raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
532         self.outf.write("Enabled user '%s'\n" % (username or filter))
533
534
535 class cmd_user_disable(Command):
536     """Disable an user."""
537
538     synopsis = "%prog (<username>|--filter <filter>) [options]"
539
540     takes_options = [
541         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
542                metavar="URL", dest="H"),
543         Option("--filter", help="LDAP Filter to set password on", type=str),
544         ]
545
546     takes_args = ["username?"]
547
548     takes_optiongroups = {
549         "sambaopts": options.SambaOptions,
550         "credopts": options.CredentialsOptions,
551         "versionopts": options.VersionOptions,
552         }
553
554     def run(self, username=None, sambaopts=None, credopts=None,
555             versionopts=None, filter=None, H=None):
556         if username is None and filter is None:
557             raise CommandError("Either the username or '--filter' must be specified!")
558
559         if filter is None:
560             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
561
562         lp = sambaopts.get_loadparm()
563         creds = credopts.get_credentials(lp, fallback_machine=True)
564
565         samdb = SamDB(url=H, session_info=system_session(),
566             credentials=creds, lp=lp)
567         try:
568             samdb.disable_account(filter)
569         except Exception, msg:
570             raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
571
572
573 class cmd_user_setexpiry(Command):
574     """Set the expiration of a user account.
575
576 The user can either be specified by their sAMAccountName or using the --filter option.
577
578 When a user account expires, it becomes disabled and the user is unable to logon.  The administrator may issue the samba-tool user enable command to enable the account for logon.  The permissions and memberships associated with the account are retained when the account is enabled.
579
580 The command may be run from the root userid or another authorized userid.  The -H or --URL= option can be used to execute the command on a remote server.
581
582 Example1:
583 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
584
585 Example1 shows how to set the expiration of an account in a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server.
586
587 Example2:
588 su samba-tool user setexpiry User2
589
590 Example2 shows how to set the account expiration of user User2 so it will never expire.  The user in this example resides on the  local server.   sudo is used so a user may run the command as root.
591
592 Example3:
593 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
594
595 Example3 shows how to set the account expiration date to end of day 20 days from the current day.  The username or sAMAccountName is specified using the --filter= parameter and the username in this example is User3.
596
597 Example4:
598 samba-tool user setexpiry --noexpiry User4
599 Example4 shows how to set the account expiration so that it will never expire.  The username and sAMAccountName in this example is User4.
600
601 """
602     synopsis = "%prog (<username>|--filter <filter>) [options]"
603
604     takes_optiongroups = {
605         "sambaopts": options.SambaOptions,
606         "versionopts": options.VersionOptions,
607         "credopts": options.CredentialsOptions,
608     }
609
610     takes_options = [
611         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
612                metavar="URL", dest="H"),
613         Option("--filter", help="LDAP Filter to set password on", type=str),
614         Option("--days", help="Days to expiry", type=int, default=0),
615         Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
616     ]
617
618     takes_args = ["username?"]
619
620     def run(self, username=None, sambaopts=None, credopts=None,
621             versionopts=None, H=None, filter=None, days=None, noexpiry=None):
622         if username is None and filter is None:
623             raise CommandError("Either the username or '--filter' must be specified!")
624
625         if filter is None:
626             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
627
628         lp = sambaopts.get_loadparm()
629         creds = credopts.get_credentials(lp)
630
631         samdb = SamDB(url=H, session_info=system_session(),
632             credentials=creds, lp=lp)
633
634         try:
635             samdb.setexpiry(filter, days*24*3600, no_expiry_req=noexpiry)
636         except Exception, msg:
637             # FIXME: Catch more specific exception
638             raise CommandError("Failed to set expiry for user '%s': %s" % (
639                 username or filter, msg))
640         if noexpiry:
641             self.outf.write("Expiry for user '%s' disabled.\n" % (
642                 username or filter))
643         else:
644             self.outf.write("Expiry for user '%s' set to %u days.\n" % (
645                 username or filter, days))
646
647
648 class cmd_user_password(Command):
649     """Change password for a user account (the one provided in authentication).
650 """
651
652     synopsis = "%prog [options]"
653
654     takes_options = [
655         Option("--newpassword", help="New password", type=str),
656         ]
657
658     takes_optiongroups = {
659         "sambaopts": options.SambaOptions,
660         "credopts": options.CredentialsOptions,
661         "versionopts": options.VersionOptions,
662         }
663
664     def run(self, credopts=None, sambaopts=None, versionopts=None,
665                 newpassword=None):
666
667         lp = sambaopts.get_loadparm()
668         creds = credopts.get_credentials(lp)
669
670         # get old password now, to get the password prompts in the right order
671         old_password = creds.get_password()
672
673         net = Net(creds, lp, server=credopts.ipaddress)
674
675         password = newpassword
676         while True:
677             if password is not None and password is not '':
678                 break
679             password = getpass("New Password: ")
680             passwordverify = getpass("Retype Password: ")
681             if not password == passwordverify:
682                 password = None
683                 self.outf.write("Sorry, passwords do not match.\n")
684
685         try:
686             net.change_password(password.encode('utf-8'))
687         except Exception, msg:
688             # FIXME: catch more specific exception
689             raise CommandError("Failed to change password : %s" % msg)
690         self.outf.write("Changed password OK\n")
691
692
693 class cmd_user_setpassword(Command):
694     """Set or reset the password of a user account.
695
696 This command sets or resets the logon password for a user account.  The username specified on the command is the sAMAccountName.  The username may also be specified using the --filter option.
697
698 If the password is not specified on the command through the --newpassword parameter, the user is prompted for the password to be entered through the command line.
699
700 It is good security practice for the administrator to use the --must-change-at-next-login option which requires that when the user logs on to the account for the first time following the password change, he/she must change the password.
701
702 The command may be run from the root userid or another authorized userid.  The -H or --URL= option can be used to execute the command against a remote server.
703
704 Example1:
705 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
706
707 Example1 shows how to set the password of user TestUser1 on a remote LDAP server.  The --URL parameter is used to specify the remote target server.  The -U option is used to pass the username and password of a user that exists on the remote server and is authorized to update the server.
708
709 Example2:
710 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
711
712 Example2 shows how an administrator would reset the TestUser2 user's password to passw0rd.  The user is running under the root userid using the sudo command.  In this example the user TestUser2 must change their password the next time they logon to the account.
713
714 Example3:
715 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
716
717 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
718
719 """
720     synopsis = "%prog (<username>|--filter <filter>) [options]"
721
722     takes_optiongroups = {
723         "sambaopts": options.SambaOptions,
724         "versionopts": options.VersionOptions,
725         "credopts": options.CredentialsOptions,
726     }
727
728     takes_options = [
729         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
730                metavar="URL", dest="H"),
731         Option("--filter", help="LDAP Filter to set password on", type=str),
732         Option("--newpassword", help="Set password", type=str),
733         Option("--must-change-at-next-login",
734                help="Force password to be changed on next login",
735                action="store_true"),
736         Option("--random-password",
737                 help="Generate random password",
738                 action="store_true"),
739         Option("--smartcard-required",
740                 help="Require a smartcard for interactive logons",
741                 action="store_true"),
742         Option("--clear-smartcard-required",
743                 help="Don't require a smartcard for interactive logons",
744                 action="store_true"),
745         ]
746
747     takes_args = ["username?"]
748
749     def run(self, username=None, filter=None, credopts=None, sambaopts=None,
750             versionopts=None, H=None, newpassword=None,
751             must_change_at_next_login=False, random_password=False,
752             smartcard_required=False, clear_smartcard_required=False):
753         if filter is None and username is None:
754             raise CommandError("Either the username or '--filter' must be specified!")
755
756         password = newpassword
757
758         if smartcard_required:
759             if password is not None and password is not '':
760                 raise CommandError('It is not allowed to specify '
761                                    '--newpassword '
762                                    'together with --smartcard-required.')
763             if must_change_at_next_login:
764                 raise CommandError('It is not allowed to specify '
765                                    '--must-change-at-next-login '
766                                    'together with --smartcard-required.')
767             if clear_smartcard_required:
768                 raise CommandError('It is not allowed to specify '
769                                    '--clear-smartcard-required '
770                                    'together with --smartcard-required.')
771
772         if random_password and not smartcard_required:
773             password = generate_random_password(128, 255)
774
775         while True:
776             if smartcard_required:
777                 break
778             if password is not None and password is not '':
779                 break
780             password = getpass("New Password: ")
781             passwordverify = getpass("Retype Password: ")
782             if not password == passwordverify:
783                 password = None
784                 self.outf.write("Sorry, passwords do not match.\n")
785
786         if filter is None:
787             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
788
789         lp = sambaopts.get_loadparm()
790         creds = credopts.get_credentials(lp)
791
792         creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
793
794         samdb = SamDB(url=H, session_info=system_session(),
795                       credentials=creds, lp=lp)
796
797         if smartcard_required:
798             command = ""
799             try:
800                 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
801                 flags = dsdb.UF_SMARTCARD_REQUIRED
802                 samdb.toggle_userAccountFlags(filter, flags, on=True)
803                 command = "Failed to enable account for user '%s'" % (username or filter)
804                 samdb.enable_account(filter)
805             except Exception, msg:
806                 # FIXME: catch more specific exception
807                 raise CommandError("%s: %s" % (command, msg))
808             self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
809         else:
810             command = ""
811             try:
812                 if clear_smartcard_required:
813                     command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
814                     flags = dsdb.UF_SMARTCARD_REQUIRED
815                     samdb.toggle_userAccountFlags(filter, flags, on=False)
816                 command = "Failed to set password for user '%s'" % (username or filter)
817                 samdb.setpassword(filter, password,
818                                   force_change_at_next_login=must_change_at_next_login,
819                                   username=username)
820             except Exception, msg:
821                 # FIXME: catch more specific exception
822                 raise CommandError("%s: %s" % (command, msg))
823             self.outf.write("Changed password OK\n")
824
825 class GetPasswordCommand(Command):
826
827     def __init__(self):
828         super(GetPasswordCommand, self).__init__()
829         self.lp = None
830
831     def connect_system_samdb(self, url, allow_local=False, verbose=False):
832
833         # using anonymous here, results in no authentication
834         # which means we can get system privileges via
835         # the privileged ldapi socket
836         creds = credentials.Credentials()
837         creds.set_anonymous()
838
839         if url is None and allow_local:
840             pass
841         elif url.lower().startswith("ldapi://"):
842             pass
843         elif url.lower().startswith("ldap://"):
844             raise CommandError("--url ldap:// is not supported for this command")
845         elif url.lower().startswith("ldaps://"):
846             raise CommandError("--url ldaps:// is not supported for this command")
847         elif not allow_local:
848             raise CommandError("--url requires an ldapi:// url for this command")
849
850         if verbose:
851             self.outf.write("Connecting to '%s'\n" % url)
852
853         samdb = SamDB(url=url, session_info=system_session(),
854                       credentials=creds, lp=self.lp)
855
856         try:
857             #
858             # Make sure we're connected as SYSTEM
859             #
860             res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
861             assert len(res) == 1
862             sids = res[0].get("tokenGroups")
863             assert len(sids) == 1
864             sid = ndr_unpack(security.dom_sid, sids[0])
865             assert str(sid) == security.SID_NT_SYSTEM
866         except Exception as msg:
867             raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
868                                (security.SID_NT_SYSTEM))
869
870         # We use sort here in order to have a predictable processing order
871         # this might not be strictly needed, but also doesn't hurt here
872         for a in sorted(virtual_attributes.keys()):
873             flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
874             samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
875
876         return samdb
877
878     def get_account_attributes(self, samdb, username, basedn, filter, scope,
879                                attrs, decrypt):
880
881         require_supplementalCredentials = False
882         search_attrs = attrs[:]
883         lower_attrs = [x.lower() for x in search_attrs]
884         for a in virtual_attributes.keys():
885             if a.lower() in lower_attrs:
886                 require_supplementalCredentials = True
887         add_supplementalCredentials = False
888         add_unicodePwd = False
889         if require_supplementalCredentials:
890             a = "supplementalCredentials"
891             if a.lower() not in lower_attrs:
892                 search_attrs += [a]
893                 add_supplementalCredentials = True
894             a = "unicodePwd"
895             if a.lower() not in lower_attrs:
896                 search_attrs += [a]
897                 add_unicodePwd = True
898         add_sAMAcountName = False
899         a = "sAMAccountName"
900         if a.lower() not in lower_attrs:
901             search_attrs += [a]
902             add_sAMAcountName = True
903
904         if scope == ldb.SCOPE_BASE:
905             search_controls = ["show_deleted:1", "show_recycled:1"]
906         else:
907             search_controls = []
908         try:
909             res = samdb.search(base=basedn, expression=filter,
910                                scope=scope, attrs=search_attrs,
911                                controls=search_controls)
912             if len(res) == 0:
913                 raise Exception('Unable to find user "%s"' % (username or filter))
914             if len(res) > 1:
915                 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
916         except Exception as msg:
917             # FIXME: catch more specific exception
918             raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
919         obj = res[0]
920
921         sc = None
922         unicodePwd = None
923         if "supplementalCredentials" in obj:
924             sc_blob = obj["supplementalCredentials"][0]
925             sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
926             if add_supplementalCredentials:
927                 del obj["supplementalCredentials"]
928         if "unicodePwd" in obj:
929             unicodePwd = obj["unicodePwd"][0]
930             if add_unicodePwd:
931                 del obj["unicodePwd"]
932         account_name = obj["sAMAccountName"][0]
933         if add_sAMAcountName:
934             del obj["sAMAccountName"]
935
936         calculated = {}
937         def get_package(name, min_idx=0):
938             if name in calculated:
939                 return calculated[name]
940             if sc is None:
941                 return None
942             if min_idx < 0:
943                 min_idx = len(sc.sub.packages) + min_idx
944             idx = 0
945             for p in sc.sub.packages:
946                 idx += 1
947                 if idx <= min_idx:
948                     continue
949                 if name != p.name:
950                     continue
951
952                 return binascii.a2b_hex(p.data)
953             return None
954
955         if decrypt:
956             #
957             # Samba adds 'Primary:SambaGPG' at the end.
958             # When Windows sets the password it keeps
959             # 'Primary:SambaGPG' and rotates it to
960             # the begining. So we can only use the value,
961             # if it is the last one.
962             #
963             # In order to get more protection we verify
964             # the nthash of the decrypted utf16 password
965             # against the stored nthash in unicodePwd.
966             #
967             sgv = get_package("Primary:SambaGPG", min_idx=-1)
968             if sgv is not None and unicodePwd is not None:
969                 ctx = gpgme.Context()
970                 ctx.armor = True
971                 cipher_io = io.BytesIO(sgv)
972                 plain_io = io.BytesIO()
973                 try:
974                     ctx.decrypt(cipher_io, plain_io)
975                     cv = plain_io.getvalue()
976                     #
977                     # We only use the password if it matches
978                     # the current nthash stored in the unicodePwd
979                     # attribute
980                     #
981                     tmp = credentials.Credentials()
982                     tmp.set_anonymous()
983                     tmp.set_utf16_password(cv)
984                     nthash = tmp.get_nt_hash()
985                     if nthash == unicodePwd:
986                         calculated["Primary:CLEARTEXT"] = cv
987                 except gpgme.GpgmeError as (major, minor, msg):
988                     if major == gpgme.ERR_BAD_SECKEY:
989                         msg = "ERR_BAD_SECKEY: " + msg
990                     else:
991                         msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
992                     self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
993                                     username or account_name, msg))
994
995         def get_utf8(a, b, username):
996             try:
997                 u = unicode(b, 'utf-16-le')
998             except UnicodeDecodeError as e:
999                 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1000                                 username, a))
1001                 return None
1002             u8 = u.encode('utf-8')
1003             return u8
1004
1005         # We use sort here in order to have a predictable processing order
1006         for a in sorted(virtual_attributes.keys()):
1007             if not a.lower() in lower_attrs:
1008                 continue
1009
1010             if a == "virtualClearTextUTF8":
1011                 b = get_package("Primary:CLEARTEXT")
1012                 if b is None:
1013                     continue
1014                 u8 = get_utf8(a, b, username or account_name)
1015                 if u8 is None:
1016                     continue
1017                 v = u8
1018             elif a == "virtualClearTextUTF16":
1019                 v = get_package("Primary:CLEARTEXT")
1020                 if v is None:
1021                     continue
1022             elif a == "virtualSSHA":
1023                 b = get_package("Primary:CLEARTEXT")
1024                 if b is None:
1025                     continue
1026                 u8 = get_utf8(a, b, username or account_name)
1027                 if u8 is None:
1028                     continue
1029                 salt = get_random_bytes(4)
1030                 h = hashlib.sha1()
1031                 h.update(u8)
1032                 h.update(salt)
1033                 bv = h.digest() + salt
1034                 v = "{SSHA}" + base64.b64encode(bv)
1035             elif a == "virtualCryptSHA256":
1036                 b = get_package("Primary:CLEARTEXT")
1037                 if b is None:
1038                     continue
1039                 u8 = get_utf8(a, b, username or account_name)
1040                 if u8 is None:
1041                     continue
1042                 sv = get_crypt_value("5", u8)
1043                 v = "{CRYPT}" + sv
1044             elif a == "virtualCryptSHA512":
1045                 b = get_package("Primary:CLEARTEXT")
1046                 if b is None:
1047                     continue
1048                 u8 = get_utf8(a, b, username or account_name)
1049                 if u8 is None:
1050                     continue
1051                 sv = get_crypt_value("6", u8)
1052                 v = "{CRYPT}" + sv
1053             elif a == "virtualSambaGPG":
1054                 # Samba adds 'Primary:SambaGPG' at the end.
1055                 # When Windows sets the password it keeps
1056                 # 'Primary:SambaGPG' and rotates it to
1057                 # the begining. So we can only use the value,
1058                 # if it is the last one.
1059                 v = get_package("Primary:SambaGPG", min_idx=-1)
1060                 if v is None:
1061                     continue
1062             else:
1063                 continue
1064             obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1065         return obj
1066
1067     def parse_attributes(self, attributes):
1068
1069         if attributes is None:
1070             raise CommandError("Please specify --attributes")
1071         attrs = attributes.split(',')
1072         password_attrs = []
1073         for pa in attrs:
1074             pa = pa.lstrip().rstrip()
1075             for da in disabled_virtual_attributes.keys():
1076                 if pa.lower() == da.lower():
1077                     r = disabled_virtual_attributes[da]["reason"]
1078                     raise CommandError("Virtual attribute '%s' not supported: %s" % (
1079                                        da, r))
1080             for va in virtual_attributes.keys():
1081                 if pa.lower() == va.lower():
1082                     # Take the real name
1083                     pa = va
1084                     break
1085             password_attrs += [pa]
1086
1087         return password_attrs
1088
1089 class cmd_user_getpassword(GetPasswordCommand):
1090     """Get the password fields of a user/computer account.
1091
1092 This command gets the logon password for a user/computer account.
1093
1094 The username specified on the command is the sAMAccountName.
1095 The username may also be specified using the --filter option.
1096
1097 The command must be run from the root user id or another authorized user id.
1098 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1099 used to adjust the local path. By default tdb:// is used by default.
1100
1101 The '--attributes' parameter takes a comma separated list of attributes,
1102 which will be printed or given to the script specified by '--script'. If a
1103 specified attribute is not available on an object it's silently omitted.
1104 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1105 the NTHASH) and the following virtual attributes are possible (see --help
1106 for which virtual attributes are supported in your environment):
1107
1108    virtualClearTextUTF16: The raw cleartext as stored in the
1109                           'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1110                           with '--decrypt-samba-gpg') buffer inside of the
1111                           supplementalCredentials attribute. This typically
1112                           contains valid UTF-16-LE, but may contain random
1113                           bytes, e.g. for computer accounts.
1114
1115    virtualClearTextUTF8:  As virtualClearTextUTF16, but converted to UTF-8
1116                           (only from valid UTF-16-LE)
1117
1118    virtualSSHA:           As virtualClearTextUTF8, but a salted SHA-1
1119                           checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1120
1121    virtualCryptSHA256:    As virtualClearTextUTF8, but a salted SHA256
1122                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1123                           with a $5$... salt, see crypt(3) on modern systems.
1124
1125    virtualCryptSHA512:    As virtualClearTextUTF8, but a salted SHA512
1126                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1127                           with a $6$... salt, see crypt(3) on modern systems.
1128
1129    virtualSambaGPG:       The raw cleartext as stored in the
1130                           'Primary:SambaGPG' buffer inside of the
1131                           supplementalCredentials attribute.
1132                           See the 'password hash gpg key ids' option in
1133                           smb.conf.
1134
1135 The '--decrypt-samba-gpg' option triggers decryption of the
1136 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1137 in your environment or not (the python-gpgme package is required).  Please
1138 note that you might need to set the GNUPGHOME environment variable.  If the
1139 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1140 environment variable has been set correctly and the passphrase is already
1141 known by the gpg-agent.
1142
1143 Example1:
1144 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1145
1146 Example2:
1147 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1148
1149 """
1150     def __init__(self):
1151         super(cmd_user_getpassword, self).__init__()
1152
1153     synopsis = "%prog (<username>|--filter <filter>) [options]"
1154
1155     takes_optiongroups = {
1156         "sambaopts": options.SambaOptions,
1157         "versionopts": options.VersionOptions,
1158     }
1159
1160     takes_options = [
1161         Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1162                metavar="URL", dest="H"),
1163         Option("--filter", help="LDAP Filter to set password on", type=str),
1164         Option("--attributes", type=str,
1165                help=virtual_attributes_help,
1166                metavar="ATTRIBUTELIST", dest="attributes"),
1167         Option("--decrypt-samba-gpg",
1168                help=decrypt_samba_gpg_help,
1169                action="store_true", default=False, dest="decrypt_samba_gpg"),
1170         ]
1171
1172     takes_args = ["username?"]
1173
1174     def run(self, username=None, H=None, filter=None,
1175             attributes=None, decrypt_samba_gpg=None,
1176             sambaopts=None, versionopts=None):
1177         self.lp = sambaopts.get_loadparm()
1178
1179         if decrypt_samba_gpg and not gpgme_support:
1180             raise CommandError(decrypt_samba_gpg_help)
1181
1182         if filter is None and username is None:
1183             raise CommandError("Either the username or '--filter' must be specified!")
1184
1185         if filter is None:
1186             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1187
1188         if attributes is None:
1189             raise CommandError("Please specify --attributes")
1190
1191         password_attrs = self.parse_attributes(attributes)
1192
1193         samdb = self.connect_system_samdb(url=H, allow_local=True)
1194
1195         obj = self.get_account_attributes(samdb, username,
1196                                           basedn=None,
1197                                           filter=filter,
1198                                           scope=ldb.SCOPE_SUBTREE,
1199                                           attrs=password_attrs,
1200                                           decrypt=decrypt_samba_gpg)
1201
1202         ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1203         self.outf.write("%s" % ldif)
1204         self.outf.write("Got password OK\n")
1205
1206 class cmd_user_syncpasswords(GetPasswordCommand):
1207     """Sync the password of user accounts.
1208
1209 This syncs logon passwords for user accounts.
1210
1211 Note that this command should run on a single domain controller only
1212 (typically the PDC-emulator). However the "password hash gpg key ids"
1213 option should to be configured on all domain controllers.
1214
1215 The command must be run from the root user id or another authorized user id.
1216 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1217 local path.  By default, ldapi:// is used with the default path to the
1218 privileged ldapi socket.
1219
1220 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1221 "Sync Loop Terminate".
1222
1223
1224 Cache Initialization
1225 ====================
1226
1227 The first time, this command needs to be called with
1228 '--cache-ldb-initialize' in order to initialize its cache.
1229
1230 The cache initialization requires '--attributes' and allows the following
1231 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1232 '-H/--URL'.
1233
1234 The '--attributes' parameter takes a comma separated list of attributes,
1235 which will be printed or given to the script specified by '--script'. If a
1236 specified attribute is not available on an object it will be silently omitted.
1237 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1238 the NTHASH) and the following virtual attributes are possible (see '--help'
1239 for supported virtual attributes in your environment):
1240
1241    virtualClearTextUTF16: The raw cleartext as stored in the
1242                           'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1243                           with '--decrypt-samba-gpg') buffer inside of the
1244                           supplementalCredentials attribute. This typically
1245                           contains valid UTF-16-LE, but may contain random
1246                           bytes, e.g. for computer accounts.
1247
1248    virtualClearTextUTF8:  As virtualClearTextUTF16, but converted to UTF-8
1249                           (only from valid UTF-16-LE)
1250
1251    virtualSSHA:           As virtualClearTextUTF8, but a salted SHA-1
1252                           checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1253
1254    virtualCryptSHA256:    As virtualClearTextUTF8, but a salted SHA256
1255                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1256                           with a $5$... salt, see crypt(3) on modern systems.
1257
1258    virtualCryptSHA512:    As virtualClearTextUTF8, but a salted SHA512
1259                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1260                           with a $6$... salt, see crypt(3) on modern systems.
1261
1262    virtualSambaGPG:       The raw cleartext as stored in the
1263                           'Primary:SambaGPG' buffer inside of the
1264                           supplementalCredentials attribute.
1265                           See the 'password hash gpg key ids' option in
1266                           smb.conf.
1267
1268 The '--decrypt-samba-gpg' option triggers decryption of the
1269 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1270 in your environment or not (the python-gpgme package is required).  Please
1271 note that you might need to set the GNUPGHOME environment variable.  If the
1272 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1273 environment variable has been set correctly and the passphrase is already
1274 known by the gpg-agent.
1275
1276 The '--script' option specifies a custom script that is called whenever any
1277 of the dirsyncAttributes (see below) was changed. The script is called
1278 without any arguments. It gets the LDIF for exactly one object on STDIN.
1279 If the script processed the object successfully it has to respond with a
1280 single line starting with 'DONE-EXIT: ' followed by an optional message.
1281
1282 Note that the script might be called without any password change, e.g. if
1283 the account was disabled (an userAccountControl change) or the
1284 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1285 are always returned as unique identifier of the account. It might be useful
1286 to also ask for non-password attributes like: objectSid, sAMAccountName,
1287 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1288 Depending on the object, some attributes may not be present/available,
1289 but you always get the current state (and not a diff).
1290
1291 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1292 into the logfile.
1293
1294 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1295 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1296     (!(sAMAccountName=krbtgt*)))
1297 This means only normal (non-krbtgt) user
1298 accounts are monitored.  The '--filter' can modify that, e.g. if it's
1299 required to also sync computer accounts.
1300
1301
1302 Sync Loop Run
1303 =============
1304
1305 This (default) mode runs in an endless loop waiting for password related
1306 changes in the active directory database. It makes use of the
1307 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1308 get changes in a reliable fashion. Objects are monitored for changes of the
1309 following dirsyncAttributes:
1310
1311   unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1312   userPrincipalName and userAccountControl.
1313
1314 It recovers from LDAP disconnects and updates the cache in conservative way
1315 (in single steps after each succesfully processed change).  An error from
1316 the script (specified by '--script') will result in fatal error and this
1317 command will exit.  But the cache state should be still valid and can be
1318 resumed in the next "Sync Loop Run".
1319
1320 The '--logfile' option specifies an optional (required if '--daemon' is
1321 specified) logfile that takes all output of the command. The logfile is
1322 automatically reopened if fstat returns st_nlink == 0.
1323
1324 The optional '--daemon' option will put the command into the background.
1325
1326 You can stop the command without the '--daemon' option, also by hitting
1327 strg+c.
1328
1329 If you specify the '--no-wait' option the command skips the
1330 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1331 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1332
1333 Sync Loop Terminate
1334 ===================
1335
1336 In order to terminate an already running command (likely as daemon) the
1337 '--terminate' option can be used. This also requires the '--logfile' option
1338 to be specified.
1339
1340
1341 Example1:
1342 samba-tool user syncpasswords --cache-ldb-initialize \\
1343     --attributes=virtualClearTextUTF8
1344 samba-tool user syncpasswords
1345
1346 Example2:
1347 samba-tool user syncpasswords --cache-ldb-initialize \\
1348     --attributes=objectGUID,objectSID,sAMAccountName,\\
1349     userPrincipalName,userAccountControl,pwdLastSet,\\
1350     msDS-KeyVersionNumber,virtualCryptSHA512 \\
1351     --script=/path/to/my-custom-syncpasswords-script.py
1352 samba-tool user syncpasswords --daemon \\
1353     --logfile=/var/log/samba/user-syncpasswords.log
1354 samba-tool user syncpasswords --terminate \\
1355     --logfile=/var/log/samba/user-syncpasswords.log
1356
1357 """
1358     def __init__(self):
1359         super(cmd_user_syncpasswords, self).__init__()
1360
1361     synopsis = "%prog [--cache-ldb-initialize] [options]"
1362
1363     takes_optiongroups = {
1364         "sambaopts": options.SambaOptions,
1365         "versionopts": options.VersionOptions,
1366     }
1367
1368     takes_options = [
1369         Option("--cache-ldb-initialize",
1370                help="Initialize the cache for the first time",
1371                dest="cache_ldb_initialize", action="store_true"),
1372         Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1373                metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1374         Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1375                metavar="URL", dest="H"),
1376         Option("--filter", help="optional LDAP filter to set password on", type=str,
1377                metavar="LDAP-SEARCH-FILTER", dest="filter"),
1378         Option("--attributes", type=str,
1379                help=virtual_attributes_help,
1380                metavar="ATTRIBUTELIST", dest="attributes"),
1381         Option("--decrypt-samba-gpg",
1382                help=decrypt_samba_gpg_help,
1383                action="store_true", default=False, dest="decrypt_samba_gpg"),
1384         Option("--script", help="Script that is called for each password change", type=str,
1385                metavar="/path/to/syncpasswords.script", dest="script"),
1386         Option("--no-wait", help="Don't block waiting for changes",
1387                action="store_true", default=False, dest="nowait"),
1388         Option("--logfile", type=str,
1389                help="The logfile to use (required in --daemon mode).",
1390                metavar="/path/to/syncpasswords.log", dest="logfile"),
1391         Option("--daemon", help="daemonize after initial setup",
1392                action="store_true", default=False, dest="daemon"),
1393         Option("--terminate",
1394                help="Send a SIGTERM to an already running (daemon) process",
1395                action="store_true", default=False, dest="terminate"),
1396         ]
1397
1398     def run(self, cache_ldb_initialize=False, cache_ldb=None,
1399             H=None, filter=None,
1400             attributes=None, decrypt_samba_gpg=None,
1401             script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1402             sambaopts=None, versionopts=None):
1403
1404         self.lp = sambaopts.get_loadparm()
1405         self.logfile = None
1406         self.samdb_url = None
1407         self.samdb = None
1408         self.cache = None
1409
1410         if not cache_ldb_initialize:
1411             if attributes is not None:
1412                 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1413             if decrypt_samba_gpg:
1414                 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1415             if script is not None:
1416                 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1417             if filter is not None:
1418                 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1419             if H is not None:
1420                 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1421         else:
1422             if nowait is not False:
1423                 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1424             if logfile is not None:
1425                 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1426             if daemon is not False:
1427                 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1428             if terminate is not False:
1429                 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1430
1431         if nowait is True:
1432             if daemon is True:
1433                 raise CommandError("--daemon is not allowed together with --no-wait")
1434             if terminate is not False:
1435                 raise CommandError("--terminate is not allowed together with --no-wait")
1436
1437         if terminate is True and daemon is True:
1438             raise CommandError("--terminate is not allowed together with --daemon")
1439
1440         if daemon is True and logfile is None:
1441             raise CommandError("--daemon is only allowed together with --logfile")
1442
1443         if terminate is True and logfile is None:
1444             raise CommandError("--terminate is only allowed together with --logfile")
1445
1446         if script is not None:
1447             if not os.path.exists(script):
1448                 raise CommandError("script[%s] does not exist!" % script)
1449
1450             sync_command = "%s" % os.path.abspath(script)
1451         else:
1452             sync_command = None
1453
1454         dirsync_filter = filter
1455         if dirsync_filter is None:
1456             dirsync_filter = "(&" + \
1457                                "(objectClass=user)" + \
1458                                "(userAccountControl:%s:=%u)" % (
1459                                 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1460                                "(!(sAMAccountName=krbtgt*))" + \
1461                              ")"
1462
1463         dirsync_secret_attrs = [
1464             "unicodePwd",
1465             "dBCSPwd",
1466             "supplementalCredentials",
1467         ]
1468
1469         dirsync_attrs = dirsync_secret_attrs + [
1470             "pwdLastSet",
1471             "sAMAccountName",
1472             "userPrincipalName",
1473             "userAccountControl",
1474             "isDeleted",
1475             "isRecycled",
1476         ]
1477
1478         password_attrs = None
1479
1480         if cache_ldb_initialize:
1481             if H is None:
1482                 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1483
1484             if decrypt_samba_gpg and not gpgme_support:
1485                 raise CommandError(decrypt_samba_gpg_help)
1486
1487             password_attrs = self.parse_attributes(attributes)
1488             lower_attrs = [x.lower() for x in password_attrs]
1489             # We always return these in order to track deletions
1490             for a in ["objectGUID", "isDeleted", "isRecycled"]:
1491                 if a.lower() not in lower_attrs:
1492                     password_attrs += [a]
1493
1494         if cache_ldb is not None:
1495             if cache_ldb.lower().startswith("ldapi://"):
1496                 raise CommandError("--cache_ldb ldapi:// is not supported")
1497             elif cache_ldb.lower().startswith("ldap://"):
1498                 raise CommandError("--cache_ldb ldap:// is not supported")
1499             elif cache_ldb.lower().startswith("ldaps://"):
1500                 raise CommandError("--cache_ldb ldaps:// is not supported")
1501             elif cache_ldb.lower().startswith("tdb://"):
1502                 pass
1503             else:
1504                 if not os.path.exists(cache_ldb):
1505                     cache_ldb = self.lp.private_path(cache_ldb)
1506         else:
1507             cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1508
1509         self.lockfile = "%s.pid" % cache_ldb
1510
1511         def log_msg(msg):
1512             if self.logfile is not None:
1513                 info = os.fstat(0)
1514                 if info.st_nlink == 0:
1515                     logfile = self.logfile
1516                     self.logfile = None
1517                     log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1518                     logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
1519                     os.dup2(logfd, 0)
1520                     os.dup2(logfd, 1)
1521                     os.dup2(logfd, 2)
1522                     os.close(logfd)
1523                     log_msg("Reopened logfile[%s]\n" % (logfile))
1524                     self.logfile = logfile
1525             msg = "%s: pid[%d]: %s" % (
1526                     time.ctime(),
1527                     os.getpid(),
1528                     msg)
1529             self.outf.write(msg)
1530             return
1531
1532         def load_cache():
1533             cache_attrs = [
1534                 "samdbUrl",
1535                 "dirsyncFilter",
1536                 "dirsyncAttribute",
1537                 "dirsyncControl",
1538                 "passwordAttribute",
1539                 "decryptSambaGPG",
1540                 "syncCommand",
1541                 "currentPid",
1542             ]
1543
1544             self.cache = Ldb(cache_ldb)
1545             self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1546             res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1547                                     attrs=cache_attrs)
1548             if len(res) == 1:
1549                 try:
1550                     self.samdb_url = res[0]["samdbUrl"][0]
1551                 except KeyError as e:
1552                     self.samdb_url = None
1553             else:
1554                 self.samdb_url = None
1555             if self.samdb_url is None and not cache_ldb_initialize:
1556                 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1557                                    cache_ldb))
1558             if self.samdb_url is not None and cache_ldb_initialize:
1559                 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1560                                    cache_ldb))
1561             if self.samdb_url is None:
1562                 self.samdb_url = H
1563                 self.dirsync_filter = dirsync_filter
1564                 self.dirsync_attrs = dirsync_attrs
1565                 self.dirsync_controls = ["dirsync:1:0:0","extended_dn:1:0"];
1566                 self.password_attrs = password_attrs
1567                 self.decrypt_samba_gpg = decrypt_samba_gpg
1568                 self.sync_command = sync_command
1569                 add_ldif  = "dn: %s\n" % self.cache_dn
1570                 add_ldif += "objectClass: userSyncPasswords\n"
1571                 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url)
1572                 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter)
1573                 for a in self.dirsync_attrs:
1574                     add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a)
1575                 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1576                 for a in self.password_attrs:
1577                     add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
1578                 if self.decrypt_samba_gpg == True:
1579                     add_ldif += "decryptSambaGPG: TRUE\n"
1580                 else:
1581                     add_ldif += "decryptSambaGPG: FALSE\n"
1582                 if self.sync_command is not None:
1583                     add_ldif += "syncCommand: %s\n" % self.sync_command
1584                 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1585                 self.cache.add_ldif(add_ldif)
1586                 self.current_pid = None
1587                 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1588                 msgs = self.cache.parse_ldif(add_ldif)
1589                 changetype,msg = msgs.next()
1590                 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1591                 self.outf.write("%s" % ldif)
1592             else:
1593                 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1594                 self.dirsync_attrs = []
1595                 for a in res[0]["dirsyncAttribute"]:
1596                     self.dirsync_attrs.append(a)
1597                 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1598                 self.password_attrs = []
1599                 for a in res[0]["passwordAttribute"]:
1600                     self.password_attrs.append(a)
1601                 decrypt_string = res[0]["decryptSambaGPG"][0]
1602                 assert(decrypt_string in ["TRUE", "FALSE"])
1603                 if decrypt_string == "TRUE":
1604                     self.decrypt_samba_gpg = True
1605                 else:
1606                     self.decrypt_samba_gpg = False
1607                 if "syncCommand" in res[0]:
1608                     self.sync_command = res[0]["syncCommand"][0]
1609                 else:
1610                     self.sync_command = None
1611                 if "currentPid" in res[0]:
1612                     self.current_pid = int(res[0]["currentPid"][0])
1613                 else:
1614                     self.current_pid = None
1615                 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1616
1617             return
1618
1619         def run_sync_command(dn, ldif):
1620             log_msg("Call Popen[%s] for %s\n" % (dn, self.sync_command))
1621             sync_command_p = Popen(self.sync_command,
1622                                    stdin=PIPE,
1623                                    stdout=PIPE,
1624                                    stderr=STDOUT)
1625
1626             res = sync_command_p.poll()
1627             assert res is None
1628
1629             input = "%s" % (ldif)
1630             reply = sync_command_p.communicate(input)[0]
1631             log_msg("%s\n" % (reply))
1632             res = sync_command_p.poll()
1633             if res is None:
1634                 sync_command_p.terminate()
1635             res = sync_command_p.wait()
1636
1637             if reply.startswith("DONE-EXIT: "):
1638                 return
1639
1640             log_msg("RESULT: %s\n" % (res))
1641             raise Exception("ERROR: %s - %s\n" % (res, reply))
1642
1643         def handle_object(idx, dirsync_obj):
1644             binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1645             guid = ndr_unpack(misc.GUID, binary_guid)
1646             binary_sid = dirsync_obj.dn.get_extended_component("SID")
1647             sid = ndr_unpack(security.dom_sid, binary_sid)
1648             domain_sid, rid = sid.split()
1649             if rid == security.DOMAIN_RID_KRBTGT:
1650                 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1651                 return
1652             for a in list(dirsync_obj.keys()):
1653                 for h in dirsync_secret_attrs:
1654                     if a.lower() == h.lower():
1655                         del dirsync_obj[a]
1656                         dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1657             dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1658             log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif))
1659             obj = self.get_account_attributes(self.samdb,
1660                                               username="%s" % sid,
1661                                               basedn="<GUID=%s>" % guid,
1662                                               filter="(objectClass=user)",
1663                                               scope=ldb.SCOPE_BASE,
1664                                               attrs=self.password_attrs,
1665                                               decrypt=self.decrypt_samba_gpg)
1666             ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1667             log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1668             if self.sync_command is None:
1669                 self.outf.write("%s" % (ldif))
1670                 return
1671             self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1672             run_sync_command(obj.dn, ldif)
1673
1674         def check_current_pid_conflict(terminate):
1675             flags = os.O_RDWR
1676             if not terminate:
1677                 flags |= os.O_CREAT
1678
1679             try:
1680                 self.lockfd = os.open(self.lockfile, flags, 0600)
1681             except IOError as (err, msg):
1682                 if err == errno.ENOENT:
1683                     if terminate:
1684                         return False
1685                 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1686                         (self.lockfile, msg, err))
1687                 raise
1688
1689             got_exclusive = False
1690             try:
1691                 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1692                 got_exclusive = True
1693             except IOError as (err, msg):
1694                 if err != errno.EACCES and err != errno.EAGAIN:
1695                     log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
1696                             (self.lockfile, msg, err))
1697                     raise
1698
1699             if not got_exclusive:
1700                 buf = os.read(self.lockfd, 64)
1701                 self.current_pid = None
1702                 try:
1703                     self.current_pid = int(buf)
1704                 except ValueError as e:
1705                     pass
1706                 if self.current_pid is not None:
1707                     return True
1708
1709             if got_exclusive and terminate:
1710                 try:
1711                     os.ftruncate(self.lockfd, 0)
1712                 except IOError as (err, msg):
1713                     log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
1714                             (self.lockfile, msg, err))
1715                     raise
1716                 os.close(self.lockfd)
1717                 self.lockfd = -1
1718                 return False
1719
1720             try:
1721                 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
1722             except IOError as (err, msg):
1723                 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
1724                         (self.lockfile, msg, err))
1725
1726             # We leave the function with the shared lock.
1727             return False
1728
1729         def update_pid(pid):
1730             if self.lockfd != -1:
1731                 got_exclusive = False
1732                 # Try 5 times to get the exclusiv lock.
1733                 for i in xrange(0, 5):
1734                     try:
1735                         fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1736                         got_exclusive = True
1737                     except IOError as (err, msg):
1738                         if err != errno.EACCES and err != errno.EAGAIN:
1739                             log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
1740                                     (pid, self.lockfile, msg, err))
1741                             raise
1742                     if got_exclusive:
1743                         break
1744                     time.sleep(1)
1745                 if not got_exclusive:
1746                     log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
1747                             (pid, self.lockfile))
1748                     raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
1749                                        (pid, self.lockfile))
1750
1751                 if pid is not None:
1752                     buf = "%d\n" % pid
1753                 else:
1754                     buf = None
1755                 try:
1756                     os.ftruncate(self.lockfd, 0)
1757                     if buf is not None:
1758                         os.write(self.lockfd, buf)
1759                 except IOError as (err, msg):
1760                     log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
1761                             (self.lockfile, msg, err))
1762                     raise
1763             self.current_pid = pid
1764             if self.current_pid is not None:
1765                 log_msg("currentPid: %d\n" % self.current_pid)
1766
1767             modify_ldif =  "dn: %s\n" % (self.cache_dn)
1768             modify_ldif += "changetype: modify\n"
1769             modify_ldif += "replace: currentPid\n"
1770             if self.current_pid is not None:
1771                 modify_ldif += "currentPid: %d\n" % (self.current_pid)
1772             modify_ldif += "replace: currentTime\n"
1773             modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1774             self.cache.modify_ldif(modify_ldif)
1775             return
1776
1777         def update_cache(res_controls):
1778             assert len(res_controls) > 0
1779             assert res_controls[0].oid == "1.2.840.113556.1.4.841"
1780             res_controls[0].critical = True
1781             self.dirsync_controls = [str(res_controls[0]),"extended_dn:1:0"]
1782             log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
1783
1784             modify_ldif =  "dn: %s\n" % (self.cache_dn)
1785             modify_ldif += "changetype: modify\n"
1786             modify_ldif += "replace: dirsyncControl\n"
1787             modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
1788             modify_ldif += "replace: currentTime\n"
1789             modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1790             self.cache.modify_ldif(modify_ldif)
1791             return
1792
1793         def check_object(dirsync_obj, res_controls):
1794             assert len(res_controls) > 0
1795             assert res_controls[0].oid == "1.2.840.113556.1.4.841"
1796
1797             binary_sid = dirsync_obj.dn.get_extended_component("SID")
1798             sid = ndr_unpack(security.dom_sid, binary_sid)
1799             dn = "KEY=%s" % sid
1800             lastCookie = str(res_controls[0])
1801
1802             res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
1803                                     expression="(lastCookie=%s)" % (
1804                                         ldb.binary_encode(lastCookie)),
1805                                     attrs=[])
1806             if len(res) == 1:
1807                 return True
1808             return False
1809
1810         def update_object(dirsync_obj, res_controls):
1811             assert len(res_controls) > 0
1812             assert res_controls[0].oid == "1.2.840.113556.1.4.841"
1813
1814             binary_sid = dirsync_obj.dn.get_extended_component("SID")
1815             sid = ndr_unpack(security.dom_sid, binary_sid)
1816             dn = "KEY=%s" % sid
1817             lastCookie = str(res_controls[0])
1818
1819             self.cache.transaction_start()
1820             try:
1821                 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
1822                                         expression="(objectClass=*)",
1823                                         attrs=["lastCookie"])
1824                 if len(res) == 0:
1825                     add_ldif  = "dn: %s\n" % (dn)
1826                     add_ldif += "objectClass: userCookie\n"
1827                     add_ldif += "lastCookie: %s\n" % (lastCookie)
1828                     add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1829                     self.cache.add_ldif(add_ldif)
1830                 else:
1831                     modify_ldif =  "dn: %s\n" % (dn)
1832                     modify_ldif += "changetype: modify\n"
1833                     modify_ldif += "replace: lastCookie\n"
1834                     modify_ldif += "lastCookie: %s\n" % (lastCookie)
1835                     modify_ldif += "replace: currentTime\n"
1836                     modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1837                     self.cache.modify_ldif(modify_ldif)
1838                 self.cache.transaction_commit()
1839             except Exception as e:
1840                 self.cache.transaction_cancel()
1841
1842             return
1843
1844         def dirsync_loop():
1845             while True:
1846                 res = self.samdb.search(expression=self.dirsync_filter,
1847                                         scope=ldb.SCOPE_SUBTREE,
1848                                         attrs=self.dirsync_attrs,
1849                                         controls=self.dirsync_controls)
1850                 log_msg("dirsync_loop(): results %d\n" % len(res))
1851                 ri = 0
1852                 for r in res:
1853                     done = check_object(r, res.controls)
1854                     if not done:
1855                         handle_object(ri, r)
1856                         update_object(r, res.controls)
1857                     ri += 1
1858                 update_cache(res.controls)
1859                 if len(res) == 0:
1860                     break
1861
1862         def sync_loop(wait):
1863             notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
1864             notify_controls = ["notification:1"]
1865             notify_handle = self.samdb.search_iterator(expression="objectClass=*",
1866                                                        scope=ldb.SCOPE_SUBTREE,
1867                                                        attrs=notify_attrs,
1868                                                        controls=notify_controls,
1869                                                        timeout=-1)
1870
1871             if wait is True:
1872                 log_msg("Resuming monitoring\n")
1873             else:
1874                 log_msg("Getting changes\n")
1875             self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
1876             self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
1877             self.outf.write("syncCommand: %s\n" % self.sync_command)
1878             dirsync_loop()
1879
1880             if wait is not True:
1881                 return
1882
1883             for msg in notify_handle:
1884                 if not isinstance(msg, ldb.Message):
1885                     self.outf.write("referal: %s\n" % msg)
1886                     continue
1887                 created = msg.get("uSNCreated")[0]
1888                 changed = msg.get("uSNChanged")[0]
1889                 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
1890                         (msg.dn, created, changed))
1891
1892                 dirsync_loop()
1893
1894             res = notify_handle.result()
1895
1896         def daemonize():
1897             self.samdb = None
1898             self.cache = None
1899             orig_pid = os.getpid()
1900             pid = os.fork()
1901             if pid == 0:
1902                 os.setsid()
1903                 pid = os.fork()
1904                 if pid == 0: # Actual daemon
1905                     pid = os.getpid()
1906                     log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
1907                     load_cache()
1908                     return
1909             os._exit(0)
1910
1911         if cache_ldb_initialize:
1912             self.samdb_url = H
1913             self.samdb = self.connect_system_samdb(url=self.samdb_url,
1914                                                    verbose=True)
1915             load_cache()
1916             return
1917
1918         if logfile is not None:
1919             import resource      # Resource usage information.
1920             maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1921             if maxfd == resource.RLIM_INFINITY:
1922                 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
1923             logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
1924             self.outf.write("Using logfile[%s]\n" % logfile)
1925             for fd in range(0, maxfd):
1926                 if fd == logfd:
1927                     continue
1928                 try:
1929                     os.close(fd)
1930                 except OSError:
1931                     pass
1932             os.dup2(logfd, 0)
1933             os.dup2(logfd, 1)
1934             os.dup2(logfd, 2)
1935             os.close(logfd)
1936             log_msg("Attached to logfile[%s]\n" % (logfile))
1937             self.logfile = logfile
1938
1939         load_cache()
1940         conflict = check_current_pid_conflict(terminate)
1941         if terminate:
1942             if self.current_pid is None:
1943                 log_msg("No process running.\n")
1944                 return
1945             if not conflict:
1946                 log_msg("Proccess %d is not running anymore.\n" % (
1947                         self.current_pid))
1948                 update_pid(None)
1949                 return
1950             log_msg("Sending SIGTERM to proccess %d.\n" % (
1951                     self.current_pid))
1952             os.kill(self.current_pid, signal.SIGTERM)
1953             return
1954         if conflict:
1955             raise CommandError("Exiting pid %d, command is already running as pid %d" % (
1956                                os.getpid(), self.current_pid))
1957
1958         if daemon is True:
1959             daemonize()
1960         update_pid(os.getpid())
1961
1962         wait = True
1963         while wait is True:
1964             retry_sleep_min = 1
1965             retry_sleep_max = 600
1966             if nowait is True:
1967                 wait = False
1968                 retry_sleep = 0
1969             else:
1970                 retry_sleep = retry_sleep_min
1971
1972             while self.samdb is None:
1973                 if retry_sleep != 0:
1974                     log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
1975                     time.sleep(retry_sleep)
1976                 retry_sleep = retry_sleep * 2
1977                 if retry_sleep >= retry_sleep_max:
1978                     retry_sleep = retry_sleep_max
1979                 log_msg("Connecting to '%s'\n" % self.samdb_url)
1980                 try:
1981                     self.samdb = self.connect_system_samdb(url=self.samdb_url)
1982                 except Exception as msg:
1983                     self.samdb = None
1984                     log_msg("Connect to samdb Exception => (%s)\n" % msg)
1985                     if wait is not True:
1986                         raise
1987
1988             try:
1989                 sync_loop(wait)
1990             except ldb.LdbError as (enum, estr):
1991                 self.samdb = None
1992                 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
1993
1994         update_pid(None)
1995         return
1996
1997 class cmd_user(SuperCommand):
1998     """User management."""
1999
2000     subcommands = {}
2001     subcommands["add"] = cmd_user_add()
2002     subcommands["create"] = cmd_user_create()
2003     subcommands["delete"] = cmd_user_delete()
2004     subcommands["disable"] = cmd_user_disable()
2005     subcommands["enable"] = cmd_user_enable()
2006     subcommands["list"] = cmd_user_list()
2007     subcommands["setexpiry"] = cmd_user_setexpiry()
2008     subcommands["password"] = cmd_user_password()
2009     subcommands["setpassword"] = cmd_user_setpassword()
2010     subcommands["getpassword"] = cmd_user_getpassword()
2011     subcommands["syncpasswords"] = cmd_user_syncpasswords()