3 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
4 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import samba.getopt as options
34 from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError
35 from getpass import getpass
36 from samba.auth import system_session
37 from samba.samdb import SamDB
38 from samba.dcerpc import misc
39 from samba.dcerpc import security
40 from samba.dcerpc import drsblobs
41 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
46 generate_random_password,
49 from samba.net import Net
51 from samba.netcmd import (
57 from samba.compat import text_type
58 from samba.compat import get_bytes
59 from samba.compat import get_string
65 decrypt_samba_gpg_help = "Decrypt the SambaGPG password as cleartext source"
66 except ImportError as e:
68 decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
69 "python-gpgme required"
71 disabled_virtual_attributes = {
74 virtual_attributes = {
75 "virtualClearTextUTF8": {
76 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
78 "virtualClearTextUTF16": {
79 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
82 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
86 get_random_bytes_fn = None
87 if get_random_bytes_fn is None:
90 get_random_bytes_fn = Crypto.Random.get_random_bytes
91 except ImportError as e:
93 if get_random_bytes_fn is None:
96 get_random_bytes_fn = M2Crypto.Rand.rand_bytes
97 except ImportError as e:
102 if get_random_bytes_fn is not None:
104 return "Crypto.Random or M2Crypto.Rand required"
107 def get_random_bytes(num):
108 random_reason = check_random()
109 if random_reason is not None:
110 raise ImportError(random_reason)
111 return get_random_bytes_fn(num)
114 def get_crypt_value(alg, utf8pw, rounds=0):
120 salt = get_random_bytes(16)
121 # The salt needs to be in [A-Za-z0-9./]
122 # base64 is close enough and as we had 16
123 # random bytes but only need 16 characters
124 # we can ignore the possible == at the end
125 # of the base64 string
126 # we just need to replace '+' by '.'
127 b64salt = base64.b64encode(salt)[0:16].replace(b'+', b'.').decode('utf8')
130 crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
132 crypt_salt = "$%s$%s$" % (alg, b64salt)
134 crypt_value = crypt.crypt(utf8pw, crypt_salt)
135 if crypt_value is None:
136 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
137 expected_len = len(crypt_salt) + algs[alg]["length"]
138 if len(crypt_value) != expected_len:
139 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
140 crypt_salt, len(crypt_value), expected_len))
143 # Extract the rounds value from the options of a virtualCrypt attribute
144 # i.e. options = "rounds=20;other=ignored;" will return 20
145 # if the rounds option is not found or the value is not a number, 0 is returned
146 # which indicates that the default number of rounds should be used.
149 def get_rounds(options):
153 opts = options.split(';')
155 if o.lower().startswith("rounds="):
156 (key, _, val) = o.partition('=')
165 random_reason = check_random()
166 if random_reason is not None:
167 raise ImportError(random_reason)
171 virtual_attributes["virtualSSHA"] = {
173 except ImportError as e:
174 reason = "hashlib.sha1()"
176 reason += " and " + random_reason
177 reason += " required"
178 disabled_virtual_attributes["virtualSSHA"] = {
182 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
184 random_reason = check_random()
185 if random_reason is not None:
186 raise ImportError(random_reason)
188 v = get_crypt_value(alg, "")
190 virtual_attributes[attr] = {
192 except ImportError as e:
195 reason += " and " + random_reason
196 reason += " required"
197 disabled_virtual_attributes[attr] = {
200 except NotImplementedError as e:
201 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
202 disabled_virtual_attributes[attr] = {
206 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
207 for x in range(1, 30):
208 virtual_attributes["virtualWDigest%02d" % x] = {}
210 # Add Kerberos virtual attributes
211 virtual_attributes["virtualKerberosSalt"] = {}
213 virtual_attributes_help = "The attributes to display (comma separated). "
214 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
215 if len(disabled_virtual_attributes) != 0:
216 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
219 class cmd_user_create(Command):
220 """Create a new user.
222 This command creates a new user account in the Active Directory domain. The username specified on the command is the sAMaccountName.
224 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).
226 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.
228 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.
230 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.
233 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
235 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.
238 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
240 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.
243 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
245 Example3 shows how to create a new user in the OrgUnit organizational unit.
248 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
250 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'.
253 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
254 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
256 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
257 --nis-domain is set, then the other four parameters are mandatory.
260 synopsis = "%prog <username> [<password>] [options]"
263 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
264 metavar="URL", dest="H"),
265 Option("--must-change-at-next-login",
266 help="Force password to be changed on next login",
267 action="store_true"),
268 Option("--random-password",
269 help="Generate random password",
270 action="store_true"),
271 Option("--smartcard-required",
272 help="Require a smartcard for interactive logons",
273 action="store_true"),
274 Option("--use-username-as-cn",
275 help="Force use of username as user's CN",
276 action="store_true"),
278 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>'",
280 Option("--surname", help="User's surname", type=str),
281 Option("--given-name", help="User's given name", type=str),
282 Option("--initials", help="User's initials", type=str),
283 Option("--profile-path", help="User's profile path", type=str),
284 Option("--script-path", help="User's logon script path", type=str),
285 Option("--home-drive", help="User's home drive letter", type=str),
286 Option("--home-directory", help="User's home directory path", type=str),
287 Option("--job-title", help="User's job title", type=str),
288 Option("--department", help="User's department", type=str),
289 Option("--company", help="User's company", type=str),
290 Option("--description", help="User's description", type=str),
291 Option("--mail-address", help="User's email address", type=str),
292 Option("--internet-address", help="User's home page", type=str),
293 Option("--telephone-number", help="User's phone number", type=str),
294 Option("--physical-delivery-office", help="User's office location", type=str),
295 Option("--rfc2307-from-nss",
296 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
297 action="store_true"),
298 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
299 Option("--unix-home", help="User's Unix/RFC2307 home directory",
301 Option("--uid", help="User's Unix/RFC2307 username", type=str),
302 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
303 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
304 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
305 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
308 takes_args = ["username", "password?"]
310 takes_optiongroups = {
311 "sambaopts": options.SambaOptions,
312 "credopts": options.CredentialsOptions,
313 "versionopts": options.VersionOptions,
316 def run(self, username, password=None, credopts=None, sambaopts=None,
317 versionopts=None, H=None, must_change_at_next_login=False,
318 random_password=False, use_username_as_cn=False, userou=None,
319 surname=None, given_name=None, initials=None, profile_path=None,
320 script_path=None, home_drive=None, home_directory=None,
321 job_title=None, department=None, company=None, description=None,
322 mail_address=None, internet_address=None, telephone_number=None,
323 physical_delivery_office=None, rfc2307_from_nss=False,
324 nis_domain=None, unix_home=None, uid=None, uid_number=None,
325 gid_number=None, gecos=None, login_shell=None,
326 smartcard_required=False):
328 if smartcard_required:
329 if password is not None and password is not '':
330 raise CommandError('It is not allowed to specify '
332 'together with --smartcard-required.')
333 if must_change_at_next_login:
334 raise CommandError('It is not allowed to specify '
335 '--must-change-at-next-login '
336 'together with --smartcard-required.')
338 if random_password and not smartcard_required:
339 password = generate_random_password(128, 255)
342 if smartcard_required:
344 if password is not None and password is not '':
346 password = getpass("New Password: ")
347 passwordverify = getpass("Retype Password: ")
348 if not password == passwordverify:
350 self.outf.write("Sorry, passwords do not match.\n")
353 pwent = pwd.getpwnam(username)
356 if uid_number is None:
357 uid_number = pwent[2]
358 if gid_number is None:
359 gid_number = pwent[3]
362 if login_shell is None:
363 login_shell = pwent[6]
365 lp = sambaopts.get_loadparm()
366 creds = credopts.get_credentials(lp)
368 if uid_number or gid_number:
369 if not lp.get("idmap_ldb:use rfc2307"):
370 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")
372 if nis_domain is not None:
373 if None in (uid_number, login_shell, unix_home, gid_number):
374 raise CommandError('Missing parameters. To enable NIS features, '
375 'the following options have to be given: '
376 '--nis-domain=, --uidNumber=, --login-shell='
377 ', --unix-home=, --gid-number= Operation '
381 samdb = SamDB(url=H, session_info=system_session(),
382 credentials=creds, lp=lp)
383 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
384 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
385 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
386 jobtitle=job_title, department=department, company=company, description=description,
387 mailaddress=mail_address, internetaddress=internet_address,
388 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
389 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
390 uidnumber=uid_number, gidnumber=gid_number,
391 gecos=gecos, loginshell=login_shell,
392 smartcard_required=smartcard_required)
393 except Exception as e:
394 raise CommandError("Failed to add user '%s': " % username, e)
396 self.outf.write("User '%s' created successfully\n" % username)
399 class cmd_user_add(cmd_user_create):
400 __doc__ = cmd_user_create.__doc__
401 # take this print out after the add subcommand is removed.
402 # the add subcommand is deprecated but left in for now to allow people to
405 def run(self, *args, **kwargs):
407 "Note: samba-tool user add is deprecated. "
408 "Please use samba-tool user create for the same function.\n")
409 return super(cmd_user_add, self).run(*args, **kwargs)
412 class cmd_user_delete(Command):
415 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
417 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.
419 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.
422 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
424 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.
427 sudo samba-tool user delete User2
429 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.
432 synopsis = "%prog <username> [options]"
435 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
436 metavar="URL", dest="H"),
439 takes_args = ["username"]
440 takes_optiongroups = {
441 "sambaopts": options.SambaOptions,
442 "credopts": options.CredentialsOptions,
443 "versionopts": options.VersionOptions,
446 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
448 lp = sambaopts.get_loadparm()
449 creds = credopts.get_credentials(lp, fallback_machine=True)
451 samdb = SamDB(url=H, session_info=system_session(),
452 credentials=creds, lp=lp)
454 filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
455 ldb.binary_encode(username))
458 res = samdb.search(base=samdb.domain_dn(),
459 scope=ldb.SCOPE_SUBTREE,
464 raise CommandError('Unable to find user "%s"' % (username))
467 samdb.delete(user_dn)
468 except Exception as e:
469 raise CommandError('Failed to remove user "%s"' % username, e)
470 self.outf.write("Deleted user %s\n" % username)
473 class cmd_user_list(Command):
474 """List all users."""
476 synopsis = "%prog [options]"
479 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
480 metavar="URL", dest="H"),
483 takes_optiongroups = {
484 "sambaopts": options.SambaOptions,
485 "credopts": options.CredentialsOptions,
486 "versionopts": options.VersionOptions,
489 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
490 lp = sambaopts.get_loadparm()
491 creds = credopts.get_credentials(lp, fallback_machine=True)
493 samdb = SamDB(url=H, session_info=system_session(),
494 credentials=creds, lp=lp)
496 domain_dn = samdb.domain_dn()
497 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
498 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
499 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
500 attrs=["samaccountname"])
505 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
508 class cmd_user_enable(Command):
511 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.
513 There are many reasons why an account may become disabled. These include:
514 - If a user exceeds the account policy for logon attempts
515 - If an administrator disables the account
516 - If the account expires
518 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
520 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.
522 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.
525 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
527 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.
530 su samba-tool user enable Testuser2
532 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.
535 samba-tool user enable --filter=samaccountname=Testuser3
537 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
540 synopsis = "%prog (<username>|--filter <filter>) [options]"
542 takes_optiongroups = {
543 "sambaopts": options.SambaOptions,
544 "versionopts": options.VersionOptions,
545 "credopts": options.CredentialsOptions,
549 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
550 metavar="URL", dest="H"),
551 Option("--filter", help="LDAP Filter to set password on", type=str),
554 takes_args = ["username?"]
556 def run(self, username=None, sambaopts=None, credopts=None,
557 versionopts=None, filter=None, H=None):
558 if username is None and filter is None:
559 raise CommandError("Either the username or '--filter' must be specified!")
562 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
564 lp = sambaopts.get_loadparm()
565 creds = credopts.get_credentials(lp, fallback_machine=True)
567 samdb = SamDB(url=H, session_info=system_session(),
568 credentials=creds, lp=lp)
570 samdb.enable_account(filter)
571 except Exception as msg:
572 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
573 self.outf.write("Enabled user '%s'\n" % (username or filter))
576 class cmd_user_disable(Command):
577 """Disable a user."""
579 synopsis = "%prog (<username>|--filter <filter>) [options]"
582 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
583 metavar="URL", dest="H"),
584 Option("--filter", help="LDAP Filter to set password on", type=str),
587 takes_args = ["username?"]
589 takes_optiongroups = {
590 "sambaopts": options.SambaOptions,
591 "credopts": options.CredentialsOptions,
592 "versionopts": options.VersionOptions,
595 def run(self, username=None, sambaopts=None, credopts=None,
596 versionopts=None, filter=None, H=None):
597 if username is None and filter is None:
598 raise CommandError("Either the username or '--filter' must be specified!")
601 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
603 lp = sambaopts.get_loadparm()
604 creds = credopts.get_credentials(lp, fallback_machine=True)
606 samdb = SamDB(url=H, session_info=system_session(),
607 credentials=creds, lp=lp)
609 samdb.disable_account(filter)
610 except Exception as msg:
611 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
614 class cmd_user_setexpiry(Command):
615 """Set the expiration of a user account.
617 The user can either be specified by their sAMAccountName or using the --filter option.
619 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.
621 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.
624 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
626 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.
629 sudo samba-tool user setexpiry User2 --noexpiry
631 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.
634 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
636 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.
639 samba-tool user setexpiry --noexpiry User4
640 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
643 synopsis = "%prog (<username>|--filter <filter>) [options]"
645 takes_optiongroups = {
646 "sambaopts": options.SambaOptions,
647 "versionopts": options.VersionOptions,
648 "credopts": options.CredentialsOptions,
652 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
653 metavar="URL", dest="H"),
654 Option("--filter", help="LDAP Filter to set password on", type=str),
655 Option("--days", help="Days to expiry", type=int, default=0),
656 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
659 takes_args = ["username?"]
661 def run(self, username=None, sambaopts=None, credopts=None,
662 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
663 if username is None and filter is None:
664 raise CommandError("Either the username or '--filter' must be specified!")
667 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
669 lp = sambaopts.get_loadparm()
670 creds = credopts.get_credentials(lp)
672 samdb = SamDB(url=H, session_info=system_session(),
673 credentials=creds, lp=lp)
676 samdb.setexpiry(filter, days * 24 * 3600, no_expiry_req=noexpiry)
677 except Exception as msg:
678 # FIXME: Catch more specific exception
679 raise CommandError("Failed to set expiry for user '%s': %s" % (
680 username or filter, msg))
682 self.outf.write("Expiry for user '%s' disabled.\n" % (
685 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
686 username or filter, days))
689 class cmd_user_password(Command):
690 """Change password for a user account (the one provided in authentication).
693 synopsis = "%prog [options]"
696 Option("--newpassword", help="New password", type=str),
699 takes_optiongroups = {
700 "sambaopts": options.SambaOptions,
701 "credopts": options.CredentialsOptions,
702 "versionopts": options.VersionOptions,
705 def run(self, credopts=None, sambaopts=None, versionopts=None,
708 lp = sambaopts.get_loadparm()
709 creds = credopts.get_credentials(lp)
711 # get old password now, to get the password prompts in the right order
712 old_password = creds.get_password()
714 net = Net(creds, lp, server=credopts.ipaddress)
716 password = newpassword
718 if password is not None and password is not '':
720 password = getpass("New Password: ")
721 passwordverify = getpass("Retype Password: ")
722 if not password == passwordverify:
724 self.outf.write("Sorry, passwords do not match.\n")
727 if not isinstance(password, text_type):
728 password = password.decode('utf8')
729 net.change_password(password)
730 except Exception as msg:
731 # FIXME: catch more specific exception
732 raise CommandError("Failed to change password : %s" % msg)
733 self.outf.write("Changed password OK\n")
736 class cmd_user_setpassword(Command):
737 """Set or reset the password of a user account.
739 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.
741 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.
743 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.
745 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.
748 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
750 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.
753 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
755 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.
758 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
760 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
763 synopsis = "%prog (<username>|--filter <filter>) [options]"
765 takes_optiongroups = {
766 "sambaopts": options.SambaOptions,
767 "versionopts": options.VersionOptions,
768 "credopts": options.CredentialsOptions,
772 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
773 metavar="URL", dest="H"),
774 Option("--filter", help="LDAP Filter to set password on", type=str),
775 Option("--newpassword", help="Set password", type=str),
776 Option("--must-change-at-next-login",
777 help="Force password to be changed on next login",
778 action="store_true"),
779 Option("--random-password",
780 help="Generate random password",
781 action="store_true"),
782 Option("--smartcard-required",
783 help="Require a smartcard for interactive logons",
784 action="store_true"),
785 Option("--clear-smartcard-required",
786 help="Don't require a smartcard for interactive logons",
787 action="store_true"),
790 takes_args = ["username?"]
792 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
793 versionopts=None, H=None, newpassword=None,
794 must_change_at_next_login=False, random_password=False,
795 smartcard_required=False, clear_smartcard_required=False):
796 if filter is None and username is None:
797 raise CommandError("Either the username or '--filter' must be specified!")
799 password = newpassword
801 if smartcard_required:
802 if password is not None and password is not '':
803 raise CommandError('It is not allowed to specify '
805 'together with --smartcard-required.')
806 if must_change_at_next_login:
807 raise CommandError('It is not allowed to specify '
808 '--must-change-at-next-login '
809 'together with --smartcard-required.')
810 if clear_smartcard_required:
811 raise CommandError('It is not allowed to specify '
812 '--clear-smartcard-required '
813 'together with --smartcard-required.')
815 if random_password and not smartcard_required:
816 password = generate_random_password(128, 255)
819 if smartcard_required:
821 if password is not None and password is not '':
823 password = getpass("New Password: ")
824 passwordverify = getpass("Retype Password: ")
825 if not password == passwordverify:
827 self.outf.write("Sorry, passwords do not match.\n")
830 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
832 lp = sambaopts.get_loadparm()
833 creds = credopts.get_credentials(lp)
835 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
837 samdb = SamDB(url=H, session_info=system_session(),
838 credentials=creds, lp=lp)
840 if smartcard_required:
843 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
844 flags = dsdb.UF_SMARTCARD_REQUIRED
845 samdb.toggle_userAccountFlags(filter, flags, on=True)
846 command = "Failed to enable account for user '%s'" % (username or filter)
847 samdb.enable_account(filter)
848 except Exception as msg:
849 # FIXME: catch more specific exception
850 raise CommandError("%s: %s" % (command, msg))
851 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
855 if clear_smartcard_required:
856 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
857 flags = dsdb.UF_SMARTCARD_REQUIRED
858 samdb.toggle_userAccountFlags(filter, flags, on=False)
859 command = "Failed to set password for user '%s'" % (username or filter)
860 samdb.setpassword(filter, password,
861 force_change_at_next_login=must_change_at_next_login,
863 except Exception as msg:
864 # FIXME: catch more specific exception
865 raise CommandError("%s: %s" % (command, msg))
866 self.outf.write("Changed password OK\n")
869 class GetPasswordCommand(Command):
872 super(GetPasswordCommand, self).__init__()
875 def connect_system_samdb(self, url, allow_local=False, verbose=False):
877 # using anonymous here, results in no authentication
878 # which means we can get system privileges via
879 # the privileged ldapi socket
880 creds = credentials.Credentials()
881 creds.set_anonymous()
883 if url is None and allow_local:
885 elif url.lower().startswith("ldapi://"):
887 elif url.lower().startswith("ldap://"):
888 raise CommandError("--url ldap:// is not supported for this command")
889 elif url.lower().startswith("ldaps://"):
890 raise CommandError("--url ldaps:// is not supported for this command")
891 elif not allow_local:
892 raise CommandError("--url requires an ldapi:// url for this command")
895 self.outf.write("Connecting to '%s'\n" % url)
897 samdb = SamDB(url=url, session_info=system_session(),
898 credentials=creds, lp=self.lp)
902 # Make sure we're connected as SYSTEM
904 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
906 sids = res[0].get("tokenGroups")
907 assert len(sids) == 1
908 sid = ndr_unpack(security.dom_sid, sids[0])
909 assert str(sid) == security.SID_NT_SYSTEM
910 except Exception as msg:
911 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
912 (security.SID_NT_SYSTEM))
914 # We use sort here in order to have a predictable processing order
915 # this might not be strictly needed, but also doesn't hurt here
916 for a in sorted(virtual_attributes.keys()):
917 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
918 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
922 def get_account_attributes(self, samdb, username, basedn, filter, scope,
929 (attr, _, opts) = a.partition(';')
931 attr_opts[attr] = opts
933 attr_opts[attr] = None
934 search_attrs.append(attr)
935 lower_attrs = [x.lower() for x in search_attrs]
937 require_supplementalCredentials = False
938 for a in virtual_attributes.keys():
939 if a.lower() in lower_attrs:
940 require_supplementalCredentials = True
941 add_supplementalCredentials = False
942 add_unicodePwd = False
943 if require_supplementalCredentials:
944 a = "supplementalCredentials"
945 if a.lower() not in lower_attrs:
947 add_supplementalCredentials = True
949 if a.lower() not in lower_attrs:
951 add_unicodePwd = True
952 add_sAMAcountName = False
954 if a.lower() not in lower_attrs:
956 add_sAMAcountName = True
958 add_userPrincipalName = False
959 upn = "usePrincipalName"
960 if upn.lower() not in lower_attrs:
961 search_attrs += [upn]
962 add_userPrincipalName = True
964 if scope == ldb.SCOPE_BASE:
965 search_controls = ["show_deleted:1", "show_recycled:1"]
969 res = samdb.search(base=basedn, expression=filter,
970 scope=scope, attrs=search_attrs,
971 controls=search_controls)
973 raise Exception('Unable to find user "%s"' % (username or filter))
975 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
976 except Exception as msg:
977 # FIXME: catch more specific exception
978 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
983 if "supplementalCredentials" in obj:
984 sc_blob = obj["supplementalCredentials"][0]
985 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
986 if add_supplementalCredentials:
987 del obj["supplementalCredentials"]
988 if "unicodePwd" in obj:
989 unicodePwd = obj["unicodePwd"][0]
991 del obj["unicodePwd"]
992 account_name = str(obj["sAMAccountName"][0])
993 if add_sAMAcountName:
994 del obj["sAMAccountName"]
995 if "userPrincipalName" in obj:
996 account_upn = str(obj["userPrincipalName"][0])
998 realm = self.lp.get("realm")
999 account_upn = "%s@%s" % (account_name, realm.lower())
1000 if add_userPrincipalName:
1001 del obj["userPrincipalName"]
1005 def get_package(name, min_idx=0):
1006 if name in calculated:
1007 return calculated[name]
1011 min_idx = len(sc.sub.packages) + min_idx
1013 for p in sc.sub.packages:
1020 return binascii.a2b_hex(p.data)
1025 # Samba adds 'Primary:SambaGPG' at the end.
1026 # When Windows sets the password it keeps
1027 # 'Primary:SambaGPG' and rotates it to
1028 # the begining. So we can only use the value,
1029 # if it is the last one.
1031 # In order to get more protection we verify
1032 # the nthash of the decrypted utf16 password
1033 # against the stored nthash in unicodePwd.
1035 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1036 if sgv is not None and unicodePwd is not None:
1037 ctx = gpgme.Context()
1039 cipher_io = io.BytesIO(sgv)
1040 plain_io = io.BytesIO()
1042 ctx.decrypt(cipher_io, plain_io)
1043 cv = plain_io.getvalue()
1045 # We only use the password if it matches
1046 # the current nthash stored in the unicodePwd
1049 tmp = credentials.Credentials()
1051 tmp.set_utf16_password(cv)
1052 nthash = tmp.get_nt_hash()
1053 if nthash == unicodePwd:
1054 calculated["Primary:CLEARTEXT"] = cv
1055 except gpgme.GpgmeError as e1:
1056 (major, minor, msg) = e1.args
1057 if major == gpgme.ERR_BAD_SECKEY:
1058 msg = "ERR_BAD_SECKEY: " + msg
1060 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1061 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1062 username or account_name, msg))
1064 def get_utf8(a, b, username):
1066 u = text_type(get_bytes(b), 'utf-16-le')
1067 except UnicodeDecodeError as e:
1068 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1071 u8 = u.encode('utf-8')
1074 # Extract the WDigest hash for the value specified by i.
1075 # Builds an htdigest compatible value
1078 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1079 domain, dns_domain):
1084 user = account_name.lower()
1085 realm = domain.lower()
1087 user = account_name.upper()
1088 realm = domain.upper()
1091 realm = domain.upper()
1094 realm = domain.lower()
1096 user = account_name.upper()
1097 realm = domain.lower()
1099 user = account_name.lower()
1100 realm = domain.upper()
1103 realm = dns_domain.lower()
1105 user = account_name.lower()
1106 realm = dns_domain.lower()
1108 user = account_name.upper()
1109 realm = dns_domain.upper()
1112 realm = dns_domain.upper()
1115 realm = dns_domain.lower()
1117 user = account_name.upper()
1118 realm = dns_domain.lower()
1120 user = account_name.lower()
1121 realm = dns_domain.upper()
1126 user = account_upn.lower()
1129 user = account_upn.upper()
1132 user = "%s\\%s" % (domain, account_name)
1135 user = "%s\\%s" % (domain.lower(), account_name.lower())
1138 user = "%s\\%s" % (domain.upper(), account_name.upper())
1144 user = account_name.lower()
1147 user = account_name.upper()
1153 user = account_upn.lower()
1156 user = account_upn.upper()
1159 user = "%s\\%s" % (domain, account_name)
1162 # Differs from spec, see tests
1163 user = "%s\\%s" % (domain.lower(), account_name.lower())
1166 # Differs from spec, see tests
1167 user = "%s\\%s" % (domain.upper(), account_name.upper())
1172 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1175 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
1176 return "%s:%s:%s" % (user, realm, get_string(digest))
1180 # get the value for a virtualCrypt attribute.
1181 # look for an exact match on algorithm and rounds in supplemental creds
1182 # if not found calculate using Primary:CLEARTEXT
1183 # if no Primary:CLEARTEXT return the first supplementalCredential
1184 # that matches the algorithm.
1185 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1188 b = get_package("Primary:userPassword")
1190 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1192 # No exact match on algorithm and number of rounds
1193 # try and calculate one from the Primary:CLEARTEXT
1194 b = get_package("Primary:CLEARTEXT")
1196 u8 = get_utf8(a, b, username or account_name)
1198 # in py2 using get_bytes should ensure u8 is unmodified
1199 # in py3 it will be decoded
1200 sv = get_crypt_value(str(algorithm), get_string(u8), rounds)
1202 # Unable to calculate a hash with the specified
1203 # number of rounds, fall back to the first hash using
1204 # the specified algorithm
1208 return "{CRYPT}" + sv
1210 def get_userPassword_hash(blob, algorithm, rounds):
1211 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1214 # Check that the NT hash has not been changed without updating
1215 # the user password hashes. This indicates that password has been
1216 # changed without updating the supplemental credentials.
1217 if unicodePwd != bytearray(up.current_nt_hash.hash):
1220 scheme_prefix = "$%d$" % algorithm
1221 prefix = scheme_prefix
1223 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1227 # in PY2 this should just do nothing and in PY3 if bytes
1228 # it will decode them
1229 h_value = get_string(h.value)
1230 if (scheme_match is None and
1231 h.scheme == SCHEME and
1232 h_value.startswith(scheme_prefix)):
1233 scheme_match = h_value
1234 if h.scheme == SCHEME and h_value.startswith(prefix):
1235 return (h_value, scheme_match)
1237 # No match on the number of rounds, return the value of the
1238 # first matching scheme
1239 return (None, scheme_match)
1241 def get_kerberos_ctr():
1242 primary_krb5 = get_package("Primary:Kerberos-Newer-Keys")
1243 if primary_krb5 is None:
1244 primary_krb5 = get_package("Primary:Kerberos")
1245 if primary_krb5 is None:
1247 krb5_blob = ndr_unpack(drsblobs.package_PrimaryKerberosBlob,
1249 return (krb5_blob.version, krb5_blob.ctr)
1251 # We use sort here in order to have a predictable processing order
1252 for a in sorted(virtual_attributes.keys()):
1253 if not a.lower() in lower_attrs:
1256 if a == "virtualClearTextUTF8":
1257 b = get_package("Primary:CLEARTEXT")
1260 u8 = get_utf8(a, b, username or account_name)
1264 elif a == "virtualClearTextUTF16":
1265 v = get_package("Primary:CLEARTEXT")
1268 elif a == "virtualSSHA":
1269 b = get_package("Primary:CLEARTEXT")
1272 u8 = get_utf8(a, b, username or account_name)
1275 salt = get_random_bytes(4)
1279 bv = h.digest() + salt
1280 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
1281 elif a == "virtualCryptSHA256":
1282 rounds = get_rounds(attr_opts[a])
1283 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1287 elif a == "virtualCryptSHA512":
1288 rounds = get_rounds(attr_opts[a])
1289 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1293 elif a == "virtualSambaGPG":
1294 # Samba adds 'Primary:SambaGPG' at the end.
1295 # When Windows sets the password it keeps
1296 # 'Primary:SambaGPG' and rotates it to
1297 # the begining. So we can only use the value,
1298 # if it is the last one.
1299 v = get_package("Primary:SambaGPG", min_idx=-1)
1302 elif a == "virtualKerberosSalt":
1303 (krb5_v, krb5_ctr) = get_kerberos_ctr()
1304 if krb5_v not in [3, 4]:
1306 v = krb5_ctr.salt.string
1307 elif a.startswith("virtualWDigest"):
1308 primary_wdigest = get_package("Primary:WDigest")
1309 if primary_wdigest is None:
1311 x = a[len("virtualWDigest"):]
1316 domain = self.lp.get("workgroup")
1317 dns_domain = samdb.domain_dns_name()
1318 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1323 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1326 def parse_attributes(self, attributes):
1328 if attributes is None:
1329 raise CommandError("Please specify --attributes")
1330 attrs = attributes.split(',')
1333 pa = pa.lstrip().rstrip()
1334 for da in disabled_virtual_attributes.keys():
1335 if pa.lower() == da.lower():
1336 r = disabled_virtual_attributes[da]["reason"]
1337 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1339 for va in virtual_attributes.keys():
1340 if pa.lower() == va.lower():
1341 # Take the real name
1344 password_attrs += [pa]
1346 return password_attrs
1349 class cmd_user_getpassword(GetPasswordCommand):
1350 """Get the password fields of a user/computer account.
1352 This command gets the logon password for a user/computer account.
1354 The username specified on the command is the sAMAccountName.
1355 The username may also be specified using the --filter option.
1357 The command must be run from the root user id or another authorized user id.
1358 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1359 used to adjust the local path. By default tdb:// is used by default.
1361 The '--attributes' parameter takes a comma separated list of attributes,
1362 which will be printed or given to the script specified by '--script'. If a
1363 specified attribute is not available on an object it's silently omitted.
1364 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1365 the NTHASH) and the following virtual attributes are possible (see --help
1366 for which virtual attributes are supported in your environment):
1368 virtualClearTextUTF16: The raw cleartext as stored in the
1369 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1370 with '--decrypt-samba-gpg') buffer inside of the
1371 supplementalCredentials attribute. This typically
1372 contains valid UTF-16-LE, but may contain random
1373 bytes, e.g. for computer accounts.
1375 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1376 (only from valid UTF-16-LE)
1378 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1379 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1381 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1382 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1383 with a $5$... salt, see crypt(3) on modern systems.
1384 The number of rounds used to calculate the hash can
1385 also be specified. By appending ";rounds=x" to the
1386 attribute name i.e. virtualCryptSHA256;rounds=10000
1387 will calculate a SHA256 hash with 10,000 rounds.
1388 non numeric values for rounds are silently ignored
1389 The value is calculated as follows:
1390 1) If a value exists in 'Primary:userPassword' with
1391 the specified number of rounds it is returned.
1392 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1393 '--decrypt-samba-gpg'. Calculate a hash with
1394 the specified number of rounds
1395 3) Return the first CryptSHA256 value in
1396 'Primary:userPassword'
1399 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1400 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1401 with a $6$... salt, see crypt(3) on modern systems.
1402 The number of rounds used to calculate the hash can
1403 also be specified. By appending ";rounds=x" to the
1404 attribute name i.e. virtualCryptSHA512;rounds=10000
1405 will calculate a SHA512 hash with 10,000 rounds.
1406 non numeric values for rounds are silently ignored
1407 The value is calculated as follows:
1408 1) If a value exists in 'Primary:userPassword' with
1409 the specified number of rounds it is returned.
1410 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1411 '--decrypt-samba-gpg'. Calculate a hash with
1412 the specified number of rounds
1413 3) Return the first CryptSHA512 value in
1414 'Primary:userPassword'
1416 virtualWDigestNN: The individual hash values stored in
1417 'Primary:WDigest' where NN is the hash number in
1419 NOTE: As at 22-05-2017 the documentation:
1420 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1421 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1424 virtualKerberosSalt: This results the salt string that is used to compute
1425 Kerberos keys from a UTF-8 cleartext password.
1427 virtualSambaGPG: The raw cleartext as stored in the
1428 'Primary:SambaGPG' buffer inside of the
1429 supplementalCredentials attribute.
1430 See the 'password hash gpg key ids' option in
1433 The '--decrypt-samba-gpg' option triggers decryption of the
1434 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1435 in your environment or not (the python-gpgme package is required). Please
1436 note that you might need to set the GNUPGHOME environment variable. If the
1437 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1438 environment variable has been set correctly and the passphrase is already
1439 known by the gpg-agent.
1442 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1445 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1449 super(cmd_user_getpassword, self).__init__()
1451 synopsis = "%prog (<username>|--filter <filter>) [options]"
1453 takes_optiongroups = {
1454 "sambaopts": options.SambaOptions,
1455 "versionopts": options.VersionOptions,
1459 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1460 metavar="URL", dest="H"),
1461 Option("--filter", help="LDAP Filter to set password on", type=str),
1462 Option("--attributes", type=str,
1463 help=virtual_attributes_help,
1464 metavar="ATTRIBUTELIST", dest="attributes"),
1465 Option("--decrypt-samba-gpg",
1466 help=decrypt_samba_gpg_help,
1467 action="store_true", default=False, dest="decrypt_samba_gpg"),
1470 takes_args = ["username?"]
1472 def run(self, username=None, H=None, filter=None,
1473 attributes=None, decrypt_samba_gpg=None,
1474 sambaopts=None, versionopts=None):
1475 self.lp = sambaopts.get_loadparm()
1477 if decrypt_samba_gpg and not gpgme_support:
1478 raise CommandError(decrypt_samba_gpg_help)
1480 if filter is None and username is None:
1481 raise CommandError("Either the username or '--filter' must be specified!")
1484 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1486 if attributes is None:
1487 raise CommandError("Please specify --attributes")
1489 password_attrs = self.parse_attributes(attributes)
1491 samdb = self.connect_system_samdb(url=H, allow_local=True)
1493 obj = self.get_account_attributes(samdb, username,
1496 scope=ldb.SCOPE_SUBTREE,
1497 attrs=password_attrs,
1498 decrypt=decrypt_samba_gpg)
1500 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1501 self.outf.write("%s" % ldif)
1502 self.outf.write("Got password OK\n")
1505 class cmd_user_syncpasswords(GetPasswordCommand):
1506 """Sync the password of user accounts.
1508 This syncs logon passwords for user accounts.
1510 Note that this command should run on a single domain controller only
1511 (typically the PDC-emulator). However the "password hash gpg key ids"
1512 option should to be configured on all domain controllers.
1514 The command must be run from the root user id or another authorized user id.
1515 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1516 local path. By default, ldapi:// is used with the default path to the
1517 privileged ldapi socket.
1519 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1520 "Sync Loop Terminate".
1523 Cache Initialization
1524 ====================
1526 The first time, this command needs to be called with
1527 '--cache-ldb-initialize' in order to initialize its cache.
1529 The cache initialization requires '--attributes' and allows the following
1530 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1533 The '--attributes' parameter takes a comma separated list of attributes,
1534 which will be printed or given to the script specified by '--script'. If a
1535 specified attribute is not available on an object it will be silently omitted.
1536 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1537 the NTHASH) and the following virtual attributes are possible (see '--help'
1538 for supported virtual attributes in your environment):
1540 virtualClearTextUTF16: The raw cleartext as stored in the
1541 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1542 with '--decrypt-samba-gpg') buffer inside of the
1543 supplementalCredentials attribute. This typically
1544 contains valid UTF-16-LE, but may contain random
1545 bytes, e.g. for computer accounts.
1547 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1548 (only from valid UTF-16-LE)
1550 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1551 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1553 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1554 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1555 with a $5$... salt, see crypt(3) on modern systems.
1556 The number of rounds used to calculate the hash can
1557 also be specified. By appending ";rounds=x" to the
1558 attribute name i.e. virtualCryptSHA256;rounds=10000
1559 will calculate a SHA256 hash with 10,000 rounds.
1560 non numeric values for rounds are silently ignored
1561 The value is calculated as follows:
1562 1) If a value exists in 'Primary:userPassword' with
1563 the specified number of rounds it is returned.
1564 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1565 '--decrypt-samba-gpg'. Calculate a hash with
1566 the specified number of rounds
1567 3) Return the first CryptSHA256 value in
1568 'Primary:userPassword'
1570 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1571 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1572 with a $6$... salt, see crypt(3) on modern systems.
1573 The number of rounds used to calculate the hash can
1574 also be specified. By appending ";rounds=x" to the
1575 attribute name i.e. virtualCryptSHA512;rounds=10000
1576 will calculate a SHA512 hash with 10,000 rounds.
1577 non numeric values for rounds are silently ignored
1578 The value is calculated as follows:
1579 1) If a value exists in 'Primary:userPassword' with
1580 the specified number of rounds it is returned.
1581 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1582 '--decrypt-samba-gpg'. Calculate a hash with
1583 the specified number of rounds
1584 3) Return the first CryptSHA512 value in
1585 'Primary:userPassword'
1587 virtualWDigestNN: The individual hash values stored in
1588 'Primary:WDigest' where NN is the hash number in
1590 NOTE: As at 22-05-2017 the documentation:
1591 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1592 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1595 virtualKerberosSalt: This results the salt string that is used to compute
1596 Kerberos keys from a UTF-8 cleartext password.
1598 virtualSambaGPG: The raw cleartext as stored in the
1599 'Primary:SambaGPG' buffer inside of the
1600 supplementalCredentials attribute.
1601 See the 'password hash gpg key ids' option in
1604 The '--decrypt-samba-gpg' option triggers decryption of the
1605 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1606 in your environment or not (the python-gpgme package is required). Please
1607 note that you might need to set the GNUPGHOME environment variable. If the
1608 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1609 environment variable has been set correctly and the passphrase is already
1610 known by the gpg-agent.
1612 The '--script' option specifies a custom script that is called whenever any
1613 of the dirsyncAttributes (see below) was changed. The script is called
1614 without any arguments. It gets the LDIF for exactly one object on STDIN.
1615 If the script processed the object successfully it has to respond with a
1616 single line starting with 'DONE-EXIT: ' followed by an optional message.
1618 Note that the script might be called without any password change, e.g. if
1619 the account was disabled (a userAccountControl change) or the
1620 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1621 are always returned as unique identifier of the account. It might be useful
1622 to also ask for non-password attributes like: objectSid, sAMAccountName,
1623 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1624 Depending on the object, some attributes may not be present/available,
1625 but you always get the current state (and not a diff).
1627 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1630 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1631 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1632 (!(sAMAccountName=krbtgt*)))
1633 This means only normal (non-krbtgt) user
1634 accounts are monitored. The '--filter' can modify that, e.g. if it's
1635 required to also sync computer accounts.
1641 This (default) mode runs in an endless loop waiting for password related
1642 changes in the active directory database. It makes use of the
1643 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1644 get changes in a reliable fashion. Objects are monitored for changes of the
1645 following dirsyncAttributes:
1647 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1648 userPrincipalName and userAccountControl.
1650 It recovers from LDAP disconnects and updates the cache in conservative way
1651 (in single steps after each successfully processed change). An error from
1652 the script (specified by '--script') will result in fatal error and this
1653 command will exit. But the cache state should be still valid and can be
1654 resumed in the next "Sync Loop Run".
1656 The '--logfile' option specifies an optional (required if '--daemon' is
1657 specified) logfile that takes all output of the command. The logfile is
1658 automatically reopened if fstat returns st_nlink == 0.
1660 The optional '--daemon' option will put the command into the background.
1662 You can stop the command without the '--daemon' option, also by hitting
1665 If you specify the '--no-wait' option the command skips the
1666 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1667 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1672 In order to terminate an already running command (likely as daemon) the
1673 '--terminate' option can be used. This also requires the '--logfile' option
1678 samba-tool user syncpasswords --cache-ldb-initialize \\
1679 --attributes=virtualClearTextUTF8
1680 samba-tool user syncpasswords
1683 samba-tool user syncpasswords --cache-ldb-initialize \\
1684 --attributes=objectGUID,objectSID,sAMAccountName,\\
1685 userPrincipalName,userAccountControl,pwdLastSet,\\
1686 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1687 --script=/path/to/my-custom-syncpasswords-script.py
1688 samba-tool user syncpasswords --daemon \\
1689 --logfile=/var/log/samba/user-syncpasswords.log
1690 samba-tool user syncpasswords --terminate \\
1691 --logfile=/var/log/samba/user-syncpasswords.log
1695 super(cmd_user_syncpasswords, self).__init__()
1697 synopsis = "%prog [--cache-ldb-initialize] [options]"
1699 takes_optiongroups = {
1700 "sambaopts": options.SambaOptions,
1701 "versionopts": options.VersionOptions,
1705 Option("--cache-ldb-initialize",
1706 help="Initialize the cache for the first time",
1707 dest="cache_ldb_initialize", action="store_true"),
1708 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1709 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1710 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1711 metavar="URL", dest="H"),
1712 Option("--filter", help="optional LDAP filter to set password on", type=str,
1713 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1714 Option("--attributes", type=str,
1715 help=virtual_attributes_help,
1716 metavar="ATTRIBUTELIST", dest="attributes"),
1717 Option("--decrypt-samba-gpg",
1718 help=decrypt_samba_gpg_help,
1719 action="store_true", default=False, dest="decrypt_samba_gpg"),
1720 Option("--script", help="Script that is called for each password change", type=str,
1721 metavar="/path/to/syncpasswords.script", dest="script"),
1722 Option("--no-wait", help="Don't block waiting for changes",
1723 action="store_true", default=False, dest="nowait"),
1724 Option("--logfile", type=str,
1725 help="The logfile to use (required in --daemon mode).",
1726 metavar="/path/to/syncpasswords.log", dest="logfile"),
1727 Option("--daemon", help="daemonize after initial setup",
1728 action="store_true", default=False, dest="daemon"),
1729 Option("--terminate",
1730 help="Send a SIGTERM to an already running (daemon) process",
1731 action="store_true", default=False, dest="terminate"),
1734 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1735 H=None, filter=None,
1736 attributes=None, decrypt_samba_gpg=None,
1737 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1738 sambaopts=None, versionopts=None):
1740 self.lp = sambaopts.get_loadparm()
1742 self.samdb_url = None
1746 if not cache_ldb_initialize:
1747 if attributes is not None:
1748 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1749 if decrypt_samba_gpg:
1750 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1751 if script is not None:
1752 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1753 if filter is not None:
1754 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1756 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1758 if nowait is not False:
1759 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1760 if logfile is not None:
1761 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1762 if daemon is not False:
1763 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1764 if terminate is not False:
1765 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1769 raise CommandError("--daemon is not allowed together with --no-wait")
1770 if terminate is not False:
1771 raise CommandError("--terminate is not allowed together with --no-wait")
1773 if terminate is True and daemon is True:
1774 raise CommandError("--terminate is not allowed together with --daemon")
1776 if daemon is True and logfile is None:
1777 raise CommandError("--daemon is only allowed together with --logfile")
1779 if terminate is True and logfile is None:
1780 raise CommandError("--terminate is only allowed together with --logfile")
1782 if script is not None:
1783 if not os.path.exists(script):
1784 raise CommandError("script[%s] does not exist!" % script)
1786 sync_command = "%s" % os.path.abspath(script)
1790 dirsync_filter = filter
1791 if dirsync_filter is None:
1792 dirsync_filter = "(&" + \
1793 "(objectClass=user)" + \
1794 "(userAccountControl:%s:=%u)" % (
1795 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1796 "(!(sAMAccountName=krbtgt*))" + \
1799 dirsync_secret_attrs = [
1802 "supplementalCredentials",
1805 dirsync_attrs = dirsync_secret_attrs + [
1808 "userPrincipalName",
1809 "userAccountControl",
1814 password_attrs = None
1816 if cache_ldb_initialize:
1818 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1820 if decrypt_samba_gpg and not gpgme_support:
1821 raise CommandError(decrypt_samba_gpg_help)
1823 password_attrs = self.parse_attributes(attributes)
1824 lower_attrs = [x.lower() for x in password_attrs]
1825 # We always return these in order to track deletions
1826 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1827 if a.lower() not in lower_attrs:
1828 password_attrs += [a]
1830 if cache_ldb is not None:
1831 if cache_ldb.lower().startswith("ldapi://"):
1832 raise CommandError("--cache_ldb ldapi:// is not supported")
1833 elif cache_ldb.lower().startswith("ldap://"):
1834 raise CommandError("--cache_ldb ldap:// is not supported")
1835 elif cache_ldb.lower().startswith("ldaps://"):
1836 raise CommandError("--cache_ldb ldaps:// is not supported")
1837 elif cache_ldb.lower().startswith("tdb://"):
1840 if not os.path.exists(cache_ldb):
1841 cache_ldb = self.lp.private_path(cache_ldb)
1843 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1845 self.lockfile = "%s.pid" % cache_ldb
1848 if self.logfile is not None:
1850 if info.st_nlink == 0:
1851 logfile = self.logfile
1853 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1854 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
1859 log_msg("Reopened logfile[%s]\n" % (logfile))
1860 self.logfile = logfile
1861 msg = "%s: pid[%d]: %s" % (
1865 self.outf.write(msg)
1874 "passwordAttribute",
1880 self.cache = Ldb(cache_ldb)
1881 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1882 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1886 self.samdb_url = str(res[0]["samdbUrl"][0])
1887 except KeyError as e:
1888 self.samdb_url = None
1890 self.samdb_url = None
1891 if self.samdb_url is None and not cache_ldb_initialize:
1892 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1894 if self.samdb_url is not None and cache_ldb_initialize:
1895 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1897 if self.samdb_url is None:
1899 self.dirsync_filter = dirsync_filter
1900 self.dirsync_attrs = dirsync_attrs
1901 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"]
1902 self.password_attrs = password_attrs
1903 self.decrypt_samba_gpg = decrypt_samba_gpg
1904 self.sync_command = sync_command
1905 add_ldif = "dn: %s\n" % self.cache_dn
1906 add_ldif += "objectClass: userSyncPasswords\n"
1907 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(get_bytes(self.samdb_url)).decode('utf8')
1908 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(get_bytes(self.dirsync_filter)).decode('utf8')
1909 for a in self.dirsync_attrs:
1910 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8')
1911 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1912 for a in self.password_attrs:
1913 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8')
1914 if self.decrypt_samba_gpg:
1915 add_ldif += "decryptSambaGPG: TRUE\n"
1917 add_ldif += "decryptSambaGPG: FALSE\n"
1918 if self.sync_command is not None:
1919 add_ldif += "syncCommand: %s\n" % self.sync_command
1920 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1921 self.cache.add_ldif(add_ldif)
1922 self.current_pid = None
1923 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1924 msgs = self.cache.parse_ldif(add_ldif)
1925 changetype, msg = next(msgs)
1926 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1927 self.outf.write("%s" % ldif)
1929 self.dirsync_filter = str(res[0]["dirsyncFilter"][0])
1930 self.dirsync_attrs = []
1931 for a in res[0]["dirsyncAttribute"]:
1932 self.dirsync_attrs.append(str(a))
1933 self.dirsync_controls = [str(res[0]["dirsyncControl"][0]), "extended_dn:1:0"]
1934 self.password_attrs = []
1935 for a in res[0]["passwordAttribute"]:
1936 self.password_attrs.append(str(a))
1937 decrypt_string = str(res[0]["decryptSambaGPG"][0])
1938 assert(decrypt_string in ["TRUE", "FALSE"])
1939 if decrypt_string == "TRUE":
1940 self.decrypt_samba_gpg = True
1942 self.decrypt_samba_gpg = False
1943 if "syncCommand" in res[0]:
1944 self.sync_command = str(res[0]["syncCommand"][0])
1946 self.sync_command = None
1947 if "currentPid" in res[0]:
1948 self.current_pid = int(res[0]["currentPid"][0])
1950 self.current_pid = None
1951 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1955 def run_sync_command(dn, ldif):
1956 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1957 sync_command_p = Popen(self.sync_command,
1962 res = sync_command_p.poll()
1965 input = "%s" % (ldif)
1966 reply = sync_command_p.communicate(input)[0]
1967 log_msg("%s\n" % (reply))
1968 res = sync_command_p.poll()
1970 sync_command_p.terminate()
1971 res = sync_command_p.wait()
1973 if reply.startswith("DONE-EXIT: "):
1976 log_msg("RESULT: %s\n" % (res))
1977 raise Exception("ERROR: %s - %s\n" % (res, reply))
1979 def handle_object(idx, dirsync_obj):
1980 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1981 guid = ndr_unpack(misc.GUID, binary_guid)
1982 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1983 sid = ndr_unpack(security.dom_sid, binary_sid)
1984 domain_sid, rid = sid.split()
1985 if rid == security.DOMAIN_RID_KRBTGT:
1986 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1988 for a in list(dirsync_obj.keys()):
1989 for h in dirsync_secret_attrs:
1990 if a.lower() == h.lower():
1992 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1993 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1994 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
1995 obj = self.get_account_attributes(self.samdb,
1996 username="%s" % sid,
1997 basedn="<GUID=%s>" % guid,
1998 filter="(objectClass=user)",
1999 scope=ldb.SCOPE_BASE,
2000 attrs=self.password_attrs,
2001 decrypt=self.decrypt_samba_gpg)
2002 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
2003 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
2004 if self.sync_command is None:
2005 self.outf.write("%s" % (ldif))
2007 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
2008 run_sync_command(obj.dn, ldif)
2010 def check_current_pid_conflict(terminate):
2016 self.lockfd = os.open(self.lockfile, flags, 0o600)
2017 except IOError as e4:
2018 (err, msg) = e4.args
2019 if err == errno.ENOENT:
2022 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
2023 (self.lockfile, msg, err))
2026 got_exclusive = False
2028 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2029 got_exclusive = True
2030 except IOError as e5:
2031 (err, msg) = e5.args
2032 if err != errno.EACCES and err != errno.EAGAIN:
2033 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
2034 (self.lockfile, msg, err))
2037 if not got_exclusive:
2038 buf = os.read(self.lockfd, 64)
2039 self.current_pid = None
2041 self.current_pid = int(buf)
2042 except ValueError as e:
2044 if self.current_pid is not None:
2047 if got_exclusive and terminate:
2049 os.ftruncate(self.lockfd, 0)
2050 except IOError as e2:
2051 (err, msg) = e2.args
2052 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2053 (self.lockfile, msg, err))
2055 os.close(self.lockfd)
2060 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2061 except IOError as e6:
2062 (err, msg) = e6.args
2063 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2064 (self.lockfile, msg, err))
2066 # We leave the function with the shared lock.
2069 def update_pid(pid):
2070 if self.lockfd != -1:
2071 got_exclusive = False
2072 # Try 5 times to get the exclusiv lock.
2073 for i in range(0, 5):
2075 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2076 got_exclusive = True
2077 except IOError as e:
2079 if err != errno.EACCES and err != errno.EAGAIN:
2080 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2081 (pid, self.lockfile, msg, err))
2086 if not got_exclusive:
2087 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
2088 (pid, self.lockfile))
2089 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2090 (pid, self.lockfile))
2097 os.ftruncate(self.lockfd, 0)
2099 os.write(self.lockfd, get_bytes(buf))
2100 except IOError as e3:
2101 (err, msg) = e3.args
2102 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2103 (self.lockfile, msg, err))
2105 self.current_pid = pid
2106 if self.current_pid is not None:
2107 log_msg("currentPid: %d\n" % self.current_pid)
2109 modify_ldif = "dn: %s\n" % (self.cache_dn)
2110 modify_ldif += "changetype: modify\n"
2111 modify_ldif += "replace: currentPid\n"
2112 if self.current_pid is not None:
2113 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2114 modify_ldif += "replace: currentTime\n"
2115 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2116 self.cache.modify_ldif(modify_ldif)
2119 def update_cache(res_controls):
2120 assert len(res_controls) > 0
2121 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2122 res_controls[0].critical = True
2123 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2124 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2126 modify_ldif = "dn: %s\n" % (self.cache_dn)
2127 modify_ldif += "changetype: modify\n"
2128 modify_ldif += "replace: dirsyncControl\n"
2129 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2130 modify_ldif += "replace: currentTime\n"
2131 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2132 self.cache.modify_ldif(modify_ldif)
2135 def check_object(dirsync_obj, res_controls):
2136 assert len(res_controls) > 0
2137 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2139 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2140 sid = ndr_unpack(security.dom_sid, binary_sid)
2142 lastCookie = str(res_controls[0])
2144 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2145 expression="(lastCookie=%s)" % (
2146 ldb.binary_encode(lastCookie)),
2152 def update_object(dirsync_obj, res_controls):
2153 assert len(res_controls) > 0
2154 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2156 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2157 sid = ndr_unpack(security.dom_sid, binary_sid)
2159 lastCookie = str(res_controls[0])
2161 self.cache.transaction_start()
2163 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2164 expression="(objectClass=*)",
2165 attrs=["lastCookie"])
2167 add_ldif = "dn: %s\n" % (dn)
2168 add_ldif += "objectClass: userCookie\n"
2169 add_ldif += "lastCookie: %s\n" % (lastCookie)
2170 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2171 self.cache.add_ldif(add_ldif)
2173 modify_ldif = "dn: %s\n" % (dn)
2174 modify_ldif += "changetype: modify\n"
2175 modify_ldif += "replace: lastCookie\n"
2176 modify_ldif += "lastCookie: %s\n" % (lastCookie)
2177 modify_ldif += "replace: currentTime\n"
2178 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2179 self.cache.modify_ldif(modify_ldif)
2180 self.cache.transaction_commit()
2181 except Exception as e:
2182 self.cache.transaction_cancel()
2188 res = self.samdb.search(expression=str(self.dirsync_filter),
2189 scope=ldb.SCOPE_SUBTREE,
2190 attrs=self.dirsync_attrs,
2191 controls=self.dirsync_controls)
2192 log_msg("dirsync_loop(): results %d\n" % len(res))
2195 done = check_object(r, res.controls)
2197 handle_object(ri, r)
2198 update_object(r, res.controls)
2200 update_cache(res.controls)
2204 def sync_loop(wait):
2205 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2206 notify_controls = ["notification:1", "show_recycled:1"]
2207 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2208 scope=ldb.SCOPE_SUBTREE,
2210 controls=notify_controls,
2214 log_msg("Resuming monitoring\n")
2216 log_msg("Getting changes\n")
2217 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2218 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2219 self.outf.write("syncCommand: %s\n" % self.sync_command)
2222 if wait is not True:
2225 for msg in notify_handle:
2226 if not isinstance(msg, ldb.Message):
2227 self.outf.write("referal: %s\n" % msg)
2229 created = msg.get("uSNCreated")[0]
2230 changed = msg.get("uSNChanged")[0]
2231 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2232 (msg.dn, created, changed))
2236 res = notify_handle.result()
2241 orig_pid = os.getpid()
2246 if pid == 0: # Actual daemon
2248 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2253 if cache_ldb_initialize:
2255 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2260 if logfile is not None:
2261 import resource # Resource usage information.
2262 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2263 if maxfd == resource.RLIM_INFINITY:
2264 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2265 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2266 self.outf.write("Using logfile[%s]\n" % logfile)
2267 for fd in range(0, maxfd):
2278 log_msg("Attached to logfile[%s]\n" % (logfile))
2279 self.logfile = logfile
2282 conflict = check_current_pid_conflict(terminate)
2284 if self.current_pid is None:
2285 log_msg("No process running.\n")
2288 log_msg("Proccess %d is not running anymore.\n" % (
2292 log_msg("Sending SIGTERM to proccess %d.\n" % (
2294 os.kill(self.current_pid, signal.SIGTERM)
2297 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2298 os.getpid(), self.current_pid))
2302 update_pid(os.getpid())
2307 retry_sleep_max = 600
2312 retry_sleep = retry_sleep_min
2314 while self.samdb is None:
2315 if retry_sleep != 0:
2316 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2317 time.sleep(retry_sleep)
2318 retry_sleep = retry_sleep * 2
2319 if retry_sleep >= retry_sleep_max:
2320 retry_sleep = retry_sleep_max
2321 log_msg("Connecting to '%s'\n" % self.samdb_url)
2323 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2324 except Exception as msg:
2326 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2327 if wait is not True:
2332 except ldb.LdbError as e7:
2333 (enum, estr) = e7.args
2335 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2341 class cmd_user_edit(Command):
2342 """Modify User AD object.
2344 This command will allow editing of a user account in the Active Directory
2345 domain. You will then be able to add or change attributes and their values.
2347 The username specified on the command is the sAMAccountName.
2349 The command may be run from the root userid or another authorized userid.
2351 The -H or --URL= option can be used to execute the command against a remote
2355 samba-tool user edit User1 -H ldap://samba.samdom.example.com \
2356 -U administrator --password=passw1rd
2358 Example1 shows how to edit a users attributes in the domain against a remote
2361 The -H parameter is used to specify the remote target server.
2364 samba-tool user edit User2
2366 Example2 shows how to edit a users attributes in the domain against a local
2370 samba-tool user edit User3 --editor=nano
2372 Example3 shows how to edit a users attributes in the domain against a local
2373 LDAP server using the 'nano' editor.
2376 synopsis = "%prog <username> [options]"
2379 Option("-H", "--URL", help="LDB URL for database or target server",
2380 type=str, metavar="URL", dest="H"),
2381 Option("--editor", help="Editor to use instead of the system default,"
2382 " or 'vi' if no system default is set.", type=str),
2385 takes_args = ["username"]
2386 takes_optiongroups = {
2387 "sambaopts": options.SambaOptions,
2388 "credopts": options.CredentialsOptions,
2389 "versionopts": options.VersionOptions,
2392 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2393 H=None, editor=None):
2395 lp = sambaopts.get_loadparm()
2396 creds = credopts.get_credentials(lp, fallback_machine=True)
2397 samdb = SamDB(url=H, session_info=system_session(),
2398 credentials=creds, lp=lp)
2400 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2401 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2403 domaindn = samdb.domain_dn()
2406 res = samdb.search(base=domaindn,
2408 scope=ldb.SCOPE_SUBTREE)
2411 raise CommandError('Unable to find user "%s"' % (username))
2414 r_ldif = samdb.write_ldif(msg, 1)
2415 # remove 'changetype' line
2416 result_ldif = re.sub('changetype: add\n', '', r_ldif)
2419 editor = os.environ.get('EDITOR')
2423 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2424 t_file.write(result_ldif)
2427 check_call([editor, t_file.name])
2428 except CalledProcessError as e:
2429 raise CalledProcessError("ERROR: ", e)
2430 with open(t_file.name) as edited_file:
2431 edited_message = edited_file.read()
2433 if result_ldif != edited_message:
2434 diff = difflib.ndiff(result_ldif.splitlines(),
2435 edited_message.splitlines())
2439 if line.startswith('-'):
2441 minus_lines.append(line)
2442 elif line.startswith('+'):
2444 plus_lines.append(line)
2446 user_ldif = "dn: %s\n" % user_dn
2447 user_ldif += "changetype: modify\n"
2449 for line in minus_lines:
2450 attr, val = line.split(':', 1)
2451 search_attr = "%s:" % attr
2452 if not re.search(r'^' + search_attr, str(plus_lines)):
2453 user_ldif += "delete: %s\n" % attr
2454 user_ldif += "%s: %s\n" % (attr, val)
2456 for line in plus_lines:
2457 attr, val = line.split(':', 1)
2458 search_attr = "%s:" % attr
2459 if re.search(r'^' + search_attr, str(minus_lines)):
2460 user_ldif += "replace: %s\n" % attr
2461 user_ldif += "%s: %s\n" % (attr, val)
2462 if not re.search(r'^' + search_attr, str(minus_lines)):
2463 user_ldif += "add: %s\n" % attr
2464 user_ldif += "%s: %s\n" % (attr, val)
2467 samdb.modify_ldif(user_ldif)
2468 except Exception as e:
2469 raise CommandError("Failed to modify user '%s': " %
2472 self.outf.write("Modified User '%s' successfully\n" % username)
2475 class cmd_user_show(Command):
2476 """Display a user AD object.
2478 This command displays a user account and it's attributes in the Active
2480 The username specified on the command is the sAMAccountName.
2482 The command may be run from the root userid or another authorized userid.
2484 The -H or --URL= option can be used to execute the command against a remote
2488 samba-tool user show User1 -H ldap://samba.samdom.example.com \
2489 -U administrator --password=passw1rd
2491 Example1 shows how to display a users attributes in the domain against a remote
2494 The -H parameter is used to specify the remote target server.
2497 samba-tool user show User2
2499 Example2 shows how to display a users attributes in the domain against a local
2503 samba-tool user show User2 --attributes=objectSid,memberOf
2505 Example3 shows how to display a users objectSid and memberOf attributes.
2507 synopsis = "%prog <username> [options]"
2510 Option("-H", "--URL", help="LDB URL for database or target server",
2511 type=str, metavar="URL", dest="H"),
2512 Option("--attributes",
2513 help=("Comma separated list of attributes, "
2514 "which will be printed."),
2515 type=str, dest="user_attrs"),
2518 takes_args = ["username"]
2519 takes_optiongroups = {
2520 "sambaopts": options.SambaOptions,
2521 "credopts": options.CredentialsOptions,
2522 "versionopts": options.VersionOptions,
2525 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2526 H=None, user_attrs=None):
2528 lp = sambaopts.get_loadparm()
2529 creds = credopts.get_credentials(lp, fallback_machine=True)
2530 samdb = SamDB(url=H, session_info=system_session(),
2531 credentials=creds, lp=lp)
2535 attrs = user_attrs.split(",")
2537 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2538 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2540 domaindn = samdb.domain_dn()
2543 res = samdb.search(base=domaindn, expression=filter,
2544 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2547 raise CommandError('Unable to find user "%s"' % (username))
2550 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2551 self.outf.write(user_ldif)
2554 class cmd_user_move(Command):
2555 """Move a user to an organizational unit/container.
2557 This command moves a user account into the specified organizational unit
2559 The username specified on the command is the sAMAccountName.
2560 The name of the organizational unit or container can be specified as a
2561 full DN or without the domainDN component.
2563 The command may be run from the root userid or another authorized userid.
2565 The -H or --URL= option can be used to execute the command against a remote
2569 samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
2570 -H ldap://samba.samdom.example.com -U administrator
2572 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2573 unit on a remote LDAP server.
2575 The -H parameter is used to specify the remote target server.
2578 samba-tool user move User1 CN=Users
2580 Example2 shows how to move a user User1 back into the CN=Users container
2581 on the local server.
2584 synopsis = "%prog <username> <new_parent_dn> [options]"
2587 Option("-H", "--URL", help="LDB URL for database or target server",
2588 type=str, metavar="URL", dest="H"),
2591 takes_args = ["username", "new_parent_dn"]
2592 takes_optiongroups = {
2593 "sambaopts": options.SambaOptions,
2594 "credopts": options.CredentialsOptions,
2595 "versionopts": options.VersionOptions,
2598 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2599 versionopts=None, H=None):
2600 lp = sambaopts.get_loadparm()
2601 creds = credopts.get_credentials(lp, fallback_machine=True)
2602 samdb = SamDB(url=H, session_info=system_session(),
2603 credentials=creds, lp=lp)
2604 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2606 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2607 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2609 res = samdb.search(base=domain_dn,
2611 scope=ldb.SCOPE_SUBTREE)
2614 raise CommandError('Unable to find user "%s"' % (username))
2617 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2618 except Exception as e:
2619 raise CommandError('Invalid new_parent_dn "%s": %s' %
2620 (new_parent_dn, e.message))
2622 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2623 full_new_user_dn.remove_base_components(len(user_dn) - 1)
2624 full_new_user_dn.add_base(full_new_parent_dn)
2627 samdb.rename(user_dn, full_new_user_dn)
2628 except Exception as e:
2629 raise CommandError('Failed to move user "%s"' % username, e)
2630 self.outf.write('Moved user "%s" into "%s"\n' %
2631 (username, full_new_parent_dn))
2634 class cmd_user(SuperCommand):
2635 """User management."""
2638 subcommands["add"] = cmd_user_add()
2639 subcommands["create"] = cmd_user_create()
2640 subcommands["delete"] = cmd_user_delete()
2641 subcommands["disable"] = cmd_user_disable()
2642 subcommands["enable"] = cmd_user_enable()
2643 subcommands["list"] = cmd_user_list()
2644 subcommands["setexpiry"] = cmd_user_setexpiry()
2645 subcommands["password"] = cmd_user_password()
2646 subcommands["setpassword"] = cmd_user_setpassword()
2647 subcommands["getpassword"] = cmd_user_getpassword()
2648 subcommands["syncpasswords"] = cmd_user_syncpasswords()
2649 subcommands["edit"] = cmd_user_edit()
2650 subcommands["show"] = cmd_user_show()
2651 subcommands["move"] = cmd_user_move()