samba-tool user: implement the user move command
[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, 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, 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, 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, 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 su samba-tool user setexpiry User2
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, 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, 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, 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, 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 (major, minor, msg):
1041                     if major == gpgme.ERR_BAD_SECKEY:
1042                         msg = "ERR_BAD_SECKEY: " + msg
1043                     else:
1044                         msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1045                     self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1046                                     username or account_name, msg))
1047
1048         def get_utf8(a, b, username):
1049             try:
1050                 u = unicode(b, 'utf-16-le')
1051             except UnicodeDecodeError as e:
1052                 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1053                                 username, a))
1054                 return None
1055             u8 = u.encode('utf-8')
1056             return u8
1057
1058         # Extract the WDigest hash for the value specified by i.
1059         # Builds an htdigest compatible value
1060         DIGEST = "Digest"
1061         def get_wDigest(i, primary_wdigest, account_name, account_upn,
1062                         domain, dns_domain):
1063             if i == 1:
1064                 user  = account_name
1065                 realm= domain
1066             elif i == 2:
1067                 user  = account_name.lower()
1068                 realm = domain.lower()
1069             elif i == 3:
1070                 user  = account_name.upper()
1071                 realm = domain.upper()
1072             elif i == 4:
1073                 user  = account_name
1074                 realm = domain.upper()
1075             elif i == 5:
1076                 user  = account_name
1077                 realm = domain.lower()
1078             elif i == 6:
1079                 user  = account_name.upper()
1080                 realm = domain.lower()
1081             elif i == 7:
1082                 user  = account_name.lower()
1083                 realm = domain.upper()
1084             elif i == 8:
1085                 user  = account_name
1086                 realm = dns_domain.lower()
1087             elif i == 9:
1088                 user  = account_name.lower()
1089                 realm = dns_domain.lower()
1090             elif i == 10:
1091                 user  = account_name.upper()
1092                 realm = dns_domain.upper()
1093             elif i == 11:
1094                 user  = account_name
1095                 realm = dns_domain.upper()
1096             elif i == 12:
1097                 user  = account_name
1098                 realm = dns_domain.lower()
1099             elif i == 13:
1100                 user  = account_name.upper()
1101                 realm = dns_domain.lower()
1102             elif i == 14:
1103                 user  = account_name.lower()
1104                 realm = dns_domain.upper()
1105             elif i == 15:
1106                 user  = account_upn
1107                 realm = ""
1108             elif i == 16:
1109                 user  = account_upn.lower()
1110                 realm = ""
1111             elif i == 17:
1112                 user  = account_upn.upper()
1113                 realm = ""
1114             elif i == 18:
1115                 user  = "%s\\%s" % (domain, account_name)
1116                 realm = ""
1117             elif i == 19:
1118                 user  = "%s\\%s" % (domain.lower(), account_name.lower())
1119                 realm = ""
1120             elif i == 20:
1121                 user  = "%s\\%s" % (domain.upper(), account_name.upper())
1122                 realm = ""
1123             elif i == 21:
1124                 user  = account_name
1125                 realm = DIGEST
1126             elif i == 22:
1127                 user  = account_name.lower()
1128                 realm = DIGEST
1129             elif i == 23:
1130                 user  = account_name.upper()
1131                 realm = DIGEST
1132             elif i == 24:
1133                 user  = account_upn
1134                 realm = DIGEST
1135             elif i == 25:
1136                 user  = account_upn.lower()
1137                 realm = DIGEST
1138             elif i == 26:
1139                 user  = account_upn.upper()
1140                 realm = DIGEST
1141             elif i == 27:
1142                 user  = "%s\\%s" % (domain, account_name)
1143                 realm = DIGEST
1144             elif i == 28:
1145                 # Differs from spec, see tests
1146                 user  = "%s\\%s" % (domain.lower(), account_name.lower())
1147                 realm = DIGEST
1148             elif i == 29:
1149                 # Differs from spec, see tests
1150                 user  = "%s\\%s" % (domain.upper(), account_name.upper())
1151                 realm = DIGEST
1152             else:
1153                 user  = ""
1154
1155             digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1156                                  primary_wdigest)
1157             try:
1158                 digest = binascii.hexlify(bytearray(digests.hashes[i-1].hash))
1159                 return "%s:%s:%s" % (user, realm, digest)
1160             except IndexError:
1161                 return None
1162
1163
1164         # get the value for a virtualCrypt attribute.
1165         # look for an exact match on algorithm and rounds in supplemental creds
1166         # if not found calculate using Primary:CLEARTEXT
1167         # if no Primary:CLEARTEXT return the first supplementalCredential
1168         #    that matches the algorithm.
1169         def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1170             sv = None
1171             fb = None
1172             b = get_package("Primary:userPassword")
1173             if b is not None:
1174                 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1175             if sv is None:
1176                 # No exact match on algorithm and number of rounds
1177                 # try and calculate one from the Primary:CLEARTEXT
1178                 b = get_package("Primary:CLEARTEXT")
1179                 if b is not None:
1180                     u8 = get_utf8(a, b, username or account_name)
1181                     if u8 is not None:
1182                         sv = get_crypt_value(str(algorithm), u8, rounds)
1183                 if sv is None:
1184                     # Unable to calculate a hash with the specified
1185                     # number of rounds, fall back to the first hash using
1186                     # the specified algorithm
1187                     sv = fb
1188             if sv is None:
1189                 return None
1190             return "{CRYPT}" + sv
1191
1192         def get_userPassword_hash(blob, algorithm, rounds):
1193             up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1194             SCHEME = "{CRYPT}"
1195
1196             # Check that the NT hash has not been changed without updating
1197             # the user password hashes. This indicates that password has been
1198             # changed without updating the supplemental credentials.
1199             if unicodePwd != bytearray(up.current_nt_hash.hash):
1200                 return None
1201
1202             scheme_prefix = "$%d$" % algorithm
1203             prefix = scheme_prefix
1204             if rounds > 0:
1205                 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1206             scheme_match = None
1207
1208             for h in up.hashes:
1209                 if (scheme_match is None and
1210                       h.scheme == SCHEME and
1211                       h.value.startswith(scheme_prefix)):
1212                     scheme_match = h.value
1213                 if h.scheme == SCHEME and h.value.startswith(prefix):
1214                     return (h.value, scheme_match)
1215
1216             # No match on the number of rounds, return the value of the
1217             # first matching scheme
1218             return (None, scheme_match)
1219
1220         # We use sort here in order to have a predictable processing order
1221         for a in sorted(virtual_attributes.keys()):
1222             if not a.lower() in lower_attrs:
1223                 continue
1224
1225             if a == "virtualClearTextUTF8":
1226                 b = get_package("Primary:CLEARTEXT")
1227                 if b is None:
1228                     continue
1229                 u8 = get_utf8(a, b, username or account_name)
1230                 if u8 is None:
1231                     continue
1232                 v = u8
1233             elif a == "virtualClearTextUTF16":
1234                 v = get_package("Primary:CLEARTEXT")
1235                 if v is None:
1236                     continue
1237             elif a == "virtualSSHA":
1238                 b = get_package("Primary:CLEARTEXT")
1239                 if b is None:
1240                     continue
1241                 u8 = get_utf8(a, b, username or account_name)
1242                 if u8 is None:
1243                     continue
1244                 salt = get_random_bytes(4)
1245                 h = hashlib.sha1()
1246                 h.update(u8)
1247                 h.update(salt)
1248                 bv = h.digest() + salt
1249                 v = "{SSHA}" + base64.b64encode(bv)
1250             elif a == "virtualCryptSHA256":
1251                 rounds = get_rounds(attr_opts[a])
1252                 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1253                 if x is None:
1254                     continue
1255                 v = x
1256             elif a == "virtualCryptSHA512":
1257                 rounds = get_rounds(attr_opts[a])
1258                 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1259                 if x is None:
1260                     continue
1261                 v = x
1262             elif a == "virtualSambaGPG":
1263                 # Samba adds 'Primary:SambaGPG' at the end.
1264                 # When Windows sets the password it keeps
1265                 # 'Primary:SambaGPG' and rotates it to
1266                 # the begining. So we can only use the value,
1267                 # if it is the last one.
1268                 v = get_package("Primary:SambaGPG", min_idx=-1)
1269                 if v is None:
1270                     continue
1271             elif a.startswith("virtualWDigest"):
1272                 primary_wdigest = get_package("Primary:WDigest")
1273                 if primary_wdigest is None:
1274                     continue
1275                 x = a[len("virtualWDigest"):]
1276                 try:
1277                     i = int(x)
1278                 except ValueError:
1279                     continue
1280                 domain = self.lp.get("workgroup")
1281                 dns_domain = samdb.domain_dns_name()
1282                 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1283                 if v is None:
1284                     continue
1285             else:
1286                 continue
1287             obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1288         return obj
1289
1290     def parse_attributes(self, attributes):
1291
1292         if attributes is None:
1293             raise CommandError("Please specify --attributes")
1294         attrs = attributes.split(',')
1295         password_attrs = []
1296         for pa in attrs:
1297             pa = pa.lstrip().rstrip()
1298             for da in disabled_virtual_attributes.keys():
1299                 if pa.lower() == da.lower():
1300                     r = disabled_virtual_attributes[da]["reason"]
1301                     raise CommandError("Virtual attribute '%s' not supported: %s" % (
1302                                        da, r))
1303             for va in virtual_attributes.keys():
1304                 if pa.lower() == va.lower():
1305                     # Take the real name
1306                     pa = va
1307                     break
1308             password_attrs += [pa]
1309
1310         return password_attrs
1311
1312 class cmd_user_getpassword(GetPasswordCommand):
1313     """Get the password fields of a user/computer account.
1314
1315 This command gets the logon password for a user/computer account.
1316
1317 The username specified on the command is the sAMAccountName.
1318 The username may also be specified using the --filter option.
1319
1320 The command must be run from the root user id or another authorized user id.
1321 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1322 used to adjust the local path. By default tdb:// is used by default.
1323
1324 The '--attributes' parameter takes a comma separated list of attributes,
1325 which will be printed or given to the script specified by '--script'. If a
1326 specified attribute is not available on an object it's silently omitted.
1327 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1328 the NTHASH) and the following virtual attributes are possible (see --help
1329 for which virtual attributes are supported in your environment):
1330
1331    virtualClearTextUTF16: The raw cleartext as stored in the
1332                           'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1333                           with '--decrypt-samba-gpg') buffer inside of the
1334                           supplementalCredentials attribute. This typically
1335                           contains valid UTF-16-LE, but may contain random
1336                           bytes, e.g. for computer accounts.
1337
1338    virtualClearTextUTF8:  As virtualClearTextUTF16, but converted to UTF-8
1339                           (only from valid UTF-16-LE)
1340
1341    virtualSSHA:           As virtualClearTextUTF8, but a salted SHA-1
1342                           checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1343
1344    virtualCryptSHA256:    As virtualClearTextUTF8, but a salted SHA256
1345                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1346                           with a $5$... salt, see crypt(3) on modern systems.
1347                           The number of rounds used to calculate the hash can
1348                           also be specified. By appending ";rounds=x" to the
1349                           attribute name i.e. virtualCryptSHA256;rounds=10000
1350                           will calculate a SHA256 hash with 10,000 rounds.
1351                           non numeric values for rounds are silently ignored
1352                           The value is calculated as follows:
1353                           1) If a value exists in 'Primary:userPassword' with
1354                              the specified number of rounds it is returned.
1355                           2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1356                              '--decrypt-samba-gpg'. Calculate a hash with
1357                              the specified number of rounds
1358                           3) Return the first CryptSHA256 value in
1359                              'Primary:userPassword'
1360
1361
1362    virtualCryptSHA512:    As virtualClearTextUTF8, but a salted SHA512
1363                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1364                           with a $6$... salt, see crypt(3) on modern systems.
1365                           The number of rounds used to calculate the hash can
1366                           also be specified. By appending ";rounds=x" to the
1367                           attribute name i.e. virtualCryptSHA512;rounds=10000
1368                           will calculate a SHA512 hash with 10,000 rounds.
1369                           non numeric values for rounds are silently ignored
1370                           The value is calculated as follows:
1371                           1) If a value exists in 'Primary:userPassword' with
1372                              the specified number of rounds it is returned.
1373                           2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1374                              '--decrypt-samba-gpg'. Calculate a hash with
1375                              the specified number of rounds
1376                           3) Return the first CryptSHA512 value in
1377                              'Primary:userPassword'
1378
1379    virtualWDigestNN:      The individual hash values stored in
1380                           'Primary:WDigest' where NN is the hash number in
1381                           the range 01 to 29.
1382                           NOTE: As at 22-05-2017 the documentation:
1383                           3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1384                         https://msdn.microsoft.com/en-us/library/cc245680.aspx
1385                           is incorrect
1386
1387    virtualSambaGPG:       The raw cleartext as stored in the
1388                           'Primary:SambaGPG' buffer inside of the
1389                           supplementalCredentials attribute.
1390                           See the 'password hash gpg key ids' option in
1391                           smb.conf.
1392
1393 The '--decrypt-samba-gpg' option triggers decryption of the
1394 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1395 in your environment or not (the python-gpgme package is required).  Please
1396 note that you might need to set the GNUPGHOME environment variable.  If the
1397 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1398 environment variable has been set correctly and the passphrase is already
1399 known by the gpg-agent.
1400
1401 Example1:
1402 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1403
1404 Example2:
1405 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1406
1407 """
1408     def __init__(self):
1409         super(cmd_user_getpassword, self).__init__()
1410
1411     synopsis = "%prog (<username>|--filter <filter>) [options]"
1412
1413     takes_optiongroups = {
1414         "sambaopts": options.SambaOptions,
1415         "versionopts": options.VersionOptions,
1416     }
1417
1418     takes_options = [
1419         Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1420                metavar="URL", dest="H"),
1421         Option("--filter", help="LDAP Filter to set password on", type=str),
1422         Option("--attributes", type=str,
1423                help=virtual_attributes_help,
1424                metavar="ATTRIBUTELIST", dest="attributes"),
1425         Option("--decrypt-samba-gpg",
1426                help=decrypt_samba_gpg_help,
1427                action="store_true", default=False, dest="decrypt_samba_gpg"),
1428         ]
1429
1430     takes_args = ["username?"]
1431
1432     def run(self, username=None, H=None, filter=None,
1433             attributes=None, decrypt_samba_gpg=None,
1434             sambaopts=None, versionopts=None):
1435         self.lp = sambaopts.get_loadparm()
1436
1437         if decrypt_samba_gpg and not gpgme_support:
1438             raise CommandError(decrypt_samba_gpg_help)
1439
1440         if filter is None and username is None:
1441             raise CommandError("Either the username or '--filter' must be specified!")
1442
1443         if filter is None:
1444             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1445
1446         if attributes is None:
1447             raise CommandError("Please specify --attributes")
1448
1449         password_attrs = self.parse_attributes(attributes)
1450
1451         samdb = self.connect_system_samdb(url=H, allow_local=True)
1452
1453         obj = self.get_account_attributes(samdb, username,
1454                                           basedn=None,
1455                                           filter=filter,
1456                                           scope=ldb.SCOPE_SUBTREE,
1457                                           attrs=password_attrs,
1458                                           decrypt=decrypt_samba_gpg)
1459
1460         ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1461         self.outf.write("%s" % ldif)
1462         self.outf.write("Got password OK\n")
1463
1464 class cmd_user_syncpasswords(GetPasswordCommand):
1465     """Sync the password of user accounts.
1466
1467 This syncs logon passwords for user accounts.
1468
1469 Note that this command should run on a single domain controller only
1470 (typically the PDC-emulator). However the "password hash gpg key ids"
1471 option should to be configured on all domain controllers.
1472
1473 The command must be run from the root user id or another authorized user id.
1474 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1475 local path.  By default, ldapi:// is used with the default path to the
1476 privileged ldapi socket.
1477
1478 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1479 "Sync Loop Terminate".
1480
1481
1482 Cache Initialization
1483 ====================
1484
1485 The first time, this command needs to be called with
1486 '--cache-ldb-initialize' in order to initialize its cache.
1487
1488 The cache initialization requires '--attributes' and allows the following
1489 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1490 '-H/--URL'.
1491
1492 The '--attributes' parameter takes a comma separated list of attributes,
1493 which will be printed or given to the script specified by '--script'. If a
1494 specified attribute is not available on an object it will be silently omitted.
1495 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1496 the NTHASH) and the following virtual attributes are possible (see '--help'
1497 for supported virtual attributes in your environment):
1498
1499    virtualClearTextUTF16: The raw cleartext as stored in the
1500                           'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1501                           with '--decrypt-samba-gpg') buffer inside of the
1502                           supplementalCredentials attribute. This typically
1503                           contains valid UTF-16-LE, but may contain random
1504                           bytes, e.g. for computer accounts.
1505
1506    virtualClearTextUTF8:  As virtualClearTextUTF16, but converted to UTF-8
1507                           (only from valid UTF-16-LE)
1508
1509    virtualSSHA:           As virtualClearTextUTF8, but a salted SHA-1
1510                           checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1511
1512    virtualCryptSHA256:    As virtualClearTextUTF8, but a salted SHA256
1513                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1514                           with a $5$... salt, see crypt(3) on modern systems.
1515                           The number of rounds used to calculate the hash can
1516                           also be specified. By appending ";rounds=x" to the
1517                           attribute name i.e. virtualCryptSHA256;rounds=10000
1518                           will calculate a SHA256 hash with 10,000 rounds.
1519                           non numeric values for rounds are silently ignored
1520                           The value is calculated as follows:
1521                           1) If a value exists in 'Primary:userPassword' with
1522                              the specified number of rounds it is returned.
1523                           2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1524                              '--decrypt-samba-gpg'. Calculate a hash with
1525                              the specified number of rounds
1526                           3) Return the first CryptSHA256 value in
1527                              'Primary:userPassword'
1528
1529    virtualCryptSHA512:    As virtualClearTextUTF8, but a salted SHA512
1530                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1531                           with a $6$... salt, see crypt(3) on modern systems.
1532                           The number of rounds used to calculate the hash can
1533                           also be specified. By appending ";rounds=x" to the
1534                           attribute name i.e. virtualCryptSHA512;rounds=10000
1535                           will calculate a SHA512 hash with 10,000 rounds.
1536                           non numeric values for rounds are silently ignored
1537                           The value is calculated as follows:
1538                           1) If a value exists in 'Primary:userPassword' with
1539                              the specified number of rounds it is returned.
1540                           2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1541                              '--decrypt-samba-gpg'. Calculate a hash with
1542                              the specified number of rounds
1543                           3) Return the first CryptSHA512 value in
1544                              'Primary:userPassword'
1545
1546    virtualWDigestNN:      The individual hash values stored in
1547                           'Primary:WDigest' where NN is the hash number in
1548                           the range 01 to 29.
1549                           NOTE: As at 22-05-2017 the documentation:
1550                           3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1551                         https://msdn.microsoft.com/en-us/library/cc245680.aspx
1552                           is incorrect.
1553
1554    virtualSambaGPG:       The raw cleartext as stored in the
1555                           'Primary:SambaGPG' buffer inside of the
1556                           supplementalCredentials attribute.
1557                           See the 'password hash gpg key ids' option in
1558                           smb.conf.
1559
1560 The '--decrypt-samba-gpg' option triggers decryption of the
1561 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1562 in your environment or not (the python-gpgme package is required).  Please
1563 note that you might need to set the GNUPGHOME environment variable.  If the
1564 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1565 environment variable has been set correctly and the passphrase is already
1566 known by the gpg-agent.
1567
1568 The '--script' option specifies a custom script that is called whenever any
1569 of the dirsyncAttributes (see below) was changed. The script is called
1570 without any arguments. It gets the LDIF for exactly one object on STDIN.
1571 If the script processed the object successfully it has to respond with a
1572 single line starting with 'DONE-EXIT: ' followed by an optional message.
1573
1574 Note that the script might be called without any password change, e.g. if
1575 the account was disabled (a userAccountControl change) or the
1576 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1577 are always returned as unique identifier of the account. It might be useful
1578 to also ask for non-password attributes like: objectSid, sAMAccountName,
1579 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1580 Depending on the object, some attributes may not be present/available,
1581 but you always get the current state (and not a diff).
1582
1583 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1584 into the logfile.
1585
1586 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1587 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1588     (!(sAMAccountName=krbtgt*)))
1589 This means only normal (non-krbtgt) user
1590 accounts are monitored.  The '--filter' can modify that, e.g. if it's
1591 required to also sync computer accounts.
1592
1593
1594 Sync Loop Run
1595 =============
1596
1597 This (default) mode runs in an endless loop waiting for password related
1598 changes in the active directory database. It makes use of the
1599 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1600 get changes in a reliable fashion. Objects are monitored for changes of the
1601 following dirsyncAttributes:
1602
1603   unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1604   userPrincipalName and userAccountControl.
1605
1606 It recovers from LDAP disconnects and updates the cache in conservative way
1607 (in single steps after each succesfully processed change).  An error from
1608 the script (specified by '--script') will result in fatal error and this
1609 command will exit.  But the cache state should be still valid and can be
1610 resumed in the next "Sync Loop Run".
1611
1612 The '--logfile' option specifies an optional (required if '--daemon' is
1613 specified) logfile that takes all output of the command. The logfile is
1614 automatically reopened if fstat returns st_nlink == 0.
1615
1616 The optional '--daemon' option will put the command into the background.
1617
1618 You can stop the command without the '--daemon' option, also by hitting
1619 strg+c.
1620
1621 If you specify the '--no-wait' option the command skips the
1622 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1623 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1624
1625 Sync Loop Terminate
1626 ===================
1627
1628 In order to terminate an already running command (likely as daemon) the
1629 '--terminate' option can be used. This also requires the '--logfile' option
1630 to be specified.
1631
1632
1633 Example1:
1634 samba-tool user syncpasswords --cache-ldb-initialize \\
1635     --attributes=virtualClearTextUTF8
1636 samba-tool user syncpasswords
1637
1638 Example2:
1639 samba-tool user syncpasswords --cache-ldb-initialize \\
1640     --attributes=objectGUID,objectSID,sAMAccountName,\\
1641     userPrincipalName,userAccountControl,pwdLastSet,\\
1642     msDS-KeyVersionNumber,virtualCryptSHA512 \\
1643     --script=/path/to/my-custom-syncpasswords-script.py
1644 samba-tool user syncpasswords --daemon \\
1645     --logfile=/var/log/samba/user-syncpasswords.log
1646 samba-tool user syncpasswords --terminate \\
1647     --logfile=/var/log/samba/user-syncpasswords.log
1648
1649 """
1650     def __init__(self):
1651         super(cmd_user_syncpasswords, self).__init__()
1652
1653     synopsis = "%prog [--cache-ldb-initialize] [options]"
1654
1655     takes_optiongroups = {
1656         "sambaopts": options.SambaOptions,
1657         "versionopts": options.VersionOptions,
1658     }
1659
1660     takes_options = [
1661         Option("--cache-ldb-initialize",
1662                help="Initialize the cache for the first time",
1663                dest="cache_ldb_initialize", action="store_true"),
1664         Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1665                metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1666         Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1667                metavar="URL", dest="H"),
1668         Option("--filter", help="optional LDAP filter to set password on", type=str,
1669                metavar="LDAP-SEARCH-FILTER", dest="filter"),
1670         Option("--attributes", type=str,
1671                help=virtual_attributes_help,
1672                metavar="ATTRIBUTELIST", dest="attributes"),
1673         Option("--decrypt-samba-gpg",
1674                help=decrypt_samba_gpg_help,
1675                action="store_true", default=False, dest="decrypt_samba_gpg"),
1676         Option("--script", help="Script that is called for each password change", type=str,
1677                metavar="/path/to/syncpasswords.script", dest="script"),
1678         Option("--no-wait", help="Don't block waiting for changes",
1679                action="store_true", default=False, dest="nowait"),
1680         Option("--logfile", type=str,
1681                help="The logfile to use (required in --daemon mode).",
1682                metavar="/path/to/syncpasswords.log", dest="logfile"),
1683         Option("--daemon", help="daemonize after initial setup",
1684                action="store_true", default=False, dest="daemon"),
1685         Option("--terminate",
1686                help="Send a SIGTERM to an already running (daemon) process",
1687                action="store_true", default=False, dest="terminate"),
1688         ]
1689
1690     def run(self, cache_ldb_initialize=False, cache_ldb=None,
1691             H=None, filter=None,
1692             attributes=None, decrypt_samba_gpg=None,
1693             script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1694             sambaopts=None, versionopts=None):
1695
1696         self.lp = sambaopts.get_loadparm()
1697         self.logfile = None
1698         self.samdb_url = None
1699         self.samdb = None
1700         self.cache = None
1701
1702         if not cache_ldb_initialize:
1703             if attributes is not None:
1704                 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1705             if decrypt_samba_gpg:
1706                 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1707             if script is not None:
1708                 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1709             if filter is not None:
1710                 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1711             if H is not None:
1712                 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1713         else:
1714             if nowait is not False:
1715                 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1716             if logfile is not None:
1717                 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1718             if daemon is not False:
1719                 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1720             if terminate is not False:
1721                 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1722
1723         if nowait is True:
1724             if daemon is True:
1725                 raise CommandError("--daemon is not allowed together with --no-wait")
1726             if terminate is not False:
1727                 raise CommandError("--terminate is not allowed together with --no-wait")
1728
1729         if terminate is True and daemon is True:
1730             raise CommandError("--terminate is not allowed together with --daemon")
1731
1732         if daemon is True and logfile is None:
1733             raise CommandError("--daemon is only allowed together with --logfile")
1734
1735         if terminate is True and logfile is None:
1736             raise CommandError("--terminate is only allowed together with --logfile")
1737
1738         if script is not None:
1739             if not os.path.exists(script):
1740                 raise CommandError("script[%s] does not exist!" % script)
1741
1742             sync_command = "%s" % os.path.abspath(script)
1743         else:
1744             sync_command = None
1745
1746         dirsync_filter = filter
1747         if dirsync_filter is None:
1748             dirsync_filter = "(&" + \
1749                                "(objectClass=user)" + \
1750                                "(userAccountControl:%s:=%u)" % (
1751                                 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1752                                "(!(sAMAccountName=krbtgt*))" + \
1753                              ")"
1754
1755         dirsync_secret_attrs = [
1756             "unicodePwd",
1757             "dBCSPwd",
1758             "supplementalCredentials",
1759         ]
1760
1761         dirsync_attrs = dirsync_secret_attrs + [
1762             "pwdLastSet",
1763             "sAMAccountName",
1764             "userPrincipalName",
1765             "userAccountControl",
1766             "isDeleted",
1767             "isRecycled",
1768         ]
1769
1770         password_attrs = None
1771
1772         if cache_ldb_initialize:
1773             if H is None:
1774                 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1775
1776             if decrypt_samba_gpg and not gpgme_support:
1777                 raise CommandError(decrypt_samba_gpg_help)
1778
1779             password_attrs = self.parse_attributes(attributes)
1780             lower_attrs = [x.lower() for x in password_attrs]
1781             # We always return these in order to track deletions
1782             for a in ["objectGUID", "isDeleted", "isRecycled"]:
1783                 if a.lower() not in lower_attrs:
1784                     password_attrs += [a]
1785
1786         if cache_ldb is not None:
1787             if cache_ldb.lower().startswith("ldapi://"):
1788                 raise CommandError("--cache_ldb ldapi:// is not supported")
1789             elif cache_ldb.lower().startswith("ldap://"):
1790                 raise CommandError("--cache_ldb ldap:// is not supported")
1791             elif cache_ldb.lower().startswith("ldaps://"):
1792                 raise CommandError("--cache_ldb ldaps:// is not supported")
1793             elif cache_ldb.lower().startswith("tdb://"):
1794                 pass
1795             else:
1796                 if not os.path.exists(cache_ldb):
1797                     cache_ldb = self.lp.private_path(cache_ldb)
1798         else:
1799             cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1800
1801         self.lockfile = "%s.pid" % cache_ldb
1802
1803         def log_msg(msg):
1804             if self.logfile is not None:
1805                 info = os.fstat(0)
1806                 if info.st_nlink == 0:
1807                     logfile = self.logfile
1808                     self.logfile = None
1809                     log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1810                     logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
1811                     os.dup2(logfd, 0)
1812                     os.dup2(logfd, 1)
1813                     os.dup2(logfd, 2)
1814                     os.close(logfd)
1815                     log_msg("Reopened logfile[%s]\n" % (logfile))
1816                     self.logfile = logfile
1817             msg = "%s: pid[%d]: %s" % (
1818                     time.ctime(),
1819                     os.getpid(),
1820                     msg)
1821             self.outf.write(msg)
1822             return
1823
1824         def load_cache():
1825             cache_attrs = [
1826                 "samdbUrl",
1827                 "dirsyncFilter",
1828                 "dirsyncAttribute",
1829                 "dirsyncControl",
1830                 "passwordAttribute",
1831                 "decryptSambaGPG",
1832                 "syncCommand",
1833                 "currentPid",
1834             ]
1835
1836             self.cache = Ldb(cache_ldb)
1837             self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1838             res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1839                                     attrs=cache_attrs)
1840             if len(res) == 1:
1841                 try:
1842                     self.samdb_url = res[0]["samdbUrl"][0]
1843                 except KeyError as e:
1844                     self.samdb_url = None
1845             else:
1846                 self.samdb_url = None
1847             if self.samdb_url is None and not cache_ldb_initialize:
1848                 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1849                                    cache_ldb))
1850             if self.samdb_url is not None and cache_ldb_initialize:
1851                 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1852                                    cache_ldb))
1853             if self.samdb_url is None:
1854                 self.samdb_url = H
1855                 self.dirsync_filter = dirsync_filter
1856                 self.dirsync_attrs = dirsync_attrs
1857                 self.dirsync_controls = ["dirsync:1:0:0","extended_dn:1:0"];
1858                 self.password_attrs = password_attrs
1859                 self.decrypt_samba_gpg = decrypt_samba_gpg
1860                 self.sync_command = sync_command
1861                 add_ldif  = "dn: %s\n" % self.cache_dn
1862                 add_ldif += "objectClass: userSyncPasswords\n"
1863                 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url)
1864                 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter)
1865                 for a in self.dirsync_attrs:
1866                     add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a)
1867                 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1868                 for a in self.password_attrs:
1869                     add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
1870                 if self.decrypt_samba_gpg == True:
1871                     add_ldif += "decryptSambaGPG: TRUE\n"
1872                 else:
1873                     add_ldif += "decryptSambaGPG: FALSE\n"
1874                 if self.sync_command is not None:
1875                     add_ldif += "syncCommand: %s\n" % self.sync_command
1876                 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1877                 self.cache.add_ldif(add_ldif)
1878                 self.current_pid = None
1879                 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1880                 msgs = self.cache.parse_ldif(add_ldif)
1881                 changetype,msg = msgs.next()
1882                 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1883                 self.outf.write("%s" % ldif)
1884             else:
1885                 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1886                 self.dirsync_attrs = []
1887                 for a in res[0]["dirsyncAttribute"]:
1888                     self.dirsync_attrs.append(a)
1889                 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1890                 self.password_attrs = []
1891                 for a in res[0]["passwordAttribute"]:
1892                     self.password_attrs.append(a)
1893                 decrypt_string = res[0]["decryptSambaGPG"][0]
1894                 assert(decrypt_string in ["TRUE", "FALSE"])
1895                 if decrypt_string == "TRUE":
1896                     self.decrypt_samba_gpg = True
1897                 else:
1898                     self.decrypt_samba_gpg = False
1899                 if "syncCommand" in res[0]:
1900                     self.sync_command = res[0]["syncCommand"][0]
1901                 else:
1902                     self.sync_command = None
1903                 if "currentPid" in res[0]:
1904                     self.current_pid = int(res[0]["currentPid"][0])
1905                 else:
1906                     self.current_pid = None
1907                 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1908
1909             return
1910
1911         def run_sync_command(dn, ldif):
1912             log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1913             sync_command_p = Popen(self.sync_command,
1914                                    stdin=PIPE,
1915                                    stdout=PIPE,
1916                                    stderr=STDOUT)
1917
1918             res = sync_command_p.poll()
1919             assert res is None
1920
1921             input = "%s" % (ldif)
1922             reply = sync_command_p.communicate(input)[0]
1923             log_msg("%s\n" % (reply))
1924             res = sync_command_p.poll()
1925             if res is None:
1926                 sync_command_p.terminate()
1927             res = sync_command_p.wait()
1928
1929             if reply.startswith("DONE-EXIT: "):
1930                 return
1931
1932             log_msg("RESULT: %s\n" % (res))
1933             raise Exception("ERROR: %s - %s\n" % (res, reply))
1934
1935         def handle_object(idx, dirsync_obj):
1936             binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1937             guid = ndr_unpack(misc.GUID, binary_guid)
1938             binary_sid = dirsync_obj.dn.get_extended_component("SID")
1939             sid = ndr_unpack(security.dom_sid, binary_sid)
1940             domain_sid, rid = sid.split()
1941             if rid == security.DOMAIN_RID_KRBTGT:
1942                 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1943                 return
1944             for a in list(dirsync_obj.keys()):
1945                 for h in dirsync_secret_attrs:
1946                     if a.lower() == h.lower():
1947                         del dirsync_obj[a]
1948                         dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1949             dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1950             log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif))
1951             obj = self.get_account_attributes(self.samdb,
1952                                               username="%s" % sid,
1953                                               basedn="<GUID=%s>" % guid,
1954                                               filter="(objectClass=user)",
1955                                               scope=ldb.SCOPE_BASE,
1956                                               attrs=self.password_attrs,
1957                                               decrypt=self.decrypt_samba_gpg)
1958             ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1959             log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1960             if self.sync_command is None:
1961                 self.outf.write("%s" % (ldif))
1962                 return
1963             self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1964             run_sync_command(obj.dn, ldif)
1965
1966         def check_current_pid_conflict(terminate):
1967             flags = os.O_RDWR
1968             if not terminate:
1969                 flags |= os.O_CREAT
1970
1971             try:
1972                 self.lockfd = os.open(self.lockfile, flags, 0600)
1973             except IOError as (err, msg):
1974                 if err == errno.ENOENT:
1975                     if terminate:
1976                         return False
1977                 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1978                         (self.lockfile, msg, err))
1979                 raise
1980
1981             got_exclusive = False
1982             try:
1983                 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1984                 got_exclusive = True
1985             except IOError as (err, msg):
1986                 if err != errno.EACCES and err != errno.EAGAIN:
1987                     log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
1988                             (self.lockfile, msg, err))
1989                     raise
1990
1991             if not got_exclusive:
1992                 buf = os.read(self.lockfd, 64)
1993                 self.current_pid = None
1994                 try:
1995                     self.current_pid = int(buf)
1996                 except ValueError as e:
1997                     pass
1998                 if self.current_pid is not None:
1999                     return True
2000
2001             if got_exclusive and terminate:
2002                 try:
2003                     os.ftruncate(self.lockfd, 0)
2004                 except IOError as (err, msg):
2005                     log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2006                             (self.lockfile, msg, err))
2007                     raise
2008                 os.close(self.lockfd)
2009                 self.lockfd = -1
2010                 return False
2011
2012             try:
2013                 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2014             except IOError as (err, msg):
2015                 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2016                         (self.lockfile, msg, err))
2017
2018             # We leave the function with the shared lock.
2019             return False
2020
2021         def update_pid(pid):
2022             if self.lockfd != -1:
2023                 got_exclusive = False
2024                 # Try 5 times to get the exclusiv lock.
2025                 for i in xrange(0, 5):
2026                     try:
2027                         fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2028                         got_exclusive = True
2029                     except IOError as (err, msg):
2030                         if err != errno.EACCES and err != errno.EAGAIN:
2031                             log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2032                                     (pid, self.lockfile, msg, err))
2033                             raise
2034                     if got_exclusive:
2035                         break
2036                     time.sleep(1)
2037                 if not got_exclusive:
2038                     log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
2039                             (pid, self.lockfile))
2040                     raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2041                                        (pid, self.lockfile))
2042
2043                 if pid is not None:
2044                     buf = "%d\n" % pid
2045                 else:
2046                     buf = None
2047                 try:
2048                     os.ftruncate(self.lockfd, 0)
2049                     if buf is not None:
2050                         os.write(self.lockfd, buf)
2051                 except IOError as (err, msg):
2052                     log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2053                             (self.lockfile, msg, err))
2054                     raise
2055             self.current_pid = pid
2056             if self.current_pid is not None:
2057                 log_msg("currentPid: %d\n" % self.current_pid)
2058
2059             modify_ldif =  "dn: %s\n" % (self.cache_dn)
2060             modify_ldif += "changetype: modify\n"
2061             modify_ldif += "replace: currentPid\n"
2062             if self.current_pid is not None:
2063                 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2064             modify_ldif += "replace: currentTime\n"
2065             modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2066             self.cache.modify_ldif(modify_ldif)
2067             return
2068
2069         def update_cache(res_controls):
2070             assert len(res_controls) > 0
2071             assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2072             res_controls[0].critical = True
2073             self.dirsync_controls = [str(res_controls[0]),"extended_dn:1:0"]
2074             log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2075
2076             modify_ldif =  "dn: %s\n" % (self.cache_dn)
2077             modify_ldif += "changetype: modify\n"
2078             modify_ldif += "replace: dirsyncControl\n"
2079             modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2080             modify_ldif += "replace: currentTime\n"
2081             modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2082             self.cache.modify_ldif(modify_ldif)
2083             return
2084
2085         def check_object(dirsync_obj, res_controls):
2086             assert len(res_controls) > 0
2087             assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2088
2089             binary_sid = dirsync_obj.dn.get_extended_component("SID")
2090             sid = ndr_unpack(security.dom_sid, binary_sid)
2091             dn = "KEY=%s" % sid
2092             lastCookie = str(res_controls[0])
2093
2094             res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2095                                     expression="(lastCookie=%s)" % (
2096                                         ldb.binary_encode(lastCookie)),
2097                                     attrs=[])
2098             if len(res) == 1:
2099                 return True
2100             return False
2101
2102         def update_object(dirsync_obj, res_controls):
2103             assert len(res_controls) > 0
2104             assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2105
2106             binary_sid = dirsync_obj.dn.get_extended_component("SID")
2107             sid = ndr_unpack(security.dom_sid, binary_sid)
2108             dn = "KEY=%s" % sid
2109             lastCookie = str(res_controls[0])
2110
2111             self.cache.transaction_start()
2112             try:
2113                 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2114                                         expression="(objectClass=*)",
2115                                         attrs=["lastCookie"])
2116                 if len(res) == 0:
2117                     add_ldif  = "dn: %s\n" % (dn)
2118                     add_ldif += "objectClass: userCookie\n"
2119                     add_ldif += "lastCookie: %s\n" % (lastCookie)
2120                     add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2121                     self.cache.add_ldif(add_ldif)
2122                 else:
2123                     modify_ldif =  "dn: %s\n" % (dn)
2124                     modify_ldif += "changetype: modify\n"
2125                     modify_ldif += "replace: lastCookie\n"
2126                     modify_ldif += "lastCookie: %s\n" % (lastCookie)
2127                     modify_ldif += "replace: currentTime\n"
2128                     modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2129                     self.cache.modify_ldif(modify_ldif)
2130                 self.cache.transaction_commit()
2131             except Exception as e:
2132                 self.cache.transaction_cancel()
2133
2134             return
2135
2136         def dirsync_loop():
2137             while True:
2138                 res = self.samdb.search(expression=self.dirsync_filter,
2139                                         scope=ldb.SCOPE_SUBTREE,
2140                                         attrs=self.dirsync_attrs,
2141                                         controls=self.dirsync_controls)
2142                 log_msg("dirsync_loop(): results %d\n" % len(res))
2143                 ri = 0
2144                 for r in res:
2145                     done = check_object(r, res.controls)
2146                     if not done:
2147                         handle_object(ri, r)
2148                         update_object(r, res.controls)
2149                     ri += 1
2150                 update_cache(res.controls)
2151                 if len(res) == 0:
2152                     break
2153
2154         def sync_loop(wait):
2155             notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2156             notify_controls = ["notification:1", "show_recycled:1"]
2157             notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2158                                                        scope=ldb.SCOPE_SUBTREE,
2159                                                        attrs=notify_attrs,
2160                                                        controls=notify_controls,
2161                                                        timeout=-1)
2162
2163             if wait is True:
2164                 log_msg("Resuming monitoring\n")
2165             else:
2166                 log_msg("Getting changes\n")
2167             self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2168             self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2169             self.outf.write("syncCommand: %s\n" % self.sync_command)
2170             dirsync_loop()
2171
2172             if wait is not True:
2173                 return
2174
2175             for msg in notify_handle:
2176                 if not isinstance(msg, ldb.Message):
2177                     self.outf.write("referal: %s\n" % msg)
2178                     continue
2179                 created = msg.get("uSNCreated")[0]
2180                 changed = msg.get("uSNChanged")[0]
2181                 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2182                         (msg.dn, created, changed))
2183
2184                 dirsync_loop()
2185
2186             res = notify_handle.result()
2187
2188         def daemonize():
2189             self.samdb = None
2190             self.cache = None
2191             orig_pid = os.getpid()
2192             pid = os.fork()
2193             if pid == 0:
2194                 os.setsid()
2195                 pid = os.fork()
2196                 if pid == 0: # Actual daemon
2197                     pid = os.getpid()
2198                     log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2199                     load_cache()
2200                     return
2201             os._exit(0)
2202
2203         if cache_ldb_initialize:
2204             self.samdb_url = H
2205             self.samdb = self.connect_system_samdb(url=self.samdb_url,
2206                                                    verbose=True)
2207             load_cache()
2208             return
2209
2210         if logfile is not None:
2211             import resource      # Resource usage information.
2212             maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2213             if maxfd == resource.RLIM_INFINITY:
2214                 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2215             logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
2216             self.outf.write("Using logfile[%s]\n" % logfile)
2217             for fd in range(0, maxfd):
2218                 if fd == logfd:
2219                     continue
2220                 try:
2221                     os.close(fd)
2222                 except OSError:
2223                     pass
2224             os.dup2(logfd, 0)
2225             os.dup2(logfd, 1)
2226             os.dup2(logfd, 2)
2227             os.close(logfd)
2228             log_msg("Attached to logfile[%s]\n" % (logfile))
2229             self.logfile = logfile
2230
2231         load_cache()
2232         conflict = check_current_pid_conflict(terminate)
2233         if terminate:
2234             if self.current_pid is None:
2235                 log_msg("No process running.\n")
2236                 return
2237             if not conflict:
2238                 log_msg("Proccess %d is not running anymore.\n" % (
2239                         self.current_pid))
2240                 update_pid(None)
2241                 return
2242             log_msg("Sending SIGTERM to proccess %d.\n" % (
2243                     self.current_pid))
2244             os.kill(self.current_pid, signal.SIGTERM)
2245             return
2246         if conflict:
2247             raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2248                                os.getpid(), self.current_pid))
2249
2250         if daemon is True:
2251             daemonize()
2252         update_pid(os.getpid())
2253
2254         wait = True
2255         while wait is True:
2256             retry_sleep_min = 1
2257             retry_sleep_max = 600
2258             if nowait is True:
2259                 wait = False
2260                 retry_sleep = 0
2261             else:
2262                 retry_sleep = retry_sleep_min
2263
2264             while self.samdb is None:
2265                 if retry_sleep != 0:
2266                     log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2267                     time.sleep(retry_sleep)
2268                 retry_sleep = retry_sleep * 2
2269                 if retry_sleep >= retry_sleep_max:
2270                     retry_sleep = retry_sleep_max
2271                 log_msg("Connecting to '%s'\n" % self.samdb_url)
2272                 try:
2273                     self.samdb = self.connect_system_samdb(url=self.samdb_url)
2274                 except Exception as msg:
2275                     self.samdb = None
2276                     log_msg("Connect to samdb Exception => (%s)\n" % msg)
2277                     if wait is not True:
2278                         raise
2279
2280             try:
2281                 sync_loop(wait)
2282             except ldb.LdbError as (enum, estr):
2283                 self.samdb = None
2284                 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2285
2286         update_pid(None)
2287         return
2288
2289 class cmd_user_edit(Command):
2290     """Modify User AD object.
2291
2292 This command will allow editing of a user account in the Active Directory
2293 domain. You will then be able to add or change attributes and their values.
2294
2295 The username specified on the command is the sAMAccountName.
2296
2297 The command may be run from the root userid or another authorized userid.
2298
2299 The -H or --URL= option can be used to execute the command against a remote
2300 server.
2301
2302 Example1:
2303 samba-tool user edit User1 -H ldap://samba.samdom.example.com \
2304 -U administrator --password=passw1rd
2305
2306 Example1 shows how to edit a users attributes in the domain against a remote
2307 LDAP server.
2308
2309 The -H parameter is used to specify the remote target server.
2310
2311 Example2:
2312 samba-tool user edit User2
2313
2314 Example2 shows how to edit a users attributes in the domain against a local
2315 LDAP server.
2316
2317 Example3:
2318 samba-tool user edit User3 --editor=nano
2319
2320 Example3 shows how to edit a users attributes in the domain against a local
2321 LDAP server using the 'nano' editor.
2322
2323 """
2324     synopsis = "%prog <username> [options]"
2325
2326     takes_options = [
2327         Option("-H", "--URL", help="LDB URL for database or target server",
2328                type=str, metavar="URL", dest="H"),
2329         Option("--editor", help="Editor to use instead of the system default,"
2330                " or 'vi' if no system default is set.", type=str),
2331     ]
2332
2333     takes_args = ["username"]
2334     takes_optiongroups = {
2335         "sambaopts": options.SambaOptions,
2336         "credopts": options.CredentialsOptions,
2337         "versionopts": options.VersionOptions,
2338         }
2339
2340     def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2341             H=None, editor=None):
2342
2343         lp = sambaopts.get_loadparm()
2344         creds = credopts.get_credentials(lp, fallback_machine=True)
2345         samdb = SamDB(url=H, session_info=system_session(),
2346                       credentials=creds, lp=lp)
2347
2348         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2349                   (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2350
2351         domaindn = samdb.domain_dn()
2352
2353         try:
2354             res = samdb.search(base=domaindn,
2355                                expression=filter,
2356                                scope=ldb.SCOPE_SUBTREE)
2357             user_dn = res[0].dn
2358         except IndexError:
2359             raise CommandError('Unable to find user "%s"' % (username))
2360
2361         for msg in res:
2362             r_ldif = samdb.write_ldif(msg, 1)
2363             # remove 'changetype' line
2364             result_ldif = re.sub('changetype: add\n', '', r_ldif)
2365
2366             if editor is None:
2367                 editor = os.environ.get('EDITOR')
2368                 if editor is None:
2369                     editor = 'vi'
2370
2371             with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2372                 t_file.write(result_ldif)
2373                 t_file.flush()
2374                 try:
2375                     check_call([editor, t_file.name])
2376                 except CalledProcessError as e:
2377                     raise CalledProcessError("ERROR: ", e)
2378                 with open(t_file.name) as edited_file:
2379                     edited_message = edited_file.read()
2380
2381         if result_ldif != edited_message:
2382             diff = difflib.ndiff(result_ldif.splitlines(),
2383                                  edited_message.splitlines())
2384             minus_lines = []
2385             plus_lines = []
2386             for line in diff:
2387                 if line.startswith('-'):
2388                     line = line[2:]
2389                     minus_lines.append(line)
2390                 elif line.startswith('+'):
2391                     line = line[2:]
2392                     plus_lines.append(line)
2393
2394             user_ldif="dn: %s\n" % user_dn
2395             user_ldif += "changetype: modify\n"
2396
2397             for line in minus_lines:
2398                 attr, val = line.split(':', 1)
2399                 search_attr="%s:" % attr
2400                 if not re.search(r'^' + search_attr, str(plus_lines)):
2401                     user_ldif += "delete: %s\n" % attr
2402                     user_ldif += "%s: %s\n" % (attr, val)
2403
2404             for line in plus_lines:
2405                 attr, val = line.split(':', 1)
2406                 search_attr="%s:" % attr
2407                 if re.search(r'^' + search_attr, str(minus_lines)):
2408                     user_ldif += "replace: %s\n" % attr
2409                     user_ldif += "%s: %s\n" % (attr, val)
2410                 if not re.search(r'^' + search_attr, str(minus_lines)):
2411                     user_ldif += "add: %s\n" % attr
2412                     user_ldif += "%s: %s\n" % (attr, val)
2413
2414             try:
2415                 samdb.modify_ldif(user_ldif)
2416             except Exception as e:
2417                 raise CommandError("Failed to modify user '%s': " %
2418                                    username, e)
2419
2420             self.outf.write("Modified User '%s' successfully\n" % username)
2421
2422 class cmd_user_show(Command):
2423     """Display a user AD object.
2424
2425 This command displays a user account and it's attributes in the Active
2426 Directory domain.
2427 The username specified on the command is the sAMAccountName.
2428
2429 The command may be run from the root userid or another authorized userid.
2430
2431 The -H or --URL= option can be used to execute the command against a remote
2432 server.
2433
2434 Example1:
2435 samba-tool user show User1 -H ldap://samba.samdom.example.com \
2436 -U administrator --password=passw1rd
2437
2438 Example1 shows how to display a users attributes in the domain against a remote
2439 LDAP server.
2440
2441 The -H parameter is used to specify the remote target server.
2442
2443 Example2:
2444 samba-tool user show User2
2445
2446 Example2 shows how to display a users attributes in the domain against a local
2447 LDAP server.
2448
2449 Example3:
2450 samba-tool user show User2 --attributes=objectSid,memberOf
2451
2452 Example3 shows how to display a users objectSid and memberOf attributes.
2453 """
2454     synopsis = "%prog <username> [options]"
2455
2456     takes_options = [
2457         Option("-H", "--URL", help="LDB URL for database or target server",
2458                type=str, metavar="URL", dest="H"),
2459         Option("--attributes",
2460                help=("Comma separated list of attributes, "
2461                      "which will be printed."),
2462                type=str, dest="user_attrs"),
2463     ]
2464
2465     takes_args = ["username"]
2466     takes_optiongroups = {
2467         "sambaopts": options.SambaOptions,
2468         "credopts": options.CredentialsOptions,
2469         "versionopts": options.VersionOptions,
2470         }
2471
2472     def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2473             H=None, user_attrs=None):
2474
2475         lp = sambaopts.get_loadparm()
2476         creds = credopts.get_credentials(lp, fallback_machine=True)
2477         samdb = SamDB(url=H, session_info=system_session(),
2478                       credentials=creds, lp=lp)
2479
2480         attrs = None
2481         if user_attrs:
2482             attrs = user_attrs.split(",")
2483
2484         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2485                   (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2486
2487         domaindn = samdb.domain_dn()
2488
2489         try:
2490             res = samdb.search(base=domaindn, expression=filter,
2491                                scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2492             user_dn = res[0].dn
2493         except IndexError:
2494             raise CommandError('Unable to find user "%s"' % (username))
2495
2496         for msg in res:
2497             user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2498             self.outf.write(user_ldif)
2499
2500 class cmd_user_move(Command):
2501     """Move a user to an organizational unit/container.
2502
2503     This command moves a user account into the specified organizational unit
2504     or container.
2505     The username specified on the command is the sAMAccountName.
2506     The name of the organizational unit or container can be specified as a
2507     full DN or without the domainDN component.
2508
2509     The command may be run from the root userid or another authorized userid.
2510
2511     The -H or --URL= option can be used to execute the command against a remote
2512     server.
2513
2514     Example1:
2515     samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
2516         -H ldap://samba.samdom.example.com -U administrator
2517
2518     Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2519     unit on a remote LDAP server.
2520
2521     The -H parameter is used to specify the remote target server.
2522
2523     Example2:
2524     samba-tool user move User1 CN=Users
2525
2526     Example2 shows how to move a user User1 back into the CN=Users container
2527     on the local server.
2528     """
2529
2530     synopsis = "%prog <username> <new_parent_dn> [options]"
2531
2532     takes_options = [
2533         Option("-H", "--URL", help="LDB URL for database or target server",
2534                type=str, metavar="URL", dest="H"),
2535     ]
2536
2537     takes_args = [ "username", "new_parent_dn" ]
2538     takes_optiongroups = {
2539         "sambaopts": options.SambaOptions,
2540         "credopts": options.CredentialsOptions,
2541         "versionopts": options.VersionOptions,
2542         }
2543
2544     def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2545             versionopts=None, H=None):
2546         lp = sambaopts.get_loadparm()
2547         creds = credopts.get_credentials(lp, fallback_machine=True)
2548         samdb = SamDB(url=H, session_info=system_session(),
2549                       credentials=creds, lp=lp)
2550         domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2551
2552         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2553                   (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2554         try:
2555             res = samdb.search(base=domain_dn,
2556                                expression=filter,
2557                                scope=ldb.SCOPE_SUBTREE)
2558             user_dn = res[0].dn
2559         except IndexError:
2560             raise CommandError('Unable to find user "%s"' % (username))
2561
2562         try:
2563             full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2564         except Exception, e:
2565             raise CommandError('Invalid new_parent_dn "%s": %s' %
2566                                (new_parent_dn, e.message))
2567
2568         full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2569         full_new_user_dn.remove_base_components(len(user_dn)-1)
2570         full_new_user_dn.add_base(full_new_parent_dn)
2571
2572         try:
2573             samdb.rename(user_dn, full_new_user_dn)
2574         except Exception, e:
2575             raise CommandError('Failed to move user "%s"' % username, e)
2576         self.outf.write('Moved user "%s" into "%s"\n' %
2577                         (username, full_new_parent_dn))
2578
2579 class cmd_user(SuperCommand):
2580     """User management."""
2581
2582     subcommands = {}
2583     subcommands["add"] = cmd_user_add()
2584     subcommands["create"] = cmd_user_create()
2585     subcommands["delete"] = cmd_user_delete()
2586     subcommands["disable"] = cmd_user_disable()
2587     subcommands["enable"] = cmd_user_enable()
2588     subcommands["list"] = cmd_user_list()
2589     subcommands["setexpiry"] = cmd_user_setexpiry()
2590     subcommands["password"] = cmd_user_password()
2591     subcommands["setpassword"] = cmd_user_setpassword()
2592     subcommands["getpassword"] = cmd_user_getpassword()
2593     subcommands["syncpasswords"] = cmd_user_syncpasswords()
2594     subcommands["edit"] = cmd_user_edit()
2595     subcommands["show"] = cmd_user_show()
2596     subcommands["move"] = cmd_user_move()