samba-tool: add new "user unlock" command
[bbaumbach/samba-autobuild/.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 io
25 import re
26 import difflib
27 import fcntl
28 import signal
29 import errno
30 import time
31 import base64
32 import binascii
33 from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError
34 from getpass import getpass
35 from samba.auth import system_session
36 from samba.samdb import SamDB, SamDBError, SamDBNotFoundError
37 from samba.dcerpc import misc
38 from samba.dcerpc import security
39 from samba.dcerpc import drsblobs
40 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
41 from samba import (
42     credentials,
43     dsdb,
44     gensec,
45     generate_random_password,
46     Ldb,
47 )
48 from samba.net import Net
49
50 from samba.netcmd import (
51     Command,
52     CommandError,
53     SuperCommand,
54     Option,
55 )
56 from samba.common import get_bytes
57 from samba.common import get_string
58 from . import common
59
60 # python[3]-gpgme is abandoned since ubuntu 1804 and debian 9
61 # have to use python[3]-gpg instead
62 # The API is different, need to adapt.
63
64 def _gpgme_decrypt(encrypted_bytes):
65     """
66     Use python[3]-gpgme to decrypt GPG.
67     """
68     ctx = gpgme.Context()
69     ctx.armor = True  # use ASCII-armored
70     out = io.BytesIO()
71     ctx.decrypt(io.BytesIO(encrypted_bytes), out)
72     return out.getvalue()
73
74
75 def _gpg_decrypt(encrypted_bytes):
76     """
77     Use python[3]-gpg to decrypt GPG.
78     """
79     ciphertext = gpg.Data(string=encrypted_bytes)
80     ctx = gpg.Context(armor=True)
81     # plaintext, result, verify_result
82     plaintext, _, _ = ctx.decrypt(ciphertext)
83     return plaintext
84
85
86 gpg_decrypt = None
87
88 if not gpg_decrypt:
89     try:
90         import gpgme
91         gpg_decrypt = _gpgme_decrypt
92     except ImportError:
93         pass
94
95 if not gpg_decrypt:
96     try:
97         import gpg
98         gpg_decrypt = _gpg_decrypt
99     except ImportError:
100         pass
101
102 if gpg_decrypt:
103     decrypt_samba_gpg_help = ("Decrypt the SambaGPG password as "
104                               "cleartext source")
105 else:
106     decrypt_samba_gpg_help = ("Decrypt the SambaGPG password not supported, "
107                               "python[3]-gpgme or python[3]-gpg required")
108
109
110 disabled_virtual_attributes = {
111 }
112
113 virtual_attributes = {
114     "virtualClearTextUTF8": {
115         "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
116     },
117     "virtualClearTextUTF16": {
118         "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
119     },
120     "virtualSambaGPG": {
121         "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
122     },
123 }
124
125
126 def get_crypt_value(alg, utf8pw, rounds=0):
127     algs = {
128         "5": {"length": 43},
129         "6": {"length": 86},
130     }
131     assert alg in algs
132     salt = os.urandom(16)
133     # The salt needs to be in [A-Za-z0-9./]
134     # base64 is close enough and as we had 16
135     # random bytes but only need 16 characters
136     # we can ignore the possible == at the end
137     # of the base64 string
138     # we just need to replace '+' by '.'
139     b64salt = base64.b64encode(salt)[0:16].replace(b'+', b'.').decode('utf8')
140     crypt_salt = ""
141     if rounds != 0:
142         crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
143     else:
144         crypt_salt = "$%s$%s$" % (alg, b64salt)
145
146     crypt_value = crypt.crypt(utf8pw, crypt_salt)
147     if crypt_value is None:
148         raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
149     expected_len = len(crypt_salt) + algs[alg]["length"]
150     if len(crypt_value) != expected_len:
151         raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
152             crypt_salt, len(crypt_value), expected_len))
153     return crypt_value
154
155 # Extract the rounds value from the options of a virtualCrypt attribute
156 # i.e. options = "rounds=20;other=ignored;" will return 20
157 # if the rounds option is not found or the value is not a number, 0 is returned
158 # which indicates that the default number of rounds should be used.
159
160
161 def get_rounds(options):
162     if not options:
163         return 0
164
165     opts = options.split(';')
166     for o in opts:
167         if o.lower().startswith("rounds="):
168             (key, _, val) = o.partition('=')
169             try:
170                 return int(val)
171             except ValueError:
172                 return 0
173     return 0
174
175
176 try:
177     import hashlib
178     h = hashlib.sha1()
179     h = None
180     virtual_attributes["virtualSSHA"] = {
181     }
182 except ImportError as e:
183     reason = "hashlib.sha1()"
184     reason += " required"
185     disabled_virtual_attributes["virtualSSHA"] = {
186         "reason": reason,
187     }
188
189 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
190     try:
191         import crypt
192         v = get_crypt_value(alg, "")
193         v = None
194         virtual_attributes[attr] = {
195         }
196     except ImportError as e:
197         reason = "crypt"
198         reason += " required"
199         disabled_virtual_attributes[attr] = {
200             "reason": reason,
201         }
202     except NotImplementedError as e:
203         reason = "modern '$%s$' salt in crypt(3) required" % (alg)
204         disabled_virtual_attributes[attr] = {
205             "reason": reason,
206         }
207
208 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
209 for x in range(1, 30):
210     virtual_attributes["virtualWDigest%02d" % x] = {}
211
212 # Add Kerberos virtual attributes
213 virtual_attributes["virtualKerberosSalt"] = {}
214
215 virtual_attributes_help  = "The attributes to display (comma separated). "
216 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
217 if len(disabled_virtual_attributes) != 0:
218     virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
219
220
221 class cmd_user_add(Command):
222     """Add a new user.
223
224 This command adds a new user account to the Active Directory domain.  The username specified on the command is the sAMaccountName.
225
226 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).
227
228 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.
229
230 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.
231
232 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.
233
234 Example1:
235 samba-tool user add User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
236
237 Example1 shows how to add a new user to 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.
238
239 Example2:
240 sudo samba-tool user add User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
241
242 Example2 shows how to add a new user to 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.
243
244 Example3:
245 samba-tool user add User3 passw3rd --userou='OU=OrgUnit'
246
247 Example3 shows how to add a new user in the OrgUnit organizational unit.
248
249 Example4:
250 samba-tool user add User4 passw4rd --rfc2307-from-nss --gecos 'some text'
251
252 Example4 shows how to add a new user with Unix UID, GID and login-shell set from the local NSS and GECOS set to 'some text'.
253
254 Example5:
255 samba-tool user add User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \\
256     --uid-number=10005 --login-shell=/bin/false --gid-number=10000
257
258 Example5 shows how to add a new RFC2307/NIS domain enabled user account. If
259 --nis-domain is set, then the other four parameters are mandatory.
260
261 """
262     synopsis = "%prog <username> [<password>] [options]"
263
264     takes_options = [
265         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
266                metavar="URL", dest="H"),
267         Option("--must-change-at-next-login",
268                help="Force password to be changed on next login",
269                action="store_true"),
270         Option("--random-password",
271                help="Generate random password",
272                action="store_true"),
273         Option("--smartcard-required",
274                help="Require a smartcard for interactive logons",
275                action="store_true"),
276         Option("--use-username-as-cn",
277                help="Force use of username as user's CN",
278                action="store_true"),
279         Option("--userou",
280                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>'",
281                type=str),
282         Option("--surname", help="User's surname", type=str),
283         Option("--given-name", help="User's given name", type=str),
284         Option("--initials", help="User's initials", type=str),
285         Option("--profile-path", help="User's profile path", type=str),
286         Option("--script-path", help="User's logon script path", type=str),
287         Option("--home-drive", help="User's home drive letter", type=str),
288         Option("--home-directory", help="User's home directory path", type=str),
289         Option("--job-title", help="User's job title", type=str),
290         Option("--department", help="User's department", type=str),
291         Option("--company", help="User's company", type=str),
292         Option("--description", help="User's description", type=str),
293         Option("--mail-address", help="User's email address", type=str),
294         Option("--internet-address", help="User's home page", type=str),
295         Option("--telephone-number", help="User's phone number", type=str),
296         Option("--physical-delivery-office", help="User's office location", type=str),
297         Option("--rfc2307-from-nss",
298                help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
299                action="store_true"),
300         Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
301         Option("--unix-home", help="User's Unix/RFC2307 home directory",
302                type=str),
303         Option("--uid", help="User's Unix/RFC2307 username", type=str),
304         Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
305         Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
306         Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
307         Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
308     ]
309
310     takes_args = ["username", "password?"]
311
312     takes_optiongroups = {
313         "sambaopts": options.SambaOptions,
314         "credopts": options.CredentialsOptions,
315         "versionopts": options.VersionOptions,
316     }
317
318     def run(self, username, password=None, credopts=None, sambaopts=None,
319             versionopts=None, H=None, must_change_at_next_login=False,
320             random_password=False, use_username_as_cn=False, userou=None,
321             surname=None, given_name=None, initials=None, profile_path=None,
322             script_path=None, home_drive=None, home_directory=None,
323             job_title=None, department=None, company=None, description=None,
324             mail_address=None, internet_address=None, telephone_number=None,
325             physical_delivery_office=None, rfc2307_from_nss=False,
326             nis_domain=None, unix_home=None, uid=None, uid_number=None,
327             gid_number=None, gecos=None, login_shell=None,
328             smartcard_required=False):
329
330         if smartcard_required:
331             if password is not None and password != '':
332                 raise CommandError('It is not allowed to specify '
333                                    '--newpassword '
334                                    'together with --smartcard-required.')
335             if must_change_at_next_login:
336                 raise CommandError('It is not allowed to specify '
337                                    '--must-change-at-next-login '
338                                    'together with --smartcard-required.')
339
340         if random_password and not smartcard_required:
341             password = generate_random_password(128, 255)
342
343         while True:
344             if smartcard_required:
345                 break
346             if password is not None and password != '':
347                 break
348             password = getpass("New Password: ")
349             passwordverify = getpass("Retype Password: ")
350             if not password == passwordverify:
351                 password = None
352                 self.outf.write("Sorry, passwords do not match.\n")
353
354         if rfc2307_from_nss:
355                 pwent = pwd.getpwnam(username)
356                 if uid is None:
357                     uid = username
358                 if uid_number is None:
359                     uid_number = pwent[2]
360                 if gid_number is None:
361                     gid_number = pwent[3]
362                 if gecos is None:
363                     gecos = pwent[4]
364                 if login_shell is None:
365                     login_shell = pwent[6]
366
367         lp = sambaopts.get_loadparm()
368         creds = credopts.get_credentials(lp)
369
370         if uid_number or gid_number:
371             if not lp.get("idmap_ldb:use rfc2307"):
372                 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")
373
374         if nis_domain is not None:
375             if None in (uid_number, login_shell, unix_home, gid_number):
376                 raise CommandError('Missing parameters. To enable NIS features, '
377                                    'the following options have to be given: '
378                                    '--nis-domain=, --uidNumber=, --login-shell='
379                                    ', --unix-home=, --gid-number= Operation '
380                                    'cancelled.')
381
382         try:
383             samdb = SamDB(url=H, session_info=system_session(),
384                           credentials=creds, lp=lp)
385             samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
386                           useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
387                           profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
388                           jobtitle=job_title, department=department, company=company, description=description,
389                           mailaddress=mail_address, internetaddress=internet_address,
390                           telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
391                           nisdomain=nis_domain, unixhome=unix_home, uid=uid,
392                           uidnumber=uid_number, gidnumber=gid_number,
393                           gecos=gecos, loginshell=login_shell,
394                           smartcard_required=smartcard_required)
395         except Exception as e:
396             raise CommandError("Failed to add user '%s': " % username, e)
397
398         self.outf.write("User '%s' added successfully\n" % username)
399
400 class cmd_user_delete(Command):
401     """Delete a user.
402
403 This command deletes a user account from the Active Directory domain.  The username specified on the command is the sAMAccountName.
404
405 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.
406
407 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.
408
409 Example1:
410 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
411
412 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.
413
414 Example2:
415 sudo samba-tool user delete User2
416
417 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.
418
419 """
420     synopsis = "%prog <username> [options]"
421
422     takes_options = [
423         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
424                metavar="URL", dest="H"),
425     ]
426
427     takes_args = ["username"]
428     takes_optiongroups = {
429         "sambaopts": options.SambaOptions,
430         "credopts": options.CredentialsOptions,
431         "versionopts": options.VersionOptions,
432     }
433
434     def run(self, username, credopts=None, sambaopts=None, versionopts=None,
435             H=None):
436         lp = sambaopts.get_loadparm()
437         creds = credopts.get_credentials(lp, fallback_machine=True)
438
439         samdb = SamDB(url=H, session_info=system_session(),
440                       credentials=creds, lp=lp)
441
442         filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
443                   ldb.binary_encode(username))
444
445         try:
446             res = samdb.search(base=samdb.domain_dn(),
447                                scope=ldb.SCOPE_SUBTREE,
448                                expression=filter,
449                                attrs=["dn"])
450             user_dn = res[0].dn
451         except IndexError:
452             raise CommandError('Unable to find user "%s"' % (username))
453
454         try:
455             samdb.delete(user_dn)
456         except Exception as e:
457             raise CommandError('Failed to remove user "%s"' % username, e)
458         self.outf.write("Deleted user %s\n" % username)
459
460
461 class cmd_user_list(Command):
462     """List all users."""
463
464     synopsis = "%prog [options]"
465
466     takes_options = [
467         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
468                metavar="URL", dest="H"),
469         Option("-b", "--base-dn",
470                help="Specify base DN to use",
471                type=str),
472         Option("--full-dn", dest="full_dn",
473                default=False,
474                action='store_true',
475                help="Display DN instead of the sAMAccountName.")
476     ]
477
478     takes_optiongroups = {
479         "sambaopts": options.SambaOptions,
480         "credopts": options.CredentialsOptions,
481         "versionopts": options.VersionOptions,
482     }
483
484     def run(self,
485             sambaopts=None,
486             credopts=None,
487             versionopts=None,
488             H=None,
489             base_dn=None,
490             full_dn=False):
491         lp = sambaopts.get_loadparm()
492         creds = credopts.get_credentials(lp, fallback_machine=True)
493
494         samdb = SamDB(url=H, session_info=system_session(),
495                       credentials=creds, lp=lp)
496
497         search_dn = samdb.domain_dn()
498         if base_dn:
499             search_dn = samdb.normalize_dn_in_domain(base_dn)
500
501         res = samdb.search(search_dn,
502                            scope=ldb.SCOPE_SUBTREE,
503                            expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
504                                        % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
505                            attrs=["samaccountname"])
506         if (len(res) == 0):
507             return
508
509         for msg in res:
510             if full_dn:
511                 self.outf.write("%s\n" % msg.get("dn"))
512                 continue
513
514             self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
515
516
517 class cmd_user_enable(Command):
518     """Enable a user.
519
520 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.
521
522 There are many reasons why an account may become disabled.  These include:
523 - If a user exceeds the account policy for logon attempts
524 - If an administrator disables the account
525 - If the account expires
526
527 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
528
529 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.
530
531 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.
532
533 Example1:
534 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
535
536 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.
537
538 Example2:
539 su samba-tool user enable Testuser2
540
541 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.
542
543 Example3:
544 samba-tool user enable --filter=samaccountname=Testuser3
545
546 Example3 shows how to enable a user in the domain against a local LDAP server.  It uses the --filter=samaccountname to specify the username.
547
548 """
549     synopsis = "%prog (<username>|--filter <filter>) [options]"
550
551     takes_optiongroups = {
552         "sambaopts": options.SambaOptions,
553         "versionopts": options.VersionOptions,
554         "credopts": options.CredentialsOptions,
555     }
556
557     takes_options = [
558         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
559                metavar="URL", dest="H"),
560         Option("--filter", help="LDAP Filter to set password on", type=str),
561     ]
562
563     takes_args = ["username?"]
564
565     def run(self, username=None, sambaopts=None, credopts=None,
566             versionopts=None, filter=None, H=None):
567         if username is None and filter is None:
568             raise CommandError("Either the username or '--filter' must be specified!")
569
570         if filter is None:
571             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
572
573         lp = sambaopts.get_loadparm()
574         creds = credopts.get_credentials(lp, fallback_machine=True)
575
576         samdb = SamDB(url=H, session_info=system_session(),
577                       credentials=creds, lp=lp)
578         try:
579             samdb.enable_account(filter)
580         except Exception as msg:
581             raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
582         self.outf.write("Enabled user '%s'\n" % (username or filter))
583
584
585 class cmd_user_disable(Command):
586     """Disable a user."""
587
588     synopsis = "%prog (<username>|--filter <filter>) [options]"
589
590     takes_options = [
591         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
592                metavar="URL", dest="H"),
593         Option("--filter", help="LDAP Filter to set password on", type=str),
594     ]
595
596     takes_args = ["username?"]
597
598     takes_optiongroups = {
599         "sambaopts": options.SambaOptions,
600         "credopts": options.CredentialsOptions,
601         "versionopts": options.VersionOptions,
602     }
603
604     def run(self, username=None, sambaopts=None, credopts=None,
605             versionopts=None, filter=None, H=None):
606         if username is None and filter is None:
607             raise CommandError("Either the username or '--filter' must be specified!")
608
609         if filter is None:
610             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
611
612         lp = sambaopts.get_loadparm()
613         creds = credopts.get_credentials(lp, fallback_machine=True)
614
615         samdb = SamDB(url=H, session_info=system_session(),
616                       credentials=creds, lp=lp)
617         try:
618             samdb.disable_account(filter)
619         except Exception as msg:
620             raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
621
622
623 class cmd_user_setexpiry(Command):
624     """Set the expiration of a user account.
625
626 The user can either be specified by their sAMAccountName or using the --filter option.
627
628 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.
629
630 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.
631
632 Example1:
633 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
634
635 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.
636
637 Example2:
638 sudo samba-tool user setexpiry User2 --noexpiry
639
640 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.
641
642 Example3:
643 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
644
645 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.
646
647 Example4:
648 samba-tool user setexpiry --noexpiry User4
649 Example4 shows how to set the account expiration so that it will never expire.  The username and sAMAccountName in this example is User4.
650
651 """
652     synopsis = "%prog (<username>|--filter <filter>) [options]"
653
654     takes_optiongroups = {
655         "sambaopts": options.SambaOptions,
656         "versionopts": options.VersionOptions,
657         "credopts": options.CredentialsOptions,
658     }
659
660     takes_options = [
661         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
662                metavar="URL", dest="H"),
663         Option("--filter", help="LDAP Filter to set password on", type=str),
664         Option("--days", help="Days to expiry", type=int, default=0),
665         Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
666     ]
667
668     takes_args = ["username?"]
669
670     def run(self, username=None, sambaopts=None, credopts=None,
671             versionopts=None, H=None, filter=None, days=None, noexpiry=None):
672         if username is None and filter is None:
673             raise CommandError("Either the username or '--filter' must be specified!")
674
675         if filter is None:
676             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
677
678         lp = sambaopts.get_loadparm()
679         creds = credopts.get_credentials(lp)
680
681         samdb = SamDB(url=H, session_info=system_session(),
682                       credentials=creds, lp=lp)
683
684         try:
685             samdb.setexpiry(filter, days * 24 * 3600, no_expiry_req=noexpiry)
686         except Exception as msg:
687             # FIXME: Catch more specific exception
688             raise CommandError("Failed to set expiry for user '%s': %s" % (
689                 username or filter, msg))
690         if noexpiry:
691             self.outf.write("Expiry for user '%s' disabled.\n" % (
692                 username or filter))
693         else:
694             self.outf.write("Expiry for user '%s' set to %u days.\n" % (
695                 username or filter, days))
696
697
698 class cmd_user_password(Command):
699     """Change password for a user account (the one provided in authentication).
700 """
701
702     synopsis = "%prog [options]"
703
704     takes_options = [
705         Option("--newpassword", help="New password", type=str),
706     ]
707
708     takes_optiongroups = {
709         "sambaopts": options.SambaOptions,
710         "credopts": options.CredentialsOptions,
711         "versionopts": options.VersionOptions,
712     }
713
714     def run(self, credopts=None, sambaopts=None, versionopts=None,
715             newpassword=None):
716
717         lp = sambaopts.get_loadparm()
718         creds = credopts.get_credentials(lp)
719
720         # get old password now, to get the password prompts in the right order
721         old_password = creds.get_password()
722
723         net = Net(creds, lp, server=credopts.ipaddress)
724
725         password = newpassword
726         while True:
727             if password is not None and password != '':
728                 break
729             password = getpass("New Password: ")
730             passwordverify = getpass("Retype Password: ")
731             if not password == passwordverify:
732                 password = None
733                 self.outf.write("Sorry, passwords do not match.\n")
734
735         try:
736             if not isinstance(password, str):
737                 password = password.decode('utf8')
738             net.change_password(password)
739         except Exception as msg:
740             # FIXME: catch more specific exception
741             raise CommandError("Failed to change password : %s" % msg)
742         self.outf.write("Changed password OK\n")
743
744
745 class cmd_user_getgroups(Command):
746     """Get the direct group memberships of a user account.
747
748 The username specified on the command is the sAMAccountName."""
749     synopsis = "%prog <username> [options]"
750
751     takes_optiongroups = {
752         "sambaopts": options.SambaOptions,
753         "versionopts": options.VersionOptions,
754         "credopts": options.CredentialsOptions,
755     }
756
757     takes_options = [
758         Option("-H", "--URL", help="LDB URL for database or target server",
759                type=str, metavar="URL", dest="H"),
760         Option("--full-dn", dest="full_dn",
761                default=False,
762                action='store_true',
763                help="Display DN instead of the sAMAccountName."),
764         ]
765
766     takes_args = ["username"]
767
768     def run(self,
769             username,
770             credopts=None,
771             sambaopts=None,
772             versionopts=None,
773             H=None,
774             full_dn=False):
775
776         lp = sambaopts.get_loadparm()
777         creds = credopts.get_credentials(lp)
778
779         samdb = SamDB(url=H, session_info=system_session(),
780                       credentials=creds, lp=lp)
781
782         filter = ("(&(sAMAccountName=%s)(objectClass=user))" %
783                   ldb.binary_encode(username))
784         try:
785             res = samdb.search(base=samdb.domain_dn(),
786                                expression=filter,
787                                scope=ldb.SCOPE_SUBTREE,
788                                attrs=["objectSid",
789                                       "memberOf",
790                                       "primaryGroupID"])
791             user_sid_binary = res[0].get('objectSid', idx=0)
792             user_sid = ndr_unpack(security.dom_sid, user_sid_binary)
793             (user_dom_sid, user_rid) = user_sid.split()
794             user_sid_dn = "<SID=%s>" % user_sid
795             user_pgid = int(res[0].get('primaryGroupID', idx=0))
796             user_groups = res[0].get('memberOf')
797             if user_groups is None:
798                 user_groups = []
799         except IndexError:
800             raise CommandError("Unable to find user '%s'" % (username))
801
802         primarygroup_sid_dn = "<SID=%s-%u>" % (user_dom_sid, user_pgid)
803
804         filter = "(objectClass=group)"
805         try:
806             res = samdb.search(base=primarygroup_sid_dn,
807                                expression=filter,
808                                scope=ldb.SCOPE_BASE,
809                                attrs=['sAMAccountName'])
810             primary_group_dn = str(res[0].dn)
811             primary_group_name = res[0].get('sAMAccountName')
812         except IndexError:
813             raise CommandError("Unable to find primary group '%s'" % (primarygroup_sid_dn))
814
815         if full_dn:
816             self.outf.write("%s\n" % primary_group_dn)
817             for group_dn in user_groups:
818                 self.outf.write("%s\n" % group_dn)
819             return
820
821         group_names = []
822         for gdn in user_groups:
823             try:
824                 res = samdb.search(base=gdn,
825                                    expression=filter,
826                                    scope=ldb.SCOPE_BASE,
827                                    attrs=['sAMAccountName'])
828                 group_names.extend(res[0].get('sAMAccountName'))
829             except IndexError:
830                 raise CommandError("Unable to find group '%s'" % (gdn))
831
832         self.outf.write("%s\n" % primary_group_name)
833         for group_name in group_names:
834             self.outf.write("%s\n" % group_name)
835
836
837 class cmd_user_setprimarygroup(Command):
838     """Set the primary group a user account.
839
840 This command sets the primary group a user account. The username specified on
841 the command is the sAMAccountName. The primarygroupname is the sAMAccountName
842 of the new primary group. The user must be a member of the group.
843
844 The command may be run from the root userid or another authorized userid. The
845 -H or --URL= option can be used to execute the command against a remote server.
846
847 Example1:
848 samba-tool user setprimarygroup TestUser1 newPrimaryGroup --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
849
850 Example1 shows how to set the primary group for TestUser1 on a remote LDAP
851 server. The --URL parameter is used to specify the remote target server.  The
852 -U option is used to pass the username and password of a user that exists on
853 the remote server and is authorized to update the server.
854 """
855     synopsis = "%prog <username> <primarygroupname> [options]"
856
857     takes_optiongroups = {
858         "sambaopts": options.SambaOptions,
859         "versionopts": options.VersionOptions,
860         "credopts": options.CredentialsOptions,
861     }
862
863     takes_options = [
864         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
865                metavar="URL", dest="H"),
866         ]
867
868     takes_args = ["username", "primarygroupname"]
869
870     def run(self, username, primarygroupname, credopts=None, sambaopts=None,
871             versionopts=None, H=None):
872
873         lp = sambaopts.get_loadparm()
874         creds = credopts.get_credentials(lp)
875
876         samdb = SamDB(url=H, session_info=system_session(),
877                       credentials=creds, lp=lp)
878
879         filter = ("(&(sAMAccountName=%s)(objectClass=user))" %
880                   ldb.binary_encode(username))
881         try:
882             res = samdb.search(base=samdb.domain_dn(),
883                                expression=filter,
884                                scope=ldb.SCOPE_SUBTREE,
885                                controls=["extended_dn:1:1"],
886                                attrs=["objectSid",
887                                       "memberOf",
888                                       "primaryGroupID"])
889             user_sid_binary = res[0].get('objectSid', idx=0)
890             user_sid = ndr_unpack(security.dom_sid, user_sid_binary)
891             (user_dom_sid, user_rid) = user_sid.split()
892             user_sid_dn = "<SID=%s>" % user_sid
893             user_pgid = int(res[0].get('primaryGroupID', idx=0))
894             user_groups = res[0].get('memberOf')
895             if user_groups is None:
896                 user_groups = []
897         except IndexError:
898             raise CommandError("Unable to find user '%s'" % (username))
899
900         user_group_sids = []
901         for user_group in user_groups:
902             user_group_dn = ldb.Dn(samdb, str(user_group))
903             user_group_binary_sid = user_group_dn.get_extended_component("SID")
904             user_group_sid = ndr_unpack(security.dom_sid, user_group_binary_sid)
905             user_group_sids.append(user_group_sid)
906
907         filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
908                   ldb.binary_encode(primarygroupname))
909         try:
910             res = samdb.search(base=samdb.domain_dn(),
911                                expression=filter,
912                                scope=ldb.SCOPE_SUBTREE,
913                                attrs=["objectSid"])
914             group_sid_binary = res[0].get('objectSid', idx=0)
915         except IndexError:
916             raise CommandError("Unable to find group '%s'" % (primarygroupname))
917
918         primarygroup_sid = ndr_unpack(security.dom_sid, group_sid_binary)
919         (primarygroup_dom_sid, primarygroup_rid) = primarygroup_sid.split()
920
921         if user_dom_sid != primarygroup_dom_sid:
922             raise CommandError("Group '%s' does not belong to the user's "
923                                "domain" % primarygroupname)
924
925         if primarygroup_rid != user_pgid and primarygroup_sid not in user_group_sids:
926             raise CommandError("User '%s' is not member of group '%s'" %
927                                (username, primarygroupname))
928
929         setprimarygroup_ldif = """
930 dn: %s
931 changetype: modify
932 delete: primaryGroupID
933 primaryGroupID: %u
934 add: primaryGroupID
935 primaryGroupID: %u
936 """ % (user_sid_dn, user_pgid, primarygroup_rid)
937
938         try:
939             samdb.modify_ldif(setprimarygroup_ldif)
940         except Exception as msg:
941             raise CommandError("Failed to set primary group '%s' "
942                                "for user '%s': %s" %
943                                (primarygroupname, username, msg))
944         self.outf.write("Changed primary group to '%s'\n" % primarygroupname)
945
946
947 class cmd_user_setpassword(Command):
948     """Set or reset the password of a user account.
949
950 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.
951
952 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.
953
954 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.
955
956 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.
957
958 Example1:
959 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
960
961 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.
962
963 Example2:
964 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
965
966 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.
967
968 Example3:
969 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
970
971 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
972
973 """
974     synopsis = "%prog (<username>|--filter <filter>) [options]"
975
976     takes_optiongroups = {
977         "sambaopts": options.SambaOptions,
978         "versionopts": options.VersionOptions,
979         "credopts": options.CredentialsOptions,
980     }
981
982     takes_options = [
983         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
984                metavar="URL", dest="H"),
985         Option("--filter", help="LDAP Filter to set password on", type=str),
986         Option("--newpassword", help="Set password", type=str),
987         Option("--must-change-at-next-login",
988                help="Force password to be changed on next login",
989                action="store_true"),
990         Option("--random-password",
991                help="Generate random password",
992                action="store_true"),
993         Option("--smartcard-required",
994                help="Require a smartcard for interactive logons",
995                action="store_true"),
996         Option("--clear-smartcard-required",
997                help="Don't require a smartcard for interactive logons",
998                action="store_true"),
999     ]
1000
1001     takes_args = ["username?"]
1002
1003     def run(self, username=None, filter=None, credopts=None, sambaopts=None,
1004             versionopts=None, H=None, newpassword=None,
1005             must_change_at_next_login=False, random_password=False,
1006             smartcard_required=False, clear_smartcard_required=False):
1007         if filter is None and username is None:
1008             raise CommandError("Either the username or '--filter' must be specified!")
1009
1010         password = newpassword
1011
1012         if smartcard_required:
1013             if password is not None and password != '':
1014                 raise CommandError('It is not allowed to specify '
1015                                    '--newpassword '
1016                                    'together with --smartcard-required.')
1017             if must_change_at_next_login:
1018                 raise CommandError('It is not allowed to specify '
1019                                    '--must-change-at-next-login '
1020                                    'together with --smartcard-required.')
1021             if clear_smartcard_required:
1022                 raise CommandError('It is not allowed to specify '
1023                                    '--clear-smartcard-required '
1024                                    'together with --smartcard-required.')
1025
1026         if random_password and not smartcard_required:
1027             password = generate_random_password(128, 255)
1028
1029         while True:
1030             if smartcard_required:
1031                 break
1032             if password is not None and password != '':
1033                 break
1034             password = getpass("New Password: ")
1035             passwordverify = getpass("Retype Password: ")
1036             if not password == passwordverify:
1037                 password = None
1038                 self.outf.write("Sorry, passwords do not match.\n")
1039
1040         if filter is None:
1041             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1042
1043         lp = sambaopts.get_loadparm()
1044         creds = credopts.get_credentials(lp)
1045
1046         creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
1047
1048         samdb = SamDB(url=H, session_info=system_session(),
1049                       credentials=creds, lp=lp)
1050
1051         if smartcard_required:
1052             command = ""
1053             try:
1054                 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
1055                 flags = dsdb.UF_SMARTCARD_REQUIRED
1056                 samdb.toggle_userAccountFlags(filter, flags, on=True)
1057                 command = "Failed to enable account for user '%s'" % (username or filter)
1058                 samdb.enable_account(filter)
1059             except Exception as msg:
1060                 # FIXME: catch more specific exception
1061                 raise CommandError("%s: %s" % (command, msg))
1062             self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
1063         else:
1064             command = ""
1065             try:
1066                 if clear_smartcard_required:
1067                     command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
1068                     flags = dsdb.UF_SMARTCARD_REQUIRED
1069                     samdb.toggle_userAccountFlags(filter, flags, on=False)
1070                 command = "Failed to set password for user '%s'" % (username or filter)
1071                 samdb.setpassword(filter, password,
1072                                   force_change_at_next_login=must_change_at_next_login,
1073                                   username=username)
1074             except Exception as msg:
1075                 # FIXME: catch more specific exception
1076                 raise CommandError("%s: %s" % (command, msg))
1077             self.outf.write("Changed password OK\n")
1078
1079
1080 class GetPasswordCommand(Command):
1081
1082     def __init__(self):
1083         super(GetPasswordCommand, self).__init__()
1084         self.lp = None
1085
1086     def connect_system_samdb(self, url, allow_local=False, verbose=False):
1087
1088         # using anonymous here, results in no authentication
1089         # which means we can get system privileges via
1090         # the privileged ldapi socket
1091         creds = credentials.Credentials()
1092         creds.set_anonymous()
1093
1094         if url is None and allow_local:
1095             pass
1096         elif url.lower().startswith("ldapi://"):
1097             pass
1098         elif url.lower().startswith("ldap://"):
1099             raise CommandError("--url ldap:// is not supported for this command")
1100         elif url.lower().startswith("ldaps://"):
1101             raise CommandError("--url ldaps:// is not supported for this command")
1102         elif not allow_local:
1103             raise CommandError("--url requires an ldapi:// url for this command")
1104
1105         if verbose:
1106             self.outf.write("Connecting to '%s'\n" % url)
1107
1108         samdb = SamDB(url=url, session_info=system_session(),
1109                       credentials=creds, lp=self.lp)
1110
1111         try:
1112             #
1113             # Make sure we're connected as SYSTEM
1114             #
1115             res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
1116             assert len(res) == 1
1117             sids = res[0].get("tokenGroups")
1118             assert len(sids) == 1
1119             sid = ndr_unpack(security.dom_sid, sids[0])
1120             assert str(sid) == security.SID_NT_SYSTEM
1121         except Exception as msg:
1122             raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
1123                                (security.SID_NT_SYSTEM))
1124
1125         # We use sort here in order to have a predictable processing order
1126         # this might not be strictly needed, but also doesn't hurt here
1127         for a in sorted(virtual_attributes.keys()):
1128             flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
1129             samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
1130
1131         return samdb
1132
1133     def get_account_attributes(self, samdb, username, basedn, filter, scope,
1134                                attrs, decrypt):
1135
1136         raw_attrs = attrs[:]
1137         search_attrs = []
1138         attr_opts = {}
1139         for a in raw_attrs:
1140             (attr, _, opts) = a.partition(';')
1141             if opts:
1142                 attr_opts[attr] = opts
1143             else:
1144                 attr_opts[attr] = None
1145             search_attrs.append(attr)
1146         lower_attrs = [x.lower() for x in search_attrs]
1147
1148         require_supplementalCredentials = False
1149         for a in virtual_attributes.keys():
1150             if a.lower() in lower_attrs:
1151                 require_supplementalCredentials = True
1152         add_supplementalCredentials = False
1153         add_unicodePwd = False
1154         if require_supplementalCredentials:
1155             a = "supplementalCredentials"
1156             if a.lower() not in lower_attrs:
1157                 search_attrs += [a]
1158                 add_supplementalCredentials = True
1159             a = "unicodePwd"
1160             if a.lower() not in lower_attrs:
1161                 search_attrs += [a]
1162                 add_unicodePwd = True
1163         add_sAMAcountName = False
1164         a = "sAMAccountName"
1165         if a.lower() not in lower_attrs:
1166             search_attrs += [a]
1167             add_sAMAcountName = True
1168
1169         add_userPrincipalName = False
1170         upn = "userPrincipalName"
1171         if upn.lower() not in lower_attrs:
1172             search_attrs += [upn]
1173             add_userPrincipalName = True
1174
1175         if scope == ldb.SCOPE_BASE:
1176             search_controls = ["show_deleted:1", "show_recycled:1"]
1177         else:
1178             search_controls = []
1179         try:
1180             res = samdb.search(base=basedn, expression=filter,
1181                                scope=scope, attrs=search_attrs,
1182                                controls=search_controls)
1183             if len(res) == 0:
1184                 raise Exception('Unable to find user "%s"' % (username or filter))
1185             if len(res) > 1:
1186                 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
1187         except Exception as msg:
1188             # FIXME: catch more specific exception
1189             raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
1190         obj = res[0]
1191
1192         sc = None
1193         unicodePwd = None
1194         if "supplementalCredentials" in obj:
1195             sc_blob = obj["supplementalCredentials"][0]
1196             sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
1197             if add_supplementalCredentials:
1198                 del obj["supplementalCredentials"]
1199         if "unicodePwd" in obj:
1200             unicodePwd = obj["unicodePwd"][0]
1201             if add_unicodePwd:
1202                 del obj["unicodePwd"]
1203         account_name = str(obj["sAMAccountName"][0])
1204         if add_sAMAcountName:
1205             del obj["sAMAccountName"]
1206         if "userPrincipalName" in obj:
1207             account_upn = str(obj["userPrincipalName"][0])
1208         else:
1209             realm = self.lp.get("realm")
1210             account_upn = "%s@%s" % (account_name, realm.lower())
1211         if add_userPrincipalName:
1212             del obj["userPrincipalName"]
1213
1214         calculated = {}
1215
1216         def get_package(name, min_idx=0):
1217             if name in calculated:
1218                 return calculated[name]
1219             if sc is None:
1220                 return None
1221             if min_idx < 0:
1222                 min_idx = len(sc.sub.packages) + min_idx
1223             idx = 0
1224             for p in sc.sub.packages:
1225                 idx += 1
1226                 if idx <= min_idx:
1227                     continue
1228                 if name != p.name:
1229                     continue
1230
1231                 return binascii.a2b_hex(p.data)
1232             return None
1233
1234         if decrypt:
1235             #
1236             # Samba adds 'Primary:SambaGPG' at the end.
1237             # When Windows sets the password it keeps
1238             # 'Primary:SambaGPG' and rotates it to
1239             # the begining. So we can only use the value,
1240             # if it is the last one.
1241             #
1242             # In order to get more protection we verify
1243             # the nthash of the decrypted utf16 password
1244             # against the stored nthash in unicodePwd.
1245             #
1246             sgv = get_package("Primary:SambaGPG", min_idx=-1)
1247             if sgv is not None and unicodePwd is not None:
1248                 try:
1249                     cv = gpg_decrypt(sgv)
1250                     #
1251                     # We only use the password if it matches
1252                     # the current nthash stored in the unicodePwd
1253                     # attribute
1254                     #
1255                     tmp = credentials.Credentials()
1256                     tmp.set_anonymous()
1257                     tmp.set_utf16_password(cv)
1258                     nthash = tmp.get_nt_hash()
1259                     if nthash == unicodePwd:
1260                         calculated["Primary:CLEARTEXT"] = cv
1261
1262                 except Exception as e:
1263                     self.outf.write(
1264                         "WARNING: '%s': SambaGPG can't be decrypted "
1265                         "into CLEARTEXT: %s\n" % (
1266                             username or account_name, e))
1267
1268
1269         def get_utf8(a, b, username):
1270             try:
1271                 u = str(get_bytes(b), 'utf-16-le')
1272             except UnicodeDecodeError as e:
1273                 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1274                                 username, a))
1275                 return None
1276             u8 = u.encode('utf-8')
1277             return u8
1278
1279         # Extract the WDigest hash for the value specified by i.
1280         # Builds an htdigest compatible value
1281         DIGEST = "Digest"
1282
1283         def get_wDigest(i, primary_wdigest, account_name, account_upn,
1284                         domain, dns_domain):
1285             if i == 1:
1286                 user  = account_name
1287                 realm = domain
1288             elif i == 2:
1289                 user  = account_name.lower()
1290                 realm = domain.lower()
1291             elif i == 3:
1292                 user  = account_name.upper()
1293                 realm = domain.upper()
1294             elif i == 4:
1295                 user  = account_name
1296                 realm = domain.upper()
1297             elif i == 5:
1298                 user  = account_name
1299                 realm = domain.lower()
1300             elif i == 6:
1301                 user  = account_name.upper()
1302                 realm = domain.lower()
1303             elif i == 7:
1304                 user  = account_name.lower()
1305                 realm = domain.upper()
1306             elif i == 8:
1307                 user  = account_name
1308                 realm = dns_domain.lower()
1309             elif i == 9:
1310                 user  = account_name.lower()
1311                 realm = dns_domain.lower()
1312             elif i == 10:
1313                 user  = account_name.upper()
1314                 realm = dns_domain.upper()
1315             elif i == 11:
1316                 user  = account_name
1317                 realm = dns_domain.upper()
1318             elif i == 12:
1319                 user  = account_name
1320                 realm = dns_domain.lower()
1321             elif i == 13:
1322                 user  = account_name.upper()
1323                 realm = dns_domain.lower()
1324             elif i == 14:
1325                 user  = account_name.lower()
1326                 realm = dns_domain.upper()
1327             elif i == 15:
1328                 user  = account_upn
1329                 realm = ""
1330             elif i == 16:
1331                 user  = account_upn.lower()
1332                 realm = ""
1333             elif i == 17:
1334                 user  = account_upn.upper()
1335                 realm = ""
1336             elif i == 18:
1337                 user  = "%s\\%s" % (domain, account_name)
1338                 realm = ""
1339             elif i == 19:
1340                 user  = "%s\\%s" % (domain.lower(), account_name.lower())
1341                 realm = ""
1342             elif i == 20:
1343                 user  = "%s\\%s" % (domain.upper(), account_name.upper())
1344                 realm = ""
1345             elif i == 21:
1346                 user  = account_name
1347                 realm = DIGEST
1348             elif i == 22:
1349                 user  = account_name.lower()
1350                 realm = DIGEST
1351             elif i == 23:
1352                 user  = account_name.upper()
1353                 realm = DIGEST
1354             elif i == 24:
1355                 user  = account_upn
1356                 realm = DIGEST
1357             elif i == 25:
1358                 user  = account_upn.lower()
1359                 realm = DIGEST
1360             elif i == 26:
1361                 user  = account_upn.upper()
1362                 realm = DIGEST
1363             elif i == 27:
1364                 user  = "%s\\%s" % (domain, account_name)
1365                 realm = DIGEST
1366             elif i == 28:
1367                 # Differs from spec, see tests
1368                 user  = "%s\\%s" % (domain.lower(), account_name.lower())
1369                 realm = DIGEST
1370             elif i == 29:
1371                 # Differs from spec, see tests
1372                 user  = "%s\\%s" % (domain.upper(), account_name.upper())
1373                 realm = DIGEST
1374             else:
1375                 user  = ""
1376
1377             digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1378                                  primary_wdigest)
1379             try:
1380                 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
1381                 return "%s:%s:%s" % (user, realm, get_string(digest))
1382             except IndexError:
1383                 return None
1384
1385         # get the value for a virtualCrypt attribute.
1386         # look for an exact match on algorithm and rounds in supplemental creds
1387         # if not found calculate using Primary:CLEARTEXT
1388         # if no Primary:CLEARTEXT return the first supplementalCredential
1389         #    that matches the algorithm.
1390         def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1391             sv = None
1392             fb = None
1393             b = get_package("Primary:userPassword")
1394             if b is not None:
1395                 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1396             if sv is None:
1397                 # No exact match on algorithm and number of rounds
1398                 # try and calculate one from the Primary:CLEARTEXT
1399                 b = get_package("Primary:CLEARTEXT")
1400                 if b is not None:
1401                     u8 = get_utf8(a, b, username or account_name)
1402                     if u8 is not None:
1403                         # in py2 using get_bytes should ensure u8 is unmodified
1404                         # in py3 it will be decoded
1405                         sv = get_crypt_value(str(algorithm), get_string(u8), rounds)
1406                 if sv is None:
1407                     # Unable to calculate a hash with the specified
1408                     # number of rounds, fall back to the first hash using
1409                     # the specified algorithm
1410                     sv = fb
1411             if sv is None:
1412                 return None
1413             return "{CRYPT}" + sv
1414
1415         def get_userPassword_hash(blob, algorithm, rounds):
1416             up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1417             SCHEME = "{CRYPT}"
1418
1419             # Check that the NT hash has not been changed without updating
1420             # the user password hashes. This indicates that password has been
1421             # changed without updating the supplemental credentials.
1422             if unicodePwd != bytearray(up.current_nt_hash.hash):
1423                 return None
1424
1425             scheme_prefix = "$%d$" % algorithm
1426             prefix = scheme_prefix
1427             if rounds > 0:
1428                 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1429             scheme_match = None
1430
1431             for h in up.hashes:
1432                 # in PY2 this should just do nothing and in PY3 if bytes
1433                 # it will decode them
1434                 h_value = get_string(h.value)
1435                 if (scheme_match is None and
1436                     h.scheme == SCHEME and
1437                     h_value.startswith(scheme_prefix)):
1438                     scheme_match = h_value
1439                 if h.scheme == SCHEME and h_value.startswith(prefix):
1440                     return (h_value, scheme_match)
1441
1442             # No match on the number of rounds, return the value of the
1443             # first matching scheme
1444             return (None, scheme_match)
1445
1446         def get_kerberos_ctr():
1447             primary_krb5 = get_package("Primary:Kerberos-Newer-Keys")
1448             if primary_krb5 is None:
1449                 primary_krb5 = get_package("Primary:Kerberos")
1450             if primary_krb5 is None:
1451                 return (0, None)
1452             krb5_blob = ndr_unpack(drsblobs.package_PrimaryKerberosBlob,
1453                                    primary_krb5)
1454             return (krb5_blob.version, krb5_blob.ctr)
1455
1456         # We use sort here in order to have a predictable processing order
1457         for a in sorted(virtual_attributes.keys()):
1458             if not a.lower() in lower_attrs:
1459                 continue
1460
1461             if a == "virtualClearTextUTF8":
1462                 b = get_package("Primary:CLEARTEXT")
1463                 if b is None:
1464                     continue
1465                 u8 = get_utf8(a, b, username or account_name)
1466                 if u8 is None:
1467                     continue
1468                 v = u8
1469             elif a == "virtualClearTextUTF16":
1470                 v = get_package("Primary:CLEARTEXT")
1471                 if v is None:
1472                     continue
1473             elif a == "virtualSSHA":
1474                 b = get_package("Primary:CLEARTEXT")
1475                 if b is None:
1476                     continue
1477                 u8 = get_utf8(a, b, username or account_name)
1478                 if u8 is None:
1479                     continue
1480                 salt = os.urandom(4)
1481                 h = hashlib.sha1()
1482                 h.update(u8)
1483                 h.update(salt)
1484                 bv = h.digest() + salt
1485                 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
1486             elif a == "virtualCryptSHA256":
1487                 rounds = get_rounds(attr_opts[a])
1488                 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1489                 if x is None:
1490                     continue
1491                 v = x
1492             elif a == "virtualCryptSHA512":
1493                 rounds = get_rounds(attr_opts[a])
1494                 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1495                 if x is None:
1496                     continue
1497                 v = x
1498             elif a == "virtualSambaGPG":
1499                 # Samba adds 'Primary:SambaGPG' at the end.
1500                 # When Windows sets the password it keeps
1501                 # 'Primary:SambaGPG' and rotates it to
1502                 # the begining. So we can only use the value,
1503                 # if it is the last one.
1504                 v = get_package("Primary:SambaGPG", min_idx=-1)
1505                 if v is None:
1506                     continue
1507             elif a == "virtualKerberosSalt":
1508                 (krb5_v, krb5_ctr) = get_kerberos_ctr()
1509                 if krb5_v not in [3, 4]:
1510                     continue
1511                 v = krb5_ctr.salt.string
1512             elif a.startswith("virtualWDigest"):
1513                 primary_wdigest = get_package("Primary:WDigest")
1514                 if primary_wdigest is None:
1515                     continue
1516                 x = a[len("virtualWDigest"):]
1517                 try:
1518                     i = int(x)
1519                 except ValueError:
1520                     continue
1521                 domain = self.lp.get("workgroup")
1522                 dns_domain = samdb.domain_dns_name()
1523                 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1524                 if v is None:
1525                     continue
1526             else:
1527                 continue
1528             obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1529         return obj
1530
1531     def parse_attributes(self, attributes):
1532
1533         if attributes is None:
1534             raise CommandError("Please specify --attributes")
1535         attrs = attributes.split(',')
1536         password_attrs = []
1537         for pa in attrs:
1538             pa = pa.lstrip().rstrip()
1539             for da in disabled_virtual_attributes.keys():
1540                 if pa.lower() == da.lower():
1541                     r = disabled_virtual_attributes[da]["reason"]
1542                     raise CommandError("Virtual attribute '%s' not supported: %s" % (
1543                                        da, r))
1544             for va in virtual_attributes.keys():
1545                 if pa.lower() == va.lower():
1546                     # Take the real name
1547                     pa = va
1548                     break
1549             password_attrs += [pa]
1550
1551         return password_attrs
1552
1553
1554 class cmd_user_getpassword(GetPasswordCommand):
1555     """Get the password fields of a user/computer account.
1556
1557 This command gets the logon password for a user/computer account.
1558
1559 The username specified on the command is the sAMAccountName.
1560 The username may also be specified using the --filter option.
1561
1562 The command must be run from the root user id or another authorized user id.
1563 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1564 used to adjust the local path. By default tdb:// is used by default.
1565
1566 The '--attributes' parameter takes a comma separated list of attributes,
1567 which will be printed or given to the script specified by '--script'. If a
1568 specified attribute is not available on an object it's silently omitted.
1569 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1570 the NTHASH) and the following virtual attributes are possible (see --help
1571 for which virtual attributes are supported in your environment):
1572
1573    virtualClearTextUTF16: The raw cleartext as stored in the
1574                           'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1575                           with '--decrypt-samba-gpg') buffer inside of the
1576                           supplementalCredentials attribute. This typically
1577                           contains valid UTF-16-LE, but may contain random
1578                           bytes, e.g. for computer accounts.
1579
1580    virtualClearTextUTF8:  As virtualClearTextUTF16, but converted to UTF-8
1581                           (only from valid UTF-16-LE)
1582
1583    virtualSSHA:           As virtualClearTextUTF8, but a salted SHA-1
1584                           checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1585
1586    virtualCryptSHA256:    As virtualClearTextUTF8, but a salted SHA256
1587                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1588                           with a $5$... salt, see crypt(3) on modern systems.
1589                           The number of rounds used to calculate the hash can
1590                           also be specified. By appending ";rounds=x" to the
1591                           attribute name i.e. virtualCryptSHA256;rounds=10000
1592                           will calculate a SHA256 hash with 10,000 rounds.
1593                           non numeric values for rounds are silently ignored
1594                           The value is calculated as follows:
1595                           1) If a value exists in 'Primary:userPassword' with
1596                              the specified number of rounds it is returned.
1597                           2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1598                              '--decrypt-samba-gpg'. Calculate a hash with
1599                              the specified number of rounds
1600                           3) Return the first CryptSHA256 value in
1601                              'Primary:userPassword'
1602
1603
1604    virtualCryptSHA512:    As virtualClearTextUTF8, but a salted SHA512
1605                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1606                           with a $6$... salt, see crypt(3) on modern systems.
1607                           The number of rounds used to calculate the hash can
1608                           also be specified. By appending ";rounds=x" to the
1609                           attribute name i.e. virtualCryptSHA512;rounds=10000
1610                           will calculate a SHA512 hash with 10,000 rounds.
1611                           non numeric values for rounds are silently ignored
1612                           The value is calculated as follows:
1613                           1) If a value exists in 'Primary:userPassword' with
1614                              the specified number of rounds it is returned.
1615                           2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1616                              '--decrypt-samba-gpg'. Calculate a hash with
1617                              the specified number of rounds
1618                           3) Return the first CryptSHA512 value in
1619                              'Primary:userPassword'
1620
1621    virtualWDigestNN:      The individual hash values stored in
1622                           'Primary:WDigest' where NN is the hash number in
1623                           the range 01 to 29.
1624                           NOTE: As at 22-05-2017 the documentation:
1625                           3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1626                         https://msdn.microsoft.com/en-us/library/cc245680.aspx
1627                           is incorrect
1628
1629    virtualKerberosSalt:   This results the salt string that is used to compute
1630                           Kerberos keys from a UTF-8 cleartext password.
1631
1632    virtualSambaGPG:       The raw cleartext as stored in the
1633                           'Primary:SambaGPG' buffer inside of the
1634                           supplementalCredentials attribute.
1635                           See the 'password hash gpg key ids' option in
1636                           smb.conf.
1637
1638 The '--decrypt-samba-gpg' option triggers decryption of the
1639 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1640 in your environment or not (the python-gpgme package is required).  Please
1641 note that you might need to set the GNUPGHOME environment variable.  If the
1642 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1643 environment variable has been set correctly and the passphrase is already
1644 known by the gpg-agent.
1645
1646 Example1:
1647 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1648
1649 Example2:
1650 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1651
1652 """
1653     def __init__(self):
1654         super(cmd_user_getpassword, self).__init__()
1655
1656     synopsis = "%prog (<username>|--filter <filter>) [options]"
1657
1658     takes_optiongroups = {
1659         "sambaopts": options.SambaOptions,
1660         "versionopts": options.VersionOptions,
1661     }
1662
1663     takes_options = [
1664         Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1665                metavar="URL", dest="H"),
1666         Option("--filter", help="LDAP Filter to set password on", type=str),
1667         Option("--attributes", type=str,
1668                help=virtual_attributes_help,
1669                metavar="ATTRIBUTELIST", dest="attributes"),
1670         Option("--decrypt-samba-gpg",
1671                help=decrypt_samba_gpg_help,
1672                action="store_true", default=False, dest="decrypt_samba_gpg"),
1673     ]
1674
1675     takes_args = ["username?"]
1676
1677     def run(self, username=None, H=None, filter=None,
1678             attributes=None, decrypt_samba_gpg=None,
1679             sambaopts=None, versionopts=None):
1680         self.lp = sambaopts.get_loadparm()
1681
1682         if decrypt_samba_gpg and not gpg_decrypt:
1683             raise CommandError(decrypt_samba_gpg_help)
1684
1685         if filter is None and username is None:
1686             raise CommandError("Either the username or '--filter' must be specified!")
1687
1688         if filter is None:
1689             filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1690
1691         if attributes is None:
1692             raise CommandError("Please specify --attributes")
1693
1694         password_attrs = self.parse_attributes(attributes)
1695
1696         samdb = self.connect_system_samdb(url=H, allow_local=True)
1697
1698         obj = self.get_account_attributes(samdb, username,
1699                                           basedn=None,
1700                                           filter=filter,
1701                                           scope=ldb.SCOPE_SUBTREE,
1702                                           attrs=password_attrs,
1703                                           decrypt=decrypt_samba_gpg)
1704
1705         ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1706         self.outf.write("%s" % ldif)
1707         self.outf.write("Got password OK\n")
1708
1709
1710 class cmd_user_syncpasswords(GetPasswordCommand):
1711     """Sync the password of user accounts.
1712
1713 This syncs logon passwords for user accounts.
1714
1715 Note that this command should run on a single domain controller only
1716 (typically the PDC-emulator). However the "password hash gpg key ids"
1717 option should to be configured on all domain controllers.
1718
1719 The command must be run from the root user id or another authorized user id.
1720 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1721 local path.  By default, ldapi:// is used with the default path to the
1722 privileged ldapi socket.
1723
1724 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1725 "Sync Loop Terminate".
1726
1727
1728 Cache Initialization
1729 ====================
1730
1731 The first time, this command needs to be called with
1732 '--cache-ldb-initialize' in order to initialize its cache.
1733
1734 The cache initialization requires '--attributes' and allows the following
1735 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1736 '-H/--URL'.
1737
1738 The '--attributes' parameter takes a comma separated list of attributes,
1739 which will be printed or given to the script specified by '--script'. If a
1740 specified attribute is not available on an object it will be silently omitted.
1741 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1742 the NTHASH) and the following virtual attributes are possible (see '--help'
1743 for supported virtual attributes in your environment):
1744
1745    virtualClearTextUTF16: The raw cleartext as stored in the
1746                           'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1747                           with '--decrypt-samba-gpg') buffer inside of the
1748                           supplementalCredentials attribute. This typically
1749                           contains valid UTF-16-LE, but may contain random
1750                           bytes, e.g. for computer accounts.
1751
1752    virtualClearTextUTF8:  As virtualClearTextUTF16, but converted to UTF-8
1753                           (only from valid UTF-16-LE)
1754
1755    virtualSSHA:           As virtualClearTextUTF8, but a salted SHA-1
1756                           checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1757
1758    virtualCryptSHA256:    As virtualClearTextUTF8, but a salted SHA256
1759                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1760                           with a $5$... salt, see crypt(3) on modern systems.
1761                           The number of rounds used to calculate the hash can
1762                           also be specified. By appending ";rounds=x" to the
1763                           attribute name i.e. virtualCryptSHA256;rounds=10000
1764                           will calculate a SHA256 hash with 10,000 rounds.
1765                           non numeric values for rounds are silently ignored
1766                           The value is calculated as follows:
1767                           1) If a value exists in 'Primary:userPassword' with
1768                              the specified number of rounds it is returned.
1769                           2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1770                              '--decrypt-samba-gpg'. Calculate a hash with
1771                              the specified number of rounds
1772                           3) Return the first CryptSHA256 value in
1773                              'Primary:userPassword'
1774
1775    virtualCryptSHA512:    As virtualClearTextUTF8, but a salted SHA512
1776                           checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1777                           with a $6$... salt, see crypt(3) on modern systems.
1778                           The number of rounds used to calculate the hash can
1779                           also be specified. By appending ";rounds=x" to the
1780                           attribute name i.e. virtualCryptSHA512;rounds=10000
1781                           will calculate a SHA512 hash with 10,000 rounds.
1782                           non numeric values for rounds are silently ignored
1783                           The value is calculated as follows:
1784                           1) If a value exists in 'Primary:userPassword' with
1785                              the specified number of rounds it is returned.
1786                           2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1787                              '--decrypt-samba-gpg'. Calculate a hash with
1788                              the specified number of rounds
1789                           3) Return the first CryptSHA512 value in
1790                              'Primary:userPassword'
1791
1792    virtualWDigestNN:      The individual hash values stored in
1793                           'Primary:WDigest' where NN is the hash number in
1794                           the range 01 to 29.
1795                           NOTE: As at 22-05-2017 the documentation:
1796                           3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1797                         https://msdn.microsoft.com/en-us/library/cc245680.aspx
1798                           is incorrect.
1799
1800    virtualKerberosSalt:   This results the salt string that is used to compute
1801                           Kerberos keys from a UTF-8 cleartext password.
1802
1803    virtualSambaGPG:       The raw cleartext as stored in the
1804                           'Primary:SambaGPG' buffer inside of the
1805                           supplementalCredentials attribute.
1806                           See the 'password hash gpg key ids' option in
1807                           smb.conf.
1808
1809 The '--decrypt-samba-gpg' option triggers decryption of the
1810 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1811 in your environment or not (the python-gpgme package is required).  Please
1812 note that you might need to set the GNUPGHOME environment variable.  If the
1813 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1814 environment variable has been set correctly and the passphrase is already
1815 known by the gpg-agent.
1816
1817 The '--script' option specifies a custom script that is called whenever any
1818 of the dirsyncAttributes (see below) was changed. The script is called
1819 without any arguments. It gets the LDIF for exactly one object on STDIN.
1820 If the script processed the object successfully it has to respond with a
1821 single line starting with 'DONE-EXIT: ' followed by an optional message.
1822
1823 Note that the script might be called without any password change, e.g. if
1824 the account was disabled (a userAccountControl change) or the
1825 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1826 are always returned as unique identifier of the account. It might be useful
1827 to also ask for non-password attributes like: objectSid, sAMAccountName,
1828 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1829 Depending on the object, some attributes may not be present/available,
1830 but you always get the current state (and not a diff).
1831
1832 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1833 into the logfile.
1834
1835 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1836 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1837     (!(sAMAccountName=krbtgt*)))
1838 This means only normal (non-krbtgt) user
1839 accounts are monitored.  The '--filter' can modify that, e.g. if it's
1840 required to also sync computer accounts.
1841
1842
1843 Sync Loop Run
1844 =============
1845
1846 This (default) mode runs in an endless loop waiting for password related
1847 changes in the active directory database. It makes use of the
1848 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1849 get changes in a reliable fashion. Objects are monitored for changes of the
1850 following dirsyncAttributes:
1851
1852   unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1853   userPrincipalName and userAccountControl.
1854
1855 It recovers from LDAP disconnects and updates the cache in conservative way
1856 (in single steps after each successfully processed change).  An error from
1857 the script (specified by '--script') will result in fatal error and this
1858 command will exit.  But the cache state should be still valid and can be
1859 resumed in the next "Sync Loop Run".
1860
1861 The '--logfile' option specifies an optional (required if '--daemon' is
1862 specified) logfile that takes all output of the command. The logfile is
1863 automatically reopened if fstat returns st_nlink == 0.
1864
1865 The optional '--daemon' option will put the command into the background.
1866
1867 You can stop the command without the '--daemon' option, also by hitting
1868 strg+c.
1869
1870 If you specify the '--no-wait' option the command skips the
1871 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1872 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1873
1874 Sync Loop Terminate
1875 ===================
1876
1877 In order to terminate an already running command (likely as daemon) the
1878 '--terminate' option can be used. This also requires the '--logfile' option
1879 to be specified.
1880
1881
1882 Example1:
1883 samba-tool user syncpasswords --cache-ldb-initialize \\
1884     --attributes=virtualClearTextUTF8
1885 samba-tool user syncpasswords
1886
1887 Example2:
1888 samba-tool user syncpasswords --cache-ldb-initialize \\
1889     --attributes=objectGUID,objectSID,sAMAccountName,\\
1890     userPrincipalName,userAccountControl,pwdLastSet,\\
1891     msDS-KeyVersionNumber,virtualCryptSHA512 \\
1892     --script=/path/to/my-custom-syncpasswords-script.py
1893 samba-tool user syncpasswords --daemon \\
1894     --logfile=/var/log/samba/user-syncpasswords.log
1895 samba-tool user syncpasswords --terminate \\
1896     --logfile=/var/log/samba/user-syncpasswords.log
1897
1898 """
1899     def __init__(self):
1900         super(cmd_user_syncpasswords, self).__init__()
1901
1902     synopsis = "%prog [--cache-ldb-initialize] [options]"
1903
1904     takes_optiongroups = {
1905         "sambaopts": options.SambaOptions,
1906         "versionopts": options.VersionOptions,
1907     }
1908
1909     takes_options = [
1910         Option("--cache-ldb-initialize",
1911                help="Initialize the cache for the first time",
1912                dest="cache_ldb_initialize", action="store_true"),
1913         Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1914                metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1915         Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1916                metavar="URL", dest="H"),
1917         Option("--filter", help="optional LDAP filter to set password on", type=str,
1918                metavar="LDAP-SEARCH-FILTER", dest="filter"),
1919         Option("--attributes", type=str,
1920                help=virtual_attributes_help,
1921                metavar="ATTRIBUTELIST", dest="attributes"),
1922         Option("--decrypt-samba-gpg",
1923                help=decrypt_samba_gpg_help,
1924                action="store_true", default=False, dest="decrypt_samba_gpg"),
1925         Option("--script", help="Script that is called for each password change", type=str,
1926                metavar="/path/to/syncpasswords.script", dest="script"),
1927         Option("--no-wait", help="Don't block waiting for changes",
1928                action="store_true", default=False, dest="nowait"),
1929         Option("--logfile", type=str,
1930                help="The logfile to use (required in --daemon mode).",
1931                metavar="/path/to/syncpasswords.log", dest="logfile"),
1932         Option("--daemon", help="daemonize after initial setup",
1933                action="store_true", default=False, dest="daemon"),
1934         Option("--terminate",
1935                help="Send a SIGTERM to an already running (daemon) process",
1936                action="store_true", default=False, dest="terminate"),
1937     ]
1938
1939     def run(self, cache_ldb_initialize=False, cache_ldb=None,
1940             H=None, filter=None,
1941             attributes=None, decrypt_samba_gpg=None,
1942             script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1943             sambaopts=None, versionopts=None):
1944
1945         self.lp = sambaopts.get_loadparm()
1946         self.logfile = None
1947         self.samdb_url = None
1948         self.samdb = None
1949         self.cache = None
1950
1951         if not cache_ldb_initialize:
1952             if attributes is not None:
1953                 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1954             if decrypt_samba_gpg:
1955                 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1956             if script is not None:
1957                 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1958             if filter is not None:
1959                 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1960             if H is not None:
1961                 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1962         else:
1963             if nowait is not False:
1964                 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1965             if logfile is not None:
1966                 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1967             if daemon is not False:
1968                 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1969             if terminate is not False:
1970                 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1971
1972         if nowait is True:
1973             if daemon is True:
1974                 raise CommandError("--daemon is not allowed together with --no-wait")
1975             if terminate is not False:
1976                 raise CommandError("--terminate is not allowed together with --no-wait")
1977
1978         if terminate is True and daemon is True:
1979             raise CommandError("--terminate is not allowed together with --daemon")
1980
1981         if daemon is True and logfile is None:
1982             raise CommandError("--daemon is only allowed together with --logfile")
1983
1984         if terminate is True and logfile is None:
1985             raise CommandError("--terminate is only allowed together with --logfile")
1986
1987         if script is not None:
1988             if not os.path.exists(script):
1989                 raise CommandError("script[%s] does not exist!" % script)
1990
1991             sync_command = "%s" % os.path.abspath(script)
1992         else:
1993             sync_command = None
1994
1995         dirsync_filter = filter
1996         if dirsync_filter is None:
1997             dirsync_filter = "(&" + \
1998                                "(objectClass=user)" + \
1999                                "(userAccountControl:%s:=%u)" % (
2000                                    ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
2001                                "(!(sAMAccountName=krbtgt*))" + \
2002                              ")"
2003
2004         dirsync_secret_attrs = [
2005             "unicodePwd",
2006             "dBCSPwd",
2007             "supplementalCredentials",
2008         ]
2009
2010         dirsync_attrs = dirsync_secret_attrs + [
2011             "pwdLastSet",
2012             "sAMAccountName",
2013             "userPrincipalName",
2014             "userAccountControl",
2015             "isDeleted",
2016             "isRecycled",
2017         ]
2018
2019         password_attrs = None
2020
2021         if cache_ldb_initialize:
2022             if H is None:
2023                 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
2024
2025             if decrypt_samba_gpg and not gpg_decrypt:
2026                 raise CommandError(decrypt_samba_gpg_help)
2027
2028             password_attrs = self.parse_attributes(attributes)
2029             lower_attrs = [x.lower() for x in password_attrs]
2030             # We always return these in order to track deletions
2031             for a in ["objectGUID", "isDeleted", "isRecycled"]:
2032                 if a.lower() not in lower_attrs:
2033                     password_attrs += [a]
2034
2035         if cache_ldb is not None:
2036             if cache_ldb.lower().startswith("ldapi://"):
2037                 raise CommandError("--cache_ldb ldapi:// is not supported")
2038             elif cache_ldb.lower().startswith("ldap://"):
2039                 raise CommandError("--cache_ldb ldap:// is not supported")
2040             elif cache_ldb.lower().startswith("ldaps://"):
2041                 raise CommandError("--cache_ldb ldaps:// is not supported")
2042             elif cache_ldb.lower().startswith("tdb://"):
2043                 pass
2044             else:
2045                 if not os.path.exists(cache_ldb):
2046                     cache_ldb = self.lp.private_path(cache_ldb)
2047         else:
2048             cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
2049
2050         self.lockfile = "%s.pid" % cache_ldb
2051
2052         def log_msg(msg):
2053             if self.logfile is not None:
2054                 info = os.fstat(0)
2055                 if info.st_nlink == 0:
2056                     logfile = self.logfile
2057                     self.logfile = None
2058                     log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
2059                     logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2060                     os.dup2(logfd, 0)
2061                     os.dup2(logfd, 1)
2062                     os.dup2(logfd, 2)
2063                     os.close(logfd)
2064                     log_msg("Reopened logfile[%s]\n" % (logfile))
2065                     self.logfile = logfile
2066             msg = "%s: pid[%d]: %s" % (
2067                     time.ctime(),
2068                     os.getpid(),
2069                     msg)
2070             self.outf.write(msg)
2071             return
2072
2073         def load_cache():
2074             cache_attrs = [
2075                 "samdbUrl",
2076                 "dirsyncFilter",
2077                 "dirsyncAttribute",
2078                 "dirsyncControl",
2079                 "passwordAttribute",
2080                 "decryptSambaGPG",
2081                 "syncCommand",
2082                 "currentPid",
2083             ]
2084
2085             self.cache = Ldb(cache_ldb)
2086             self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
2087             res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
2088                                     attrs=cache_attrs)
2089             if len(res) == 1:
2090                 try:
2091                     self.samdb_url = str(res[0]["samdbUrl"][0])
2092                 except KeyError as e:
2093                     self.samdb_url = None
2094             else:
2095                 self.samdb_url = None
2096             if self.samdb_url is None and not cache_ldb_initialize:
2097                 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
2098                                    cache_ldb))
2099             if self.samdb_url is not None and cache_ldb_initialize:
2100                 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
2101                                    cache_ldb))
2102             if self.samdb_url is None:
2103                 self.samdb_url = H
2104                 self.dirsync_filter = dirsync_filter
2105                 self.dirsync_attrs = dirsync_attrs
2106                 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"]
2107                 self.password_attrs = password_attrs
2108                 self.decrypt_samba_gpg = decrypt_samba_gpg
2109                 self.sync_command = sync_command
2110                 add_ldif = "dn: %s\n" % self.cache_dn +\
2111                            "objectClass: userSyncPasswords\n" +\
2112                            "samdbUrl:: %s\n" % base64.b64encode(get_bytes(self.samdb_url)).decode('utf8') +\
2113                            "dirsyncFilter:: %s\n" % base64.b64encode(get_bytes(self.dirsync_filter)).decode('utf8') +\
2114                            "".join("dirsyncAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.dirsync_attrs) +\
2115                            "dirsyncControl: %s\n" % self.dirsync_controls[0] +\
2116                            "".join("passwordAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.password_attrs)
2117                 if self.decrypt_samba_gpg:
2118                     add_ldif += "decryptSambaGPG: TRUE\n"
2119                 else:
2120                     add_ldif += "decryptSambaGPG: FALSE\n"
2121                 if self.sync_command is not None:
2122                     add_ldif += "syncCommand: %s\n" % self.sync_command
2123                 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2124                 self.cache.add_ldif(add_ldif)
2125                 self.current_pid = None
2126                 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
2127                 msgs = self.cache.parse_ldif(add_ldif)
2128                 changetype, msg = next(msgs)
2129                 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
2130                 self.outf.write("%s" % ldif)
2131             else:
2132                 self.dirsync_filter = str(res[0]["dirsyncFilter"][0])
2133                 self.dirsync_attrs = []
2134                 for a in res[0]["dirsyncAttribute"]:
2135                     self.dirsync_attrs.append(str(a))
2136                 self.dirsync_controls = [str(res[0]["dirsyncControl"][0]), "extended_dn:1:0"]
2137                 self.password_attrs = []
2138                 for a in res[0]["passwordAttribute"]:
2139                     self.password_attrs.append(str(a))
2140                 decrypt_string = str(res[0]["decryptSambaGPG"][0])
2141                 assert(decrypt_string in ["TRUE", "FALSE"])
2142                 if decrypt_string == "TRUE":
2143                     self.decrypt_samba_gpg = True
2144                 else:
2145                     self.decrypt_samba_gpg = False
2146                 if "syncCommand" in res[0]:
2147                     self.sync_command = str(res[0]["syncCommand"][0])
2148                 else:
2149                     self.sync_command = None
2150                 if "currentPid" in res[0]:
2151                     self.current_pid = int(res[0]["currentPid"][0])
2152                 else:
2153                     self.current_pid = None
2154                 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
2155
2156             return
2157
2158         def run_sync_command(dn, ldif):
2159             log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
2160             sync_command_p = Popen(self.sync_command,
2161                                    stdin=PIPE,
2162                                    stdout=PIPE,
2163                                    stderr=STDOUT)
2164
2165             res = sync_command_p.poll()
2166             assert res is None
2167
2168             input = "%s" % (ldif)
2169             reply = sync_command_p.communicate(
2170                 input.encode('utf-8'))[0].decode('utf-8')
2171             log_msg("%s\n" % (reply))
2172             res = sync_command_p.poll()
2173             if res is None:
2174                 sync_command_p.terminate()
2175             res = sync_command_p.wait()
2176
2177             if reply.startswith("DONE-EXIT: "):
2178                 return
2179
2180             log_msg("RESULT: %s\n" % (res))
2181             raise Exception("ERROR: %s - %s\n" % (res, reply))
2182
2183         def handle_object(idx, dirsync_obj):
2184             binary_guid = dirsync_obj.dn.get_extended_component("GUID")
2185             guid = ndr_unpack(misc.GUID, binary_guid)
2186             binary_sid = dirsync_obj.dn.get_extended_component("SID")
2187             sid = ndr_unpack(security.dom_sid, binary_sid)
2188             domain_sid, rid = sid.split()
2189             if rid == security.DOMAIN_RID_KRBTGT:
2190                 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
2191                 return
2192             for a in list(dirsync_obj.keys()):
2193                 for h in dirsync_secret_attrs:
2194                     if a.lower() == h.lower():
2195                         del dirsync_obj[a]
2196                         dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
2197             dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
2198             log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
2199             obj = self.get_account_attributes(self.samdb,
2200                                               username="%s" % sid,
2201                                               basedn="<GUID=%s>" % guid,
2202                                               filter="(objectClass=user)",
2203                                               scope=ldb.SCOPE_BASE,
2204                                               attrs=self.password_attrs,
2205                                               decrypt=self.decrypt_samba_gpg)
2206             ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
2207             log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
2208             if self.sync_command is None:
2209                 self.outf.write("%s" % (ldif))
2210                 return
2211             self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
2212             run_sync_command(obj.dn, ldif)
2213
2214         def check_current_pid_conflict(terminate):
2215             flags = os.O_RDWR
2216             if not terminate:
2217                 flags |= os.O_CREAT
2218
2219             try:
2220                 self.lockfd = os.open(self.lockfile, flags, 0o600)
2221             except IOError as e4:
2222                 (err, msg) = e4.args
2223                 if err == errno.ENOENT:
2224                     if terminate:
2225                         return False
2226                 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
2227                         (self.lockfile, msg, err))
2228                 raise
2229
2230             got_exclusive = False
2231             try:
2232                 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2233                 got_exclusive = True
2234             except IOError as e5:
2235                 (err, msg) = e5.args
2236                 if err != errno.EACCES and err != errno.EAGAIN:
2237                     log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
2238                             (self.lockfile, msg, err))
2239                     raise
2240
2241             if not got_exclusive:
2242                 buf = os.read(self.lockfd, 64)
2243                 self.current_pid = None
2244                 try:
2245                     self.current_pid = int(buf)
2246                 except ValueError as e:
2247                     pass
2248                 if self.current_pid is not None:
2249                     return True
2250
2251             if got_exclusive and terminate:
2252                 try:
2253                     os.ftruncate(self.lockfd, 0)
2254                 except IOError as e2:
2255                     (err, msg) = e2.args
2256                     log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2257                             (self.lockfile, msg, err))
2258                     raise
2259                 os.close(self.lockfd)
2260                 self.lockfd = -1
2261                 return False
2262
2263             try:
2264                 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2265             except IOError as e6:
2266                 (err, msg) = e6.args
2267                 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2268                         (self.lockfile, msg, err))
2269
2270             # We leave the function with the shared lock.
2271             return False
2272
2273         def update_pid(pid):
2274             if self.lockfd != -1:
2275                 got_exclusive = False
2276                 # Try 5 times to get the exclusiv lock.
2277                 for i in range(0, 5):
2278                     try:
2279                         fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2280                         got_exclusive = True
2281                     except IOError as e:
2282                         (err, msg) = e.args
2283                         if err != errno.EACCES and err != errno.EAGAIN:
2284                             log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2285                                     (pid, self.lockfile, msg, err))
2286                             raise
2287                     if got_exclusive:
2288                         break
2289                     time.sleep(1)
2290                 if not got_exclusive:
2291                     log_msg("update_pid(%r): failed to get exclusive lock[%s]" %
2292                             (pid, self.lockfile))
2293                     raise CommandError("update_pid(%r): failed to get "
2294                                        "exclusive lock[%s] after 5 seconds" %
2295                                        (pid, self.lockfile))
2296
2297                 if pid is not None:
2298                     buf = "%d\n" % pid
2299                 else:
2300                     buf = None
2301                 try:
2302                     os.ftruncate(self.lockfd, 0)
2303                     if buf is not None:
2304                         os.write(self.lockfd, get_bytes(buf))
2305                 except IOError as e3:
2306                     (err, msg) = e3.args
2307                     log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2308                             (self.lockfile, msg, err))
2309                     raise
2310             self.current_pid = pid
2311             if self.current_pid is not None:
2312                 log_msg("currentPid: %d\n" % self.current_pid)
2313
2314             modify_ldif = "dn: %s\n" % (self.cache_dn) +\
2315                           "changetype: modify\n" +\
2316                           "replace: currentPid\n"
2317             if self.current_pid is not None:
2318                 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2319             modify_ldif += "replace: currentTime\n" +\
2320                            "currentTime: %s\n" % ldb.timestring(int(time.time()))
2321             self.cache.modify_ldif(modify_ldif)
2322             return
2323
2324         def update_cache(res_controls):
2325             assert len(res_controls) > 0
2326             assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2327             res_controls[0].critical = True
2328             self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2329             # This cookie can be extremely long
2330             # log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2331
2332             modify_ldif = "dn: %s\n" % (self.cache_dn) +\
2333                           "changetype: modify\n" +\
2334                           "replace: dirsyncControl\n" +\
2335                           "dirsyncControl: %s\n" % (self.dirsync_controls[0]) +\
2336                           "replace: currentTime\n" +\
2337                           "currentTime: %s\n" % ldb.timestring(int(time.time()))
2338             self.cache.modify_ldif(modify_ldif)
2339             return
2340
2341         def check_object(dirsync_obj, res_controls):
2342             assert len(res_controls) > 0
2343             assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2344
2345             binary_sid = dirsync_obj.dn.get_extended_component("SID")
2346             sid = ndr_unpack(security.dom_sid, binary_sid)
2347             dn = "KEY=%s" % sid
2348             lastCookie = str(res_controls[0])
2349
2350             res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2351                                     expression="(lastCookie=%s)" % (
2352                                         ldb.binary_encode(lastCookie)),
2353                                     attrs=[])
2354             if len(res) == 1:
2355                 return True
2356             return False
2357
2358         def update_object(dirsync_obj, res_controls):
2359             assert len(res_controls) > 0
2360             assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2361
2362             binary_sid = dirsync_obj.dn.get_extended_component("SID")
2363             sid = ndr_unpack(security.dom_sid, binary_sid)
2364             dn = "KEY=%s" % sid
2365             lastCookie = str(res_controls[0])
2366
2367             self.cache.transaction_start()
2368             try:
2369                 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2370                                         expression="(objectClass=*)",
2371                                         attrs=["lastCookie"])
2372                 if len(res) == 0:
2373                     add_ldif  = "dn: %s\n" % (dn) +\
2374                                 "objectClass: userCookie\n" +\
2375                                 "lastCookie: %s\n" % (lastCookie) +\
2376                                 "currentTime: %s\n" % ldb.timestring(int(time.time()))
2377                     self.cache.add_ldif(add_ldif)
2378                 else:
2379                     modify_ldif = "dn: %s\n" % (dn) +\
2380                                   "changetype: modify\n" +\
2381                                   "replace: lastCookie\n" +\
2382                                   "lastCookie: %s\n" % (lastCookie) +\
2383                                   "replace: currentTime\n" +\
2384                                   "currentTime: %s\n" % ldb.timestring(int(time.time()))
2385                     self.cache.modify_ldif(modify_ldif)
2386                 self.cache.transaction_commit()
2387             except Exception as e:
2388                 self.cache.transaction_cancel()
2389
2390             return
2391
2392         def dirsync_loop():
2393             while True:
2394                 res = self.samdb.search(expression=str(self.dirsync_filter),
2395                                         scope=ldb.SCOPE_SUBTREE,
2396                                         attrs=self.dirsync_attrs,
2397                                         controls=self.dirsync_controls)
2398                 log_msg("dirsync_loop(): results %d\n" % len(res))
2399                 ri = 0
2400                 for r in res:
2401                     done = check_object(r, res.controls)
2402                     if not done:
2403                         handle_object(ri, r)
2404                         update_object(r, res.controls)
2405                     ri += 1
2406                 update_cache(res.controls)
2407                 if len(res) == 0:
2408                     break
2409
2410         def sync_loop(wait):
2411             notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2412             notify_controls = ["notification:1", "show_recycled:1"]
2413             notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2414                                                        scope=ldb.SCOPE_SUBTREE,
2415                                                        attrs=notify_attrs,
2416                                                        controls=notify_controls,
2417                                                        timeout=-1)
2418
2419             if wait is True:
2420                 log_msg("Resuming monitoring\n")
2421             else:
2422                 log_msg("Getting changes\n")
2423             self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2424             self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2425             self.outf.write("syncCommand: %s\n" % self.sync_command)
2426             dirsync_loop()
2427
2428             if wait is not True:
2429                 return
2430
2431             for msg in notify_handle:
2432                 if not isinstance(msg, ldb.Message):
2433                     self.outf.write("referal: %s\n" % msg)
2434                     continue
2435                 created = msg.get("uSNCreated")[0]
2436                 changed = msg.get("uSNChanged")[0]
2437                 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2438                         (msg.dn, created, changed))
2439
2440                 dirsync_loop()
2441
2442             res = notify_handle.result()
2443
2444         def daemonize():
2445             self.samdb = None
2446             self.cache = None
2447             orig_pid = os.getpid()
2448             pid = os.fork()
2449             if pid == 0:
2450                 os.setsid()
2451                 pid = os.fork()
2452                 if pid == 0:  # Actual daemon
2453                     pid = os.getpid()
2454                     log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2455                     load_cache()
2456                     return
2457             os._exit(0)
2458
2459         if cache_ldb_initialize:
2460             self.samdb_url = H
2461             self.samdb = self.connect_system_samdb(url=self.samdb_url,
2462                                                    verbose=True)
2463             load_cache()
2464             return
2465
2466         if logfile is not None:
2467             import resource      # Resource usage information.
2468             maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2469             if maxfd == resource.RLIM_INFINITY:
2470                 maxfd = 1024  # Rough guess at maximum number of open file descriptors.
2471             logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2472             self.outf.write("Using logfile[%s]\n" % logfile)
2473             for fd in range(0, maxfd):
2474                 if fd == logfd:
2475                     continue
2476                 try:
2477                     os.close(fd)
2478                 except OSError:
2479                     pass
2480             os.dup2(logfd, 0)
2481             os.dup2(logfd, 1)
2482             os.dup2(logfd, 2)
2483             os.close(logfd)
2484             log_msg("Attached to logfile[%s]\n" % (logfile))
2485             self.logfile = logfile
2486
2487         load_cache()
2488         conflict = check_current_pid_conflict(terminate)
2489         if terminate:
2490             if self.current_pid is None:
2491                 log_msg("No process running.\n")
2492                 return
2493             if not conflict:
2494                 log_msg("Proccess %d is not running anymore.\n" % (
2495                         self.current_pid))
2496                 update_pid(None)
2497                 return
2498             log_msg("Sending SIGTERM to proccess %d.\n" % (
2499                     self.current_pid))
2500             os.kill(self.current_pid, signal.SIGTERM)
2501             return
2502         if conflict:
2503             raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2504                                os.getpid(), self.current_pid))
2505
2506         if daemon is True:
2507             daemonize()
2508         update_pid(os.getpid())
2509
2510         wait = True
2511         while wait is True:
2512             retry_sleep_min = 1
2513             retry_sleep_max = 600
2514             if nowait is True:
2515                 wait = False
2516                 retry_sleep = 0
2517             else:
2518                 retry_sleep = retry_sleep_min
2519
2520             while self.samdb is None:
2521                 if retry_sleep != 0:
2522                     log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2523                     time.sleep(retry_sleep)
2524                 retry_sleep = retry_sleep * 2
2525                 if retry_sleep >= retry_sleep_max:
2526                     retry_sleep = retry_sleep_max
2527                 log_msg("Connecting to '%s'\n" % self.samdb_url)
2528                 try:
2529                     self.samdb = self.connect_system_samdb(url=self.samdb_url)
2530                 except Exception as msg:
2531                     self.samdb = None
2532                     log_msg("Connect to samdb Exception => (%s)\n" % msg)
2533                     if wait is not True:
2534                         raise
2535
2536             try:
2537                 sync_loop(wait)
2538             except ldb.LdbError as e7:
2539                 (enum, estr) = e7.args
2540                 self.samdb = None
2541                 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2542
2543         update_pid(None)
2544         return
2545
2546
2547 class cmd_user_edit(Command):
2548     """Modify User AD object.
2549
2550 This command will allow editing of a user account in the Active Directory
2551 domain. You will then be able to add or change attributes and their values.
2552
2553 The username specified on the command is the sAMAccountName.
2554
2555 The command may be run from the root userid or another authorized userid.
2556
2557 The -H or --URL= option can be used to execute the command against a remote
2558 server.
2559
2560 Example1:
2561 samba-tool user edit User1 -H ldap://samba.samdom.example.com \\
2562     -U administrator --password=passw1rd
2563
2564 Example1 shows how to edit a users attributes in the domain against a remote
2565 LDAP server.
2566
2567 The -H parameter is used to specify the remote target server.
2568
2569 Example2:
2570 samba-tool user edit User2
2571
2572 Example2 shows how to edit a users attributes in the domain against a local
2573 LDAP server.
2574
2575 Example3:
2576 samba-tool user edit User3 --editor=nano
2577
2578 Example3 shows how to edit a users attributes in the domain against a local
2579 LDAP server using the 'nano' editor.
2580
2581 """
2582     synopsis = "%prog <username> [options]"
2583
2584     takes_options = [
2585         Option("-H", "--URL", help="LDB URL for database or target server",
2586                type=str, metavar="URL", dest="H"),
2587         Option("--editor", help="Editor to use instead of the system default,"
2588                " or 'vi' if no system default is set.", type=str),
2589     ]
2590
2591     takes_args = ["username"]
2592     takes_optiongroups = {
2593         "sambaopts": options.SambaOptions,
2594         "credopts": options.CredentialsOptions,
2595         "versionopts": options.VersionOptions,
2596     }
2597
2598     def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2599             H=None, editor=None):
2600         lp = sambaopts.get_loadparm()
2601         creds = credopts.get_credentials(lp, fallback_machine=True)
2602         samdb = SamDB(url=H, session_info=system_session(),
2603                       credentials=creds, lp=lp)
2604
2605         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2606                   (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2607
2608         domaindn = samdb.domain_dn()
2609
2610         try:
2611             res = samdb.search(base=domaindn,
2612                                expression=filter,
2613                                scope=ldb.SCOPE_SUBTREE)
2614             user_dn = res[0].dn
2615         except IndexError:
2616             raise CommandError('Unable to find user "%s"' % (username))
2617
2618         import tempfile
2619         for msg in res:
2620             result_ldif = common.get_ldif_for_editor(samdb, msg)
2621
2622             if editor is None:
2623                 editor = os.environ.get('EDITOR')
2624                 if editor is None:
2625                     editor = 'vi'
2626
2627             with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2628                 t_file.write(get_bytes(result_ldif))
2629                 t_file.flush()
2630                 try:
2631                     check_call([editor, t_file.name])
2632                 except CalledProcessError as e:
2633                     raise CalledProcessError("ERROR: ", e)
2634                 with open(t_file.name) as edited_file:
2635                     edited_message = edited_file.read()
2636
2637
2638         msgs_edited = samdb.parse_ldif(edited_message)
2639         msg_edited = next(msgs_edited)[1]
2640
2641         res_msg_diff = samdb.msg_diff(msg, msg_edited)
2642         if len(res_msg_diff) == 0:
2643             self.outf.write("Nothing to do\n")
2644             return
2645
2646         try:
2647             samdb.modify(res_msg_diff)
2648         except Exception as e:
2649             raise CommandError("Failed to modify user '%s': " % username, e)
2650
2651         self.outf.write("Modified User '%s' successfully\n" % username)
2652
2653
2654 class cmd_user_show(Command):
2655     """Display a user AD object.
2656
2657 This command displays a user account and it's attributes in the Active
2658 Directory domain.
2659 The username specified on the command is the sAMAccountName.
2660
2661 The command may be run from the root userid or another authorized userid.
2662
2663 The -H or --URL= option can be used to execute the command against a remote
2664 server.
2665
2666 Example1:
2667 samba-tool user show User1 -H ldap://samba.samdom.example.com \\
2668     -U administrator --password=passw1rd
2669
2670 Example1 shows how to display a users attributes in the domain against a remote
2671 LDAP server.
2672
2673 The -H parameter is used to specify the remote target server.
2674
2675 Example2:
2676 samba-tool user show User2
2677
2678 Example2 shows how to display a users attributes in the domain against a local
2679 LDAP server.
2680
2681 Example3:
2682 samba-tool user show User2 --attributes=objectSid,memberOf
2683
2684 Example3 shows how to display a users objectSid and memberOf attributes.
2685 """
2686     synopsis = "%prog <username> [options]"
2687
2688     takes_options = [
2689         Option("-H", "--URL", help="LDB URL for database or target server",
2690                type=str, metavar="URL", dest="H"),
2691         Option("--attributes",
2692                help=("Comma separated list of attributes, "
2693                      "which will be printed."),
2694                type=str, dest="user_attrs"),
2695     ]
2696
2697     takes_args = ["username"]
2698     takes_optiongroups = {
2699         "sambaopts": options.SambaOptions,
2700         "credopts": options.CredentialsOptions,
2701         "versionopts": options.VersionOptions,
2702     }
2703
2704     def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2705             H=None, user_attrs=None):
2706
2707         lp = sambaopts.get_loadparm()
2708         creds = credopts.get_credentials(lp, fallback_machine=True)
2709         samdb = SamDB(url=H, session_info=system_session(),
2710                       credentials=creds, lp=lp)
2711
2712         attrs = None
2713         if user_attrs:
2714             attrs = user_attrs.split(",")
2715
2716         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2717                   (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2718
2719         domaindn = samdb.domain_dn()
2720
2721         try:
2722             res = samdb.search(base=domaindn, expression=filter,
2723                                scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2724             user_dn = res[0].dn
2725         except IndexError:
2726             raise CommandError('Unable to find user "%s"' % (username))
2727
2728         for msg in res:
2729             user_ldif = common.get_ldif_for_editor(samdb, msg)
2730             self.outf.write(user_ldif)
2731
2732
2733 class cmd_user_move(Command):
2734     """Move a user to an organizational unit/container.
2735
2736     This command moves a user account into the specified organizational unit
2737     or container.
2738     The username specified on the command is the sAMAccountName.
2739     The name of the organizational unit or container can be specified as a
2740     full DN or without the domainDN component.
2741
2742     The command may be run from the root userid or another authorized userid.
2743
2744     The -H or --URL= option can be used to execute the command against a remote
2745     server.
2746
2747     Example1:
2748     samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\
2749         -H ldap://samba.samdom.example.com -U administrator
2750
2751     Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2752     unit on a remote LDAP server.
2753
2754     The -H parameter is used to specify the remote target server.
2755
2756     Example2:
2757     samba-tool user move User1 CN=Users
2758
2759     Example2 shows how to move a user User1 back into the CN=Users container
2760     on the local server.
2761     """
2762
2763     synopsis = "%prog <username> <new_parent_dn> [options]"
2764
2765     takes_options = [
2766         Option("-H", "--URL", help="LDB URL for database or target server",
2767                type=str, metavar="URL", dest="H"),
2768     ]
2769
2770     takes_args = ["username", "new_parent_dn"]
2771     takes_optiongroups = {
2772         "sambaopts": options.SambaOptions,
2773         "credopts": options.CredentialsOptions,
2774         "versionopts": options.VersionOptions,
2775     }
2776
2777     def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2778             versionopts=None, H=None):
2779         lp = sambaopts.get_loadparm()
2780         creds = credopts.get_credentials(lp, fallback_machine=True)
2781         samdb = SamDB(url=H, session_info=system_session(),
2782                       credentials=creds, lp=lp)
2783         domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2784
2785         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2786                   (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2787         try:
2788             res = samdb.search(base=domain_dn,
2789                                expression=filter,
2790                                scope=ldb.SCOPE_SUBTREE)
2791             user_dn = res[0].dn
2792         except IndexError:
2793             raise CommandError('Unable to find user "%s"' % (username))
2794
2795         try:
2796             full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2797         except Exception as e:
2798             raise CommandError('Invalid new_parent_dn "%s": %s' %
2799                                (new_parent_dn, e))
2800
2801         full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2802         full_new_user_dn.remove_base_components(len(user_dn) - 1)
2803         full_new_user_dn.add_base(full_new_parent_dn)
2804
2805         try:
2806             samdb.rename(user_dn, full_new_user_dn)
2807         except Exception as e:
2808             raise CommandError('Failed to move user "%s"' % username, e)
2809         self.outf.write('Moved user "%s" into "%s"\n' %
2810                         (username, full_new_parent_dn))
2811
2812
2813 class cmd_user_rename(Command):
2814     """Rename a user and related attributes.
2815
2816     This command allows to set the user's name related attributes. The user's
2817     CN will be renamed automatically.
2818     The user's new CN will be made up by combining the given-name, initials
2819     and surname. A dot ('.') will be appended to the initials automatically
2820     if required.
2821     Use the --force-new-cn option to specify the new CN manually and the
2822     --reset-cn option to reset this change.
2823
2824     Use an empty attribute value to remove the specified attribute.
2825
2826     The username specified on the command is the sAMAccountName.
2827
2828     The command may be run locally from the root userid or another authorized
2829     userid.
2830
2831     The -H or --URL= option can be used to execute the command against a remote
2832     server.
2833
2834     Example1:
2835     samba-tool user rename johndoe --surname='Bloggs'
2836
2837     Example1 shows how to change the surname of a user 'johndoe' to 'Bloggs' on
2838     the local server. The user's CN will be renamed automatically, based on
2839     the given name, initials and surname.
2840
2841     Example2:
2842     samba-tool user rename johndoe --force-new-cn='John Bloggs (Sales)' \\
2843         --surname=Bloggs -H ldap://samba.samdom.example.com -U administrator
2844
2845     Example2 shows how to rename the CN of a user 'johndoe' to 'John Bloggs (Sales)'.
2846     Additionally the surname ('sn' attribute) is set to 'Bloggs'.
2847     The -H parameter is used to specify the remote target server.
2848     """
2849
2850     synopsis = "%prog <username> [options]"
2851
2852     takes_options = [
2853         Option("-H", "--URL",
2854                help="LDB URL for database or target server",
2855                type=str, metavar="URL", dest="H"),
2856         Option("--surname",
2857                help="New surname",
2858                type=str),
2859         Option("--given-name",
2860                help="New given name",
2861                type=str),
2862         Option("--initials",
2863                help="New initials",
2864                type=str),
2865         Option("--force-new-cn",
2866                help="Specify a new CN (RDN) instead of using a combination "
2867                     "of the given name, initials and surname.",
2868                type=str, metavar="NEW_CN"),
2869         Option("--reset-cn",
2870                help="Set the CN (RDN) to the combination of the given name, "
2871                     "initials and surname. Use this option to reset "
2872                     "the changes made with the --force-new-cn option.",
2873                action="store_true"),
2874         Option("--display-name",
2875                help="New display name",
2876                type=str),
2877         Option("--mail-address",
2878                help="New email address",
2879                type=str),
2880         Option("--samaccountname",
2881                help="New account name (sAMAccountName/logon name)",
2882                type=str),
2883         Option("--upn",
2884                help="New user principal name",
2885                type=str),
2886     ]
2887
2888     takes_args = ["username"]
2889     takes_optiongroups = {
2890         "sambaopts": options.SambaOptions,
2891         "credopts": options.CredentialsOptions,
2892         "versionopts": options.VersionOptions,
2893     }
2894
2895     def run(self, username, credopts=None, sambaopts=None,
2896             versionopts=None, H=None, surname=None, given_name=None,
2897             initials=None, display_name=None, mail_address=None,
2898             samaccountname=None, upn=None, force_new_cn=None,
2899             reset_cn=None):
2900         # illegal options
2901         if force_new_cn and reset_cn:
2902             raise CommandError("It is not allowed to specify --force-new-cn "
2903                                "together with --reset-cn.")
2904         if force_new_cn == "":
2905             raise CommandError("Failed to rename user - delete protected "
2906                                "attribute 'CN'")
2907         if samaccountname == "":
2908             raise CommandError("Failed to rename user - delete protected "
2909                                "attribute 'sAMAccountName'")
2910
2911         lp = sambaopts.get_loadparm()
2912         creds = credopts.get_credentials(lp, fallback_machine=True)
2913         samdb = SamDB(url=H, session_info=system_session(),
2914                       credentials=creds, lp=lp)
2915         domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2916
2917         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2918                   (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2919         try:
2920             res = samdb.search(base=domain_dn,
2921                                expression=filter,
2922                                scope=ldb.SCOPE_SUBTREE,
2923                                attrs=["sAMAccountName",
2924                                       "givenName",
2925                                       "initials",
2926                                       "sn",
2927                                       "mail",
2928                                       "userPrincipalName",
2929                                       "displayName",
2930                                       "cn"])
2931             old_user = res[0]
2932             user_dn = old_user.dn
2933         except IndexError:
2934             raise CommandError('Unable to find user "%s"' % (username))
2935
2936         user_parent_dn = user_dn.parent()
2937         old_cn = old_user["cn"][0]
2938
2939         # use the sAMAccountname as CN if no name is given
2940         new_fallback_cn = samaccountname if samaccountname is not None \
2941                                      else old_user["sAMAccountName"]
2942
2943         if force_new_cn is not None:
2944             new_user_cn = force_new_cn
2945         else:
2946             new_user_cn = samdb.fullname_from_names(old_attrs=old_user,
2947                                                     given_name=given_name,
2948                                                     initials=initials,
2949                                                     surname=surname,
2950                                                     fallback_default=new_fallback_cn)
2951
2952         # CN must change, if the new CN is different and the old CN is the
2953         # standard CN or the change is forced with force-new-cn or reset-cn
2954         expected_cn = samdb.fullname_from_names(old_attrs=old_user,
2955                                         fallback_default=old_user["sAMAccountName"])
2956         must_change_cn = str(old_cn) != str(new_user_cn) and \
2957                          (str(old_cn) == str(expected_cn) or \
2958                           reset_cn or bool(force_new_cn))
2959
2960         new_user_dn = ldb.Dn(samdb, "CN=%s" % new_user_cn)
2961         new_user_dn.add_base(user_parent_dn)
2962
2963         if upn is not None:
2964             if self.is_valid_upn(samdb, upn) == False:
2965                 raise CommandError('"%s" is not a valid upn. '
2966                                    'You can manage the upn '
2967                                    'suffixes with the "samba-tool domain '
2968                                    'trust namespaces" command.' % upn)
2969
2970         user_attrs = ldb.Message()
2971         user_attrs.dn = user_dn
2972         samdb.prepare_attr_replace(user_attrs, old_user, "givenName", given_name)
2973         samdb.prepare_attr_replace(user_attrs, old_user, "initials", initials)
2974         samdb.prepare_attr_replace(user_attrs, old_user, "sn", surname)
2975         samdb.prepare_attr_replace(user_attrs, old_user, "displayName", display_name)
2976         samdb.prepare_attr_replace(user_attrs, old_user, "mail", mail_address)
2977         samdb.prepare_attr_replace(user_attrs, old_user, "sAMAccountName", samaccountname)
2978         samdb.prepare_attr_replace(user_attrs, old_user, "userPrincipalName", upn)
2979
2980         attributes_changed = len(user_attrs) > 0
2981
2982         samdb.transaction_start()
2983         try:
2984             if attributes_changed == True:
2985                 samdb.modify(user_attrs)
2986             if must_change_cn == True:
2987                 samdb.rename(user_dn, new_user_dn)
2988         except Exception as e:
2989             samdb.transaction_cancel()
2990             raise CommandError('Failed to rename user "%s"' % username, e)
2991         samdb.transaction_commit()
2992
2993         if must_change_cn == True:
2994             self.outf.write('Renamed CN of user "%s" from "%s" to "%s" '
2995                             'successfully\n' % (username, old_cn, new_user_cn))
2996
2997         if attributes_changed == True:
2998             self.outf.write('Following attributes of user "%s" have been '
2999                             'changed successfully:\n' % (username))
3000             for attr in user_attrs.keys():
3001                 if (attr == "dn"):
3002                     continue
3003                 self.outf.write('%s: %s\n' % (attr, user_attrs[attr]
3004                                 if user_attrs[attr] else '[removed]'))
3005
3006     def is_valid_upn(self, samdb, upn):
3007         domain_dns = samdb.domain_dns_name()
3008         forest_dns = samdb.forest_dns_name()
3009         upn_suffixes = [domain_dns, forest_dns]
3010
3011         config_basedn = samdb.get_config_basedn()
3012         partitions_dn = "CN=Partitions,%s" % config_basedn
3013         res = samdb.search(
3014             base=partitions_dn,
3015             scope=ldb.SCOPE_BASE,
3016             expression="(objectClass=crossRefContainer)",
3017             attrs=['uPNSuffixes'])
3018
3019         if (len(res) >= 1):
3020             msg = res[0]
3021             if 'uPNSuffixes' in msg:
3022                 for s in msg['uPNSuffixes']:
3023                     upn_suffixes.append(str(s).lower())
3024
3025         upn_suffix = upn.split('@')[-1].lower()
3026         upn_split = upn.split('@')
3027         if (len(upn_split) < 2):
3028             return False
3029
3030         upn_suffix = upn_split[-1].lower()
3031         if upn_suffix not in upn_suffixes:
3032             return False
3033
3034         return True
3035
3036
3037 class cmd_user_add_unix_attrs(Command):
3038     """Add RFC2307 attributes to a user.
3039
3040 This command adds Unix attributes to a user account in the Active
3041 Directory domain.
3042
3043 The username specified on the command is the sAMaccountName.
3044
3045 You must supply a unique uidNumber.
3046
3047 Unix (RFC2307) attributes will be added to the user account.
3048
3049 If you supply a gidNumber with '--gid-number', this will be used for the
3050 users Unix 'gidNumber' attribute.
3051
3052 If '--gid-number' is not supplied, the users Unix gidNumber will be set to the
3053 one found in 'Domain Users', this means Domain Users must have a gidNumber
3054 attribute.
3055
3056 if '--unix-home' is not supplied, the users Unix home directory will be
3057 set to /home/DOMAIN/username
3058
3059 if '--login-shell' is not supplied, the users Unix login shell will be
3060 set to '/bin/sh'
3061
3062 if ---gecos' is not supplied, the users Unix gecos field will be set to the
3063 users 'CN'
3064
3065 Add 'idmap_ldb:use rfc2307 = Yes' to the smb.conf on DCs, to use these
3066 attributes for UID/GID mapping.
3067
3068 The command may be run from the root userid or another authorised userid.
3069 The -H or --URL= option can be used to execute the command against a
3070 remote server.
3071
3072 Example1:
3073 samba-tool user addunixattrs User1 10001
3074
3075 Example1 shows how to add RFC2307 attributes to a domain enabled user
3076 account, Domain Users will be set as the users gidNumber.
3077
3078 The users Unix ID will be set to '10001', provided this ID isn't already
3079 in use.
3080
3081 Example2:
3082 samba-tool user addunixattrs User2 10002 --gid-number=10001 \
3083 --unix-home=/home/User2
3084
3085 Example2 shows how to add RFC2307 attributes to a domain enabled user
3086 account.
3087
3088 The users Unix ID will be set to '10002', provided this ID isn't already
3089 in use.
3090
3091 The users gidNumber attribute will be set to '10001'
3092
3093 The users Unix home directory will be set to '/home/user2'
3094
3095 Example3:
3096 samba-tool user addunixattrs User3 10003 --gid-number=10001 \
3097 --login-shell=/bin/false --gecos='User3 test'
3098
3099 Example3 shows how to add RFC2307 attributes to a domain enabled user
3100 account.
3101
3102 The users Unix ID will be set to '10003', provided this ID isn't already
3103 in use.
3104
3105 The users gidNumber attribute will be set to '10001'
3106
3107 The users Unix login shell will be set to '/bin/false'
3108
3109 The users gecos field will be set to 'User3 test'
3110
3111 Example4:
3112 samba-tool user addunixattrs User4 10004 --gid-number=10001 \
3113 --unix-home=/home/User4 --login-shell=/bin/bash --gecos='User4 test'
3114
3115 Example4 shows how to add RFC2307 attributes to a domain enabled user
3116 account.
3117
3118 The users Unix ID will be set to '10004', provided this ID isn't already
3119 in use.
3120
3121 The users gidNumber attribute will be set to '10001'
3122
3123 The users Unix home directory will be set to '/home/User4'
3124
3125 The users Unix login shell will be set to '/bin/bash'
3126
3127 The users gecos field will be set to 'User4 test'
3128
3129 """
3130
3131     synopsis = "%prog <username> <uid-number> [options]"
3132
3133     takes_options = [
3134         Option("-H", "--URL", help="LDB URL for database or target server",
3135                type=str, metavar="URL", dest="H"),
3136         Option("--gid-number", help="User's Unix/RFC2307 GID", type=str),
3137         Option("--unix-home", help="User's Unix/RFC2307 home directory",
3138                type=str),
3139         Option("--login-shell", help="User's Unix/RFC2307 login shell",
3140                type=str),
3141         Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
3142         Option("--uid", help="User's Unix/RFC2307 username", type=str),
3143     ]
3144
3145     takes_args = ["username", "uid-number"]
3146
3147     takes_optiongroups = {
3148         "sambaopts": options.SambaOptions,
3149         "credopts": options.CredentialsOptions,
3150         "versionopts": options.VersionOptions,
3151         }
3152
3153     def run(self, username, uid_number, credopts=None, sambaopts=None,
3154             versionopts=None, H=None, gid_number=None, unix_home=None,
3155             login_shell=None, gecos=None, uid=None):
3156
3157         lp = sambaopts.get_loadparm()
3158         creds = credopts.get_credentials(lp)
3159
3160         samdb = SamDB(url=H, session_info=system_session(),
3161                       credentials=creds, lp=lp)
3162
3163         domaindn = samdb.domain_dn()
3164
3165         # Check that uidNumber supplied isn't already in use
3166         filter = ("(&(objectClass=person)(uidNumber={}))"
3167                   .format(uid_number))
3168         res = samdb.search(domaindn,
3169                            scope=ldb.SCOPE_SUBTREE,
3170                            expression=filter)
3171         if (len(res) != 0):
3172             raise CommandError("uidNumber {} is already being used."
3173                                .format(uid_number))
3174
3175         # Check user exists and doesn't have a uidNumber
3176         filter = "(samaccountname={})".format(ldb.binary_encode(username))
3177         res = samdb.search(domaindn,
3178                            scope=ldb.SCOPE_SUBTREE,
3179                            expression=filter)
3180         if (len(res) == 0):
3181             raise CommandError("Unable to find user '{}'".format(username))
3182
3183         user_dn = res[0].dn
3184
3185         if "uidNumber" in res[0]:
3186             raise CommandError("User {} is already a Unix user."
3187                                .format(username))
3188
3189         if gecos is None:
3190             gecos = res[0]["cn"][0]
3191
3192         if uid is None:
3193             uid = res[0]["cn"][0]
3194
3195         if gid_number is None:
3196             search_filter = ("(samaccountname={})"
3197                               .format(ldb.binary_encode('Domain Users')))
3198             try:
3199                 res = samdb.search(domaindn,
3200                                    scope=ldb.SCOPE_SUBTREE,
3201                                    expression=search_filter)
3202                 for msg in res:
3203                     gid_number=msg.get('gidNumber')
3204             except IndexError:
3205                 raise CommandError('Domain Users does not have a'
3206                                    ' gidNumber attribute')
3207
3208         if login_shell is None:
3209             login_shell = "/bin/sh"
3210
3211         if unix_home is None:
3212             # obtain nETBIOS Domain Name
3213             filter = "(&(objectClass=crossRef)(nETBIOSName=*))"
3214             searchdn = ("CN=Partitions,CN=Configuration," + domaindn)
3215             try:
3216                 res = samdb.search(searchdn,
3217                                    scope=ldb.SCOPE_SUBTREE,
3218                                    expression=filter)
3219                 unix_domain = res[0]["nETBIOSName"][0].decode()
3220             except IndexError:
3221                 raise CommandError('Unable to find Unix domain')
3222
3223             tmpl = lp.get('template homedir')
3224             unix_home = tmpl.replace('%D', unix_domain).replace('%U', username)
3225
3226         if not lp.get("idmap_ldb:use rfc2307"):
3227             self.outf.write("You are setting a Unix/RFC2307 UID & GID. "
3228                             "You may want to set 'idmap_ldb:use rfc2307 = Yes'"
3229                             " in smb.conf to use the attributes for "
3230                             "XID/SID-mapping.\n")
3231
3232         user_mod = """
3233 dn: {0}
3234 changetype: modify
3235 add: uidNumber
3236 uidNumber: {1}
3237 add: gidnumber
3238 gidNumber: {2}
3239 add: gecos
3240 gecos: {3}
3241 add: uid
3242 uid: {4}
3243 add: loginshell
3244 loginShell: {5}
3245 add: unixHomeDirectory
3246 unixHomeDirectory: {6}
3247 """.format(user_dn, uid_number, gid_number, gecos, uid, login_shell, unix_home)
3248
3249         samdb.transaction_start()
3250         try:
3251             samdb.modify_ldif(user_mod)
3252         except ldb.LdbError as e:
3253             raise CommandError("Failed to modify user '{0}': {1}"
3254                                .format(username, e))
3255         else:
3256             samdb.transaction_commit()
3257             self.outf.write("Modified User '{}' successfully\n"
3258                             .format(username))
3259
3260 class cmd_user_unlock(Command):
3261     """Unlock a user account.
3262
3263     This command unlocks a user account in the Active Directory domain. The
3264     username specified on the command is the sAMAccountName. The username may
3265     also be specified using the --filter option.
3266
3267     The command may be run from the root userid or another authorized userid.
3268     The -H or --URL= option can be used to execute the command against a remote
3269     server.
3270
3271     Example:
3272     samba-tool user unlock user1 -H ldap://samba.samdom.example.com \\
3273         --username=Administrator --password=Passw0rd
3274
3275     The example shows how to unlock a user account in the domain against a
3276     remote LDAP server. The -H parameter is used to specify the remote target
3277     server. The --username= and --password= options are used to pass the
3278     username and password of a user that exists on the remote server and is
3279     authorized to issue the command on that server.
3280 """
3281
3282     synopsis = "%prog (<username>|--filter <filter>) [options]"
3283
3284     takes_options = [
3285         Option("-H",
3286                "--URL",
3287                help="LDB URL for database or target server",
3288                type=str,
3289                metavar="URL",
3290                dest="H"),
3291         Option("--filter",
3292                help="LDAP Filter to set password on",
3293                type=str),
3294     ]
3295
3296     takes_args = ["username?"]
3297
3298     takes_optiongroups = {
3299         "sambaopts": options.SambaOptions,
3300         "credopts": options.CredentialsOptions,
3301         "versionopts": options.VersionOptions,
3302     }
3303
3304     def run(self,
3305             username=None,
3306             sambaopts=None,
3307             credopts=None,
3308             versionopts=None,
3309             filter=None,
3310             H=None):
3311         if username is None and filter is None:
3312             raise CommandError("Either the username or '--filter' must be "
3313                                "specified!")
3314
3315         if filter is None:
3316             filter = ("(&(objectClass=user)(sAMAccountName=%s))" % (
3317                 ldb.binary_encode(username)))
3318
3319         lp = sambaopts.get_loadparm()
3320         creds = credopts.get_credentials(lp, fallback_machine=True)
3321
3322         samdb = SamDB(url=H,
3323                       session_info=system_session(),
3324                       credentials=creds,
3325                       lp=lp)
3326         try:
3327             samdb.unlock_account(filter)
3328         except (SamDBError, ldb.LdbError) as msg:
3329             raise CommandError("Failed to unlock user '%s': %s" % (
3330                                username or filter, msg))
3331
3332 class cmd_user_sensitive(Command):
3333     """Set/unset or show UF_NOT_DELEGATED for an account."""
3334
3335     synopsis = "%prog <accountname> [(show|on|off)] [options]"
3336
3337     takes_optiongroups = {
3338         "sambaopts": options.SambaOptions,
3339         "credopts": options.CredentialsOptions,
3340         "versionopts": options.VersionOptions,
3341     }
3342
3343     takes_options = [
3344         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
3345                metavar="URL", dest="H"),
3346     ]
3347
3348     takes_args = ["accountname", "cmd"]
3349
3350     def run(self, accountname, cmd, H=None, credopts=None, sambaopts=None,
3351             versionopts=None):
3352
3353         if cmd not in ("show", "on", "off"):
3354             raise CommandError("invalid argument: '%s' (choose from 'show', 'on', 'off')" % cmd)
3355
3356         lp = sambaopts.get_loadparm()
3357         creds = credopts.get_credentials(lp, fallback_machine=True)
3358         sam = SamDB(url=H, session_info=system_session(),
3359                     credentials=creds, lp=lp)
3360
3361         search_filter = "sAMAccountName=%s" % ldb.binary_encode(accountname)
3362         flag = dsdb.UF_NOT_DELEGATED;
3363
3364         if cmd == "show":
3365             res = sam.search(scope=ldb.SCOPE_SUBTREE, expression=search_filter,
3366                              attrs=["userAccountControl"])
3367             if len(res) == 0:
3368                 raise Exception("Unable to find account where '%s'" % search_filter)
3369
3370             uac = int(res[0].get("userAccountControl")[0])
3371
3372             self.outf.write("Account-DN: %s\n" % str(res[0].dn))
3373             self.outf.write("UF_NOT_DELEGATED: %s\n" % bool(uac & flag))
3374
3375             return
3376
3377         if cmd == "on":
3378             on = True
3379         elif cmd == "off":
3380             on = False
3381
3382         try:
3383             sam.toggle_userAccountFlags(search_filter, flag, flags_str="Not-Delegated",
3384                                         on=on, strict=True)
3385         except Exception as err:
3386             raise CommandError(err)
3387
3388
3389 class cmd_user(SuperCommand):
3390     """User management."""
3391
3392     subcommands = {}
3393     subcommands["add"] = cmd_user_add()
3394     subcommands["create"] = cmd_user_add()
3395     subcommands["delete"] = cmd_user_delete()
3396     subcommands["disable"] = cmd_user_disable()
3397     subcommands["enable"] = cmd_user_enable()
3398     subcommands["list"] = cmd_user_list()
3399     subcommands["setexpiry"] = cmd_user_setexpiry()
3400     subcommands["password"] = cmd_user_password()
3401     subcommands["getgroups"] = cmd_user_getgroups()
3402     subcommands["setprimarygroup"] = cmd_user_setprimarygroup()
3403     subcommands["setpassword"] = cmd_user_setpassword()
3404     subcommands["getpassword"] = cmd_user_getpassword()
3405     subcommands["syncpasswords"] = cmd_user_syncpasswords()
3406     subcommands["edit"] = cmd_user_edit()
3407     subcommands["show"] = cmd_user_show()
3408     subcommands["move"] = cmd_user_move()
3409     subcommands["rename"] = cmd_user_rename()
3410     subcommands["unlock"] = cmd_user_unlock()
3411     subcommands["addunixattrs"] = cmd_user_add_unix_attrs()
3412     subcommands["sensitive"] = cmd_user_sensitive()