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