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