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('=')
162 random_reason = check_random()
163 if random_reason is not None:
164 raise ImportError(random_reason)
168 virtual_attributes["virtualSSHA"] = {
170 except ImportError as e:
171 reason = "hashlib.sha1()"
173 reason += " and " + random_reason
174 reason += " required"
175 disabled_virtual_attributes["virtualSSHA"] = {
179 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
181 random_reason = check_random()
182 if random_reason is not None:
183 raise ImportError(random_reason)
185 v = get_crypt_value(alg, "")
187 virtual_attributes[attr] = {
189 except ImportError as e:
192 reason += " and " + random_reason
193 reason += " required"
194 disabled_virtual_attributes[attr] = {
197 except NotImplementedError as e:
198 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
199 disabled_virtual_attributes[attr] = {
203 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
204 for x in range(1, 30):
205 virtual_attributes["virtualWDigest%02d" % x] = {}
207 virtual_attributes_help = "The attributes to display (comma separated). "
208 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
209 if len(disabled_virtual_attributes) != 0:
210 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
213 class cmd_user_create(Command):
214 """Create a new user.
216 This command creates a new user account in the Active Directory domain. The username specified on the command is the sAMaccountName.
218 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).
220 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.
222 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.
224 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.
227 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
229 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.
232 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
234 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.
237 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
239 Example3 shows how to create a new user in the OrgUnit organizational unit.
242 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
244 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'.
247 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
248 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
250 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
251 --nis-domain is set, then the other four parameters are mandatory.
254 synopsis = "%prog <username> [<password>] [options]"
257 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
258 metavar="URL", dest="H"),
259 Option("--must-change-at-next-login",
260 help="Force password to be changed on next login",
261 action="store_true"),
262 Option("--random-password",
263 help="Generate random password",
264 action="store_true"),
265 Option("--smartcard-required",
266 help="Require a smartcard for interactive logons",
267 action="store_true"),
268 Option("--use-username-as-cn",
269 help="Force use of username as user's CN",
270 action="store_true"),
272 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>'",
274 Option("--surname", help="User's surname", type=str),
275 Option("--given-name", help="User's given name", type=str),
276 Option("--initials", help="User's initials", type=str),
277 Option("--profile-path", help="User's profile path", type=str),
278 Option("--script-path", help="User's logon script path", type=str),
279 Option("--home-drive", help="User's home drive letter", type=str),
280 Option("--home-directory", help="User's home directory path", type=str),
281 Option("--job-title", help="User's job title", type=str),
282 Option("--department", help="User's department", type=str),
283 Option("--company", help="User's company", type=str),
284 Option("--description", help="User's description", type=str),
285 Option("--mail-address", help="User's email address", type=str),
286 Option("--internet-address", help="User's home page", type=str),
287 Option("--telephone-number", help="User's phone number", type=str),
288 Option("--physical-delivery-office", help="User's office location", type=str),
289 Option("--rfc2307-from-nss",
290 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
291 action="store_true"),
292 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
293 Option("--unix-home", help="User's Unix/RFC2307 home directory",
295 Option("--uid", help="User's Unix/RFC2307 username", type=str),
296 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
297 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
298 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
299 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
302 takes_args = ["username", "password?"]
304 takes_optiongroups = {
305 "sambaopts": options.SambaOptions,
306 "credopts": options.CredentialsOptions,
307 "versionopts": options.VersionOptions,
310 def run(self, username, password=None, credopts=None, sambaopts=None,
311 versionopts=None, H=None, must_change_at_next_login=False,
312 random_password=False, use_username_as_cn=False, userou=None,
313 surname=None, given_name=None, initials=None, profile_path=None,
314 script_path=None, home_drive=None, home_directory=None,
315 job_title=None, department=None, company=None, description=None,
316 mail_address=None, internet_address=None, telephone_number=None,
317 physical_delivery_office=None, rfc2307_from_nss=False,
318 nis_domain=None, unix_home=None, uid=None, uid_number=None,
319 gid_number=None, gecos=None, login_shell=None,
320 smartcard_required=False):
322 if smartcard_required:
323 if password is not None and password is not '':
324 raise CommandError('It is not allowed to specify '
326 'together with --smartcard-required.')
327 if must_change_at_next_login:
328 raise CommandError('It is not allowed to specify '
329 '--must-change-at-next-login '
330 'together with --smartcard-required.')
332 if random_password and not smartcard_required:
333 password = generate_random_password(128, 255)
336 if smartcard_required:
338 if password is not None and password is not '':
340 password = getpass("New Password: ")
341 passwordverify = getpass("Retype Password: ")
342 if not password == passwordverify:
344 self.outf.write("Sorry, passwords do not match.\n")
347 pwent = pwd.getpwnam(username)
350 if uid_number is None:
351 uid_number = pwent[2]
352 if gid_number is None:
353 gid_number = pwent[3]
356 if login_shell is None:
357 login_shell = pwent[6]
359 lp = sambaopts.get_loadparm()
360 creds = credopts.get_credentials(lp)
362 if uid_number or gid_number:
363 if not lp.get("idmap_ldb:use rfc2307"):
364 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")
366 if nis_domain is not None:
367 if None in (uid_number, login_shell, unix_home, gid_number):
368 raise CommandError('Missing parameters. To enable NIS features, '
369 'the following options have to be given: '
370 '--nis-domain=, --uidNumber=, --login-shell='
371 ', --unix-home=, --gid-number= Operation '
375 samdb = SamDB(url=H, session_info=system_session(),
376 credentials=creds, lp=lp)
377 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
378 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
379 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
380 jobtitle=job_title, department=department, company=company, description=description,
381 mailaddress=mail_address, internetaddress=internet_address,
382 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
383 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
384 uidnumber=uid_number, gidnumber=gid_number,
385 gecos=gecos, loginshell=login_shell,
386 smartcard_required=smartcard_required)
387 except Exception as e:
388 raise CommandError("Failed to add user '%s': " % username, e)
390 self.outf.write("User '%s' created successfully\n" % username)
393 class cmd_user_add(cmd_user_create):
394 __doc__ = cmd_user_create.__doc__
395 # take this print out after the add subcommand is removed.
396 # the add subcommand is deprecated but left in for now to allow people to
399 def run(self, *args, **kwargs):
401 "Note: samba-tool user add is deprecated. "
402 "Please use samba-tool user create for the same function.\n")
403 return super(cmd_user_add, self).run(*args, **kwargs)
406 class cmd_user_delete(Command):
409 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
411 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.
413 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.
416 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
418 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.
421 sudo samba-tool user delete User2
423 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.
426 synopsis = "%prog <username> [options]"
429 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
430 metavar="URL", dest="H"),
433 takes_args = ["username"]
434 takes_optiongroups = {
435 "sambaopts": options.SambaOptions,
436 "credopts": options.CredentialsOptions,
437 "versionopts": options.VersionOptions,
440 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
442 lp = sambaopts.get_loadparm()
443 creds = credopts.get_credentials(lp, fallback_machine=True)
445 samdb = SamDB(url=H, session_info=system_session(),
446 credentials=creds, lp=lp)
448 filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
449 ldb.binary_encode(username))
452 res = samdb.search(base=samdb.domain_dn(),
453 scope=ldb.SCOPE_SUBTREE,
458 raise CommandError('Unable to find user "%s"' % (username))
461 samdb.delete(user_dn)
462 except Exception as e:
463 raise CommandError('Failed to remove user "%s"' % username, e)
464 self.outf.write("Deleted user %s\n" % username)
467 class cmd_user_list(Command):
468 """List all users."""
470 synopsis = "%prog [options]"
473 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
474 metavar="URL", dest="H"),
477 takes_optiongroups = {
478 "sambaopts": options.SambaOptions,
479 "credopts": options.CredentialsOptions,
480 "versionopts": options.VersionOptions,
483 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
484 lp = sambaopts.get_loadparm()
485 creds = credopts.get_credentials(lp, fallback_machine=True)
487 samdb = SamDB(url=H, session_info=system_session(),
488 credentials=creds, lp=lp)
490 domain_dn = samdb.domain_dn()
491 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
492 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
493 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
494 attrs=["samaccountname"])
499 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
502 class cmd_user_enable(Command):
505 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.
507 There are many reasons why an account may become disabled. These include:
508 - If a user exceeds the account policy for logon attempts
509 - If an administrator disables the account
510 - If the account expires
512 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
514 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.
516 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.
519 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
521 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.
524 su samba-tool user enable Testuser2
526 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.
529 samba-tool user enable --filter=samaccountname=Testuser3
531 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
534 synopsis = "%prog (<username>|--filter <filter>) [options]"
536 takes_optiongroups = {
537 "sambaopts": options.SambaOptions,
538 "versionopts": options.VersionOptions,
539 "credopts": options.CredentialsOptions,
543 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
544 metavar="URL", dest="H"),
545 Option("--filter", help="LDAP Filter to set password on", type=str),
548 takes_args = ["username?"]
550 def run(self, username=None, sambaopts=None, credopts=None,
551 versionopts=None, filter=None, H=None):
552 if username is None and filter is None:
553 raise CommandError("Either the username or '--filter' must be specified!")
556 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
558 lp = sambaopts.get_loadparm()
559 creds = credopts.get_credentials(lp, fallback_machine=True)
561 samdb = SamDB(url=H, session_info=system_session(),
562 credentials=creds, lp=lp)
564 samdb.enable_account(filter)
565 except Exception as msg:
566 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
567 self.outf.write("Enabled user '%s'\n" % (username or filter))
570 class cmd_user_disable(Command):
571 """Disable a user."""
573 synopsis = "%prog (<username>|--filter <filter>) [options]"
576 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
577 metavar="URL", dest="H"),
578 Option("--filter", help="LDAP Filter to set password on", type=str),
581 takes_args = ["username?"]
583 takes_optiongroups = {
584 "sambaopts": options.SambaOptions,
585 "credopts": options.CredentialsOptions,
586 "versionopts": options.VersionOptions,
589 def run(self, username=None, sambaopts=None, credopts=None,
590 versionopts=None, filter=None, H=None):
591 if username is None and filter is None:
592 raise CommandError("Either the username or '--filter' must be specified!")
595 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
597 lp = sambaopts.get_loadparm()
598 creds = credopts.get_credentials(lp, fallback_machine=True)
600 samdb = SamDB(url=H, session_info=system_session(),
601 credentials=creds, lp=lp)
603 samdb.disable_account(filter)
604 except Exception as msg:
605 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
608 class cmd_user_setexpiry(Command):
609 """Set the expiration of a user account.
611 The user can either be specified by their sAMAccountName or using the --filter option.
613 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.
615 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.
618 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
620 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.
623 sudo samba-tool user setexpiry User2 --noexpiry
625 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.
628 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
630 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.
633 samba-tool user setexpiry --noexpiry User4
634 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
637 synopsis = "%prog (<username>|--filter <filter>) [options]"
639 takes_optiongroups = {
640 "sambaopts": options.SambaOptions,
641 "versionopts": options.VersionOptions,
642 "credopts": options.CredentialsOptions,
646 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
647 metavar="URL", dest="H"),
648 Option("--filter", help="LDAP Filter to set password on", type=str),
649 Option("--days", help="Days to expiry", type=int, default=0),
650 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
653 takes_args = ["username?"]
655 def run(self, username=None, sambaopts=None, credopts=None,
656 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
657 if username is None and filter is None:
658 raise CommandError("Either the username or '--filter' must be specified!")
661 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
663 lp = sambaopts.get_loadparm()
664 creds = credopts.get_credentials(lp)
666 samdb = SamDB(url=H, session_info=system_session(),
667 credentials=creds, lp=lp)
670 samdb.setexpiry(filter, days * 24 * 3600, no_expiry_req=noexpiry)
671 except Exception as msg:
672 # FIXME: Catch more specific exception
673 raise CommandError("Failed to set expiry for user '%s': %s" % (
674 username or filter, msg))
676 self.outf.write("Expiry for user '%s' disabled.\n" % (
679 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
680 username or filter, days))
683 class cmd_user_password(Command):
684 """Change password for a user account (the one provided in authentication).
687 synopsis = "%prog [options]"
690 Option("--newpassword", help="New password", type=str),
693 takes_optiongroups = {
694 "sambaopts": options.SambaOptions,
695 "credopts": options.CredentialsOptions,
696 "versionopts": options.VersionOptions,
699 def run(self, credopts=None, sambaopts=None, versionopts=None,
702 lp = sambaopts.get_loadparm()
703 creds = credopts.get_credentials(lp)
705 # get old password now, to get the password prompts in the right order
706 old_password = creds.get_password()
708 net = Net(creds, lp, server=credopts.ipaddress)
710 password = newpassword
712 if password is not None and password is not '':
714 password = getpass("New Password: ")
715 passwordverify = getpass("Retype Password: ")
716 if not password == passwordverify:
718 self.outf.write("Sorry, passwords do not match.\n")
721 if not isinstance(password, text_type):
722 password = password.decode('utf8')
723 net.change_password(password)
724 except Exception as msg:
725 # FIXME: catch more specific exception
726 raise CommandError("Failed to change password : %s" % msg)
727 self.outf.write("Changed password OK\n")
730 class cmd_user_setpassword(Command):
731 """Set or reset the password of a user account.
733 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.
735 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.
737 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.
739 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.
742 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
744 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.
747 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
749 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.
752 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
754 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
757 synopsis = "%prog (<username>|--filter <filter>) [options]"
759 takes_optiongroups = {
760 "sambaopts": options.SambaOptions,
761 "versionopts": options.VersionOptions,
762 "credopts": options.CredentialsOptions,
766 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
767 metavar="URL", dest="H"),
768 Option("--filter", help="LDAP Filter to set password on", type=str),
769 Option("--newpassword", help="Set password", type=str),
770 Option("--must-change-at-next-login",
771 help="Force password to be changed on next login",
772 action="store_true"),
773 Option("--random-password",
774 help="Generate random password",
775 action="store_true"),
776 Option("--smartcard-required",
777 help="Require a smartcard for interactive logons",
778 action="store_true"),
779 Option("--clear-smartcard-required",
780 help="Don't require a smartcard for interactive logons",
781 action="store_true"),
784 takes_args = ["username?"]
786 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
787 versionopts=None, H=None, newpassword=None,
788 must_change_at_next_login=False, random_password=False,
789 smartcard_required=False, clear_smartcard_required=False):
790 if filter is None and username is None:
791 raise CommandError("Either the username or '--filter' must be specified!")
793 password = newpassword
795 if smartcard_required:
796 if password is not None and password is not '':
797 raise CommandError('It is not allowed to specify '
799 'together with --smartcard-required.')
800 if must_change_at_next_login:
801 raise CommandError('It is not allowed to specify '
802 '--must-change-at-next-login '
803 'together with --smartcard-required.')
804 if clear_smartcard_required:
805 raise CommandError('It is not allowed to specify '
806 '--clear-smartcard-required '
807 'together with --smartcard-required.')
809 if random_password and not smartcard_required:
810 password = generate_random_password(128, 255)
813 if smartcard_required:
815 if password is not None and password is not '':
817 password = getpass("New Password: ")
818 passwordverify = getpass("Retype Password: ")
819 if not password == passwordverify:
821 self.outf.write("Sorry, passwords do not match.\n")
824 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
826 lp = sambaopts.get_loadparm()
827 creds = credopts.get_credentials(lp)
829 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
831 samdb = SamDB(url=H, session_info=system_session(),
832 credentials=creds, lp=lp)
834 if smartcard_required:
837 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
838 flags = dsdb.UF_SMARTCARD_REQUIRED
839 samdb.toggle_userAccountFlags(filter, flags, on=True)
840 command = "Failed to enable account for user '%s'" % (username or filter)
841 samdb.enable_account(filter)
842 except Exception as msg:
843 # FIXME: catch more specific exception
844 raise CommandError("%s: %s" % (command, msg))
845 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
849 if clear_smartcard_required:
850 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
851 flags = dsdb.UF_SMARTCARD_REQUIRED
852 samdb.toggle_userAccountFlags(filter, flags, on=False)
853 command = "Failed to set password for user '%s'" % (username or filter)
854 samdb.setpassword(filter, password,
855 force_change_at_next_login=must_change_at_next_login,
857 except Exception as msg:
858 # FIXME: catch more specific exception
859 raise CommandError("%s: %s" % (command, msg))
860 self.outf.write("Changed password OK\n")
863 class GetPasswordCommand(Command):
866 super(GetPasswordCommand, self).__init__()
869 def connect_system_samdb(self, url, allow_local=False, verbose=False):
871 # using anonymous here, results in no authentication
872 # which means we can get system privileges via
873 # the privileged ldapi socket
874 creds = credentials.Credentials()
875 creds.set_anonymous()
877 if url is None and allow_local:
879 elif url.lower().startswith("ldapi://"):
881 elif url.lower().startswith("ldap://"):
882 raise CommandError("--url ldap:// is not supported for this command")
883 elif url.lower().startswith("ldaps://"):
884 raise CommandError("--url ldaps:// is not supported for this command")
885 elif not allow_local:
886 raise CommandError("--url requires an ldapi:// url for this command")
889 self.outf.write("Connecting to '%s'\n" % url)
891 samdb = SamDB(url=url, session_info=system_session(),
892 credentials=creds, lp=self.lp)
896 # Make sure we're connected as SYSTEM
898 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
900 sids = res[0].get("tokenGroups")
901 assert len(sids) == 1
902 sid = ndr_unpack(security.dom_sid, sids[0])
903 assert str(sid) == security.SID_NT_SYSTEM
904 except Exception as msg:
905 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
906 (security.SID_NT_SYSTEM))
908 # We use sort here in order to have a predictable processing order
909 # this might not be strictly needed, but also doesn't hurt here
910 for a in sorted(virtual_attributes.keys()):
911 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
912 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
916 def get_account_attributes(self, samdb, username, basedn, filter, scope,
923 (attr, _, opts) = a.partition(';')
925 attr_opts[attr] = opts
927 attr_opts[attr] = None
928 search_attrs.append(attr)
929 lower_attrs = [x.lower() for x in search_attrs]
931 require_supplementalCredentials = False
932 for a in virtual_attributes.keys():
933 if a.lower() in lower_attrs:
934 require_supplementalCredentials = True
935 add_supplementalCredentials = False
936 add_unicodePwd = False
937 if require_supplementalCredentials:
938 a = "supplementalCredentials"
939 if a.lower() not in lower_attrs:
941 add_supplementalCredentials = True
943 if a.lower() not in lower_attrs:
945 add_unicodePwd = True
946 add_sAMAcountName = False
948 if a.lower() not in lower_attrs:
950 add_sAMAcountName = True
952 add_userPrincipalName = False
953 upn = "usePrincipalName"
954 if upn.lower() not in lower_attrs:
955 search_attrs += [upn]
956 add_userPrincipalName = True
958 if scope == ldb.SCOPE_BASE:
959 search_controls = ["show_deleted:1", "show_recycled:1"]
963 res = samdb.search(base=basedn, expression=filter,
964 scope=scope, attrs=search_attrs,
965 controls=search_controls)
967 raise Exception('Unable to find user "%s"' % (username or filter))
969 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
970 except Exception as msg:
971 # FIXME: catch more specific exception
972 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
977 if "supplementalCredentials" in obj:
978 sc_blob = obj["supplementalCredentials"][0]
979 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
980 if add_supplementalCredentials:
981 del obj["supplementalCredentials"]
982 if "unicodePwd" in obj:
983 unicodePwd = obj["unicodePwd"][0]
985 del obj["unicodePwd"]
986 account_name = obj["sAMAccountName"][0]
987 if add_sAMAcountName:
988 del obj["sAMAccountName"]
989 if "userPrincipalName" in obj:
990 account_upn = obj["userPrincipalName"][0]
992 realm = self.lp.get("realm")
993 account_upn = "%s@%s" % (account_name, realm.lower())
994 if add_userPrincipalName:
995 del obj["userPrincipalName"]
998 def get_package(name, min_idx=0):
999 if name in calculated:
1000 return calculated[name]
1004 min_idx = len(sc.sub.packages) + min_idx
1006 for p in sc.sub.packages:
1013 return binascii.a2b_hex(p.data)
1018 # Samba adds 'Primary:SambaGPG' at the end.
1019 # When Windows sets the password it keeps
1020 # 'Primary:SambaGPG' and rotates it to
1021 # the begining. So we can only use the value,
1022 # if it is the last one.
1024 # In order to get more protection we verify
1025 # the nthash of the decrypted utf16 password
1026 # against the stored nthash in unicodePwd.
1028 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1029 if sgv is not None and unicodePwd is not None:
1030 ctx = gpgme.Context()
1032 cipher_io = io.BytesIO(sgv)
1033 plain_io = io.BytesIO()
1035 ctx.decrypt(cipher_io, plain_io)
1036 cv = plain_io.getvalue()
1038 # We only use the password if it matches
1039 # the current nthash stored in the unicodePwd
1042 tmp = credentials.Credentials()
1044 tmp.set_utf16_password(cv)
1045 nthash = tmp.get_nt_hash()
1046 if nthash == unicodePwd:
1047 calculated["Primary:CLEARTEXT"] = cv
1048 except gpgme.GpgmeError as e1:
1049 (major, minor, msg) = e1.args
1050 if major == gpgme.ERR_BAD_SECKEY:
1051 msg = "ERR_BAD_SECKEY: " + msg
1053 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1054 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1055 username or account_name, msg))
1057 def get_utf8(a, b, username):
1059 u = unicode(b, 'utf-16-le')
1060 except UnicodeDecodeError as e:
1061 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1064 u8 = u.encode('utf-8')
1067 # Extract the WDigest hash for the value specified by i.
1068 # Builds an htdigest compatible value
1070 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1071 domain, dns_domain):
1076 user = account_name.lower()
1077 realm = domain.lower()
1079 user = account_name.upper()
1080 realm = domain.upper()
1083 realm = domain.upper()
1086 realm = domain.lower()
1088 user = account_name.upper()
1089 realm = domain.lower()
1091 user = account_name.lower()
1092 realm = domain.upper()
1095 realm = dns_domain.lower()
1097 user = account_name.lower()
1098 realm = dns_domain.lower()
1100 user = account_name.upper()
1101 realm = dns_domain.upper()
1104 realm = dns_domain.upper()
1107 realm = dns_domain.lower()
1109 user = account_name.upper()
1110 realm = dns_domain.lower()
1112 user = account_name.lower()
1113 realm = dns_domain.upper()
1118 user = account_upn.lower()
1121 user = account_upn.upper()
1124 user = "%s\\%s" % (domain, account_name)
1127 user = "%s\\%s" % (domain.lower(), account_name.lower())
1130 user = "%s\\%s" % (domain.upper(), account_name.upper())
1136 user = account_name.lower()
1139 user = account_name.upper()
1145 user = account_upn.lower()
1148 user = account_upn.upper()
1151 user = "%s\\%s" % (domain, account_name)
1154 # Differs from spec, see tests
1155 user = "%s\\%s" % (domain.lower(), account_name.lower())
1158 # Differs from spec, see tests
1159 user = "%s\\%s" % (domain.upper(), account_name.upper())
1164 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1167 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
1168 return "%s:%s:%s" % (user, realm, digest)
1172 # get the value for a virtualCrypt attribute.
1173 # look for an exact match on algorithm and rounds in supplemental creds
1174 # if not found calculate using Primary:CLEARTEXT
1175 # if no Primary:CLEARTEXT return the first supplementalCredential
1176 # that matches the algorithm.
1177 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1180 b = get_package("Primary:userPassword")
1182 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1184 # No exact match on algorithm and number of rounds
1185 # try and calculate one from the Primary:CLEARTEXT
1186 b = get_package("Primary:CLEARTEXT")
1188 u8 = get_utf8(a, b, username or account_name)
1190 sv = get_crypt_value(str(algorithm), u8, rounds)
1192 # Unable to calculate a hash with the specified
1193 # number of rounds, fall back to the first hash using
1194 # the specified algorithm
1198 return "{CRYPT}" + sv
1200 def get_userPassword_hash(blob, algorithm, rounds):
1201 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1204 # Check that the NT hash has not been changed without updating
1205 # the user password hashes. This indicates that password has been
1206 # changed without updating the supplemental credentials.
1207 if unicodePwd != bytearray(up.current_nt_hash.hash):
1210 scheme_prefix = "$%d$" % algorithm
1211 prefix = scheme_prefix
1213 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1217 if (scheme_match is None and
1218 h.scheme == SCHEME and
1219 h.value.startswith(scheme_prefix)):
1220 scheme_match = h.value
1221 if h.scheme == SCHEME and h.value.startswith(prefix):
1222 return (h.value, scheme_match)
1224 # No match on the number of rounds, return the value of the
1225 # first matching scheme
1226 return (None, scheme_match)
1228 # We use sort here in order to have a predictable processing order
1229 for a in sorted(virtual_attributes.keys()):
1230 if not a.lower() in lower_attrs:
1233 if a == "virtualClearTextUTF8":
1234 b = get_package("Primary:CLEARTEXT")
1237 u8 = get_utf8(a, b, username or account_name)
1241 elif a == "virtualClearTextUTF16":
1242 v = get_package("Primary:CLEARTEXT")
1245 elif a == "virtualSSHA":
1246 b = get_package("Primary:CLEARTEXT")
1249 u8 = get_utf8(a, b, username or account_name)
1252 salt = get_random_bytes(4)
1256 bv = h.digest() + salt
1257 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
1258 elif a == "virtualCryptSHA256":
1259 rounds = get_rounds(attr_opts[a])
1260 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1264 elif a == "virtualCryptSHA512":
1265 rounds = get_rounds(attr_opts[a])
1266 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1270 elif a == "virtualSambaGPG":
1271 # Samba adds 'Primary:SambaGPG' at the end.
1272 # When Windows sets the password it keeps
1273 # 'Primary:SambaGPG' and rotates it to
1274 # the begining. So we can only use the value,
1275 # if it is the last one.
1276 v = get_package("Primary:SambaGPG", min_idx=-1)
1279 elif a.startswith("virtualWDigest"):
1280 primary_wdigest = get_package("Primary:WDigest")
1281 if primary_wdigest is None:
1283 x = a[len("virtualWDigest"):]
1288 domain = self.lp.get("workgroup")
1289 dns_domain = samdb.domain_dns_name()
1290 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1295 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1298 def parse_attributes(self, attributes):
1300 if attributes is None:
1301 raise CommandError("Please specify --attributes")
1302 attrs = attributes.split(',')
1305 pa = pa.lstrip().rstrip()
1306 for da in disabled_virtual_attributes.keys():
1307 if pa.lower() == da.lower():
1308 r = disabled_virtual_attributes[da]["reason"]
1309 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1311 for va in virtual_attributes.keys():
1312 if pa.lower() == va.lower():
1313 # Take the real name
1316 password_attrs += [pa]
1318 return password_attrs
1321 class cmd_user_getpassword(GetPasswordCommand):
1322 """Get the password fields of a user/computer account.
1324 This command gets the logon password for a user/computer account.
1326 The username specified on the command is the sAMAccountName.
1327 The username may also be specified using the --filter option.
1329 The command must be run from the root user id or another authorized user id.
1330 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1331 used to adjust the local path. By default tdb:// is used by default.
1333 The '--attributes' parameter takes a comma separated list of attributes,
1334 which will be printed or given to the script specified by '--script'. If a
1335 specified attribute is not available on an object it's silently omitted.
1336 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1337 the NTHASH) and the following virtual attributes are possible (see --help
1338 for which virtual attributes are supported in your environment):
1340 virtualClearTextUTF16: The raw cleartext as stored in the
1341 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1342 with '--decrypt-samba-gpg') buffer inside of the
1343 supplementalCredentials attribute. This typically
1344 contains valid UTF-16-LE, but may contain random
1345 bytes, e.g. for computer accounts.
1347 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1348 (only from valid UTF-16-LE)
1350 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1351 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1353 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1354 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1355 with a $5$... salt, see crypt(3) on modern systems.
1356 The number of rounds used to calculate the hash can
1357 also be specified. By appending ";rounds=x" to the
1358 attribute name i.e. virtualCryptSHA256;rounds=10000
1359 will calculate a SHA256 hash with 10,000 rounds.
1360 non numeric values for rounds are silently ignored
1361 The value is calculated as follows:
1362 1) If a value exists in 'Primary:userPassword' with
1363 the specified number of rounds it is returned.
1364 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1365 '--decrypt-samba-gpg'. Calculate a hash with
1366 the specified number of rounds
1367 3) Return the first CryptSHA256 value in
1368 'Primary:userPassword'
1371 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1372 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1373 with a $6$... salt, see crypt(3) on modern systems.
1374 The number of rounds used to calculate the hash can
1375 also be specified. By appending ";rounds=x" to the
1376 attribute name i.e. virtualCryptSHA512;rounds=10000
1377 will calculate a SHA512 hash with 10,000 rounds.
1378 non numeric values for rounds are silently ignored
1379 The value is calculated as follows:
1380 1) If a value exists in 'Primary:userPassword' with
1381 the specified number of rounds it is returned.
1382 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1383 '--decrypt-samba-gpg'. Calculate a hash with
1384 the specified number of rounds
1385 3) Return the first CryptSHA512 value in
1386 'Primary:userPassword'
1388 virtualWDigestNN: The individual hash values stored in
1389 'Primary:WDigest' where NN is the hash number in
1391 NOTE: As at 22-05-2017 the documentation:
1392 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1393 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1396 virtualSambaGPG: The raw cleartext as stored in the
1397 'Primary:SambaGPG' buffer inside of the
1398 supplementalCredentials attribute.
1399 See the 'password hash gpg key ids' option in
1402 The '--decrypt-samba-gpg' option triggers decryption of the
1403 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1404 in your environment or not (the python-gpgme package is required). Please
1405 note that you might need to set the GNUPGHOME environment variable. If the
1406 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1407 environment variable has been set correctly and the passphrase is already
1408 known by the gpg-agent.
1411 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1414 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1418 super(cmd_user_getpassword, self).__init__()
1420 synopsis = "%prog (<username>|--filter <filter>) [options]"
1422 takes_optiongroups = {
1423 "sambaopts": options.SambaOptions,
1424 "versionopts": options.VersionOptions,
1428 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1429 metavar="URL", dest="H"),
1430 Option("--filter", help="LDAP Filter to set password on", type=str),
1431 Option("--attributes", type=str,
1432 help=virtual_attributes_help,
1433 metavar="ATTRIBUTELIST", dest="attributes"),
1434 Option("--decrypt-samba-gpg",
1435 help=decrypt_samba_gpg_help,
1436 action="store_true", default=False, dest="decrypt_samba_gpg"),
1439 takes_args = ["username?"]
1441 def run(self, username=None, H=None, filter=None,
1442 attributes=None, decrypt_samba_gpg=None,
1443 sambaopts=None, versionopts=None):
1444 self.lp = sambaopts.get_loadparm()
1446 if decrypt_samba_gpg and not gpgme_support:
1447 raise CommandError(decrypt_samba_gpg_help)
1449 if filter is None and username is None:
1450 raise CommandError("Either the username or '--filter' must be specified!")
1453 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1455 if attributes is None:
1456 raise CommandError("Please specify --attributes")
1458 password_attrs = self.parse_attributes(attributes)
1460 samdb = self.connect_system_samdb(url=H, allow_local=True)
1462 obj = self.get_account_attributes(samdb, username,
1465 scope=ldb.SCOPE_SUBTREE,
1466 attrs=password_attrs,
1467 decrypt=decrypt_samba_gpg)
1469 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1470 self.outf.write("%s" % ldif)
1471 self.outf.write("Got password OK\n")
1474 class cmd_user_syncpasswords(GetPasswordCommand):
1475 """Sync the password of user accounts.
1477 This syncs logon passwords for user accounts.
1479 Note that this command should run on a single domain controller only
1480 (typically the PDC-emulator). However the "password hash gpg key ids"
1481 option should to be configured on all domain controllers.
1483 The command must be run from the root user id or another authorized user id.
1484 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1485 local path. By default, ldapi:// is used with the default path to the
1486 privileged ldapi socket.
1488 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1489 "Sync Loop Terminate".
1492 Cache Initialization
1493 ====================
1495 The first time, this command needs to be called with
1496 '--cache-ldb-initialize' in order to initialize its cache.
1498 The cache initialization requires '--attributes' and allows the following
1499 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1502 The '--attributes' parameter takes a comma separated list of attributes,
1503 which will be printed or given to the script specified by '--script'. If a
1504 specified attribute is not available on an object it will be silently omitted.
1505 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1506 the NTHASH) and the following virtual attributes are possible (see '--help'
1507 for supported virtual attributes in your environment):
1509 virtualClearTextUTF16: The raw cleartext as stored in the
1510 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1511 with '--decrypt-samba-gpg') buffer inside of the
1512 supplementalCredentials attribute. This typically
1513 contains valid UTF-16-LE, but may contain random
1514 bytes, e.g. for computer accounts.
1516 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1517 (only from valid UTF-16-LE)
1519 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1520 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1522 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1523 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1524 with a $5$... salt, see crypt(3) on modern systems.
1525 The number of rounds used to calculate the hash can
1526 also be specified. By appending ";rounds=x" to the
1527 attribute name i.e. virtualCryptSHA256;rounds=10000
1528 will calculate a SHA256 hash with 10,000 rounds.
1529 non numeric values for rounds are silently ignored
1530 The value is calculated as follows:
1531 1) If a value exists in 'Primary:userPassword' with
1532 the specified number of rounds it is returned.
1533 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1534 '--decrypt-samba-gpg'. Calculate a hash with
1535 the specified number of rounds
1536 3) Return the first CryptSHA256 value in
1537 'Primary:userPassword'
1539 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1540 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1541 with a $6$... salt, see crypt(3) on modern systems.
1542 The number of rounds used to calculate the hash can
1543 also be specified. By appending ";rounds=x" to the
1544 attribute name i.e. virtualCryptSHA512;rounds=10000
1545 will calculate a SHA512 hash with 10,000 rounds.
1546 non numeric values for rounds are silently ignored
1547 The value is calculated as follows:
1548 1) If a value exists in 'Primary:userPassword' with
1549 the specified number of rounds it is returned.
1550 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1551 '--decrypt-samba-gpg'. Calculate a hash with
1552 the specified number of rounds
1553 3) Return the first CryptSHA512 value in
1554 'Primary:userPassword'
1556 virtualWDigestNN: The individual hash values stored in
1557 'Primary:WDigest' where NN is the hash number in
1559 NOTE: As at 22-05-2017 the documentation:
1560 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1561 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1564 virtualSambaGPG: The raw cleartext as stored in the
1565 'Primary:SambaGPG' buffer inside of the
1566 supplementalCredentials attribute.
1567 See the 'password hash gpg key ids' option in
1570 The '--decrypt-samba-gpg' option triggers decryption of the
1571 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1572 in your environment or not (the python-gpgme package is required). Please
1573 note that you might need to set the GNUPGHOME environment variable. If the
1574 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1575 environment variable has been set correctly and the passphrase is already
1576 known by the gpg-agent.
1578 The '--script' option specifies a custom script that is called whenever any
1579 of the dirsyncAttributes (see below) was changed. The script is called
1580 without any arguments. It gets the LDIF for exactly one object on STDIN.
1581 If the script processed the object successfully it has to respond with a
1582 single line starting with 'DONE-EXIT: ' followed by an optional message.
1584 Note that the script might be called without any password change, e.g. if
1585 the account was disabled (a userAccountControl change) or the
1586 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1587 are always returned as unique identifier of the account. It might be useful
1588 to also ask for non-password attributes like: objectSid, sAMAccountName,
1589 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1590 Depending on the object, some attributes may not be present/available,
1591 but you always get the current state (and not a diff).
1593 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1596 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1597 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1598 (!(sAMAccountName=krbtgt*)))
1599 This means only normal (non-krbtgt) user
1600 accounts are monitored. The '--filter' can modify that, e.g. if it's
1601 required to also sync computer accounts.
1607 This (default) mode runs in an endless loop waiting for password related
1608 changes in the active directory database. It makes use of the
1609 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1610 get changes in a reliable fashion. Objects are monitored for changes of the
1611 following dirsyncAttributes:
1613 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1614 userPrincipalName and userAccountControl.
1616 It recovers from LDAP disconnects and updates the cache in conservative way
1617 (in single steps after each successfully processed change). An error from
1618 the script (specified by '--script') will result in fatal error and this
1619 command will exit. But the cache state should be still valid and can be
1620 resumed in the next "Sync Loop Run".
1622 The '--logfile' option specifies an optional (required if '--daemon' is
1623 specified) logfile that takes all output of the command. The logfile is
1624 automatically reopened if fstat returns st_nlink == 0.
1626 The optional '--daemon' option will put the command into the background.
1628 You can stop the command without the '--daemon' option, also by hitting
1631 If you specify the '--no-wait' option the command skips the
1632 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1633 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1638 In order to terminate an already running command (likely as daemon) the
1639 '--terminate' option can be used. This also requires the '--logfile' option
1644 samba-tool user syncpasswords --cache-ldb-initialize \\
1645 --attributes=virtualClearTextUTF8
1646 samba-tool user syncpasswords
1649 samba-tool user syncpasswords --cache-ldb-initialize \\
1650 --attributes=objectGUID,objectSID,sAMAccountName,\\
1651 userPrincipalName,userAccountControl,pwdLastSet,\\
1652 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1653 --script=/path/to/my-custom-syncpasswords-script.py
1654 samba-tool user syncpasswords --daemon \\
1655 --logfile=/var/log/samba/user-syncpasswords.log
1656 samba-tool user syncpasswords --terminate \\
1657 --logfile=/var/log/samba/user-syncpasswords.log
1661 super(cmd_user_syncpasswords, self).__init__()
1663 synopsis = "%prog [--cache-ldb-initialize] [options]"
1665 takes_optiongroups = {
1666 "sambaopts": options.SambaOptions,
1667 "versionopts": options.VersionOptions,
1671 Option("--cache-ldb-initialize",
1672 help="Initialize the cache for the first time",
1673 dest="cache_ldb_initialize", action="store_true"),
1674 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1675 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1676 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1677 metavar="URL", dest="H"),
1678 Option("--filter", help="optional LDAP filter to set password on", type=str,
1679 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1680 Option("--attributes", type=str,
1681 help=virtual_attributes_help,
1682 metavar="ATTRIBUTELIST", dest="attributes"),
1683 Option("--decrypt-samba-gpg",
1684 help=decrypt_samba_gpg_help,
1685 action="store_true", default=False, dest="decrypt_samba_gpg"),
1686 Option("--script", help="Script that is called for each password change", type=str,
1687 metavar="/path/to/syncpasswords.script", dest="script"),
1688 Option("--no-wait", help="Don't block waiting for changes",
1689 action="store_true", default=False, dest="nowait"),
1690 Option("--logfile", type=str,
1691 help="The logfile to use (required in --daemon mode).",
1692 metavar="/path/to/syncpasswords.log", dest="logfile"),
1693 Option("--daemon", help="daemonize after initial setup",
1694 action="store_true", default=False, dest="daemon"),
1695 Option("--terminate",
1696 help="Send a SIGTERM to an already running (daemon) process",
1697 action="store_true", default=False, dest="terminate"),
1700 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1701 H=None, filter=None,
1702 attributes=None, decrypt_samba_gpg=None,
1703 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1704 sambaopts=None, versionopts=None):
1706 self.lp = sambaopts.get_loadparm()
1708 self.samdb_url = None
1712 if not cache_ldb_initialize:
1713 if attributes is not None:
1714 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1715 if decrypt_samba_gpg:
1716 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1717 if script is not None:
1718 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1719 if filter is not None:
1720 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1722 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1724 if nowait is not False:
1725 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1726 if logfile is not None:
1727 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1728 if daemon is not False:
1729 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1730 if terminate is not False:
1731 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1735 raise CommandError("--daemon is not allowed together with --no-wait")
1736 if terminate is not False:
1737 raise CommandError("--terminate is not allowed together with --no-wait")
1739 if terminate is True and daemon is True:
1740 raise CommandError("--terminate is not allowed together with --daemon")
1742 if daemon is True and logfile is None:
1743 raise CommandError("--daemon is only allowed together with --logfile")
1745 if terminate is True and logfile is None:
1746 raise CommandError("--terminate is only allowed together with --logfile")
1748 if script is not None:
1749 if not os.path.exists(script):
1750 raise CommandError("script[%s] does not exist!" % script)
1752 sync_command = "%s" % os.path.abspath(script)
1756 dirsync_filter = filter
1757 if dirsync_filter is None:
1758 dirsync_filter = "(&" + \
1759 "(objectClass=user)" + \
1760 "(userAccountControl:%s:=%u)" % (
1761 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1762 "(!(sAMAccountName=krbtgt*))" + \
1765 dirsync_secret_attrs = [
1768 "supplementalCredentials",
1771 dirsync_attrs = dirsync_secret_attrs + [
1774 "userPrincipalName",
1775 "userAccountControl",
1780 password_attrs = None
1782 if cache_ldb_initialize:
1784 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1786 if decrypt_samba_gpg and not gpgme_support:
1787 raise CommandError(decrypt_samba_gpg_help)
1789 password_attrs = self.parse_attributes(attributes)
1790 lower_attrs = [x.lower() for x in password_attrs]
1791 # We always return these in order to track deletions
1792 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1793 if a.lower() not in lower_attrs:
1794 password_attrs += [a]
1796 if cache_ldb is not None:
1797 if cache_ldb.lower().startswith("ldapi://"):
1798 raise CommandError("--cache_ldb ldapi:// is not supported")
1799 elif cache_ldb.lower().startswith("ldap://"):
1800 raise CommandError("--cache_ldb ldap:// is not supported")
1801 elif cache_ldb.lower().startswith("ldaps://"):
1802 raise CommandError("--cache_ldb ldaps:// is not supported")
1803 elif cache_ldb.lower().startswith("tdb://"):
1806 if not os.path.exists(cache_ldb):
1807 cache_ldb = self.lp.private_path(cache_ldb)
1809 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1811 self.lockfile = "%s.pid" % cache_ldb
1814 if self.logfile is not None:
1816 if info.st_nlink == 0:
1817 logfile = self.logfile
1819 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1820 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
1825 log_msg("Reopened logfile[%s]\n" % (logfile))
1826 self.logfile = logfile
1827 msg = "%s: pid[%d]: %s" % (
1831 self.outf.write(msg)
1840 "passwordAttribute",
1846 self.cache = Ldb(cache_ldb)
1847 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1848 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1852 self.samdb_url = res[0]["samdbUrl"][0]
1853 except KeyError as e:
1854 self.samdb_url = None
1856 self.samdb_url = None
1857 if self.samdb_url is None and not cache_ldb_initialize:
1858 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1860 if self.samdb_url is not None and cache_ldb_initialize:
1861 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1863 if self.samdb_url is None:
1865 self.dirsync_filter = dirsync_filter
1866 self.dirsync_attrs = dirsync_attrs
1867 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"];
1868 self.password_attrs = password_attrs
1869 self.decrypt_samba_gpg = decrypt_samba_gpg
1870 self.sync_command = sync_command
1871 add_ldif = "dn: %s\n" % self.cache_dn
1872 add_ldif += "objectClass: userSyncPasswords\n"
1873 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url).decode('utf8')
1874 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter).decode('utf8')
1875 for a in self.dirsync_attrs:
1876 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a).decode('utf8')
1877 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1878 for a in self.password_attrs:
1879 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a).decode('utf8')
1880 if self.decrypt_samba_gpg == True:
1881 add_ldif += "decryptSambaGPG: TRUE\n"
1883 add_ldif += "decryptSambaGPG: FALSE\n"
1884 if self.sync_command is not None:
1885 add_ldif += "syncCommand: %s\n" % self.sync_command
1886 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1887 self.cache.add_ldif(add_ldif)
1888 self.current_pid = None
1889 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1890 msgs = self.cache.parse_ldif(add_ldif)
1891 changetype, msg = next(msgs)
1892 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1893 self.outf.write("%s" % ldif)
1895 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1896 self.dirsync_attrs = []
1897 for a in res[0]["dirsyncAttribute"]:
1898 self.dirsync_attrs.append(a)
1899 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1900 self.password_attrs = []
1901 for a in res[0]["passwordAttribute"]:
1902 self.password_attrs.append(a)
1903 decrypt_string = res[0]["decryptSambaGPG"][0]
1904 assert(decrypt_string in ["TRUE", "FALSE"])
1905 if decrypt_string == "TRUE":
1906 self.decrypt_samba_gpg = True
1908 self.decrypt_samba_gpg = False
1909 if "syncCommand" in res[0]:
1910 self.sync_command = res[0]["syncCommand"][0]
1912 self.sync_command = None
1913 if "currentPid" in res[0]:
1914 self.current_pid = int(res[0]["currentPid"][0])
1916 self.current_pid = None
1917 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1921 def run_sync_command(dn, ldif):
1922 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1923 sync_command_p = Popen(self.sync_command,
1928 res = sync_command_p.poll()
1931 input = "%s" % (ldif)
1932 reply = sync_command_p.communicate(input)[0]
1933 log_msg("%s\n" % (reply))
1934 res = sync_command_p.poll()
1936 sync_command_p.terminate()
1937 res = sync_command_p.wait()
1939 if reply.startswith("DONE-EXIT: "):
1942 log_msg("RESULT: %s\n" % (res))
1943 raise Exception("ERROR: %s - %s\n" % (res, reply))
1945 def handle_object(idx, dirsync_obj):
1946 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1947 guid = ndr_unpack(misc.GUID, binary_guid)
1948 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1949 sid = ndr_unpack(security.dom_sid, binary_sid)
1950 domain_sid, rid = sid.split()
1951 if rid == security.DOMAIN_RID_KRBTGT:
1952 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1954 for a in list(dirsync_obj.keys()):
1955 for h in dirsync_secret_attrs:
1956 if a.lower() == h.lower():
1958 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1959 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1960 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
1961 obj = self.get_account_attributes(self.samdb,
1962 username="%s" % sid,
1963 basedn="<GUID=%s>" % guid,
1964 filter="(objectClass=user)",
1965 scope=ldb.SCOPE_BASE,
1966 attrs=self.password_attrs,
1967 decrypt=self.decrypt_samba_gpg)
1968 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1969 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1970 if self.sync_command is None:
1971 self.outf.write("%s" % (ldif))
1973 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1974 run_sync_command(obj.dn, ldif)
1976 def check_current_pid_conflict(terminate):
1982 self.lockfd = os.open(self.lockfile, flags, 0o600)
1983 except IOError as e4:
1984 (err, msg) = e4.args
1985 if err == errno.ENOENT:
1988 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1989 (self.lockfile, msg, err))
1992 got_exclusive = False
1994 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1995 got_exclusive = True
1996 except IOError as e5:
1997 (err, msg) = e5.args
1998 if err != errno.EACCES and err != errno.EAGAIN:
1999 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
2000 (self.lockfile, msg, err))
2003 if not got_exclusive:
2004 buf = os.read(self.lockfd, 64)
2005 self.current_pid = None
2007 self.current_pid = int(buf)
2008 except ValueError as e:
2010 if self.current_pid is not None:
2013 if got_exclusive and terminate:
2015 os.ftruncate(self.lockfd, 0)
2016 except IOError as e2:
2017 (err, msg) = e2.args
2018 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2019 (self.lockfile, msg, err))
2021 os.close(self.lockfd)
2026 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2027 except IOError as e6:
2028 (err, msg) = e6.args
2029 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2030 (self.lockfile, msg, err))
2032 # We leave the function with the shared lock.
2035 def update_pid(pid):
2036 if self.lockfd != -1:
2037 got_exclusive = False
2038 # Try 5 times to get the exclusiv lock.
2039 for i in range(0, 5):
2041 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2042 got_exclusive = True
2043 except IOError as e:
2045 if err != errno.EACCES and err != errno.EAGAIN:
2046 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2047 (pid, self.lockfile, msg, err))
2052 if not got_exclusive:
2053 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
2054 (pid, self.lockfile))
2055 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2056 (pid, self.lockfile))
2063 os.ftruncate(self.lockfd, 0)
2065 os.write(self.lockfd, buf)
2066 except IOError as e3:
2067 (err, msg) = e3.args
2068 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2069 (self.lockfile, msg, err))
2071 self.current_pid = pid
2072 if self.current_pid is not None:
2073 log_msg("currentPid: %d\n" % self.current_pid)
2075 modify_ldif = "dn: %s\n" % (self.cache_dn)
2076 modify_ldif += "changetype: modify\n"
2077 modify_ldif += "replace: currentPid\n"
2078 if self.current_pid is not None:
2079 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2080 modify_ldif += "replace: currentTime\n"
2081 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2082 self.cache.modify_ldif(modify_ldif)
2085 def update_cache(res_controls):
2086 assert len(res_controls) > 0
2087 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2088 res_controls[0].critical = True
2089 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2090 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2092 modify_ldif = "dn: %s\n" % (self.cache_dn)
2093 modify_ldif += "changetype: modify\n"
2094 modify_ldif += "replace: dirsyncControl\n"
2095 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2096 modify_ldif += "replace: currentTime\n"
2097 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2098 self.cache.modify_ldif(modify_ldif)
2101 def check_object(dirsync_obj, res_controls):
2102 assert len(res_controls) > 0
2103 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2105 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2106 sid = ndr_unpack(security.dom_sid, binary_sid)
2108 lastCookie = str(res_controls[0])
2110 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2111 expression="(lastCookie=%s)" % (
2112 ldb.binary_encode(lastCookie)),
2118 def update_object(dirsync_obj, res_controls):
2119 assert len(res_controls) > 0
2120 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2122 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2123 sid = ndr_unpack(security.dom_sid, binary_sid)
2125 lastCookie = str(res_controls[0])
2127 self.cache.transaction_start()
2129 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2130 expression="(objectClass=*)",
2131 attrs=["lastCookie"])
2133 add_ldif = "dn: %s\n" % (dn)
2134 add_ldif += "objectClass: userCookie\n"
2135 add_ldif += "lastCookie: %s\n" % (lastCookie)
2136 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2137 self.cache.add_ldif(add_ldif)
2139 modify_ldif = "dn: %s\n" % (dn)
2140 modify_ldif += "changetype: modify\n"
2141 modify_ldif += "replace: lastCookie\n"
2142 modify_ldif += "lastCookie: %s\n" % (lastCookie)
2143 modify_ldif += "replace: currentTime\n"
2144 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2145 self.cache.modify_ldif(modify_ldif)
2146 self.cache.transaction_commit()
2147 except Exception as e:
2148 self.cache.transaction_cancel()
2154 res = self.samdb.search(expression=self.dirsync_filter,
2155 scope=ldb.SCOPE_SUBTREE,
2156 attrs=self.dirsync_attrs,
2157 controls=self.dirsync_controls)
2158 log_msg("dirsync_loop(): results %d\n" % len(res))
2161 done = check_object(r, res.controls)
2163 handle_object(ri, r)
2164 update_object(r, res.controls)
2166 update_cache(res.controls)
2170 def sync_loop(wait):
2171 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2172 notify_controls = ["notification:1", "show_recycled:1"]
2173 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2174 scope=ldb.SCOPE_SUBTREE,
2176 controls=notify_controls,
2180 log_msg("Resuming monitoring\n")
2182 log_msg("Getting changes\n")
2183 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2184 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2185 self.outf.write("syncCommand: %s\n" % self.sync_command)
2188 if wait is not True:
2191 for msg in notify_handle:
2192 if not isinstance(msg, ldb.Message):
2193 self.outf.write("referal: %s\n" % msg)
2195 created = msg.get("uSNCreated")[0]
2196 changed = msg.get("uSNChanged")[0]
2197 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2198 (msg.dn, created, changed))
2202 res = notify_handle.result()
2207 orig_pid = os.getpid()
2212 if pid == 0: # Actual daemon
2214 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2219 if cache_ldb_initialize:
2221 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2226 if logfile is not None:
2227 import resource # Resource usage information.
2228 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2229 if maxfd == resource.RLIM_INFINITY:
2230 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2231 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2232 self.outf.write("Using logfile[%s]\n" % logfile)
2233 for fd in range(0, maxfd):
2244 log_msg("Attached to logfile[%s]\n" % (logfile))
2245 self.logfile = logfile
2248 conflict = check_current_pid_conflict(terminate)
2250 if self.current_pid is None:
2251 log_msg("No process running.\n")
2254 log_msg("Proccess %d is not running anymore.\n" % (
2258 log_msg("Sending SIGTERM to proccess %d.\n" % (
2260 os.kill(self.current_pid, signal.SIGTERM)
2263 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2264 os.getpid(), self.current_pid))
2268 update_pid(os.getpid())
2273 retry_sleep_max = 600
2278 retry_sleep = retry_sleep_min
2280 while self.samdb is None:
2281 if retry_sleep != 0:
2282 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2283 time.sleep(retry_sleep)
2284 retry_sleep = retry_sleep * 2
2285 if retry_sleep >= retry_sleep_max:
2286 retry_sleep = retry_sleep_max
2287 log_msg("Connecting to '%s'\n" % self.samdb_url)
2289 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2290 except Exception as msg:
2292 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2293 if wait is not True:
2298 except ldb.LdbError as e7:
2299 (enum, estr) = e7.args
2301 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2307 class cmd_user_edit(Command):
2308 """Modify User AD object.
2310 This command will allow editing of a user account in the Active Directory
2311 domain. You will then be able to add or change attributes and their values.
2313 The username specified on the command is the sAMAccountName.
2315 The command may be run from the root userid or another authorized userid.
2317 The -H or --URL= option can be used to execute the command against a remote
2321 samba-tool user edit User1 -H ldap://samba.samdom.example.com \
2322 -U administrator --password=passw1rd
2324 Example1 shows how to edit a users attributes in the domain against a remote
2327 The -H parameter is used to specify the remote target server.
2330 samba-tool user edit User2
2332 Example2 shows how to edit a users attributes in the domain against a local
2336 samba-tool user edit User3 --editor=nano
2338 Example3 shows how to edit a users attributes in the domain against a local
2339 LDAP server using the 'nano' editor.
2342 synopsis = "%prog <username> [options]"
2345 Option("-H", "--URL", help="LDB URL for database or target server",
2346 type=str, metavar="URL", dest="H"),
2347 Option("--editor", help="Editor to use instead of the system default,"
2348 " or 'vi' if no system default is set.", type=str),
2351 takes_args = ["username"]
2352 takes_optiongroups = {
2353 "sambaopts": options.SambaOptions,
2354 "credopts": options.CredentialsOptions,
2355 "versionopts": options.VersionOptions,
2358 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2359 H=None, editor=None):
2361 lp = sambaopts.get_loadparm()
2362 creds = credopts.get_credentials(lp, fallback_machine=True)
2363 samdb = SamDB(url=H, session_info=system_session(),
2364 credentials=creds, lp=lp)
2366 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2367 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2369 domaindn = samdb.domain_dn()
2372 res = samdb.search(base=domaindn,
2374 scope=ldb.SCOPE_SUBTREE)
2377 raise CommandError('Unable to find user "%s"' % (username))
2380 r_ldif = samdb.write_ldif(msg, 1)
2381 # remove 'changetype' line
2382 result_ldif = re.sub('changetype: add\n', '', r_ldif)
2385 editor = os.environ.get('EDITOR')
2389 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2390 t_file.write(result_ldif)
2393 check_call([editor, t_file.name])
2394 except CalledProcessError as e:
2395 raise CalledProcessError("ERROR: ", e)
2396 with open(t_file.name) as edited_file:
2397 edited_message = edited_file.read()
2399 if result_ldif != edited_message:
2400 diff = difflib.ndiff(result_ldif.splitlines(),
2401 edited_message.splitlines())
2405 if line.startswith('-'):
2407 minus_lines.append(line)
2408 elif line.startswith('+'):
2410 plus_lines.append(line)
2412 user_ldif = "dn: %s\n" % user_dn
2413 user_ldif += "changetype: modify\n"
2415 for line in minus_lines:
2416 attr, val = line.split(':', 1)
2417 search_attr = "%s:" % attr
2418 if not re.search(r'^' + search_attr, str(plus_lines)):
2419 user_ldif += "delete: %s\n" % attr
2420 user_ldif += "%s: %s\n" % (attr, val)
2422 for line in plus_lines:
2423 attr, val = line.split(':', 1)
2424 search_attr = "%s:" % attr
2425 if re.search(r'^' + search_attr, str(minus_lines)):
2426 user_ldif += "replace: %s\n" % attr
2427 user_ldif += "%s: %s\n" % (attr, val)
2428 if not re.search(r'^' + search_attr, str(minus_lines)):
2429 user_ldif += "add: %s\n" % attr
2430 user_ldif += "%s: %s\n" % (attr, val)
2433 samdb.modify_ldif(user_ldif)
2434 except Exception as e:
2435 raise CommandError("Failed to modify user '%s': " %
2438 self.outf.write("Modified User '%s' successfully\n" % username)
2441 class cmd_user_show(Command):
2442 """Display a user AD object.
2444 This command displays a user account and it's attributes in the Active
2446 The username specified on the command is the sAMAccountName.
2448 The command may be run from the root userid or another authorized userid.
2450 The -H or --URL= option can be used to execute the command against a remote
2454 samba-tool user show User1 -H ldap://samba.samdom.example.com \
2455 -U administrator --password=passw1rd
2457 Example1 shows how to display a users attributes in the domain against a remote
2460 The -H parameter is used to specify the remote target server.
2463 samba-tool user show User2
2465 Example2 shows how to display a users attributes in the domain against a local
2469 samba-tool user show User2 --attributes=objectSid,memberOf
2471 Example3 shows how to display a users objectSid and memberOf attributes.
2473 synopsis = "%prog <username> [options]"
2476 Option("-H", "--URL", help="LDB URL for database or target server",
2477 type=str, metavar="URL", dest="H"),
2478 Option("--attributes",
2479 help=("Comma separated list of attributes, "
2480 "which will be printed."),
2481 type=str, dest="user_attrs"),
2484 takes_args = ["username"]
2485 takes_optiongroups = {
2486 "sambaopts": options.SambaOptions,
2487 "credopts": options.CredentialsOptions,
2488 "versionopts": options.VersionOptions,
2491 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2492 H=None, user_attrs=None):
2494 lp = sambaopts.get_loadparm()
2495 creds = credopts.get_credentials(lp, fallback_machine=True)
2496 samdb = SamDB(url=H, session_info=system_session(),
2497 credentials=creds, lp=lp)
2501 attrs = user_attrs.split(",")
2503 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2504 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2506 domaindn = samdb.domain_dn()
2509 res = samdb.search(base=domaindn, expression=filter,
2510 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2513 raise CommandError('Unable to find user "%s"' % (username))
2516 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2517 self.outf.write(user_ldif)
2520 class cmd_user_move(Command):
2521 """Move a user to an organizational unit/container.
2523 This command moves a user account into the specified organizational unit
2525 The username specified on the command is the sAMAccountName.
2526 The name of the organizational unit or container can be specified as a
2527 full DN or without the domainDN component.
2529 The command may be run from the root userid or another authorized userid.
2531 The -H or --URL= option can be used to execute the command against a remote
2535 samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
2536 -H ldap://samba.samdom.example.com -U administrator
2538 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2539 unit on a remote LDAP server.
2541 The -H parameter is used to specify the remote target server.
2544 samba-tool user move User1 CN=Users
2546 Example2 shows how to move a user User1 back into the CN=Users container
2547 on the local server.
2550 synopsis = "%prog <username> <new_parent_dn> [options]"
2553 Option("-H", "--URL", help="LDB URL for database or target server",
2554 type=str, metavar="URL", dest="H"),
2557 takes_args = ["username", "new_parent_dn"]
2558 takes_optiongroups = {
2559 "sambaopts": options.SambaOptions,
2560 "credopts": options.CredentialsOptions,
2561 "versionopts": options.VersionOptions,
2564 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2565 versionopts=None, H=None):
2566 lp = sambaopts.get_loadparm()
2567 creds = credopts.get_credentials(lp, fallback_machine=True)
2568 samdb = SamDB(url=H, session_info=system_session(),
2569 credentials=creds, lp=lp)
2570 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2572 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2573 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2575 res = samdb.search(base=domain_dn,
2577 scope=ldb.SCOPE_SUBTREE)
2580 raise CommandError('Unable to find user "%s"' % (username))
2583 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2584 except Exception as e:
2585 raise CommandError('Invalid new_parent_dn "%s": %s' %
2586 (new_parent_dn, e.message))
2588 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2589 full_new_user_dn.remove_base_components(len(user_dn) - 1)
2590 full_new_user_dn.add_base(full_new_parent_dn)
2593 samdb.rename(user_dn, full_new_user_dn)
2594 except Exception as e:
2595 raise CommandError('Failed to move user "%s"' % username, e)
2596 self.outf.write('Moved user "%s" into "%s"\n' %
2597 (username, full_new_parent_dn))
2600 class cmd_user(SuperCommand):
2601 """User management."""
2604 subcommands["add"] = cmd_user_add()
2605 subcommands["create"] = cmd_user_create()
2606 subcommands["delete"] = cmd_user_delete()
2607 subcommands["disable"] = cmd_user_disable()
2608 subcommands["enable"] = cmd_user_enable()
2609 subcommands["list"] = cmd_user_list()
2610 subcommands["setexpiry"] = cmd_user_setexpiry()
2611 subcommands["password"] = cmd_user_password()
2612 subcommands["setpassword"] = cmd_user_setpassword()
2613 subcommands["getpassword"] = cmd_user_getpassword()
2614 subcommands["syncpasswords"] = cmd_user_syncpasswords()
2615 subcommands["edit"] = cmd_user_edit()
2616 subcommands["show"] = cmd_user_show()
2617 subcommands["move"] = cmd_user_move()