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