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