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