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