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