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
31 from subprocess import Popen, PIPE, STDOUT
32 from getpass import getpass
33 from samba.auth import system_session
34 from samba.samdb import SamDB
35 from samba.dcerpc import misc
36 from samba.dcerpc import security
37 from samba.dcerpc import drsblobs
38 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
43 generate_random_password,
46 from samba.net import Net
48 from samba.netcmd import (
60 decrypt_samba_gpg_help = "Decrypt the SambaGPG password as cleartext source"
61 except ImportError as e:
63 decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
64 "python-gpgme required"
66 disabled_virtual_attributes = {
69 virtual_attributes = {
70 "virtualClearTextUTF8": {
71 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
73 "virtualClearTextUTF16": {
74 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
77 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
81 get_random_bytes_fn = None
82 if get_random_bytes_fn is None:
85 get_random_bytes_fn = Crypto.Random.get_random_bytes
86 except ImportError as e:
88 if get_random_bytes_fn is None:
91 get_random_bytes_fn = M2Crypto.Rand.rand_bytes
92 except ImportError as e:
96 if get_random_bytes_fn is not None:
98 return "Crypto.Random or M2Crypto.Rand required"
100 def get_random_bytes(num):
101 random_reason = check_random()
102 if random_reason is not None:
103 raise ImportError(random_reason)
104 return get_random_bytes_fn(num)
106 def get_crypt_value(alg, utf8pw):
112 salt = get_random_bytes(16)
113 # The salt needs to be in [A-Za-z0-9./]
114 # base64 is close enough and as we had 16
115 # random bytes but only need 16 characters
116 # we can ignore the possible == at the end
117 # of the base64 string
118 # we just need to replace '+' by '.'
119 b64salt = base64.b64encode(salt)
120 crypt_salt = "$%s$%s$" % (alg, b64salt[0:16].replace('+', '.'))
121 crypt_value = crypt.crypt(utf8pw, crypt_salt)
122 if crypt_value is None:
123 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
124 expected_len = len(crypt_salt) + algs[alg]["length"]
125 if len(crypt_value) != expected_len:
126 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
127 crypt_salt, len(crypt_value), expected_len))
131 random_reason = check_random()
132 if random_reason is not None:
133 raise ImportError(random_reason)
137 virtual_attributes["virtualSSHA"] = {
139 except ImportError as e:
140 reason = "hashlib.sha1()"
142 reason += " and " + random_reason
143 reason += " required"
144 disabled_virtual_attributes["virtualSSHA"] = {
148 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
150 random_reason = check_random()
151 if random_reason is not None:
152 raise ImportError(random_reason)
154 v = get_crypt_value(alg, "")
156 virtual_attributes[attr] = {
158 except ImportError as e:
161 reason += " and " + random_reason
162 reason += " required"
163 disabled_virtual_attributes[attr] = {
166 except NotImplementedError as e:
167 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
168 disabled_virtual_attributes[attr] = {
172 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
173 for x in range(1, 30):
174 virtual_attributes["virtualWDigest%02d" % x] = {}
176 virtual_attributes_help = "The attributes to display (comma separated). "
177 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
178 if len(disabled_virtual_attributes) != 0:
179 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
181 class cmd_user_create(Command):
182 """Create a new user.
184 This command creates a new user account in the Active Directory domain. The username specified on the command is the sAMaccountName.
186 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).
188 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.
190 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.
192 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.
195 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
197 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.
200 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
202 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.
205 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
207 Example3 shows how to create a new user in the OrgUnit organizational unit.
210 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
212 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'.
215 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
216 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
218 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
219 --nis-domain is set, then the other four parameters are mandatory.
222 synopsis = "%prog <username> [<password>] [options]"
225 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
226 metavar="URL", dest="H"),
227 Option("--must-change-at-next-login",
228 help="Force password to be changed on next login",
229 action="store_true"),
230 Option("--random-password",
231 help="Generate random password",
232 action="store_true"),
233 Option("--smartcard-required",
234 help="Require a smartcard for interactive logons",
235 action="store_true"),
236 Option("--use-username-as-cn",
237 help="Force use of username as user's CN",
238 action="store_true"),
240 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>'",
242 Option("--surname", help="User's surname", type=str),
243 Option("--given-name", help="User's given name", type=str),
244 Option("--initials", help="User's initials", type=str),
245 Option("--profile-path", help="User's profile path", type=str),
246 Option("--script-path", help="User's logon script path", type=str),
247 Option("--home-drive", help="User's home drive letter", type=str),
248 Option("--home-directory", help="User's home directory path", type=str),
249 Option("--job-title", help="User's job title", type=str),
250 Option("--department", help="User's department", type=str),
251 Option("--company", help="User's company", type=str),
252 Option("--description", help="User's description", type=str),
253 Option("--mail-address", help="User's email address", type=str),
254 Option("--internet-address", help="User's home page", type=str),
255 Option("--telephone-number", help="User's phone number", type=str),
256 Option("--physical-delivery-office", help="User's office location", type=str),
257 Option("--rfc2307-from-nss",
258 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
259 action="store_true"),
260 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
261 Option("--unix-home", help="User's Unix/RFC2307 home directory",
263 Option("--uid", help="User's Unix/RFC2307 username", type=str),
264 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
265 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
266 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
267 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
270 takes_args = ["username", "password?"]
272 takes_optiongroups = {
273 "sambaopts": options.SambaOptions,
274 "credopts": options.CredentialsOptions,
275 "versionopts": options.VersionOptions,
278 def run(self, username, password=None, credopts=None, sambaopts=None,
279 versionopts=None, H=None, must_change_at_next_login=False,
280 random_password=False, use_username_as_cn=False, userou=None,
281 surname=None, given_name=None, initials=None, profile_path=None,
282 script_path=None, home_drive=None, home_directory=None,
283 job_title=None, department=None, company=None, description=None,
284 mail_address=None, internet_address=None, telephone_number=None,
285 physical_delivery_office=None, rfc2307_from_nss=False,
286 nis_domain=None, unix_home=None, uid=None, uid_number=None,
287 gid_number=None, gecos=None, login_shell=None,
288 smartcard_required=False):
290 if smartcard_required:
291 if password is not None and password is not '':
292 raise CommandError('It is not allowed to specify '
294 'together with --smartcard-required.')
295 if must_change_at_next_login:
296 raise CommandError('It is not allowed to specify '
297 '--must-change-at-next-login '
298 'together with --smartcard-required.')
300 if random_password and not smartcard_required:
301 password = generate_random_password(128, 255)
304 if smartcard_required:
306 if password is not None and password is not '':
308 password = getpass("New Password: ")
309 passwordverify = getpass("Retype Password: ")
310 if not password == passwordverify:
312 self.outf.write("Sorry, passwords do not match.\n")
315 pwent = pwd.getpwnam(username)
318 if uid_number is None:
319 uid_number = pwent[2]
320 if gid_number is None:
321 gid_number = pwent[3]
324 if login_shell is None:
325 login_shell = pwent[6]
327 lp = sambaopts.get_loadparm()
328 creds = credopts.get_credentials(lp)
330 if uid_number or gid_number:
331 if not lp.get("idmap_ldb:use rfc2307"):
332 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")
334 if nis_domain is not None:
335 if None in (uid_number, login_shell, unix_home, gid_number):
336 raise CommandError('Missing parameters. To enable NIS features, '
337 'the following options have to be given: '
338 '--nis-domain=, --uidNumber=, --login-shell='
339 ', --unix-home=, --gid-number= Operation '
343 samdb = SamDB(url=H, session_info=system_session(),
344 credentials=creds, lp=lp)
345 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
346 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
347 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
348 jobtitle=job_title, department=department, company=company, description=description,
349 mailaddress=mail_address, internetaddress=internet_address,
350 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
351 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
352 uidnumber=uid_number, gidnumber=gid_number,
353 gecos=gecos, loginshell=login_shell,
354 smartcard_required=smartcard_required)
356 raise CommandError("Failed to add user '%s': " % username, e)
358 self.outf.write("User '%s' created successfully\n" % username)
361 class cmd_user_add(cmd_user_create):
362 __doc__ = cmd_user_create.__doc__
363 # take this print out after the add subcommand is removed.
364 # the add subcommand is deprecated but left in for now to allow people to
367 def run(self, *args, **kwargs):
369 "Note: samba-tool user add is deprecated. "
370 "Please use samba-tool user create for the same function.\n")
371 return super(cmd_user_add, self).run(*args, **kwargs)
374 class cmd_user_delete(Command):
377 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
379 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.
381 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.
384 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
386 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.
389 sudo samba-tool user delete User2
391 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.
394 synopsis = "%prog <username> [options]"
397 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
398 metavar="URL", dest="H"),
401 takes_args = ["username"]
402 takes_optiongroups = {
403 "sambaopts": options.SambaOptions,
404 "credopts": options.CredentialsOptions,
405 "versionopts": options.VersionOptions,
408 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
410 lp = sambaopts.get_loadparm()
411 creds = credopts.get_credentials(lp, fallback_machine=True)
413 samdb = SamDB(url=H, session_info=system_session(),
414 credentials=creds, lp=lp)
416 filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
420 res = samdb.search(base=samdb.domain_dn(),
421 scope=ldb.SCOPE_SUBTREE,
426 raise CommandError('Unable to find user "%s"' % (username))
429 samdb.delete(user_dn)
431 raise CommandError('Failed to remove user "%s"' % username, e)
432 self.outf.write("Deleted user %s\n" % username)
435 class cmd_user_list(Command):
436 """List all users."""
438 synopsis = "%prog [options]"
441 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
442 metavar="URL", dest="H"),
445 takes_optiongroups = {
446 "sambaopts": options.SambaOptions,
447 "credopts": options.CredentialsOptions,
448 "versionopts": options.VersionOptions,
451 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
452 lp = sambaopts.get_loadparm()
453 creds = credopts.get_credentials(lp, fallback_machine=True)
455 samdb = SamDB(url=H, session_info=system_session(),
456 credentials=creds, lp=lp)
458 domain_dn = samdb.domain_dn()
459 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
460 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
461 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
462 attrs=["samaccountname"])
467 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
470 class cmd_user_enable(Command):
473 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.
475 There are many reasons why an account may become disabled. These include:
476 - If a user exceeds the account policy for logon attempts
477 - If an administrator disables the account
478 - If the account expires
480 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
482 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.
484 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.
487 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
489 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.
492 su samba-tool user enable Testuser2
494 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.
497 samba-tool user enable --filter=samaccountname=Testuser3
499 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
502 synopsis = "%prog (<username>|--filter <filter>) [options]"
505 takes_optiongroups = {
506 "sambaopts": options.SambaOptions,
507 "versionopts": options.VersionOptions,
508 "credopts": options.CredentialsOptions,
512 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
513 metavar="URL", dest="H"),
514 Option("--filter", help="LDAP Filter to set password on", type=str),
517 takes_args = ["username?"]
519 def run(self, username=None, sambaopts=None, credopts=None,
520 versionopts=None, filter=None, H=None):
521 if username is None and filter is None:
522 raise CommandError("Either the username or '--filter' must be specified!")
525 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
527 lp = sambaopts.get_loadparm()
528 creds = credopts.get_credentials(lp, fallback_machine=True)
530 samdb = SamDB(url=H, session_info=system_session(),
531 credentials=creds, lp=lp)
533 samdb.enable_account(filter)
534 except Exception, msg:
535 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
536 self.outf.write("Enabled user '%s'\n" % (username or filter))
539 class cmd_user_disable(Command):
540 """Disable an user."""
542 synopsis = "%prog (<username>|--filter <filter>) [options]"
545 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
546 metavar="URL", dest="H"),
547 Option("--filter", help="LDAP Filter to set password on", type=str),
550 takes_args = ["username?"]
552 takes_optiongroups = {
553 "sambaopts": options.SambaOptions,
554 "credopts": options.CredentialsOptions,
555 "versionopts": options.VersionOptions,
558 def run(self, username=None, sambaopts=None, credopts=None,
559 versionopts=None, filter=None, H=None):
560 if username is None and filter is None:
561 raise CommandError("Either the username or '--filter' must be specified!")
564 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
566 lp = sambaopts.get_loadparm()
567 creds = credopts.get_credentials(lp, fallback_machine=True)
569 samdb = SamDB(url=H, session_info=system_session(),
570 credentials=creds, lp=lp)
572 samdb.disable_account(filter)
573 except Exception, msg:
574 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
577 class cmd_user_setexpiry(Command):
578 """Set the expiration of a user account.
580 The user can either be specified by their sAMAccountName or using the --filter option.
582 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.
584 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.
587 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
589 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.
592 su samba-tool user setexpiry User2
594 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.
597 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
599 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.
602 samba-tool user setexpiry --noexpiry User4
603 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
606 synopsis = "%prog (<username>|--filter <filter>) [options]"
608 takes_optiongroups = {
609 "sambaopts": options.SambaOptions,
610 "versionopts": options.VersionOptions,
611 "credopts": options.CredentialsOptions,
615 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
616 metavar="URL", dest="H"),
617 Option("--filter", help="LDAP Filter to set password on", type=str),
618 Option("--days", help="Days to expiry", type=int, default=0),
619 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
622 takes_args = ["username?"]
624 def run(self, username=None, sambaopts=None, credopts=None,
625 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
626 if username is None and filter is None:
627 raise CommandError("Either the username or '--filter' must be specified!")
630 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
632 lp = sambaopts.get_loadparm()
633 creds = credopts.get_credentials(lp)
635 samdb = SamDB(url=H, session_info=system_session(),
636 credentials=creds, lp=lp)
639 samdb.setexpiry(filter, days*24*3600, no_expiry_req=noexpiry)
640 except Exception, msg:
641 # FIXME: Catch more specific exception
642 raise CommandError("Failed to set expiry for user '%s': %s" % (
643 username or filter, msg))
645 self.outf.write("Expiry for user '%s' disabled.\n" % (
648 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
649 username or filter, days))
652 class cmd_user_password(Command):
653 """Change password for a user account (the one provided in authentication).
656 synopsis = "%prog [options]"
659 Option("--newpassword", help="New password", type=str),
662 takes_optiongroups = {
663 "sambaopts": options.SambaOptions,
664 "credopts": options.CredentialsOptions,
665 "versionopts": options.VersionOptions,
668 def run(self, credopts=None, sambaopts=None, versionopts=None,
671 lp = sambaopts.get_loadparm()
672 creds = credopts.get_credentials(lp)
674 # get old password now, to get the password prompts in the right order
675 old_password = creds.get_password()
677 net = Net(creds, lp, server=credopts.ipaddress)
679 password = newpassword
681 if password is not None and password is not '':
683 password = getpass("New Password: ")
684 passwordverify = getpass("Retype Password: ")
685 if not password == passwordverify:
687 self.outf.write("Sorry, passwords do not match.\n")
690 net.change_password(password.encode('utf-8'))
691 except Exception, msg:
692 # FIXME: catch more specific exception
693 raise CommandError("Failed to change password : %s" % msg)
694 self.outf.write("Changed password OK\n")
697 class cmd_user_setpassword(Command):
698 """Set or reset the password of a user account.
700 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.
702 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.
704 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.
706 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.
709 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
711 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.
714 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
716 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.
719 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
721 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
724 synopsis = "%prog (<username>|--filter <filter>) [options]"
726 takes_optiongroups = {
727 "sambaopts": options.SambaOptions,
728 "versionopts": options.VersionOptions,
729 "credopts": options.CredentialsOptions,
733 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
734 metavar="URL", dest="H"),
735 Option("--filter", help="LDAP Filter to set password on", type=str),
736 Option("--newpassword", help="Set password", type=str),
737 Option("--must-change-at-next-login",
738 help="Force password to be changed on next login",
739 action="store_true"),
740 Option("--random-password",
741 help="Generate random password",
742 action="store_true"),
743 Option("--smartcard-required",
744 help="Require a smartcard for interactive logons",
745 action="store_true"),
746 Option("--clear-smartcard-required",
747 help="Don't require a smartcard for interactive logons",
748 action="store_true"),
751 takes_args = ["username?"]
753 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
754 versionopts=None, H=None, newpassword=None,
755 must_change_at_next_login=False, random_password=False,
756 smartcard_required=False, clear_smartcard_required=False):
757 if filter is None and username is None:
758 raise CommandError("Either the username or '--filter' must be specified!")
760 password = newpassword
762 if smartcard_required:
763 if password is not None and password is not '':
764 raise CommandError('It is not allowed to specify '
766 'together with --smartcard-required.')
767 if must_change_at_next_login:
768 raise CommandError('It is not allowed to specify '
769 '--must-change-at-next-login '
770 'together with --smartcard-required.')
771 if clear_smartcard_required:
772 raise CommandError('It is not allowed to specify '
773 '--clear-smartcard-required '
774 'together with --smartcard-required.')
776 if random_password and not smartcard_required:
777 password = generate_random_password(128, 255)
780 if smartcard_required:
782 if password is not None and password is not '':
784 password = getpass("New Password: ")
785 passwordverify = getpass("Retype Password: ")
786 if not password == passwordverify:
788 self.outf.write("Sorry, passwords do not match.\n")
791 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
793 lp = sambaopts.get_loadparm()
794 creds = credopts.get_credentials(lp)
796 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
798 samdb = SamDB(url=H, session_info=system_session(),
799 credentials=creds, lp=lp)
801 if smartcard_required:
804 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
805 flags = dsdb.UF_SMARTCARD_REQUIRED
806 samdb.toggle_userAccountFlags(filter, flags, on=True)
807 command = "Failed to enable account for user '%s'" % (username or filter)
808 samdb.enable_account(filter)
809 except Exception, msg:
810 # FIXME: catch more specific exception
811 raise CommandError("%s: %s" % (command, msg))
812 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
816 if clear_smartcard_required:
817 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
818 flags = dsdb.UF_SMARTCARD_REQUIRED
819 samdb.toggle_userAccountFlags(filter, flags, on=False)
820 command = "Failed to set password for user '%s'" % (username or filter)
821 samdb.setpassword(filter, password,
822 force_change_at_next_login=must_change_at_next_login,
824 except Exception, msg:
825 # FIXME: catch more specific exception
826 raise CommandError("%s: %s" % (command, msg))
827 self.outf.write("Changed password OK\n")
829 class GetPasswordCommand(Command):
832 super(GetPasswordCommand, self).__init__()
835 def connect_system_samdb(self, url, allow_local=False, verbose=False):
837 # using anonymous here, results in no authentication
838 # which means we can get system privileges via
839 # the privileged ldapi socket
840 creds = credentials.Credentials()
841 creds.set_anonymous()
843 if url is None and allow_local:
845 elif url.lower().startswith("ldapi://"):
847 elif url.lower().startswith("ldap://"):
848 raise CommandError("--url ldap:// is not supported for this command")
849 elif url.lower().startswith("ldaps://"):
850 raise CommandError("--url ldaps:// is not supported for this command")
851 elif not allow_local:
852 raise CommandError("--url requires an ldapi:// url for this command")
855 self.outf.write("Connecting to '%s'\n" % url)
857 samdb = SamDB(url=url, session_info=system_session(),
858 credentials=creds, lp=self.lp)
862 # Make sure we're connected as SYSTEM
864 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
866 sids = res[0].get("tokenGroups")
867 assert len(sids) == 1
868 sid = ndr_unpack(security.dom_sid, sids[0])
869 assert str(sid) == security.SID_NT_SYSTEM
870 except Exception as msg:
871 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
872 (security.SID_NT_SYSTEM))
874 # We use sort here in order to have a predictable processing order
875 # this might not be strictly needed, but also doesn't hurt here
876 for a in sorted(virtual_attributes.keys()):
877 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
878 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
882 def get_account_attributes(self, samdb, username, basedn, filter, scope,
885 require_supplementalCredentials = False
886 search_attrs = attrs[:]
887 lower_attrs = [x.lower() for x in search_attrs]
888 for a in virtual_attributes.keys():
889 if a.lower() in lower_attrs:
890 require_supplementalCredentials = True
891 add_supplementalCredentials = False
892 add_unicodePwd = False
893 if require_supplementalCredentials:
894 a = "supplementalCredentials"
895 if a.lower() not in lower_attrs:
897 add_supplementalCredentials = True
899 if a.lower() not in lower_attrs:
901 add_unicodePwd = True
902 add_sAMAcountName = False
904 if a.lower() not in lower_attrs:
906 add_sAMAcountName = True
908 add_userPrincipalName = False
909 upn = "usePrincipalName"
910 if upn.lower() not in lower_attrs:
911 search_attrs += [upn]
912 add_userPrincipalName = True
914 if scope == ldb.SCOPE_BASE:
915 search_controls = ["show_deleted:1", "show_recycled:1"]
919 res = samdb.search(base=basedn, expression=filter,
920 scope=scope, attrs=search_attrs,
921 controls=search_controls)
923 raise Exception('Unable to find user "%s"' % (username or filter))
925 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
926 except Exception as msg:
927 # FIXME: catch more specific exception
928 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
933 if "supplementalCredentials" in obj:
934 sc_blob = obj["supplementalCredentials"][0]
935 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
936 if add_supplementalCredentials:
937 del obj["supplementalCredentials"]
938 if "unicodePwd" in obj:
939 unicodePwd = obj["unicodePwd"][0]
941 del obj["unicodePwd"]
942 account_name = obj["sAMAccountName"][0]
943 if add_sAMAcountName:
944 del obj["sAMAccountName"]
945 if "userPrincipalName" in obj:
946 account_upn = obj["userPrincipalName"][0]
948 realm = self.lp.get("realm")
949 account_upn = "%s@%s" % (account_name, realm.lower())
950 if add_userPrincipalName:
951 del obj["userPrincipalName"]
954 def get_package(name, min_idx=0):
955 if name in calculated:
956 return calculated[name]
960 min_idx = len(sc.sub.packages) + min_idx
962 for p in sc.sub.packages:
969 return binascii.a2b_hex(p.data)
974 # Samba adds 'Primary:SambaGPG' at the end.
975 # When Windows sets the password it keeps
976 # 'Primary:SambaGPG' and rotates it to
977 # the begining. So we can only use the value,
978 # if it is the last one.
980 # In order to get more protection we verify
981 # the nthash of the decrypted utf16 password
982 # against the stored nthash in unicodePwd.
984 sgv = get_package("Primary:SambaGPG", min_idx=-1)
985 if sgv is not None and unicodePwd is not None:
986 ctx = gpgme.Context()
988 cipher_io = io.BytesIO(sgv)
989 plain_io = io.BytesIO()
991 ctx.decrypt(cipher_io, plain_io)
992 cv = plain_io.getvalue()
994 # We only use the password if it matches
995 # the current nthash stored in the unicodePwd
998 tmp = credentials.Credentials()
1000 tmp.set_utf16_password(cv)
1001 nthash = tmp.get_nt_hash()
1002 if nthash == unicodePwd:
1003 calculated["Primary:CLEARTEXT"] = cv
1004 except gpgme.GpgmeError as (major, minor, msg):
1005 if major == gpgme.ERR_BAD_SECKEY:
1006 msg = "ERR_BAD_SECKEY: " + msg
1008 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1009 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1010 username or account_name, msg))
1012 def get_utf8(a, b, username):
1014 u = unicode(b, 'utf-16-le')
1015 except UnicodeDecodeError as e:
1016 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1019 u8 = u.encode('utf-8')
1022 # Extract the WDigest hash for the value specified by i.
1023 # Builds an htdigest compatible value
1025 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1026 domain, dns_domain):
1031 user = account_name.lower()
1032 realm = domain.lower()
1034 user = account_name.upper()
1035 realm = domain.upper()
1038 realm = domain.upper()
1041 realm = domain.lower()
1043 user = account_name.upper()
1044 realm = domain.lower()
1046 user = account_name.lower()
1047 realm = domain.upper()
1050 realm = dns_domain.lower()
1052 user = account_name.lower()
1053 realm = dns_domain.lower()
1055 user = account_name.upper()
1056 realm = dns_domain.upper()
1059 realm = dns_domain.upper()
1062 realm = dns_domain.lower()
1064 user = account_name.upper()
1065 realm = dns_domain.lower()
1067 user = account_name.lower()
1068 realm = dns_domain.upper()
1073 user = account_upn.lower()
1076 user = account_upn.upper()
1079 user = "%s\\%s" % (domain, account_name)
1082 user = "%s\\%s" % (domain.lower(), account_name.lower())
1085 user = "%s\\%s" % (domain.upper(), account_name.upper())
1091 user = account_name.lower()
1094 user = account_name.upper()
1100 user = account_upn.lower()
1103 user = account_upn.upper()
1106 user = "%s\\%s" % (domain, account_name)
1109 # Differs from spec, see tests
1110 user = "%s\\%s" % (domain.lower(), account_name.lower())
1113 # Differs from spec, see tests
1114 user = "%s\\%s" % (domain.upper(), account_name.upper())
1119 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1122 digest = binascii.hexlify(bytearray(digests.hashes[i-1].hash))
1123 return "%s:%s:%s" % (user, realm, digest)
1127 # We use sort here in order to have a predictable processing order
1128 for a in sorted(virtual_attributes.keys()):
1129 if not a.lower() in lower_attrs:
1132 if a == "virtualClearTextUTF8":
1133 b = get_package("Primary:CLEARTEXT")
1136 u8 = get_utf8(a, b, username or account_name)
1140 elif a == "virtualClearTextUTF16":
1141 v = get_package("Primary:CLEARTEXT")
1144 elif a == "virtualSSHA":
1145 b = get_package("Primary:CLEARTEXT")
1148 u8 = get_utf8(a, b, username or account_name)
1151 salt = get_random_bytes(4)
1155 bv = h.digest() + salt
1156 v = "{SSHA}" + base64.b64encode(bv)
1157 elif a == "virtualCryptSHA256":
1158 b = get_package("Primary:CLEARTEXT")
1161 u8 = get_utf8(a, b, username or account_name)
1164 sv = get_crypt_value("5", u8)
1166 elif a == "virtualCryptSHA512":
1167 b = get_package("Primary:CLEARTEXT")
1170 u8 = get_utf8(a, b, username or account_name)
1173 sv = get_crypt_value("6", u8)
1175 elif a == "virtualSambaGPG":
1176 # Samba adds 'Primary:SambaGPG' at the end.
1177 # When Windows sets the password it keeps
1178 # 'Primary:SambaGPG' and rotates it to
1179 # the begining. So we can only use the value,
1180 # if it is the last one.
1181 v = get_package("Primary:SambaGPG", min_idx=-1)
1184 elif a.startswith("virtualWDigest"):
1185 primary_wdigest = get_package("Primary:WDigest")
1186 if primary_wdigest is None:
1188 x = a[len("virtualWDigest"):]
1193 domain = self.lp.get("workgroup")
1194 dns_domain = samdb.domain_dns_name()
1195 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1200 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1203 def parse_attributes(self, attributes):
1205 if attributes is None:
1206 raise CommandError("Please specify --attributes")
1207 attrs = attributes.split(',')
1210 pa = pa.lstrip().rstrip()
1211 for da in disabled_virtual_attributes.keys():
1212 if pa.lower() == da.lower():
1213 r = disabled_virtual_attributes[da]["reason"]
1214 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1216 for va in virtual_attributes.keys():
1217 if pa.lower() == va.lower():
1218 # Take the real name
1221 password_attrs += [pa]
1223 return password_attrs
1225 class cmd_user_getpassword(GetPasswordCommand):
1226 """Get the password fields of a user/computer account.
1228 This command gets the logon password for a user/computer account.
1230 The username specified on the command is the sAMAccountName.
1231 The username may also be specified using the --filter option.
1233 The command must be run from the root user id or another authorized user id.
1234 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1235 used to adjust the local path. By default tdb:// is used by default.
1237 The '--attributes' parameter takes a comma separated list of attributes,
1238 which will be printed or given to the script specified by '--script'. If a
1239 specified attribute is not available on an object it's silently omitted.
1240 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1241 the NTHASH) and the following virtual attributes are possible (see --help
1242 for which virtual attributes are supported in your environment):
1244 virtualClearTextUTF16: The raw cleartext as stored in the
1245 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1246 with '--decrypt-samba-gpg') buffer inside of the
1247 supplementalCredentials attribute. This typically
1248 contains valid UTF-16-LE, but may contain random
1249 bytes, e.g. for computer accounts.
1251 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1252 (only from valid UTF-16-LE)
1254 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1255 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1257 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1258 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1259 with a $5$... salt, see crypt(3) on modern systems.
1261 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1262 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1263 with a $6$... salt, see crypt(3) on modern systems.
1265 virtualWDigestNN: The individual hash values stored in
1266 'Primary:WDigest' where NN is the hash number in
1268 NOTE: As at 22-05-2017 the documentation:
1269 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1270 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1273 virtualSambaGPG: The raw cleartext as stored in the
1274 'Primary:SambaGPG' buffer inside of the
1275 supplementalCredentials attribute.
1276 See the 'password hash gpg key ids' option in
1279 The '--decrypt-samba-gpg' option triggers decryption of the
1280 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1281 in your environment or not (the python-gpgme package is required). Please
1282 note that you might need to set the GNUPGHOME environment variable. If the
1283 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1284 environment variable has been set correctly and the passphrase is already
1285 known by the gpg-agent.
1288 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1291 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1295 super(cmd_user_getpassword, self).__init__()
1297 synopsis = "%prog (<username>|--filter <filter>) [options]"
1299 takes_optiongroups = {
1300 "sambaopts": options.SambaOptions,
1301 "versionopts": options.VersionOptions,
1305 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1306 metavar="URL", dest="H"),
1307 Option("--filter", help="LDAP Filter to set password on", type=str),
1308 Option("--attributes", type=str,
1309 help=virtual_attributes_help,
1310 metavar="ATTRIBUTELIST", dest="attributes"),
1311 Option("--decrypt-samba-gpg",
1312 help=decrypt_samba_gpg_help,
1313 action="store_true", default=False, dest="decrypt_samba_gpg"),
1316 takes_args = ["username?"]
1318 def run(self, username=None, H=None, filter=None,
1319 attributes=None, decrypt_samba_gpg=None,
1320 sambaopts=None, versionopts=None):
1321 self.lp = sambaopts.get_loadparm()
1323 if decrypt_samba_gpg and not gpgme_support:
1324 raise CommandError(decrypt_samba_gpg_help)
1326 if filter is None and username is None:
1327 raise CommandError("Either the username or '--filter' must be specified!")
1330 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1332 if attributes is None:
1333 raise CommandError("Please specify --attributes")
1335 password_attrs = self.parse_attributes(attributes)
1337 samdb = self.connect_system_samdb(url=H, allow_local=True)
1339 obj = self.get_account_attributes(samdb, username,
1342 scope=ldb.SCOPE_SUBTREE,
1343 attrs=password_attrs,
1344 decrypt=decrypt_samba_gpg)
1346 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1347 self.outf.write("%s" % ldif)
1348 self.outf.write("Got password OK\n")
1350 class cmd_user_syncpasswords(GetPasswordCommand):
1351 """Sync the password of user accounts.
1353 This syncs logon passwords for user accounts.
1355 Note that this command should run on a single domain controller only
1356 (typically the PDC-emulator). However the "password hash gpg key ids"
1357 option should to be configured on all domain controllers.
1359 The command must be run from the root user id or another authorized user id.
1360 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1361 local path. By default, ldapi:// is used with the default path to the
1362 privileged ldapi socket.
1364 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1365 "Sync Loop Terminate".
1368 Cache Initialization
1369 ====================
1371 The first time, this command needs to be called with
1372 '--cache-ldb-initialize' in order to initialize its cache.
1374 The cache initialization requires '--attributes' and allows the following
1375 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1378 The '--attributes' parameter takes a comma separated list of attributes,
1379 which will be printed or given to the script specified by '--script'. If a
1380 specified attribute is not available on an object it will be silently omitted.
1381 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1382 the NTHASH) and the following virtual attributes are possible (see '--help'
1383 for supported virtual attributes in your environment):
1385 virtualClearTextUTF16: The raw cleartext as stored in the
1386 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1387 with '--decrypt-samba-gpg') buffer inside of the
1388 supplementalCredentials attribute. This typically
1389 contains valid UTF-16-LE, but may contain random
1390 bytes, e.g. for computer accounts.
1392 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1393 (only from valid UTF-16-LE)
1395 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1396 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1398 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1399 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1400 with a $5$... salt, see crypt(3) on modern systems.
1402 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1403 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1404 with a $6$... salt, see crypt(3) on modern systems.
1406 virtualWDigestNN: The individual hash values stored in
1407 'Primary:WDigest' where NN is the hash number in
1409 NOTE: As at 22-05-2017 the documentation:
1410 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1411 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1414 virtualSambaGPG: The raw cleartext as stored in the
1415 'Primary:SambaGPG' buffer inside of the
1416 supplementalCredentials attribute.
1417 See the 'password hash gpg key ids' option in
1420 The '--decrypt-samba-gpg' option triggers decryption of the
1421 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1422 in your environment or not (the python-gpgme package is required). Please
1423 note that you might need to set the GNUPGHOME environment variable. If the
1424 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1425 environment variable has been set correctly and the passphrase is already
1426 known by the gpg-agent.
1428 The '--script' option specifies a custom script that is called whenever any
1429 of the dirsyncAttributes (see below) was changed. The script is called
1430 without any arguments. It gets the LDIF for exactly one object on STDIN.
1431 If the script processed the object successfully it has to respond with a
1432 single line starting with 'DONE-EXIT: ' followed by an optional message.
1434 Note that the script might be called without any password change, e.g. if
1435 the account was disabled (an userAccountControl change) or the
1436 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1437 are always returned as unique identifier of the account. It might be useful
1438 to also ask for non-password attributes like: objectSid, sAMAccountName,
1439 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1440 Depending on the object, some attributes may not be present/available,
1441 but you always get the current state (and not a diff).
1443 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1446 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1447 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1448 (!(sAMAccountName=krbtgt*)))
1449 This means only normal (non-krbtgt) user
1450 accounts are monitored. The '--filter' can modify that, e.g. if it's
1451 required to also sync computer accounts.
1457 This (default) mode runs in an endless loop waiting for password related
1458 changes in the active directory database. It makes use of the
1459 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1460 get changes in a reliable fashion. Objects are monitored for changes of the
1461 following dirsyncAttributes:
1463 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1464 userPrincipalName and userAccountControl.
1466 It recovers from LDAP disconnects and updates the cache in conservative way
1467 (in single steps after each succesfully processed change). An error from
1468 the script (specified by '--script') will result in fatal error and this
1469 command will exit. But the cache state should be still valid and can be
1470 resumed in the next "Sync Loop Run".
1472 The '--logfile' option specifies an optional (required if '--daemon' is
1473 specified) logfile that takes all output of the command. The logfile is
1474 automatically reopened if fstat returns st_nlink == 0.
1476 The optional '--daemon' option will put the command into the background.
1478 You can stop the command without the '--daemon' option, also by hitting
1481 If you specify the '--no-wait' option the command skips the
1482 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1483 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1488 In order to terminate an already running command (likely as daemon) the
1489 '--terminate' option can be used. This also requires the '--logfile' option
1494 samba-tool user syncpasswords --cache-ldb-initialize \\
1495 --attributes=virtualClearTextUTF8
1496 samba-tool user syncpasswords
1499 samba-tool user syncpasswords --cache-ldb-initialize \\
1500 --attributes=objectGUID,objectSID,sAMAccountName,\\
1501 userPrincipalName,userAccountControl,pwdLastSet,\\
1502 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1503 --script=/path/to/my-custom-syncpasswords-script.py
1504 samba-tool user syncpasswords --daemon \\
1505 --logfile=/var/log/samba/user-syncpasswords.log
1506 samba-tool user syncpasswords --terminate \\
1507 --logfile=/var/log/samba/user-syncpasswords.log
1511 super(cmd_user_syncpasswords, self).__init__()
1513 synopsis = "%prog [--cache-ldb-initialize] [options]"
1515 takes_optiongroups = {
1516 "sambaopts": options.SambaOptions,
1517 "versionopts": options.VersionOptions,
1521 Option("--cache-ldb-initialize",
1522 help="Initialize the cache for the first time",
1523 dest="cache_ldb_initialize", action="store_true"),
1524 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1525 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1526 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1527 metavar="URL", dest="H"),
1528 Option("--filter", help="optional LDAP filter to set password on", type=str,
1529 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1530 Option("--attributes", type=str,
1531 help=virtual_attributes_help,
1532 metavar="ATTRIBUTELIST", dest="attributes"),
1533 Option("--decrypt-samba-gpg",
1534 help=decrypt_samba_gpg_help,
1535 action="store_true", default=False, dest="decrypt_samba_gpg"),
1536 Option("--script", help="Script that is called for each password change", type=str,
1537 metavar="/path/to/syncpasswords.script", dest="script"),
1538 Option("--no-wait", help="Don't block waiting for changes",
1539 action="store_true", default=False, dest="nowait"),
1540 Option("--logfile", type=str,
1541 help="The logfile to use (required in --daemon mode).",
1542 metavar="/path/to/syncpasswords.log", dest="logfile"),
1543 Option("--daemon", help="daemonize after initial setup",
1544 action="store_true", default=False, dest="daemon"),
1545 Option("--terminate",
1546 help="Send a SIGTERM to an already running (daemon) process",
1547 action="store_true", default=False, dest="terminate"),
1550 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1551 H=None, filter=None,
1552 attributes=None, decrypt_samba_gpg=None,
1553 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1554 sambaopts=None, versionopts=None):
1556 self.lp = sambaopts.get_loadparm()
1558 self.samdb_url = None
1562 if not cache_ldb_initialize:
1563 if attributes is not None:
1564 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1565 if decrypt_samba_gpg:
1566 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1567 if script is not None:
1568 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1569 if filter is not None:
1570 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1572 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1574 if nowait is not False:
1575 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1576 if logfile is not None:
1577 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1578 if daemon is not False:
1579 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1580 if terminate is not False:
1581 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1585 raise CommandError("--daemon is not allowed together with --no-wait")
1586 if terminate is not False:
1587 raise CommandError("--terminate is not allowed together with --no-wait")
1589 if terminate is True and daemon is True:
1590 raise CommandError("--terminate is not allowed together with --daemon")
1592 if daemon is True and logfile is None:
1593 raise CommandError("--daemon is only allowed together with --logfile")
1595 if terminate is True and logfile is None:
1596 raise CommandError("--terminate is only allowed together with --logfile")
1598 if script is not None:
1599 if not os.path.exists(script):
1600 raise CommandError("script[%s] does not exist!" % script)
1602 sync_command = "%s" % os.path.abspath(script)
1606 dirsync_filter = filter
1607 if dirsync_filter is None:
1608 dirsync_filter = "(&" + \
1609 "(objectClass=user)" + \
1610 "(userAccountControl:%s:=%u)" % (
1611 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1612 "(!(sAMAccountName=krbtgt*))" + \
1615 dirsync_secret_attrs = [
1618 "supplementalCredentials",
1621 dirsync_attrs = dirsync_secret_attrs + [
1624 "userPrincipalName",
1625 "userAccountControl",
1630 password_attrs = None
1632 if cache_ldb_initialize:
1634 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1636 if decrypt_samba_gpg and not gpgme_support:
1637 raise CommandError(decrypt_samba_gpg_help)
1639 password_attrs = self.parse_attributes(attributes)
1640 lower_attrs = [x.lower() for x in password_attrs]
1641 # We always return these in order to track deletions
1642 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1643 if a.lower() not in lower_attrs:
1644 password_attrs += [a]
1646 if cache_ldb is not None:
1647 if cache_ldb.lower().startswith("ldapi://"):
1648 raise CommandError("--cache_ldb ldapi:// is not supported")
1649 elif cache_ldb.lower().startswith("ldap://"):
1650 raise CommandError("--cache_ldb ldap:// is not supported")
1651 elif cache_ldb.lower().startswith("ldaps://"):
1652 raise CommandError("--cache_ldb ldaps:// is not supported")
1653 elif cache_ldb.lower().startswith("tdb://"):
1656 if not os.path.exists(cache_ldb):
1657 cache_ldb = self.lp.private_path(cache_ldb)
1659 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1661 self.lockfile = "%s.pid" % cache_ldb
1664 if self.logfile is not None:
1666 if info.st_nlink == 0:
1667 logfile = self.logfile
1669 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1670 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
1675 log_msg("Reopened logfile[%s]\n" % (logfile))
1676 self.logfile = logfile
1677 msg = "%s: pid[%d]: %s" % (
1681 self.outf.write(msg)
1690 "passwordAttribute",
1696 self.cache = Ldb(cache_ldb)
1697 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1698 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1702 self.samdb_url = res[0]["samdbUrl"][0]
1703 except KeyError as e:
1704 self.samdb_url = None
1706 self.samdb_url = None
1707 if self.samdb_url is None and not cache_ldb_initialize:
1708 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1710 if self.samdb_url is not None and cache_ldb_initialize:
1711 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1713 if self.samdb_url is None:
1715 self.dirsync_filter = dirsync_filter
1716 self.dirsync_attrs = dirsync_attrs
1717 self.dirsync_controls = ["dirsync:1:0:0","extended_dn:1:0"];
1718 self.password_attrs = password_attrs
1719 self.decrypt_samba_gpg = decrypt_samba_gpg
1720 self.sync_command = sync_command
1721 add_ldif = "dn: %s\n" % self.cache_dn
1722 add_ldif += "objectClass: userSyncPasswords\n"
1723 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url)
1724 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter)
1725 for a in self.dirsync_attrs:
1726 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a)
1727 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1728 for a in self.password_attrs:
1729 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
1730 if self.decrypt_samba_gpg == True:
1731 add_ldif += "decryptSambaGPG: TRUE\n"
1733 add_ldif += "decryptSambaGPG: FALSE\n"
1734 if self.sync_command is not None:
1735 add_ldif += "syncCommand: %s\n" % self.sync_command
1736 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1737 self.cache.add_ldif(add_ldif)
1738 self.current_pid = None
1739 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1740 msgs = self.cache.parse_ldif(add_ldif)
1741 changetype,msg = msgs.next()
1742 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1743 self.outf.write("%s" % ldif)
1745 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1746 self.dirsync_attrs = []
1747 for a in res[0]["dirsyncAttribute"]:
1748 self.dirsync_attrs.append(a)
1749 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1750 self.password_attrs = []
1751 for a in res[0]["passwordAttribute"]:
1752 self.password_attrs.append(a)
1753 decrypt_string = res[0]["decryptSambaGPG"][0]
1754 assert(decrypt_string in ["TRUE", "FALSE"])
1755 if decrypt_string == "TRUE":
1756 self.decrypt_samba_gpg = True
1758 self.decrypt_samba_gpg = False
1759 if "syncCommand" in res[0]:
1760 self.sync_command = res[0]["syncCommand"][0]
1762 self.sync_command = None
1763 if "currentPid" in res[0]:
1764 self.current_pid = int(res[0]["currentPid"][0])
1766 self.current_pid = None
1767 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1771 def run_sync_command(dn, ldif):
1772 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1773 sync_command_p = Popen(self.sync_command,
1778 res = sync_command_p.poll()
1781 input = "%s" % (ldif)
1782 reply = sync_command_p.communicate(input)[0]
1783 log_msg("%s\n" % (reply))
1784 res = sync_command_p.poll()
1786 sync_command_p.terminate()
1787 res = sync_command_p.wait()
1789 if reply.startswith("DONE-EXIT: "):
1792 log_msg("RESULT: %s\n" % (res))
1793 raise Exception("ERROR: %s - %s\n" % (res, reply))
1795 def handle_object(idx, dirsync_obj):
1796 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1797 guid = ndr_unpack(misc.GUID, binary_guid)
1798 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1799 sid = ndr_unpack(security.dom_sid, binary_sid)
1800 domain_sid, rid = sid.split()
1801 if rid == security.DOMAIN_RID_KRBTGT:
1802 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1804 for a in list(dirsync_obj.keys()):
1805 for h in dirsync_secret_attrs:
1806 if a.lower() == h.lower():
1808 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1809 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1810 log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif))
1811 obj = self.get_account_attributes(self.samdb,
1812 username="%s" % sid,
1813 basedn="<GUID=%s>" % guid,
1814 filter="(objectClass=user)",
1815 scope=ldb.SCOPE_BASE,
1816 attrs=self.password_attrs,
1817 decrypt=self.decrypt_samba_gpg)
1818 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1819 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1820 if self.sync_command is None:
1821 self.outf.write("%s" % (ldif))
1823 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1824 run_sync_command(obj.dn, ldif)
1826 def check_current_pid_conflict(terminate):
1832 self.lockfd = os.open(self.lockfile, flags, 0600)
1833 except IOError as (err, msg):
1834 if err == errno.ENOENT:
1837 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1838 (self.lockfile, msg, err))
1841 got_exclusive = False
1843 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1844 got_exclusive = True
1845 except IOError as (err, msg):
1846 if err != errno.EACCES and err != errno.EAGAIN:
1847 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
1848 (self.lockfile, msg, err))
1851 if not got_exclusive:
1852 buf = os.read(self.lockfd, 64)
1853 self.current_pid = None
1855 self.current_pid = int(buf)
1856 except ValueError as e:
1858 if self.current_pid is not None:
1861 if got_exclusive and terminate:
1863 os.ftruncate(self.lockfd, 0)
1864 except IOError as (err, msg):
1865 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
1866 (self.lockfile, msg, err))
1868 os.close(self.lockfd)
1873 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
1874 except IOError as (err, msg):
1875 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
1876 (self.lockfile, msg, err))
1878 # We leave the function with the shared lock.
1881 def update_pid(pid):
1882 if self.lockfd != -1:
1883 got_exclusive = False
1884 # Try 5 times to get the exclusiv lock.
1885 for i in xrange(0, 5):
1887 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1888 got_exclusive = True
1889 except IOError as (err, msg):
1890 if err != errno.EACCES and err != errno.EAGAIN:
1891 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
1892 (pid, self.lockfile, msg, err))
1897 if not got_exclusive:
1898 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
1899 (pid, self.lockfile))
1900 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
1901 (pid, self.lockfile))
1908 os.ftruncate(self.lockfd, 0)
1910 os.write(self.lockfd, buf)
1911 except IOError as (err, msg):
1912 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
1913 (self.lockfile, msg, err))
1915 self.current_pid = pid
1916 if self.current_pid is not None:
1917 log_msg("currentPid: %d\n" % self.current_pid)
1919 modify_ldif = "dn: %s\n" % (self.cache_dn)
1920 modify_ldif += "changetype: modify\n"
1921 modify_ldif += "replace: currentPid\n"
1922 if self.current_pid is not None:
1923 modify_ldif += "currentPid: %d\n" % (self.current_pid)
1924 modify_ldif += "replace: currentTime\n"
1925 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1926 self.cache.modify_ldif(modify_ldif)
1929 def update_cache(res_controls):
1930 assert len(res_controls) > 0
1931 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
1932 res_controls[0].critical = True
1933 self.dirsync_controls = [str(res_controls[0]),"extended_dn:1:0"]
1934 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
1936 modify_ldif = "dn: %s\n" % (self.cache_dn)
1937 modify_ldif += "changetype: modify\n"
1938 modify_ldif += "replace: dirsyncControl\n"
1939 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
1940 modify_ldif += "replace: currentTime\n"
1941 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1942 self.cache.modify_ldif(modify_ldif)
1945 def check_object(dirsync_obj, res_controls):
1946 assert len(res_controls) > 0
1947 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
1949 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1950 sid = ndr_unpack(security.dom_sid, binary_sid)
1952 lastCookie = str(res_controls[0])
1954 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
1955 expression="(lastCookie=%s)" % (
1956 ldb.binary_encode(lastCookie)),
1962 def update_object(dirsync_obj, res_controls):
1963 assert len(res_controls) > 0
1964 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
1966 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1967 sid = ndr_unpack(security.dom_sid, binary_sid)
1969 lastCookie = str(res_controls[0])
1971 self.cache.transaction_start()
1973 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
1974 expression="(objectClass=*)",
1975 attrs=["lastCookie"])
1977 add_ldif = "dn: %s\n" % (dn)
1978 add_ldif += "objectClass: userCookie\n"
1979 add_ldif += "lastCookie: %s\n" % (lastCookie)
1980 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1981 self.cache.add_ldif(add_ldif)
1983 modify_ldif = "dn: %s\n" % (dn)
1984 modify_ldif += "changetype: modify\n"
1985 modify_ldif += "replace: lastCookie\n"
1986 modify_ldif += "lastCookie: %s\n" % (lastCookie)
1987 modify_ldif += "replace: currentTime\n"
1988 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1989 self.cache.modify_ldif(modify_ldif)
1990 self.cache.transaction_commit()
1991 except Exception as e:
1992 self.cache.transaction_cancel()
1998 res = self.samdb.search(expression=self.dirsync_filter,
1999 scope=ldb.SCOPE_SUBTREE,
2000 attrs=self.dirsync_attrs,
2001 controls=self.dirsync_controls)
2002 log_msg("dirsync_loop(): results %d\n" % len(res))
2005 done = check_object(r, res.controls)
2007 handle_object(ri, r)
2008 update_object(r, res.controls)
2010 update_cache(res.controls)
2014 def sync_loop(wait):
2015 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2016 notify_controls = ["notification:1", "show_recycled:1"]
2017 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2018 scope=ldb.SCOPE_SUBTREE,
2020 controls=notify_controls,
2024 log_msg("Resuming monitoring\n")
2026 log_msg("Getting changes\n")
2027 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2028 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2029 self.outf.write("syncCommand: %s\n" % self.sync_command)
2032 if wait is not True:
2035 for msg in notify_handle:
2036 if not isinstance(msg, ldb.Message):
2037 self.outf.write("referal: %s\n" % msg)
2039 created = msg.get("uSNCreated")[0]
2040 changed = msg.get("uSNChanged")[0]
2041 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2042 (msg.dn, created, changed))
2046 res = notify_handle.result()
2051 orig_pid = os.getpid()
2056 if pid == 0: # Actual daemon
2058 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2063 if cache_ldb_initialize:
2065 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2070 if logfile is not None:
2071 import resource # Resource usage information.
2072 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2073 if maxfd == resource.RLIM_INFINITY:
2074 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2075 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
2076 self.outf.write("Using logfile[%s]\n" % logfile)
2077 for fd in range(0, maxfd):
2088 log_msg("Attached to logfile[%s]\n" % (logfile))
2089 self.logfile = logfile
2092 conflict = check_current_pid_conflict(terminate)
2094 if self.current_pid is None:
2095 log_msg("No process running.\n")
2098 log_msg("Proccess %d is not running anymore.\n" % (
2102 log_msg("Sending SIGTERM to proccess %d.\n" % (
2104 os.kill(self.current_pid, signal.SIGTERM)
2107 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2108 os.getpid(), self.current_pid))
2112 update_pid(os.getpid())
2117 retry_sleep_max = 600
2122 retry_sleep = retry_sleep_min
2124 while self.samdb is None:
2125 if retry_sleep != 0:
2126 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2127 time.sleep(retry_sleep)
2128 retry_sleep = retry_sleep * 2
2129 if retry_sleep >= retry_sleep_max:
2130 retry_sleep = retry_sleep_max
2131 log_msg("Connecting to '%s'\n" % self.samdb_url)
2133 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2134 except Exception as msg:
2136 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2137 if wait is not True:
2142 except ldb.LdbError as (enum, estr):
2144 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2149 class cmd_user(SuperCommand):
2150 """User management."""
2153 subcommands["add"] = cmd_user_add()
2154 subcommands["create"] = cmd_user_create()
2155 subcommands["delete"] = cmd_user_delete()
2156 subcommands["disable"] = cmd_user_disable()
2157 subcommands["enable"] = cmd_user_enable()
2158 subcommands["list"] = cmd_user_list()
2159 subcommands["setexpiry"] = cmd_user_setexpiry()
2160 subcommands["password"] = cmd_user_password()
2161 subcommands["setpassword"] = cmd_user_setpassword()
2162 subcommands["getpassword"] = cmd_user_getpassword()
2163 subcommands["syncpasswords"] = cmd_user_syncpasswords()