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]"
537 takes_optiongroups = {
538 "sambaopts": options.SambaOptions,
539 "versionopts": options.VersionOptions,
540 "credopts": options.CredentialsOptions,
544 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
545 metavar="URL", dest="H"),
546 Option("--filter", help="LDAP Filter to set password on", type=str),
549 takes_args = ["username?"]
551 def run(self, username=None, sambaopts=None, credopts=None,
552 versionopts=None, filter=None, H=None):
553 if username is None and filter is None:
554 raise CommandError("Either the username or '--filter' must be specified!")
557 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
559 lp = sambaopts.get_loadparm()
560 creds = credopts.get_credentials(lp, fallback_machine=True)
562 samdb = SamDB(url=H, session_info=system_session(),
563 credentials=creds, lp=lp)
565 samdb.enable_account(filter)
566 except Exception as msg:
567 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
568 self.outf.write("Enabled user '%s'\n" % (username or filter))
571 class cmd_user_disable(Command):
572 """Disable a user."""
574 synopsis = "%prog (<username>|--filter <filter>) [options]"
577 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
578 metavar="URL", dest="H"),
579 Option("--filter", help="LDAP Filter to set password on", type=str),
582 takes_args = ["username?"]
584 takes_optiongroups = {
585 "sambaopts": options.SambaOptions,
586 "credopts": options.CredentialsOptions,
587 "versionopts": options.VersionOptions,
590 def run(self, username=None, sambaopts=None, credopts=None,
591 versionopts=None, filter=None, H=None):
592 if username is None and filter is None:
593 raise CommandError("Either the username or '--filter' must be specified!")
596 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
598 lp = sambaopts.get_loadparm()
599 creds = credopts.get_credentials(lp, fallback_machine=True)
601 samdb = SamDB(url=H, session_info=system_session(),
602 credentials=creds, lp=lp)
604 samdb.disable_account(filter)
605 except Exception as msg:
606 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
609 class cmd_user_setexpiry(Command):
610 """Set the expiration of a user account.
612 The user can either be specified by their sAMAccountName or using the --filter option.
614 When a user account expires, it becomes disabled and the user is unable to logon. The administrator may issue the samba-tool user enable command to enable the account for logon. The permissions and memberships associated with the account are retained when the account is enabled.
616 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command on a remote server.
619 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
621 Example1 shows how to set the expiration of an account in a remote LDAP server. The --URL parameter is used to specify the remote target server. The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to update that server.
624 sudo samba-tool user setexpiry User2 --noexpiry
626 Example2 shows how to set the account expiration of user User2 so it will never expire. The user in this example resides on the local server. sudo is used so a user may run the command as root.
629 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
631 Example3 shows how to set the account expiration date to end of day 20 days from the current day. The username or sAMAccountName is specified using the --filter= parameter and the username in this example is User3.
634 samba-tool user setexpiry --noexpiry User4
635 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
638 synopsis = "%prog (<username>|--filter <filter>) [options]"
640 takes_optiongroups = {
641 "sambaopts": options.SambaOptions,
642 "versionopts": options.VersionOptions,
643 "credopts": options.CredentialsOptions,
647 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
648 metavar="URL", dest="H"),
649 Option("--filter", help="LDAP Filter to set password on", type=str),
650 Option("--days", help="Days to expiry", type=int, default=0),
651 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
654 takes_args = ["username?"]
656 def run(self, username=None, sambaopts=None, credopts=None,
657 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
658 if username is None and filter is None:
659 raise CommandError("Either the username or '--filter' must be specified!")
662 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
664 lp = sambaopts.get_loadparm()
665 creds = credopts.get_credentials(lp)
667 samdb = SamDB(url=H, session_info=system_session(),
668 credentials=creds, lp=lp)
671 samdb.setexpiry(filter, days * 24 * 3600, no_expiry_req=noexpiry)
672 except Exception as msg:
673 # FIXME: Catch more specific exception
674 raise CommandError("Failed to set expiry for user '%s': %s" % (
675 username or filter, msg))
677 self.outf.write("Expiry for user '%s' disabled.\n" % (
680 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
681 username or filter, days))
684 class cmd_user_password(Command):
685 """Change password for a user account (the one provided in authentication).
688 synopsis = "%prog [options]"
691 Option("--newpassword", help="New password", type=str),
694 takes_optiongroups = {
695 "sambaopts": options.SambaOptions,
696 "credopts": options.CredentialsOptions,
697 "versionopts": options.VersionOptions,
700 def run(self, credopts=None, sambaopts=None, versionopts=None,
703 lp = sambaopts.get_loadparm()
704 creds = credopts.get_credentials(lp)
706 # get old password now, to get the password prompts in the right order
707 old_password = creds.get_password()
709 net = Net(creds, lp, server=credopts.ipaddress)
711 password = newpassword
713 if password is not None and password is not '':
715 password = getpass("New Password: ")
716 passwordverify = getpass("Retype Password: ")
717 if not password == passwordverify:
719 self.outf.write("Sorry, passwords do not match.\n")
722 if not isinstance(password, text_type):
723 password = password.decode('utf8')
724 net.change_password(password)
725 except Exception as msg:
726 # FIXME: catch more specific exception
727 raise CommandError("Failed to change password : %s" % msg)
728 self.outf.write("Changed password OK\n")
731 class cmd_user_setpassword(Command):
732 """Set or reset the password of a user account.
734 This command sets or resets the logon password for a user account. The username specified on the command is the sAMAccountName. The username may also be specified using the --filter option.
736 If the password is not specified on the command through the --newpassword parameter, the user is prompted for the password to be entered through the command line.
738 It is good security practice for the administrator to use the --must-change-at-next-login option which requires that when the user logs on to the account for the first time following the password change, he/she must change the password.
740 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server.
743 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
745 Example1 shows how to set the password of user TestUser1 on a remote LDAP server. The --URL parameter is used to specify the remote target server. The -U option is used to pass the username and password of a user that exists on the remote server and is authorized to update the server.
748 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
750 Example2 shows how an administrator would reset the TestUser2 user's password to passw0rd. The user is running under the root userid using the sudo command. In this example the user TestUser2 must change their password the next time they logon to the account.
753 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
755 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
758 synopsis = "%prog (<username>|--filter <filter>) [options]"
760 takes_optiongroups = {
761 "sambaopts": options.SambaOptions,
762 "versionopts": options.VersionOptions,
763 "credopts": options.CredentialsOptions,
767 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
768 metavar="URL", dest="H"),
769 Option("--filter", help="LDAP Filter to set password on", type=str),
770 Option("--newpassword", help="Set password", type=str),
771 Option("--must-change-at-next-login",
772 help="Force password to be changed on next login",
773 action="store_true"),
774 Option("--random-password",
775 help="Generate random password",
776 action="store_true"),
777 Option("--smartcard-required",
778 help="Require a smartcard for interactive logons",
779 action="store_true"),
780 Option("--clear-smartcard-required",
781 help="Don't require a smartcard for interactive logons",
782 action="store_true"),
785 takes_args = ["username?"]
787 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
788 versionopts=None, H=None, newpassword=None,
789 must_change_at_next_login=False, random_password=False,
790 smartcard_required=False, clear_smartcard_required=False):
791 if filter is None and username is None:
792 raise CommandError("Either the username or '--filter' must be specified!")
794 password = newpassword
796 if smartcard_required:
797 if password is not None and password is not '':
798 raise CommandError('It is not allowed to specify '
800 'together with --smartcard-required.')
801 if must_change_at_next_login:
802 raise CommandError('It is not allowed to specify '
803 '--must-change-at-next-login '
804 'together with --smartcard-required.')
805 if clear_smartcard_required:
806 raise CommandError('It is not allowed to specify '
807 '--clear-smartcard-required '
808 'together with --smartcard-required.')
810 if random_password and not smartcard_required:
811 password = generate_random_password(128, 255)
814 if smartcard_required:
816 if password is not None and password is not '':
818 password = getpass("New Password: ")
819 passwordverify = getpass("Retype Password: ")
820 if not password == passwordverify:
822 self.outf.write("Sorry, passwords do not match.\n")
825 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
827 lp = sambaopts.get_loadparm()
828 creds = credopts.get_credentials(lp)
830 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
832 samdb = SamDB(url=H, session_info=system_session(),
833 credentials=creds, lp=lp)
835 if smartcard_required:
838 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
839 flags = dsdb.UF_SMARTCARD_REQUIRED
840 samdb.toggle_userAccountFlags(filter, flags, on=True)
841 command = "Failed to enable account for user '%s'" % (username or filter)
842 samdb.enable_account(filter)
843 except Exception as msg:
844 # FIXME: catch more specific exception
845 raise CommandError("%s: %s" % (command, msg))
846 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
850 if clear_smartcard_required:
851 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
852 flags = dsdb.UF_SMARTCARD_REQUIRED
853 samdb.toggle_userAccountFlags(filter, flags, on=False)
854 command = "Failed to set password for user '%s'" % (username or filter)
855 samdb.setpassword(filter, password,
856 force_change_at_next_login=must_change_at_next_login,
858 except Exception as msg:
859 # FIXME: catch more specific exception
860 raise CommandError("%s: %s" % (command, msg))
861 self.outf.write("Changed password OK\n")
864 class GetPasswordCommand(Command):
867 super(GetPasswordCommand, self).__init__()
870 def connect_system_samdb(self, url, allow_local=False, verbose=False):
872 # using anonymous here, results in no authentication
873 # which means we can get system privileges via
874 # the privileged ldapi socket
875 creds = credentials.Credentials()
876 creds.set_anonymous()
878 if url is None and allow_local:
880 elif url.lower().startswith("ldapi://"):
882 elif url.lower().startswith("ldap://"):
883 raise CommandError("--url ldap:// is not supported for this command")
884 elif url.lower().startswith("ldaps://"):
885 raise CommandError("--url ldaps:// is not supported for this command")
886 elif not allow_local:
887 raise CommandError("--url requires an ldapi:// url for this command")
890 self.outf.write("Connecting to '%s'\n" % url)
892 samdb = SamDB(url=url, session_info=system_session(),
893 credentials=creds, lp=self.lp)
897 # Make sure we're connected as SYSTEM
899 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
901 sids = res[0].get("tokenGroups")
902 assert len(sids) == 1
903 sid = ndr_unpack(security.dom_sid, sids[0])
904 assert str(sid) == security.SID_NT_SYSTEM
905 except Exception as msg:
906 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
907 (security.SID_NT_SYSTEM))
909 # We use sort here in order to have a predictable processing order
910 # this might not be strictly needed, but also doesn't hurt here
911 for a in sorted(virtual_attributes.keys()):
912 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
913 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
917 def get_account_attributes(self, samdb, username, basedn, filter, scope,
924 (attr, _, opts) = a.partition(';')
926 attr_opts[attr] = opts
928 attr_opts[attr] = None
929 search_attrs.append(attr)
930 lower_attrs = [x.lower() for x in search_attrs]
932 require_supplementalCredentials = False
933 for a in virtual_attributes.keys():
934 if a.lower() in lower_attrs:
935 require_supplementalCredentials = True
936 add_supplementalCredentials = False
937 add_unicodePwd = False
938 if require_supplementalCredentials:
939 a = "supplementalCredentials"
940 if a.lower() not in lower_attrs:
942 add_supplementalCredentials = True
944 if a.lower() not in lower_attrs:
946 add_unicodePwd = True
947 add_sAMAcountName = False
949 if a.lower() not in lower_attrs:
951 add_sAMAcountName = True
953 add_userPrincipalName = False
954 upn = "usePrincipalName"
955 if upn.lower() not in lower_attrs:
956 search_attrs += [upn]
957 add_userPrincipalName = True
959 if scope == ldb.SCOPE_BASE:
960 search_controls = ["show_deleted:1", "show_recycled:1"]
964 res = samdb.search(base=basedn, expression=filter,
965 scope=scope, attrs=search_attrs,
966 controls=search_controls)
968 raise Exception('Unable to find user "%s"' % (username or filter))
970 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
971 except Exception as msg:
972 # FIXME: catch more specific exception
973 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
978 if "supplementalCredentials" in obj:
979 sc_blob = obj["supplementalCredentials"][0]
980 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
981 if add_supplementalCredentials:
982 del obj["supplementalCredentials"]
983 if "unicodePwd" in obj:
984 unicodePwd = obj["unicodePwd"][0]
986 del obj["unicodePwd"]
987 account_name = obj["sAMAccountName"][0]
988 if add_sAMAcountName:
989 del obj["sAMAccountName"]
990 if "userPrincipalName" in obj:
991 account_upn = obj["userPrincipalName"][0]
993 realm = self.lp.get("realm")
994 account_upn = "%s@%s" % (account_name, realm.lower())
995 if add_userPrincipalName:
996 del obj["userPrincipalName"]
999 def get_package(name, min_idx=0):
1000 if name in calculated:
1001 return calculated[name]
1005 min_idx = len(sc.sub.packages) + min_idx
1007 for p in sc.sub.packages:
1014 return binascii.a2b_hex(p.data)
1019 # Samba adds 'Primary:SambaGPG' at the end.
1020 # When Windows sets the password it keeps
1021 # 'Primary:SambaGPG' and rotates it to
1022 # the begining. So we can only use the value,
1023 # if it is the last one.
1025 # In order to get more protection we verify
1026 # the nthash of the decrypted utf16 password
1027 # against the stored nthash in unicodePwd.
1029 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1030 if sgv is not None and unicodePwd is not None:
1031 ctx = gpgme.Context()
1033 cipher_io = io.BytesIO(sgv)
1034 plain_io = io.BytesIO()
1036 ctx.decrypt(cipher_io, plain_io)
1037 cv = plain_io.getvalue()
1039 # We only use the password if it matches
1040 # the current nthash stored in the unicodePwd
1043 tmp = credentials.Credentials()
1045 tmp.set_utf16_password(cv)
1046 nthash = tmp.get_nt_hash()
1047 if nthash == unicodePwd:
1048 calculated["Primary:CLEARTEXT"] = cv
1049 except gpgme.GpgmeError as e1:
1050 (major, minor, msg) = e1.args
1051 if major == gpgme.ERR_BAD_SECKEY:
1052 msg = "ERR_BAD_SECKEY: " + msg
1054 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1055 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1056 username or account_name, msg))
1058 def get_utf8(a, b, username):
1060 u = unicode(b, 'utf-16-le')
1061 except UnicodeDecodeError as e:
1062 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1065 u8 = u.encode('utf-8')
1068 # Extract the WDigest hash for the value specified by i.
1069 # Builds an htdigest compatible value
1071 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1072 domain, dns_domain):
1077 user = account_name.lower()
1078 realm = domain.lower()
1080 user = account_name.upper()
1081 realm = domain.upper()
1084 realm = domain.upper()
1087 realm = domain.lower()
1089 user = account_name.upper()
1090 realm = domain.lower()
1092 user = account_name.lower()
1093 realm = domain.upper()
1096 realm = dns_domain.lower()
1098 user = account_name.lower()
1099 realm = dns_domain.lower()
1101 user = account_name.upper()
1102 realm = dns_domain.upper()
1105 realm = dns_domain.upper()
1108 realm = dns_domain.lower()
1110 user = account_name.upper()
1111 realm = dns_domain.lower()
1113 user = account_name.lower()
1114 realm = dns_domain.upper()
1119 user = account_upn.lower()
1122 user = account_upn.upper()
1125 user = "%s\\%s" % (domain, account_name)
1128 user = "%s\\%s" % (domain.lower(), account_name.lower())
1131 user = "%s\\%s" % (domain.upper(), account_name.upper())
1137 user = account_name.lower()
1140 user = account_name.upper()
1146 user = account_upn.lower()
1149 user = account_upn.upper()
1152 user = "%s\\%s" % (domain, account_name)
1155 # Differs from spec, see tests
1156 user = "%s\\%s" % (domain.lower(), account_name.lower())
1159 # Differs from spec, see tests
1160 user = "%s\\%s" % (domain.upper(), account_name.upper())
1165 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1168 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
1169 return "%s:%s:%s" % (user, realm, digest)
1174 # get the value for a virtualCrypt attribute.
1175 # look for an exact match on algorithm and rounds in supplemental creds
1176 # if not found calculate using Primary:CLEARTEXT
1177 # if no Primary:CLEARTEXT return the first supplementalCredential
1178 # that matches the algorithm.
1179 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1182 b = get_package("Primary:userPassword")
1184 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1186 # No exact match on algorithm and number of rounds
1187 # try and calculate one from the Primary:CLEARTEXT
1188 b = get_package("Primary:CLEARTEXT")
1190 u8 = get_utf8(a, b, username or account_name)
1192 sv = get_crypt_value(str(algorithm), u8, rounds)
1194 # Unable to calculate a hash with the specified
1195 # number of rounds, fall back to the first hash using
1196 # the specified algorithm
1200 return "{CRYPT}" + sv
1202 def get_userPassword_hash(blob, algorithm, rounds):
1203 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1206 # Check that the NT hash has not been changed without updating
1207 # the user password hashes. This indicates that password has been
1208 # changed without updating the supplemental credentials.
1209 if unicodePwd != bytearray(up.current_nt_hash.hash):
1212 scheme_prefix = "$%d$" % algorithm
1213 prefix = scheme_prefix
1215 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1219 if (scheme_match is None and
1220 h.scheme == SCHEME and
1221 h.value.startswith(scheme_prefix)):
1222 scheme_match = h.value
1223 if h.scheme == SCHEME and h.value.startswith(prefix):
1224 return (h.value, scheme_match)
1226 # No match on the number of rounds, return the value of the
1227 # first matching scheme
1228 return (None, scheme_match)
1230 # We use sort here in order to have a predictable processing order
1231 for a in sorted(virtual_attributes.keys()):
1232 if not a.lower() in lower_attrs:
1235 if a == "virtualClearTextUTF8":
1236 b = get_package("Primary:CLEARTEXT")
1239 u8 = get_utf8(a, b, username or account_name)
1243 elif a == "virtualClearTextUTF16":
1244 v = get_package("Primary:CLEARTEXT")
1247 elif a == "virtualSSHA":
1248 b = get_package("Primary:CLEARTEXT")
1251 u8 = get_utf8(a, b, username or account_name)
1254 salt = get_random_bytes(4)
1258 bv = h.digest() + salt
1259 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
1260 elif a == "virtualCryptSHA256":
1261 rounds = get_rounds(attr_opts[a])
1262 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1266 elif a == "virtualCryptSHA512":
1267 rounds = get_rounds(attr_opts[a])
1268 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1272 elif a == "virtualSambaGPG":
1273 # Samba adds 'Primary:SambaGPG' at the end.
1274 # When Windows sets the password it keeps
1275 # 'Primary:SambaGPG' and rotates it to
1276 # the begining. So we can only use the value,
1277 # if it is the last one.
1278 v = get_package("Primary:SambaGPG", min_idx=-1)
1281 elif a.startswith("virtualWDigest"):
1282 primary_wdigest = get_package("Primary:WDigest")
1283 if primary_wdigest is None:
1285 x = a[len("virtualWDigest"):]
1290 domain = self.lp.get("workgroup")
1291 dns_domain = samdb.domain_dns_name()
1292 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1297 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1300 def parse_attributes(self, attributes):
1302 if attributes is None:
1303 raise CommandError("Please specify --attributes")
1304 attrs = attributes.split(',')
1307 pa = pa.lstrip().rstrip()
1308 for da in disabled_virtual_attributes.keys():
1309 if pa.lower() == da.lower():
1310 r = disabled_virtual_attributes[da]["reason"]
1311 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1313 for va in virtual_attributes.keys():
1314 if pa.lower() == va.lower():
1315 # Take the real name
1318 password_attrs += [pa]
1320 return password_attrs
1323 class cmd_user_getpassword(GetPasswordCommand):
1324 """Get the password fields of a user/computer account.
1326 This command gets the logon password for a user/computer account.
1328 The username specified on the command is the sAMAccountName.
1329 The username may also be specified using the --filter option.
1331 The command must be run from the root user id or another authorized user id.
1332 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1333 used to adjust the local path. By default tdb:// is used by default.
1335 The '--attributes' parameter takes a comma separated list of attributes,
1336 which will be printed or given to the script specified by '--script'. If a
1337 specified attribute is not available on an object it's silently omitted.
1338 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1339 the NTHASH) and the following virtual attributes are possible (see --help
1340 for which virtual attributes are supported in your environment):
1342 virtualClearTextUTF16: The raw cleartext as stored in the
1343 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1344 with '--decrypt-samba-gpg') buffer inside of the
1345 supplementalCredentials attribute. This typically
1346 contains valid UTF-16-LE, but may contain random
1347 bytes, e.g. for computer accounts.
1349 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1350 (only from valid UTF-16-LE)
1352 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1353 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1355 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1356 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1357 with a $5$... salt, see crypt(3) on modern systems.
1358 The number of rounds used to calculate the hash can
1359 also be specified. By appending ";rounds=x" to the
1360 attribute name i.e. virtualCryptSHA256;rounds=10000
1361 will calculate a SHA256 hash with 10,000 rounds.
1362 non numeric values for rounds are silently ignored
1363 The value is calculated as follows:
1364 1) If a value exists in 'Primary:userPassword' with
1365 the specified number of rounds it is returned.
1366 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1367 '--decrypt-samba-gpg'. Calculate a hash with
1368 the specified number of rounds
1369 3) Return the first CryptSHA256 value in
1370 'Primary:userPassword'
1373 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1374 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1375 with a $6$... salt, see crypt(3) on modern systems.
1376 The number of rounds used to calculate the hash can
1377 also be specified. By appending ";rounds=x" to the
1378 attribute name i.e. virtualCryptSHA512;rounds=10000
1379 will calculate a SHA512 hash with 10,000 rounds.
1380 non numeric values for rounds are silently ignored
1381 The value is calculated as follows:
1382 1) If a value exists in 'Primary:userPassword' with
1383 the specified number of rounds it is returned.
1384 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1385 '--decrypt-samba-gpg'. Calculate a hash with
1386 the specified number of rounds
1387 3) Return the first CryptSHA512 value in
1388 'Primary:userPassword'
1390 virtualWDigestNN: The individual hash values stored in
1391 'Primary:WDigest' where NN is the hash number in
1393 NOTE: As at 22-05-2017 the documentation:
1394 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1395 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1398 virtualSambaGPG: The raw cleartext as stored in the
1399 'Primary:SambaGPG' buffer inside of the
1400 supplementalCredentials attribute.
1401 See the 'password hash gpg key ids' option in
1404 The '--decrypt-samba-gpg' option triggers decryption of the
1405 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1406 in your environment or not (the python-gpgme package is required). Please
1407 note that you might need to set the GNUPGHOME environment variable. If the
1408 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1409 environment variable has been set correctly and the passphrase is already
1410 known by the gpg-agent.
1413 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1416 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1420 super(cmd_user_getpassword, self).__init__()
1422 synopsis = "%prog (<username>|--filter <filter>) [options]"
1424 takes_optiongroups = {
1425 "sambaopts": options.SambaOptions,
1426 "versionopts": options.VersionOptions,
1430 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1431 metavar="URL", dest="H"),
1432 Option("--filter", help="LDAP Filter to set password on", type=str),
1433 Option("--attributes", type=str,
1434 help=virtual_attributes_help,
1435 metavar="ATTRIBUTELIST", dest="attributes"),
1436 Option("--decrypt-samba-gpg",
1437 help=decrypt_samba_gpg_help,
1438 action="store_true", default=False, dest="decrypt_samba_gpg"),
1441 takes_args = ["username?"]
1443 def run(self, username=None, H=None, filter=None,
1444 attributes=None, decrypt_samba_gpg=None,
1445 sambaopts=None, versionopts=None):
1446 self.lp = sambaopts.get_loadparm()
1448 if decrypt_samba_gpg and not gpgme_support:
1449 raise CommandError(decrypt_samba_gpg_help)
1451 if filter is None and username is None:
1452 raise CommandError("Either the username or '--filter' must be specified!")
1455 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1457 if attributes is None:
1458 raise CommandError("Please specify --attributes")
1460 password_attrs = self.parse_attributes(attributes)
1462 samdb = self.connect_system_samdb(url=H, allow_local=True)
1464 obj = self.get_account_attributes(samdb, username,
1467 scope=ldb.SCOPE_SUBTREE,
1468 attrs=password_attrs,
1469 decrypt=decrypt_samba_gpg)
1471 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1472 self.outf.write("%s" % ldif)
1473 self.outf.write("Got password OK\n")
1476 class cmd_user_syncpasswords(GetPasswordCommand):
1477 """Sync the password of user accounts.
1479 This syncs logon passwords for user accounts.
1481 Note that this command should run on a single domain controller only
1482 (typically the PDC-emulator). However the "password hash gpg key ids"
1483 option should to be configured on all domain controllers.
1485 The command must be run from the root user id or another authorized user id.
1486 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1487 local path. By default, ldapi:// is used with the default path to the
1488 privileged ldapi socket.
1490 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1491 "Sync Loop Terminate".
1494 Cache Initialization
1495 ====================
1497 The first time, this command needs to be called with
1498 '--cache-ldb-initialize' in order to initialize its cache.
1500 The cache initialization requires '--attributes' and allows the following
1501 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1504 The '--attributes' parameter takes a comma separated list of attributes,
1505 which will be printed or given to the script specified by '--script'. If a
1506 specified attribute is not available on an object it will be silently omitted.
1507 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1508 the NTHASH) and the following virtual attributes are possible (see '--help'
1509 for supported virtual attributes in your environment):
1511 virtualClearTextUTF16: The raw cleartext as stored in the
1512 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1513 with '--decrypt-samba-gpg') buffer inside of the
1514 supplementalCredentials attribute. This typically
1515 contains valid UTF-16-LE, but may contain random
1516 bytes, e.g. for computer accounts.
1518 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1519 (only from valid UTF-16-LE)
1521 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1522 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1524 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1525 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1526 with a $5$... salt, see crypt(3) on modern systems.
1527 The number of rounds used to calculate the hash can
1528 also be specified. By appending ";rounds=x" to the
1529 attribute name i.e. virtualCryptSHA256;rounds=10000
1530 will calculate a SHA256 hash with 10,000 rounds.
1531 non numeric values for rounds are silently ignored
1532 The value is calculated as follows:
1533 1) If a value exists in 'Primary:userPassword' with
1534 the specified number of rounds it is returned.
1535 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1536 '--decrypt-samba-gpg'. Calculate a hash with
1537 the specified number of rounds
1538 3) Return the first CryptSHA256 value in
1539 'Primary:userPassword'
1541 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1542 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1543 with a $6$... salt, see crypt(3) on modern systems.
1544 The number of rounds used to calculate the hash can
1545 also be specified. By appending ";rounds=x" to the
1546 attribute name i.e. virtualCryptSHA512;rounds=10000
1547 will calculate a SHA512 hash with 10,000 rounds.
1548 non numeric values for rounds are silently ignored
1549 The value is calculated as follows:
1550 1) If a value exists in 'Primary:userPassword' with
1551 the specified number of rounds it is returned.
1552 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1553 '--decrypt-samba-gpg'. Calculate a hash with
1554 the specified number of rounds
1555 3) Return the first CryptSHA512 value in
1556 'Primary:userPassword'
1558 virtualWDigestNN: The individual hash values stored in
1559 'Primary:WDigest' where NN is the hash number in
1561 NOTE: As at 22-05-2017 the documentation:
1562 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1563 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1566 virtualSambaGPG: The raw cleartext as stored in the
1567 'Primary:SambaGPG' buffer inside of the
1568 supplementalCredentials attribute.
1569 See the 'password hash gpg key ids' option in
1572 The '--decrypt-samba-gpg' option triggers decryption of the
1573 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1574 in your environment or not (the python-gpgme package is required). Please
1575 note that you might need to set the GNUPGHOME environment variable. If the
1576 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1577 environment variable has been set correctly and the passphrase is already
1578 known by the gpg-agent.
1580 The '--script' option specifies a custom script that is called whenever any
1581 of the dirsyncAttributes (see below) was changed. The script is called
1582 without any arguments. It gets the LDIF for exactly one object on STDIN.
1583 If the script processed the object successfully it has to respond with a
1584 single line starting with 'DONE-EXIT: ' followed by an optional message.
1586 Note that the script might be called without any password change, e.g. if
1587 the account was disabled (a userAccountControl change) or the
1588 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1589 are always returned as unique identifier of the account. It might be useful
1590 to also ask for non-password attributes like: objectSid, sAMAccountName,
1591 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1592 Depending on the object, some attributes may not be present/available,
1593 but you always get the current state (and not a diff).
1595 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1598 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1599 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1600 (!(sAMAccountName=krbtgt*)))
1601 This means only normal (non-krbtgt) user
1602 accounts are monitored. The '--filter' can modify that, e.g. if it's
1603 required to also sync computer accounts.
1609 This (default) mode runs in an endless loop waiting for password related
1610 changes in the active directory database. It makes use of the
1611 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1612 get changes in a reliable fashion. Objects are monitored for changes of the
1613 following dirsyncAttributes:
1615 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1616 userPrincipalName and userAccountControl.
1618 It recovers from LDAP disconnects and updates the cache in conservative way
1619 (in single steps after each successfully processed change). An error from
1620 the script (specified by '--script') will result in fatal error and this
1621 command will exit. But the cache state should be still valid and can be
1622 resumed in the next "Sync Loop Run".
1624 The '--logfile' option specifies an optional (required if '--daemon' is
1625 specified) logfile that takes all output of the command. The logfile is
1626 automatically reopened if fstat returns st_nlink == 0.
1628 The optional '--daemon' option will put the command into the background.
1630 You can stop the command without the '--daemon' option, also by hitting
1633 If you specify the '--no-wait' option the command skips the
1634 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1635 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1640 In order to terminate an already running command (likely as daemon) the
1641 '--terminate' option can be used. This also requires the '--logfile' option
1646 samba-tool user syncpasswords --cache-ldb-initialize \\
1647 --attributes=virtualClearTextUTF8
1648 samba-tool user syncpasswords
1651 samba-tool user syncpasswords --cache-ldb-initialize \\
1652 --attributes=objectGUID,objectSID,sAMAccountName,\\
1653 userPrincipalName,userAccountControl,pwdLastSet,\\
1654 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1655 --script=/path/to/my-custom-syncpasswords-script.py
1656 samba-tool user syncpasswords --daemon \\
1657 --logfile=/var/log/samba/user-syncpasswords.log
1658 samba-tool user syncpasswords --terminate \\
1659 --logfile=/var/log/samba/user-syncpasswords.log
1663 super(cmd_user_syncpasswords, self).__init__()
1665 synopsis = "%prog [--cache-ldb-initialize] [options]"
1667 takes_optiongroups = {
1668 "sambaopts": options.SambaOptions,
1669 "versionopts": options.VersionOptions,
1673 Option("--cache-ldb-initialize",
1674 help="Initialize the cache for the first time",
1675 dest="cache_ldb_initialize", action="store_true"),
1676 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1677 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1678 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1679 metavar="URL", dest="H"),
1680 Option("--filter", help="optional LDAP filter to set password on", type=str,
1681 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1682 Option("--attributes", type=str,
1683 help=virtual_attributes_help,
1684 metavar="ATTRIBUTELIST", dest="attributes"),
1685 Option("--decrypt-samba-gpg",
1686 help=decrypt_samba_gpg_help,
1687 action="store_true", default=False, dest="decrypt_samba_gpg"),
1688 Option("--script", help="Script that is called for each password change", type=str,
1689 metavar="/path/to/syncpasswords.script", dest="script"),
1690 Option("--no-wait", help="Don't block waiting for changes",
1691 action="store_true", default=False, dest="nowait"),
1692 Option("--logfile", type=str,
1693 help="The logfile to use (required in --daemon mode).",
1694 metavar="/path/to/syncpasswords.log", dest="logfile"),
1695 Option("--daemon", help="daemonize after initial setup",
1696 action="store_true", default=False, dest="daemon"),
1697 Option("--terminate",
1698 help="Send a SIGTERM to an already running (daemon) process",
1699 action="store_true", default=False, dest="terminate"),
1702 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1703 H=None, filter=None,
1704 attributes=None, decrypt_samba_gpg=None,
1705 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1706 sambaopts=None, versionopts=None):
1708 self.lp = sambaopts.get_loadparm()
1710 self.samdb_url = None
1714 if not cache_ldb_initialize:
1715 if attributes is not None:
1716 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1717 if decrypt_samba_gpg:
1718 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1719 if script is not None:
1720 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1721 if filter is not None:
1722 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1724 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1726 if nowait is not False:
1727 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1728 if logfile is not None:
1729 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1730 if daemon is not False:
1731 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1732 if terminate is not False:
1733 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1737 raise CommandError("--daemon is not allowed together with --no-wait")
1738 if terminate is not False:
1739 raise CommandError("--terminate is not allowed together with --no-wait")
1741 if terminate is True and daemon is True:
1742 raise CommandError("--terminate is not allowed together with --daemon")
1744 if daemon is True and logfile is None:
1745 raise CommandError("--daemon is only allowed together with --logfile")
1747 if terminate is True and logfile is None:
1748 raise CommandError("--terminate is only allowed together with --logfile")
1750 if script is not None:
1751 if not os.path.exists(script):
1752 raise CommandError("script[%s] does not exist!" % script)
1754 sync_command = "%s" % os.path.abspath(script)
1758 dirsync_filter = filter
1759 if dirsync_filter is None:
1760 dirsync_filter = "(&" + \
1761 "(objectClass=user)" + \
1762 "(userAccountControl:%s:=%u)" % (
1763 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1764 "(!(sAMAccountName=krbtgt*))" + \
1767 dirsync_secret_attrs = [
1770 "supplementalCredentials",
1773 dirsync_attrs = dirsync_secret_attrs + [
1776 "userPrincipalName",
1777 "userAccountControl",
1782 password_attrs = None
1784 if cache_ldb_initialize:
1786 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1788 if decrypt_samba_gpg and not gpgme_support:
1789 raise CommandError(decrypt_samba_gpg_help)
1791 password_attrs = self.parse_attributes(attributes)
1792 lower_attrs = [x.lower() for x in password_attrs]
1793 # We always return these in order to track deletions
1794 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1795 if a.lower() not in lower_attrs:
1796 password_attrs += [a]
1798 if cache_ldb is not None:
1799 if cache_ldb.lower().startswith("ldapi://"):
1800 raise CommandError("--cache_ldb ldapi:// is not supported")
1801 elif cache_ldb.lower().startswith("ldap://"):
1802 raise CommandError("--cache_ldb ldap:// is not supported")
1803 elif cache_ldb.lower().startswith("ldaps://"):
1804 raise CommandError("--cache_ldb ldaps:// is not supported")
1805 elif cache_ldb.lower().startswith("tdb://"):
1808 if not os.path.exists(cache_ldb):
1809 cache_ldb = self.lp.private_path(cache_ldb)
1811 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1813 self.lockfile = "%s.pid" % cache_ldb
1816 if self.logfile is not None:
1818 if info.st_nlink == 0:
1819 logfile = self.logfile
1821 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1822 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
1827 log_msg("Reopened logfile[%s]\n" % (logfile))
1828 self.logfile = logfile
1829 msg = "%s: pid[%d]: %s" % (
1833 self.outf.write(msg)
1842 "passwordAttribute",
1848 self.cache = Ldb(cache_ldb)
1849 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1850 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1854 self.samdb_url = res[0]["samdbUrl"][0]
1855 except KeyError as e:
1856 self.samdb_url = None
1858 self.samdb_url = None
1859 if self.samdb_url is None and not cache_ldb_initialize:
1860 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1862 if self.samdb_url is not None and cache_ldb_initialize:
1863 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1865 if self.samdb_url is None:
1867 self.dirsync_filter = dirsync_filter
1868 self.dirsync_attrs = dirsync_attrs
1869 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"];
1870 self.password_attrs = password_attrs
1871 self.decrypt_samba_gpg = decrypt_samba_gpg
1872 self.sync_command = sync_command
1873 add_ldif = "dn: %s\n" % self.cache_dn
1874 add_ldif += "objectClass: userSyncPasswords\n"
1875 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url).decode('utf8')
1876 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter).decode('utf8')
1877 for a in self.dirsync_attrs:
1878 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a).decode('utf8')
1879 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1880 for a in self.password_attrs:
1881 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a).decode('utf8')
1882 if self.decrypt_samba_gpg == True:
1883 add_ldif += "decryptSambaGPG: TRUE\n"
1885 add_ldif += "decryptSambaGPG: FALSE\n"
1886 if self.sync_command is not None:
1887 add_ldif += "syncCommand: %s\n" % self.sync_command
1888 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1889 self.cache.add_ldif(add_ldif)
1890 self.current_pid = None
1891 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1892 msgs = self.cache.parse_ldif(add_ldif)
1893 changetype, msg = next(msgs)
1894 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1895 self.outf.write("%s" % ldif)
1897 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1898 self.dirsync_attrs = []
1899 for a in res[0]["dirsyncAttribute"]:
1900 self.dirsync_attrs.append(a)
1901 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1902 self.password_attrs = []
1903 for a in res[0]["passwordAttribute"]:
1904 self.password_attrs.append(a)
1905 decrypt_string = res[0]["decryptSambaGPG"][0]
1906 assert(decrypt_string in ["TRUE", "FALSE"])
1907 if decrypt_string == "TRUE":
1908 self.decrypt_samba_gpg = True
1910 self.decrypt_samba_gpg = False
1911 if "syncCommand" in res[0]:
1912 self.sync_command = res[0]["syncCommand"][0]
1914 self.sync_command = None
1915 if "currentPid" in res[0]:
1916 self.current_pid = int(res[0]["currentPid"][0])
1918 self.current_pid = None
1919 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1923 def run_sync_command(dn, ldif):
1924 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1925 sync_command_p = Popen(self.sync_command,
1930 res = sync_command_p.poll()
1933 input = "%s" % (ldif)
1934 reply = sync_command_p.communicate(input)[0]
1935 log_msg("%s\n" % (reply))
1936 res = sync_command_p.poll()
1938 sync_command_p.terminate()
1939 res = sync_command_p.wait()
1941 if reply.startswith("DONE-EXIT: "):
1944 log_msg("RESULT: %s\n" % (res))
1945 raise Exception("ERROR: %s - %s\n" % (res, reply))
1947 def handle_object(idx, dirsync_obj):
1948 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1949 guid = ndr_unpack(misc.GUID, binary_guid)
1950 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1951 sid = ndr_unpack(security.dom_sid, binary_sid)
1952 domain_sid, rid = sid.split()
1953 if rid == security.DOMAIN_RID_KRBTGT:
1954 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1956 for a in list(dirsync_obj.keys()):
1957 for h in dirsync_secret_attrs:
1958 if a.lower() == h.lower():
1960 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1961 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1962 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
1963 obj = self.get_account_attributes(self.samdb,
1964 username="%s" % sid,
1965 basedn="<GUID=%s>" % guid,
1966 filter="(objectClass=user)",
1967 scope=ldb.SCOPE_BASE,
1968 attrs=self.password_attrs,
1969 decrypt=self.decrypt_samba_gpg)
1970 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1971 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1972 if self.sync_command is None:
1973 self.outf.write("%s" % (ldif))
1975 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1976 run_sync_command(obj.dn, ldif)
1978 def check_current_pid_conflict(terminate):
1984 self.lockfd = os.open(self.lockfile, flags, 0o600)
1985 except IOError as e4:
1986 (err, msg) = e4.args
1987 if err == errno.ENOENT:
1990 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1991 (self.lockfile, msg, err))
1994 got_exclusive = False
1996 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1997 got_exclusive = True
1998 except IOError as e5:
1999 (err, msg) = e5.args
2000 if err != errno.EACCES and err != errno.EAGAIN:
2001 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
2002 (self.lockfile, msg, err))
2005 if not got_exclusive:
2006 buf = os.read(self.lockfd, 64)
2007 self.current_pid = None
2009 self.current_pid = int(buf)
2010 except ValueError as e:
2012 if self.current_pid is not None:
2015 if got_exclusive and terminate:
2017 os.ftruncate(self.lockfd, 0)
2018 except IOError as e2:
2019 (err, msg) = e2.args
2020 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2021 (self.lockfile, msg, err))
2023 os.close(self.lockfd)
2028 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2029 except IOError as e6:
2030 (err, msg) = e6.args
2031 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2032 (self.lockfile, msg, err))
2034 # We leave the function with the shared lock.
2037 def update_pid(pid):
2038 if self.lockfd != -1:
2039 got_exclusive = False
2040 # Try 5 times to get the exclusiv lock.
2041 for i in range(0, 5):
2043 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2044 got_exclusive = True
2045 except IOError as e:
2047 if err != errno.EACCES and err != errno.EAGAIN:
2048 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2049 (pid, self.lockfile, msg, err))
2054 if not got_exclusive:
2055 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
2056 (pid, self.lockfile))
2057 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2058 (pid, self.lockfile))
2065 os.ftruncate(self.lockfd, 0)
2067 os.write(self.lockfd, buf)
2068 except IOError as e3:
2069 (err, msg) = e3.args
2070 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2071 (self.lockfile, msg, err))
2073 self.current_pid = pid
2074 if self.current_pid is not None:
2075 log_msg("currentPid: %d\n" % self.current_pid)
2077 modify_ldif = "dn: %s\n" % (self.cache_dn)
2078 modify_ldif += "changetype: modify\n"
2079 modify_ldif += "replace: currentPid\n"
2080 if self.current_pid is not None:
2081 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2082 modify_ldif += "replace: currentTime\n"
2083 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2084 self.cache.modify_ldif(modify_ldif)
2087 def update_cache(res_controls):
2088 assert len(res_controls) > 0
2089 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2090 res_controls[0].critical = True
2091 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2092 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2094 modify_ldif = "dn: %s\n" % (self.cache_dn)
2095 modify_ldif += "changetype: modify\n"
2096 modify_ldif += "replace: dirsyncControl\n"
2097 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2098 modify_ldif += "replace: currentTime\n"
2099 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2100 self.cache.modify_ldif(modify_ldif)
2103 def check_object(dirsync_obj, res_controls):
2104 assert len(res_controls) > 0
2105 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2107 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2108 sid = ndr_unpack(security.dom_sid, binary_sid)
2110 lastCookie = str(res_controls[0])
2112 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2113 expression="(lastCookie=%s)" % (
2114 ldb.binary_encode(lastCookie)),
2120 def update_object(dirsync_obj, res_controls):
2121 assert len(res_controls) > 0
2122 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2124 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2125 sid = ndr_unpack(security.dom_sid, binary_sid)
2127 lastCookie = str(res_controls[0])
2129 self.cache.transaction_start()
2131 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2132 expression="(objectClass=*)",
2133 attrs=["lastCookie"])
2135 add_ldif = "dn: %s\n" % (dn)
2136 add_ldif += "objectClass: userCookie\n"
2137 add_ldif += "lastCookie: %s\n" % (lastCookie)
2138 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2139 self.cache.add_ldif(add_ldif)
2141 modify_ldif = "dn: %s\n" % (dn)
2142 modify_ldif += "changetype: modify\n"
2143 modify_ldif += "replace: lastCookie\n"
2144 modify_ldif += "lastCookie: %s\n" % (lastCookie)
2145 modify_ldif += "replace: currentTime\n"
2146 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2147 self.cache.modify_ldif(modify_ldif)
2148 self.cache.transaction_commit()
2149 except Exception as e:
2150 self.cache.transaction_cancel()
2156 res = self.samdb.search(expression=self.dirsync_filter,
2157 scope=ldb.SCOPE_SUBTREE,
2158 attrs=self.dirsync_attrs,
2159 controls=self.dirsync_controls)
2160 log_msg("dirsync_loop(): results %d\n" % len(res))
2163 done = check_object(r, res.controls)
2165 handle_object(ri, r)
2166 update_object(r, res.controls)
2168 update_cache(res.controls)
2172 def sync_loop(wait):
2173 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2174 notify_controls = ["notification:1", "show_recycled:1"]
2175 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2176 scope=ldb.SCOPE_SUBTREE,
2178 controls=notify_controls,
2182 log_msg("Resuming monitoring\n")
2184 log_msg("Getting changes\n")
2185 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2186 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2187 self.outf.write("syncCommand: %s\n" % self.sync_command)
2190 if wait is not True:
2193 for msg in notify_handle:
2194 if not isinstance(msg, ldb.Message):
2195 self.outf.write("referal: %s\n" % msg)
2197 created = msg.get("uSNCreated")[0]
2198 changed = msg.get("uSNChanged")[0]
2199 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2200 (msg.dn, created, changed))
2204 res = notify_handle.result()
2209 orig_pid = os.getpid()
2214 if pid == 0: # Actual daemon
2216 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2221 if cache_ldb_initialize:
2223 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2228 if logfile is not None:
2229 import resource # Resource usage information.
2230 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2231 if maxfd == resource.RLIM_INFINITY:
2232 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2233 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2234 self.outf.write("Using logfile[%s]\n" % logfile)
2235 for fd in range(0, maxfd):
2246 log_msg("Attached to logfile[%s]\n" % (logfile))
2247 self.logfile = logfile
2250 conflict = check_current_pid_conflict(terminate)
2252 if self.current_pid is None:
2253 log_msg("No process running.\n")
2256 log_msg("Proccess %d is not running anymore.\n" % (
2260 log_msg("Sending SIGTERM to proccess %d.\n" % (
2262 os.kill(self.current_pid, signal.SIGTERM)
2265 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2266 os.getpid(), self.current_pid))
2270 update_pid(os.getpid())
2275 retry_sleep_max = 600
2280 retry_sleep = retry_sleep_min
2282 while self.samdb is None:
2283 if retry_sleep != 0:
2284 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2285 time.sleep(retry_sleep)
2286 retry_sleep = retry_sleep * 2
2287 if retry_sleep >= retry_sleep_max:
2288 retry_sleep = retry_sleep_max
2289 log_msg("Connecting to '%s'\n" % self.samdb_url)
2291 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2292 except Exception as msg:
2294 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2295 if wait is not True:
2300 except ldb.LdbError as e7:
2301 (enum, estr) = e7.args
2303 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2309 class cmd_user_edit(Command):
2310 """Modify User AD object.
2312 This command will allow editing of a user account in the Active Directory
2313 domain. You will then be able to add or change attributes and their values.
2315 The username specified on the command is the sAMAccountName.
2317 The command may be run from the root userid or another authorized userid.
2319 The -H or --URL= option can be used to execute the command against a remote
2323 samba-tool user edit User1 -H ldap://samba.samdom.example.com \
2324 -U administrator --password=passw1rd
2326 Example1 shows how to edit a users attributes in the domain against a remote
2329 The -H parameter is used to specify the remote target server.
2332 samba-tool user edit User2
2334 Example2 shows how to edit a users attributes in the domain against a local
2338 samba-tool user edit User3 --editor=nano
2340 Example3 shows how to edit a users attributes in the domain against a local
2341 LDAP server using the 'nano' editor.
2344 synopsis = "%prog <username> [options]"
2347 Option("-H", "--URL", help="LDB URL for database or target server",
2348 type=str, metavar="URL", dest="H"),
2349 Option("--editor", help="Editor to use instead of the system default,"
2350 " or 'vi' if no system default is set.", type=str),
2353 takes_args = ["username"]
2354 takes_optiongroups = {
2355 "sambaopts": options.SambaOptions,
2356 "credopts": options.CredentialsOptions,
2357 "versionopts": options.VersionOptions,
2360 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2361 H=None, editor=None):
2363 lp = sambaopts.get_loadparm()
2364 creds = credopts.get_credentials(lp, fallback_machine=True)
2365 samdb = SamDB(url=H, session_info=system_session(),
2366 credentials=creds, lp=lp)
2368 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2369 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2371 domaindn = samdb.domain_dn()
2374 res = samdb.search(base=domaindn,
2376 scope=ldb.SCOPE_SUBTREE)
2379 raise CommandError('Unable to find user "%s"' % (username))
2382 r_ldif = samdb.write_ldif(msg, 1)
2383 # remove 'changetype' line
2384 result_ldif = re.sub('changetype: add\n', '', r_ldif)
2387 editor = os.environ.get('EDITOR')
2391 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2392 t_file.write(result_ldif)
2395 check_call([editor, t_file.name])
2396 except CalledProcessError as e:
2397 raise CalledProcessError("ERROR: ", e)
2398 with open(t_file.name) as edited_file:
2399 edited_message = edited_file.read()
2401 if result_ldif != edited_message:
2402 diff = difflib.ndiff(result_ldif.splitlines(),
2403 edited_message.splitlines())
2407 if line.startswith('-'):
2409 minus_lines.append(line)
2410 elif line.startswith('+'):
2412 plus_lines.append(line)
2414 user_ldif = "dn: %s\n" % user_dn
2415 user_ldif += "changetype: modify\n"
2417 for line in minus_lines:
2418 attr, val = line.split(':', 1)
2419 search_attr = "%s:" % attr
2420 if not re.search(r'^' + search_attr, str(plus_lines)):
2421 user_ldif += "delete: %s\n" % attr
2422 user_ldif += "%s: %s\n" % (attr, val)
2424 for line in plus_lines:
2425 attr, val = line.split(':', 1)
2426 search_attr = "%s:" % attr
2427 if re.search(r'^' + search_attr, str(minus_lines)):
2428 user_ldif += "replace: %s\n" % attr
2429 user_ldif += "%s: %s\n" % (attr, val)
2430 if not re.search(r'^' + search_attr, str(minus_lines)):
2431 user_ldif += "add: %s\n" % attr
2432 user_ldif += "%s: %s\n" % (attr, val)
2435 samdb.modify_ldif(user_ldif)
2436 except Exception as e:
2437 raise CommandError("Failed to modify user '%s': " %
2440 self.outf.write("Modified User '%s' successfully\n" % username)
2443 class cmd_user_show(Command):
2444 """Display a user AD object.
2446 This command displays a user account and it's attributes in the Active
2448 The username specified on the command is the sAMAccountName.
2450 The command may be run from the root userid or another authorized userid.
2452 The -H or --URL= option can be used to execute the command against a remote
2456 samba-tool user show User1 -H ldap://samba.samdom.example.com \
2457 -U administrator --password=passw1rd
2459 Example1 shows how to display a users attributes in the domain against a remote
2462 The -H parameter is used to specify the remote target server.
2465 samba-tool user show User2
2467 Example2 shows how to display a users attributes in the domain against a local
2471 samba-tool user show User2 --attributes=objectSid,memberOf
2473 Example3 shows how to display a users objectSid and memberOf attributes.
2475 synopsis = "%prog <username> [options]"
2478 Option("-H", "--URL", help="LDB URL for database or target server",
2479 type=str, metavar="URL", dest="H"),
2480 Option("--attributes",
2481 help=("Comma separated list of attributes, "
2482 "which will be printed."),
2483 type=str, dest="user_attrs"),
2486 takes_args = ["username"]
2487 takes_optiongroups = {
2488 "sambaopts": options.SambaOptions,
2489 "credopts": options.CredentialsOptions,
2490 "versionopts": options.VersionOptions,
2493 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2494 H=None, user_attrs=None):
2496 lp = sambaopts.get_loadparm()
2497 creds = credopts.get_credentials(lp, fallback_machine=True)
2498 samdb = SamDB(url=H, session_info=system_session(),
2499 credentials=creds, lp=lp)
2503 attrs = user_attrs.split(",")
2505 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2506 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2508 domaindn = samdb.domain_dn()
2511 res = samdb.search(base=domaindn, expression=filter,
2512 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2515 raise CommandError('Unable to find user "%s"' % (username))
2518 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2519 self.outf.write(user_ldif)
2522 class cmd_user_move(Command):
2523 """Move a user to an organizational unit/container.
2525 This command moves a user account into the specified organizational unit
2527 The username specified on the command is the sAMAccountName.
2528 The name of the organizational unit or container can be specified as a
2529 full DN or without the domainDN component.
2531 The command may be run from the root userid or another authorized userid.
2533 The -H or --URL= option can be used to execute the command against a remote
2537 samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
2538 -H ldap://samba.samdom.example.com -U administrator
2540 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2541 unit on a remote LDAP server.
2543 The -H parameter is used to specify the remote target server.
2546 samba-tool user move User1 CN=Users
2548 Example2 shows how to move a user User1 back into the CN=Users container
2549 on the local server.
2552 synopsis = "%prog <username> <new_parent_dn> [options]"
2555 Option("-H", "--URL", help="LDB URL for database or target server",
2556 type=str, metavar="URL", dest="H"),
2559 takes_args = ["username", "new_parent_dn"]
2560 takes_optiongroups = {
2561 "sambaopts": options.SambaOptions,
2562 "credopts": options.CredentialsOptions,
2563 "versionopts": options.VersionOptions,
2566 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2567 versionopts=None, H=None):
2568 lp = sambaopts.get_loadparm()
2569 creds = credopts.get_credentials(lp, fallback_machine=True)
2570 samdb = SamDB(url=H, session_info=system_session(),
2571 credentials=creds, lp=lp)
2572 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2574 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2575 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2577 res = samdb.search(base=domain_dn,
2579 scope=ldb.SCOPE_SUBTREE)
2582 raise CommandError('Unable to find user "%s"' % (username))
2585 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2586 except Exception as e:
2587 raise CommandError('Invalid new_parent_dn "%s": %s' %
2588 (new_parent_dn, e.message))
2590 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2591 full_new_user_dn.remove_base_components(len(user_dn) - 1)
2592 full_new_user_dn.add_base(full_new_parent_dn)
2595 samdb.rename(user_dn, full_new_user_dn)
2596 except Exception as e:
2597 raise CommandError('Failed to move user "%s"' % username, e)
2598 self.outf.write('Moved user "%s" into "%s"\n' %
2599 (username, full_new_parent_dn))
2602 class cmd_user(SuperCommand):
2603 """User management."""
2606 subcommands["add"] = cmd_user_add()
2607 subcommands["create"] = cmd_user_create()
2608 subcommands["delete"] = cmd_user_delete()
2609 subcommands["disable"] = cmd_user_disable()
2610 subcommands["enable"] = cmd_user_enable()
2611 subcommands["list"] = cmd_user_list()
2612 subcommands["setexpiry"] = cmd_user_setexpiry()
2613 subcommands["password"] = cmd_user_password()
2614 subcommands["setpassword"] = cmd_user_setpassword()
2615 subcommands["getpassword"] = cmd_user_getpassword()
2616 subcommands["syncpasswords"] = cmd_user_syncpasswords()
2617 subcommands["edit"] = cmd_user_edit()
2618 subcommands["show"] = cmd_user_show()
2619 subcommands["move"] = cmd_user_move()