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