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