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"]
1000 def get_package(name, min_idx=0):
1001 if name in calculated:
1002 return calculated[name]
1006 min_idx = len(sc.sub.packages) + min_idx
1008 for p in sc.sub.packages:
1015 return binascii.a2b_hex(p.data)
1020 # Samba adds 'Primary:SambaGPG' at the end.
1021 # When Windows sets the password it keeps
1022 # 'Primary:SambaGPG' and rotates it to
1023 # the begining. So we can only use the value,
1024 # if it is the last one.
1026 # In order to get more protection we verify
1027 # the nthash of the decrypted utf16 password
1028 # against the stored nthash in unicodePwd.
1030 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1031 if sgv is not None and unicodePwd is not None:
1032 ctx = gpgme.Context()
1034 cipher_io = io.BytesIO(sgv)
1035 plain_io = io.BytesIO()
1037 ctx.decrypt(cipher_io, plain_io)
1038 cv = plain_io.getvalue()
1040 # We only use the password if it matches
1041 # the current nthash stored in the unicodePwd
1044 tmp = credentials.Credentials()
1046 tmp.set_utf16_password(cv)
1047 nthash = tmp.get_nt_hash()
1048 if nthash == unicodePwd:
1049 calculated["Primary:CLEARTEXT"] = cv
1050 except gpgme.GpgmeError as e1:
1051 (major, minor, msg) = e1.args
1052 if major == gpgme.ERR_BAD_SECKEY:
1053 msg = "ERR_BAD_SECKEY: " + msg
1055 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1056 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1057 username or account_name, msg))
1059 def get_utf8(a, b, username):
1061 u = unicode(b, 'utf-16-le')
1062 except UnicodeDecodeError as e:
1063 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1066 u8 = u.encode('utf-8')
1069 # Extract the WDigest hash for the value specified by i.
1070 # Builds an htdigest compatible value
1073 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1074 domain, dns_domain):
1079 user = account_name.lower()
1080 realm = domain.lower()
1082 user = account_name.upper()
1083 realm = domain.upper()
1086 realm = domain.upper()
1089 realm = domain.lower()
1091 user = account_name.upper()
1092 realm = domain.lower()
1094 user = account_name.lower()
1095 realm = domain.upper()
1098 realm = dns_domain.lower()
1100 user = account_name.lower()
1101 realm = dns_domain.lower()
1103 user = account_name.upper()
1104 realm = dns_domain.upper()
1107 realm = dns_domain.upper()
1110 realm = dns_domain.lower()
1112 user = account_name.upper()
1113 realm = dns_domain.lower()
1115 user = account_name.lower()
1116 realm = dns_domain.upper()
1121 user = account_upn.lower()
1124 user = account_upn.upper()
1127 user = "%s\\%s" % (domain, account_name)
1130 user = "%s\\%s" % (domain.lower(), account_name.lower())
1133 user = "%s\\%s" % (domain.upper(), account_name.upper())
1139 user = account_name.lower()
1142 user = account_name.upper()
1148 user = account_upn.lower()
1151 user = account_upn.upper()
1154 user = "%s\\%s" % (domain, account_name)
1157 # Differs from spec, see tests
1158 user = "%s\\%s" % (domain.lower(), account_name.lower())
1161 # Differs from spec, see tests
1162 user = "%s\\%s" % (domain.upper(), account_name.upper())
1167 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1170 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
1171 return "%s:%s:%s" % (user, realm, digest)
1175 # get the value for a virtualCrypt attribute.
1176 # look for an exact match on algorithm and rounds in supplemental creds
1177 # if not found calculate using Primary:CLEARTEXT
1178 # if no Primary:CLEARTEXT return the first supplementalCredential
1179 # that matches the algorithm.
1180 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1183 b = get_package("Primary:userPassword")
1185 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1187 # No exact match on algorithm and number of rounds
1188 # try and calculate one from the Primary:CLEARTEXT
1189 b = get_package("Primary:CLEARTEXT")
1191 u8 = get_utf8(a, b, username or account_name)
1193 sv = get_crypt_value(str(algorithm), u8, rounds)
1195 # Unable to calculate a hash with the specified
1196 # number of rounds, fall back to the first hash using
1197 # the specified algorithm
1201 return "{CRYPT}" + sv
1203 def get_userPassword_hash(blob, algorithm, rounds):
1204 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1207 # Check that the NT hash has not been changed without updating
1208 # the user password hashes. This indicates that password has been
1209 # changed without updating the supplemental credentials.
1210 if unicodePwd != bytearray(up.current_nt_hash.hash):
1213 scheme_prefix = "$%d$" % algorithm
1214 prefix = scheme_prefix
1216 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1220 if (scheme_match is None and
1221 h.scheme == SCHEME and
1222 h.value.startswith(scheme_prefix)):
1223 scheme_match = h.value
1224 if h.scheme == SCHEME and h.value.startswith(prefix):
1225 return (h.value, scheme_match)
1227 # No match on the number of rounds, return the value of the
1228 # first matching scheme
1229 return (None, scheme_match)
1231 # We use sort here in order to have a predictable processing order
1232 for a in sorted(virtual_attributes.keys()):
1233 if not a.lower() in lower_attrs:
1236 if a == "virtualClearTextUTF8":
1237 b = get_package("Primary:CLEARTEXT")
1240 u8 = get_utf8(a, b, username or account_name)
1244 elif a == "virtualClearTextUTF16":
1245 v = get_package("Primary:CLEARTEXT")
1248 elif a == "virtualSSHA":
1249 b = get_package("Primary:CLEARTEXT")
1252 u8 = get_utf8(a, b, username or account_name)
1255 salt = get_random_bytes(4)
1259 bv = h.digest() + salt
1260 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
1261 elif a == "virtualCryptSHA256":
1262 rounds = get_rounds(attr_opts[a])
1263 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1267 elif a == "virtualCryptSHA512":
1268 rounds = get_rounds(attr_opts[a])
1269 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1273 elif a == "virtualSambaGPG":
1274 # Samba adds 'Primary:SambaGPG' at the end.
1275 # When Windows sets the password it keeps
1276 # 'Primary:SambaGPG' and rotates it to
1277 # the begining. So we can only use the value,
1278 # if it is the last one.
1279 v = get_package("Primary:SambaGPG", min_idx=-1)
1282 elif a.startswith("virtualWDigest"):
1283 primary_wdigest = get_package("Primary:WDigest")
1284 if primary_wdigest is None:
1286 x = a[len("virtualWDigest"):]
1291 domain = self.lp.get("workgroup")
1292 dns_domain = samdb.domain_dns_name()
1293 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1298 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1301 def parse_attributes(self, attributes):
1303 if attributes is None:
1304 raise CommandError("Please specify --attributes")
1305 attrs = attributes.split(',')
1308 pa = pa.lstrip().rstrip()
1309 for da in disabled_virtual_attributes.keys():
1310 if pa.lower() == da.lower():
1311 r = disabled_virtual_attributes[da]["reason"]
1312 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1314 for va in virtual_attributes.keys():
1315 if pa.lower() == va.lower():
1316 # Take the real name
1319 password_attrs += [pa]
1321 return password_attrs
1324 class cmd_user_getpassword(GetPasswordCommand):
1325 """Get the password fields of a user/computer account.
1327 This command gets the logon password for a user/computer account.
1329 The username specified on the command is the sAMAccountName.
1330 The username may also be specified using the --filter option.
1332 The command must be run from the root user id or another authorized user id.
1333 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1334 used to adjust the local path. By default tdb:// is used by default.
1336 The '--attributes' parameter takes a comma separated list of attributes,
1337 which will be printed or given to the script specified by '--script'. If a
1338 specified attribute is not available on an object it's silently omitted.
1339 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1340 the NTHASH) and the following virtual attributes are possible (see --help
1341 for which virtual attributes are supported in your environment):
1343 virtualClearTextUTF16: The raw cleartext as stored in the
1344 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1345 with '--decrypt-samba-gpg') buffer inside of the
1346 supplementalCredentials attribute. This typically
1347 contains valid UTF-16-LE, but may contain random
1348 bytes, e.g. for computer accounts.
1350 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1351 (only from valid UTF-16-LE)
1353 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1354 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1356 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1357 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1358 with a $5$... salt, see crypt(3) on modern systems.
1359 The number of rounds used to calculate the hash can
1360 also be specified. By appending ";rounds=x" to the
1361 attribute name i.e. virtualCryptSHA256;rounds=10000
1362 will calculate a SHA256 hash with 10,000 rounds.
1363 non numeric values for rounds are silently ignored
1364 The value is calculated as follows:
1365 1) If a value exists in 'Primary:userPassword' with
1366 the specified number of rounds it is returned.
1367 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1368 '--decrypt-samba-gpg'. Calculate a hash with
1369 the specified number of rounds
1370 3) Return the first CryptSHA256 value in
1371 'Primary:userPassword'
1374 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1375 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1376 with a $6$... salt, see crypt(3) on modern systems.
1377 The number of rounds used to calculate the hash can
1378 also be specified. By appending ";rounds=x" to the
1379 attribute name i.e. virtualCryptSHA512;rounds=10000
1380 will calculate a SHA512 hash with 10,000 rounds.
1381 non numeric values for rounds are silently ignored
1382 The value is calculated as follows:
1383 1) If a value exists in 'Primary:userPassword' with
1384 the specified number of rounds it is returned.
1385 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1386 '--decrypt-samba-gpg'. Calculate a hash with
1387 the specified number of rounds
1388 3) Return the first CryptSHA512 value in
1389 'Primary:userPassword'
1391 virtualWDigestNN: The individual hash values stored in
1392 'Primary:WDigest' where NN is the hash number in
1394 NOTE: As at 22-05-2017 the documentation:
1395 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1396 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1399 virtualSambaGPG: The raw cleartext as stored in the
1400 'Primary:SambaGPG' buffer inside of the
1401 supplementalCredentials attribute.
1402 See the 'password hash gpg key ids' option in
1405 The '--decrypt-samba-gpg' option triggers decryption of the
1406 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1407 in your environment or not (the python-gpgme package is required). Please
1408 note that you might need to set the GNUPGHOME environment variable. If the
1409 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1410 environment variable has been set correctly and the passphrase is already
1411 known by the gpg-agent.
1414 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1417 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1421 super(cmd_user_getpassword, self).__init__()
1423 synopsis = "%prog (<username>|--filter <filter>) [options]"
1425 takes_optiongroups = {
1426 "sambaopts": options.SambaOptions,
1427 "versionopts": options.VersionOptions,
1431 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1432 metavar="URL", dest="H"),
1433 Option("--filter", help="LDAP Filter to set password on", type=str),
1434 Option("--attributes", type=str,
1435 help=virtual_attributes_help,
1436 metavar="ATTRIBUTELIST", dest="attributes"),
1437 Option("--decrypt-samba-gpg",
1438 help=decrypt_samba_gpg_help,
1439 action="store_true", default=False, dest="decrypt_samba_gpg"),
1442 takes_args = ["username?"]
1444 def run(self, username=None, H=None, filter=None,
1445 attributes=None, decrypt_samba_gpg=None,
1446 sambaopts=None, versionopts=None):
1447 self.lp = sambaopts.get_loadparm()
1449 if decrypt_samba_gpg and not gpgme_support:
1450 raise CommandError(decrypt_samba_gpg_help)
1452 if filter is None and username is None:
1453 raise CommandError("Either the username or '--filter' must be specified!")
1456 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1458 if attributes is None:
1459 raise CommandError("Please specify --attributes")
1461 password_attrs = self.parse_attributes(attributes)
1463 samdb = self.connect_system_samdb(url=H, allow_local=True)
1465 obj = self.get_account_attributes(samdb, username,
1468 scope=ldb.SCOPE_SUBTREE,
1469 attrs=password_attrs,
1470 decrypt=decrypt_samba_gpg)
1472 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1473 self.outf.write("%s" % ldif)
1474 self.outf.write("Got password OK\n")
1477 class cmd_user_syncpasswords(GetPasswordCommand):
1478 """Sync the password of user accounts.
1480 This syncs logon passwords for user accounts.
1482 Note that this command should run on a single domain controller only
1483 (typically the PDC-emulator). However the "password hash gpg key ids"
1484 option should to be configured on all domain controllers.
1486 The command must be run from the root user id or another authorized user id.
1487 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1488 local path. By default, ldapi:// is used with the default path to the
1489 privileged ldapi socket.
1491 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1492 "Sync Loop Terminate".
1495 Cache Initialization
1496 ====================
1498 The first time, this command needs to be called with
1499 '--cache-ldb-initialize' in order to initialize its cache.
1501 The cache initialization requires '--attributes' and allows the following
1502 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1505 The '--attributes' parameter takes a comma separated list of attributes,
1506 which will be printed or given to the script specified by '--script'. If a
1507 specified attribute is not available on an object it will be silently omitted.
1508 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1509 the NTHASH) and the following virtual attributes are possible (see '--help'
1510 for supported virtual attributes in your environment):
1512 virtualClearTextUTF16: The raw cleartext as stored in the
1513 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1514 with '--decrypt-samba-gpg') buffer inside of the
1515 supplementalCredentials attribute. This typically
1516 contains valid UTF-16-LE, but may contain random
1517 bytes, e.g. for computer accounts.
1519 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1520 (only from valid UTF-16-LE)
1522 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1523 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1525 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1526 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1527 with a $5$... salt, see crypt(3) on modern systems.
1528 The number of rounds used to calculate the hash can
1529 also be specified. By appending ";rounds=x" to the
1530 attribute name i.e. virtualCryptSHA256;rounds=10000
1531 will calculate a SHA256 hash with 10,000 rounds.
1532 non numeric values for rounds are silently ignored
1533 The value is calculated as follows:
1534 1) If a value exists in 'Primary:userPassword' with
1535 the specified number of rounds it is returned.
1536 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1537 '--decrypt-samba-gpg'. Calculate a hash with
1538 the specified number of rounds
1539 3) Return the first CryptSHA256 value in
1540 'Primary:userPassword'
1542 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1543 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1544 with a $6$... salt, see crypt(3) on modern systems.
1545 The number of rounds used to calculate the hash can
1546 also be specified. By appending ";rounds=x" to the
1547 attribute name i.e. virtualCryptSHA512;rounds=10000
1548 will calculate a SHA512 hash with 10,000 rounds.
1549 non numeric values for rounds are silently ignored
1550 The value is calculated as follows:
1551 1) If a value exists in 'Primary:userPassword' with
1552 the specified number of rounds it is returned.
1553 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1554 '--decrypt-samba-gpg'. Calculate a hash with
1555 the specified number of rounds
1556 3) Return the first CryptSHA512 value in
1557 'Primary:userPassword'
1559 virtualWDigestNN: The individual hash values stored in
1560 'Primary:WDigest' where NN is the hash number in
1562 NOTE: As at 22-05-2017 the documentation:
1563 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1564 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1567 virtualSambaGPG: The raw cleartext as stored in the
1568 'Primary:SambaGPG' buffer inside of the
1569 supplementalCredentials attribute.
1570 See the 'password hash gpg key ids' option in
1573 The '--decrypt-samba-gpg' option triggers decryption of the
1574 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1575 in your environment or not (the python-gpgme package is required). Please
1576 note that you might need to set the GNUPGHOME environment variable. If the
1577 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1578 environment variable has been set correctly and the passphrase is already
1579 known by the gpg-agent.
1581 The '--script' option specifies a custom script that is called whenever any
1582 of the dirsyncAttributes (see below) was changed. The script is called
1583 without any arguments. It gets the LDIF for exactly one object on STDIN.
1584 If the script processed the object successfully it has to respond with a
1585 single line starting with 'DONE-EXIT: ' followed by an optional message.
1587 Note that the script might be called without any password change, e.g. if
1588 the account was disabled (a userAccountControl change) or the
1589 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1590 are always returned as unique identifier of the account. It might be useful
1591 to also ask for non-password attributes like: objectSid, sAMAccountName,
1592 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1593 Depending on the object, some attributes may not be present/available,
1594 but you always get the current state (and not a diff).
1596 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1599 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1600 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1601 (!(sAMAccountName=krbtgt*)))
1602 This means only normal (non-krbtgt) user
1603 accounts are monitored. The '--filter' can modify that, e.g. if it's
1604 required to also sync computer accounts.
1610 This (default) mode runs in an endless loop waiting for password related
1611 changes in the active directory database. It makes use of the
1612 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1613 get changes in a reliable fashion. Objects are monitored for changes of the
1614 following dirsyncAttributes:
1616 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1617 userPrincipalName and userAccountControl.
1619 It recovers from LDAP disconnects and updates the cache in conservative way
1620 (in single steps after each successfully processed change). An error from
1621 the script (specified by '--script') will result in fatal error and this
1622 command will exit. But the cache state should be still valid and can be
1623 resumed in the next "Sync Loop Run".
1625 The '--logfile' option specifies an optional (required if '--daemon' is
1626 specified) logfile that takes all output of the command. The logfile is
1627 automatically reopened if fstat returns st_nlink == 0.
1629 The optional '--daemon' option will put the command into the background.
1631 You can stop the command without the '--daemon' option, also by hitting
1634 If you specify the '--no-wait' option the command skips the
1635 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1636 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1641 In order to terminate an already running command (likely as daemon) the
1642 '--terminate' option can be used. This also requires the '--logfile' option
1647 samba-tool user syncpasswords --cache-ldb-initialize \\
1648 --attributes=virtualClearTextUTF8
1649 samba-tool user syncpasswords
1652 samba-tool user syncpasswords --cache-ldb-initialize \\
1653 --attributes=objectGUID,objectSID,sAMAccountName,\\
1654 userPrincipalName,userAccountControl,pwdLastSet,\\
1655 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1656 --script=/path/to/my-custom-syncpasswords-script.py
1657 samba-tool user syncpasswords --daemon \\
1658 --logfile=/var/log/samba/user-syncpasswords.log
1659 samba-tool user syncpasswords --terminate \\
1660 --logfile=/var/log/samba/user-syncpasswords.log
1664 super(cmd_user_syncpasswords, self).__init__()
1666 synopsis = "%prog [--cache-ldb-initialize] [options]"
1668 takes_optiongroups = {
1669 "sambaopts": options.SambaOptions,
1670 "versionopts": options.VersionOptions,
1674 Option("--cache-ldb-initialize",
1675 help="Initialize the cache for the first time",
1676 dest="cache_ldb_initialize", action="store_true"),
1677 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1678 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1679 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1680 metavar="URL", dest="H"),
1681 Option("--filter", help="optional LDAP filter to set password on", type=str,
1682 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1683 Option("--attributes", type=str,
1684 help=virtual_attributes_help,
1685 metavar="ATTRIBUTELIST", dest="attributes"),
1686 Option("--decrypt-samba-gpg",
1687 help=decrypt_samba_gpg_help,
1688 action="store_true", default=False, dest="decrypt_samba_gpg"),
1689 Option("--script", help="Script that is called for each password change", type=str,
1690 metavar="/path/to/syncpasswords.script", dest="script"),
1691 Option("--no-wait", help="Don't block waiting for changes",
1692 action="store_true", default=False, dest="nowait"),
1693 Option("--logfile", type=str,
1694 help="The logfile to use (required in --daemon mode).",
1695 metavar="/path/to/syncpasswords.log", dest="logfile"),
1696 Option("--daemon", help="daemonize after initial setup",
1697 action="store_true", default=False, dest="daemon"),
1698 Option("--terminate",
1699 help="Send a SIGTERM to an already running (daemon) process",
1700 action="store_true", default=False, dest="terminate"),
1703 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1704 H=None, filter=None,
1705 attributes=None, decrypt_samba_gpg=None,
1706 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1707 sambaopts=None, versionopts=None):
1709 self.lp = sambaopts.get_loadparm()
1711 self.samdb_url = None
1715 if not cache_ldb_initialize:
1716 if attributes is not None:
1717 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1718 if decrypt_samba_gpg:
1719 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1720 if script is not None:
1721 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1722 if filter is not None:
1723 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1725 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1727 if nowait is not False:
1728 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1729 if logfile is not None:
1730 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1731 if daemon is not False:
1732 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1733 if terminate is not False:
1734 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1738 raise CommandError("--daemon is not allowed together with --no-wait")
1739 if terminate is not False:
1740 raise CommandError("--terminate is not allowed together with --no-wait")
1742 if terminate is True and daemon is True:
1743 raise CommandError("--terminate is not allowed together with --daemon")
1745 if daemon is True and logfile is None:
1746 raise CommandError("--daemon is only allowed together with --logfile")
1748 if terminate is True and logfile is None:
1749 raise CommandError("--terminate is only allowed together with --logfile")
1751 if script is not None:
1752 if not os.path.exists(script):
1753 raise CommandError("script[%s] does not exist!" % script)
1755 sync_command = "%s" % os.path.abspath(script)
1759 dirsync_filter = filter
1760 if dirsync_filter is None:
1761 dirsync_filter = "(&" + \
1762 "(objectClass=user)" + \
1763 "(userAccountControl:%s:=%u)" % (
1764 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1765 "(!(sAMAccountName=krbtgt*))" + \
1768 dirsync_secret_attrs = [
1771 "supplementalCredentials",
1774 dirsync_attrs = dirsync_secret_attrs + [
1777 "userPrincipalName",
1778 "userAccountControl",
1783 password_attrs = None
1785 if cache_ldb_initialize:
1787 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1789 if decrypt_samba_gpg and not gpgme_support:
1790 raise CommandError(decrypt_samba_gpg_help)
1792 password_attrs = self.parse_attributes(attributes)
1793 lower_attrs = [x.lower() for x in password_attrs]
1794 # We always return these in order to track deletions
1795 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1796 if a.lower() not in lower_attrs:
1797 password_attrs += [a]
1799 if cache_ldb is not None:
1800 if cache_ldb.lower().startswith("ldapi://"):
1801 raise CommandError("--cache_ldb ldapi:// is not supported")
1802 elif cache_ldb.lower().startswith("ldap://"):
1803 raise CommandError("--cache_ldb ldap:// is not supported")
1804 elif cache_ldb.lower().startswith("ldaps://"):
1805 raise CommandError("--cache_ldb ldaps:// is not supported")
1806 elif cache_ldb.lower().startswith("tdb://"):
1809 if not os.path.exists(cache_ldb):
1810 cache_ldb = self.lp.private_path(cache_ldb)
1812 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1814 self.lockfile = "%s.pid" % cache_ldb
1817 if self.logfile is not None:
1819 if info.st_nlink == 0:
1820 logfile = self.logfile
1822 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1823 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
1828 log_msg("Reopened logfile[%s]\n" % (logfile))
1829 self.logfile = logfile
1830 msg = "%s: pid[%d]: %s" % (
1834 self.outf.write(msg)
1843 "passwordAttribute",
1849 self.cache = Ldb(cache_ldb)
1850 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1851 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1855 self.samdb_url = res[0]["samdbUrl"][0]
1856 except KeyError as e:
1857 self.samdb_url = None
1859 self.samdb_url = None
1860 if self.samdb_url is None and not cache_ldb_initialize:
1861 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1863 if self.samdb_url is not None and cache_ldb_initialize:
1864 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1866 if self.samdb_url is None:
1868 self.dirsync_filter = dirsync_filter
1869 self.dirsync_attrs = dirsync_attrs
1870 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"];
1871 self.password_attrs = password_attrs
1872 self.decrypt_samba_gpg = decrypt_samba_gpg
1873 self.sync_command = sync_command
1874 add_ldif = "dn: %s\n" % self.cache_dn
1875 add_ldif += "objectClass: userSyncPasswords\n"
1876 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url).decode('utf8')
1877 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter).decode('utf8')
1878 for a in self.dirsync_attrs:
1879 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a).decode('utf8')
1880 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1881 for a in self.password_attrs:
1882 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a).decode('utf8')
1883 if self.decrypt_samba_gpg == True:
1884 add_ldif += "decryptSambaGPG: TRUE\n"
1886 add_ldif += "decryptSambaGPG: FALSE\n"
1887 if self.sync_command is not None:
1888 add_ldif += "syncCommand: %s\n" % self.sync_command
1889 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1890 self.cache.add_ldif(add_ldif)
1891 self.current_pid = None
1892 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1893 msgs = self.cache.parse_ldif(add_ldif)
1894 changetype, msg = next(msgs)
1895 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1896 self.outf.write("%s" % ldif)
1898 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1899 self.dirsync_attrs = []
1900 for a in res[0]["dirsyncAttribute"]:
1901 self.dirsync_attrs.append(a)
1902 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1903 self.password_attrs = []
1904 for a in res[0]["passwordAttribute"]:
1905 self.password_attrs.append(a)
1906 decrypt_string = res[0]["decryptSambaGPG"][0]
1907 assert(decrypt_string in ["TRUE", "FALSE"])
1908 if decrypt_string == "TRUE":
1909 self.decrypt_samba_gpg = True
1911 self.decrypt_samba_gpg = False
1912 if "syncCommand" in res[0]:
1913 self.sync_command = res[0]["syncCommand"][0]
1915 self.sync_command = None
1916 if "currentPid" in res[0]:
1917 self.current_pid = int(res[0]["currentPid"][0])
1919 self.current_pid = None
1920 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1924 def run_sync_command(dn, ldif):
1925 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1926 sync_command_p = Popen(self.sync_command,
1931 res = sync_command_p.poll()
1934 input = "%s" % (ldif)
1935 reply = sync_command_p.communicate(input)[0]
1936 log_msg("%s\n" % (reply))
1937 res = sync_command_p.poll()
1939 sync_command_p.terminate()
1940 res = sync_command_p.wait()
1942 if reply.startswith("DONE-EXIT: "):
1945 log_msg("RESULT: %s\n" % (res))
1946 raise Exception("ERROR: %s - %s\n" % (res, reply))
1948 def handle_object(idx, dirsync_obj):
1949 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1950 guid = ndr_unpack(misc.GUID, binary_guid)
1951 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1952 sid = ndr_unpack(security.dom_sid, binary_sid)
1953 domain_sid, rid = sid.split()
1954 if rid == security.DOMAIN_RID_KRBTGT:
1955 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1957 for a in list(dirsync_obj.keys()):
1958 for h in dirsync_secret_attrs:
1959 if a.lower() == h.lower():
1961 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1962 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1963 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
1964 obj = self.get_account_attributes(self.samdb,
1965 username="%s" % sid,
1966 basedn="<GUID=%s>" % guid,
1967 filter="(objectClass=user)",
1968 scope=ldb.SCOPE_BASE,
1969 attrs=self.password_attrs,
1970 decrypt=self.decrypt_samba_gpg)
1971 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1972 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1973 if self.sync_command is None:
1974 self.outf.write("%s" % (ldif))
1976 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1977 run_sync_command(obj.dn, ldif)
1979 def check_current_pid_conflict(terminate):
1985 self.lockfd = os.open(self.lockfile, flags, 0o600)
1986 except IOError as e4:
1987 (err, msg) = e4.args
1988 if err == errno.ENOENT:
1991 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1992 (self.lockfile, msg, err))
1995 got_exclusive = False
1997 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1998 got_exclusive = True
1999 except IOError as e5:
2000 (err, msg) = e5.args
2001 if err != errno.EACCES and err != errno.EAGAIN:
2002 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
2003 (self.lockfile, msg, err))
2006 if not got_exclusive:
2007 buf = os.read(self.lockfd, 64)
2008 self.current_pid = None
2010 self.current_pid = int(buf)
2011 except ValueError as e:
2013 if self.current_pid is not None:
2016 if got_exclusive and terminate:
2018 os.ftruncate(self.lockfd, 0)
2019 except IOError as e2:
2020 (err, msg) = e2.args
2021 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2022 (self.lockfile, msg, err))
2024 os.close(self.lockfd)
2029 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2030 except IOError as e6:
2031 (err, msg) = e6.args
2032 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2033 (self.lockfile, msg, err))
2035 # We leave the function with the shared lock.
2038 def update_pid(pid):
2039 if self.lockfd != -1:
2040 got_exclusive = False
2041 # Try 5 times to get the exclusiv lock.
2042 for i in range(0, 5):
2044 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2045 got_exclusive = True
2046 except IOError as e:
2048 if err != errno.EACCES and err != errno.EAGAIN:
2049 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2050 (pid, self.lockfile, msg, err))
2055 if not got_exclusive:
2056 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
2057 (pid, self.lockfile))
2058 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2059 (pid, self.lockfile))
2066 os.ftruncate(self.lockfd, 0)
2068 os.write(self.lockfd, buf)
2069 except IOError as e3:
2070 (err, msg) = e3.args
2071 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2072 (self.lockfile, msg, err))
2074 self.current_pid = pid
2075 if self.current_pid is not None:
2076 log_msg("currentPid: %d\n" % self.current_pid)
2078 modify_ldif = "dn: %s\n" % (self.cache_dn)
2079 modify_ldif += "changetype: modify\n"
2080 modify_ldif += "replace: currentPid\n"
2081 if self.current_pid is not None:
2082 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2083 modify_ldif += "replace: currentTime\n"
2084 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2085 self.cache.modify_ldif(modify_ldif)
2088 def update_cache(res_controls):
2089 assert len(res_controls) > 0
2090 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2091 res_controls[0].critical = True
2092 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2093 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2095 modify_ldif = "dn: %s\n" % (self.cache_dn)
2096 modify_ldif += "changetype: modify\n"
2097 modify_ldif += "replace: dirsyncControl\n"
2098 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2099 modify_ldif += "replace: currentTime\n"
2100 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2101 self.cache.modify_ldif(modify_ldif)
2104 def check_object(dirsync_obj, res_controls):
2105 assert len(res_controls) > 0
2106 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2108 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2109 sid = ndr_unpack(security.dom_sid, binary_sid)
2111 lastCookie = str(res_controls[0])
2113 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2114 expression="(lastCookie=%s)" % (
2115 ldb.binary_encode(lastCookie)),
2121 def update_object(dirsync_obj, res_controls):
2122 assert len(res_controls) > 0
2123 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2125 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2126 sid = ndr_unpack(security.dom_sid, binary_sid)
2128 lastCookie = str(res_controls[0])
2130 self.cache.transaction_start()
2132 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2133 expression="(objectClass=*)",
2134 attrs=["lastCookie"])
2136 add_ldif = "dn: %s\n" % (dn)
2137 add_ldif += "objectClass: userCookie\n"
2138 add_ldif += "lastCookie: %s\n" % (lastCookie)
2139 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2140 self.cache.add_ldif(add_ldif)
2142 modify_ldif = "dn: %s\n" % (dn)
2143 modify_ldif += "changetype: modify\n"
2144 modify_ldif += "replace: lastCookie\n"
2145 modify_ldif += "lastCookie: %s\n" % (lastCookie)
2146 modify_ldif += "replace: currentTime\n"
2147 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2148 self.cache.modify_ldif(modify_ldif)
2149 self.cache.transaction_commit()
2150 except Exception as e:
2151 self.cache.transaction_cancel()
2157 res = self.samdb.search(expression=self.dirsync_filter,
2158 scope=ldb.SCOPE_SUBTREE,
2159 attrs=self.dirsync_attrs,
2160 controls=self.dirsync_controls)
2161 log_msg("dirsync_loop(): results %d\n" % len(res))
2164 done = check_object(r, res.controls)
2166 handle_object(ri, r)
2167 update_object(r, res.controls)
2169 update_cache(res.controls)
2173 def sync_loop(wait):
2174 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2175 notify_controls = ["notification:1", "show_recycled:1"]
2176 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2177 scope=ldb.SCOPE_SUBTREE,
2179 controls=notify_controls,
2183 log_msg("Resuming monitoring\n")
2185 log_msg("Getting changes\n")
2186 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2187 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2188 self.outf.write("syncCommand: %s\n" % self.sync_command)
2191 if wait is not True:
2194 for msg in notify_handle:
2195 if not isinstance(msg, ldb.Message):
2196 self.outf.write("referal: %s\n" % msg)
2198 created = msg.get("uSNCreated")[0]
2199 changed = msg.get("uSNChanged")[0]
2200 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2201 (msg.dn, created, changed))
2205 res = notify_handle.result()
2210 orig_pid = os.getpid()
2215 if pid == 0: # Actual daemon
2217 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2222 if cache_ldb_initialize:
2224 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2229 if logfile is not None:
2230 import resource # Resource usage information.
2231 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2232 if maxfd == resource.RLIM_INFINITY:
2233 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2234 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2235 self.outf.write("Using logfile[%s]\n" % logfile)
2236 for fd in range(0, maxfd):
2247 log_msg("Attached to logfile[%s]\n" % (logfile))
2248 self.logfile = logfile
2251 conflict = check_current_pid_conflict(terminate)
2253 if self.current_pid is None:
2254 log_msg("No process running.\n")
2257 log_msg("Proccess %d is not running anymore.\n" % (
2261 log_msg("Sending SIGTERM to proccess %d.\n" % (
2263 os.kill(self.current_pid, signal.SIGTERM)
2266 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2267 os.getpid(), self.current_pid))
2271 update_pid(os.getpid())
2276 retry_sleep_max = 600
2281 retry_sleep = retry_sleep_min
2283 while self.samdb is None:
2284 if retry_sleep != 0:
2285 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2286 time.sleep(retry_sleep)
2287 retry_sleep = retry_sleep * 2
2288 if retry_sleep >= retry_sleep_max:
2289 retry_sleep = retry_sleep_max
2290 log_msg("Connecting to '%s'\n" % self.samdb_url)
2292 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2293 except Exception as msg:
2295 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2296 if wait is not True:
2301 except ldb.LdbError as e7:
2302 (enum, estr) = e7.args
2304 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2310 class cmd_user_edit(Command):
2311 """Modify User AD object.
2313 This command will allow editing of a user account in the Active Directory
2314 domain. You will then be able to add or change attributes and their values.
2316 The username specified on the command is the sAMAccountName.
2318 The command may be run from the root userid or another authorized userid.
2320 The -H or --URL= option can be used to execute the command against a remote
2324 samba-tool user edit User1 -H ldap://samba.samdom.example.com \
2325 -U administrator --password=passw1rd
2327 Example1 shows how to edit a users attributes in the domain against a remote
2330 The -H parameter is used to specify the remote target server.
2333 samba-tool user edit User2
2335 Example2 shows how to edit a users attributes in the domain against a local
2339 samba-tool user edit User3 --editor=nano
2341 Example3 shows how to edit a users attributes in the domain against a local
2342 LDAP server using the 'nano' editor.
2345 synopsis = "%prog <username> [options]"
2348 Option("-H", "--URL", help="LDB URL for database or target server",
2349 type=str, metavar="URL", dest="H"),
2350 Option("--editor", help="Editor to use instead of the system default,"
2351 " or 'vi' if no system default is set.", type=str),
2354 takes_args = ["username"]
2355 takes_optiongroups = {
2356 "sambaopts": options.SambaOptions,
2357 "credopts": options.CredentialsOptions,
2358 "versionopts": options.VersionOptions,
2361 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2362 H=None, editor=None):
2364 lp = sambaopts.get_loadparm()
2365 creds = credopts.get_credentials(lp, fallback_machine=True)
2366 samdb = SamDB(url=H, session_info=system_session(),
2367 credentials=creds, lp=lp)
2369 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2370 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2372 domaindn = samdb.domain_dn()
2375 res = samdb.search(base=domaindn,
2377 scope=ldb.SCOPE_SUBTREE)
2380 raise CommandError('Unable to find user "%s"' % (username))
2383 r_ldif = samdb.write_ldif(msg, 1)
2384 # remove 'changetype' line
2385 result_ldif = re.sub('changetype: add\n', '', r_ldif)
2388 editor = os.environ.get('EDITOR')
2392 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2393 t_file.write(result_ldif)
2396 check_call([editor, t_file.name])
2397 except CalledProcessError as e:
2398 raise CalledProcessError("ERROR: ", e)
2399 with open(t_file.name) as edited_file:
2400 edited_message = edited_file.read()
2402 if result_ldif != edited_message:
2403 diff = difflib.ndiff(result_ldif.splitlines(),
2404 edited_message.splitlines())
2408 if line.startswith('-'):
2410 minus_lines.append(line)
2411 elif line.startswith('+'):
2413 plus_lines.append(line)
2415 user_ldif = "dn: %s\n" % user_dn
2416 user_ldif += "changetype: modify\n"
2418 for line in minus_lines:
2419 attr, val = line.split(':', 1)
2420 search_attr = "%s:" % attr
2421 if not re.search(r'^' + search_attr, str(plus_lines)):
2422 user_ldif += "delete: %s\n" % attr
2423 user_ldif += "%s: %s\n" % (attr, val)
2425 for line in plus_lines:
2426 attr, val = line.split(':', 1)
2427 search_attr = "%s:" % attr
2428 if re.search(r'^' + search_attr, str(minus_lines)):
2429 user_ldif += "replace: %s\n" % attr
2430 user_ldif += "%s: %s\n" % (attr, val)
2431 if not re.search(r'^' + search_attr, str(minus_lines)):
2432 user_ldif += "add: %s\n" % attr
2433 user_ldif += "%s: %s\n" % (attr, val)
2436 samdb.modify_ldif(user_ldif)
2437 except Exception as e:
2438 raise CommandError("Failed to modify user '%s': " %
2441 self.outf.write("Modified User '%s' successfully\n" % username)
2444 class cmd_user_show(Command):
2445 """Display a user AD object.
2447 This command displays a user account and it's attributes in the Active
2449 The username specified on the command is the sAMAccountName.
2451 The command may be run from the root userid or another authorized userid.
2453 The -H or --URL= option can be used to execute the command against a remote
2457 samba-tool user show User1 -H ldap://samba.samdom.example.com \
2458 -U administrator --password=passw1rd
2460 Example1 shows how to display a users attributes in the domain against a remote
2463 The -H parameter is used to specify the remote target server.
2466 samba-tool user show User2
2468 Example2 shows how to display a users attributes in the domain against a local
2472 samba-tool user show User2 --attributes=objectSid,memberOf
2474 Example3 shows how to display a users objectSid and memberOf attributes.
2476 synopsis = "%prog <username> [options]"
2479 Option("-H", "--URL", help="LDB URL for database or target server",
2480 type=str, metavar="URL", dest="H"),
2481 Option("--attributes",
2482 help=("Comma separated list of attributes, "
2483 "which will be printed."),
2484 type=str, dest="user_attrs"),
2487 takes_args = ["username"]
2488 takes_optiongroups = {
2489 "sambaopts": options.SambaOptions,
2490 "credopts": options.CredentialsOptions,
2491 "versionopts": options.VersionOptions,
2494 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2495 H=None, user_attrs=None):
2497 lp = sambaopts.get_loadparm()
2498 creds = credopts.get_credentials(lp, fallback_machine=True)
2499 samdb = SamDB(url=H, session_info=system_session(),
2500 credentials=creds, lp=lp)
2504 attrs = user_attrs.split(",")
2506 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2507 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2509 domaindn = samdb.domain_dn()
2512 res = samdb.search(base=domaindn, expression=filter,
2513 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2516 raise CommandError('Unable to find user "%s"' % (username))
2519 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2520 self.outf.write(user_ldif)
2523 class cmd_user_move(Command):
2524 """Move a user to an organizational unit/container.
2526 This command moves a user account into the specified organizational unit
2528 The username specified on the command is the sAMAccountName.
2529 The name of the organizational unit or container can be specified as a
2530 full DN or without the domainDN component.
2532 The command may be run from the root userid or another authorized userid.
2534 The -H or --URL= option can be used to execute the command against a remote
2538 samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
2539 -H ldap://samba.samdom.example.com -U administrator
2541 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2542 unit on a remote LDAP server.
2544 The -H parameter is used to specify the remote target server.
2547 samba-tool user move User1 CN=Users
2549 Example2 shows how to move a user User1 back into the CN=Users container
2550 on the local server.
2553 synopsis = "%prog <username> <new_parent_dn> [options]"
2556 Option("-H", "--URL", help="LDB URL for database or target server",
2557 type=str, metavar="URL", dest="H"),
2560 takes_args = ["username", "new_parent_dn"]
2561 takes_optiongroups = {
2562 "sambaopts": options.SambaOptions,
2563 "credopts": options.CredentialsOptions,
2564 "versionopts": options.VersionOptions,
2567 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2568 versionopts=None, H=None):
2569 lp = sambaopts.get_loadparm()
2570 creds = credopts.get_credentials(lp, fallback_machine=True)
2571 samdb = SamDB(url=H, session_info=system_session(),
2572 credentials=creds, lp=lp)
2573 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2575 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2576 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2578 res = samdb.search(base=domain_dn,
2580 scope=ldb.SCOPE_SUBTREE)
2583 raise CommandError('Unable to find user "%s"' % (username))
2586 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2587 except Exception as e:
2588 raise CommandError('Invalid new_parent_dn "%s": %s' %
2589 (new_parent_dn, e.message))
2591 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2592 full_new_user_dn.remove_base_components(len(user_dn) - 1)
2593 full_new_user_dn.add_base(full_new_parent_dn)
2596 samdb.rename(user_dn, full_new_user_dn)
2597 except Exception as e:
2598 raise CommandError('Failed to move user "%s"' % username, e)
2599 self.outf.write('Moved user "%s" into "%s"\n' %
2600 (username, full_new_parent_dn))
2603 class cmd_user(SuperCommand):
2604 """User management."""
2607 subcommands["add"] = cmd_user_add()
2608 subcommands["create"] = cmd_user_create()
2609 subcommands["delete"] = cmd_user_delete()
2610 subcommands["disable"] = cmd_user_disable()
2611 subcommands["enable"] = cmd_user_enable()
2612 subcommands["list"] = cmd_user_list()
2613 subcommands["setexpiry"] = cmd_user_setexpiry()
2614 subcommands["password"] = cmd_user_password()
2615 subcommands["setpassword"] = cmd_user_setpassword()
2616 subcommands["getpassword"] = cmd_user_getpassword()
2617 subcommands["syncpasswords"] = cmd_user_syncpasswords()
2618 subcommands["edit"] = cmd_user_edit()
2619 subcommands["show"] = cmd_user_show()
2620 subcommands["move"] = cmd_user_move()