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