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