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
58 from samba.compat import get_bytes
59 from samba.compat import get_string
62 # python[3]-gpgme is abandoned since ubuntu 1804 and debian 9
63 # have to use python[3]-gpg instead
64 # The API is different, need to adapt.
66 def _gpgme_decrypt(encrypted_bytes):
68 Use python[3]-gpgme to decrypt GPG.
71 ctx.armor = True # use ASCII-armored
73 ctx.decrypt(io.BytesIO(encrypted_bytes), out)
77 def _gpg_decrypt(encrypted_bytes):
79 Use python[3]-gpg to decrypt GPG.
81 ciphertext = gpg.Data(string=encrypted_bytes)
82 ctx = gpg.Context(armor=True)
83 # plaintext, result, verify_result
84 plaintext, _, _ = ctx.decrypt(ciphertext)
93 gpg_decrypt = _gpgme_decrypt
100 gpg_decrypt = _gpg_decrypt
105 decrypt_samba_gpg_help = ("Decrypt the SambaGPG password as "
108 decrypt_samba_gpg_help = ("Decrypt the SambaGPG password not supported, "
109 "python[3]-gpgme or python[3]-gpg required")
112 disabled_virtual_attributes = {
115 virtual_attributes = {
116 "virtualClearTextUTF8": {
117 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
119 "virtualClearTextUTF16": {
120 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
123 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
128 def get_crypt_value(alg, utf8pw, rounds=0):
134 salt = os.urandom(16)
135 # The salt needs to be in [A-Za-z0-9./]
136 # base64 is close enough and as we had 16
137 # random bytes but only need 16 characters
138 # we can ignore the possible == at the end
139 # of the base64 string
140 # we just need to replace '+' by '.'
141 b64salt = base64.b64encode(salt)[0:16].replace(b'+', b'.').decode('utf8')
144 crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
146 crypt_salt = "$%s$%s$" % (alg, b64salt)
148 crypt_value = crypt.crypt(utf8pw, crypt_salt)
149 if crypt_value is None:
150 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
151 expected_len = len(crypt_salt) + algs[alg]["length"]
152 if len(crypt_value) != expected_len:
153 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
154 crypt_salt, len(crypt_value), expected_len))
157 # Extract the rounds value from the options of a virtualCrypt attribute
158 # i.e. options = "rounds=20;other=ignored;" will return 20
159 # if the rounds option is not found or the value is not a number, 0 is returned
160 # which indicates that the default number of rounds should be used.
163 def get_rounds(options):
167 opts = options.split(';')
169 if o.lower().startswith("rounds="):
170 (key, _, val) = o.partition('=')
182 virtual_attributes["virtualSSHA"] = {
184 except ImportError as e:
185 reason = "hashlib.sha1()"
186 reason += " required"
187 disabled_virtual_attributes["virtualSSHA"] = {
191 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
194 v = get_crypt_value(alg, "")
196 virtual_attributes[attr] = {
198 except ImportError as e:
200 reason += " required"
201 disabled_virtual_attributes[attr] = {
204 except NotImplementedError as e:
205 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
206 disabled_virtual_attributes[attr] = {
210 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
211 for x in range(1, 30):
212 virtual_attributes["virtualWDigest%02d" % x] = {}
214 # Add Kerberos virtual attributes
215 virtual_attributes["virtualKerberosSalt"] = {}
217 virtual_attributes_help = "The attributes to display (comma separated). "
218 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
219 if len(disabled_virtual_attributes) != 0:
220 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
223 class cmd_user_create(Command):
224 """Create a new user.
226 This command creates a new user account in the Active Directory domain. The username specified on the command is the sAMaccountName.
228 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).
230 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.
232 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.
234 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.
237 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
239 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.
242 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
244 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.
247 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
249 Example3 shows how to create a new user in the OrgUnit organizational unit.
252 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
254 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'.
257 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \\
258 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
260 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
261 --nis-domain is set, then the other four parameters are mandatory.
264 synopsis = "%prog <username> [<password>] [options]"
267 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
268 metavar="URL", dest="H"),
269 Option("--must-change-at-next-login",
270 help="Force password to be changed on next login",
271 action="store_true"),
272 Option("--random-password",
273 help="Generate random password",
274 action="store_true"),
275 Option("--smartcard-required",
276 help="Require a smartcard for interactive logons",
277 action="store_true"),
278 Option("--use-username-as-cn",
279 help="Force use of username as user's CN",
280 action="store_true"),
282 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>'",
284 Option("--surname", help="User's surname", type=str),
285 Option("--given-name", help="User's given name", type=str),
286 Option("--initials", help="User's initials", type=str),
287 Option("--profile-path", help="User's profile path", type=str),
288 Option("--script-path", help="User's logon script path", type=str),
289 Option("--home-drive", help="User's home drive letter", type=str),
290 Option("--home-directory", help="User's home directory path", type=str),
291 Option("--job-title", help="User's job title", type=str),
292 Option("--department", help="User's department", type=str),
293 Option("--company", help="User's company", type=str),
294 Option("--description", help="User's description", type=str),
295 Option("--mail-address", help="User's email address", type=str),
296 Option("--internet-address", help="User's home page", type=str),
297 Option("--telephone-number", help="User's phone number", type=str),
298 Option("--physical-delivery-office", help="User's office location", type=str),
299 Option("--rfc2307-from-nss",
300 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
301 action="store_true"),
302 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
303 Option("--unix-home", help="User's Unix/RFC2307 home directory",
305 Option("--uid", help="User's Unix/RFC2307 username", type=str),
306 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
307 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
308 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
309 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
312 takes_args = ["username", "password?"]
314 takes_optiongroups = {
315 "sambaopts": options.SambaOptions,
316 "credopts": options.CredentialsOptions,
317 "versionopts": options.VersionOptions,
320 def run(self, username, password=None, credopts=None, sambaopts=None,
321 versionopts=None, H=None, must_change_at_next_login=False,
322 random_password=False, use_username_as_cn=False, userou=None,
323 surname=None, given_name=None, initials=None, profile_path=None,
324 script_path=None, home_drive=None, home_directory=None,
325 job_title=None, department=None, company=None, description=None,
326 mail_address=None, internet_address=None, telephone_number=None,
327 physical_delivery_office=None, rfc2307_from_nss=False,
328 nis_domain=None, unix_home=None, uid=None, uid_number=None,
329 gid_number=None, gecos=None, login_shell=None,
330 smartcard_required=False):
332 if smartcard_required:
333 if password is not None and password != '':
334 raise CommandError('It is not allowed to specify '
336 'together with --smartcard-required.')
337 if must_change_at_next_login:
338 raise CommandError('It is not allowed to specify '
339 '--must-change-at-next-login '
340 'together with --smartcard-required.')
342 if random_password and not smartcard_required:
343 password = generate_random_password(128, 255)
346 if smartcard_required:
348 if password is not None and password != '':
350 password = getpass("New Password: ")
351 passwordverify = getpass("Retype Password: ")
352 if not password == passwordverify:
354 self.outf.write("Sorry, passwords do not match.\n")
357 pwent = pwd.getpwnam(username)
360 if uid_number is None:
361 uid_number = pwent[2]
362 if gid_number is None:
363 gid_number = pwent[3]
366 if login_shell is None:
367 login_shell = pwent[6]
369 lp = sambaopts.get_loadparm()
370 creds = credopts.get_credentials(lp)
372 if uid_number or gid_number:
373 if not lp.get("idmap_ldb:use rfc2307"):
374 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")
376 if nis_domain is not None:
377 if None in (uid_number, login_shell, unix_home, gid_number):
378 raise CommandError('Missing parameters. To enable NIS features, '
379 'the following options have to be given: '
380 '--nis-domain=, --uidNumber=, --login-shell='
381 ', --unix-home=, --gid-number= Operation '
385 samdb = SamDB(url=H, session_info=system_session(),
386 credentials=creds, lp=lp)
387 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
388 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
389 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
390 jobtitle=job_title, department=department, company=company, description=description,
391 mailaddress=mail_address, internetaddress=internet_address,
392 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
393 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
394 uidnumber=uid_number, gidnumber=gid_number,
395 gecos=gecos, loginshell=login_shell,
396 smartcard_required=smartcard_required)
397 except Exception as e:
398 raise CommandError("Failed to add user '%s': " % username, e)
400 self.outf.write("User '%s' created successfully\n" % username)
403 class cmd_user_add(cmd_user_create):
404 __doc__ = cmd_user_create.__doc__
405 # take this print out after the add subcommand is removed.
406 # the add subcommand is deprecated but left in for now to allow people to
409 def run(self, *args, **kwargs):
411 "Note: samba-tool user add is deprecated. "
412 "Please use samba-tool user create for the same function.\n")
413 return super(cmd_user_add, self).run(*args, **kwargs)
416 class cmd_user_delete(Command):
419 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
421 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.
423 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.
426 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
428 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.
431 sudo samba-tool user delete User2
433 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.
436 synopsis = "%prog <username> [options]"
439 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
440 metavar="URL", dest="H"),
443 takes_args = ["username"]
444 takes_optiongroups = {
445 "sambaopts": options.SambaOptions,
446 "credopts": options.CredentialsOptions,
447 "versionopts": options.VersionOptions,
450 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
452 lp = sambaopts.get_loadparm()
453 creds = credopts.get_credentials(lp, fallback_machine=True)
455 samdb = SamDB(url=H, session_info=system_session(),
456 credentials=creds, lp=lp)
458 filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
459 ldb.binary_encode(username))
462 res = samdb.search(base=samdb.domain_dn(),
463 scope=ldb.SCOPE_SUBTREE,
468 raise CommandError('Unable to find user "%s"' % (username))
471 samdb.delete(user_dn)
472 except Exception as e:
473 raise CommandError('Failed to remove user "%s"' % username, e)
474 self.outf.write("Deleted user %s\n" % username)
477 class cmd_user_list(Command):
478 """List all users."""
480 synopsis = "%prog [options]"
483 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
484 metavar="URL", dest="H"),
487 takes_optiongroups = {
488 "sambaopts": options.SambaOptions,
489 "credopts": options.CredentialsOptions,
490 "versionopts": options.VersionOptions,
493 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
494 lp = sambaopts.get_loadparm()
495 creds = credopts.get_credentials(lp, fallback_machine=True)
497 samdb = SamDB(url=H, session_info=system_session(),
498 credentials=creds, lp=lp)
500 domain_dn = samdb.domain_dn()
501 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
502 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
503 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
504 attrs=["samaccountname"])
509 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
512 class cmd_user_enable(Command):
515 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.
517 There are many reasons why an account may become disabled. These include:
518 - If a user exceeds the account policy for logon attempts
519 - If an administrator disables the account
520 - If the account expires
522 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
524 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.
526 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.
529 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
531 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.
534 su samba-tool user enable Testuser2
536 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.
539 samba-tool user enable --filter=samaccountname=Testuser3
541 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
544 synopsis = "%prog (<username>|--filter <filter>) [options]"
546 takes_optiongroups = {
547 "sambaopts": options.SambaOptions,
548 "versionopts": options.VersionOptions,
549 "credopts": options.CredentialsOptions,
553 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
554 metavar="URL", dest="H"),
555 Option("--filter", help="LDAP Filter to set password on", type=str),
558 takes_args = ["username?"]
560 def run(self, username=None, sambaopts=None, credopts=None,
561 versionopts=None, filter=None, H=None):
562 if username is None and filter is None:
563 raise CommandError("Either the username or '--filter' must be specified!")
566 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
568 lp = sambaopts.get_loadparm()
569 creds = credopts.get_credentials(lp, fallback_machine=True)
571 samdb = SamDB(url=H, session_info=system_session(),
572 credentials=creds, lp=lp)
574 samdb.enable_account(filter)
575 except Exception as msg:
576 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
577 self.outf.write("Enabled user '%s'\n" % (username or filter))
580 class cmd_user_disable(Command):
581 """Disable a user."""
583 synopsis = "%prog (<username>|--filter <filter>) [options]"
586 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
587 metavar="URL", dest="H"),
588 Option("--filter", help="LDAP Filter to set password on", type=str),
591 takes_args = ["username?"]
593 takes_optiongroups = {
594 "sambaopts": options.SambaOptions,
595 "credopts": options.CredentialsOptions,
596 "versionopts": options.VersionOptions,
599 def run(self, username=None, sambaopts=None, credopts=None,
600 versionopts=None, filter=None, H=None):
601 if username is None and filter is None:
602 raise CommandError("Either the username or '--filter' must be specified!")
605 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
607 lp = sambaopts.get_loadparm()
608 creds = credopts.get_credentials(lp, fallback_machine=True)
610 samdb = SamDB(url=H, session_info=system_session(),
611 credentials=creds, lp=lp)
613 samdb.disable_account(filter)
614 except Exception as msg:
615 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
618 class cmd_user_setexpiry(Command):
619 """Set the expiration of a user account.
621 The user can either be specified by their sAMAccountName or using the --filter option.
623 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.
625 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.
628 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
630 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.
633 sudo samba-tool user setexpiry User2 --noexpiry
635 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.
638 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
640 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.
643 samba-tool user setexpiry --noexpiry User4
644 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
647 synopsis = "%prog (<username>|--filter <filter>) [options]"
649 takes_optiongroups = {
650 "sambaopts": options.SambaOptions,
651 "versionopts": options.VersionOptions,
652 "credopts": options.CredentialsOptions,
656 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
657 metavar="URL", dest="H"),
658 Option("--filter", help="LDAP Filter to set password on", type=str),
659 Option("--days", help="Days to expiry", type=int, default=0),
660 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
663 takes_args = ["username?"]
665 def run(self, username=None, sambaopts=None, credopts=None,
666 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
667 if username is None and filter is None:
668 raise CommandError("Either the username or '--filter' must be specified!")
671 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
673 lp = sambaopts.get_loadparm()
674 creds = credopts.get_credentials(lp)
676 samdb = SamDB(url=H, session_info=system_session(),
677 credentials=creds, lp=lp)
680 samdb.setexpiry(filter, days * 24 * 3600, no_expiry_req=noexpiry)
681 except Exception as msg:
682 # FIXME: Catch more specific exception
683 raise CommandError("Failed to set expiry for user '%s': %s" % (
684 username or filter, msg))
686 self.outf.write("Expiry for user '%s' disabled.\n" % (
689 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
690 username or filter, days))
693 class cmd_user_password(Command):
694 """Change password for a user account (the one provided in authentication).
697 synopsis = "%prog [options]"
700 Option("--newpassword", help="New password", type=str),
703 takes_optiongroups = {
704 "sambaopts": options.SambaOptions,
705 "credopts": options.CredentialsOptions,
706 "versionopts": options.VersionOptions,
709 def run(self, credopts=None, sambaopts=None, versionopts=None,
712 lp = sambaopts.get_loadparm()
713 creds = credopts.get_credentials(lp)
715 # get old password now, to get the password prompts in the right order
716 old_password = creds.get_password()
718 net = Net(creds, lp, server=credopts.ipaddress)
720 password = newpassword
722 if password is not None and password != '':
724 password = getpass("New Password: ")
725 passwordverify = getpass("Retype Password: ")
726 if not password == passwordverify:
728 self.outf.write("Sorry, passwords do not match.\n")
731 if not isinstance(password, text_type):
732 password = password.decode('utf8')
733 net.change_password(password)
734 except Exception as msg:
735 # FIXME: catch more specific exception
736 raise CommandError("Failed to change password : %s" % msg)
737 self.outf.write("Changed password OK\n")
740 class cmd_user_setpassword(Command):
741 """Set or reset the password of a user account.
743 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.
745 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.
747 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.
749 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.
752 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
754 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.
757 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
759 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.
762 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
764 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
767 synopsis = "%prog (<username>|--filter <filter>) [options]"
769 takes_optiongroups = {
770 "sambaopts": options.SambaOptions,
771 "versionopts": options.VersionOptions,
772 "credopts": options.CredentialsOptions,
776 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
777 metavar="URL", dest="H"),
778 Option("--filter", help="LDAP Filter to set password on", type=str),
779 Option("--newpassword", help="Set password", type=str),
780 Option("--must-change-at-next-login",
781 help="Force password to be changed on next login",
782 action="store_true"),
783 Option("--random-password",
784 help="Generate random password",
785 action="store_true"),
786 Option("--smartcard-required",
787 help="Require a smartcard for interactive logons",
788 action="store_true"),
789 Option("--clear-smartcard-required",
790 help="Don't require a smartcard for interactive logons",
791 action="store_true"),
794 takes_args = ["username?"]
796 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
797 versionopts=None, H=None, newpassword=None,
798 must_change_at_next_login=False, random_password=False,
799 smartcard_required=False, clear_smartcard_required=False):
800 if filter is None and username is None:
801 raise CommandError("Either the username or '--filter' must be specified!")
803 password = newpassword
805 if smartcard_required:
806 if password is not None and password != '':
807 raise CommandError('It is not allowed to specify '
809 'together with --smartcard-required.')
810 if must_change_at_next_login:
811 raise CommandError('It is not allowed to specify '
812 '--must-change-at-next-login '
813 'together with --smartcard-required.')
814 if clear_smartcard_required:
815 raise CommandError('It is not allowed to specify '
816 '--clear-smartcard-required '
817 'together with --smartcard-required.')
819 if random_password and not smartcard_required:
820 password = generate_random_password(128, 255)
823 if smartcard_required:
825 if password is not None and password != '':
827 password = getpass("New Password: ")
828 passwordverify = getpass("Retype Password: ")
829 if not password == passwordverify:
831 self.outf.write("Sorry, passwords do not match.\n")
834 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
836 lp = sambaopts.get_loadparm()
837 creds = credopts.get_credentials(lp)
839 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
841 samdb = SamDB(url=H, session_info=system_session(),
842 credentials=creds, lp=lp)
844 if smartcard_required:
847 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
848 flags = dsdb.UF_SMARTCARD_REQUIRED
849 samdb.toggle_userAccountFlags(filter, flags, on=True)
850 command = "Failed to enable account for user '%s'" % (username or filter)
851 samdb.enable_account(filter)
852 except Exception as msg:
853 # FIXME: catch more specific exception
854 raise CommandError("%s: %s" % (command, msg))
855 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
859 if clear_smartcard_required:
860 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
861 flags = dsdb.UF_SMARTCARD_REQUIRED
862 samdb.toggle_userAccountFlags(filter, flags, on=False)
863 command = "Failed to set password for user '%s'" % (username or filter)
864 samdb.setpassword(filter, password,
865 force_change_at_next_login=must_change_at_next_login,
867 except Exception as msg:
868 # FIXME: catch more specific exception
869 raise CommandError("%s: %s" % (command, msg))
870 self.outf.write("Changed password OK\n")
873 class GetPasswordCommand(Command):
876 super(GetPasswordCommand, self).__init__()
879 def connect_system_samdb(self, url, allow_local=False, verbose=False):
881 # using anonymous here, results in no authentication
882 # which means we can get system privileges via
883 # the privileged ldapi socket
884 creds = credentials.Credentials()
885 creds.set_anonymous()
887 if url is None and allow_local:
889 elif url.lower().startswith("ldapi://"):
891 elif url.lower().startswith("ldap://"):
892 raise CommandError("--url ldap:// is not supported for this command")
893 elif url.lower().startswith("ldaps://"):
894 raise CommandError("--url ldaps:// is not supported for this command")
895 elif not allow_local:
896 raise CommandError("--url requires an ldapi:// url for this command")
899 self.outf.write("Connecting to '%s'\n" % url)
901 samdb = SamDB(url=url, session_info=system_session(),
902 credentials=creds, lp=self.lp)
906 # Make sure we're connected as SYSTEM
908 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
910 sids = res[0].get("tokenGroups")
911 assert len(sids) == 1
912 sid = ndr_unpack(security.dom_sid, sids[0])
913 assert str(sid) == security.SID_NT_SYSTEM
914 except Exception as msg:
915 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
916 (security.SID_NT_SYSTEM))
918 # We use sort here in order to have a predictable processing order
919 # this might not be strictly needed, but also doesn't hurt here
920 for a in sorted(virtual_attributes.keys()):
921 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
922 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
926 def get_account_attributes(self, samdb, username, basedn, filter, scope,
933 (attr, _, opts) = a.partition(';')
935 attr_opts[attr] = opts
937 attr_opts[attr] = None
938 search_attrs.append(attr)
939 lower_attrs = [x.lower() for x in search_attrs]
941 require_supplementalCredentials = False
942 for a in virtual_attributes.keys():
943 if a.lower() in lower_attrs:
944 require_supplementalCredentials = True
945 add_supplementalCredentials = False
946 add_unicodePwd = False
947 if require_supplementalCredentials:
948 a = "supplementalCredentials"
949 if a.lower() not in lower_attrs:
951 add_supplementalCredentials = True
953 if a.lower() not in lower_attrs:
955 add_unicodePwd = True
956 add_sAMAcountName = False
958 if a.lower() not in lower_attrs:
960 add_sAMAcountName = True
962 add_userPrincipalName = False
963 upn = "usePrincipalName"
964 if upn.lower() not in lower_attrs:
965 search_attrs += [upn]
966 add_userPrincipalName = True
968 if scope == ldb.SCOPE_BASE:
969 search_controls = ["show_deleted:1", "show_recycled:1"]
973 res = samdb.search(base=basedn, expression=filter,
974 scope=scope, attrs=search_attrs,
975 controls=search_controls)
977 raise Exception('Unable to find user "%s"' % (username or filter))
979 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
980 except Exception as msg:
981 # FIXME: catch more specific exception
982 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
987 if "supplementalCredentials" in obj:
988 sc_blob = obj["supplementalCredentials"][0]
989 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
990 if add_supplementalCredentials:
991 del obj["supplementalCredentials"]
992 if "unicodePwd" in obj:
993 unicodePwd = obj["unicodePwd"][0]
995 del obj["unicodePwd"]
996 account_name = str(obj["sAMAccountName"][0])
997 if add_sAMAcountName:
998 del obj["sAMAccountName"]
999 if "userPrincipalName" in obj:
1000 account_upn = str(obj["userPrincipalName"][0])
1002 realm = self.lp.get("realm")
1003 account_upn = "%s@%s" % (account_name, realm.lower())
1004 if add_userPrincipalName:
1005 del obj["userPrincipalName"]
1009 def get_package(name, min_idx=0):
1010 if name in calculated:
1011 return calculated[name]
1015 min_idx = len(sc.sub.packages) + min_idx
1017 for p in sc.sub.packages:
1024 return binascii.a2b_hex(p.data)
1029 # Samba adds 'Primary:SambaGPG' at the end.
1030 # When Windows sets the password it keeps
1031 # 'Primary:SambaGPG' and rotates it to
1032 # the begining. So we can only use the value,
1033 # if it is the last one.
1035 # In order to get more protection we verify
1036 # the nthash of the decrypted utf16 password
1037 # against the stored nthash in unicodePwd.
1039 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1040 if sgv is not None and unicodePwd is not None:
1042 cv = gpg_decrypt(sgv)
1044 # We only use the password if it matches
1045 # the current nthash stored in the unicodePwd
1048 tmp = credentials.Credentials()
1050 tmp.set_utf16_password(cv)
1051 nthash = tmp.get_nt_hash()
1052 if nthash == unicodePwd:
1053 calculated["Primary:CLEARTEXT"] = cv
1055 except Exception as e:
1057 "WARNING: '%s': SambaGPG can't be decrypted "
1058 "into CLEARTEXT: %s\n" % (
1059 username or account_name, e))
1062 def get_utf8(a, b, username):
1064 u = text_type(get_bytes(b), 'utf-16-le')
1065 except UnicodeDecodeError as e:
1066 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1069 u8 = u.encode('utf-8')
1072 # Extract the WDigest hash for the value specified by i.
1073 # Builds an htdigest compatible value
1076 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1077 domain, dns_domain):
1082 user = account_name.lower()
1083 realm = domain.lower()
1085 user = account_name.upper()
1086 realm = domain.upper()
1089 realm = domain.upper()
1092 realm = domain.lower()
1094 user = account_name.upper()
1095 realm = domain.lower()
1097 user = account_name.lower()
1098 realm = domain.upper()
1101 realm = dns_domain.lower()
1103 user = account_name.lower()
1104 realm = dns_domain.lower()
1106 user = account_name.upper()
1107 realm = dns_domain.upper()
1110 realm = dns_domain.upper()
1113 realm = dns_domain.lower()
1115 user = account_name.upper()
1116 realm = dns_domain.lower()
1118 user = account_name.lower()
1119 realm = dns_domain.upper()
1124 user = account_upn.lower()
1127 user = account_upn.upper()
1130 user = "%s\\%s" % (domain, account_name)
1133 user = "%s\\%s" % (domain.lower(), account_name.lower())
1136 user = "%s\\%s" % (domain.upper(), account_name.upper())
1142 user = account_name.lower()
1145 user = account_name.upper()
1151 user = account_upn.lower()
1154 user = account_upn.upper()
1157 user = "%s\\%s" % (domain, account_name)
1160 # Differs from spec, see tests
1161 user = "%s\\%s" % (domain.lower(), account_name.lower())
1164 # Differs from spec, see tests
1165 user = "%s\\%s" % (domain.upper(), account_name.upper())
1170 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1173 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
1174 return "%s:%s:%s" % (user, realm, get_string(digest))
1178 # get the value for a virtualCrypt attribute.
1179 # look for an exact match on algorithm and rounds in supplemental creds
1180 # if not found calculate using Primary:CLEARTEXT
1181 # if no Primary:CLEARTEXT return the first supplementalCredential
1182 # that matches the algorithm.
1183 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1186 b = get_package("Primary:userPassword")
1188 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1190 # No exact match on algorithm and number of rounds
1191 # try and calculate one from the Primary:CLEARTEXT
1192 b = get_package("Primary:CLEARTEXT")
1194 u8 = get_utf8(a, b, username or account_name)
1196 # in py2 using get_bytes should ensure u8 is unmodified
1197 # in py3 it will be decoded
1198 sv = get_crypt_value(str(algorithm), get_string(u8), rounds)
1200 # Unable to calculate a hash with the specified
1201 # number of rounds, fall back to the first hash using
1202 # the specified algorithm
1206 return "{CRYPT}" + sv
1208 def get_userPassword_hash(blob, algorithm, rounds):
1209 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1212 # Check that the NT hash has not been changed without updating
1213 # the user password hashes. This indicates that password has been
1214 # changed without updating the supplemental credentials.
1215 if unicodePwd != bytearray(up.current_nt_hash.hash):
1218 scheme_prefix = "$%d$" % algorithm
1219 prefix = scheme_prefix
1221 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1225 # in PY2 this should just do nothing and in PY3 if bytes
1226 # it will decode them
1227 h_value = get_string(h.value)
1228 if (scheme_match is None and
1229 h.scheme == SCHEME and
1230 h_value.startswith(scheme_prefix)):
1231 scheme_match = h_value
1232 if h.scheme == SCHEME and h_value.startswith(prefix):
1233 return (h_value, scheme_match)
1235 # No match on the number of rounds, return the value of the
1236 # first matching scheme
1237 return (None, scheme_match)
1239 def get_kerberos_ctr():
1240 primary_krb5 = get_package("Primary:Kerberos-Newer-Keys")
1241 if primary_krb5 is None:
1242 primary_krb5 = get_package("Primary:Kerberos")
1243 if primary_krb5 is None:
1245 krb5_blob = ndr_unpack(drsblobs.package_PrimaryKerberosBlob,
1247 return (krb5_blob.version, krb5_blob.ctr)
1249 # We use sort here in order to have a predictable processing order
1250 for a in sorted(virtual_attributes.keys()):
1251 if not a.lower() in lower_attrs:
1254 if a == "virtualClearTextUTF8":
1255 b = get_package("Primary:CLEARTEXT")
1258 u8 = get_utf8(a, b, username or account_name)
1262 elif a == "virtualClearTextUTF16":
1263 v = get_package("Primary:CLEARTEXT")
1266 elif a == "virtualSSHA":
1267 b = get_package("Primary:CLEARTEXT")
1270 u8 = get_utf8(a, b, username or account_name)
1273 salt = os.urandom(4)
1277 bv = h.digest() + salt
1278 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
1279 elif a == "virtualCryptSHA256":
1280 rounds = get_rounds(attr_opts[a])
1281 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1285 elif a == "virtualCryptSHA512":
1286 rounds = get_rounds(attr_opts[a])
1287 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1291 elif a == "virtualSambaGPG":
1292 # Samba adds 'Primary:SambaGPG' at the end.
1293 # When Windows sets the password it keeps
1294 # 'Primary:SambaGPG' and rotates it to
1295 # the begining. So we can only use the value,
1296 # if it is the last one.
1297 v = get_package("Primary:SambaGPG", min_idx=-1)
1300 elif a == "virtualKerberosSalt":
1301 (krb5_v, krb5_ctr) = get_kerberos_ctr()
1302 if krb5_v not in [3, 4]:
1304 v = krb5_ctr.salt.string
1305 elif a.startswith("virtualWDigest"):
1306 primary_wdigest = get_package("Primary:WDigest")
1307 if primary_wdigest is None:
1309 x = a[len("virtualWDigest"):]
1314 domain = self.lp.get("workgroup")
1315 dns_domain = samdb.domain_dns_name()
1316 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1321 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1324 def parse_attributes(self, attributes):
1326 if attributes is None:
1327 raise CommandError("Please specify --attributes")
1328 attrs = attributes.split(',')
1331 pa = pa.lstrip().rstrip()
1332 for da in disabled_virtual_attributes.keys():
1333 if pa.lower() == da.lower():
1334 r = disabled_virtual_attributes[da]["reason"]
1335 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1337 for va in virtual_attributes.keys():
1338 if pa.lower() == va.lower():
1339 # Take the real name
1342 password_attrs += [pa]
1344 return password_attrs
1347 class cmd_user_getpassword(GetPasswordCommand):
1348 """Get the password fields of a user/computer account.
1350 This command gets the logon password for a user/computer account.
1352 The username specified on the command is the sAMAccountName.
1353 The username may also be specified using the --filter option.
1355 The command must be run from the root user id or another authorized user id.
1356 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1357 used to adjust the local path. By default tdb:// is used by default.
1359 The '--attributes' parameter takes a comma separated list of attributes,
1360 which will be printed or given to the script specified by '--script'. If a
1361 specified attribute is not available on an object it's silently omitted.
1362 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1363 the NTHASH) and the following virtual attributes are possible (see --help
1364 for which virtual attributes are supported in your environment):
1366 virtualClearTextUTF16: The raw cleartext as stored in the
1367 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1368 with '--decrypt-samba-gpg') buffer inside of the
1369 supplementalCredentials attribute. This typically
1370 contains valid UTF-16-LE, but may contain random
1371 bytes, e.g. for computer accounts.
1373 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1374 (only from valid UTF-16-LE)
1376 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1377 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1379 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1380 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1381 with a $5$... salt, see crypt(3) on modern systems.
1382 The number of rounds used to calculate the hash can
1383 also be specified. By appending ";rounds=x" to the
1384 attribute name i.e. virtualCryptSHA256;rounds=10000
1385 will calculate a SHA256 hash with 10,000 rounds.
1386 non numeric values for rounds are silently ignored
1387 The value is calculated as follows:
1388 1) If a value exists in 'Primary:userPassword' with
1389 the specified number of rounds it is returned.
1390 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1391 '--decrypt-samba-gpg'. Calculate a hash with
1392 the specified number of rounds
1393 3) Return the first CryptSHA256 value in
1394 'Primary:userPassword'
1397 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1398 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1399 with a $6$... salt, see crypt(3) on modern systems.
1400 The number of rounds used to calculate the hash can
1401 also be specified. By appending ";rounds=x" to the
1402 attribute name i.e. virtualCryptSHA512;rounds=10000
1403 will calculate a SHA512 hash with 10,000 rounds.
1404 non numeric values for rounds are silently ignored
1405 The value is calculated as follows:
1406 1) If a value exists in 'Primary:userPassword' with
1407 the specified number of rounds it is returned.
1408 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1409 '--decrypt-samba-gpg'. Calculate a hash with
1410 the specified number of rounds
1411 3) Return the first CryptSHA512 value in
1412 'Primary:userPassword'
1414 virtualWDigestNN: The individual hash values stored in
1415 'Primary:WDigest' where NN is the hash number in
1417 NOTE: As at 22-05-2017 the documentation:
1418 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1419 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1422 virtualKerberosSalt: This results the salt string that is used to compute
1423 Kerberos keys from a UTF-8 cleartext password.
1425 virtualSambaGPG: The raw cleartext as stored in the
1426 'Primary:SambaGPG' buffer inside of the
1427 supplementalCredentials attribute.
1428 See the 'password hash gpg key ids' option in
1431 The '--decrypt-samba-gpg' option triggers decryption of the
1432 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1433 in your environment or not (the python-gpgme package is required). Please
1434 note that you might need to set the GNUPGHOME environment variable. If the
1435 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1436 environment variable has been set correctly and the passphrase is already
1437 known by the gpg-agent.
1440 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1443 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1447 super(cmd_user_getpassword, self).__init__()
1449 synopsis = "%prog (<username>|--filter <filter>) [options]"
1451 takes_optiongroups = {
1452 "sambaopts": options.SambaOptions,
1453 "versionopts": options.VersionOptions,
1457 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1458 metavar="URL", dest="H"),
1459 Option("--filter", help="LDAP Filter to set password on", type=str),
1460 Option("--attributes", type=str,
1461 help=virtual_attributes_help,
1462 metavar="ATTRIBUTELIST", dest="attributes"),
1463 Option("--decrypt-samba-gpg",
1464 help=decrypt_samba_gpg_help,
1465 action="store_true", default=False, dest="decrypt_samba_gpg"),
1468 takes_args = ["username?"]
1470 def run(self, username=None, H=None, filter=None,
1471 attributes=None, decrypt_samba_gpg=None,
1472 sambaopts=None, versionopts=None):
1473 self.lp = sambaopts.get_loadparm()
1475 if decrypt_samba_gpg and not gpg_decrypt:
1476 raise CommandError(decrypt_samba_gpg_help)
1478 if filter is None and username is None:
1479 raise CommandError("Either the username or '--filter' must be specified!")
1482 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1484 if attributes is None:
1485 raise CommandError("Please specify --attributes")
1487 password_attrs = self.parse_attributes(attributes)
1489 samdb = self.connect_system_samdb(url=H, allow_local=True)
1491 obj = self.get_account_attributes(samdb, username,
1494 scope=ldb.SCOPE_SUBTREE,
1495 attrs=password_attrs,
1496 decrypt=decrypt_samba_gpg)
1498 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1499 self.outf.write("%s" % ldif)
1500 self.outf.write("Got password OK\n")
1503 class cmd_user_syncpasswords(GetPasswordCommand):
1504 """Sync the password of user accounts.
1506 This syncs logon passwords for user accounts.
1508 Note that this command should run on a single domain controller only
1509 (typically the PDC-emulator). However the "password hash gpg key ids"
1510 option should to be configured on all domain controllers.
1512 The command must be run from the root user id or another authorized user id.
1513 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1514 local path. By default, ldapi:// is used with the default path to the
1515 privileged ldapi socket.
1517 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1518 "Sync Loop Terminate".
1521 Cache Initialization
1522 ====================
1524 The first time, this command needs to be called with
1525 '--cache-ldb-initialize' in order to initialize its cache.
1527 The cache initialization requires '--attributes' and allows the following
1528 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1531 The '--attributes' parameter takes a comma separated list of attributes,
1532 which will be printed or given to the script specified by '--script'. If a
1533 specified attribute is not available on an object it will be silently omitted.
1534 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1535 the NTHASH) and the following virtual attributes are possible (see '--help'
1536 for supported virtual attributes in your environment):
1538 virtualClearTextUTF16: The raw cleartext as stored in the
1539 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1540 with '--decrypt-samba-gpg') buffer inside of the
1541 supplementalCredentials attribute. This typically
1542 contains valid UTF-16-LE, but may contain random
1543 bytes, e.g. for computer accounts.
1545 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1546 (only from valid UTF-16-LE)
1548 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1549 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1551 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1552 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1553 with a $5$... salt, see crypt(3) on modern systems.
1554 The number of rounds used to calculate the hash can
1555 also be specified. By appending ";rounds=x" to the
1556 attribute name i.e. virtualCryptSHA256;rounds=10000
1557 will calculate a SHA256 hash with 10,000 rounds.
1558 non numeric values for rounds are silently ignored
1559 The value is calculated as follows:
1560 1) If a value exists in 'Primary:userPassword' with
1561 the specified number of rounds it is returned.
1562 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1563 '--decrypt-samba-gpg'. Calculate a hash with
1564 the specified number of rounds
1565 3) Return the first CryptSHA256 value in
1566 'Primary:userPassword'
1568 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1569 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1570 with a $6$... salt, see crypt(3) on modern systems.
1571 The number of rounds used to calculate the hash can
1572 also be specified. By appending ";rounds=x" to the
1573 attribute name i.e. virtualCryptSHA512;rounds=10000
1574 will calculate a SHA512 hash with 10,000 rounds.
1575 non numeric values for rounds are silently ignored
1576 The value is calculated as follows:
1577 1) If a value exists in 'Primary:userPassword' with
1578 the specified number of rounds it is returned.
1579 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1580 '--decrypt-samba-gpg'. Calculate a hash with
1581 the specified number of rounds
1582 3) Return the first CryptSHA512 value in
1583 'Primary:userPassword'
1585 virtualWDigestNN: The individual hash values stored in
1586 'Primary:WDigest' where NN is the hash number in
1588 NOTE: As at 22-05-2017 the documentation:
1589 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1590 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1593 virtualKerberosSalt: This results the salt string that is used to compute
1594 Kerberos keys from a UTF-8 cleartext password.
1596 virtualSambaGPG: The raw cleartext as stored in the
1597 'Primary:SambaGPG' buffer inside of the
1598 supplementalCredentials attribute.
1599 See the 'password hash gpg key ids' option in
1602 The '--decrypt-samba-gpg' option triggers decryption of the
1603 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1604 in your environment or not (the python-gpgme package is required). Please
1605 note that you might need to set the GNUPGHOME environment variable. If the
1606 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1607 environment variable has been set correctly and the passphrase is already
1608 known by the gpg-agent.
1610 The '--script' option specifies a custom script that is called whenever any
1611 of the dirsyncAttributes (see below) was changed. The script is called
1612 without any arguments. It gets the LDIF for exactly one object on STDIN.
1613 If the script processed the object successfully it has to respond with a
1614 single line starting with 'DONE-EXIT: ' followed by an optional message.
1616 Note that the script might be called without any password change, e.g. if
1617 the account was disabled (a userAccountControl change) or the
1618 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1619 are always returned as unique identifier of the account. It might be useful
1620 to also ask for non-password attributes like: objectSid, sAMAccountName,
1621 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1622 Depending on the object, some attributes may not be present/available,
1623 but you always get the current state (and not a diff).
1625 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1628 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1629 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1630 (!(sAMAccountName=krbtgt*)))
1631 This means only normal (non-krbtgt) user
1632 accounts are monitored. The '--filter' can modify that, e.g. if it's
1633 required to also sync computer accounts.
1639 This (default) mode runs in an endless loop waiting for password related
1640 changes in the active directory database. It makes use of the
1641 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1642 get changes in a reliable fashion. Objects are monitored for changes of the
1643 following dirsyncAttributes:
1645 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1646 userPrincipalName and userAccountControl.
1648 It recovers from LDAP disconnects and updates the cache in conservative way
1649 (in single steps after each successfully processed change). An error from
1650 the script (specified by '--script') will result in fatal error and this
1651 command will exit. But the cache state should be still valid and can be
1652 resumed in the next "Sync Loop Run".
1654 The '--logfile' option specifies an optional (required if '--daemon' is
1655 specified) logfile that takes all output of the command. The logfile is
1656 automatically reopened if fstat returns st_nlink == 0.
1658 The optional '--daemon' option will put the command into the background.
1660 You can stop the command without the '--daemon' option, also by hitting
1663 If you specify the '--no-wait' option the command skips the
1664 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1665 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1670 In order to terminate an already running command (likely as daemon) the
1671 '--terminate' option can be used. This also requires the '--logfile' option
1676 samba-tool user syncpasswords --cache-ldb-initialize \\
1677 --attributes=virtualClearTextUTF8
1678 samba-tool user syncpasswords
1681 samba-tool user syncpasswords --cache-ldb-initialize \\
1682 --attributes=objectGUID,objectSID,sAMAccountName,\\
1683 userPrincipalName,userAccountControl,pwdLastSet,\\
1684 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1685 --script=/path/to/my-custom-syncpasswords-script.py
1686 samba-tool user syncpasswords --daemon \\
1687 --logfile=/var/log/samba/user-syncpasswords.log
1688 samba-tool user syncpasswords --terminate \\
1689 --logfile=/var/log/samba/user-syncpasswords.log
1693 super(cmd_user_syncpasswords, self).__init__()
1695 synopsis = "%prog [--cache-ldb-initialize] [options]"
1697 takes_optiongroups = {
1698 "sambaopts": options.SambaOptions,
1699 "versionopts": options.VersionOptions,
1703 Option("--cache-ldb-initialize",
1704 help="Initialize the cache for the first time",
1705 dest="cache_ldb_initialize", action="store_true"),
1706 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1707 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1708 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1709 metavar="URL", dest="H"),
1710 Option("--filter", help="optional LDAP filter to set password on", type=str,
1711 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1712 Option("--attributes", type=str,
1713 help=virtual_attributes_help,
1714 metavar="ATTRIBUTELIST", dest="attributes"),
1715 Option("--decrypt-samba-gpg",
1716 help=decrypt_samba_gpg_help,
1717 action="store_true", default=False, dest="decrypt_samba_gpg"),
1718 Option("--script", help="Script that is called for each password change", type=str,
1719 metavar="/path/to/syncpasswords.script", dest="script"),
1720 Option("--no-wait", help="Don't block waiting for changes",
1721 action="store_true", default=False, dest="nowait"),
1722 Option("--logfile", type=str,
1723 help="The logfile to use (required in --daemon mode).",
1724 metavar="/path/to/syncpasswords.log", dest="logfile"),
1725 Option("--daemon", help="daemonize after initial setup",
1726 action="store_true", default=False, dest="daemon"),
1727 Option("--terminate",
1728 help="Send a SIGTERM to an already running (daemon) process",
1729 action="store_true", default=False, dest="terminate"),
1732 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1733 H=None, filter=None,
1734 attributes=None, decrypt_samba_gpg=None,
1735 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1736 sambaopts=None, versionopts=None):
1738 self.lp = sambaopts.get_loadparm()
1740 self.samdb_url = None
1744 if not cache_ldb_initialize:
1745 if attributes is not None:
1746 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1747 if decrypt_samba_gpg:
1748 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1749 if script is not None:
1750 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1751 if filter is not None:
1752 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1754 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1756 if nowait is not False:
1757 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1758 if logfile is not None:
1759 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1760 if daemon is not False:
1761 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1762 if terminate is not False:
1763 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1767 raise CommandError("--daemon is not allowed together with --no-wait")
1768 if terminate is not False:
1769 raise CommandError("--terminate is not allowed together with --no-wait")
1771 if terminate is True and daemon is True:
1772 raise CommandError("--terminate is not allowed together with --daemon")
1774 if daemon is True and logfile is None:
1775 raise CommandError("--daemon is only allowed together with --logfile")
1777 if terminate is True and logfile is None:
1778 raise CommandError("--terminate is only allowed together with --logfile")
1780 if script is not None:
1781 if not os.path.exists(script):
1782 raise CommandError("script[%s] does not exist!" % script)
1784 sync_command = "%s" % os.path.abspath(script)
1788 dirsync_filter = filter
1789 if dirsync_filter is None:
1790 dirsync_filter = "(&" + \
1791 "(objectClass=user)" + \
1792 "(userAccountControl:%s:=%u)" % (
1793 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1794 "(!(sAMAccountName=krbtgt*))" + \
1797 dirsync_secret_attrs = [
1800 "supplementalCredentials",
1803 dirsync_attrs = dirsync_secret_attrs + [
1806 "userPrincipalName",
1807 "userAccountControl",
1812 password_attrs = None
1814 if cache_ldb_initialize:
1816 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1818 if decrypt_samba_gpg and not gpg_decrypt:
1819 raise CommandError(decrypt_samba_gpg_help)
1821 password_attrs = self.parse_attributes(attributes)
1822 lower_attrs = [x.lower() for x in password_attrs]
1823 # We always return these in order to track deletions
1824 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1825 if a.lower() not in lower_attrs:
1826 password_attrs += [a]
1828 if cache_ldb is not None:
1829 if cache_ldb.lower().startswith("ldapi://"):
1830 raise CommandError("--cache_ldb ldapi:// is not supported")
1831 elif cache_ldb.lower().startswith("ldap://"):
1832 raise CommandError("--cache_ldb ldap:// is not supported")
1833 elif cache_ldb.lower().startswith("ldaps://"):
1834 raise CommandError("--cache_ldb ldaps:// is not supported")
1835 elif cache_ldb.lower().startswith("tdb://"):
1838 if not os.path.exists(cache_ldb):
1839 cache_ldb = self.lp.private_path(cache_ldb)
1841 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1843 self.lockfile = "%s.pid" % cache_ldb
1846 if self.logfile is not None:
1848 if info.st_nlink == 0:
1849 logfile = self.logfile
1851 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1852 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
1857 log_msg("Reopened logfile[%s]\n" % (logfile))
1858 self.logfile = logfile
1859 msg = "%s: pid[%d]: %s" % (
1863 self.outf.write(msg)
1872 "passwordAttribute",
1878 self.cache = Ldb(cache_ldb)
1879 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1880 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1884 self.samdb_url = str(res[0]["samdbUrl"][0])
1885 except KeyError as e:
1886 self.samdb_url = None
1888 self.samdb_url = None
1889 if self.samdb_url is None and not cache_ldb_initialize:
1890 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1892 if self.samdb_url is not None and cache_ldb_initialize:
1893 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1895 if self.samdb_url is None:
1897 self.dirsync_filter = dirsync_filter
1898 self.dirsync_attrs = dirsync_attrs
1899 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"]
1900 self.password_attrs = password_attrs
1901 self.decrypt_samba_gpg = decrypt_samba_gpg
1902 self.sync_command = sync_command
1903 add_ldif = "dn: %s\n" % self.cache_dn
1904 add_ldif += "objectClass: userSyncPasswords\n"
1905 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(get_bytes(self.samdb_url)).decode('utf8')
1906 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(get_bytes(self.dirsync_filter)).decode('utf8')
1907 for a in self.dirsync_attrs:
1908 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8')
1909 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1910 for a in self.password_attrs:
1911 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8')
1912 if self.decrypt_samba_gpg:
1913 add_ldif += "decryptSambaGPG: TRUE\n"
1915 add_ldif += "decryptSambaGPG: FALSE\n"
1916 if self.sync_command is not None:
1917 add_ldif += "syncCommand: %s\n" % self.sync_command
1918 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1919 self.cache.add_ldif(add_ldif)
1920 self.current_pid = None
1921 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1922 msgs = self.cache.parse_ldif(add_ldif)
1923 changetype, msg = next(msgs)
1924 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1925 self.outf.write("%s" % ldif)
1927 self.dirsync_filter = str(res[0]["dirsyncFilter"][0])
1928 self.dirsync_attrs = []
1929 for a in res[0]["dirsyncAttribute"]:
1930 self.dirsync_attrs.append(str(a))
1931 self.dirsync_controls = [str(res[0]["dirsyncControl"][0]), "extended_dn:1:0"]
1932 self.password_attrs = []
1933 for a in res[0]["passwordAttribute"]:
1934 self.password_attrs.append(str(a))
1935 decrypt_string = str(res[0]["decryptSambaGPG"][0])
1936 assert(decrypt_string in ["TRUE", "FALSE"])
1937 if decrypt_string == "TRUE":
1938 self.decrypt_samba_gpg = True
1940 self.decrypt_samba_gpg = False
1941 if "syncCommand" in res[0]:
1942 self.sync_command = str(res[0]["syncCommand"][0])
1944 self.sync_command = None
1945 if "currentPid" in res[0]:
1946 self.current_pid = int(res[0]["currentPid"][0])
1948 self.current_pid = None
1949 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1953 def run_sync_command(dn, ldif):
1954 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1955 sync_command_p = Popen(self.sync_command,
1960 res = sync_command_p.poll()
1963 input = "%s" % (ldif)
1964 reply = sync_command_p.communicate(input)[0]
1965 log_msg("%s\n" % (reply))
1966 res = sync_command_p.poll()
1968 sync_command_p.terminate()
1969 res = sync_command_p.wait()
1971 if reply.startswith("DONE-EXIT: "):
1974 log_msg("RESULT: %s\n" % (res))
1975 raise Exception("ERROR: %s - %s\n" % (res, reply))
1977 def handle_object(idx, dirsync_obj):
1978 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1979 guid = ndr_unpack(misc.GUID, binary_guid)
1980 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1981 sid = ndr_unpack(security.dom_sid, binary_sid)
1982 domain_sid, rid = sid.split()
1983 if rid == security.DOMAIN_RID_KRBTGT:
1984 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1986 for a in list(dirsync_obj.keys()):
1987 for h in dirsync_secret_attrs:
1988 if a.lower() == h.lower():
1990 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1991 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1992 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
1993 obj = self.get_account_attributes(self.samdb,
1994 username="%s" % sid,
1995 basedn="<GUID=%s>" % guid,
1996 filter="(objectClass=user)",
1997 scope=ldb.SCOPE_BASE,
1998 attrs=self.password_attrs,
1999 decrypt=self.decrypt_samba_gpg)
2000 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
2001 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
2002 if self.sync_command is None:
2003 self.outf.write("%s" % (ldif))
2005 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
2006 run_sync_command(obj.dn, ldif)
2008 def check_current_pid_conflict(terminate):
2014 self.lockfd = os.open(self.lockfile, flags, 0o600)
2015 except IOError as e4:
2016 (err, msg) = e4.args
2017 if err == errno.ENOENT:
2020 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
2021 (self.lockfile, msg, err))
2024 got_exclusive = False
2026 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2027 got_exclusive = True
2028 except IOError as e5:
2029 (err, msg) = e5.args
2030 if err != errno.EACCES and err != errno.EAGAIN:
2031 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
2032 (self.lockfile, msg, err))
2035 if not got_exclusive:
2036 buf = os.read(self.lockfd, 64)
2037 self.current_pid = None
2039 self.current_pid = int(buf)
2040 except ValueError as e:
2042 if self.current_pid is not None:
2045 if got_exclusive and terminate:
2047 os.ftruncate(self.lockfd, 0)
2048 except IOError as e2:
2049 (err, msg) = e2.args
2050 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2051 (self.lockfile, msg, err))
2053 os.close(self.lockfd)
2058 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2059 except IOError as e6:
2060 (err, msg) = e6.args
2061 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2062 (self.lockfile, msg, err))
2064 # We leave the function with the shared lock.
2067 def update_pid(pid):
2068 if self.lockfd != -1:
2069 got_exclusive = False
2070 # Try 5 times to get the exclusiv lock.
2071 for i in range(0, 5):
2073 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2074 got_exclusive = True
2075 except IOError as e:
2077 if err != errno.EACCES and err != errno.EAGAIN:
2078 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2079 (pid, self.lockfile, msg, err))
2084 if not got_exclusive:
2085 log_msg("update_pid(%r): failed to get exclusive lock[%s]" %
2086 (pid, self.lockfile))
2087 raise CommandError("update_pid(%r): failed to get "
2088 "exclusive lock[%s] after 5 seconds" %
2089 (pid, self.lockfile))
2096 os.ftruncate(self.lockfd, 0)
2098 os.write(self.lockfd, get_bytes(buf))
2099 except IOError as e3:
2100 (err, msg) = e3.args
2101 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2102 (self.lockfile, msg, err))
2104 self.current_pid = pid
2105 if self.current_pid is not None:
2106 log_msg("currentPid: %d\n" % self.current_pid)
2108 modify_ldif = "dn: %s\n" % (self.cache_dn)
2109 modify_ldif += "changetype: modify\n"
2110 modify_ldif += "replace: currentPid\n"
2111 if self.current_pid is not None:
2112 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2113 modify_ldif += "replace: currentTime\n"
2114 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2115 self.cache.modify_ldif(modify_ldif)
2118 def update_cache(res_controls):
2119 assert len(res_controls) > 0
2120 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2121 res_controls[0].critical = True
2122 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2123 # This cookie can be extremely long
2124 # log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2126 modify_ldif = "dn: %s\n" % (self.cache_dn)
2127 modify_ldif += "changetype: modify\n"
2128 modify_ldif += "replace: dirsyncControl\n"
2129 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2130 modify_ldif += "replace: currentTime\n"
2131 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2132 self.cache.modify_ldif(modify_ldif)
2135 def check_object(dirsync_obj, res_controls):
2136 assert len(res_controls) > 0
2137 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2139 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2140 sid = ndr_unpack(security.dom_sid, binary_sid)
2142 lastCookie = str(res_controls[0])
2144 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2145 expression="(lastCookie=%s)" % (
2146 ldb.binary_encode(lastCookie)),
2152 def update_object(dirsync_obj, res_controls):
2153 assert len(res_controls) > 0
2154 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2156 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2157 sid = ndr_unpack(security.dom_sid, binary_sid)
2159 lastCookie = str(res_controls[0])
2161 self.cache.transaction_start()
2163 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2164 expression="(objectClass=*)",
2165 attrs=["lastCookie"])
2167 add_ldif = "dn: %s\n" % (dn)
2168 add_ldif += "objectClass: userCookie\n"
2169 add_ldif += "lastCookie: %s\n" % (lastCookie)
2170 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2171 self.cache.add_ldif(add_ldif)
2173 modify_ldif = "dn: %s\n" % (dn)
2174 modify_ldif += "changetype: modify\n"
2175 modify_ldif += "replace: lastCookie\n"
2176 modify_ldif += "lastCookie: %s\n" % (lastCookie)
2177 modify_ldif += "replace: currentTime\n"
2178 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2179 self.cache.modify_ldif(modify_ldif)
2180 self.cache.transaction_commit()
2181 except Exception as e:
2182 self.cache.transaction_cancel()
2188 res = self.samdb.search(expression=str(self.dirsync_filter),
2189 scope=ldb.SCOPE_SUBTREE,
2190 attrs=self.dirsync_attrs,
2191 controls=self.dirsync_controls)
2192 log_msg("dirsync_loop(): results %d\n" % len(res))
2195 done = check_object(r, res.controls)
2197 handle_object(ri, r)
2198 update_object(r, res.controls)
2200 update_cache(res.controls)
2204 def sync_loop(wait):
2205 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2206 notify_controls = ["notification:1", "show_recycled:1"]
2207 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2208 scope=ldb.SCOPE_SUBTREE,
2210 controls=notify_controls,
2214 log_msg("Resuming monitoring\n")
2216 log_msg("Getting changes\n")
2217 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2218 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2219 self.outf.write("syncCommand: %s\n" % self.sync_command)
2222 if wait is not True:
2225 for msg in notify_handle:
2226 if not isinstance(msg, ldb.Message):
2227 self.outf.write("referal: %s\n" % msg)
2229 created = msg.get("uSNCreated")[0]
2230 changed = msg.get("uSNChanged")[0]
2231 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2232 (msg.dn, created, changed))
2236 res = notify_handle.result()
2241 orig_pid = os.getpid()
2246 if pid == 0: # Actual daemon
2248 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2253 if cache_ldb_initialize:
2255 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2260 if logfile is not None:
2261 import resource # Resource usage information.
2262 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2263 if maxfd == resource.RLIM_INFINITY:
2264 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2265 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2266 self.outf.write("Using logfile[%s]\n" % logfile)
2267 for fd in range(0, maxfd):
2278 log_msg("Attached to logfile[%s]\n" % (logfile))
2279 self.logfile = logfile
2282 conflict = check_current_pid_conflict(terminate)
2284 if self.current_pid is None:
2285 log_msg("No process running.\n")
2288 log_msg("Proccess %d is not running anymore.\n" % (
2292 log_msg("Sending SIGTERM to proccess %d.\n" % (
2294 os.kill(self.current_pid, signal.SIGTERM)
2297 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2298 os.getpid(), self.current_pid))
2302 update_pid(os.getpid())
2307 retry_sleep_max = 600
2312 retry_sleep = retry_sleep_min
2314 while self.samdb is None:
2315 if retry_sleep != 0:
2316 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2317 time.sleep(retry_sleep)
2318 retry_sleep = retry_sleep * 2
2319 if retry_sleep >= retry_sleep_max:
2320 retry_sleep = retry_sleep_max
2321 log_msg("Connecting to '%s'\n" % self.samdb_url)
2323 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2324 except Exception as msg:
2326 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2327 if wait is not True:
2332 except ldb.LdbError as e7:
2333 (enum, estr) = e7.args
2335 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2341 class cmd_user_edit(Command):
2342 """Modify User AD object.
2344 This command will allow editing of a user account in the Active Directory
2345 domain. You will then be able to add or change attributes and their values.
2347 The username specified on the command is the sAMAccountName.
2349 The command may be run from the root userid or another authorized userid.
2351 The -H or --URL= option can be used to execute the command against a remote
2355 samba-tool user edit User1 -H ldap://samba.samdom.example.com \\
2356 -U administrator --password=passw1rd
2358 Example1 shows how to edit a users attributes in the domain against a remote
2361 The -H parameter is used to specify the remote target server.
2364 samba-tool user edit User2
2366 Example2 shows how to edit a users attributes in the domain against a local
2370 samba-tool user edit User3 --editor=nano
2372 Example3 shows how to edit a users attributes in the domain against a local
2373 LDAP server using the 'nano' editor.
2376 synopsis = "%prog <username> [options]"
2379 Option("-H", "--URL", help="LDB URL for database or target server",
2380 type=str, metavar="URL", dest="H"),
2381 Option("--editor", help="Editor to use instead of the system default,"
2382 " or 'vi' if no system default is set.", type=str),
2385 takes_args = ["username"]
2386 takes_optiongroups = {
2387 "sambaopts": options.SambaOptions,
2388 "credopts": options.CredentialsOptions,
2389 "versionopts": options.VersionOptions,
2392 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2393 H=None, editor=None):
2394 from . import common
2396 lp = sambaopts.get_loadparm()
2397 creds = credopts.get_credentials(lp, fallback_machine=True)
2398 samdb = SamDB(url=H, session_info=system_session(),
2399 credentials=creds, lp=lp)
2401 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2402 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2404 domaindn = samdb.domain_dn()
2407 res = samdb.search(base=domaindn,
2409 scope=ldb.SCOPE_SUBTREE)
2412 raise CommandError('Unable to find user "%s"' % (username))
2415 result_ldif = common.get_ldif_for_editor(samdb, msg)
2418 editor = os.environ.get('EDITOR')
2422 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2423 t_file.write(get_bytes(result_ldif))
2426 check_call([editor, t_file.name])
2427 except CalledProcessError as e:
2428 raise CalledProcessError("ERROR: ", e)
2429 with open(t_file.name) as edited_file:
2430 edited_message = edited_file.read()
2433 msgs_edited = samdb.parse_ldif(edited_message)
2434 msg_edited = next(msgs_edited)[1]
2436 res_msg_diff = samdb.msg_diff(msg, msg_edited)
2437 if len(res_msg_diff) == 0:
2438 self.outf.write("Nothing to do\n")
2442 samdb.modify(res_msg_diff)
2443 except Exception as e:
2444 raise CommandError("Failed to modify user '%s': " % username, e)
2446 self.outf.write("Modified User '%s' successfully\n" % username)
2449 class cmd_user_show(Command):
2450 """Display a user AD object.
2452 This command displays a user account and it's attributes in the Active
2454 The username specified on the command is the sAMAccountName.
2456 The command may be run from the root userid or another authorized userid.
2458 The -H or --URL= option can be used to execute the command against a remote
2462 samba-tool user show User1 -H ldap://samba.samdom.example.com \\
2463 -U administrator --password=passw1rd
2465 Example1 shows how to display a users attributes in the domain against a remote
2468 The -H parameter is used to specify the remote target server.
2471 samba-tool user show User2
2473 Example2 shows how to display a users attributes in the domain against a local
2477 samba-tool user show User2 --attributes=objectSid,memberOf
2479 Example3 shows how to display a users objectSid and memberOf attributes.
2481 synopsis = "%prog <username> [options]"
2484 Option("-H", "--URL", help="LDB URL for database or target server",
2485 type=str, metavar="URL", dest="H"),
2486 Option("--attributes",
2487 help=("Comma separated list of attributes, "
2488 "which will be printed."),
2489 type=str, dest="user_attrs"),
2492 takes_args = ["username"]
2493 takes_optiongroups = {
2494 "sambaopts": options.SambaOptions,
2495 "credopts": options.CredentialsOptions,
2496 "versionopts": options.VersionOptions,
2499 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2500 H=None, user_attrs=None):
2502 lp = sambaopts.get_loadparm()
2503 creds = credopts.get_credentials(lp, fallback_machine=True)
2504 samdb = SamDB(url=H, session_info=system_session(),
2505 credentials=creds, lp=lp)
2509 attrs = user_attrs.split(",")
2511 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2512 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2514 domaindn = samdb.domain_dn()
2517 res = samdb.search(base=domaindn, expression=filter,
2518 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2521 raise CommandError('Unable to find user "%s"' % (username))
2524 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2525 self.outf.write(user_ldif)
2528 class cmd_user_move(Command):
2529 """Move a user to an organizational unit/container.
2531 This command moves a user account into the specified organizational unit
2533 The username specified on the command is the sAMAccountName.
2534 The name of the organizational unit or container can be specified as a
2535 full DN or without the domainDN component.
2537 The command may be run from the root userid or another authorized userid.
2539 The -H or --URL= option can be used to execute the command against a remote
2543 samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\
2544 -H ldap://samba.samdom.example.com -U administrator
2546 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2547 unit on a remote LDAP server.
2549 The -H parameter is used to specify the remote target server.
2552 samba-tool user move User1 CN=Users
2554 Example2 shows how to move a user User1 back into the CN=Users container
2555 on the local server.
2558 synopsis = "%prog <username> <new_parent_dn> [options]"
2561 Option("-H", "--URL", help="LDB URL for database or target server",
2562 type=str, metavar="URL", dest="H"),
2565 takes_args = ["username", "new_parent_dn"]
2566 takes_optiongroups = {
2567 "sambaopts": options.SambaOptions,
2568 "credopts": options.CredentialsOptions,
2569 "versionopts": options.VersionOptions,
2572 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2573 versionopts=None, H=None):
2574 lp = sambaopts.get_loadparm()
2575 creds = credopts.get_credentials(lp, fallback_machine=True)
2576 samdb = SamDB(url=H, session_info=system_session(),
2577 credentials=creds, lp=lp)
2578 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2580 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2581 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2583 res = samdb.search(base=domain_dn,
2585 scope=ldb.SCOPE_SUBTREE)
2588 raise CommandError('Unable to find user "%s"' % (username))
2591 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2592 except Exception as e:
2593 raise CommandError('Invalid new_parent_dn "%s": %s' %
2596 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2597 full_new_user_dn.remove_base_components(len(user_dn) - 1)
2598 full_new_user_dn.add_base(full_new_parent_dn)
2601 samdb.rename(user_dn, full_new_user_dn)
2602 except Exception as e:
2603 raise CommandError('Failed to move user "%s"' % username, e)
2604 self.outf.write('Moved user "%s" into "%s"\n' %
2605 (username, full_new_parent_dn))
2608 class cmd_user(SuperCommand):
2609 """User management."""
2612 subcommands["add"] = cmd_user_add()
2613 subcommands["create"] = cmd_user_create()
2614 subcommands["delete"] = cmd_user_delete()
2615 subcommands["disable"] = cmd_user_disable()
2616 subcommands["enable"] = cmd_user_enable()
2617 subcommands["list"] = cmd_user_list()
2618 subcommands["setexpiry"] = cmd_user_setexpiry()
2619 subcommands["password"] = cmd_user_password()
2620 subcommands["setpassword"] = cmd_user_setpassword()
2621 subcommands["getpassword"] = cmd_user_getpassword()
2622 subcommands["syncpasswords"] = cmd_user_syncpasswords()
2623 subcommands["edit"] = cmd_user_edit()
2624 subcommands["show"] = cmd_user_show()
2625 subcommands["move"] = cmd_user_move()