90828cbfb7a6ff317f32d6f1522a97bcf0fdc2a7
[amitay/samba.git] / python / samba / netcmd / user.py
1 # user management
2 #
3 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
4 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19
20 import samba.getopt as options
21 import ldb
22 import pwd
23 import os
24 import re
25 import tempfile
26 import difflib
27 import 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] - %s" %
2087                             (pid, self.lockfile))
2088                     raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2089                                        (pid, self.lockfile))
2090
2091                 if pid is not None:
2092                     buf = "%d\n" % pid
2093                 else:
2094                     buf = None
2095                 try:
2096                     os.ftruncate(self.lockfd, 0)
2097                     if buf is not None:
2098                         os.write(self.lockfd, get_bytes(buf))
2099                 except IOError as e3:
2100                     (err, msg) = e3.args
2101                     log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2102                             (self.lockfile, msg, err))
2103                     raise
2104             self.current_pid = pid
2105             if self.current_pid is not None:
2106                 log_msg("currentPid: %d\n" % self.current_pid)
2107
2108             modify_ldif = "dn: %s\n" % (self.cache_dn)
2109             modify_ldif += "changetype: modify\n"
2110             modify_ldif += "replace: currentPid\n"
2111             if self.current_pid is not None:
2112                 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2113             modify_ldif += "replace: currentTime\n"
2114             modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2115             self.cache.modify_ldif(modify_ldif)
2116             return
2117
2118         def update_cache(res_controls):
2119             assert len(res_controls) > 0
2120             assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2121             res_controls[0].critical = True
2122             self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2123             log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2124
2125             modify_ldif = "dn: %s\n" % (self.cache_dn)
2126             modify_ldif += "changetype: modify\n"
2127             modify_ldif += "replace: dirsyncControl\n"
2128             modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2129             modify_ldif += "replace: currentTime\n"
2130             modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2131             self.cache.modify_ldif(modify_ldif)
2132             return
2133
2134         def check_object(dirsync_obj, res_controls):
2135             assert len(res_controls) > 0
2136             assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2137
2138             binary_sid = dirsync_obj.dn.get_extended_component("SID")
2139             sid = ndr_unpack(security.dom_sid, binary_sid)
2140             dn = "KEY=%s" % sid
2141             lastCookie = str(res_controls[0])
2142
2143             res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2144                                     expression="(lastCookie=%s)" % (
2145                                         ldb.binary_encode(lastCookie)),
2146                                     attrs=[])
2147             if len(res) == 1:
2148                 return True
2149             return False
2150
2151         def update_object(dirsync_obj, res_controls):
2152             assert len(res_controls) > 0
2153             assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2154
2155             binary_sid = dirsync_obj.dn.get_extended_component("SID")
2156             sid = ndr_unpack(security.dom_sid, binary_sid)
2157             dn = "KEY=%s" % sid
2158             lastCookie = str(res_controls[0])
2159
2160             self.cache.transaction_start()
2161             try:
2162                 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2163                                         expression="(objectClass=*)",
2164                                         attrs=["lastCookie"])
2165                 if len(res) == 0:
2166                     add_ldif  = "dn: %s\n" % (dn)
2167                     add_ldif += "objectClass: userCookie\n"
2168                     add_ldif += "lastCookie: %s\n" % (lastCookie)
2169                     add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2170                     self.cache.add_ldif(add_ldif)
2171                 else:
2172                     modify_ldif = "dn: %s\n" % (dn)
2173                     modify_ldif += "changetype: modify\n"
2174                     modify_ldif += "replace: lastCookie\n"
2175                     modify_ldif += "lastCookie: %s\n" % (lastCookie)
2176                     modify_ldif += "replace: currentTime\n"
2177                     modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2178                     self.cache.modify_ldif(modify_ldif)
2179                 self.cache.transaction_commit()
2180             except Exception as e:
2181                 self.cache.transaction_cancel()
2182
2183             return
2184
2185         def dirsync_loop():
2186             while True:
2187                 res = self.samdb.search(expression=str(self.dirsync_filter),
2188                                         scope=ldb.SCOPE_SUBTREE,
2189                                         attrs=self.dirsync_attrs,
2190                                         controls=self.dirsync_controls)
2191                 log_msg("dirsync_loop(): results %d\n" % len(res))
2192                 ri = 0
2193                 for r in res:
2194                     done = check_object(r, res.controls)
2195                     if not done:
2196                         handle_object(ri, r)
2197                         update_object(r, res.controls)
2198                     ri += 1
2199                 update_cache(res.controls)
2200                 if len(res) == 0:
2201                     break
2202
2203         def sync_loop(wait):
2204             notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2205             notify_controls = ["notification:1", "show_recycled:1"]
2206             notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2207                                                        scope=ldb.SCOPE_SUBTREE,
2208                                                        attrs=notify_attrs,
2209                                                        controls=notify_controls,
2210                                                        timeout=-1)
2211
2212             if wait is True:
2213                 log_msg("Resuming monitoring\n")
2214             else:
2215                 log_msg("Getting changes\n")
2216             self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2217             self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2218             self.outf.write("syncCommand: %s\n" % self.sync_command)
2219             dirsync_loop()
2220
2221             if wait is not True:
2222                 return
2223
2224             for msg in notify_handle:
2225                 if not isinstance(msg, ldb.Message):
2226                     self.outf.write("referal: %s\n" % msg)
2227                     continue
2228                 created = msg.get("uSNCreated")[0]
2229                 changed = msg.get("uSNChanged")[0]
2230                 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2231                         (msg.dn, created, changed))
2232
2233                 dirsync_loop()
2234
2235             res = notify_handle.result()
2236
2237         def daemonize():
2238             self.samdb = None
2239             self.cache = None
2240             orig_pid = os.getpid()
2241             pid = os.fork()
2242             if pid == 0:
2243                 os.setsid()
2244                 pid = os.fork()
2245                 if pid == 0:  # Actual daemon
2246                     pid = os.getpid()
2247                     log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2248                     load_cache()
2249                     return
2250             os._exit(0)
2251
2252         if cache_ldb_initialize:
2253             self.samdb_url = H
2254             self.samdb = self.connect_system_samdb(url=self.samdb_url,
2255                                                    verbose=True)
2256             load_cache()
2257             return
2258
2259         if logfile is not None:
2260             import resource      # Resource usage information.
2261             maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2262             if maxfd == resource.RLIM_INFINITY:
2263                 maxfd = 1024  # Rough guess at maximum number of open file descriptors.
2264             logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2265             self.outf.write("Using logfile[%s]\n" % logfile)
2266             for fd in range(0, maxfd):
2267                 if fd == logfd:
2268                     continue
2269                 try:
2270                     os.close(fd)
2271                 except OSError:
2272                     pass
2273             os.dup2(logfd, 0)
2274             os.dup2(logfd, 1)
2275             os.dup2(logfd, 2)
2276             os.close(logfd)
2277             log_msg("Attached to logfile[%s]\n" % (logfile))
2278             self.logfile = logfile
2279
2280         load_cache()
2281         conflict = check_current_pid_conflict(terminate)
2282         if terminate:
2283             if self.current_pid is None:
2284                 log_msg("No process running.\n")
2285                 return
2286             if not conflict:
2287                 log_msg("Proccess %d is not running anymore.\n" % (
2288                         self.current_pid))
2289                 update_pid(None)
2290                 return
2291             log_msg("Sending SIGTERM to proccess %d.\n" % (
2292                     self.current_pid))
2293             os.kill(self.current_pid, signal.SIGTERM)
2294             return
2295         if conflict:
2296             raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2297                                os.getpid(), self.current_pid))
2298
2299         if daemon is True:
2300             daemonize()
2301         update_pid(os.getpid())
2302
2303         wait = True
2304         while wait is True:
2305             retry_sleep_min = 1
2306             retry_sleep_max = 600
2307             if nowait is True:
2308                 wait = False
2309                 retry_sleep = 0
2310             else:
2311                 retry_sleep = retry_sleep_min
2312
2313             while self.samdb is None:
2314                 if retry_sleep != 0:
2315                     log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2316                     time.sleep(retry_sleep)
2317                 retry_sleep = retry_sleep * 2
2318                 if retry_sleep >= retry_sleep_max:
2319                     retry_sleep = retry_sleep_max
2320                 log_msg("Connecting to '%s'\n" % self.samdb_url)
2321                 try:
2322                     self.samdb = self.connect_system_samdb(url=self.samdb_url)
2323                 except Exception as msg:
2324                     self.samdb = None
2325                     log_msg("Connect to samdb Exception => (%s)\n" % msg)
2326                     if wait is not True:
2327                         raise
2328
2329             try:
2330                 sync_loop(wait)
2331             except ldb.LdbError as e7:
2332                 (enum, estr) = e7.args
2333                 self.samdb = None
2334                 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2335
2336         update_pid(None)
2337         return
2338
2339
2340 class cmd_user_edit(Command):
2341     """Modify User AD object.
2342
2343 This command will allow editing of a user account in the Active Directory
2344 domain. You will then be able to add or change attributes and their values.
2345
2346 The username specified on the command is the sAMAccountName.
2347
2348 The command may be run from the root userid or another authorized userid.
2349
2350 The -H or --URL= option can be used to execute the command against a remote
2351 server.
2352
2353 Example1:
2354 samba-tool user edit User1 -H ldap://samba.samdom.example.com \
2355 -U administrator --password=passw1rd
2356
2357 Example1 shows how to edit a users attributes in the domain against a remote
2358 LDAP server.
2359
2360 The -H parameter is used to specify the remote target server.
2361
2362 Example2:
2363 samba-tool user edit User2
2364
2365 Example2 shows how to edit a users attributes in the domain against a local
2366 LDAP server.
2367
2368 Example3:
2369 samba-tool user edit User3 --editor=nano
2370
2371 Example3 shows how to edit a users attributes in the domain against a local
2372 LDAP server using the 'nano' editor.
2373
2374 """
2375     synopsis = "%prog <username> [options]"
2376
2377     takes_options = [
2378         Option("-H", "--URL", help="LDB URL for database or target server",
2379                type=str, metavar="URL", dest="H"),
2380         Option("--editor", help="Editor to use instead of the system default,"
2381                " or 'vi' if no system default is set.", type=str),
2382     ]
2383
2384     takes_args = ["username"]
2385     takes_optiongroups = {
2386         "sambaopts": options.SambaOptions,
2387         "credopts": options.CredentialsOptions,
2388         "versionopts": options.VersionOptions,
2389     }
2390
2391     def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2392             H=None, editor=None):
2393
2394         lp = sambaopts.get_loadparm()
2395         creds = credopts.get_credentials(lp, fallback_machine=True)
2396         samdb = SamDB(url=H, session_info=system_session(),
2397                       credentials=creds, lp=lp)
2398
2399         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2400                   (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2401
2402         domaindn = samdb.domain_dn()
2403
2404         try:
2405             res = samdb.search(base=domaindn,
2406                                expression=filter,
2407                                scope=ldb.SCOPE_SUBTREE)
2408             user_dn = res[0].dn
2409         except IndexError:
2410             raise CommandError('Unable to find user "%s"' % (username))
2411
2412         for msg in res:
2413             r_ldif = samdb.write_ldif(msg, 1)
2414             # remove 'changetype' line
2415             result_ldif = re.sub('changetype: add\n', '', r_ldif)
2416
2417             if editor is None:
2418                 editor = os.environ.get('EDITOR')
2419                 if editor is None:
2420                     editor = 'vi'
2421
2422             with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2423                 t_file.write(result_ldif)
2424                 t_file.flush()
2425                 try:
2426                     check_call([editor, t_file.name])
2427                 except CalledProcessError as e:
2428                     raise CalledProcessError("ERROR: ", e)
2429                 with open(t_file.name) as edited_file:
2430                     edited_message = edited_file.read()
2431
2432         if result_ldif != edited_message:
2433             diff = difflib.ndiff(result_ldif.splitlines(),
2434                                  edited_message.splitlines())
2435             minus_lines = []
2436             plus_lines = []
2437             for line in diff:
2438                 if line.startswith('-'):
2439                     line = line[2:]
2440                     minus_lines.append(line)
2441                 elif line.startswith('+'):
2442                     line = line[2:]
2443                     plus_lines.append(line)
2444
2445             user_ldif = "dn: %s\n" % user_dn
2446             user_ldif += "changetype: modify\n"
2447
2448             for line in minus_lines:
2449                 attr, val = line.split(':', 1)
2450                 search_attr = "%s:" % attr
2451                 if not re.search(r'^' + search_attr, str(plus_lines)):
2452                     user_ldif += "delete: %s\n" % attr
2453                     user_ldif += "%s: %s\n" % (attr, val)
2454
2455             for line in plus_lines:
2456                 attr, val = line.split(':', 1)
2457                 search_attr = "%s:" % attr
2458                 if re.search(r'^' + search_attr, str(minus_lines)):
2459                     user_ldif += "replace: %s\n" % attr
2460                     user_ldif += "%s: %s\n" % (attr, val)
2461                 if not re.search(r'^' + search_attr, str(minus_lines)):
2462                     user_ldif += "add: %s\n" % attr
2463                     user_ldif += "%s: %s\n" % (attr, val)
2464
2465             try:
2466                 samdb.modify_ldif(user_ldif)
2467             except Exception as e:
2468                 raise CommandError("Failed to modify user '%s': " %
2469                                    username, e)
2470
2471             self.outf.write("Modified User '%s' successfully\n" % username)
2472
2473
2474 class cmd_user_show(Command):
2475     """Display a user AD object.
2476
2477 This command displays a user account and it's attributes in the Active
2478 Directory domain.
2479 The username specified on the command is the sAMAccountName.
2480
2481 The command may be run from the root userid or another authorized userid.
2482
2483 The -H or --URL= option can be used to execute the command against a remote
2484 server.
2485
2486 Example1:
2487 samba-tool user show User1 -H ldap://samba.samdom.example.com \
2488 -U administrator --password=passw1rd
2489
2490 Example1 shows how to display a users attributes in the domain against a remote
2491 LDAP server.
2492
2493 The -H parameter is used to specify the remote target server.
2494
2495 Example2:
2496 samba-tool user show User2
2497
2498 Example2 shows how to display a users attributes in the domain against a local
2499 LDAP server.
2500
2501 Example3:
2502 samba-tool user show User2 --attributes=objectSid,memberOf
2503
2504 Example3 shows how to display a users objectSid and memberOf attributes.
2505 """
2506     synopsis = "%prog <username> [options]"
2507
2508     takes_options = [
2509         Option("-H", "--URL", help="LDB URL for database or target server",
2510                type=str, metavar="URL", dest="H"),
2511         Option("--attributes",
2512                help=("Comma separated list of attributes, "
2513                      "which will be printed."),
2514                type=str, dest="user_attrs"),
2515     ]
2516
2517     takes_args = ["username"]
2518     takes_optiongroups = {
2519         "sambaopts": options.SambaOptions,
2520         "credopts": options.CredentialsOptions,
2521         "versionopts": options.VersionOptions,
2522     }
2523
2524     def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2525             H=None, user_attrs=None):
2526
2527         lp = sambaopts.get_loadparm()
2528         creds = credopts.get_credentials(lp, fallback_machine=True)
2529         samdb = SamDB(url=H, session_info=system_session(),
2530                       credentials=creds, lp=lp)
2531
2532         attrs = None
2533         if user_attrs:
2534             attrs = user_attrs.split(",")
2535
2536         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2537                   (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2538
2539         domaindn = samdb.domain_dn()
2540
2541         try:
2542             res = samdb.search(base=domaindn, expression=filter,
2543                                scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2544             user_dn = res[0].dn
2545         except IndexError:
2546             raise CommandError('Unable to find user "%s"' % (username))
2547
2548         for msg in res:
2549             user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2550             self.outf.write(user_ldif)
2551
2552
2553 class cmd_user_move(Command):
2554     """Move a user to an organizational unit/container.
2555
2556     This command moves a user account into the specified organizational unit
2557     or container.
2558     The username specified on the command is the sAMAccountName.
2559     The name of the organizational unit or container can be specified as a
2560     full DN or without the domainDN component.
2561
2562     The command may be run from the root userid or another authorized userid.
2563
2564     The -H or --URL= option can be used to execute the command against a remote
2565     server.
2566
2567     Example1:
2568     samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
2569         -H ldap://samba.samdom.example.com -U administrator
2570
2571     Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2572     unit on a remote LDAP server.
2573
2574     The -H parameter is used to specify the remote target server.
2575
2576     Example2:
2577     samba-tool user move User1 CN=Users
2578
2579     Example2 shows how to move a user User1 back into the CN=Users container
2580     on the local server.
2581     """
2582
2583     synopsis = "%prog <username> <new_parent_dn> [options]"
2584
2585     takes_options = [
2586         Option("-H", "--URL", help="LDB URL for database or target server",
2587                type=str, metavar="URL", dest="H"),
2588     ]
2589
2590     takes_args = ["username", "new_parent_dn"]
2591     takes_optiongroups = {
2592         "sambaopts": options.SambaOptions,
2593         "credopts": options.CredentialsOptions,
2594         "versionopts": options.VersionOptions,
2595     }
2596
2597     def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2598             versionopts=None, H=None):
2599         lp = sambaopts.get_loadparm()
2600         creds = credopts.get_credentials(lp, fallback_machine=True)
2601         samdb = SamDB(url=H, session_info=system_session(),
2602                       credentials=creds, lp=lp)
2603         domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2604
2605         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2606                   (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2607         try:
2608             res = samdb.search(base=domain_dn,
2609                                expression=filter,
2610                                scope=ldb.SCOPE_SUBTREE)
2611             user_dn = res[0].dn
2612         except IndexError:
2613             raise CommandError('Unable to find user "%s"' % (username))
2614
2615         try:
2616             full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2617         except Exception as e:
2618             raise CommandError('Invalid new_parent_dn "%s": %s' %
2619                                (new_parent_dn, e))
2620
2621         full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2622         full_new_user_dn.remove_base_components(len(user_dn) - 1)
2623         full_new_user_dn.add_base(full_new_parent_dn)
2624
2625         try:
2626             samdb.rename(user_dn, full_new_user_dn)
2627         except Exception as e:
2628             raise CommandError('Failed to move user "%s"' % username, e)
2629         self.outf.write('Moved user "%s" into "%s"\n' %
2630                         (username, full_new_parent_dn))
2631
2632
2633 class cmd_user(SuperCommand):
2634     """User management."""
2635
2636     subcommands = {}
2637     subcommands["add"] = cmd_user_add()
2638     subcommands["create"] = cmd_user_create()
2639     subcommands["delete"] = cmd_user_delete()
2640     subcommands["disable"] = cmd_user_disable()
2641     subcommands["enable"] = cmd_user_enable()
2642     subcommands["list"] = cmd_user_list()
2643     subcommands["setexpiry"] = cmd_user_setexpiry()
2644     subcommands["password"] = cmd_user_password()
2645     subcommands["setpassword"] = cmd_user_setpassword()
2646     subcommands["getpassword"] = cmd_user_getpassword()
2647     subcommands["syncpasswords"] = cmd_user_syncpasswords()
2648     subcommands["edit"] = cmd_user_edit()
2649     subcommands["show"] = cmd_user_show()
2650     subcommands["move"] = cmd_user_move()