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
33 from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError
34 from getpass import getpass
35 from samba.auth import system_session
36 from samba.samdb import SamDB
37 from samba.dcerpc import misc
38 from samba.dcerpc import security
39 from samba.dcerpc import drsblobs
40 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
45 generate_random_password,
48 from samba.net import Net
50 from samba.netcmd import (
56 from samba.compat import text_type
57 from samba.compat import get_bytes
58 from samba.compat import get_string
64 decrypt_samba_gpg_help = "Decrypt the SambaGPG password as cleartext source"
65 except ImportError as e:
67 decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
68 "python-gpgme required"
70 disabled_virtual_attributes = {
73 virtual_attributes = {
74 "virtualClearTextUTF8": {
75 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
77 "virtualClearTextUTF16": {
78 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
81 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
85 get_random_bytes_fn = None
86 if get_random_bytes_fn is None:
89 get_random_bytes_fn = Crypto.Random.get_random_bytes
90 except ImportError as e:
92 if get_random_bytes_fn is None:
95 get_random_bytes_fn = M2Crypto.Rand.rand_bytes
96 except ImportError as e:
101 if get_random_bytes_fn is not None:
103 return "Crypto.Random or M2Crypto.Rand required"
106 def get_random_bytes(num):
107 random_reason = check_random()
108 if random_reason is not None:
109 raise ImportError(random_reason)
110 return get_random_bytes_fn(num)
113 def get_crypt_value(alg, utf8pw, rounds=0):
119 salt = get_random_bytes(16)
120 # The salt needs to be in [A-Za-z0-9./]
121 # base64 is close enough and as we had 16
122 # random bytes but only need 16 characters
123 # we can ignore the possible == at the end
124 # of the base64 string
125 # we just need to replace '+' by '.'
126 b64salt = base64.b64encode(salt)[0:16].replace(b'+', b'.').decode('utf8')
129 crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
131 crypt_salt = "$%s$%s$" % (alg, b64salt)
133 crypt_value = crypt.crypt(utf8pw, crypt_salt)
134 if crypt_value is None:
135 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
136 expected_len = len(crypt_salt) + algs[alg]["length"]
137 if len(crypt_value) != expected_len:
138 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
139 crypt_salt, len(crypt_value), expected_len))
142 # Extract the rounds value from the options of a virtualCrypt attribute
143 # i.e. options = "rounds=20;other=ignored;" will return 20
144 # if the rounds option is not found or the value is not a number, 0 is returned
145 # which indicates that the default number of rounds should be used.
148 def get_rounds(options):
152 opts = options.split(';')
154 if o.lower().startswith("rounds="):
155 (key, _, val) = o.partition('=')
164 random_reason = check_random()
165 if random_reason is not None:
166 raise ImportError(random_reason)
170 virtual_attributes["virtualSSHA"] = {
172 except ImportError as e:
173 reason = "hashlib.sha1()"
175 reason += " and " + random_reason
176 reason += " required"
177 disabled_virtual_attributes["virtualSSHA"] = {
181 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
183 random_reason = check_random()
184 if random_reason is not None:
185 raise ImportError(random_reason)
187 v = get_crypt_value(alg, "")
189 virtual_attributes[attr] = {
191 except ImportError as e:
194 reason += " and " + random_reason
195 reason += " required"
196 disabled_virtual_attributes[attr] = {
199 except NotImplementedError as e:
200 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
201 disabled_virtual_attributes[attr] = {
205 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
206 for x in range(1, 30):
207 virtual_attributes["virtualWDigest%02d" % x] = {}
209 # Add Kerberos virtual attributes
210 virtual_attributes["virtualKerberosSalt"] = {}
212 virtual_attributes_help = "The attributes to display (comma separated). "
213 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
214 if len(disabled_virtual_attributes) != 0:
215 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
218 class cmd_user_create(Command):
219 """Create a new user.
221 This command creates a new user account in the Active Directory domain. The username specified on the command is the sAMaccountName.
223 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).
225 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.
227 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.
229 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.
232 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
234 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.
237 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
239 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.
242 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
244 Example3 shows how to create a new user in the OrgUnit organizational unit.
247 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
249 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'.
252 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
253 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
255 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
256 --nis-domain is set, then the other four parameters are mandatory.
259 synopsis = "%prog <username> [<password>] [options]"
262 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
263 metavar="URL", dest="H"),
264 Option("--must-change-at-next-login",
265 help="Force password to be changed on next login",
266 action="store_true"),
267 Option("--random-password",
268 help="Generate random password",
269 action="store_true"),
270 Option("--smartcard-required",
271 help="Require a smartcard for interactive logons",
272 action="store_true"),
273 Option("--use-username-as-cn",
274 help="Force use of username as user's CN",
275 action="store_true"),
277 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>'",
279 Option("--surname", help="User's surname", type=str),
280 Option("--given-name", help="User's given name", type=str),
281 Option("--initials", help="User's initials", type=str),
282 Option("--profile-path", help="User's profile path", type=str),
283 Option("--script-path", help="User's logon script path", type=str),
284 Option("--home-drive", help="User's home drive letter", type=str),
285 Option("--home-directory", help="User's home directory path", type=str),
286 Option("--job-title", help="User's job title", type=str),
287 Option("--department", help="User's department", type=str),
288 Option("--company", help="User's company", type=str),
289 Option("--description", help="User's description", type=str),
290 Option("--mail-address", help="User's email address", type=str),
291 Option("--internet-address", help="User's home page", type=str),
292 Option("--telephone-number", help="User's phone number", type=str),
293 Option("--physical-delivery-office", help="User's office location", type=str),
294 Option("--rfc2307-from-nss",
295 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
296 action="store_true"),
297 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
298 Option("--unix-home", help="User's Unix/RFC2307 home directory",
300 Option("--uid", help="User's Unix/RFC2307 username", type=str),
301 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
302 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
303 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
304 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
307 takes_args = ["username", "password?"]
309 takes_optiongroups = {
310 "sambaopts": options.SambaOptions,
311 "credopts": options.CredentialsOptions,
312 "versionopts": options.VersionOptions,
315 def run(self, username, password=None, credopts=None, sambaopts=None,
316 versionopts=None, H=None, must_change_at_next_login=False,
317 random_password=False, use_username_as_cn=False, userou=None,
318 surname=None, given_name=None, initials=None, profile_path=None,
319 script_path=None, home_drive=None, home_directory=None,
320 job_title=None, department=None, company=None, description=None,
321 mail_address=None, internet_address=None, telephone_number=None,
322 physical_delivery_office=None, rfc2307_from_nss=False,
323 nis_domain=None, unix_home=None, uid=None, uid_number=None,
324 gid_number=None, gecos=None, login_shell=None,
325 smartcard_required=False):
327 if smartcard_required:
328 if password is not None and password != '':
329 raise CommandError('It is not allowed to specify '
331 'together with --smartcard-required.')
332 if must_change_at_next_login:
333 raise CommandError('It is not allowed to specify '
334 '--must-change-at-next-login '
335 'together with --smartcard-required.')
337 if random_password and not smartcard_required:
338 password = generate_random_password(128, 255)
341 if smartcard_required:
343 if password is not None and password != '':
345 password = getpass("New Password: ")
346 passwordverify = getpass("Retype Password: ")
347 if not password == passwordverify:
349 self.outf.write("Sorry, passwords do not match.\n")
352 pwent = pwd.getpwnam(username)
355 if uid_number is None:
356 uid_number = pwent[2]
357 if gid_number is None:
358 gid_number = pwent[3]
361 if login_shell is None:
362 login_shell = pwent[6]
364 lp = sambaopts.get_loadparm()
365 creds = credopts.get_credentials(lp)
367 if uid_number or gid_number:
368 if not lp.get("idmap_ldb:use rfc2307"):
369 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")
371 if nis_domain is not None:
372 if None in (uid_number, login_shell, unix_home, gid_number):
373 raise CommandError('Missing parameters. To enable NIS features, '
374 'the following options have to be given: '
375 '--nis-domain=, --uidNumber=, --login-shell='
376 ', --unix-home=, --gid-number= Operation '
380 samdb = SamDB(url=H, session_info=system_session(),
381 credentials=creds, lp=lp)
382 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
383 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
384 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
385 jobtitle=job_title, department=department, company=company, description=description,
386 mailaddress=mail_address, internetaddress=internet_address,
387 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
388 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
389 uidnumber=uid_number, gidnumber=gid_number,
390 gecos=gecos, loginshell=login_shell,
391 smartcard_required=smartcard_required)
392 except Exception as e:
393 raise CommandError("Failed to add user '%s': " % username, e)
395 self.outf.write("User '%s' created successfully\n" % username)
398 class cmd_user_add(cmd_user_create):
399 __doc__ = cmd_user_create.__doc__
400 # take this print out after the add subcommand is removed.
401 # the add subcommand is deprecated but left in for now to allow people to
404 def run(self, *args, **kwargs):
406 "Note: samba-tool user add is deprecated. "
407 "Please use samba-tool user create for the same function.\n")
408 return super(cmd_user_add, self).run(*args, **kwargs)
411 class cmd_user_delete(Command):
414 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
416 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.
418 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.
421 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
423 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.
426 sudo samba-tool user delete User2
428 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.
431 synopsis = "%prog <username> [options]"
434 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
435 metavar="URL", dest="H"),
438 takes_args = ["username"]
439 takes_optiongroups = {
440 "sambaopts": options.SambaOptions,
441 "credopts": options.CredentialsOptions,
442 "versionopts": options.VersionOptions,
445 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
447 lp = sambaopts.get_loadparm()
448 creds = credopts.get_credentials(lp, fallback_machine=True)
450 samdb = SamDB(url=H, session_info=system_session(),
451 credentials=creds, lp=lp)
453 filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
454 ldb.binary_encode(username))
457 res = samdb.search(base=samdb.domain_dn(),
458 scope=ldb.SCOPE_SUBTREE,
463 raise CommandError('Unable to find user "%s"' % (username))
466 samdb.delete(user_dn)
467 except Exception as e:
468 raise CommandError('Failed to remove user "%s"' % username, e)
469 self.outf.write("Deleted user %s\n" % username)
472 class cmd_user_list(Command):
473 """List all users."""
475 synopsis = "%prog [options]"
478 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
479 metavar="URL", dest="H"),
482 takes_optiongroups = {
483 "sambaopts": options.SambaOptions,
484 "credopts": options.CredentialsOptions,
485 "versionopts": options.VersionOptions,
488 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
489 lp = sambaopts.get_loadparm()
490 creds = credopts.get_credentials(lp, fallback_machine=True)
492 samdb = SamDB(url=H, session_info=system_session(),
493 credentials=creds, lp=lp)
495 domain_dn = samdb.domain_dn()
496 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
497 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
498 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
499 attrs=["samaccountname"])
504 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
507 class cmd_user_enable(Command):
510 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.
512 There are many reasons why an account may become disabled. These include:
513 - If a user exceeds the account policy for logon attempts
514 - If an administrator disables the account
515 - If the account expires
517 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
519 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.
521 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.
524 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
526 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.
529 su samba-tool user enable Testuser2
531 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.
534 samba-tool user enable --filter=samaccountname=Testuser3
536 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
539 synopsis = "%prog (<username>|--filter <filter>) [options]"
541 takes_optiongroups = {
542 "sambaopts": options.SambaOptions,
543 "versionopts": options.VersionOptions,
544 "credopts": options.CredentialsOptions,
548 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
549 metavar="URL", dest="H"),
550 Option("--filter", help="LDAP Filter to set password on", type=str),
553 takes_args = ["username?"]
555 def run(self, username=None, sambaopts=None, credopts=None,
556 versionopts=None, filter=None, H=None):
557 if username is None and filter is None:
558 raise CommandError("Either the username or '--filter' must be specified!")
561 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
563 lp = sambaopts.get_loadparm()
564 creds = credopts.get_credentials(lp, fallback_machine=True)
566 samdb = SamDB(url=H, session_info=system_session(),
567 credentials=creds, lp=lp)
569 samdb.enable_account(filter)
570 except Exception as msg:
571 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
572 self.outf.write("Enabled user '%s'\n" % (username or filter))
575 class cmd_user_disable(Command):
576 """Disable a user."""
578 synopsis = "%prog (<username>|--filter <filter>) [options]"
581 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
582 metavar="URL", dest="H"),
583 Option("--filter", help="LDAP Filter to set password on", type=str),
586 takes_args = ["username?"]
588 takes_optiongroups = {
589 "sambaopts": options.SambaOptions,
590 "credopts": options.CredentialsOptions,
591 "versionopts": options.VersionOptions,
594 def run(self, username=None, sambaopts=None, credopts=None,
595 versionopts=None, filter=None, H=None):
596 if username is None and filter is None:
597 raise CommandError("Either the username or '--filter' must be specified!")
600 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
602 lp = sambaopts.get_loadparm()
603 creds = credopts.get_credentials(lp, fallback_machine=True)
605 samdb = SamDB(url=H, session_info=system_session(),
606 credentials=creds, lp=lp)
608 samdb.disable_account(filter)
609 except Exception as msg:
610 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
613 class cmd_user_setexpiry(Command):
614 """Set the expiration of a user account.
616 The user can either be specified by their sAMAccountName or using the --filter option.
618 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.
620 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.
623 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
625 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.
628 sudo samba-tool user setexpiry User2 --noexpiry
630 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.
633 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
635 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.
638 samba-tool user setexpiry --noexpiry User4
639 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
642 synopsis = "%prog (<username>|--filter <filter>) [options]"
644 takes_optiongroups = {
645 "sambaopts": options.SambaOptions,
646 "versionopts": options.VersionOptions,
647 "credopts": options.CredentialsOptions,
651 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
652 metavar="URL", dest="H"),
653 Option("--filter", help="LDAP Filter to set password on", type=str),
654 Option("--days", help="Days to expiry", type=int, default=0),
655 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
658 takes_args = ["username?"]
660 def run(self, username=None, sambaopts=None, credopts=None,
661 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
662 if username is None and filter is None:
663 raise CommandError("Either the username or '--filter' must be specified!")
666 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
668 lp = sambaopts.get_loadparm()
669 creds = credopts.get_credentials(lp)
671 samdb = SamDB(url=H, session_info=system_session(),
672 credentials=creds, lp=lp)
675 samdb.setexpiry(filter, days * 24 * 3600, no_expiry_req=noexpiry)
676 except Exception as msg:
677 # FIXME: Catch more specific exception
678 raise CommandError("Failed to set expiry for user '%s': %s" % (
679 username or filter, msg))
681 self.outf.write("Expiry for user '%s' disabled.\n" % (
684 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
685 username or filter, days))
688 class cmd_user_password(Command):
689 """Change password for a user account (the one provided in authentication).
692 synopsis = "%prog [options]"
695 Option("--newpassword", help="New password", type=str),
698 takes_optiongroups = {
699 "sambaopts": options.SambaOptions,
700 "credopts": options.CredentialsOptions,
701 "versionopts": options.VersionOptions,
704 def run(self, credopts=None, sambaopts=None, versionopts=None,
707 lp = sambaopts.get_loadparm()
708 creds = credopts.get_credentials(lp)
710 # get old password now, to get the password prompts in the right order
711 old_password = creds.get_password()
713 net = Net(creds, lp, server=credopts.ipaddress)
715 password = newpassword
717 if password is not None and password != '':
719 password = getpass("New Password: ")
720 passwordverify = getpass("Retype Password: ")
721 if not password == passwordverify:
723 self.outf.write("Sorry, passwords do not match.\n")
726 if not isinstance(password, text_type):
727 password = password.decode('utf8')
728 net.change_password(password)
729 except Exception as msg:
730 # FIXME: catch more specific exception
731 raise CommandError("Failed to change password : %s" % msg)
732 self.outf.write("Changed password OK\n")
735 class cmd_user_setpassword(Command):
736 """Set or reset the password of a user account.
738 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.
740 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.
742 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.
744 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.
747 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
749 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.
752 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
754 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.
757 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
759 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
762 synopsis = "%prog (<username>|--filter <filter>) [options]"
764 takes_optiongroups = {
765 "sambaopts": options.SambaOptions,
766 "versionopts": options.VersionOptions,
767 "credopts": options.CredentialsOptions,
771 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
772 metavar="URL", dest="H"),
773 Option("--filter", help="LDAP Filter to set password on", type=str),
774 Option("--newpassword", help="Set password", type=str),
775 Option("--must-change-at-next-login",
776 help="Force password to be changed on next login",
777 action="store_true"),
778 Option("--random-password",
779 help="Generate random password",
780 action="store_true"),
781 Option("--smartcard-required",
782 help="Require a smartcard for interactive logons",
783 action="store_true"),
784 Option("--clear-smartcard-required",
785 help="Don't require a smartcard for interactive logons",
786 action="store_true"),
789 takes_args = ["username?"]
791 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
792 versionopts=None, H=None, newpassword=None,
793 must_change_at_next_login=False, random_password=False,
794 smartcard_required=False, clear_smartcard_required=False):
795 if filter is None and username is None:
796 raise CommandError("Either the username or '--filter' must be specified!")
798 password = newpassword
800 if smartcard_required:
801 if password is not None and password != '':
802 raise CommandError('It is not allowed to specify '
804 'together with --smartcard-required.')
805 if must_change_at_next_login:
806 raise CommandError('It is not allowed to specify '
807 '--must-change-at-next-login '
808 'together with --smartcard-required.')
809 if clear_smartcard_required:
810 raise CommandError('It is not allowed to specify '
811 '--clear-smartcard-required '
812 'together with --smartcard-required.')
814 if random_password and not smartcard_required:
815 password = generate_random_password(128, 255)
818 if smartcard_required:
820 if password is not None and password != '':
822 password = getpass("New Password: ")
823 passwordverify = getpass("Retype Password: ")
824 if not password == passwordverify:
826 self.outf.write("Sorry, passwords do not match.\n")
829 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
831 lp = sambaopts.get_loadparm()
832 creds = credopts.get_credentials(lp)
834 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
836 samdb = SamDB(url=H, session_info=system_session(),
837 credentials=creds, lp=lp)
839 if smartcard_required:
842 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
843 flags = dsdb.UF_SMARTCARD_REQUIRED
844 samdb.toggle_userAccountFlags(filter, flags, on=True)
845 command = "Failed to enable account for user '%s'" % (username or filter)
846 samdb.enable_account(filter)
847 except Exception as msg:
848 # FIXME: catch more specific exception
849 raise CommandError("%s: %s" % (command, msg))
850 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
854 if clear_smartcard_required:
855 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
856 flags = dsdb.UF_SMARTCARD_REQUIRED
857 samdb.toggle_userAccountFlags(filter, flags, on=False)
858 command = "Failed to set password for user '%s'" % (username or filter)
859 samdb.setpassword(filter, password,
860 force_change_at_next_login=must_change_at_next_login,
862 except Exception as msg:
863 # FIXME: catch more specific exception
864 raise CommandError("%s: %s" % (command, msg))
865 self.outf.write("Changed password OK\n")
868 class GetPasswordCommand(Command):
871 super(GetPasswordCommand, self).__init__()
874 def connect_system_samdb(self, url, allow_local=False, verbose=False):
876 # using anonymous here, results in no authentication
877 # which means we can get system privileges via
878 # the privileged ldapi socket
879 creds = credentials.Credentials()
880 creds.set_anonymous()
882 if url is None and allow_local:
884 elif url.lower().startswith("ldapi://"):
886 elif url.lower().startswith("ldap://"):
887 raise CommandError("--url ldap:// is not supported for this command")
888 elif url.lower().startswith("ldaps://"):
889 raise CommandError("--url ldaps:// is not supported for this command")
890 elif not allow_local:
891 raise CommandError("--url requires an ldapi:// url for this command")
894 self.outf.write("Connecting to '%s'\n" % url)
896 samdb = SamDB(url=url, session_info=system_session(),
897 credentials=creds, lp=self.lp)
901 # Make sure we're connected as SYSTEM
903 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
905 sids = res[0].get("tokenGroups")
906 assert len(sids) == 1
907 sid = ndr_unpack(security.dom_sid, sids[0])
908 assert str(sid) == security.SID_NT_SYSTEM
909 except Exception as msg:
910 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
911 (security.SID_NT_SYSTEM))
913 # We use sort here in order to have a predictable processing order
914 # this might not be strictly needed, but also doesn't hurt here
915 for a in sorted(virtual_attributes.keys()):
916 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
917 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
921 def get_account_attributes(self, samdb, username, basedn, filter, scope,
928 (attr, _, opts) = a.partition(';')
930 attr_opts[attr] = opts
932 attr_opts[attr] = None
933 search_attrs.append(attr)
934 lower_attrs = [x.lower() for x in search_attrs]
936 require_supplementalCredentials = False
937 for a in virtual_attributes.keys():
938 if a.lower() in lower_attrs:
939 require_supplementalCredentials = True
940 add_supplementalCredentials = False
941 add_unicodePwd = False
942 if require_supplementalCredentials:
943 a = "supplementalCredentials"
944 if a.lower() not in lower_attrs:
946 add_supplementalCredentials = True
948 if a.lower() not in lower_attrs:
950 add_unicodePwd = True
951 add_sAMAcountName = False
953 if a.lower() not in lower_attrs:
955 add_sAMAcountName = True
957 add_userPrincipalName = False
958 upn = "usePrincipalName"
959 if upn.lower() not in lower_attrs:
960 search_attrs += [upn]
961 add_userPrincipalName = True
963 if scope == ldb.SCOPE_BASE:
964 search_controls = ["show_deleted:1", "show_recycled:1"]
968 res = samdb.search(base=basedn, expression=filter,
969 scope=scope, attrs=search_attrs,
970 controls=search_controls)
972 raise Exception('Unable to find user "%s"' % (username or filter))
974 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
975 except Exception as msg:
976 # FIXME: catch more specific exception
977 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
982 if "supplementalCredentials" in obj:
983 sc_blob = obj["supplementalCredentials"][0]
984 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
985 if add_supplementalCredentials:
986 del obj["supplementalCredentials"]
987 if "unicodePwd" in obj:
988 unicodePwd = obj["unicodePwd"][0]
990 del obj["unicodePwd"]
991 account_name = str(obj["sAMAccountName"][0])
992 if add_sAMAcountName:
993 del obj["sAMAccountName"]
994 if "userPrincipalName" in obj:
995 account_upn = str(obj["userPrincipalName"][0])
997 realm = self.lp.get("realm")
998 account_upn = "%s@%s" % (account_name, realm.lower())
999 if add_userPrincipalName:
1000 del obj["userPrincipalName"]
1004 def get_package(name, min_idx=0):
1005 if name in calculated:
1006 return calculated[name]
1010 min_idx = len(sc.sub.packages) + min_idx
1012 for p in sc.sub.packages:
1019 return binascii.a2b_hex(p.data)
1024 # Samba adds 'Primary:SambaGPG' at the end.
1025 # When Windows sets the password it keeps
1026 # 'Primary:SambaGPG' and rotates it to
1027 # the begining. So we can only use the value,
1028 # if it is the last one.
1030 # In order to get more protection we verify
1031 # the nthash of the decrypted utf16 password
1032 # against the stored nthash in unicodePwd.
1034 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1035 if sgv is not None and unicodePwd is not None:
1036 ctx = gpgme.Context()
1038 cipher_io = io.BytesIO(sgv)
1039 plain_io = io.BytesIO()
1041 ctx.decrypt(cipher_io, plain_io)
1042 cv = plain_io.getvalue()
1044 # We only use the password if it matches
1045 # the current nthash stored in the unicodePwd
1048 tmp = credentials.Credentials()
1050 tmp.set_utf16_password(cv)
1051 nthash = tmp.get_nt_hash()
1052 if nthash == unicodePwd:
1053 calculated["Primary:CLEARTEXT"] = cv
1054 except gpgme.GpgmeError as e1:
1055 (major, minor, msg) = e1.args
1056 if major == gpgme.ERR_BAD_SECKEY:
1057 msg = "ERR_BAD_SECKEY: " + msg
1059 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1060 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1061 username or account_name, msg))
1063 def get_utf8(a, b, username):
1065 u = text_type(get_bytes(b), 'utf-16-le')
1066 except UnicodeDecodeError as e:
1067 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1070 u8 = u.encode('utf-8')
1073 # Extract the WDigest hash for the value specified by i.
1074 # Builds an htdigest compatible value
1077 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1078 domain, dns_domain):
1083 user = account_name.lower()
1084 realm = domain.lower()
1086 user = account_name.upper()
1087 realm = domain.upper()
1090 realm = domain.upper()
1093 realm = domain.lower()
1095 user = account_name.upper()
1096 realm = domain.lower()
1098 user = account_name.lower()
1099 realm = domain.upper()
1102 realm = dns_domain.lower()
1104 user = account_name.lower()
1105 realm = dns_domain.lower()
1107 user = account_name.upper()
1108 realm = dns_domain.upper()
1111 realm = dns_domain.upper()
1114 realm = dns_domain.lower()
1116 user = account_name.upper()
1117 realm = dns_domain.lower()
1119 user = account_name.lower()
1120 realm = dns_domain.upper()
1125 user = account_upn.lower()
1128 user = account_upn.upper()
1131 user = "%s\\%s" % (domain, account_name)
1134 user = "%s\\%s" % (domain.lower(), account_name.lower())
1137 user = "%s\\%s" % (domain.upper(), account_name.upper())
1143 user = account_name.lower()
1146 user = account_name.upper()
1152 user = account_upn.lower()
1155 user = account_upn.upper()
1158 user = "%s\\%s" % (domain, account_name)
1161 # Differs from spec, see tests
1162 user = "%s\\%s" % (domain.lower(), account_name.lower())
1165 # Differs from spec, see tests
1166 user = "%s\\%s" % (domain.upper(), account_name.upper())
1171 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1174 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
1175 return "%s:%s:%s" % (user, realm, get_string(digest))
1179 # get the value for a virtualCrypt attribute.
1180 # look for an exact match on algorithm and rounds in supplemental creds
1181 # if not found calculate using Primary:CLEARTEXT
1182 # if no Primary:CLEARTEXT return the first supplementalCredential
1183 # that matches the algorithm.
1184 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1187 b = get_package("Primary:userPassword")
1189 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1191 # No exact match on algorithm and number of rounds
1192 # try and calculate one from the Primary:CLEARTEXT
1193 b = get_package("Primary:CLEARTEXT")
1195 u8 = get_utf8(a, b, username or account_name)
1197 # in py2 using get_bytes should ensure u8 is unmodified
1198 # in py3 it will be decoded
1199 sv = get_crypt_value(str(algorithm), get_string(u8), rounds)
1201 # Unable to calculate a hash with the specified
1202 # number of rounds, fall back to the first hash using
1203 # the specified algorithm
1207 return "{CRYPT}" + sv
1209 def get_userPassword_hash(blob, algorithm, rounds):
1210 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1213 # Check that the NT hash has not been changed without updating
1214 # the user password hashes. This indicates that password has been
1215 # changed without updating the supplemental credentials.
1216 if unicodePwd != bytearray(up.current_nt_hash.hash):
1219 scheme_prefix = "$%d$" % algorithm
1220 prefix = scheme_prefix
1222 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1226 # in PY2 this should just do nothing and in PY3 if bytes
1227 # it will decode them
1228 h_value = get_string(h.value)
1229 if (scheme_match is None and
1230 h.scheme == SCHEME and
1231 h_value.startswith(scheme_prefix)):
1232 scheme_match = h_value
1233 if h.scheme == SCHEME and h_value.startswith(prefix):
1234 return (h_value, scheme_match)
1236 # No match on the number of rounds, return the value of the
1237 # first matching scheme
1238 return (None, scheme_match)
1240 def get_kerberos_ctr():
1241 primary_krb5 = get_package("Primary:Kerberos-Newer-Keys")
1242 if primary_krb5 is None:
1243 primary_krb5 = get_package("Primary:Kerberos")
1244 if primary_krb5 is None:
1246 krb5_blob = ndr_unpack(drsblobs.package_PrimaryKerberosBlob,
1248 return (krb5_blob.version, krb5_blob.ctr)
1250 # We use sort here in order to have a predictable processing order
1251 for a in sorted(virtual_attributes.keys()):
1252 if not a.lower() in lower_attrs:
1255 if a == "virtualClearTextUTF8":
1256 b = get_package("Primary:CLEARTEXT")
1259 u8 = get_utf8(a, b, username or account_name)
1263 elif a == "virtualClearTextUTF16":
1264 v = get_package("Primary:CLEARTEXT")
1267 elif a == "virtualSSHA":
1268 b = get_package("Primary:CLEARTEXT")
1271 u8 = get_utf8(a, b, username or account_name)
1274 salt = get_random_bytes(4)
1278 bv = h.digest() + salt
1279 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
1280 elif a == "virtualCryptSHA256":
1281 rounds = get_rounds(attr_opts[a])
1282 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1286 elif a == "virtualCryptSHA512":
1287 rounds = get_rounds(attr_opts[a])
1288 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1292 elif a == "virtualSambaGPG":
1293 # Samba adds 'Primary:SambaGPG' at the end.
1294 # When Windows sets the password it keeps
1295 # 'Primary:SambaGPG' and rotates it to
1296 # the begining. So we can only use the value,
1297 # if it is the last one.
1298 v = get_package("Primary:SambaGPG", min_idx=-1)
1301 elif a == "virtualKerberosSalt":
1302 (krb5_v, krb5_ctr) = get_kerberos_ctr()
1303 if krb5_v not in [3, 4]:
1305 v = krb5_ctr.salt.string
1306 elif a.startswith("virtualWDigest"):
1307 primary_wdigest = get_package("Primary:WDigest")
1308 if primary_wdigest is None:
1310 x = a[len("virtualWDigest"):]
1315 domain = self.lp.get("workgroup")
1316 dns_domain = samdb.domain_dns_name()
1317 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1322 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1325 def parse_attributes(self, attributes):
1327 if attributes is None:
1328 raise CommandError("Please specify --attributes")
1329 attrs = attributes.split(',')
1332 pa = pa.lstrip().rstrip()
1333 for da in disabled_virtual_attributes.keys():
1334 if pa.lower() == da.lower():
1335 r = disabled_virtual_attributes[da]["reason"]
1336 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1338 for va in virtual_attributes.keys():
1339 if pa.lower() == va.lower():
1340 # Take the real name
1343 password_attrs += [pa]
1345 return password_attrs
1348 class cmd_user_getpassword(GetPasswordCommand):
1349 """Get the password fields of a user/computer account.
1351 This command gets the logon password for a user/computer account.
1353 The username specified on the command is the sAMAccountName.
1354 The username may also be specified using the --filter option.
1356 The command must be run from the root user id or another authorized user id.
1357 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1358 used to adjust the local path. By default tdb:// is used by default.
1360 The '--attributes' parameter takes a comma separated list of attributes,
1361 which will be printed or given to the script specified by '--script'. If a
1362 specified attribute is not available on an object it's silently omitted.
1363 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1364 the NTHASH) and the following virtual attributes are possible (see --help
1365 for which virtual attributes are supported in your environment):
1367 virtualClearTextUTF16: The raw cleartext as stored in the
1368 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1369 with '--decrypt-samba-gpg') buffer inside of the
1370 supplementalCredentials attribute. This typically
1371 contains valid UTF-16-LE, but may contain random
1372 bytes, e.g. for computer accounts.
1374 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1375 (only from valid UTF-16-LE)
1377 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1378 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1380 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1381 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1382 with a $5$... salt, see crypt(3) on modern systems.
1383 The number of rounds used to calculate the hash can
1384 also be specified. By appending ";rounds=x" to the
1385 attribute name i.e. virtualCryptSHA256;rounds=10000
1386 will calculate a SHA256 hash with 10,000 rounds.
1387 non numeric values for rounds are silently ignored
1388 The value is calculated as follows:
1389 1) If a value exists in 'Primary:userPassword' with
1390 the specified number of rounds it is returned.
1391 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1392 '--decrypt-samba-gpg'. Calculate a hash with
1393 the specified number of rounds
1394 3) Return the first CryptSHA256 value in
1395 'Primary:userPassword'
1398 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1399 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1400 with a $6$... salt, see crypt(3) on modern systems.
1401 The number of rounds used to calculate the hash can
1402 also be specified. By appending ";rounds=x" to the
1403 attribute name i.e. virtualCryptSHA512;rounds=10000
1404 will calculate a SHA512 hash with 10,000 rounds.
1405 non numeric values for rounds are silently ignored
1406 The value is calculated as follows:
1407 1) If a value exists in 'Primary:userPassword' with
1408 the specified number of rounds it is returned.
1409 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1410 '--decrypt-samba-gpg'. Calculate a hash with
1411 the specified number of rounds
1412 3) Return the first CryptSHA512 value in
1413 'Primary:userPassword'
1415 virtualWDigestNN: The individual hash values stored in
1416 'Primary:WDigest' where NN is the hash number in
1418 NOTE: As at 22-05-2017 the documentation:
1419 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1420 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1423 virtualKerberosSalt: This results the salt string that is used to compute
1424 Kerberos keys from a UTF-8 cleartext password.
1426 virtualSambaGPG: The raw cleartext as stored in the
1427 'Primary:SambaGPG' buffer inside of the
1428 supplementalCredentials attribute.
1429 See the 'password hash gpg key ids' option in
1432 The '--decrypt-samba-gpg' option triggers decryption of the
1433 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1434 in your environment or not (the python-gpgme package is required). Please
1435 note that you might need to set the GNUPGHOME environment variable. If the
1436 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1437 environment variable has been set correctly and the passphrase is already
1438 known by the gpg-agent.
1441 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1444 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1448 super(cmd_user_getpassword, self).__init__()
1450 synopsis = "%prog (<username>|--filter <filter>) [options]"
1452 takes_optiongroups = {
1453 "sambaopts": options.SambaOptions,
1454 "versionopts": options.VersionOptions,
1458 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1459 metavar="URL", dest="H"),
1460 Option("--filter", help="LDAP Filter to set password on", type=str),
1461 Option("--attributes", type=str,
1462 help=virtual_attributes_help,
1463 metavar="ATTRIBUTELIST", dest="attributes"),
1464 Option("--decrypt-samba-gpg",
1465 help=decrypt_samba_gpg_help,
1466 action="store_true", default=False, dest="decrypt_samba_gpg"),
1469 takes_args = ["username?"]
1471 def run(self, username=None, H=None, filter=None,
1472 attributes=None, decrypt_samba_gpg=None,
1473 sambaopts=None, versionopts=None):
1474 self.lp = sambaopts.get_loadparm()
1476 if decrypt_samba_gpg and not gpgme_support:
1477 raise CommandError(decrypt_samba_gpg_help)
1479 if filter is None and username is None:
1480 raise CommandError("Either the username or '--filter' must be specified!")
1483 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1485 if attributes is None:
1486 raise CommandError("Please specify --attributes")
1488 password_attrs = self.parse_attributes(attributes)
1490 samdb = self.connect_system_samdb(url=H, allow_local=True)
1492 obj = self.get_account_attributes(samdb, username,
1495 scope=ldb.SCOPE_SUBTREE,
1496 attrs=password_attrs,
1497 decrypt=decrypt_samba_gpg)
1499 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1500 self.outf.write("%s" % ldif)
1501 self.outf.write("Got password OK\n")
1504 class cmd_user_syncpasswords(GetPasswordCommand):
1505 """Sync the password of user accounts.
1507 This syncs logon passwords for user accounts.
1509 Note that this command should run on a single domain controller only
1510 (typically the PDC-emulator). However the "password hash gpg key ids"
1511 option should to be configured on all domain controllers.
1513 The command must be run from the root user id or another authorized user id.
1514 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1515 local path. By default, ldapi:// is used with the default path to the
1516 privileged ldapi socket.
1518 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1519 "Sync Loop Terminate".
1522 Cache Initialization
1523 ====================
1525 The first time, this command needs to be called with
1526 '--cache-ldb-initialize' in order to initialize its cache.
1528 The cache initialization requires '--attributes' and allows the following
1529 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1532 The '--attributes' parameter takes a comma separated list of attributes,
1533 which will be printed or given to the script specified by '--script'. If a
1534 specified attribute is not available on an object it will be silently omitted.
1535 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1536 the NTHASH) and the following virtual attributes are possible (see '--help'
1537 for supported virtual attributes in your environment):
1539 virtualClearTextUTF16: The raw cleartext as stored in the
1540 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1541 with '--decrypt-samba-gpg') buffer inside of the
1542 supplementalCredentials attribute. This typically
1543 contains valid UTF-16-LE, but may contain random
1544 bytes, e.g. for computer accounts.
1546 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1547 (only from valid UTF-16-LE)
1549 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1550 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1552 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1553 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1554 with a $5$... salt, see crypt(3) on modern systems.
1555 The number of rounds used to calculate the hash can
1556 also be specified. By appending ";rounds=x" to the
1557 attribute name i.e. virtualCryptSHA256;rounds=10000
1558 will calculate a SHA256 hash with 10,000 rounds.
1559 non numeric values for rounds are silently ignored
1560 The value is calculated as follows:
1561 1) If a value exists in 'Primary:userPassword' with
1562 the specified number of rounds it is returned.
1563 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1564 '--decrypt-samba-gpg'. Calculate a hash with
1565 the specified number of rounds
1566 3) Return the first CryptSHA256 value in
1567 'Primary:userPassword'
1569 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1570 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1571 with a $6$... salt, see crypt(3) on modern systems.
1572 The number of rounds used to calculate the hash can
1573 also be specified. By appending ";rounds=x" to the
1574 attribute name i.e. virtualCryptSHA512;rounds=10000
1575 will calculate a SHA512 hash with 10,000 rounds.
1576 non numeric values for rounds are silently ignored
1577 The value is calculated as follows:
1578 1) If a value exists in 'Primary:userPassword' with
1579 the specified number of rounds it is returned.
1580 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1581 '--decrypt-samba-gpg'. Calculate a hash with
1582 the specified number of rounds
1583 3) Return the first CryptSHA512 value in
1584 'Primary:userPassword'
1586 virtualWDigestNN: The individual hash values stored in
1587 'Primary:WDigest' where NN is the hash number in
1589 NOTE: As at 22-05-2017 the documentation:
1590 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1591 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1594 virtualKerberosSalt: This results the salt string that is used to compute
1595 Kerberos keys from a UTF-8 cleartext password.
1597 virtualSambaGPG: The raw cleartext as stored in the
1598 'Primary:SambaGPG' buffer inside of the
1599 supplementalCredentials attribute.
1600 See the 'password hash gpg key ids' option in
1603 The '--decrypt-samba-gpg' option triggers decryption of the
1604 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1605 in your environment or not (the python-gpgme package is required). Please
1606 note that you might need to set the GNUPGHOME environment variable. If the
1607 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1608 environment variable has been set correctly and the passphrase is already
1609 known by the gpg-agent.
1611 The '--script' option specifies a custom script that is called whenever any
1612 of the dirsyncAttributes (see below) was changed. The script is called
1613 without any arguments. It gets the LDIF for exactly one object on STDIN.
1614 If the script processed the object successfully it has to respond with a
1615 single line starting with 'DONE-EXIT: ' followed by an optional message.
1617 Note that the script might be called without any password change, e.g. if
1618 the account was disabled (a userAccountControl change) or the
1619 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1620 are always returned as unique identifier of the account. It might be useful
1621 to also ask for non-password attributes like: objectSid, sAMAccountName,
1622 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1623 Depending on the object, some attributes may not be present/available,
1624 but you always get the current state (and not a diff).
1626 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1629 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1630 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1631 (!(sAMAccountName=krbtgt*)))
1632 This means only normal (non-krbtgt) user
1633 accounts are monitored. The '--filter' can modify that, e.g. if it's
1634 required to also sync computer accounts.
1640 This (default) mode runs in an endless loop waiting for password related
1641 changes in the active directory database. It makes use of the
1642 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1643 get changes in a reliable fashion. Objects are monitored for changes of the
1644 following dirsyncAttributes:
1646 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1647 userPrincipalName and userAccountControl.
1649 It recovers from LDAP disconnects and updates the cache in conservative way
1650 (in single steps after each successfully processed change). An error from
1651 the script (specified by '--script') will result in fatal error and this
1652 command will exit. But the cache state should be still valid and can be
1653 resumed in the next "Sync Loop Run".
1655 The '--logfile' option specifies an optional (required if '--daemon' is
1656 specified) logfile that takes all output of the command. The logfile is
1657 automatically reopened if fstat returns st_nlink == 0.
1659 The optional '--daemon' option will put the command into the background.
1661 You can stop the command without the '--daemon' option, also by hitting
1664 If you specify the '--no-wait' option the command skips the
1665 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1666 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1671 In order to terminate an already running command (likely as daemon) the
1672 '--terminate' option can be used. This also requires the '--logfile' option
1677 samba-tool user syncpasswords --cache-ldb-initialize \\
1678 --attributes=virtualClearTextUTF8
1679 samba-tool user syncpasswords
1682 samba-tool user syncpasswords --cache-ldb-initialize \\
1683 --attributes=objectGUID,objectSID,sAMAccountName,\\
1684 userPrincipalName,userAccountControl,pwdLastSet,\\
1685 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1686 --script=/path/to/my-custom-syncpasswords-script.py
1687 samba-tool user syncpasswords --daemon \\
1688 --logfile=/var/log/samba/user-syncpasswords.log
1689 samba-tool user syncpasswords --terminate \\
1690 --logfile=/var/log/samba/user-syncpasswords.log
1694 super(cmd_user_syncpasswords, self).__init__()
1696 synopsis = "%prog [--cache-ldb-initialize] [options]"
1698 takes_optiongroups = {
1699 "sambaopts": options.SambaOptions,
1700 "versionopts": options.VersionOptions,
1704 Option("--cache-ldb-initialize",
1705 help="Initialize the cache for the first time",
1706 dest="cache_ldb_initialize", action="store_true"),
1707 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1708 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1709 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1710 metavar="URL", dest="H"),
1711 Option("--filter", help="optional LDAP filter to set password on", type=str,
1712 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1713 Option("--attributes", type=str,
1714 help=virtual_attributes_help,
1715 metavar="ATTRIBUTELIST", dest="attributes"),
1716 Option("--decrypt-samba-gpg",
1717 help=decrypt_samba_gpg_help,
1718 action="store_true", default=False, dest="decrypt_samba_gpg"),
1719 Option("--script", help="Script that is called for each password change", type=str,
1720 metavar="/path/to/syncpasswords.script", dest="script"),
1721 Option("--no-wait", help="Don't block waiting for changes",
1722 action="store_true", default=False, dest="nowait"),
1723 Option("--logfile", type=str,
1724 help="The logfile to use (required in --daemon mode).",
1725 metavar="/path/to/syncpasswords.log", dest="logfile"),
1726 Option("--daemon", help="daemonize after initial setup",
1727 action="store_true", default=False, dest="daemon"),
1728 Option("--terminate",
1729 help="Send a SIGTERM to an already running (daemon) process",
1730 action="store_true", default=False, dest="terminate"),
1733 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1734 H=None, filter=None,
1735 attributes=None, decrypt_samba_gpg=None,
1736 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1737 sambaopts=None, versionopts=None):
1739 self.lp = sambaopts.get_loadparm()
1741 self.samdb_url = None
1745 if not cache_ldb_initialize:
1746 if attributes is not None:
1747 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1748 if decrypt_samba_gpg:
1749 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1750 if script is not None:
1751 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1752 if filter is not None:
1753 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1755 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1757 if nowait is not False:
1758 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1759 if logfile is not None:
1760 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1761 if daemon is not False:
1762 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1763 if terminate is not False:
1764 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1768 raise CommandError("--daemon is not allowed together with --no-wait")
1769 if terminate is not False:
1770 raise CommandError("--terminate is not allowed together with --no-wait")
1772 if terminate is True and daemon is True:
1773 raise CommandError("--terminate is not allowed together with --daemon")
1775 if daemon is True and logfile is None:
1776 raise CommandError("--daemon is only allowed together with --logfile")
1778 if terminate is True and logfile is None:
1779 raise CommandError("--terminate is only allowed together with --logfile")
1781 if script is not None:
1782 if not os.path.exists(script):
1783 raise CommandError("script[%s] does not exist!" % script)
1785 sync_command = "%s" % os.path.abspath(script)
1789 dirsync_filter = filter
1790 if dirsync_filter is None:
1791 dirsync_filter = "(&" + \
1792 "(objectClass=user)" + \
1793 "(userAccountControl:%s:=%u)" % (
1794 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1795 "(!(sAMAccountName=krbtgt*))" + \
1798 dirsync_secret_attrs = [
1801 "supplementalCredentials",
1804 dirsync_attrs = dirsync_secret_attrs + [
1807 "userPrincipalName",
1808 "userAccountControl",
1813 password_attrs = None
1815 if cache_ldb_initialize:
1817 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1819 if decrypt_samba_gpg and not gpgme_support:
1820 raise CommandError(decrypt_samba_gpg_help)
1822 password_attrs = self.parse_attributes(attributes)
1823 lower_attrs = [x.lower() for x in password_attrs]
1824 # We always return these in order to track deletions
1825 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1826 if a.lower() not in lower_attrs:
1827 password_attrs += [a]
1829 if cache_ldb is not None:
1830 if cache_ldb.lower().startswith("ldapi://"):
1831 raise CommandError("--cache_ldb ldapi:// is not supported")
1832 elif cache_ldb.lower().startswith("ldap://"):
1833 raise CommandError("--cache_ldb ldap:// is not supported")
1834 elif cache_ldb.lower().startswith("ldaps://"):
1835 raise CommandError("--cache_ldb ldaps:// is not supported")
1836 elif cache_ldb.lower().startswith("tdb://"):
1839 if not os.path.exists(cache_ldb):
1840 cache_ldb = self.lp.private_path(cache_ldb)
1842 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1844 self.lockfile = "%s.pid" % cache_ldb
1847 if self.logfile is not None:
1849 if info.st_nlink == 0:
1850 logfile = self.logfile
1852 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1853 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
1858 log_msg("Reopened logfile[%s]\n" % (logfile))
1859 self.logfile = logfile
1860 msg = "%s: pid[%d]: %s" % (
1864 self.outf.write(msg)
1873 "passwordAttribute",
1879 self.cache = Ldb(cache_ldb)
1880 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1881 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1885 self.samdb_url = str(res[0]["samdbUrl"][0])
1886 except KeyError as e:
1887 self.samdb_url = None
1889 self.samdb_url = None
1890 if self.samdb_url is None and not cache_ldb_initialize:
1891 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1893 if self.samdb_url is not None and cache_ldb_initialize:
1894 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1896 if self.samdb_url is None:
1898 self.dirsync_filter = dirsync_filter
1899 self.dirsync_attrs = dirsync_attrs
1900 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"]
1901 self.password_attrs = password_attrs
1902 self.decrypt_samba_gpg = decrypt_samba_gpg
1903 self.sync_command = sync_command
1904 add_ldif = "dn: %s\n" % self.cache_dn
1905 add_ldif += "objectClass: userSyncPasswords\n"
1906 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(get_bytes(self.samdb_url)).decode('utf8')
1907 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(get_bytes(self.dirsync_filter)).decode('utf8')
1908 for a in self.dirsync_attrs:
1909 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8')
1910 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1911 for a in self.password_attrs:
1912 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8')
1913 if self.decrypt_samba_gpg:
1914 add_ldif += "decryptSambaGPG: TRUE\n"
1916 add_ldif += "decryptSambaGPG: FALSE\n"
1917 if self.sync_command is not None:
1918 add_ldif += "syncCommand: %s\n" % self.sync_command
1919 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1920 self.cache.add_ldif(add_ldif)
1921 self.current_pid = None
1922 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1923 msgs = self.cache.parse_ldif(add_ldif)
1924 changetype, msg = next(msgs)
1925 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1926 self.outf.write("%s" % ldif)
1928 self.dirsync_filter = str(res[0]["dirsyncFilter"][0])
1929 self.dirsync_attrs = []
1930 for a in res[0]["dirsyncAttribute"]:
1931 self.dirsync_attrs.append(str(a))
1932 self.dirsync_controls = [str(res[0]["dirsyncControl"][0]), "extended_dn:1:0"]
1933 self.password_attrs = []
1934 for a in res[0]["passwordAttribute"]:
1935 self.password_attrs.append(str(a))
1936 decrypt_string = str(res[0]["decryptSambaGPG"][0])
1937 assert(decrypt_string in ["TRUE", "FALSE"])
1938 if decrypt_string == "TRUE":
1939 self.decrypt_samba_gpg = True
1941 self.decrypt_samba_gpg = False
1942 if "syncCommand" in res[0]:
1943 self.sync_command = str(res[0]["syncCommand"][0])
1945 self.sync_command = None
1946 if "currentPid" in res[0]:
1947 self.current_pid = int(res[0]["currentPid"][0])
1949 self.current_pid = None
1950 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1954 def run_sync_command(dn, ldif):
1955 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1956 sync_command_p = Popen(self.sync_command,
1961 res = sync_command_p.poll()
1964 input = "%s" % (ldif)
1965 reply = sync_command_p.communicate(input)[0]
1966 log_msg("%s\n" % (reply))
1967 res = sync_command_p.poll()
1969 sync_command_p.terminate()
1970 res = sync_command_p.wait()
1972 if reply.startswith("DONE-EXIT: "):
1975 log_msg("RESULT: %s\n" % (res))
1976 raise Exception("ERROR: %s - %s\n" % (res, reply))
1978 def handle_object(idx, dirsync_obj):
1979 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1980 guid = ndr_unpack(misc.GUID, binary_guid)
1981 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1982 sid = ndr_unpack(security.dom_sid, binary_sid)
1983 domain_sid, rid = sid.split()
1984 if rid == security.DOMAIN_RID_KRBTGT:
1985 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1987 for a in list(dirsync_obj.keys()):
1988 for h in dirsync_secret_attrs:
1989 if a.lower() == h.lower():
1991 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1992 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1993 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
1994 obj = self.get_account_attributes(self.samdb,
1995 username="%s" % sid,
1996 basedn="<GUID=%s>" % guid,
1997 filter="(objectClass=user)",
1998 scope=ldb.SCOPE_BASE,
1999 attrs=self.password_attrs,
2000 decrypt=self.decrypt_samba_gpg)
2001 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
2002 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
2003 if self.sync_command is None:
2004 self.outf.write("%s" % (ldif))
2006 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
2007 run_sync_command(obj.dn, ldif)
2009 def check_current_pid_conflict(terminate):
2015 self.lockfd = os.open(self.lockfile, flags, 0o600)
2016 except IOError as e4:
2017 (err, msg) = e4.args
2018 if err == errno.ENOENT:
2021 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
2022 (self.lockfile, msg, err))
2025 got_exclusive = False
2027 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2028 got_exclusive = True
2029 except IOError as e5:
2030 (err, msg) = e5.args
2031 if err != errno.EACCES and err != errno.EAGAIN:
2032 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
2033 (self.lockfile, msg, err))
2036 if not got_exclusive:
2037 buf = os.read(self.lockfd, 64)
2038 self.current_pid = None
2040 self.current_pid = int(buf)
2041 except ValueError as e:
2043 if self.current_pid is not None:
2046 if got_exclusive and terminate:
2048 os.ftruncate(self.lockfd, 0)
2049 except IOError as e2:
2050 (err, msg) = e2.args
2051 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2052 (self.lockfile, msg, err))
2054 os.close(self.lockfd)
2059 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2060 except IOError as e6:
2061 (err, msg) = e6.args
2062 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2063 (self.lockfile, msg, err))
2065 # We leave the function with the shared lock.
2068 def update_pid(pid):
2069 if self.lockfd != -1:
2070 got_exclusive = False
2071 # Try 5 times to get the exclusiv lock.
2072 for i in range(0, 5):
2074 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2075 got_exclusive = True
2076 except IOError as e:
2078 if err != errno.EACCES and err != errno.EAGAIN:
2079 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2080 (pid, self.lockfile, msg, err))
2085 if not got_exclusive:
2086 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
2087 (pid, self.lockfile))
2088 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2089 (pid, self.lockfile))
2096 os.ftruncate(self.lockfd, 0)
2098 os.write(self.lockfd, get_bytes(buf))
2099 except IOError as e3:
2100 (err, msg) = e3.args
2101 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2102 (self.lockfile, msg, err))
2104 self.current_pid = pid
2105 if self.current_pid is not None:
2106 log_msg("currentPid: %d\n" % self.current_pid)
2108 modify_ldif = "dn: %s\n" % (self.cache_dn)
2109 modify_ldif += "changetype: modify\n"
2110 modify_ldif += "replace: currentPid\n"
2111 if self.current_pid is not None:
2112 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2113 modify_ldif += "replace: currentTime\n"
2114 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2115 self.cache.modify_ldif(modify_ldif)
2118 def update_cache(res_controls):
2119 assert len(res_controls) > 0
2120 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2121 res_controls[0].critical = True
2122 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2123 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2125 modify_ldif = "dn: %s\n" % (self.cache_dn)
2126 modify_ldif += "changetype: modify\n"
2127 modify_ldif += "replace: dirsyncControl\n"
2128 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2129 modify_ldif += "replace: currentTime\n"
2130 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2131 self.cache.modify_ldif(modify_ldif)
2134 def check_object(dirsync_obj, res_controls):
2135 assert len(res_controls) > 0
2136 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2138 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2139 sid = ndr_unpack(security.dom_sid, binary_sid)
2141 lastCookie = str(res_controls[0])
2143 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2144 expression="(lastCookie=%s)" % (
2145 ldb.binary_encode(lastCookie)),
2151 def update_object(dirsync_obj, res_controls):
2152 assert len(res_controls) > 0
2153 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2155 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2156 sid = ndr_unpack(security.dom_sid, binary_sid)
2158 lastCookie = str(res_controls[0])
2160 self.cache.transaction_start()
2162 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2163 expression="(objectClass=*)",
2164 attrs=["lastCookie"])
2166 add_ldif = "dn: %s\n" % (dn)
2167 add_ldif += "objectClass: userCookie\n"
2168 add_ldif += "lastCookie: %s\n" % (lastCookie)
2169 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2170 self.cache.add_ldif(add_ldif)
2172 modify_ldif = "dn: %s\n" % (dn)
2173 modify_ldif += "changetype: modify\n"
2174 modify_ldif += "replace: lastCookie\n"
2175 modify_ldif += "lastCookie: %s\n" % (lastCookie)
2176 modify_ldif += "replace: currentTime\n"
2177 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2178 self.cache.modify_ldif(modify_ldif)
2179 self.cache.transaction_commit()
2180 except Exception as e:
2181 self.cache.transaction_cancel()
2187 res = self.samdb.search(expression=str(self.dirsync_filter),
2188 scope=ldb.SCOPE_SUBTREE,
2189 attrs=self.dirsync_attrs,
2190 controls=self.dirsync_controls)
2191 log_msg("dirsync_loop(): results %d\n" % len(res))
2194 done = check_object(r, res.controls)
2196 handle_object(ri, r)
2197 update_object(r, res.controls)
2199 update_cache(res.controls)
2203 def sync_loop(wait):
2204 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2205 notify_controls = ["notification:1", "show_recycled:1"]
2206 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2207 scope=ldb.SCOPE_SUBTREE,
2209 controls=notify_controls,
2213 log_msg("Resuming monitoring\n")
2215 log_msg("Getting changes\n")
2216 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2217 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2218 self.outf.write("syncCommand: %s\n" % self.sync_command)
2221 if wait is not True:
2224 for msg in notify_handle:
2225 if not isinstance(msg, ldb.Message):
2226 self.outf.write("referal: %s\n" % msg)
2228 created = msg.get("uSNCreated")[0]
2229 changed = msg.get("uSNChanged")[0]
2230 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2231 (msg.dn, created, changed))
2235 res = notify_handle.result()
2240 orig_pid = os.getpid()
2245 if pid == 0: # Actual daemon
2247 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2252 if cache_ldb_initialize:
2254 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2259 if logfile is not None:
2260 import resource # Resource usage information.
2261 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2262 if maxfd == resource.RLIM_INFINITY:
2263 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2264 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2265 self.outf.write("Using logfile[%s]\n" % logfile)
2266 for fd in range(0, maxfd):
2277 log_msg("Attached to logfile[%s]\n" % (logfile))
2278 self.logfile = logfile
2281 conflict = check_current_pid_conflict(terminate)
2283 if self.current_pid is None:
2284 log_msg("No process running.\n")
2287 log_msg("Proccess %d is not running anymore.\n" % (
2291 log_msg("Sending SIGTERM to proccess %d.\n" % (
2293 os.kill(self.current_pid, signal.SIGTERM)
2296 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2297 os.getpid(), self.current_pid))
2301 update_pid(os.getpid())
2306 retry_sleep_max = 600
2311 retry_sleep = retry_sleep_min
2313 while self.samdb is None:
2314 if retry_sleep != 0:
2315 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2316 time.sleep(retry_sleep)
2317 retry_sleep = retry_sleep * 2
2318 if retry_sleep >= retry_sleep_max:
2319 retry_sleep = retry_sleep_max
2320 log_msg("Connecting to '%s'\n" % self.samdb_url)
2322 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2323 except Exception as msg:
2325 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2326 if wait is not True:
2331 except ldb.LdbError as e7:
2332 (enum, estr) = e7.args
2334 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2340 class cmd_user_edit(Command):
2341 """Modify User AD object.
2343 This command will allow editing of a user account in the Active Directory
2344 domain. You will then be able to add or change attributes and their values.
2346 The username specified on the command is the sAMAccountName.
2348 The command may be run from the root userid or another authorized userid.
2350 The -H or --URL= option can be used to execute the command against a remote
2354 samba-tool user edit User1 -H ldap://samba.samdom.example.com \
2355 -U administrator --password=passw1rd
2357 Example1 shows how to edit a users attributes in the domain against a remote
2360 The -H parameter is used to specify the remote target server.
2363 samba-tool user edit User2
2365 Example2 shows how to edit a users attributes in the domain against a local
2369 samba-tool user edit User3 --editor=nano
2371 Example3 shows how to edit a users attributes in the domain against a local
2372 LDAP server using the 'nano' editor.
2375 synopsis = "%prog <username> [options]"
2378 Option("-H", "--URL", help="LDB URL for database or target server",
2379 type=str, metavar="URL", dest="H"),
2380 Option("--editor", help="Editor to use instead of the system default,"
2381 " or 'vi' if no system default is set.", type=str),
2384 takes_args = ["username"]
2385 takes_optiongroups = {
2386 "sambaopts": options.SambaOptions,
2387 "credopts": options.CredentialsOptions,
2388 "versionopts": options.VersionOptions,
2391 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2392 H=None, editor=None):
2394 lp = sambaopts.get_loadparm()
2395 creds = credopts.get_credentials(lp, fallback_machine=True)
2396 samdb = SamDB(url=H, session_info=system_session(),
2397 credentials=creds, lp=lp)
2399 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2400 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2402 domaindn = samdb.domain_dn()
2405 res = samdb.search(base=domaindn,
2407 scope=ldb.SCOPE_SUBTREE)
2410 raise CommandError('Unable to find user "%s"' % (username))
2413 r_ldif = samdb.write_ldif(msg, 1)
2414 # remove 'changetype' line
2415 result_ldif = re.sub('changetype: add\n', '', r_ldif)
2418 editor = os.environ.get('EDITOR')
2422 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2423 t_file.write(result_ldif)
2426 check_call([editor, t_file.name])
2427 except CalledProcessError as e:
2428 raise CalledProcessError("ERROR: ", e)
2429 with open(t_file.name) as edited_file:
2430 edited_message = edited_file.read()
2432 if result_ldif != edited_message:
2433 diff = difflib.ndiff(result_ldif.splitlines(),
2434 edited_message.splitlines())
2438 if line.startswith('-'):
2440 minus_lines.append(line)
2441 elif line.startswith('+'):
2443 plus_lines.append(line)
2445 user_ldif = "dn: %s\n" % user_dn
2446 user_ldif += "changetype: modify\n"
2448 for line in minus_lines:
2449 attr, val = line.split(':', 1)
2450 search_attr = "%s:" % attr
2451 if not re.search(r'^' + search_attr, str(plus_lines)):
2452 user_ldif += "delete: %s\n" % attr
2453 user_ldif += "%s: %s\n" % (attr, val)
2455 for line in plus_lines:
2456 attr, val = line.split(':', 1)
2457 search_attr = "%s:" % attr
2458 if re.search(r'^' + search_attr, str(minus_lines)):
2459 user_ldif += "replace: %s\n" % attr
2460 user_ldif += "%s: %s\n" % (attr, val)
2461 if not re.search(r'^' + search_attr, str(minus_lines)):
2462 user_ldif += "add: %s\n" % attr
2463 user_ldif += "%s: %s\n" % (attr, val)
2466 samdb.modify_ldif(user_ldif)
2467 except Exception as e:
2468 raise CommandError("Failed to modify user '%s': " %
2471 self.outf.write("Modified User '%s' successfully\n" % username)
2474 class cmd_user_show(Command):
2475 """Display a user AD object.
2477 This command displays a user account and it's attributes in the Active
2479 The username specified on the command is the sAMAccountName.
2481 The command may be run from the root userid or another authorized userid.
2483 The -H or --URL= option can be used to execute the command against a remote
2487 samba-tool user show User1 -H ldap://samba.samdom.example.com \
2488 -U administrator --password=passw1rd
2490 Example1 shows how to display a users attributes in the domain against a remote
2493 The -H parameter is used to specify the remote target server.
2496 samba-tool user show User2
2498 Example2 shows how to display a users attributes in the domain against a local
2502 samba-tool user show User2 --attributes=objectSid,memberOf
2504 Example3 shows how to display a users objectSid and memberOf attributes.
2506 synopsis = "%prog <username> [options]"
2509 Option("-H", "--URL", help="LDB URL for database or target server",
2510 type=str, metavar="URL", dest="H"),
2511 Option("--attributes",
2512 help=("Comma separated list of attributes, "
2513 "which will be printed."),
2514 type=str, dest="user_attrs"),
2517 takes_args = ["username"]
2518 takes_optiongroups = {
2519 "sambaopts": options.SambaOptions,
2520 "credopts": options.CredentialsOptions,
2521 "versionopts": options.VersionOptions,
2524 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2525 H=None, user_attrs=None):
2527 lp = sambaopts.get_loadparm()
2528 creds = credopts.get_credentials(lp, fallback_machine=True)
2529 samdb = SamDB(url=H, session_info=system_session(),
2530 credentials=creds, lp=lp)
2534 attrs = user_attrs.split(",")
2536 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2537 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2539 domaindn = samdb.domain_dn()
2542 res = samdb.search(base=domaindn, expression=filter,
2543 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2546 raise CommandError('Unable to find user "%s"' % (username))
2549 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2550 self.outf.write(user_ldif)
2553 class cmd_user_move(Command):
2554 """Move a user to an organizational unit/container.
2556 This command moves a user account into the specified organizational unit
2558 The username specified on the command is the sAMAccountName.
2559 The name of the organizational unit or container can be specified as a
2560 full DN or without the domainDN component.
2562 The command may be run from the root userid or another authorized userid.
2564 The -H or --URL= option can be used to execute the command against a remote
2568 samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
2569 -H ldap://samba.samdom.example.com -U administrator
2571 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2572 unit on a remote LDAP server.
2574 The -H parameter is used to specify the remote target server.
2577 samba-tool user move User1 CN=Users
2579 Example2 shows how to move a user User1 back into the CN=Users container
2580 on the local server.
2583 synopsis = "%prog <username> <new_parent_dn> [options]"
2586 Option("-H", "--URL", help="LDB URL for database or target server",
2587 type=str, metavar="URL", dest="H"),
2590 takes_args = ["username", "new_parent_dn"]
2591 takes_optiongroups = {
2592 "sambaopts": options.SambaOptions,
2593 "credopts": options.CredentialsOptions,
2594 "versionopts": options.VersionOptions,
2597 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2598 versionopts=None, H=None):
2599 lp = sambaopts.get_loadparm()
2600 creds = credopts.get_credentials(lp, fallback_machine=True)
2601 samdb = SamDB(url=H, session_info=system_session(),
2602 credentials=creds, lp=lp)
2603 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2605 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2606 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2608 res = samdb.search(base=domain_dn,
2610 scope=ldb.SCOPE_SUBTREE)
2613 raise CommandError('Unable to find user "%s"' % (username))
2616 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2617 except Exception as e:
2618 raise CommandError('Invalid new_parent_dn "%s": %s' %
2621 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2622 full_new_user_dn.remove_base_components(len(user_dn) - 1)
2623 full_new_user_dn.add_base(full_new_parent_dn)
2626 samdb.rename(user_dn, full_new_user_dn)
2627 except Exception as e:
2628 raise CommandError('Failed to move user "%s"' % username, e)
2629 self.outf.write('Moved user "%s" into "%s"\n' %
2630 (username, full_new_parent_dn))
2633 class cmd_user(SuperCommand):
2634 """User management."""
2637 subcommands["add"] = cmd_user_add()
2638 subcommands["create"] = cmd_user_create()
2639 subcommands["delete"] = cmd_user_delete()
2640 subcommands["disable"] = cmd_user_disable()
2641 subcommands["enable"] = cmd_user_enable()
2642 subcommands["list"] = cmd_user_list()
2643 subcommands["setexpiry"] = cmd_user_setexpiry()
2644 subcommands["password"] = cmd_user_password()
2645 subcommands["setpassword"] = cmd_user_setpassword()
2646 subcommands["getpassword"] = cmd_user_getpassword()
2647 subcommands["syncpasswords"] = cmd_user_syncpasswords()
2648 subcommands["edit"] = cmd_user_edit()
2649 subcommands["show"] = cmd_user_show()
2650 subcommands["move"] = cmd_user_move()