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