3 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
4 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
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.
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.
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/>.
20 import samba.getopt as options
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
43 generate_random_password,
46 from samba.net import Net
48 from samba.netcmd import (
60 decrypt_samba_gpg_help = "Decrypt the SambaGPG password as cleartext source"
61 except ImportError as e:
63 decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
64 "python-gpgme required"
66 disabled_virtual_attributes = {
69 virtual_attributes = {
70 "virtualClearTextUTF8": {
71 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
73 "virtualClearTextUTF16": {
74 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
77 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
81 get_random_bytes_fn = None
82 if get_random_bytes_fn is None:
85 get_random_bytes_fn = Crypto.Random.get_random_bytes
86 except ImportError as e:
88 if get_random_bytes_fn is None:
91 get_random_bytes_fn = M2Crypto.Rand.rand_bytes
92 except ImportError as e:
96 if get_random_bytes_fn is not None:
98 return "Crypto.Random or M2Crypto.Rand required"
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)
106 def get_crypt_value(alg, utf8pw, rounds=0):
112 salt = get_random_bytes(16)
113 # The salt needs to be in [A-Za-z0-9./]
114 # base64 is close enough and as we had 16
115 # random bytes but only need 16 characters
116 # we can ignore the possible == at the end
117 # of the base64 string
118 # we just need to replace '+' by '.'
119 b64salt = base64.b64encode(salt)[0:16].replace('+', '.')
122 crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
124 crypt_salt = "$%s$%s$" % (alg, b64salt)
126 crypt_value = crypt.crypt(utf8pw, crypt_salt)
127 if crypt_value is None:
128 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
129 expected_len = len(crypt_salt) + algs[alg]["length"]
130 if len(crypt_value) != expected_len:
131 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
132 crypt_salt, len(crypt_value), expected_len))
135 # Extract the rounds value from the options of a virtualCrypt attribute
136 # i.e. options = "rounds=20;other=ignored;" will return 20
137 # if the rounds option is not found or the value is not a number, 0 is returned
138 # which indicates that the default number of rounds should be used.
139 def get_rounds(options):
143 opts = options.split(';')
145 if o.lower().startswith("rounds="):
146 (key, _, val) = o.partition('=')
154 random_reason = check_random()
155 if random_reason is not None:
156 raise ImportError(random_reason)
160 virtual_attributes["virtualSSHA"] = {
162 except ImportError as e:
163 reason = "hashlib.sha1()"
165 reason += " and " + random_reason
166 reason += " required"
167 disabled_virtual_attributes["virtualSSHA"] = {
171 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
173 random_reason = check_random()
174 if random_reason is not None:
175 raise ImportError(random_reason)
177 v = get_crypt_value(alg, "")
179 virtual_attributes[attr] = {
181 except ImportError as e:
184 reason += " and " + random_reason
185 reason += " required"
186 disabled_virtual_attributes[attr] = {
189 except NotImplementedError as e:
190 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
191 disabled_virtual_attributes[attr] = {
195 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
196 for x in range(1, 30):
197 virtual_attributes["virtualWDigest%02d" % x] = {}
199 virtual_attributes_help = "The attributes to display (comma separated). "
200 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
201 if len(disabled_virtual_attributes) != 0:
202 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
204 class cmd_user_create(Command):
205 """Create a new user.
207 This command creates a new user account in the Active Directory domain. The username specified on the command is the sAMaccountName.
209 User accounts may represent physical entities, such as people or may be used as service accounts for applications. User accounts are also referred to as security principals and are assigned a security identifier (SID).
211 A user account enables a user to logon to a computer and domain with an identity that can be authenticated. To maximize security, each user should have their own unique user account and password. A user's access to domain resources is based on permissions assigned to the user account.
213 Unix (RFC2307) attributes may be added to the user account. Attributes taken from NSS are obtained on the local machine. Explicitly given values override values obtained from NSS. Configure 'idmap_ldb:use rfc2307 = Yes' to use these attributes for UID/GID mapping.
215 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server.
218 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
220 Example1 shows how to create a new user in the domain against a remote LDAP server. The -H parameter is used to specify the remote target server. The -U option is used to pass the userid and password authorized to issue the command remotely.
223 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
225 Example2 shows how to create a new user in the domain against the local server. sudo is used so a user may run the command as root. In this example, after User2 is created, he/she will be forced to change their password when they logon.
228 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
230 Example3 shows how to create a new user in the OrgUnit organizational unit.
233 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
235 Example4 shows how to create a new user with Unix UID, GID and login-shell set from the local NSS and GECOS set to 'some text'.
238 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
239 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
241 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
242 --nis-domain is set, then the other four parameters are mandatory.
245 synopsis = "%prog <username> [<password>] [options]"
248 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
249 metavar="URL", dest="H"),
250 Option("--must-change-at-next-login",
251 help="Force password to be changed on next login",
252 action="store_true"),
253 Option("--random-password",
254 help="Generate random password",
255 action="store_true"),
256 Option("--smartcard-required",
257 help="Require a smartcard for interactive logons",
258 action="store_true"),
259 Option("--use-username-as-cn",
260 help="Force use of username as user's CN",
261 action="store_true"),
263 help="DN of alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created. E. g. 'OU=<OU name>'",
265 Option("--surname", help="User's surname", type=str),
266 Option("--given-name", help="User's given name", type=str),
267 Option("--initials", help="User's initials", type=str),
268 Option("--profile-path", help="User's profile path", type=str),
269 Option("--script-path", help="User's logon script path", type=str),
270 Option("--home-drive", help="User's home drive letter", type=str),
271 Option("--home-directory", help="User's home directory path", type=str),
272 Option("--job-title", help="User's job title", type=str),
273 Option("--department", help="User's department", type=str),
274 Option("--company", help="User's company", type=str),
275 Option("--description", help="User's description", type=str),
276 Option("--mail-address", help="User's email address", type=str),
277 Option("--internet-address", help="User's home page", type=str),
278 Option("--telephone-number", help="User's phone number", type=str),
279 Option("--physical-delivery-office", help="User's office location", type=str),
280 Option("--rfc2307-from-nss",
281 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
282 action="store_true"),
283 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
284 Option("--unix-home", help="User's Unix/RFC2307 home directory",
286 Option("--uid", help="User's Unix/RFC2307 username", type=str),
287 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
288 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
289 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
290 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
293 takes_args = ["username", "password?"]
295 takes_optiongroups = {
296 "sambaopts": options.SambaOptions,
297 "credopts": options.CredentialsOptions,
298 "versionopts": options.VersionOptions,
301 def run(self, username, password=None, credopts=None, sambaopts=None,
302 versionopts=None, H=None, must_change_at_next_login=False,
303 random_password=False, use_username_as_cn=False, userou=None,
304 surname=None, given_name=None, initials=None, profile_path=None,
305 script_path=None, home_drive=None, home_directory=None,
306 job_title=None, department=None, company=None, description=None,
307 mail_address=None, internet_address=None, telephone_number=None,
308 physical_delivery_office=None, rfc2307_from_nss=False,
309 nis_domain=None, unix_home=None, uid=None, uid_number=None,
310 gid_number=None, gecos=None, login_shell=None,
311 smartcard_required=False):
313 if smartcard_required:
314 if password is not None and password is not '':
315 raise CommandError('It is not allowed to specify '
317 'together with --smartcard-required.')
318 if must_change_at_next_login:
319 raise CommandError('It is not allowed to specify '
320 '--must-change-at-next-login '
321 'together with --smartcard-required.')
323 if random_password and not smartcard_required:
324 password = generate_random_password(128, 255)
327 if smartcard_required:
329 if password is not None and password is not '':
331 password = getpass("New Password: ")
332 passwordverify = getpass("Retype Password: ")
333 if not password == passwordverify:
335 self.outf.write("Sorry, passwords do not match.\n")
338 pwent = pwd.getpwnam(username)
341 if uid_number is None:
342 uid_number = pwent[2]
343 if gid_number is None:
344 gid_number = pwent[3]
347 if login_shell is None:
348 login_shell = pwent[6]
350 lp = sambaopts.get_loadparm()
351 creds = credopts.get_credentials(lp)
353 if uid_number or gid_number:
354 if not lp.get("idmap_ldb:use rfc2307"):
355 self.outf.write("You are setting a Unix/RFC2307 UID or GID. You may want to set 'idmap_ldb:use rfc2307 = Yes' to use those attributes for XID/SID-mapping.\n")
357 if nis_domain is not None:
358 if None in (uid_number, login_shell, unix_home, gid_number):
359 raise CommandError('Missing parameters. To enable NIS features, '
360 'the following options have to be given: '
361 '--nis-domain=, --uidNumber=, --login-shell='
362 ', --unix-home=, --gid-number= Operation '
366 samdb = SamDB(url=H, session_info=system_session(),
367 credentials=creds, lp=lp)
368 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
369 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
370 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
371 jobtitle=job_title, department=department, company=company, description=description,
372 mailaddress=mail_address, internetaddress=internet_address,
373 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
374 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
375 uidnumber=uid_number, gidnumber=gid_number,
376 gecos=gecos, loginshell=login_shell,
377 smartcard_required=smartcard_required)
379 raise CommandError("Failed to add user '%s': " % username, e)
381 self.outf.write("User '%s' created successfully\n" % username)
384 class cmd_user_add(cmd_user_create):
385 __doc__ = cmd_user_create.__doc__
386 # take this print out after the add subcommand is removed.
387 # the add subcommand is deprecated but left in for now to allow people to
390 def run(self, *args, **kwargs):
392 "Note: samba-tool user add is deprecated. "
393 "Please use samba-tool user create for the same function.\n")
394 return super(cmd_user_add, self).run(*args, **kwargs)
397 class cmd_user_delete(Command):
400 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
402 Once the account is deleted, all permissions and memberships associated with that account are deleted. If a new user account is added with the same name as a previously deleted account name, the new user does not have the previous permissions. The new account user will be assigned a new security identifier (SID) and permissions and memberships will have to be added.
404 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server.
407 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
409 Example1 shows how to delete a user in the domain against a remote LDAP server. The -H parameter is used to specify the remote target server. The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to issue the command on that server.
412 sudo samba-tool user delete User2
414 Example2 shows how to delete a user in the domain against the local server. sudo is used so a user may run the command as root.
417 synopsis = "%prog <username> [options]"
420 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
421 metavar="URL", dest="H"),
424 takes_args = ["username"]
425 takes_optiongroups = {
426 "sambaopts": options.SambaOptions,
427 "credopts": options.CredentialsOptions,
428 "versionopts": options.VersionOptions,
431 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
433 lp = sambaopts.get_loadparm()
434 creds = credopts.get_credentials(lp, fallback_machine=True)
436 samdb = SamDB(url=H, session_info=system_session(),
437 credentials=creds, lp=lp)
439 filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
443 res = samdb.search(base=samdb.domain_dn(),
444 scope=ldb.SCOPE_SUBTREE,
449 raise CommandError('Unable to find user "%s"' % (username))
452 samdb.delete(user_dn)
454 raise CommandError('Failed to remove user "%s"' % username, e)
455 self.outf.write("Deleted user %s\n" % username)
458 class cmd_user_list(Command):
459 """List all users."""
461 synopsis = "%prog [options]"
464 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
465 metavar="URL", dest="H"),
468 takes_optiongroups = {
469 "sambaopts": options.SambaOptions,
470 "credopts": options.CredentialsOptions,
471 "versionopts": options.VersionOptions,
474 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
475 lp = sambaopts.get_loadparm()
476 creds = credopts.get_credentials(lp, fallback_machine=True)
478 samdb = SamDB(url=H, session_info=system_session(),
479 credentials=creds, lp=lp)
481 domain_dn = samdb.domain_dn()
482 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
483 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
484 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
485 attrs=["samaccountname"])
490 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
493 class cmd_user_enable(Command):
496 This command enables a user account for logon to an Active Directory domain. The username specified on the command is the sAMAccountName. The username may also be specified using the --filter option.
498 There are many reasons why an account may become disabled. These include:
499 - If a user exceeds the account policy for logon attempts
500 - If an administrator disables the account
501 - If the account expires
503 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
505 Additionally, the enable function allows an administrator to have a set of created user accounts defined and setup with default permissions that can be easily enabled for use.
507 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server.
510 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
512 Example1 shows how to enable a user in the domain against a remote LDAP server. The --URL parameter is used to specify the remote target server. The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server.
515 su samba-tool user enable Testuser2
517 Example2 shows how to enable user Testuser2 for use in the domain on the local server. sudo is used so a user may run the command as root.
520 samba-tool user enable --filter=samaccountname=Testuser3
522 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
525 synopsis = "%prog (<username>|--filter <filter>) [options]"
528 takes_optiongroups = {
529 "sambaopts": options.SambaOptions,
530 "versionopts": options.VersionOptions,
531 "credopts": options.CredentialsOptions,
535 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
536 metavar="URL", dest="H"),
537 Option("--filter", help="LDAP Filter to set password on", type=str),
540 takes_args = ["username?"]
542 def run(self, username=None, sambaopts=None, credopts=None,
543 versionopts=None, filter=None, H=None):
544 if username is None and filter is None:
545 raise CommandError("Either the username or '--filter' must be specified!")
548 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
550 lp = sambaopts.get_loadparm()
551 creds = credopts.get_credentials(lp, fallback_machine=True)
553 samdb = SamDB(url=H, session_info=system_session(),
554 credentials=creds, lp=lp)
556 samdb.enable_account(filter)
557 except Exception, msg:
558 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
559 self.outf.write("Enabled user '%s'\n" % (username or filter))
562 class cmd_user_disable(Command):
563 """Disable an user."""
565 synopsis = "%prog (<username>|--filter <filter>) [options]"
568 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
569 metavar="URL", dest="H"),
570 Option("--filter", help="LDAP Filter to set password on", type=str),
573 takes_args = ["username?"]
575 takes_optiongroups = {
576 "sambaopts": options.SambaOptions,
577 "credopts": options.CredentialsOptions,
578 "versionopts": options.VersionOptions,
581 def run(self, username=None, sambaopts=None, credopts=None,
582 versionopts=None, filter=None, H=None):
583 if username is None and filter is None:
584 raise CommandError("Either the username or '--filter' must be specified!")
587 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
589 lp = sambaopts.get_loadparm()
590 creds = credopts.get_credentials(lp, fallback_machine=True)
592 samdb = SamDB(url=H, session_info=system_session(),
593 credentials=creds, lp=lp)
595 samdb.disable_account(filter)
596 except Exception, msg:
597 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
600 class cmd_user_setexpiry(Command):
601 """Set the expiration of a user account.
603 The user can either be specified by their sAMAccountName or using the --filter option.
605 When a user account expires, it becomes disabled and the user is unable to logon. The administrator may issue the samba-tool user enable command to enable the account for logon. The permissions and memberships associated with the account are retained when the account is enabled.
607 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command on a remote server.
610 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
612 Example1 shows how to set the expiration of an account in a remote LDAP server. The --URL parameter is used to specify the remote target server. The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server.
615 su samba-tool user setexpiry User2
617 Example2 shows how to set the account expiration of user User2 so it will never expire. The user in this example resides on the local server. sudo is used so a user may run the command as root.
620 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
622 Example3 shows how to set the account expiration date to end of day 20 days from the current day. The username or sAMAccountName is specified using the --filter= parameter and the username in this example is User3.
625 samba-tool user setexpiry --noexpiry User4
626 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
629 synopsis = "%prog (<username>|--filter <filter>) [options]"
631 takes_optiongroups = {
632 "sambaopts": options.SambaOptions,
633 "versionopts": options.VersionOptions,
634 "credopts": options.CredentialsOptions,
638 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
639 metavar="URL", dest="H"),
640 Option("--filter", help="LDAP Filter to set password on", type=str),
641 Option("--days", help="Days to expiry", type=int, default=0),
642 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
645 takes_args = ["username?"]
647 def run(self, username=None, sambaopts=None, credopts=None,
648 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
649 if username is None and filter is None:
650 raise CommandError("Either the username or '--filter' must be specified!")
653 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
655 lp = sambaopts.get_loadparm()
656 creds = credopts.get_credentials(lp)
658 samdb = SamDB(url=H, session_info=system_session(),
659 credentials=creds, lp=lp)
662 samdb.setexpiry(filter, days*24*3600, no_expiry_req=noexpiry)
663 except Exception, msg:
664 # FIXME: Catch more specific exception
665 raise CommandError("Failed to set expiry for user '%s': %s" % (
666 username or filter, msg))
668 self.outf.write("Expiry for user '%s' disabled.\n" % (
671 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
672 username or filter, days))
675 class cmd_user_password(Command):
676 """Change password for a user account (the one provided in authentication).
679 synopsis = "%prog [options]"
682 Option("--newpassword", help="New password", type=str),
685 takes_optiongroups = {
686 "sambaopts": options.SambaOptions,
687 "credopts": options.CredentialsOptions,
688 "versionopts": options.VersionOptions,
691 def run(self, credopts=None, sambaopts=None, versionopts=None,
694 lp = sambaopts.get_loadparm()
695 creds = credopts.get_credentials(lp)
697 # get old password now, to get the password prompts in the right order
698 old_password = creds.get_password()
700 net = Net(creds, lp, server=credopts.ipaddress)
702 password = newpassword
704 if password is not None and password is not '':
706 password = getpass("New Password: ")
707 passwordverify = getpass("Retype Password: ")
708 if not password == passwordverify:
710 self.outf.write("Sorry, passwords do not match.\n")
713 net.change_password(password.encode('utf-8'))
714 except Exception, msg:
715 # FIXME: catch more specific exception
716 raise CommandError("Failed to change password : %s" % msg)
717 self.outf.write("Changed password OK\n")
720 class cmd_user_setpassword(Command):
721 """Set or reset the password of a user account.
723 This command sets or resets the logon password for a user account. The username specified on the command is the sAMAccountName. The username may also be specified using the --filter option.
725 If the password is not specified on the command through the --newpassword parameter, the user is prompted for the password to be entered through the command line.
727 It is good security practice for the administrator to use the --must-change-at-next-login option which requires that when the user logs on to the account for the first time following the password change, he/she must change the password.
729 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server.
732 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
734 Example1 shows how to set the password of user TestUser1 on a remote LDAP server. The --URL parameter is used to specify the remote target server. The -U option is used to pass the username and password of a user that exists on the remote server and is authorized to update the server.
737 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
739 Example2 shows how an administrator would reset the TestUser2 user's password to passw0rd. The user is running under the root userid using the sudo command. In this example the user TestUser2 must change their password the next time they logon to the account.
742 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
744 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
747 synopsis = "%prog (<username>|--filter <filter>) [options]"
749 takes_optiongroups = {
750 "sambaopts": options.SambaOptions,
751 "versionopts": options.VersionOptions,
752 "credopts": options.CredentialsOptions,
756 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
757 metavar="URL", dest="H"),
758 Option("--filter", help="LDAP Filter to set password on", type=str),
759 Option("--newpassword", help="Set password", type=str),
760 Option("--must-change-at-next-login",
761 help="Force password to be changed on next login",
762 action="store_true"),
763 Option("--random-password",
764 help="Generate random password",
765 action="store_true"),
766 Option("--smartcard-required",
767 help="Require a smartcard for interactive logons",
768 action="store_true"),
769 Option("--clear-smartcard-required",
770 help="Don't require a smartcard for interactive logons",
771 action="store_true"),
774 takes_args = ["username?"]
776 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
777 versionopts=None, H=None, newpassword=None,
778 must_change_at_next_login=False, random_password=False,
779 smartcard_required=False, clear_smartcard_required=False):
780 if filter is None and username is None:
781 raise CommandError("Either the username or '--filter' must be specified!")
783 password = newpassword
785 if smartcard_required:
786 if password is not None and password is not '':
787 raise CommandError('It is not allowed to specify '
789 'together with --smartcard-required.')
790 if must_change_at_next_login:
791 raise CommandError('It is not allowed to specify '
792 '--must-change-at-next-login '
793 'together with --smartcard-required.')
794 if clear_smartcard_required:
795 raise CommandError('It is not allowed to specify '
796 '--clear-smartcard-required '
797 'together with --smartcard-required.')
799 if random_password and not smartcard_required:
800 password = generate_random_password(128, 255)
803 if smartcard_required:
805 if password is not None and password is not '':
807 password = getpass("New Password: ")
808 passwordverify = getpass("Retype Password: ")
809 if not password == passwordverify:
811 self.outf.write("Sorry, passwords do not match.\n")
814 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
816 lp = sambaopts.get_loadparm()
817 creds = credopts.get_credentials(lp)
819 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
821 samdb = SamDB(url=H, session_info=system_session(),
822 credentials=creds, lp=lp)
824 if smartcard_required:
827 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
828 flags = dsdb.UF_SMARTCARD_REQUIRED
829 samdb.toggle_userAccountFlags(filter, flags, on=True)
830 command = "Failed to enable account for user '%s'" % (username or filter)
831 samdb.enable_account(filter)
832 except Exception, msg:
833 # FIXME: catch more specific exception
834 raise CommandError("%s: %s" % (command, msg))
835 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
839 if clear_smartcard_required:
840 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
841 flags = dsdb.UF_SMARTCARD_REQUIRED
842 samdb.toggle_userAccountFlags(filter, flags, on=False)
843 command = "Failed to set password for user '%s'" % (username or filter)
844 samdb.setpassword(filter, password,
845 force_change_at_next_login=must_change_at_next_login,
847 except Exception, msg:
848 # FIXME: catch more specific exception
849 raise CommandError("%s: %s" % (command, msg))
850 self.outf.write("Changed password OK\n")
852 class GetPasswordCommand(Command):
855 super(GetPasswordCommand, self).__init__()
858 def connect_system_samdb(self, url, allow_local=False, verbose=False):
860 # using anonymous here, results in no authentication
861 # which means we can get system privileges via
862 # the privileged ldapi socket
863 creds = credentials.Credentials()
864 creds.set_anonymous()
866 if url is None and allow_local:
868 elif url.lower().startswith("ldapi://"):
870 elif url.lower().startswith("ldap://"):
871 raise CommandError("--url ldap:// is not supported for this command")
872 elif url.lower().startswith("ldaps://"):
873 raise CommandError("--url ldaps:// is not supported for this command")
874 elif not allow_local:
875 raise CommandError("--url requires an ldapi:// url for this command")
878 self.outf.write("Connecting to '%s'\n" % url)
880 samdb = SamDB(url=url, session_info=system_session(),
881 credentials=creds, lp=self.lp)
885 # Make sure we're connected as SYSTEM
887 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
889 sids = res[0].get("tokenGroups")
890 assert len(sids) == 1
891 sid = ndr_unpack(security.dom_sid, sids[0])
892 assert str(sid) == security.SID_NT_SYSTEM
893 except Exception as msg:
894 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
895 (security.SID_NT_SYSTEM))
897 # We use sort here in order to have a predictable processing order
898 # this might not be strictly needed, but also doesn't hurt here
899 for a in sorted(virtual_attributes.keys()):
900 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
901 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
905 def get_account_attributes(self, samdb, username, basedn, filter, scope,
912 (attr, _, opts) = a.partition(';')
914 attr_opts[attr] = opts
916 attr_opts[attr] = None
917 search_attrs.append(attr)
918 lower_attrs = [x.lower() for x in search_attrs]
920 require_supplementalCredentials = False
921 for a in virtual_attributes.keys():
922 if a.lower() in lower_attrs:
923 require_supplementalCredentials = True
924 add_supplementalCredentials = False
925 add_unicodePwd = False
926 if require_supplementalCredentials:
927 a = "supplementalCredentials"
928 if a.lower() not in lower_attrs:
930 add_supplementalCredentials = True
932 if a.lower() not in lower_attrs:
934 add_unicodePwd = True
935 add_sAMAcountName = False
937 if a.lower() not in lower_attrs:
939 add_sAMAcountName = True
941 add_userPrincipalName = False
942 upn = "usePrincipalName"
943 if upn.lower() not in lower_attrs:
944 search_attrs += [upn]
945 add_userPrincipalName = True
947 if scope == ldb.SCOPE_BASE:
948 search_controls = ["show_deleted:1", "show_recycled:1"]
952 res = samdb.search(base=basedn, expression=filter,
953 scope=scope, attrs=search_attrs,
954 controls=search_controls)
956 raise Exception('Unable to find user "%s"' % (username or filter))
958 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
959 except Exception as msg:
960 # FIXME: catch more specific exception
961 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
966 if "supplementalCredentials" in obj:
967 sc_blob = obj["supplementalCredentials"][0]
968 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
969 if add_supplementalCredentials:
970 del obj["supplementalCredentials"]
971 if "unicodePwd" in obj:
972 unicodePwd = obj["unicodePwd"][0]
974 del obj["unicodePwd"]
975 account_name = obj["sAMAccountName"][0]
976 if add_sAMAcountName:
977 del obj["sAMAccountName"]
978 if "userPrincipalName" in obj:
979 account_upn = obj["userPrincipalName"][0]
981 realm = self.lp.get("realm")
982 account_upn = "%s@%s" % (account_name, realm.lower())
983 if add_userPrincipalName:
984 del obj["userPrincipalName"]
987 def get_package(name, min_idx=0):
988 if name in calculated:
989 return calculated[name]
993 min_idx = len(sc.sub.packages) + min_idx
995 for p in sc.sub.packages:
1002 return binascii.a2b_hex(p.data)
1007 # Samba adds 'Primary:SambaGPG' at the end.
1008 # When Windows sets the password it keeps
1009 # 'Primary:SambaGPG' and rotates it to
1010 # the begining. So we can only use the value,
1011 # if it is the last one.
1013 # In order to get more protection we verify
1014 # the nthash of the decrypted utf16 password
1015 # against the stored nthash in unicodePwd.
1017 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1018 if sgv is not None and unicodePwd is not None:
1019 ctx = gpgme.Context()
1021 cipher_io = io.BytesIO(sgv)
1022 plain_io = io.BytesIO()
1024 ctx.decrypt(cipher_io, plain_io)
1025 cv = plain_io.getvalue()
1027 # We only use the password if it matches
1028 # the current nthash stored in the unicodePwd
1031 tmp = credentials.Credentials()
1033 tmp.set_utf16_password(cv)
1034 nthash = tmp.get_nt_hash()
1035 if nthash == unicodePwd:
1036 calculated["Primary:CLEARTEXT"] = cv
1037 except gpgme.GpgmeError as (major, minor, msg):
1038 if major == gpgme.ERR_BAD_SECKEY:
1039 msg = "ERR_BAD_SECKEY: " + msg
1041 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1042 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1043 username or account_name, msg))
1045 def get_utf8(a, b, username):
1047 u = unicode(b, 'utf-16-le')
1048 except UnicodeDecodeError as e:
1049 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1052 u8 = u.encode('utf-8')
1055 # Extract the WDigest hash for the value specified by i.
1056 # Builds an htdigest compatible value
1058 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1059 domain, dns_domain):
1064 user = account_name.lower()
1065 realm = domain.lower()
1067 user = account_name.upper()
1068 realm = domain.upper()
1071 realm = domain.upper()
1074 realm = domain.lower()
1076 user = account_name.upper()
1077 realm = domain.lower()
1079 user = account_name.lower()
1080 realm = domain.upper()
1083 realm = dns_domain.lower()
1085 user = account_name.lower()
1086 realm = dns_domain.lower()
1088 user = account_name.upper()
1089 realm = dns_domain.upper()
1092 realm = dns_domain.upper()
1095 realm = dns_domain.lower()
1097 user = account_name.upper()
1098 realm = dns_domain.lower()
1100 user = account_name.lower()
1101 realm = dns_domain.upper()
1106 user = account_upn.lower()
1109 user = account_upn.upper()
1112 user = "%s\\%s" % (domain, account_name)
1115 user = "%s\\%s" % (domain.lower(), account_name.lower())
1118 user = "%s\\%s" % (domain.upper(), account_name.upper())
1124 user = account_name.lower()
1127 user = account_name.upper()
1133 user = account_upn.lower()
1136 user = account_upn.upper()
1139 user = "%s\\%s" % (domain, account_name)
1142 # Differs from spec, see tests
1143 user = "%s\\%s" % (domain.lower(), account_name.lower())
1146 # Differs from spec, see tests
1147 user = "%s\\%s" % (domain.upper(), account_name.upper())
1152 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1155 digest = binascii.hexlify(bytearray(digests.hashes[i-1].hash))
1156 return "%s:%s:%s" % (user, realm, digest)
1161 # get the value for a virtualCrypt attribute.
1162 # look for an exact match on algorithm and rounds in supplemental creds
1163 # if not found calculate using Primary:CLEARTEXT
1164 # if no Primary:CLEARTEXT return the first supplementalCredential
1165 # that matches the algorithm.
1166 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1169 b = get_package("Primary:userPassword")
1171 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1173 # No exact match on algorithm and number of rounds
1174 # try and calculate one from the Primary:CLEARTEXT
1175 b = get_package("Primary:CLEARTEXT")
1177 u8 = get_utf8(a, b, username or account_name)
1179 sv = get_crypt_value(str(algorithm), u8, rounds)
1181 # Unable to calculate a hash with the specified
1182 # number of rounds, fall back to the first hash using
1183 # the specified algorithm
1187 return "{CRYPT}" + sv
1189 def get_userPassword_hash(blob, algorithm, rounds):
1190 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1193 # Check that the NT hash has not been changed without updating
1194 # the user password hashes. This indicates that password has been
1195 # changed without updating the supplemental credentials.
1196 if unicodePwd != bytearray(up.current_nt_hash.hash):
1199 scheme_prefix = "$%d$" % algorithm
1200 prefix = scheme_prefix
1202 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1206 if (scheme_match is None and
1207 h.scheme == SCHEME and
1208 h.value.startswith(scheme_prefix)):
1209 scheme_match = h.value
1210 if h.scheme == SCHEME and h.value.startswith(prefix):
1211 return (h.value, scheme_match)
1213 # No match on the number of rounds, return the value of the
1214 # first matching scheme
1215 return (None, scheme_match)
1217 # We use sort here in order to have a predictable processing order
1218 for a in sorted(virtual_attributes.keys()):
1219 if not a.lower() in lower_attrs:
1222 if a == "virtualClearTextUTF8":
1223 b = get_package("Primary:CLEARTEXT")
1226 u8 = get_utf8(a, b, username or account_name)
1230 elif a == "virtualClearTextUTF16":
1231 v = get_package("Primary:CLEARTEXT")
1234 elif a == "virtualSSHA":
1235 b = get_package("Primary:CLEARTEXT")
1238 u8 = get_utf8(a, b, username or account_name)
1241 salt = get_random_bytes(4)
1245 bv = h.digest() + salt
1246 v = "{SSHA}" + base64.b64encode(bv)
1247 elif a == "virtualCryptSHA256":
1248 rounds = get_rounds(attr_opts[a])
1249 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1253 elif a == "virtualCryptSHA512":
1254 rounds = get_rounds(attr_opts[a])
1255 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1259 elif a == "virtualSambaGPG":
1260 # Samba adds 'Primary:SambaGPG' at the end.
1261 # When Windows sets the password it keeps
1262 # 'Primary:SambaGPG' and rotates it to
1263 # the begining. So we can only use the value,
1264 # if it is the last one.
1265 v = get_package("Primary:SambaGPG", min_idx=-1)
1268 elif a.startswith("virtualWDigest"):
1269 primary_wdigest = get_package("Primary:WDigest")
1270 if primary_wdigest is None:
1272 x = a[len("virtualWDigest"):]
1277 domain = self.lp.get("workgroup")
1278 dns_domain = samdb.domain_dns_name()
1279 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1284 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1287 def parse_attributes(self, attributes):
1289 if attributes is None:
1290 raise CommandError("Please specify --attributes")
1291 attrs = attributes.split(',')
1294 pa = pa.lstrip().rstrip()
1295 for da in disabled_virtual_attributes.keys():
1296 if pa.lower() == da.lower():
1297 r = disabled_virtual_attributes[da]["reason"]
1298 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1300 for va in virtual_attributes.keys():
1301 if pa.lower() == va.lower():
1302 # Take the real name
1305 password_attrs += [pa]
1307 return password_attrs
1309 class cmd_user_getpassword(GetPasswordCommand):
1310 """Get the password fields of a user/computer account.
1312 This command gets the logon password for a user/computer account.
1314 The username specified on the command is the sAMAccountName.
1315 The username may also be specified using the --filter option.
1317 The command must be run from the root user id or another authorized user id.
1318 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1319 used to adjust the local path. By default tdb:// is used by default.
1321 The '--attributes' parameter takes a comma separated list of attributes,
1322 which will be printed or given to the script specified by '--script'. If a
1323 specified attribute is not available on an object it's silently omitted.
1324 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1325 the NTHASH) and the following virtual attributes are possible (see --help
1326 for which virtual attributes are supported in your environment):
1328 virtualClearTextUTF16: The raw cleartext as stored in the
1329 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1330 with '--decrypt-samba-gpg') buffer inside of the
1331 supplementalCredentials attribute. This typically
1332 contains valid UTF-16-LE, but may contain random
1333 bytes, e.g. for computer accounts.
1335 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1336 (only from valid UTF-16-LE)
1338 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1339 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1341 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1342 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1343 with a $5$... salt, see crypt(3) on modern systems.
1344 The number of rounds used to calculate the hash can
1345 also be specified. By appending ";rounds=x" to the
1346 attribute name i.e. virtualCryptSHA256;rounds=10000
1347 will calculate a SHA256 hash with 10,000 rounds.
1348 non numeric values for rounds are silently ignored
1349 The value is calculated as follows:
1350 1) If a value exists in 'Primary:userPassword' with
1351 the specified number of rounds it is returned.
1352 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1353 '--decrypt-samba-gpg'. Calculate a hash with
1354 the specified number of rounds
1355 3) Return the first CryptSHA256 value in
1356 'Primary:userPassword'
1359 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1360 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1361 with a $6$... salt, see crypt(3) on modern systems.
1362 The number of rounds used to calculate the hash can
1363 also be specified. By appending ";rounds=x" to the
1364 attribute name i.e. virtualCryptSHA512;rounds=10000
1365 will calculate a SHA512 hash with 10,000 rounds.
1366 non numeric values for rounds are silently ignored
1367 The value is calculated as follows:
1368 1) If a value exists in 'Primary:userPassword' with
1369 the specified number of rounds it is returned.
1370 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1371 '--decrypt-samba-gpg'. Calculate a hash with
1372 the specified number of rounds
1373 3) Return the first CryptSHA512 value in
1374 'Primary:userPassword'
1376 virtualWDigestNN: The individual hash values stored in
1377 'Primary:WDigest' where NN is the hash number in
1379 NOTE: As at 22-05-2017 the documentation:
1380 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1381 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1384 virtualSambaGPG: The raw cleartext as stored in the
1385 'Primary:SambaGPG' buffer inside of the
1386 supplementalCredentials attribute.
1387 See the 'password hash gpg key ids' option in
1390 The '--decrypt-samba-gpg' option triggers decryption of the
1391 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1392 in your environment or not (the python-gpgme package is required). Please
1393 note that you might need to set the GNUPGHOME environment variable. If the
1394 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1395 environment variable has been set correctly and the passphrase is already
1396 known by the gpg-agent.
1399 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1402 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1406 super(cmd_user_getpassword, self).__init__()
1408 synopsis = "%prog (<username>|--filter <filter>) [options]"
1410 takes_optiongroups = {
1411 "sambaopts": options.SambaOptions,
1412 "versionopts": options.VersionOptions,
1416 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1417 metavar="URL", dest="H"),
1418 Option("--filter", help="LDAP Filter to set password on", type=str),
1419 Option("--attributes", type=str,
1420 help=virtual_attributes_help,
1421 metavar="ATTRIBUTELIST", dest="attributes"),
1422 Option("--decrypt-samba-gpg",
1423 help=decrypt_samba_gpg_help,
1424 action="store_true", default=False, dest="decrypt_samba_gpg"),
1427 takes_args = ["username?"]
1429 def run(self, username=None, H=None, filter=None,
1430 attributes=None, decrypt_samba_gpg=None,
1431 sambaopts=None, versionopts=None):
1432 self.lp = sambaopts.get_loadparm()
1434 if decrypt_samba_gpg and not gpgme_support:
1435 raise CommandError(decrypt_samba_gpg_help)
1437 if filter is None and username is None:
1438 raise CommandError("Either the username or '--filter' must be specified!")
1441 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1443 if attributes is None:
1444 raise CommandError("Please specify --attributes")
1446 password_attrs = self.parse_attributes(attributes)
1448 samdb = self.connect_system_samdb(url=H, allow_local=True)
1450 obj = self.get_account_attributes(samdb, username,
1453 scope=ldb.SCOPE_SUBTREE,
1454 attrs=password_attrs,
1455 decrypt=decrypt_samba_gpg)
1457 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1458 self.outf.write("%s" % ldif)
1459 self.outf.write("Got password OK\n")
1461 class cmd_user_syncpasswords(GetPasswordCommand):
1462 """Sync the password of user accounts.
1464 This syncs logon passwords for user accounts.
1466 Note that this command should run on a single domain controller only
1467 (typically the PDC-emulator). However the "password hash gpg key ids"
1468 option should to be configured on all domain controllers.
1470 The command must be run from the root user id or another authorized user id.
1471 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1472 local path. By default, ldapi:// is used with the default path to the
1473 privileged ldapi socket.
1475 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1476 "Sync Loop Terminate".
1479 Cache Initialization
1480 ====================
1482 The first time, this command needs to be called with
1483 '--cache-ldb-initialize' in order to initialize its cache.
1485 The cache initialization requires '--attributes' and allows the following
1486 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1489 The '--attributes' parameter takes a comma separated list of attributes,
1490 which will be printed or given to the script specified by '--script'. If a
1491 specified attribute is not available on an object it will be silently omitted.
1492 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1493 the NTHASH) and the following virtual attributes are possible (see '--help'
1494 for supported virtual attributes in your environment):
1496 virtualClearTextUTF16: The raw cleartext as stored in the
1497 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1498 with '--decrypt-samba-gpg') buffer inside of the
1499 supplementalCredentials attribute. This typically
1500 contains valid UTF-16-LE, but may contain random
1501 bytes, e.g. for computer accounts.
1503 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1504 (only from valid UTF-16-LE)
1506 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1507 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1509 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1510 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1511 with a $5$... salt, see crypt(3) on modern systems.
1512 The number of rounds used to calculate the hash can
1513 also be specified. By appending ";rounds=x" to the
1514 attribute name i.e. virtualCryptSHA256;rounds=10000
1515 will calculate a SHA256 hash with 10,000 rounds.
1516 non numeric values for rounds are silently ignored
1517 The value is calculated as follows:
1518 1) If a value exists in 'Primary:userPassword' with
1519 the specified number of rounds it is returned.
1520 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1521 '--decrypt-samba-gpg'. Calculate a hash with
1522 the specified number of rounds
1523 3) Return the first CryptSHA256 value in
1524 'Primary:userPassword'
1526 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1527 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1528 with a $6$... salt, see crypt(3) on modern systems.
1529 The number of rounds used to calculate the hash can
1530 also be specified. By appending ";rounds=x" to the
1531 attribute name i.e. virtualCryptSHA512;rounds=10000
1532 will calculate a SHA512 hash with 10,000 rounds.
1533 non numeric values for rounds are silently ignored
1534 The value is calculated as follows:
1535 1) If a value exists in 'Primary:userPassword' with
1536 the specified number of rounds it is returned.
1537 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1538 '--decrypt-samba-gpg'. Calculate a hash with
1539 the specified number of rounds
1540 3) Return the first CryptSHA512 value in
1541 'Primary:userPassword'
1543 virtualWDigestNN: The individual hash values stored in
1544 'Primary:WDigest' where NN is the hash number in
1546 NOTE: As at 22-05-2017 the documentation:
1547 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1548 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1551 virtualSambaGPG: The raw cleartext as stored in the
1552 'Primary:SambaGPG' buffer inside of the
1553 supplementalCredentials attribute.
1554 See the 'password hash gpg key ids' option in
1557 The '--decrypt-samba-gpg' option triggers decryption of the
1558 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1559 in your environment or not (the python-gpgme package is required). Please
1560 note that you might need to set the GNUPGHOME environment variable. If the
1561 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1562 environment variable has been set correctly and the passphrase is already
1563 known by the gpg-agent.
1565 The '--script' option specifies a custom script that is called whenever any
1566 of the dirsyncAttributes (see below) was changed. The script is called
1567 without any arguments. It gets the LDIF for exactly one object on STDIN.
1568 If the script processed the object successfully it has to respond with a
1569 single line starting with 'DONE-EXIT: ' followed by an optional message.
1571 Note that the script might be called without any password change, e.g. if
1572 the account was disabled (an userAccountControl change) or the
1573 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1574 are always returned as unique identifier of the account. It might be useful
1575 to also ask for non-password attributes like: objectSid, sAMAccountName,
1576 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1577 Depending on the object, some attributes may not be present/available,
1578 but you always get the current state (and not a diff).
1580 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1583 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1584 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1585 (!(sAMAccountName=krbtgt*)))
1586 This means only normal (non-krbtgt) user
1587 accounts are monitored. The '--filter' can modify that, e.g. if it's
1588 required to also sync computer accounts.
1594 This (default) mode runs in an endless loop waiting for password related
1595 changes in the active directory database. It makes use of the
1596 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1597 get changes in a reliable fashion. Objects are monitored for changes of the
1598 following dirsyncAttributes:
1600 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1601 userPrincipalName and userAccountControl.
1603 It recovers from LDAP disconnects and updates the cache in conservative way
1604 (in single steps after each succesfully processed change). An error from
1605 the script (specified by '--script') will result in fatal error and this
1606 command will exit. But the cache state should be still valid and can be
1607 resumed in the next "Sync Loop Run".
1609 The '--logfile' option specifies an optional (required if '--daemon' is
1610 specified) logfile that takes all output of the command. The logfile is
1611 automatically reopened if fstat returns st_nlink == 0.
1613 The optional '--daemon' option will put the command into the background.
1615 You can stop the command without the '--daemon' option, also by hitting
1618 If you specify the '--no-wait' option the command skips the
1619 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1620 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1625 In order to terminate an already running command (likely as daemon) the
1626 '--terminate' option can be used. This also requires the '--logfile' option
1631 samba-tool user syncpasswords --cache-ldb-initialize \\
1632 --attributes=virtualClearTextUTF8
1633 samba-tool user syncpasswords
1636 samba-tool user syncpasswords --cache-ldb-initialize \\
1637 --attributes=objectGUID,objectSID,sAMAccountName,\\
1638 userPrincipalName,userAccountControl,pwdLastSet,\\
1639 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1640 --script=/path/to/my-custom-syncpasswords-script.py
1641 samba-tool user syncpasswords --daemon \\
1642 --logfile=/var/log/samba/user-syncpasswords.log
1643 samba-tool user syncpasswords --terminate \\
1644 --logfile=/var/log/samba/user-syncpasswords.log
1648 super(cmd_user_syncpasswords, self).__init__()
1650 synopsis = "%prog [--cache-ldb-initialize] [options]"
1652 takes_optiongroups = {
1653 "sambaopts": options.SambaOptions,
1654 "versionopts": options.VersionOptions,
1658 Option("--cache-ldb-initialize",
1659 help="Initialize the cache for the first time",
1660 dest="cache_ldb_initialize", action="store_true"),
1661 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1662 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1663 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1664 metavar="URL", dest="H"),
1665 Option("--filter", help="optional LDAP filter to set password on", type=str,
1666 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1667 Option("--attributes", type=str,
1668 help=virtual_attributes_help,
1669 metavar="ATTRIBUTELIST", dest="attributes"),
1670 Option("--decrypt-samba-gpg",
1671 help=decrypt_samba_gpg_help,
1672 action="store_true", default=False, dest="decrypt_samba_gpg"),
1673 Option("--script", help="Script that is called for each password change", type=str,
1674 metavar="/path/to/syncpasswords.script", dest="script"),
1675 Option("--no-wait", help="Don't block waiting for changes",
1676 action="store_true", default=False, dest="nowait"),
1677 Option("--logfile", type=str,
1678 help="The logfile to use (required in --daemon mode).",
1679 metavar="/path/to/syncpasswords.log", dest="logfile"),
1680 Option("--daemon", help="daemonize after initial setup",
1681 action="store_true", default=False, dest="daemon"),
1682 Option("--terminate",
1683 help="Send a SIGTERM to an already running (daemon) process",
1684 action="store_true", default=False, dest="terminate"),
1687 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1688 H=None, filter=None,
1689 attributes=None, decrypt_samba_gpg=None,
1690 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1691 sambaopts=None, versionopts=None):
1693 self.lp = sambaopts.get_loadparm()
1695 self.samdb_url = None
1699 if not cache_ldb_initialize:
1700 if attributes is not None:
1701 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1702 if decrypt_samba_gpg:
1703 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1704 if script is not None:
1705 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1706 if filter is not None:
1707 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1709 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1711 if nowait is not False:
1712 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1713 if logfile is not None:
1714 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1715 if daemon is not False:
1716 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1717 if terminate is not False:
1718 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1722 raise CommandError("--daemon is not allowed together with --no-wait")
1723 if terminate is not False:
1724 raise CommandError("--terminate is not allowed together with --no-wait")
1726 if terminate is True and daemon is True:
1727 raise CommandError("--terminate is not allowed together with --daemon")
1729 if daemon is True and logfile is None:
1730 raise CommandError("--daemon is only allowed together with --logfile")
1732 if terminate is True and logfile is None:
1733 raise CommandError("--terminate is only allowed together with --logfile")
1735 if script is not None:
1736 if not os.path.exists(script):
1737 raise CommandError("script[%s] does not exist!" % script)
1739 sync_command = "%s" % os.path.abspath(script)
1743 dirsync_filter = filter
1744 if dirsync_filter is None:
1745 dirsync_filter = "(&" + \
1746 "(objectClass=user)" + \
1747 "(userAccountControl:%s:=%u)" % (
1748 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1749 "(!(sAMAccountName=krbtgt*))" + \
1752 dirsync_secret_attrs = [
1755 "supplementalCredentials",
1758 dirsync_attrs = dirsync_secret_attrs + [
1761 "userPrincipalName",
1762 "userAccountControl",
1767 password_attrs = None
1769 if cache_ldb_initialize:
1771 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1773 if decrypt_samba_gpg and not gpgme_support:
1774 raise CommandError(decrypt_samba_gpg_help)
1776 password_attrs = self.parse_attributes(attributes)
1777 lower_attrs = [x.lower() for x in password_attrs]
1778 # We always return these in order to track deletions
1779 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1780 if a.lower() not in lower_attrs:
1781 password_attrs += [a]
1783 if cache_ldb is not None:
1784 if cache_ldb.lower().startswith("ldapi://"):
1785 raise CommandError("--cache_ldb ldapi:// is not supported")
1786 elif cache_ldb.lower().startswith("ldap://"):
1787 raise CommandError("--cache_ldb ldap:// is not supported")
1788 elif cache_ldb.lower().startswith("ldaps://"):
1789 raise CommandError("--cache_ldb ldaps:// is not supported")
1790 elif cache_ldb.lower().startswith("tdb://"):
1793 if not os.path.exists(cache_ldb):
1794 cache_ldb = self.lp.private_path(cache_ldb)
1796 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1798 self.lockfile = "%s.pid" % cache_ldb
1801 if self.logfile is not None:
1803 if info.st_nlink == 0:
1804 logfile = self.logfile
1806 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1807 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
1812 log_msg("Reopened logfile[%s]\n" % (logfile))
1813 self.logfile = logfile
1814 msg = "%s: pid[%d]: %s" % (
1818 self.outf.write(msg)
1827 "passwordAttribute",
1833 self.cache = Ldb(cache_ldb)
1834 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1835 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1839 self.samdb_url = res[0]["samdbUrl"][0]
1840 except KeyError as e:
1841 self.samdb_url = None
1843 self.samdb_url = None
1844 if self.samdb_url is None and not cache_ldb_initialize:
1845 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1847 if self.samdb_url is not None and cache_ldb_initialize:
1848 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1850 if self.samdb_url is None:
1852 self.dirsync_filter = dirsync_filter
1853 self.dirsync_attrs = dirsync_attrs
1854 self.dirsync_controls = ["dirsync:1:0:0","extended_dn:1:0"];
1855 self.password_attrs = password_attrs
1856 self.decrypt_samba_gpg = decrypt_samba_gpg
1857 self.sync_command = sync_command
1858 add_ldif = "dn: %s\n" % self.cache_dn
1859 add_ldif += "objectClass: userSyncPasswords\n"
1860 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url)
1861 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter)
1862 for a in self.dirsync_attrs:
1863 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a)
1864 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1865 for a in self.password_attrs:
1866 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
1867 if self.decrypt_samba_gpg == True:
1868 add_ldif += "decryptSambaGPG: TRUE\n"
1870 add_ldif += "decryptSambaGPG: FALSE\n"
1871 if self.sync_command is not None:
1872 add_ldif += "syncCommand: %s\n" % self.sync_command
1873 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1874 self.cache.add_ldif(add_ldif)
1875 self.current_pid = None
1876 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1877 msgs = self.cache.parse_ldif(add_ldif)
1878 changetype,msg = msgs.next()
1879 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1880 self.outf.write("%s" % ldif)
1882 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1883 self.dirsync_attrs = []
1884 for a in res[0]["dirsyncAttribute"]:
1885 self.dirsync_attrs.append(a)
1886 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1887 self.password_attrs = []
1888 for a in res[0]["passwordAttribute"]:
1889 self.password_attrs.append(a)
1890 decrypt_string = res[0]["decryptSambaGPG"][0]
1891 assert(decrypt_string in ["TRUE", "FALSE"])
1892 if decrypt_string == "TRUE":
1893 self.decrypt_samba_gpg = True
1895 self.decrypt_samba_gpg = False
1896 if "syncCommand" in res[0]:
1897 self.sync_command = res[0]["syncCommand"][0]
1899 self.sync_command = None
1900 if "currentPid" in res[0]:
1901 self.current_pid = int(res[0]["currentPid"][0])
1903 self.current_pid = None
1904 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1908 def run_sync_command(dn, ldif):
1909 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1910 sync_command_p = Popen(self.sync_command,
1915 res = sync_command_p.poll()
1918 input = "%s" % (ldif)
1919 reply = sync_command_p.communicate(input)[0]
1920 log_msg("%s\n" % (reply))
1921 res = sync_command_p.poll()
1923 sync_command_p.terminate()
1924 res = sync_command_p.wait()
1926 if reply.startswith("DONE-EXIT: "):
1929 log_msg("RESULT: %s\n" % (res))
1930 raise Exception("ERROR: %s - %s\n" % (res, reply))
1932 def handle_object(idx, dirsync_obj):
1933 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1934 guid = ndr_unpack(misc.GUID, binary_guid)
1935 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1936 sid = ndr_unpack(security.dom_sid, binary_sid)
1937 domain_sid, rid = sid.split()
1938 if rid == security.DOMAIN_RID_KRBTGT:
1939 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1941 for a in list(dirsync_obj.keys()):
1942 for h in dirsync_secret_attrs:
1943 if a.lower() == h.lower():
1945 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1946 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1947 log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif))
1948 obj = self.get_account_attributes(self.samdb,
1949 username="%s" % sid,
1950 basedn="<GUID=%s>" % guid,
1951 filter="(objectClass=user)",
1952 scope=ldb.SCOPE_BASE,
1953 attrs=self.password_attrs,
1954 decrypt=self.decrypt_samba_gpg)
1955 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1956 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1957 if self.sync_command is None:
1958 self.outf.write("%s" % (ldif))
1960 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1961 run_sync_command(obj.dn, ldif)
1963 def check_current_pid_conflict(terminate):
1969 self.lockfd = os.open(self.lockfile, flags, 0600)
1970 except IOError as (err, msg):
1971 if err == errno.ENOENT:
1974 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1975 (self.lockfile, msg, err))
1978 got_exclusive = False
1980 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1981 got_exclusive = True
1982 except IOError as (err, msg):
1983 if err != errno.EACCES and err != errno.EAGAIN:
1984 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
1985 (self.lockfile, msg, err))
1988 if not got_exclusive:
1989 buf = os.read(self.lockfd, 64)
1990 self.current_pid = None
1992 self.current_pid = int(buf)
1993 except ValueError as e:
1995 if self.current_pid is not None:
1998 if got_exclusive and terminate:
2000 os.ftruncate(self.lockfd, 0)
2001 except IOError as (err, msg):
2002 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2003 (self.lockfile, msg, err))
2005 os.close(self.lockfd)
2010 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2011 except IOError as (err, msg):
2012 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2013 (self.lockfile, msg, err))
2015 # We leave the function with the shared lock.
2018 def update_pid(pid):
2019 if self.lockfd != -1:
2020 got_exclusive = False
2021 # Try 5 times to get the exclusiv lock.
2022 for i in xrange(0, 5):
2024 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2025 got_exclusive = True
2026 except IOError as (err, msg):
2027 if err != errno.EACCES and err != errno.EAGAIN:
2028 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2029 (pid, self.lockfile, msg, err))
2034 if not got_exclusive:
2035 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
2036 (pid, self.lockfile))
2037 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2038 (pid, self.lockfile))
2045 os.ftruncate(self.lockfd, 0)
2047 os.write(self.lockfd, buf)
2048 except IOError as (err, msg):
2049 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2050 (self.lockfile, msg, err))
2052 self.current_pid = pid
2053 if self.current_pid is not None:
2054 log_msg("currentPid: %d\n" % self.current_pid)
2056 modify_ldif = "dn: %s\n" % (self.cache_dn)
2057 modify_ldif += "changetype: modify\n"
2058 modify_ldif += "replace: currentPid\n"
2059 if self.current_pid is not None:
2060 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2061 modify_ldif += "replace: currentTime\n"
2062 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2063 self.cache.modify_ldif(modify_ldif)
2066 def update_cache(res_controls):
2067 assert len(res_controls) > 0
2068 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2069 res_controls[0].critical = True
2070 self.dirsync_controls = [str(res_controls[0]),"extended_dn:1:0"]
2071 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2073 modify_ldif = "dn: %s\n" % (self.cache_dn)
2074 modify_ldif += "changetype: modify\n"
2075 modify_ldif += "replace: dirsyncControl\n"
2076 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2077 modify_ldif += "replace: currentTime\n"
2078 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2079 self.cache.modify_ldif(modify_ldif)
2082 def check_object(dirsync_obj, res_controls):
2083 assert len(res_controls) > 0
2084 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2086 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2087 sid = ndr_unpack(security.dom_sid, binary_sid)
2089 lastCookie = str(res_controls[0])
2091 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2092 expression="(lastCookie=%s)" % (
2093 ldb.binary_encode(lastCookie)),
2099 def update_object(dirsync_obj, res_controls):
2100 assert len(res_controls) > 0
2101 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2103 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2104 sid = ndr_unpack(security.dom_sid, binary_sid)
2106 lastCookie = str(res_controls[0])
2108 self.cache.transaction_start()
2110 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2111 expression="(objectClass=*)",
2112 attrs=["lastCookie"])
2114 add_ldif = "dn: %s\n" % (dn)
2115 add_ldif += "objectClass: userCookie\n"
2116 add_ldif += "lastCookie: %s\n" % (lastCookie)
2117 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2118 self.cache.add_ldif(add_ldif)
2120 modify_ldif = "dn: %s\n" % (dn)
2121 modify_ldif += "changetype: modify\n"
2122 modify_ldif += "replace: lastCookie\n"
2123 modify_ldif += "lastCookie: %s\n" % (lastCookie)
2124 modify_ldif += "replace: currentTime\n"
2125 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2126 self.cache.modify_ldif(modify_ldif)
2127 self.cache.transaction_commit()
2128 except Exception as e:
2129 self.cache.transaction_cancel()
2135 res = self.samdb.search(expression=self.dirsync_filter,
2136 scope=ldb.SCOPE_SUBTREE,
2137 attrs=self.dirsync_attrs,
2138 controls=self.dirsync_controls)
2139 log_msg("dirsync_loop(): results %d\n" % len(res))
2142 done = check_object(r, res.controls)
2144 handle_object(ri, r)
2145 update_object(r, res.controls)
2147 update_cache(res.controls)
2151 def sync_loop(wait):
2152 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2153 notify_controls = ["notification:1", "show_recycled:1"]
2154 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2155 scope=ldb.SCOPE_SUBTREE,
2157 controls=notify_controls,
2161 log_msg("Resuming monitoring\n")
2163 log_msg("Getting changes\n")
2164 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2165 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2166 self.outf.write("syncCommand: %s\n" % self.sync_command)
2169 if wait is not True:
2172 for msg in notify_handle:
2173 if not isinstance(msg, ldb.Message):
2174 self.outf.write("referal: %s\n" % msg)
2176 created = msg.get("uSNCreated")[0]
2177 changed = msg.get("uSNChanged")[0]
2178 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2179 (msg.dn, created, changed))
2183 res = notify_handle.result()
2188 orig_pid = os.getpid()
2193 if pid == 0: # Actual daemon
2195 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2200 if cache_ldb_initialize:
2202 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2207 if logfile is not None:
2208 import resource # Resource usage information.
2209 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2210 if maxfd == resource.RLIM_INFINITY:
2211 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2212 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
2213 self.outf.write("Using logfile[%s]\n" % logfile)
2214 for fd in range(0, maxfd):
2225 log_msg("Attached to logfile[%s]\n" % (logfile))
2226 self.logfile = logfile
2229 conflict = check_current_pid_conflict(terminate)
2231 if self.current_pid is None:
2232 log_msg("No process running.\n")
2235 log_msg("Proccess %d is not running anymore.\n" % (
2239 log_msg("Sending SIGTERM to proccess %d.\n" % (
2241 os.kill(self.current_pid, signal.SIGTERM)
2244 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2245 os.getpid(), self.current_pid))
2249 update_pid(os.getpid())
2254 retry_sleep_max = 600
2259 retry_sleep = retry_sleep_min
2261 while self.samdb is None:
2262 if retry_sleep != 0:
2263 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2264 time.sleep(retry_sleep)
2265 retry_sleep = retry_sleep * 2
2266 if retry_sleep >= retry_sleep_max:
2267 retry_sleep = retry_sleep_max
2268 log_msg("Connecting to '%s'\n" % self.samdb_url)
2270 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2271 except Exception as msg:
2273 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2274 if wait is not True:
2279 except ldb.LdbError as (enum, estr):
2281 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2286 class cmd_user(SuperCommand):
2287 """User management."""
2290 subcommands["add"] = cmd_user_add()
2291 subcommands["create"] = cmd_user_create()
2292 subcommands["delete"] = cmd_user_delete()
2293 subcommands["disable"] = cmd_user_disable()
2294 subcommands["enable"] = cmd_user_enable()
2295 subcommands["list"] = cmd_user_list()
2296 subcommands["setexpiry"] = cmd_user_setexpiry()
2297 subcommands["password"] = cmd_user_password()
2298 subcommands["setpassword"] = cmd_user_setpassword()
2299 subcommands["getpassword"] = cmd_user_getpassword()
2300 subcommands["syncpasswords"] = cmd_user_syncpasswords()