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