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
34 from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError
35 from getpass import getpass
36 from samba.auth import system_session
37 from samba.samdb import SamDB
38 from samba.dcerpc import misc
39 from samba.dcerpc import security
40 from samba.dcerpc import drsblobs
41 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
46 generate_random_password,
49 from samba.net import Net
51 from samba.netcmd import (
57 from samba.compat import text_type
63 decrypt_samba_gpg_help = "Decrypt the SambaGPG password as cleartext source"
64 except ImportError as e:
66 decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
67 "python-gpgme required"
69 disabled_virtual_attributes = {
72 virtual_attributes = {
73 "virtualClearTextUTF8": {
74 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
76 "virtualClearTextUTF16": {
77 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
80 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
84 get_random_bytes_fn = None
85 if get_random_bytes_fn is None:
88 get_random_bytes_fn = Crypto.Random.get_random_bytes
89 except ImportError as e:
91 if get_random_bytes_fn is None:
94 get_random_bytes_fn = M2Crypto.Rand.rand_bytes
95 except ImportError as e:
100 if get_random_bytes_fn is not None:
102 return "Crypto.Random or M2Crypto.Rand required"
105 def get_random_bytes(num):
106 random_reason = check_random()
107 if random_reason is not None:
108 raise ImportError(random_reason)
109 return get_random_bytes_fn(num)
112 def get_crypt_value(alg, utf8pw, rounds=0):
118 salt = get_random_bytes(16)
119 # The salt needs to be in [A-Za-z0-9./]
120 # base64 is close enough and as we had 16
121 # random bytes but only need 16 characters
122 # we can ignore the possible == at the end
123 # of the base64 string
124 # we just need to replace '+' by '.'
125 b64salt = base64.b64encode(salt)[0:16].replace(b'+', b'.').decode('utf8')
128 crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
130 crypt_salt = "$%s$%s$" % (alg, b64salt)
132 crypt_value = crypt.crypt(utf8pw, crypt_salt)
133 if crypt_value is None:
134 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
135 expected_len = len(crypt_salt) + algs[alg]["length"]
136 if len(crypt_value) != expected_len:
137 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
138 crypt_salt, len(crypt_value), expected_len))
141 # Extract the rounds value from the options of a virtualCrypt attribute
142 # i.e. options = "rounds=20;other=ignored;" will return 20
143 # if the rounds option is not found or the value is not a number, 0 is returned
144 # which indicates that the default number of rounds should be used.
147 def get_rounds(options):
151 opts = options.split(';')
153 if o.lower().startswith("rounds="):
154 (key, _, val) = o.partition('=')
163 random_reason = check_random()
164 if random_reason is not None:
165 raise ImportError(random_reason)
169 virtual_attributes["virtualSSHA"] = {
171 except ImportError as e:
172 reason = "hashlib.sha1()"
174 reason += " and " + random_reason
175 reason += " required"
176 disabled_virtual_attributes["virtualSSHA"] = {
180 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
182 random_reason = check_random()
183 if random_reason is not None:
184 raise ImportError(random_reason)
186 v = get_crypt_value(alg, "")
188 virtual_attributes[attr] = {
190 except ImportError as e:
193 reason += " and " + random_reason
194 reason += " required"
195 disabled_virtual_attributes[attr] = {
198 except NotImplementedError as e:
199 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
200 disabled_virtual_attributes[attr] = {
204 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
205 for x in range(1, 30):
206 virtual_attributes["virtualWDigest%02d" % x] = {}
208 virtual_attributes_help = "The attributes to display (comma separated). "
209 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
210 if len(disabled_virtual_attributes) != 0:
211 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
214 class cmd_user_create(Command):
215 """Create a new user.
217 This command creates a new user account in the Active Directory domain. The username specified on the command is the sAMaccountName.
219 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).
221 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.
223 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.
225 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server.
228 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
230 Example1 shows how to create a new user in the domain against a remote LDAP server. The -H parameter is used to specify the remote target server. The -U option is used to pass the userid and password authorized to issue the command remotely.
233 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
235 Example2 shows how to create a new user in the domain against the local server. sudo is used so a user may run the command as root. In this example, after User2 is created, he/she will be forced to change their password when they logon.
238 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
240 Example3 shows how to create a new user in the OrgUnit organizational unit.
243 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
245 Example4 shows how to create a new user with Unix UID, GID and login-shell set from the local NSS and GECOS set to 'some text'.
248 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
249 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
251 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
252 --nis-domain is set, then the other four parameters are mandatory.
255 synopsis = "%prog <username> [<password>] [options]"
258 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
259 metavar="URL", dest="H"),
260 Option("--must-change-at-next-login",
261 help="Force password to be changed on next login",
262 action="store_true"),
263 Option("--random-password",
264 help="Generate random password",
265 action="store_true"),
266 Option("--smartcard-required",
267 help="Require a smartcard for interactive logons",
268 action="store_true"),
269 Option("--use-username-as-cn",
270 help="Force use of username as user's CN",
271 action="store_true"),
273 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>'",
275 Option("--surname", help="User's surname", type=str),
276 Option("--given-name", help="User's given name", type=str),
277 Option("--initials", help="User's initials", type=str),
278 Option("--profile-path", help="User's profile path", type=str),
279 Option("--script-path", help="User's logon script path", type=str),
280 Option("--home-drive", help="User's home drive letter", type=str),
281 Option("--home-directory", help="User's home directory path", type=str),
282 Option("--job-title", help="User's job title", type=str),
283 Option("--department", help="User's department", type=str),
284 Option("--company", help="User's company", type=str),
285 Option("--description", help="User's description", type=str),
286 Option("--mail-address", help="User's email address", type=str),
287 Option("--internet-address", help="User's home page", type=str),
288 Option("--telephone-number", help="User's phone number", type=str),
289 Option("--physical-delivery-office", help="User's office location", type=str),
290 Option("--rfc2307-from-nss",
291 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
292 action="store_true"),
293 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
294 Option("--unix-home", help="User's Unix/RFC2307 home directory",
296 Option("--uid", help="User's Unix/RFC2307 username", type=str),
297 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
298 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
299 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
300 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
303 takes_args = ["username", "password?"]
305 takes_optiongroups = {
306 "sambaopts": options.SambaOptions,
307 "credopts": options.CredentialsOptions,
308 "versionopts": options.VersionOptions,
311 def run(self, username, password=None, credopts=None, sambaopts=None,
312 versionopts=None, H=None, must_change_at_next_login=False,
313 random_password=False, use_username_as_cn=False, userou=None,
314 surname=None, given_name=None, initials=None, profile_path=None,
315 script_path=None, home_drive=None, home_directory=None,
316 job_title=None, department=None, company=None, description=None,
317 mail_address=None, internet_address=None, telephone_number=None,
318 physical_delivery_office=None, rfc2307_from_nss=False,
319 nis_domain=None, unix_home=None, uid=None, uid_number=None,
320 gid_number=None, gecos=None, login_shell=None,
321 smartcard_required=False):
323 if smartcard_required:
324 if password is not None and password is not '':
325 raise CommandError('It is not allowed to specify '
327 'together with --smartcard-required.')
328 if must_change_at_next_login:
329 raise CommandError('It is not allowed to specify '
330 '--must-change-at-next-login '
331 'together with --smartcard-required.')
333 if random_password and not smartcard_required:
334 password = generate_random_password(128, 255)
337 if smartcard_required:
339 if password is not None and password is not '':
341 password = getpass("New Password: ")
342 passwordverify = getpass("Retype Password: ")
343 if not password == passwordverify:
345 self.outf.write("Sorry, passwords do not match.\n")
348 pwent = pwd.getpwnam(username)
351 if uid_number is None:
352 uid_number = pwent[2]
353 if gid_number is None:
354 gid_number = pwent[3]
357 if login_shell is None:
358 login_shell = pwent[6]
360 lp = sambaopts.get_loadparm()
361 creds = credopts.get_credentials(lp)
363 if uid_number or gid_number:
364 if not lp.get("idmap_ldb:use rfc2307"):
365 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")
367 if nis_domain is not None:
368 if None in (uid_number, login_shell, unix_home, gid_number):
369 raise CommandError('Missing parameters. To enable NIS features, '
370 'the following options have to be given: '
371 '--nis-domain=, --uidNumber=, --login-shell='
372 ', --unix-home=, --gid-number= Operation '
376 samdb = SamDB(url=H, session_info=system_session(),
377 credentials=creds, lp=lp)
378 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
379 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
380 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
381 jobtitle=job_title, department=department, company=company, description=description,
382 mailaddress=mail_address, internetaddress=internet_address,
383 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
384 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
385 uidnumber=uid_number, gidnumber=gid_number,
386 gecos=gecos, loginshell=login_shell,
387 smartcard_required=smartcard_required)
388 except Exception as e:
389 raise CommandError("Failed to add user '%s': " % username, e)
391 self.outf.write("User '%s' created successfully\n" % username)
394 class cmd_user_add(cmd_user_create):
395 __doc__ = cmd_user_create.__doc__
396 # take this print out after the add subcommand is removed.
397 # the add subcommand is deprecated but left in for now to allow people to
400 def run(self, *args, **kwargs):
402 "Note: samba-tool user add is deprecated. "
403 "Please use samba-tool user create for the same function.\n")
404 return super(cmd_user_add, self).run(*args, **kwargs)
407 class cmd_user_delete(Command):
410 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
412 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.
414 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server.
417 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
419 Example1 shows how to delete a user in the domain against a remote LDAP server. The -H parameter is used to specify the remote target server. The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to issue the command on that server.
422 sudo samba-tool user delete User2
424 Example2 shows how to delete a user in the domain against the local server. sudo is used so a user may run the command as root.
427 synopsis = "%prog <username> [options]"
430 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
431 metavar="URL", dest="H"),
434 takes_args = ["username"]
435 takes_optiongroups = {
436 "sambaopts": options.SambaOptions,
437 "credopts": options.CredentialsOptions,
438 "versionopts": options.VersionOptions,
441 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
443 lp = sambaopts.get_loadparm()
444 creds = credopts.get_credentials(lp, fallback_machine=True)
446 samdb = SamDB(url=H, session_info=system_session(),
447 credentials=creds, lp=lp)
449 filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
450 ldb.binary_encode(username))
453 res = samdb.search(base=samdb.domain_dn(),
454 scope=ldb.SCOPE_SUBTREE,
459 raise CommandError('Unable to find user "%s"' % (username))
462 samdb.delete(user_dn)
463 except Exception as e:
464 raise CommandError('Failed to remove user "%s"' % username, e)
465 self.outf.write("Deleted user %s\n" % username)
468 class cmd_user_list(Command):
469 """List all users."""
471 synopsis = "%prog [options]"
474 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
475 metavar="URL", dest="H"),
478 takes_optiongroups = {
479 "sambaopts": options.SambaOptions,
480 "credopts": options.CredentialsOptions,
481 "versionopts": options.VersionOptions,
484 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
485 lp = sambaopts.get_loadparm()
486 creds = credopts.get_credentials(lp, fallback_machine=True)
488 samdb = SamDB(url=H, session_info=system_session(),
489 credentials=creds, lp=lp)
491 domain_dn = samdb.domain_dn()
492 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
493 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
494 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
495 attrs=["samaccountname"])
500 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
503 class cmd_user_enable(Command):
506 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.
508 There are many reasons why an account may become disabled. These include:
509 - If a user exceeds the account policy for logon attempts
510 - If an administrator disables the account
511 - If the account expires
513 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
515 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.
517 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server.
520 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
522 Example1 shows how to enable a user in the domain against a remote LDAP server. The --URL parameter is used to specify the remote target server. The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server.
525 su samba-tool user enable Testuser2
527 Example2 shows how to enable user Testuser2 for use in the domain on the local server. sudo is used so a user may run the command as root.
530 samba-tool user enable --filter=samaccountname=Testuser3
532 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
535 synopsis = "%prog (<username>|--filter <filter>) [options]"
537 takes_optiongroups = {
538 "sambaopts": options.SambaOptions,
539 "versionopts": options.VersionOptions,
540 "credopts": options.CredentialsOptions,
544 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
545 metavar="URL", dest="H"),
546 Option("--filter", help="LDAP Filter to set password on", type=str),
549 takes_args = ["username?"]
551 def run(self, username=None, sambaopts=None, credopts=None,
552 versionopts=None, filter=None, H=None):
553 if username is None and filter is None:
554 raise CommandError("Either the username or '--filter' must be specified!")
557 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
559 lp = sambaopts.get_loadparm()
560 creds = credopts.get_credentials(lp, fallback_machine=True)
562 samdb = SamDB(url=H, session_info=system_session(),
563 credentials=creds, lp=lp)
565 samdb.enable_account(filter)
566 except Exception as msg:
567 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
568 self.outf.write("Enabled user '%s'\n" % (username or filter))
571 class cmd_user_disable(Command):
572 """Disable a user."""
574 synopsis = "%prog (<username>|--filter <filter>) [options]"
577 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
578 metavar="URL", dest="H"),
579 Option("--filter", help="LDAP Filter to set password on", type=str),
582 takes_args = ["username?"]
584 takes_optiongroups = {
585 "sambaopts": options.SambaOptions,
586 "credopts": options.CredentialsOptions,
587 "versionopts": options.VersionOptions,
590 def run(self, username=None, sambaopts=None, credopts=None,
591 versionopts=None, filter=None, H=None):
592 if username is None and filter is None:
593 raise CommandError("Either the username or '--filter' must be specified!")
596 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
598 lp = sambaopts.get_loadparm()
599 creds = credopts.get_credentials(lp, fallback_machine=True)
601 samdb = SamDB(url=H, session_info=system_session(),
602 credentials=creds, lp=lp)
604 samdb.disable_account(filter)
605 except Exception as msg:
606 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
609 class cmd_user_setexpiry(Command):
610 """Set the expiration of a user account.
612 The user can either be specified by their sAMAccountName or using the --filter option.
614 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.
616 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command on a remote server.
619 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
621 Example1 shows how to set the expiration of an account in a remote LDAP server. The --URL parameter is used to specify the remote target server. The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server.
624 sudo samba-tool user setexpiry User2 --noexpiry
626 Example2 shows how to set the account expiration of user User2 so it will never expire. The user in this example resides on the local server. sudo is used so a user may run the command as root.
629 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
631 Example3 shows how to set the account expiration date to end of day 20 days from the current day. The username or sAMAccountName is specified using the --filter= parameter and the username in this example is User3.
634 samba-tool user setexpiry --noexpiry User4
635 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
638 synopsis = "%prog (<username>|--filter <filter>) [options]"
640 takes_optiongroups = {
641 "sambaopts": options.SambaOptions,
642 "versionopts": options.VersionOptions,
643 "credopts": options.CredentialsOptions,
647 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
648 metavar="URL", dest="H"),
649 Option("--filter", help="LDAP Filter to set password on", type=str),
650 Option("--days", help="Days to expiry", type=int, default=0),
651 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
654 takes_args = ["username?"]
656 def run(self, username=None, sambaopts=None, credopts=None,
657 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
658 if username is None and filter is None:
659 raise CommandError("Either the username or '--filter' must be specified!")
662 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
664 lp = sambaopts.get_loadparm()
665 creds = credopts.get_credentials(lp)
667 samdb = SamDB(url=H, session_info=system_session(),
668 credentials=creds, lp=lp)
671 samdb.setexpiry(filter, days * 24 * 3600, no_expiry_req=noexpiry)
672 except Exception as msg:
673 # FIXME: Catch more specific exception
674 raise CommandError("Failed to set expiry for user '%s': %s" % (
675 username or filter, msg))
677 self.outf.write("Expiry for user '%s' disabled.\n" % (
680 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
681 username or filter, days))
684 class cmd_user_password(Command):
685 """Change password for a user account (the one provided in authentication).
688 synopsis = "%prog [options]"
691 Option("--newpassword", help="New password", type=str),
694 takes_optiongroups = {
695 "sambaopts": options.SambaOptions,
696 "credopts": options.CredentialsOptions,
697 "versionopts": options.VersionOptions,
700 def run(self, credopts=None, sambaopts=None, versionopts=None,
703 lp = sambaopts.get_loadparm()
704 creds = credopts.get_credentials(lp)
706 # get old password now, to get the password prompts in the right order
707 old_password = creds.get_password()
709 net = Net(creds, lp, server=credopts.ipaddress)
711 password = newpassword
713 if password is not None and password is not '':
715 password = getpass("New Password: ")
716 passwordverify = getpass("Retype Password: ")
717 if not password == passwordverify:
719 self.outf.write("Sorry, passwords do not match.\n")
722 if not isinstance(password, text_type):
723 password = password.decode('utf8')
724 net.change_password(password)
725 except Exception as msg:
726 # FIXME: catch more specific exception
727 raise CommandError("Failed to change password : %s" % msg)
728 self.outf.write("Changed password OK\n")
731 class cmd_user_setpassword(Command):
732 """Set or reset the password of a user account.
734 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.
736 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.
738 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.
740 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server.
743 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
745 Example1 shows how to set the password of user TestUser1 on a remote LDAP server. The --URL parameter is used to specify the remote target server. The -U option is used to pass the username and password of a user that exists on the remote server and is authorized to update the server.
748 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
750 Example2 shows how an administrator would reset the TestUser2 user's password to passw0rd. The user is running under the root userid using the sudo command. In this example the user TestUser2 must change their password the next time they logon to the account.
753 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
755 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
758 synopsis = "%prog (<username>|--filter <filter>) [options]"
760 takes_optiongroups = {
761 "sambaopts": options.SambaOptions,
762 "versionopts": options.VersionOptions,
763 "credopts": options.CredentialsOptions,
767 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
768 metavar="URL", dest="H"),
769 Option("--filter", help="LDAP Filter to set password on", type=str),
770 Option("--newpassword", help="Set password", type=str),
771 Option("--must-change-at-next-login",
772 help="Force password to be changed on next login",
773 action="store_true"),
774 Option("--random-password",
775 help="Generate random password",
776 action="store_true"),
777 Option("--smartcard-required",
778 help="Require a smartcard for interactive logons",
779 action="store_true"),
780 Option("--clear-smartcard-required",
781 help="Don't require a smartcard for interactive logons",
782 action="store_true"),
785 takes_args = ["username?"]
787 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
788 versionopts=None, H=None, newpassword=None,
789 must_change_at_next_login=False, random_password=False,
790 smartcard_required=False, clear_smartcard_required=False):
791 if filter is None and username is None:
792 raise CommandError("Either the username or '--filter' must be specified!")
794 password = newpassword
796 if smartcard_required:
797 if password is not None and password is not '':
798 raise CommandError('It is not allowed to specify '
800 'together with --smartcard-required.')
801 if must_change_at_next_login:
802 raise CommandError('It is not allowed to specify '
803 '--must-change-at-next-login '
804 'together with --smartcard-required.')
805 if clear_smartcard_required:
806 raise CommandError('It is not allowed to specify '
807 '--clear-smartcard-required '
808 'together with --smartcard-required.')
810 if random_password and not smartcard_required:
811 password = generate_random_password(128, 255)
814 if smartcard_required:
816 if password is not None and password is not '':
818 password = getpass("New Password: ")
819 passwordverify = getpass("Retype Password: ")
820 if not password == passwordverify:
822 self.outf.write("Sorry, passwords do not match.\n")
825 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
827 lp = sambaopts.get_loadparm()
828 creds = credopts.get_credentials(lp)
830 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
832 samdb = SamDB(url=H, session_info=system_session(),
833 credentials=creds, lp=lp)
835 if smartcard_required:
838 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
839 flags = dsdb.UF_SMARTCARD_REQUIRED
840 samdb.toggle_userAccountFlags(filter, flags, on=True)
841 command = "Failed to enable account for user '%s'" % (username or filter)
842 samdb.enable_account(filter)
843 except Exception as msg:
844 # FIXME: catch more specific exception
845 raise CommandError("%s: %s" % (command, msg))
846 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
850 if clear_smartcard_required:
851 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
852 flags = dsdb.UF_SMARTCARD_REQUIRED
853 samdb.toggle_userAccountFlags(filter, flags, on=False)
854 command = "Failed to set password for user '%s'" % (username or filter)
855 samdb.setpassword(filter, password,
856 force_change_at_next_login=must_change_at_next_login,
858 except Exception as msg:
859 # FIXME: catch more specific exception
860 raise CommandError("%s: %s" % (command, msg))
861 self.outf.write("Changed password OK\n")
864 class GetPasswordCommand(Command):
867 super(GetPasswordCommand, self).__init__()
870 def connect_system_samdb(self, url, allow_local=False, verbose=False):
872 # using anonymous here, results in no authentication
873 # which means we can get system privileges via
874 # the privileged ldapi socket
875 creds = credentials.Credentials()
876 creds.set_anonymous()
878 if url is None and allow_local:
880 elif url.lower().startswith("ldapi://"):
882 elif url.lower().startswith("ldap://"):
883 raise CommandError("--url ldap:// is not supported for this command")
884 elif url.lower().startswith("ldaps://"):
885 raise CommandError("--url ldaps:// is not supported for this command")
886 elif not allow_local:
887 raise CommandError("--url requires an ldapi:// url for this command")
890 self.outf.write("Connecting to '%s'\n" % url)
892 samdb = SamDB(url=url, session_info=system_session(),
893 credentials=creds, lp=self.lp)
897 # Make sure we're connected as SYSTEM
899 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
901 sids = res[0].get("tokenGroups")
902 assert len(sids) == 1
903 sid = ndr_unpack(security.dom_sid, sids[0])
904 assert str(sid) == security.SID_NT_SYSTEM
905 except Exception as msg:
906 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
907 (security.SID_NT_SYSTEM))
909 # We use sort here in order to have a predictable processing order
910 # this might not be strictly needed, but also doesn't hurt here
911 for a in sorted(virtual_attributes.keys()):
912 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
913 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
917 def get_account_attributes(self, samdb, username, basedn, filter, scope,
924 (attr, _, opts) = a.partition(';')
926 attr_opts[attr] = opts
928 attr_opts[attr] = None
929 search_attrs.append(attr)
930 lower_attrs = [x.lower() for x in search_attrs]
932 require_supplementalCredentials = False
933 for a in virtual_attributes.keys():
934 if a.lower() in lower_attrs:
935 require_supplementalCredentials = True
936 add_supplementalCredentials = False
937 add_unicodePwd = False
938 if require_supplementalCredentials:
939 a = "supplementalCredentials"
940 if a.lower() not in lower_attrs:
942 add_supplementalCredentials = True
944 if a.lower() not in lower_attrs:
946 add_unicodePwd = True
947 add_sAMAcountName = False
949 if a.lower() not in lower_attrs:
951 add_sAMAcountName = True
953 add_userPrincipalName = False
954 upn = "usePrincipalName"
955 if upn.lower() not in lower_attrs:
956 search_attrs += [upn]
957 add_userPrincipalName = True
959 if scope == ldb.SCOPE_BASE:
960 search_controls = ["show_deleted:1", "show_recycled:1"]
964 res = samdb.search(base=basedn, expression=filter,
965 scope=scope, attrs=search_attrs,
966 controls=search_controls)
968 raise Exception('Unable to find user "%s"' % (username or filter))
970 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
971 except Exception as msg:
972 # FIXME: catch more specific exception
973 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
978 if "supplementalCredentials" in obj:
979 sc_blob = obj["supplementalCredentials"][0]
980 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
981 if add_supplementalCredentials:
982 del obj["supplementalCredentials"]
983 if "unicodePwd" in obj:
984 unicodePwd = obj["unicodePwd"][0]
986 del obj["unicodePwd"]
987 account_name = obj["sAMAccountName"][0]
988 if add_sAMAcountName:
989 del obj["sAMAccountName"]
990 if "userPrincipalName" in obj:
991 account_upn = obj["userPrincipalName"][0]
993 realm = self.lp.get("realm")
994 account_upn = "%s@%s" % (account_name, realm.lower())
995 if add_userPrincipalName:
996 del obj["userPrincipalName"]
999 def get_package(name, min_idx=0):
1000 if name in calculated:
1001 return calculated[name]
1005 min_idx = len(sc.sub.packages) + min_idx
1007 for p in sc.sub.packages:
1014 return binascii.a2b_hex(p.data)
1019 # Samba adds 'Primary:SambaGPG' at the end.
1020 # When Windows sets the password it keeps
1021 # 'Primary:SambaGPG' and rotates it to
1022 # the begining. So we can only use the value,
1023 # if it is the last one.
1025 # In order to get more protection we verify
1026 # the nthash of the decrypted utf16 password
1027 # against the stored nthash in unicodePwd.
1029 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1030 if sgv is not None and unicodePwd is not None:
1031 ctx = gpgme.Context()
1033 cipher_io = io.BytesIO(sgv)
1034 plain_io = io.BytesIO()
1036 ctx.decrypt(cipher_io, plain_io)
1037 cv = plain_io.getvalue()
1039 # We only use the password if it matches
1040 # the current nthash stored in the unicodePwd
1043 tmp = credentials.Credentials()
1045 tmp.set_utf16_password(cv)
1046 nthash = tmp.get_nt_hash()
1047 if nthash == unicodePwd:
1048 calculated["Primary:CLEARTEXT"] = cv
1049 except gpgme.GpgmeError as e1:
1050 (major, minor, msg) = e1.args
1051 if major == gpgme.ERR_BAD_SECKEY:
1052 msg = "ERR_BAD_SECKEY: " + msg
1054 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1055 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1056 username or account_name, msg))
1058 def get_utf8(a, b, username):
1060 u = unicode(b, 'utf-16-le')
1061 except UnicodeDecodeError as e:
1062 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1065 u8 = u.encode('utf-8')
1068 # Extract the WDigest hash for the value specified by i.
1069 # Builds an htdigest compatible value
1071 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1072 domain, dns_domain):
1077 user = account_name.lower()
1078 realm = domain.lower()
1080 user = account_name.upper()
1081 realm = domain.upper()
1084 realm = domain.upper()
1087 realm = domain.lower()
1089 user = account_name.upper()
1090 realm = domain.lower()
1092 user = account_name.lower()
1093 realm = domain.upper()
1096 realm = dns_domain.lower()
1098 user = account_name.lower()
1099 realm = dns_domain.lower()
1101 user = account_name.upper()
1102 realm = dns_domain.upper()
1105 realm = dns_domain.upper()
1108 realm = dns_domain.lower()
1110 user = account_name.upper()
1111 realm = dns_domain.lower()
1113 user = account_name.lower()
1114 realm = dns_domain.upper()
1119 user = account_upn.lower()
1122 user = account_upn.upper()
1125 user = "%s\\%s" % (domain, account_name)
1128 user = "%s\\%s" % (domain.lower(), account_name.lower())
1131 user = "%s\\%s" % (domain.upper(), account_name.upper())
1137 user = account_name.lower()
1140 user = account_name.upper()
1146 user = account_upn.lower()
1149 user = account_upn.upper()
1152 user = "%s\\%s" % (domain, account_name)
1155 # Differs from spec, see tests
1156 user = "%s\\%s" % (domain.lower(), account_name.lower())
1159 # Differs from spec, see tests
1160 user = "%s\\%s" % (domain.upper(), account_name.upper())
1165 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1168 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
1169 return "%s:%s:%s" % (user, realm, digest)
1173 # get the value for a virtualCrypt attribute.
1174 # look for an exact match on algorithm and rounds in supplemental creds
1175 # if not found calculate using Primary:CLEARTEXT
1176 # if no Primary:CLEARTEXT return the first supplementalCredential
1177 # that matches the algorithm.
1178 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1181 b = get_package("Primary:userPassword")
1183 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1185 # No exact match on algorithm and number of rounds
1186 # try and calculate one from the Primary:CLEARTEXT
1187 b = get_package("Primary:CLEARTEXT")
1189 u8 = get_utf8(a, b, username or account_name)
1191 sv = get_crypt_value(str(algorithm), u8, rounds)
1193 # Unable to calculate a hash with the specified
1194 # number of rounds, fall back to the first hash using
1195 # the specified algorithm
1199 return "{CRYPT}" + sv
1201 def get_userPassword_hash(blob, algorithm, rounds):
1202 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1205 # Check that the NT hash has not been changed without updating
1206 # the user password hashes. This indicates that password has been
1207 # changed without updating the supplemental credentials.
1208 if unicodePwd != bytearray(up.current_nt_hash.hash):
1211 scheme_prefix = "$%d$" % algorithm
1212 prefix = scheme_prefix
1214 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1218 if (scheme_match is None and
1219 h.scheme == SCHEME and
1220 h.value.startswith(scheme_prefix)):
1221 scheme_match = h.value
1222 if h.scheme == SCHEME and h.value.startswith(prefix):
1223 return (h.value, scheme_match)
1225 # No match on the number of rounds, return the value of the
1226 # first matching scheme
1227 return (None, scheme_match)
1229 # We use sort here in order to have a predictable processing order
1230 for a in sorted(virtual_attributes.keys()):
1231 if not a.lower() in lower_attrs:
1234 if a == "virtualClearTextUTF8":
1235 b = get_package("Primary:CLEARTEXT")
1238 u8 = get_utf8(a, b, username or account_name)
1242 elif a == "virtualClearTextUTF16":
1243 v = get_package("Primary:CLEARTEXT")
1246 elif a == "virtualSSHA":
1247 b = get_package("Primary:CLEARTEXT")
1250 u8 = get_utf8(a, b, username or account_name)
1253 salt = get_random_bytes(4)
1257 bv = h.digest() + salt
1258 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
1259 elif a == "virtualCryptSHA256":
1260 rounds = get_rounds(attr_opts[a])
1261 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1265 elif a == "virtualCryptSHA512":
1266 rounds = get_rounds(attr_opts[a])
1267 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1271 elif a == "virtualSambaGPG":
1272 # Samba adds 'Primary:SambaGPG' at the end.
1273 # When Windows sets the password it keeps
1274 # 'Primary:SambaGPG' and rotates it to
1275 # the begining. So we can only use the value,
1276 # if it is the last one.
1277 v = get_package("Primary:SambaGPG", min_idx=-1)
1280 elif a.startswith("virtualWDigest"):
1281 primary_wdigest = get_package("Primary:WDigest")
1282 if primary_wdigest is None:
1284 x = a[len("virtualWDigest"):]
1289 domain = self.lp.get("workgroup")
1290 dns_domain = samdb.domain_dns_name()
1291 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1296 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1299 def parse_attributes(self, attributes):
1301 if attributes is None:
1302 raise CommandError("Please specify --attributes")
1303 attrs = attributes.split(',')
1306 pa = pa.lstrip().rstrip()
1307 for da in disabled_virtual_attributes.keys():
1308 if pa.lower() == da.lower():
1309 r = disabled_virtual_attributes[da]["reason"]
1310 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1312 for va in virtual_attributes.keys():
1313 if pa.lower() == va.lower():
1314 # Take the real name
1317 password_attrs += [pa]
1319 return password_attrs
1322 class cmd_user_getpassword(GetPasswordCommand):
1323 """Get the password fields of a user/computer account.
1325 This command gets the logon password for a user/computer account.
1327 The username specified on the command is the sAMAccountName.
1328 The username may also be specified using the --filter option.
1330 The command must be run from the root user id or another authorized user id.
1331 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1332 used to adjust the local path. By default tdb:// is used by default.
1334 The '--attributes' parameter takes a comma separated list of attributes,
1335 which will be printed or given to the script specified by '--script'. If a
1336 specified attribute is not available on an object it's silently omitted.
1337 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1338 the NTHASH) and the following virtual attributes are possible (see --help
1339 for which virtual attributes are supported in your environment):
1341 virtualClearTextUTF16: The raw cleartext as stored in the
1342 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1343 with '--decrypt-samba-gpg') buffer inside of the
1344 supplementalCredentials attribute. This typically
1345 contains valid UTF-16-LE, but may contain random
1346 bytes, e.g. for computer accounts.
1348 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1349 (only from valid UTF-16-LE)
1351 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1352 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1354 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1355 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1356 with a $5$... salt, see crypt(3) on modern systems.
1357 The number of rounds used to calculate the hash can
1358 also be specified. By appending ";rounds=x" to the
1359 attribute name i.e. virtualCryptSHA256;rounds=10000
1360 will calculate a SHA256 hash with 10,000 rounds.
1361 non numeric values for rounds are silently ignored
1362 The value is calculated as follows:
1363 1) If a value exists in 'Primary:userPassword' with
1364 the specified number of rounds it is returned.
1365 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1366 '--decrypt-samba-gpg'. Calculate a hash with
1367 the specified number of rounds
1368 3) Return the first CryptSHA256 value in
1369 'Primary:userPassword'
1372 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1373 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1374 with a $6$... salt, see crypt(3) on modern systems.
1375 The number of rounds used to calculate the hash can
1376 also be specified. By appending ";rounds=x" to the
1377 attribute name i.e. virtualCryptSHA512;rounds=10000
1378 will calculate a SHA512 hash with 10,000 rounds.
1379 non numeric values for rounds are silently ignored
1380 The value is calculated as follows:
1381 1) If a value exists in 'Primary:userPassword' with
1382 the specified number of rounds it is returned.
1383 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1384 '--decrypt-samba-gpg'. Calculate a hash with
1385 the specified number of rounds
1386 3) Return the first CryptSHA512 value in
1387 'Primary:userPassword'
1389 virtualWDigestNN: The individual hash values stored in
1390 'Primary:WDigest' where NN is the hash number in
1392 NOTE: As at 22-05-2017 the documentation:
1393 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1394 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1397 virtualSambaGPG: The raw cleartext as stored in the
1398 'Primary:SambaGPG' buffer inside of the
1399 supplementalCredentials attribute.
1400 See the 'password hash gpg key ids' option in
1403 The '--decrypt-samba-gpg' option triggers decryption of the
1404 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1405 in your environment or not (the python-gpgme package is required). Please
1406 note that you might need to set the GNUPGHOME environment variable. If the
1407 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1408 environment variable has been set correctly and the passphrase is already
1409 known by the gpg-agent.
1412 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1415 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1419 super(cmd_user_getpassword, self).__init__()
1421 synopsis = "%prog (<username>|--filter <filter>) [options]"
1423 takes_optiongroups = {
1424 "sambaopts": options.SambaOptions,
1425 "versionopts": options.VersionOptions,
1429 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1430 metavar="URL", dest="H"),
1431 Option("--filter", help="LDAP Filter to set password on", type=str),
1432 Option("--attributes", type=str,
1433 help=virtual_attributes_help,
1434 metavar="ATTRIBUTELIST", dest="attributes"),
1435 Option("--decrypt-samba-gpg",
1436 help=decrypt_samba_gpg_help,
1437 action="store_true", default=False, dest="decrypt_samba_gpg"),
1440 takes_args = ["username?"]
1442 def run(self, username=None, H=None, filter=None,
1443 attributes=None, decrypt_samba_gpg=None,
1444 sambaopts=None, versionopts=None):
1445 self.lp = sambaopts.get_loadparm()
1447 if decrypt_samba_gpg and not gpgme_support:
1448 raise CommandError(decrypt_samba_gpg_help)
1450 if filter is None and username is None:
1451 raise CommandError("Either the username or '--filter' must be specified!")
1454 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1456 if attributes is None:
1457 raise CommandError("Please specify --attributes")
1459 password_attrs = self.parse_attributes(attributes)
1461 samdb = self.connect_system_samdb(url=H, allow_local=True)
1463 obj = self.get_account_attributes(samdb, username,
1466 scope=ldb.SCOPE_SUBTREE,
1467 attrs=password_attrs,
1468 decrypt=decrypt_samba_gpg)
1470 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1471 self.outf.write("%s" % ldif)
1472 self.outf.write("Got password OK\n")
1475 class cmd_user_syncpasswords(GetPasswordCommand):
1476 """Sync the password of user accounts.
1478 This syncs logon passwords for user accounts.
1480 Note that this command should run on a single domain controller only
1481 (typically the PDC-emulator). However the "password hash gpg key ids"
1482 option should to be configured on all domain controllers.
1484 The command must be run from the root user id or another authorized user id.
1485 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1486 local path. By default, ldapi:// is used with the default path to the
1487 privileged ldapi socket.
1489 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1490 "Sync Loop Terminate".
1493 Cache Initialization
1494 ====================
1496 The first time, this command needs to be called with
1497 '--cache-ldb-initialize' in order to initialize its cache.
1499 The cache initialization requires '--attributes' and allows the following
1500 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1503 The '--attributes' parameter takes a comma separated list of attributes,
1504 which will be printed or given to the script specified by '--script'. If a
1505 specified attribute is not available on an object it will be silently omitted.
1506 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1507 the NTHASH) and the following virtual attributes are possible (see '--help'
1508 for supported virtual attributes in your environment):
1510 virtualClearTextUTF16: The raw cleartext as stored in the
1511 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1512 with '--decrypt-samba-gpg') buffer inside of the
1513 supplementalCredentials attribute. This typically
1514 contains valid UTF-16-LE, but may contain random
1515 bytes, e.g. for computer accounts.
1517 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1518 (only from valid UTF-16-LE)
1520 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1521 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1523 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1524 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1525 with a $5$... salt, see crypt(3) on modern systems.
1526 The number of rounds used to calculate the hash can
1527 also be specified. By appending ";rounds=x" to the
1528 attribute name i.e. virtualCryptSHA256;rounds=10000
1529 will calculate a SHA256 hash with 10,000 rounds.
1530 non numeric values for rounds are silently ignored
1531 The value is calculated as follows:
1532 1) If a value exists in 'Primary:userPassword' with
1533 the specified number of rounds it is returned.
1534 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1535 '--decrypt-samba-gpg'. Calculate a hash with
1536 the specified number of rounds
1537 3) Return the first CryptSHA256 value in
1538 'Primary:userPassword'
1540 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1541 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1542 with a $6$... salt, see crypt(3) on modern systems.
1543 The number of rounds used to calculate the hash can
1544 also be specified. By appending ";rounds=x" to the
1545 attribute name i.e. virtualCryptSHA512;rounds=10000
1546 will calculate a SHA512 hash with 10,000 rounds.
1547 non numeric values for rounds are silently ignored
1548 The value is calculated as follows:
1549 1) If a value exists in 'Primary:userPassword' with
1550 the specified number of rounds it is returned.
1551 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1552 '--decrypt-samba-gpg'. Calculate a hash with
1553 the specified number of rounds
1554 3) Return the first CryptSHA512 value in
1555 'Primary:userPassword'
1557 virtualWDigestNN: The individual hash values stored in
1558 'Primary:WDigest' where NN is the hash number in
1560 NOTE: As at 22-05-2017 the documentation:
1561 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1562 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1565 virtualSambaGPG: The raw cleartext as stored in the
1566 'Primary:SambaGPG' buffer inside of the
1567 supplementalCredentials attribute.
1568 See the 'password hash gpg key ids' option in
1571 The '--decrypt-samba-gpg' option triggers decryption of the
1572 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1573 in your environment or not (the python-gpgme package is required). Please
1574 note that you might need to set the GNUPGHOME environment variable. If the
1575 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1576 environment variable has been set correctly and the passphrase is already
1577 known by the gpg-agent.
1579 The '--script' option specifies a custom script that is called whenever any
1580 of the dirsyncAttributes (see below) was changed. The script is called
1581 without any arguments. It gets the LDIF for exactly one object on STDIN.
1582 If the script processed the object successfully it has to respond with a
1583 single line starting with 'DONE-EXIT: ' followed by an optional message.
1585 Note that the script might be called without any password change, e.g. if
1586 the account was disabled (a userAccountControl change) or the
1587 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1588 are always returned as unique identifier of the account. It might be useful
1589 to also ask for non-password attributes like: objectSid, sAMAccountName,
1590 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1591 Depending on the object, some attributes may not be present/available,
1592 but you always get the current state (and not a diff).
1594 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1597 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1598 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1599 (!(sAMAccountName=krbtgt*)))
1600 This means only normal (non-krbtgt) user
1601 accounts are monitored. The '--filter' can modify that, e.g. if it's
1602 required to also sync computer accounts.
1608 This (default) mode runs in an endless loop waiting for password related
1609 changes in the active directory database. It makes use of the
1610 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1611 get changes in a reliable fashion. Objects are monitored for changes of the
1612 following dirsyncAttributes:
1614 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1615 userPrincipalName and userAccountControl.
1617 It recovers from LDAP disconnects and updates the cache in conservative way
1618 (in single steps after each successfully processed change). An error from
1619 the script (specified by '--script') will result in fatal error and this
1620 command will exit. But the cache state should be still valid and can be
1621 resumed in the next "Sync Loop Run".
1623 The '--logfile' option specifies an optional (required if '--daemon' is
1624 specified) logfile that takes all output of the command. The logfile is
1625 automatically reopened if fstat returns st_nlink == 0.
1627 The optional '--daemon' option will put the command into the background.
1629 You can stop the command without the '--daemon' option, also by hitting
1632 If you specify the '--no-wait' option the command skips the
1633 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1634 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1639 In order to terminate an already running command (likely as daemon) the
1640 '--terminate' option can be used. This also requires the '--logfile' option
1645 samba-tool user syncpasswords --cache-ldb-initialize \\
1646 --attributes=virtualClearTextUTF8
1647 samba-tool user syncpasswords
1650 samba-tool user syncpasswords --cache-ldb-initialize \\
1651 --attributes=objectGUID,objectSID,sAMAccountName,\\
1652 userPrincipalName,userAccountControl,pwdLastSet,\\
1653 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1654 --script=/path/to/my-custom-syncpasswords-script.py
1655 samba-tool user syncpasswords --daemon \\
1656 --logfile=/var/log/samba/user-syncpasswords.log
1657 samba-tool user syncpasswords --terminate \\
1658 --logfile=/var/log/samba/user-syncpasswords.log
1662 super(cmd_user_syncpasswords, self).__init__()
1664 synopsis = "%prog [--cache-ldb-initialize] [options]"
1666 takes_optiongroups = {
1667 "sambaopts": options.SambaOptions,
1668 "versionopts": options.VersionOptions,
1672 Option("--cache-ldb-initialize",
1673 help="Initialize the cache for the first time",
1674 dest="cache_ldb_initialize", action="store_true"),
1675 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1676 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1677 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1678 metavar="URL", dest="H"),
1679 Option("--filter", help="optional LDAP filter to set password on", type=str,
1680 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1681 Option("--attributes", type=str,
1682 help=virtual_attributes_help,
1683 metavar="ATTRIBUTELIST", dest="attributes"),
1684 Option("--decrypt-samba-gpg",
1685 help=decrypt_samba_gpg_help,
1686 action="store_true", default=False, dest="decrypt_samba_gpg"),
1687 Option("--script", help="Script that is called for each password change", type=str,
1688 metavar="/path/to/syncpasswords.script", dest="script"),
1689 Option("--no-wait", help="Don't block waiting for changes",
1690 action="store_true", default=False, dest="nowait"),
1691 Option("--logfile", type=str,
1692 help="The logfile to use (required in --daemon mode).",
1693 metavar="/path/to/syncpasswords.log", dest="logfile"),
1694 Option("--daemon", help="daemonize after initial setup",
1695 action="store_true", default=False, dest="daemon"),
1696 Option("--terminate",
1697 help="Send a SIGTERM to an already running (daemon) process",
1698 action="store_true", default=False, dest="terminate"),
1701 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1702 H=None, filter=None,
1703 attributes=None, decrypt_samba_gpg=None,
1704 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1705 sambaopts=None, versionopts=None):
1707 self.lp = sambaopts.get_loadparm()
1709 self.samdb_url = None
1713 if not cache_ldb_initialize:
1714 if attributes is not None:
1715 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1716 if decrypt_samba_gpg:
1717 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1718 if script is not None:
1719 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1720 if filter is not None:
1721 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1723 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1725 if nowait is not False:
1726 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1727 if logfile is not None:
1728 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1729 if daemon is not False:
1730 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1731 if terminate is not False:
1732 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1736 raise CommandError("--daemon is not allowed together with --no-wait")
1737 if terminate is not False:
1738 raise CommandError("--terminate is not allowed together with --no-wait")
1740 if terminate is True and daemon is True:
1741 raise CommandError("--terminate is not allowed together with --daemon")
1743 if daemon is True and logfile is None:
1744 raise CommandError("--daemon is only allowed together with --logfile")
1746 if terminate is True and logfile is None:
1747 raise CommandError("--terminate is only allowed together with --logfile")
1749 if script is not None:
1750 if not os.path.exists(script):
1751 raise CommandError("script[%s] does not exist!" % script)
1753 sync_command = "%s" % os.path.abspath(script)
1757 dirsync_filter = filter
1758 if dirsync_filter is None:
1759 dirsync_filter = "(&" + \
1760 "(objectClass=user)" + \
1761 "(userAccountControl:%s:=%u)" % (
1762 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1763 "(!(sAMAccountName=krbtgt*))" + \
1766 dirsync_secret_attrs = [
1769 "supplementalCredentials",
1772 dirsync_attrs = dirsync_secret_attrs + [
1775 "userPrincipalName",
1776 "userAccountControl",
1781 password_attrs = None
1783 if cache_ldb_initialize:
1785 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1787 if decrypt_samba_gpg and not gpgme_support:
1788 raise CommandError(decrypt_samba_gpg_help)
1790 password_attrs = self.parse_attributes(attributes)
1791 lower_attrs = [x.lower() for x in password_attrs]
1792 # We always return these in order to track deletions
1793 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1794 if a.lower() not in lower_attrs:
1795 password_attrs += [a]
1797 if cache_ldb is not None:
1798 if cache_ldb.lower().startswith("ldapi://"):
1799 raise CommandError("--cache_ldb ldapi:// is not supported")
1800 elif cache_ldb.lower().startswith("ldap://"):
1801 raise CommandError("--cache_ldb ldap:// is not supported")
1802 elif cache_ldb.lower().startswith("ldaps://"):
1803 raise CommandError("--cache_ldb ldaps:// is not supported")
1804 elif cache_ldb.lower().startswith("tdb://"):
1807 if not os.path.exists(cache_ldb):
1808 cache_ldb = self.lp.private_path(cache_ldb)
1810 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1812 self.lockfile = "%s.pid" % cache_ldb
1815 if self.logfile is not None:
1817 if info.st_nlink == 0:
1818 logfile = self.logfile
1820 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1821 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
1826 log_msg("Reopened logfile[%s]\n" % (logfile))
1827 self.logfile = logfile
1828 msg = "%s: pid[%d]: %s" % (
1832 self.outf.write(msg)
1841 "passwordAttribute",
1847 self.cache = Ldb(cache_ldb)
1848 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1849 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1853 self.samdb_url = res[0]["samdbUrl"][0]
1854 except KeyError as e:
1855 self.samdb_url = None
1857 self.samdb_url = None
1858 if self.samdb_url is None and not cache_ldb_initialize:
1859 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1861 if self.samdb_url is not None and cache_ldb_initialize:
1862 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1864 if self.samdb_url is None:
1866 self.dirsync_filter = dirsync_filter
1867 self.dirsync_attrs = dirsync_attrs
1868 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"];
1869 self.password_attrs = password_attrs
1870 self.decrypt_samba_gpg = decrypt_samba_gpg
1871 self.sync_command = sync_command
1872 add_ldif = "dn: %s\n" % self.cache_dn
1873 add_ldif += "objectClass: userSyncPasswords\n"
1874 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url).decode('utf8')
1875 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter).decode('utf8')
1876 for a in self.dirsync_attrs:
1877 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a).decode('utf8')
1878 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1879 for a in self.password_attrs:
1880 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a).decode('utf8')
1881 if self.decrypt_samba_gpg == True:
1882 add_ldif += "decryptSambaGPG: TRUE\n"
1884 add_ldif += "decryptSambaGPG: FALSE\n"
1885 if self.sync_command is not None:
1886 add_ldif += "syncCommand: %s\n" % self.sync_command
1887 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1888 self.cache.add_ldif(add_ldif)
1889 self.current_pid = None
1890 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1891 msgs = self.cache.parse_ldif(add_ldif)
1892 changetype, msg = next(msgs)
1893 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1894 self.outf.write("%s" % ldif)
1896 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1897 self.dirsync_attrs = []
1898 for a in res[0]["dirsyncAttribute"]:
1899 self.dirsync_attrs.append(a)
1900 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1901 self.password_attrs = []
1902 for a in res[0]["passwordAttribute"]:
1903 self.password_attrs.append(a)
1904 decrypt_string = res[0]["decryptSambaGPG"][0]
1905 assert(decrypt_string in ["TRUE", "FALSE"])
1906 if decrypt_string == "TRUE":
1907 self.decrypt_samba_gpg = True
1909 self.decrypt_samba_gpg = False
1910 if "syncCommand" in res[0]:
1911 self.sync_command = res[0]["syncCommand"][0]
1913 self.sync_command = None
1914 if "currentPid" in res[0]:
1915 self.current_pid = int(res[0]["currentPid"][0])
1917 self.current_pid = None
1918 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1922 def run_sync_command(dn, ldif):
1923 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1924 sync_command_p = Popen(self.sync_command,
1929 res = sync_command_p.poll()
1932 input = "%s" % (ldif)
1933 reply = sync_command_p.communicate(input)[0]
1934 log_msg("%s\n" % (reply))
1935 res = sync_command_p.poll()
1937 sync_command_p.terminate()
1938 res = sync_command_p.wait()
1940 if reply.startswith("DONE-EXIT: "):
1943 log_msg("RESULT: %s\n" % (res))
1944 raise Exception("ERROR: %s - %s\n" % (res, reply))
1946 def handle_object(idx, dirsync_obj):
1947 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1948 guid = ndr_unpack(misc.GUID, binary_guid)
1949 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1950 sid = ndr_unpack(security.dom_sid, binary_sid)
1951 domain_sid, rid = sid.split()
1952 if rid == security.DOMAIN_RID_KRBTGT:
1953 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1955 for a in list(dirsync_obj.keys()):
1956 for h in dirsync_secret_attrs:
1957 if a.lower() == h.lower():
1959 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1960 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1961 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
1962 obj = self.get_account_attributes(self.samdb,
1963 username="%s" % sid,
1964 basedn="<GUID=%s>" % guid,
1965 filter="(objectClass=user)",
1966 scope=ldb.SCOPE_BASE,
1967 attrs=self.password_attrs,
1968 decrypt=self.decrypt_samba_gpg)
1969 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1970 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1971 if self.sync_command is None:
1972 self.outf.write("%s" % (ldif))
1974 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1975 run_sync_command(obj.dn, ldif)
1977 def check_current_pid_conflict(terminate):
1983 self.lockfd = os.open(self.lockfile, flags, 0o600)
1984 except IOError as e4:
1985 (err, msg) = e4.args
1986 if err == errno.ENOENT:
1989 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1990 (self.lockfile, msg, err))
1993 got_exclusive = False
1995 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1996 got_exclusive = True
1997 except IOError as e5:
1998 (err, msg) = e5.args
1999 if err != errno.EACCES and err != errno.EAGAIN:
2000 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
2001 (self.lockfile, msg, err))
2004 if not got_exclusive:
2005 buf = os.read(self.lockfd, 64)
2006 self.current_pid = None
2008 self.current_pid = int(buf)
2009 except ValueError as e:
2011 if self.current_pid is not None:
2014 if got_exclusive and terminate:
2016 os.ftruncate(self.lockfd, 0)
2017 except IOError as e2:
2018 (err, msg) = e2.args
2019 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2020 (self.lockfile, msg, err))
2022 os.close(self.lockfd)
2027 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2028 except IOError as e6:
2029 (err, msg) = e6.args
2030 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2031 (self.lockfile, msg, err))
2033 # We leave the function with the shared lock.
2036 def update_pid(pid):
2037 if self.lockfd != -1:
2038 got_exclusive = False
2039 # Try 5 times to get the exclusiv lock.
2040 for i in range(0, 5):
2042 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2043 got_exclusive = True
2044 except IOError as e:
2046 if err != errno.EACCES and err != errno.EAGAIN:
2047 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2048 (pid, self.lockfile, msg, err))
2053 if not got_exclusive:
2054 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
2055 (pid, self.lockfile))
2056 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2057 (pid, self.lockfile))
2064 os.ftruncate(self.lockfd, 0)
2066 os.write(self.lockfd, buf)
2067 except IOError as e3:
2068 (err, msg) = e3.args
2069 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2070 (self.lockfile, msg, err))
2072 self.current_pid = pid
2073 if self.current_pid is not None:
2074 log_msg("currentPid: %d\n" % self.current_pid)
2076 modify_ldif = "dn: %s\n" % (self.cache_dn)
2077 modify_ldif += "changetype: modify\n"
2078 modify_ldif += "replace: currentPid\n"
2079 if self.current_pid is not None:
2080 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2081 modify_ldif += "replace: currentTime\n"
2082 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2083 self.cache.modify_ldif(modify_ldif)
2086 def update_cache(res_controls):
2087 assert len(res_controls) > 0
2088 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2089 res_controls[0].critical = True
2090 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2091 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2093 modify_ldif = "dn: %s\n" % (self.cache_dn)
2094 modify_ldif += "changetype: modify\n"
2095 modify_ldif += "replace: dirsyncControl\n"
2096 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2097 modify_ldif += "replace: currentTime\n"
2098 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2099 self.cache.modify_ldif(modify_ldif)
2102 def check_object(dirsync_obj, res_controls):
2103 assert len(res_controls) > 0
2104 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2106 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2107 sid = ndr_unpack(security.dom_sid, binary_sid)
2109 lastCookie = str(res_controls[0])
2111 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2112 expression="(lastCookie=%s)" % (
2113 ldb.binary_encode(lastCookie)),
2119 def update_object(dirsync_obj, res_controls):
2120 assert len(res_controls) > 0
2121 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2123 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2124 sid = ndr_unpack(security.dom_sid, binary_sid)
2126 lastCookie = str(res_controls[0])
2128 self.cache.transaction_start()
2130 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2131 expression="(objectClass=*)",
2132 attrs=["lastCookie"])
2134 add_ldif = "dn: %s\n" % (dn)
2135 add_ldif += "objectClass: userCookie\n"
2136 add_ldif += "lastCookie: %s\n" % (lastCookie)
2137 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2138 self.cache.add_ldif(add_ldif)
2140 modify_ldif = "dn: %s\n" % (dn)
2141 modify_ldif += "changetype: modify\n"
2142 modify_ldif += "replace: lastCookie\n"
2143 modify_ldif += "lastCookie: %s\n" % (lastCookie)
2144 modify_ldif += "replace: currentTime\n"
2145 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2146 self.cache.modify_ldif(modify_ldif)
2147 self.cache.transaction_commit()
2148 except Exception as e:
2149 self.cache.transaction_cancel()
2155 res = self.samdb.search(expression=self.dirsync_filter,
2156 scope=ldb.SCOPE_SUBTREE,
2157 attrs=self.dirsync_attrs,
2158 controls=self.dirsync_controls)
2159 log_msg("dirsync_loop(): results %d\n" % len(res))
2162 done = check_object(r, res.controls)
2164 handle_object(ri, r)
2165 update_object(r, res.controls)
2167 update_cache(res.controls)
2171 def sync_loop(wait):
2172 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2173 notify_controls = ["notification:1", "show_recycled:1"]
2174 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2175 scope=ldb.SCOPE_SUBTREE,
2177 controls=notify_controls,
2181 log_msg("Resuming monitoring\n")
2183 log_msg("Getting changes\n")
2184 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2185 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2186 self.outf.write("syncCommand: %s\n" % self.sync_command)
2189 if wait is not True:
2192 for msg in notify_handle:
2193 if not isinstance(msg, ldb.Message):
2194 self.outf.write("referal: %s\n" % msg)
2196 created = msg.get("uSNCreated")[0]
2197 changed = msg.get("uSNChanged")[0]
2198 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2199 (msg.dn, created, changed))
2203 res = notify_handle.result()
2208 orig_pid = os.getpid()
2213 if pid == 0: # Actual daemon
2215 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2220 if cache_ldb_initialize:
2222 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2227 if logfile is not None:
2228 import resource # Resource usage information.
2229 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2230 if maxfd == resource.RLIM_INFINITY:
2231 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2232 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2233 self.outf.write("Using logfile[%s]\n" % logfile)
2234 for fd in range(0, maxfd):
2245 log_msg("Attached to logfile[%s]\n" % (logfile))
2246 self.logfile = logfile
2249 conflict = check_current_pid_conflict(terminate)
2251 if self.current_pid is None:
2252 log_msg("No process running.\n")
2255 log_msg("Proccess %d is not running anymore.\n" % (
2259 log_msg("Sending SIGTERM to proccess %d.\n" % (
2261 os.kill(self.current_pid, signal.SIGTERM)
2264 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2265 os.getpid(), self.current_pid))
2269 update_pid(os.getpid())
2274 retry_sleep_max = 600
2279 retry_sleep = retry_sleep_min
2281 while self.samdb is None:
2282 if retry_sleep != 0:
2283 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2284 time.sleep(retry_sleep)
2285 retry_sleep = retry_sleep * 2
2286 if retry_sleep >= retry_sleep_max:
2287 retry_sleep = retry_sleep_max
2288 log_msg("Connecting to '%s'\n" % self.samdb_url)
2290 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2291 except Exception as msg:
2293 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2294 if wait is not True:
2299 except ldb.LdbError as e7:
2300 (enum, estr) = e7.args
2302 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2308 class cmd_user_edit(Command):
2309 """Modify User AD object.
2311 This command will allow editing of a user account in the Active Directory
2312 domain. You will then be able to add or change attributes and their values.
2314 The username specified on the command is the sAMAccountName.
2316 The command may be run from the root userid or another authorized userid.
2318 The -H or --URL= option can be used to execute the command against a remote
2322 samba-tool user edit User1 -H ldap://samba.samdom.example.com \
2323 -U administrator --password=passw1rd
2325 Example1 shows how to edit a users attributes in the domain against a remote
2328 The -H parameter is used to specify the remote target server.
2331 samba-tool user edit User2
2333 Example2 shows how to edit a users attributes in the domain against a local
2337 samba-tool user edit User3 --editor=nano
2339 Example3 shows how to edit a users attributes in the domain against a local
2340 LDAP server using the 'nano' editor.
2343 synopsis = "%prog <username> [options]"
2346 Option("-H", "--URL", help="LDB URL for database or target server",
2347 type=str, metavar="URL", dest="H"),
2348 Option("--editor", help="Editor to use instead of the system default,"
2349 " or 'vi' if no system default is set.", type=str),
2352 takes_args = ["username"]
2353 takes_optiongroups = {
2354 "sambaopts": options.SambaOptions,
2355 "credopts": options.CredentialsOptions,
2356 "versionopts": options.VersionOptions,
2359 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2360 H=None, editor=None):
2362 lp = sambaopts.get_loadparm()
2363 creds = credopts.get_credentials(lp, fallback_machine=True)
2364 samdb = SamDB(url=H, session_info=system_session(),
2365 credentials=creds, lp=lp)
2367 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2368 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2370 domaindn = samdb.domain_dn()
2373 res = samdb.search(base=domaindn,
2375 scope=ldb.SCOPE_SUBTREE)
2378 raise CommandError('Unable to find user "%s"' % (username))
2381 r_ldif = samdb.write_ldif(msg, 1)
2382 # remove 'changetype' line
2383 result_ldif = re.sub('changetype: add\n', '', r_ldif)
2386 editor = os.environ.get('EDITOR')
2390 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2391 t_file.write(result_ldif)
2394 check_call([editor, t_file.name])
2395 except CalledProcessError as e:
2396 raise CalledProcessError("ERROR: ", e)
2397 with open(t_file.name) as edited_file:
2398 edited_message = edited_file.read()
2400 if result_ldif != edited_message:
2401 diff = difflib.ndiff(result_ldif.splitlines(),
2402 edited_message.splitlines())
2406 if line.startswith('-'):
2408 minus_lines.append(line)
2409 elif line.startswith('+'):
2411 plus_lines.append(line)
2413 user_ldif = "dn: %s\n" % user_dn
2414 user_ldif += "changetype: modify\n"
2416 for line in minus_lines:
2417 attr, val = line.split(':', 1)
2418 search_attr = "%s:" % attr
2419 if not re.search(r'^' + search_attr, str(plus_lines)):
2420 user_ldif += "delete: %s\n" % attr
2421 user_ldif += "%s: %s\n" % (attr, val)
2423 for line in plus_lines:
2424 attr, val = line.split(':', 1)
2425 search_attr = "%s:" % attr
2426 if re.search(r'^' + search_attr, str(minus_lines)):
2427 user_ldif += "replace: %s\n" % attr
2428 user_ldif += "%s: %s\n" % (attr, val)
2429 if not re.search(r'^' + search_attr, str(minus_lines)):
2430 user_ldif += "add: %s\n" % attr
2431 user_ldif += "%s: %s\n" % (attr, val)
2434 samdb.modify_ldif(user_ldif)
2435 except Exception as e:
2436 raise CommandError("Failed to modify user '%s': " %
2439 self.outf.write("Modified User '%s' successfully\n" % username)
2442 class cmd_user_show(Command):
2443 """Display a user AD object.
2445 This command displays a user account and it's attributes in the Active
2447 The username specified on the command is the sAMAccountName.
2449 The command may be run from the root userid or another authorized userid.
2451 The -H or --URL= option can be used to execute the command against a remote
2455 samba-tool user show User1 -H ldap://samba.samdom.example.com \
2456 -U administrator --password=passw1rd
2458 Example1 shows how to display a users attributes in the domain against a remote
2461 The -H parameter is used to specify the remote target server.
2464 samba-tool user show User2
2466 Example2 shows how to display a users attributes in the domain against a local
2470 samba-tool user show User2 --attributes=objectSid,memberOf
2472 Example3 shows how to display a users objectSid and memberOf attributes.
2474 synopsis = "%prog <username> [options]"
2477 Option("-H", "--URL", help="LDB URL for database or target server",
2478 type=str, metavar="URL", dest="H"),
2479 Option("--attributes",
2480 help=("Comma separated list of attributes, "
2481 "which will be printed."),
2482 type=str, dest="user_attrs"),
2485 takes_args = ["username"]
2486 takes_optiongroups = {
2487 "sambaopts": options.SambaOptions,
2488 "credopts": options.CredentialsOptions,
2489 "versionopts": options.VersionOptions,
2492 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2493 H=None, user_attrs=None):
2495 lp = sambaopts.get_loadparm()
2496 creds = credopts.get_credentials(lp, fallback_machine=True)
2497 samdb = SamDB(url=H, session_info=system_session(),
2498 credentials=creds, lp=lp)
2502 attrs = user_attrs.split(",")
2504 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2505 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2507 domaindn = samdb.domain_dn()
2510 res = samdb.search(base=domaindn, expression=filter,
2511 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2514 raise CommandError('Unable to find user "%s"' % (username))
2517 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2518 self.outf.write(user_ldif)
2521 class cmd_user_move(Command):
2522 """Move a user to an organizational unit/container.
2524 This command moves a user account into the specified organizational unit
2526 The username specified on the command is the sAMAccountName.
2527 The name of the organizational unit or container can be specified as a
2528 full DN or without the domainDN component.
2530 The command may be run from the root userid or another authorized userid.
2532 The -H or --URL= option can be used to execute the command against a remote
2536 samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
2537 -H ldap://samba.samdom.example.com -U administrator
2539 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2540 unit on a remote LDAP server.
2542 The -H parameter is used to specify the remote target server.
2545 samba-tool user move User1 CN=Users
2547 Example2 shows how to move a user User1 back into the CN=Users container
2548 on the local server.
2551 synopsis = "%prog <username> <new_parent_dn> [options]"
2554 Option("-H", "--URL", help="LDB URL for database or target server",
2555 type=str, metavar="URL", dest="H"),
2558 takes_args = ["username", "new_parent_dn"]
2559 takes_optiongroups = {
2560 "sambaopts": options.SambaOptions,
2561 "credopts": options.CredentialsOptions,
2562 "versionopts": options.VersionOptions,
2565 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2566 versionopts=None, H=None):
2567 lp = sambaopts.get_loadparm()
2568 creds = credopts.get_credentials(lp, fallback_machine=True)
2569 samdb = SamDB(url=H, session_info=system_session(),
2570 credentials=creds, lp=lp)
2571 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2573 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2574 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2576 res = samdb.search(base=domain_dn,
2578 scope=ldb.SCOPE_SUBTREE)
2581 raise CommandError('Unable to find user "%s"' % (username))
2584 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2585 except Exception as e:
2586 raise CommandError('Invalid new_parent_dn "%s": %s' %
2587 (new_parent_dn, e.message))
2589 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2590 full_new_user_dn.remove_base_components(len(user_dn) - 1)
2591 full_new_user_dn.add_base(full_new_parent_dn)
2594 samdb.rename(user_dn, full_new_user_dn)
2595 except Exception as e:
2596 raise CommandError('Failed to move user "%s"' % username, e)
2597 self.outf.write('Moved user "%s" into "%s"\n' %
2598 (username, full_new_parent_dn))
2601 class cmd_user(SuperCommand):
2602 """User management."""
2605 subcommands["add"] = cmd_user_add()
2606 subcommands["create"] = cmd_user_create()
2607 subcommands["delete"] = cmd_user_delete()
2608 subcommands["disable"] = cmd_user_disable()
2609 subcommands["enable"] = cmd_user_enable()
2610 subcommands["list"] = cmd_user_list()
2611 subcommands["setexpiry"] = cmd_user_setexpiry()
2612 subcommands["password"] = cmd_user_password()
2613 subcommands["setpassword"] = cmd_user_setpassword()
2614 subcommands["getpassword"] = cmd_user_getpassword()
2615 subcommands["syncpasswords"] = cmd_user_syncpasswords()
2616 subcommands["edit"] = cmd_user_edit()
2617 subcommands["show"] = cmd_user_show()
2618 subcommands["move"] = cmd_user_move()