3 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
4 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import samba.getopt as options
34 from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError
35 from getpass import getpass
36 from samba.auth import system_session
37 from samba.samdb import SamDB
38 from samba.dcerpc import misc
39 from samba.dcerpc import security
40 from samba.dcerpc import drsblobs
41 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
46 generate_random_password,
49 from samba.net import Net
51 from samba.netcmd import (
57 from samba.compat import text_type
58 from samba.compat import get_bytes
59 from samba.compat import get_string
65 decrypt_samba_gpg_help = "Decrypt the SambaGPG password as cleartext source"
66 except ImportError as e:
68 decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
69 "python-gpgme required"
71 disabled_virtual_attributes = {
74 virtual_attributes = {
75 "virtualClearTextUTF8": {
76 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
78 "virtualClearTextUTF16": {
79 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
82 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
86 get_random_bytes_fn = None
87 if get_random_bytes_fn is None:
90 get_random_bytes_fn = Crypto.Random.get_random_bytes
91 except ImportError as e:
93 if get_random_bytes_fn is None:
96 get_random_bytes_fn = M2Crypto.Rand.rand_bytes
97 except ImportError as e:
102 if get_random_bytes_fn is not None:
104 return "Crypto.Random or M2Crypto.Rand required"
107 def get_random_bytes(num):
108 random_reason = check_random()
109 if random_reason is not None:
110 raise ImportError(random_reason)
111 return get_random_bytes_fn(num)
114 def get_crypt_value(alg, utf8pw, rounds=0):
120 salt = get_random_bytes(16)
121 # The salt needs to be in [A-Za-z0-9./]
122 # base64 is close enough and as we had 16
123 # random bytes but only need 16 characters
124 # we can ignore the possible == at the end
125 # of the base64 string
126 # we just need to replace '+' by '.'
127 b64salt = base64.b64encode(salt)[0:16].replace(b'+', b'.').decode('utf8')
130 crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
132 crypt_salt = "$%s$%s$" % (alg, b64salt)
134 crypt_value = crypt.crypt(utf8pw, crypt_salt)
135 if crypt_value is None:
136 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
137 expected_len = len(crypt_salt) + algs[alg]["length"]
138 if len(crypt_value) != expected_len:
139 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
140 crypt_salt, len(crypt_value), expected_len))
143 # Extract the rounds value from the options of a virtualCrypt attribute
144 # i.e. options = "rounds=20;other=ignored;" will return 20
145 # if the rounds option is not found or the value is not a number, 0 is returned
146 # which indicates that the default number of rounds should be used.
149 def get_rounds(options):
153 opts = options.split(';')
155 if o.lower().startswith("rounds="):
156 (key, _, val) = o.partition('=')
165 random_reason = check_random()
166 if random_reason is not None:
167 raise ImportError(random_reason)
171 virtual_attributes["virtualSSHA"] = {
173 except ImportError as e:
174 reason = "hashlib.sha1()"
176 reason += " and " + random_reason
177 reason += " required"
178 disabled_virtual_attributes["virtualSSHA"] = {
182 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
184 random_reason = check_random()
185 if random_reason is not None:
186 raise ImportError(random_reason)
188 v = get_crypt_value(alg, "")
190 virtual_attributes[attr] = {
192 except ImportError as e:
195 reason += " and " + random_reason
196 reason += " required"
197 disabled_virtual_attributes[attr] = {
200 except NotImplementedError as e:
201 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
202 disabled_virtual_attributes[attr] = {
206 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
207 for x in range(1, 30):
208 virtual_attributes["virtualWDigest%02d" % x] = {}
210 virtual_attributes_help = "The attributes to display (comma separated). "
211 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
212 if len(disabled_virtual_attributes) != 0:
213 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
216 class cmd_user_create(Command):
217 """Create a new user.
219 This command creates a new user account in the Active Directory domain. The username specified on the command is the sAMaccountName.
221 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).
223 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.
225 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.
227 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.
230 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
232 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.
235 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
237 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.
240 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
242 Example3 shows how to create a new user in the OrgUnit organizational unit.
245 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
247 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'.
250 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
251 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
253 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
254 --nis-domain is set, then the other four parameters are mandatory.
257 synopsis = "%prog <username> [<password>] [options]"
260 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
261 metavar="URL", dest="H"),
262 Option("--must-change-at-next-login",
263 help="Force password to be changed on next login",
264 action="store_true"),
265 Option("--random-password",
266 help="Generate random password",
267 action="store_true"),
268 Option("--smartcard-required",
269 help="Require a smartcard for interactive logons",
270 action="store_true"),
271 Option("--use-username-as-cn",
272 help="Force use of username as user's CN",
273 action="store_true"),
275 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>'",
277 Option("--surname", help="User's surname", type=str),
278 Option("--given-name", help="User's given name", type=str),
279 Option("--initials", help="User's initials", type=str),
280 Option("--profile-path", help="User's profile path", type=str),
281 Option("--script-path", help="User's logon script path", type=str),
282 Option("--home-drive", help="User's home drive letter", type=str),
283 Option("--home-directory", help="User's home directory path", type=str),
284 Option("--job-title", help="User's job title", type=str),
285 Option("--department", help="User's department", type=str),
286 Option("--company", help="User's company", type=str),
287 Option("--description", help="User's description", type=str),
288 Option("--mail-address", help="User's email address", type=str),
289 Option("--internet-address", help="User's home page", type=str),
290 Option("--telephone-number", help="User's phone number", type=str),
291 Option("--physical-delivery-office", help="User's office location", type=str),
292 Option("--rfc2307-from-nss",
293 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
294 action="store_true"),
295 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
296 Option("--unix-home", help="User's Unix/RFC2307 home directory",
298 Option("--uid", help="User's Unix/RFC2307 username", type=str),
299 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
300 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
301 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
302 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
305 takes_args = ["username", "password?"]
307 takes_optiongroups = {
308 "sambaopts": options.SambaOptions,
309 "credopts": options.CredentialsOptions,
310 "versionopts": options.VersionOptions,
313 def run(self, username, password=None, credopts=None, sambaopts=None,
314 versionopts=None, H=None, must_change_at_next_login=False,
315 random_password=False, use_username_as_cn=False, userou=None,
316 surname=None, given_name=None, initials=None, profile_path=None,
317 script_path=None, home_drive=None, home_directory=None,
318 job_title=None, department=None, company=None, description=None,
319 mail_address=None, internet_address=None, telephone_number=None,
320 physical_delivery_office=None, rfc2307_from_nss=False,
321 nis_domain=None, unix_home=None, uid=None, uid_number=None,
322 gid_number=None, gecos=None, login_shell=None,
323 smartcard_required=False):
325 if smartcard_required:
326 if password is not None and password is not '':
327 raise CommandError('It is not allowed to specify '
329 'together with --smartcard-required.')
330 if must_change_at_next_login:
331 raise CommandError('It is not allowed to specify '
332 '--must-change-at-next-login '
333 'together with --smartcard-required.')
335 if random_password and not smartcard_required:
336 password = generate_random_password(128, 255)
339 if smartcard_required:
341 if password is not None and password is not '':
343 password = getpass("New Password: ")
344 passwordverify = getpass("Retype Password: ")
345 if not password == passwordverify:
347 self.outf.write("Sorry, passwords do not match.\n")
350 pwent = pwd.getpwnam(username)
353 if uid_number is None:
354 uid_number = pwent[2]
355 if gid_number is None:
356 gid_number = pwent[3]
359 if login_shell is None:
360 login_shell = pwent[6]
362 lp = sambaopts.get_loadparm()
363 creds = credopts.get_credentials(lp)
365 if uid_number or gid_number:
366 if not lp.get("idmap_ldb:use rfc2307"):
367 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")
369 if nis_domain is not None:
370 if None in (uid_number, login_shell, unix_home, gid_number):
371 raise CommandError('Missing parameters. To enable NIS features, '
372 'the following options have to be given: '
373 '--nis-domain=, --uidNumber=, --login-shell='
374 ', --unix-home=, --gid-number= Operation '
378 samdb = SamDB(url=H, session_info=system_session(),
379 credentials=creds, lp=lp)
380 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
381 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
382 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
383 jobtitle=job_title, department=department, company=company, description=description,
384 mailaddress=mail_address, internetaddress=internet_address,
385 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
386 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
387 uidnumber=uid_number, gidnumber=gid_number,
388 gecos=gecos, loginshell=login_shell,
389 smartcard_required=smartcard_required)
390 except Exception as e:
391 raise CommandError("Failed to add user '%s': " % username, e)
393 self.outf.write("User '%s' created successfully\n" % username)
396 class cmd_user_add(cmd_user_create):
397 __doc__ = cmd_user_create.__doc__
398 # take this print out after the add subcommand is removed.
399 # the add subcommand is deprecated but left in for now to allow people to
402 def run(self, *args, **kwargs):
404 "Note: samba-tool user add is deprecated. "
405 "Please use samba-tool user create for the same function.\n")
406 return super(cmd_user_add, self).run(*args, **kwargs)
409 class cmd_user_delete(Command):
412 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
414 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.
416 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.
419 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
421 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.
424 sudo samba-tool user delete User2
426 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.
429 synopsis = "%prog <username> [options]"
432 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
433 metavar="URL", dest="H"),
436 takes_args = ["username"]
437 takes_optiongroups = {
438 "sambaopts": options.SambaOptions,
439 "credopts": options.CredentialsOptions,
440 "versionopts": options.VersionOptions,
443 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
445 lp = sambaopts.get_loadparm()
446 creds = credopts.get_credentials(lp, fallback_machine=True)
448 samdb = SamDB(url=H, session_info=system_session(),
449 credentials=creds, lp=lp)
451 filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
452 ldb.binary_encode(username))
455 res = samdb.search(base=samdb.domain_dn(),
456 scope=ldb.SCOPE_SUBTREE,
461 raise CommandError('Unable to find user "%s"' % (username))
464 samdb.delete(user_dn)
465 except Exception as e:
466 raise CommandError('Failed to remove user "%s"' % username, e)
467 self.outf.write("Deleted user %s\n" % username)
470 class cmd_user_list(Command):
471 """List all users."""
473 synopsis = "%prog [options]"
476 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
477 metavar="URL", dest="H"),
480 takes_optiongroups = {
481 "sambaopts": options.SambaOptions,
482 "credopts": options.CredentialsOptions,
483 "versionopts": options.VersionOptions,
486 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
487 lp = sambaopts.get_loadparm()
488 creds = credopts.get_credentials(lp, fallback_machine=True)
490 samdb = SamDB(url=H, session_info=system_session(),
491 credentials=creds, lp=lp)
493 domain_dn = samdb.domain_dn()
494 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
495 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
496 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
497 attrs=["samaccountname"])
502 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
505 class cmd_user_enable(Command):
508 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.
510 There are many reasons why an account may become disabled. These include:
511 - If a user exceeds the account policy for logon attempts
512 - If an administrator disables the account
513 - If the account expires
515 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
517 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.
519 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.
522 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
524 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.
527 su samba-tool user enable Testuser2
529 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.
532 samba-tool user enable --filter=samaccountname=Testuser3
534 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
537 synopsis = "%prog (<username>|--filter <filter>) [options]"
539 takes_optiongroups = {
540 "sambaopts": options.SambaOptions,
541 "versionopts": options.VersionOptions,
542 "credopts": options.CredentialsOptions,
546 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
547 metavar="URL", dest="H"),
548 Option("--filter", help="LDAP Filter to set password on", type=str),
551 takes_args = ["username?"]
553 def run(self, username=None, sambaopts=None, credopts=None,
554 versionopts=None, filter=None, H=None):
555 if username is None and filter is None:
556 raise CommandError("Either the username or '--filter' must be specified!")
559 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
561 lp = sambaopts.get_loadparm()
562 creds = credopts.get_credentials(lp, fallback_machine=True)
564 samdb = SamDB(url=H, session_info=system_session(),
565 credentials=creds, lp=lp)
567 samdb.enable_account(filter)
568 except Exception as msg:
569 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
570 self.outf.write("Enabled user '%s'\n" % (username or filter))
573 class cmd_user_disable(Command):
574 """Disable a user."""
576 synopsis = "%prog (<username>|--filter <filter>) [options]"
579 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
580 metavar="URL", dest="H"),
581 Option("--filter", help="LDAP Filter to set password on", type=str),
584 takes_args = ["username?"]
586 takes_optiongroups = {
587 "sambaopts": options.SambaOptions,
588 "credopts": options.CredentialsOptions,
589 "versionopts": options.VersionOptions,
592 def run(self, username=None, sambaopts=None, credopts=None,
593 versionopts=None, filter=None, H=None):
594 if username is None and filter is None:
595 raise CommandError("Either the username or '--filter' must be specified!")
598 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
600 lp = sambaopts.get_loadparm()
601 creds = credopts.get_credentials(lp, fallback_machine=True)
603 samdb = SamDB(url=H, session_info=system_session(),
604 credentials=creds, lp=lp)
606 samdb.disable_account(filter)
607 except Exception as msg:
608 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
611 class cmd_user_setexpiry(Command):
612 """Set the expiration of a user account.
614 The user can either be specified by their sAMAccountName or using the --filter option.
616 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.
618 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.
621 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
623 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.
626 sudo samba-tool user setexpiry User2 --noexpiry
628 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.
631 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
633 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.
636 samba-tool user setexpiry --noexpiry User4
637 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
640 synopsis = "%prog (<username>|--filter <filter>) [options]"
642 takes_optiongroups = {
643 "sambaopts": options.SambaOptions,
644 "versionopts": options.VersionOptions,
645 "credopts": options.CredentialsOptions,
649 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
650 metavar="URL", dest="H"),
651 Option("--filter", help="LDAP Filter to set password on", type=str),
652 Option("--days", help="Days to expiry", type=int, default=0),
653 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
656 takes_args = ["username?"]
658 def run(self, username=None, sambaopts=None, credopts=None,
659 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
660 if username is None and filter is None:
661 raise CommandError("Either the username or '--filter' must be specified!")
664 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
666 lp = sambaopts.get_loadparm()
667 creds = credopts.get_credentials(lp)
669 samdb = SamDB(url=H, session_info=system_session(),
670 credentials=creds, lp=lp)
673 samdb.setexpiry(filter, days * 24 * 3600, no_expiry_req=noexpiry)
674 except Exception as msg:
675 # FIXME: Catch more specific exception
676 raise CommandError("Failed to set expiry for user '%s': %s" % (
677 username or filter, msg))
679 self.outf.write("Expiry for user '%s' disabled.\n" % (
682 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
683 username or filter, days))
686 class cmd_user_password(Command):
687 """Change password for a user account (the one provided in authentication).
690 synopsis = "%prog [options]"
693 Option("--newpassword", help="New password", type=str),
696 takes_optiongroups = {
697 "sambaopts": options.SambaOptions,
698 "credopts": options.CredentialsOptions,
699 "versionopts": options.VersionOptions,
702 def run(self, credopts=None, sambaopts=None, versionopts=None,
705 lp = sambaopts.get_loadparm()
706 creds = credopts.get_credentials(lp)
708 # get old password now, to get the password prompts in the right order
709 old_password = creds.get_password()
711 net = Net(creds, lp, server=credopts.ipaddress)
713 password = newpassword
715 if password is not None and password is not '':
717 password = getpass("New Password: ")
718 passwordverify = getpass("Retype Password: ")
719 if not password == passwordverify:
721 self.outf.write("Sorry, passwords do not match.\n")
724 if not isinstance(password, text_type):
725 password = password.decode('utf8')
726 net.change_password(password)
727 except Exception as msg:
728 # FIXME: catch more specific exception
729 raise CommandError("Failed to change password : %s" % msg)
730 self.outf.write("Changed password OK\n")
733 class cmd_user_setpassword(Command):
734 """Set or reset the password of a user account.
736 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.
738 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.
740 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.
742 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.
745 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
747 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.
750 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
752 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.
755 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
757 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
760 synopsis = "%prog (<username>|--filter <filter>) [options]"
762 takes_optiongroups = {
763 "sambaopts": options.SambaOptions,
764 "versionopts": options.VersionOptions,
765 "credopts": options.CredentialsOptions,
769 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
770 metavar="URL", dest="H"),
771 Option("--filter", help="LDAP Filter to set password on", type=str),
772 Option("--newpassword", help="Set password", type=str),
773 Option("--must-change-at-next-login",
774 help="Force password to be changed on next login",
775 action="store_true"),
776 Option("--random-password",
777 help="Generate random password",
778 action="store_true"),
779 Option("--smartcard-required",
780 help="Require a smartcard for interactive logons",
781 action="store_true"),
782 Option("--clear-smartcard-required",
783 help="Don't require a smartcard for interactive logons",
784 action="store_true"),
787 takes_args = ["username?"]
789 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
790 versionopts=None, H=None, newpassword=None,
791 must_change_at_next_login=False, random_password=False,
792 smartcard_required=False, clear_smartcard_required=False):
793 if filter is None and username is None:
794 raise CommandError("Either the username or '--filter' must be specified!")
796 password = newpassword
798 if smartcard_required:
799 if password is not None and password is not '':
800 raise CommandError('It is not allowed to specify '
802 'together with --smartcard-required.')
803 if must_change_at_next_login:
804 raise CommandError('It is not allowed to specify '
805 '--must-change-at-next-login '
806 'together with --smartcard-required.')
807 if clear_smartcard_required:
808 raise CommandError('It is not allowed to specify '
809 '--clear-smartcard-required '
810 'together with --smartcard-required.')
812 if random_password and not smartcard_required:
813 password = generate_random_password(128, 255)
816 if smartcard_required:
818 if password is not None and password is not '':
820 password = getpass("New Password: ")
821 passwordverify = getpass("Retype Password: ")
822 if not password == passwordverify:
824 self.outf.write("Sorry, passwords do not match.\n")
827 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
829 lp = sambaopts.get_loadparm()
830 creds = credopts.get_credentials(lp)
832 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
834 samdb = SamDB(url=H, session_info=system_session(),
835 credentials=creds, lp=lp)
837 if smartcard_required:
840 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
841 flags = dsdb.UF_SMARTCARD_REQUIRED
842 samdb.toggle_userAccountFlags(filter, flags, on=True)
843 command = "Failed to enable account for user '%s'" % (username or filter)
844 samdb.enable_account(filter)
845 except Exception as msg:
846 # FIXME: catch more specific exception
847 raise CommandError("%s: %s" % (command, msg))
848 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
852 if clear_smartcard_required:
853 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
854 flags = dsdb.UF_SMARTCARD_REQUIRED
855 samdb.toggle_userAccountFlags(filter, flags, on=False)
856 command = "Failed to set password for user '%s'" % (username or filter)
857 samdb.setpassword(filter, password,
858 force_change_at_next_login=must_change_at_next_login,
860 except Exception as msg:
861 # FIXME: catch more specific exception
862 raise CommandError("%s: %s" % (command, msg))
863 self.outf.write("Changed password OK\n")
866 class GetPasswordCommand(Command):
869 super(GetPasswordCommand, self).__init__()
872 def connect_system_samdb(self, url, allow_local=False, verbose=False):
874 # using anonymous here, results in no authentication
875 # which means we can get system privileges via
876 # the privileged ldapi socket
877 creds = credentials.Credentials()
878 creds.set_anonymous()
880 if url is None and allow_local:
882 elif url.lower().startswith("ldapi://"):
884 elif url.lower().startswith("ldap://"):
885 raise CommandError("--url ldap:// is not supported for this command")
886 elif url.lower().startswith("ldaps://"):
887 raise CommandError("--url ldaps:// is not supported for this command")
888 elif not allow_local:
889 raise CommandError("--url requires an ldapi:// url for this command")
892 self.outf.write("Connecting to '%s'\n" % url)
894 samdb = SamDB(url=url, session_info=system_session(),
895 credentials=creds, lp=self.lp)
899 # Make sure we're connected as SYSTEM
901 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
903 sids = res[0].get("tokenGroups")
904 assert len(sids) == 1
905 sid = ndr_unpack(security.dom_sid, sids[0])
906 assert str(sid) == security.SID_NT_SYSTEM
907 except Exception as msg:
908 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
909 (security.SID_NT_SYSTEM))
911 # We use sort here in order to have a predictable processing order
912 # this might not be strictly needed, but also doesn't hurt here
913 for a in sorted(virtual_attributes.keys()):
914 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
915 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
919 def get_account_attributes(self, samdb, username, basedn, filter, scope,
926 (attr, _, opts) = a.partition(';')
928 attr_opts[attr] = opts
930 attr_opts[attr] = None
931 search_attrs.append(attr)
932 lower_attrs = [x.lower() for x in search_attrs]
934 require_supplementalCredentials = False
935 for a in virtual_attributes.keys():
936 if a.lower() in lower_attrs:
937 require_supplementalCredentials = True
938 add_supplementalCredentials = False
939 add_unicodePwd = False
940 if require_supplementalCredentials:
941 a = "supplementalCredentials"
942 if a.lower() not in lower_attrs:
944 add_supplementalCredentials = True
946 if a.lower() not in lower_attrs:
948 add_unicodePwd = True
949 add_sAMAcountName = False
951 if a.lower() not in lower_attrs:
953 add_sAMAcountName = True
955 add_userPrincipalName = False
956 upn = "usePrincipalName"
957 if upn.lower() not in lower_attrs:
958 search_attrs += [upn]
959 add_userPrincipalName = True
961 if scope == ldb.SCOPE_BASE:
962 search_controls = ["show_deleted:1", "show_recycled:1"]
966 res = samdb.search(base=basedn, expression=filter,
967 scope=scope, attrs=search_attrs,
968 controls=search_controls)
970 raise Exception('Unable to find user "%s"' % (username or filter))
972 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
973 except Exception as msg:
974 # FIXME: catch more specific exception
975 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
980 if "supplementalCredentials" in obj:
981 sc_blob = obj["supplementalCredentials"][0]
982 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
983 if add_supplementalCredentials:
984 del obj["supplementalCredentials"]
985 if "unicodePwd" in obj:
986 unicodePwd = obj["unicodePwd"][0]
988 del obj["unicodePwd"]
989 account_name = str(obj["sAMAccountName"][0])
990 if add_sAMAcountName:
991 del obj["sAMAccountName"]
992 if "userPrincipalName" in obj:
993 account_upn = str(obj["userPrincipalName"][0])
995 realm = self.lp.get("realm")
996 account_upn = "%s@%s" % (account_name, realm.lower())
997 if add_userPrincipalName:
998 del obj["userPrincipalName"]
1002 def get_package(name, min_idx=0):
1003 if name in calculated:
1004 return calculated[name]
1008 min_idx = len(sc.sub.packages) + min_idx
1010 for p in sc.sub.packages:
1017 return binascii.a2b_hex(p.data)
1022 # Samba adds 'Primary:SambaGPG' at the end.
1023 # When Windows sets the password it keeps
1024 # 'Primary:SambaGPG' and rotates it to
1025 # the begining. So we can only use the value,
1026 # if it is the last one.
1028 # In order to get more protection we verify
1029 # the nthash of the decrypted utf16 password
1030 # against the stored nthash in unicodePwd.
1032 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1033 if sgv is not None and unicodePwd is not None:
1034 ctx = gpgme.Context()
1036 cipher_io = io.BytesIO(sgv)
1037 plain_io = io.BytesIO()
1039 ctx.decrypt(cipher_io, plain_io)
1040 cv = plain_io.getvalue()
1042 # We only use the password if it matches
1043 # the current nthash stored in the unicodePwd
1046 tmp = credentials.Credentials()
1048 tmp.set_utf16_password(cv)
1049 nthash = tmp.get_nt_hash()
1050 if nthash == unicodePwd:
1051 calculated["Primary:CLEARTEXT"] = cv
1052 except gpgme.GpgmeError as e1:
1053 (major, minor, msg) = e1.args
1054 if major == gpgme.ERR_BAD_SECKEY:
1055 msg = "ERR_BAD_SECKEY: " + msg
1057 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1058 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1059 username or account_name, msg))
1061 def get_utf8(a, b, username):
1063 u = unicode(b, 'utf-16-le')
1064 except UnicodeDecodeError as e:
1065 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1068 u8 = u.encode('utf-8')
1071 # Extract the WDigest hash for the value specified by i.
1072 # Builds an htdigest compatible value
1075 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1076 domain, dns_domain):
1081 user = account_name.lower()
1082 realm = domain.lower()
1084 user = account_name.upper()
1085 realm = domain.upper()
1088 realm = domain.upper()
1091 realm = domain.lower()
1093 user = account_name.upper()
1094 realm = domain.lower()
1096 user = account_name.lower()
1097 realm = domain.upper()
1100 realm = dns_domain.lower()
1102 user = account_name.lower()
1103 realm = dns_domain.lower()
1105 user = account_name.upper()
1106 realm = dns_domain.upper()
1109 realm = dns_domain.upper()
1112 realm = dns_domain.lower()
1114 user = account_name.upper()
1115 realm = dns_domain.lower()
1117 user = account_name.lower()
1118 realm = dns_domain.upper()
1123 user = account_upn.lower()
1126 user = account_upn.upper()
1129 user = "%s\\%s" % (domain, account_name)
1132 user = "%s\\%s" % (domain.lower(), account_name.lower())
1135 user = "%s\\%s" % (domain.upper(), account_name.upper())
1141 user = account_name.lower()
1144 user = account_name.upper()
1150 user = account_upn.lower()
1153 user = account_upn.upper()
1156 user = "%s\\%s" % (domain, account_name)
1159 # Differs from spec, see tests
1160 user = "%s\\%s" % (domain.lower(), account_name.lower())
1163 # Differs from spec, see tests
1164 user = "%s\\%s" % (domain.upper(), account_name.upper())
1169 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1172 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
1173 return "%s:%s:%s" % (user, realm, get_string(digest))
1177 # get the value for a virtualCrypt attribute.
1178 # look for an exact match on algorithm and rounds in supplemental creds
1179 # if not found calculate using Primary:CLEARTEXT
1180 # if no Primary:CLEARTEXT return the first supplementalCredential
1181 # that matches the algorithm.
1182 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1185 b = get_package("Primary:userPassword")
1187 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1189 # No exact match on algorithm and number of rounds
1190 # try and calculate one from the Primary:CLEARTEXT
1191 b = get_package("Primary:CLEARTEXT")
1193 u8 = get_utf8(a, b, username or account_name)
1195 sv = get_crypt_value(str(algorithm), u8, rounds)
1197 # Unable to calculate a hash with the specified
1198 # number of rounds, fall back to the first hash using
1199 # the specified algorithm
1203 return "{CRYPT}" + sv
1205 def get_userPassword_hash(blob, algorithm, rounds):
1206 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1209 # Check that the NT hash has not been changed without updating
1210 # the user password hashes. This indicates that password has been
1211 # changed without updating the supplemental credentials.
1212 if unicodePwd != bytearray(up.current_nt_hash.hash):
1215 scheme_prefix = "$%d$" % algorithm
1216 prefix = scheme_prefix
1218 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1222 if (scheme_match is None and
1223 h.scheme == SCHEME and
1224 h.value.startswith(scheme_prefix)):
1225 scheme_match = h.value
1226 if h.scheme == SCHEME and h.value.startswith(prefix):
1227 return (h.value, scheme_match)
1229 # No match on the number of rounds, return the value of the
1230 # first matching scheme
1231 return (None, scheme_match)
1233 # We use sort here in order to have a predictable processing order
1234 for a in sorted(virtual_attributes.keys()):
1235 if not a.lower() in lower_attrs:
1238 if a == "virtualClearTextUTF8":
1239 b = get_package("Primary:CLEARTEXT")
1242 u8 = get_utf8(a, b, username or account_name)
1246 elif a == "virtualClearTextUTF16":
1247 v = get_package("Primary:CLEARTEXT")
1250 elif a == "virtualSSHA":
1251 b = get_package("Primary:CLEARTEXT")
1254 u8 = get_utf8(a, b, username or account_name)
1257 salt = get_random_bytes(4)
1261 bv = h.digest() + salt
1262 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
1263 elif a == "virtualCryptSHA256":
1264 rounds = get_rounds(attr_opts[a])
1265 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1269 elif a == "virtualCryptSHA512":
1270 rounds = get_rounds(attr_opts[a])
1271 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1275 elif a == "virtualSambaGPG":
1276 # Samba adds 'Primary:SambaGPG' at the end.
1277 # When Windows sets the password it keeps
1278 # 'Primary:SambaGPG' and rotates it to
1279 # the begining. So we can only use the value,
1280 # if it is the last one.
1281 v = get_package("Primary:SambaGPG", min_idx=-1)
1284 elif a.startswith("virtualWDigest"):
1285 primary_wdigest = get_package("Primary:WDigest")
1286 if primary_wdigest is None:
1288 x = a[len("virtualWDigest"):]
1293 domain = self.lp.get("workgroup")
1294 dns_domain = samdb.domain_dns_name()
1295 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1300 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1303 def parse_attributes(self, attributes):
1305 if attributes is None:
1306 raise CommandError("Please specify --attributes")
1307 attrs = attributes.split(',')
1310 pa = pa.lstrip().rstrip()
1311 for da in disabled_virtual_attributes.keys():
1312 if pa.lower() == da.lower():
1313 r = disabled_virtual_attributes[da]["reason"]
1314 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1316 for va in virtual_attributes.keys():
1317 if pa.lower() == va.lower():
1318 # Take the real name
1321 password_attrs += [pa]
1323 return password_attrs
1326 class cmd_user_getpassword(GetPasswordCommand):
1327 """Get the password fields of a user/computer account.
1329 This command gets the logon password for a user/computer account.
1331 The username specified on the command is the sAMAccountName.
1332 The username may also be specified using the --filter option.
1334 The command must be run from the root user id or another authorized user id.
1335 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1336 used to adjust the local path. By default tdb:// is used by default.
1338 The '--attributes' parameter takes a comma separated list of attributes,
1339 which will be printed or given to the script specified by '--script'. If a
1340 specified attribute is not available on an object it's silently omitted.
1341 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1342 the NTHASH) and the following virtual attributes are possible (see --help
1343 for which virtual attributes are supported in your environment):
1345 virtualClearTextUTF16: The raw cleartext as stored in the
1346 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1347 with '--decrypt-samba-gpg') buffer inside of the
1348 supplementalCredentials attribute. This typically
1349 contains valid UTF-16-LE, but may contain random
1350 bytes, e.g. for computer accounts.
1352 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1353 (only from valid UTF-16-LE)
1355 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1356 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1358 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1359 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1360 with a $5$... salt, see crypt(3) on modern systems.
1361 The number of rounds used to calculate the hash can
1362 also be specified. By appending ";rounds=x" to the
1363 attribute name i.e. virtualCryptSHA256;rounds=10000
1364 will calculate a SHA256 hash with 10,000 rounds.
1365 non numeric values for rounds are silently ignored
1366 The value is calculated as follows:
1367 1) If a value exists in 'Primary:userPassword' with
1368 the specified number of rounds it is returned.
1369 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1370 '--decrypt-samba-gpg'. Calculate a hash with
1371 the specified number of rounds
1372 3) Return the first CryptSHA256 value in
1373 'Primary:userPassword'
1376 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1377 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1378 with a $6$... salt, see crypt(3) on modern systems.
1379 The number of rounds used to calculate the hash can
1380 also be specified. By appending ";rounds=x" to the
1381 attribute name i.e. virtualCryptSHA512;rounds=10000
1382 will calculate a SHA512 hash with 10,000 rounds.
1383 non numeric values for rounds are silently ignored
1384 The value is calculated as follows:
1385 1) If a value exists in 'Primary:userPassword' with
1386 the specified number of rounds it is returned.
1387 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1388 '--decrypt-samba-gpg'. Calculate a hash with
1389 the specified number of rounds
1390 3) Return the first CryptSHA512 value in
1391 'Primary:userPassword'
1393 virtualWDigestNN: The individual hash values stored in
1394 'Primary:WDigest' where NN is the hash number in
1396 NOTE: As at 22-05-2017 the documentation:
1397 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1398 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1401 virtualSambaGPG: The raw cleartext as stored in the
1402 'Primary:SambaGPG' buffer inside of the
1403 supplementalCredentials attribute.
1404 See the 'password hash gpg key ids' option in
1407 The '--decrypt-samba-gpg' option triggers decryption of the
1408 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1409 in your environment or not (the python-gpgme package is required). Please
1410 note that you might need to set the GNUPGHOME environment variable. If the
1411 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1412 environment variable has been set correctly and the passphrase is already
1413 known by the gpg-agent.
1416 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1419 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1423 super(cmd_user_getpassword, self).__init__()
1425 synopsis = "%prog (<username>|--filter <filter>) [options]"
1427 takes_optiongroups = {
1428 "sambaopts": options.SambaOptions,
1429 "versionopts": options.VersionOptions,
1433 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1434 metavar="URL", dest="H"),
1435 Option("--filter", help="LDAP Filter to set password on", type=str),
1436 Option("--attributes", type=str,
1437 help=virtual_attributes_help,
1438 metavar="ATTRIBUTELIST", dest="attributes"),
1439 Option("--decrypt-samba-gpg",
1440 help=decrypt_samba_gpg_help,
1441 action="store_true", default=False, dest="decrypt_samba_gpg"),
1444 takes_args = ["username?"]
1446 def run(self, username=None, H=None, filter=None,
1447 attributes=None, decrypt_samba_gpg=None,
1448 sambaopts=None, versionopts=None):
1449 self.lp = sambaopts.get_loadparm()
1451 if decrypt_samba_gpg and not gpgme_support:
1452 raise CommandError(decrypt_samba_gpg_help)
1454 if filter is None and username is None:
1455 raise CommandError("Either the username or '--filter' must be specified!")
1458 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1460 if attributes is None:
1461 raise CommandError("Please specify --attributes")
1463 password_attrs = self.parse_attributes(attributes)
1465 samdb = self.connect_system_samdb(url=H, allow_local=True)
1467 obj = self.get_account_attributes(samdb, username,
1470 scope=ldb.SCOPE_SUBTREE,
1471 attrs=password_attrs,
1472 decrypt=decrypt_samba_gpg)
1474 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1475 self.outf.write("%s" % ldif)
1476 self.outf.write("Got password OK\n")
1479 class cmd_user_syncpasswords(GetPasswordCommand):
1480 """Sync the password of user accounts.
1482 This syncs logon passwords for user accounts.
1484 Note that this command should run on a single domain controller only
1485 (typically the PDC-emulator). However the "password hash gpg key ids"
1486 option should to be configured on all domain controllers.
1488 The command must be run from the root user id or another authorized user id.
1489 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1490 local path. By default, ldapi:// is used with the default path to the
1491 privileged ldapi socket.
1493 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1494 "Sync Loop Terminate".
1497 Cache Initialization
1498 ====================
1500 The first time, this command needs to be called with
1501 '--cache-ldb-initialize' in order to initialize its cache.
1503 The cache initialization requires '--attributes' and allows the following
1504 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1507 The '--attributes' parameter takes a comma separated list of attributes,
1508 which will be printed or given to the script specified by '--script'. If a
1509 specified attribute is not available on an object it will be silently omitted.
1510 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1511 the NTHASH) and the following virtual attributes are possible (see '--help'
1512 for supported virtual attributes in your environment):
1514 virtualClearTextUTF16: The raw cleartext as stored in the
1515 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1516 with '--decrypt-samba-gpg') buffer inside of the
1517 supplementalCredentials attribute. This typically
1518 contains valid UTF-16-LE, but may contain random
1519 bytes, e.g. for computer accounts.
1521 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1522 (only from valid UTF-16-LE)
1524 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1525 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1527 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1528 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1529 with a $5$... salt, see crypt(3) on modern systems.
1530 The number of rounds used to calculate the hash can
1531 also be specified. By appending ";rounds=x" to the
1532 attribute name i.e. virtualCryptSHA256;rounds=10000
1533 will calculate a SHA256 hash with 10,000 rounds.
1534 non numeric values for rounds are silently ignored
1535 The value is calculated as follows:
1536 1) If a value exists in 'Primary:userPassword' with
1537 the specified number of rounds it is returned.
1538 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1539 '--decrypt-samba-gpg'. Calculate a hash with
1540 the specified number of rounds
1541 3) Return the first CryptSHA256 value in
1542 'Primary:userPassword'
1544 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1545 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1546 with a $6$... salt, see crypt(3) on modern systems.
1547 The number of rounds used to calculate the hash can
1548 also be specified. By appending ";rounds=x" to the
1549 attribute name i.e. virtualCryptSHA512;rounds=10000
1550 will calculate a SHA512 hash with 10,000 rounds.
1551 non numeric values for rounds are silently ignored
1552 The value is calculated as follows:
1553 1) If a value exists in 'Primary:userPassword' with
1554 the specified number of rounds it is returned.
1555 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1556 '--decrypt-samba-gpg'. Calculate a hash with
1557 the specified number of rounds
1558 3) Return the first CryptSHA512 value in
1559 'Primary:userPassword'
1561 virtualWDigestNN: The individual hash values stored in
1562 'Primary:WDigest' where NN is the hash number in
1564 NOTE: As at 22-05-2017 the documentation:
1565 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1566 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1569 virtualSambaGPG: The raw cleartext as stored in the
1570 'Primary:SambaGPG' buffer inside of the
1571 supplementalCredentials attribute.
1572 See the 'password hash gpg key ids' option in
1575 The '--decrypt-samba-gpg' option triggers decryption of the
1576 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1577 in your environment or not (the python-gpgme package is required). Please
1578 note that you might need to set the GNUPGHOME environment variable. If the
1579 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1580 environment variable has been set correctly and the passphrase is already
1581 known by the gpg-agent.
1583 The '--script' option specifies a custom script that is called whenever any
1584 of the dirsyncAttributes (see below) was changed. The script is called
1585 without any arguments. It gets the LDIF for exactly one object on STDIN.
1586 If the script processed the object successfully it has to respond with a
1587 single line starting with 'DONE-EXIT: ' followed by an optional message.
1589 Note that the script might be called without any password change, e.g. if
1590 the account was disabled (a userAccountControl change) or the
1591 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1592 are always returned as unique identifier of the account. It might be useful
1593 to also ask for non-password attributes like: objectSid, sAMAccountName,
1594 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1595 Depending on the object, some attributes may not be present/available,
1596 but you always get the current state (and not a diff).
1598 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1601 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1602 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1603 (!(sAMAccountName=krbtgt*)))
1604 This means only normal (non-krbtgt) user
1605 accounts are monitored. The '--filter' can modify that, e.g. if it's
1606 required to also sync computer accounts.
1612 This (default) mode runs in an endless loop waiting for password related
1613 changes in the active directory database. It makes use of the
1614 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1615 get changes in a reliable fashion. Objects are monitored for changes of the
1616 following dirsyncAttributes:
1618 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1619 userPrincipalName and userAccountControl.
1621 It recovers from LDAP disconnects and updates the cache in conservative way
1622 (in single steps after each successfully processed change). An error from
1623 the script (specified by '--script') will result in fatal error and this
1624 command will exit. But the cache state should be still valid and can be
1625 resumed in the next "Sync Loop Run".
1627 The '--logfile' option specifies an optional (required if '--daemon' is
1628 specified) logfile that takes all output of the command. The logfile is
1629 automatically reopened if fstat returns st_nlink == 0.
1631 The optional '--daemon' option will put the command into the background.
1633 You can stop the command without the '--daemon' option, also by hitting
1636 If you specify the '--no-wait' option the command skips the
1637 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1638 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1643 In order to terminate an already running command (likely as daemon) the
1644 '--terminate' option can be used. This also requires the '--logfile' option
1649 samba-tool user syncpasswords --cache-ldb-initialize \\
1650 --attributes=virtualClearTextUTF8
1651 samba-tool user syncpasswords
1654 samba-tool user syncpasswords --cache-ldb-initialize \\
1655 --attributes=objectGUID,objectSID,sAMAccountName,\\
1656 userPrincipalName,userAccountControl,pwdLastSet,\\
1657 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1658 --script=/path/to/my-custom-syncpasswords-script.py
1659 samba-tool user syncpasswords --daemon \\
1660 --logfile=/var/log/samba/user-syncpasswords.log
1661 samba-tool user syncpasswords --terminate \\
1662 --logfile=/var/log/samba/user-syncpasswords.log
1666 super(cmd_user_syncpasswords, self).__init__()
1668 synopsis = "%prog [--cache-ldb-initialize] [options]"
1670 takes_optiongroups = {
1671 "sambaopts": options.SambaOptions,
1672 "versionopts": options.VersionOptions,
1676 Option("--cache-ldb-initialize",
1677 help="Initialize the cache for the first time",
1678 dest="cache_ldb_initialize", action="store_true"),
1679 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1680 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1681 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1682 metavar="URL", dest="H"),
1683 Option("--filter", help="optional LDAP filter to set password on", type=str,
1684 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1685 Option("--attributes", type=str,
1686 help=virtual_attributes_help,
1687 metavar="ATTRIBUTELIST", dest="attributes"),
1688 Option("--decrypt-samba-gpg",
1689 help=decrypt_samba_gpg_help,
1690 action="store_true", default=False, dest="decrypt_samba_gpg"),
1691 Option("--script", help="Script that is called for each password change", type=str,
1692 metavar="/path/to/syncpasswords.script", dest="script"),
1693 Option("--no-wait", help="Don't block waiting for changes",
1694 action="store_true", default=False, dest="nowait"),
1695 Option("--logfile", type=str,
1696 help="The logfile to use (required in --daemon mode).",
1697 metavar="/path/to/syncpasswords.log", dest="logfile"),
1698 Option("--daemon", help="daemonize after initial setup",
1699 action="store_true", default=False, dest="daemon"),
1700 Option("--terminate",
1701 help="Send a SIGTERM to an already running (daemon) process",
1702 action="store_true", default=False, dest="terminate"),
1705 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1706 H=None, filter=None,
1707 attributes=None, decrypt_samba_gpg=None,
1708 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1709 sambaopts=None, versionopts=None):
1711 self.lp = sambaopts.get_loadparm()
1713 self.samdb_url = None
1717 if not cache_ldb_initialize:
1718 if attributes is not None:
1719 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1720 if decrypt_samba_gpg:
1721 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1722 if script is not None:
1723 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1724 if filter is not None:
1725 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1727 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1729 if nowait is not False:
1730 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1731 if logfile is not None:
1732 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1733 if daemon is not False:
1734 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1735 if terminate is not False:
1736 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1740 raise CommandError("--daemon is not allowed together with --no-wait")
1741 if terminate is not False:
1742 raise CommandError("--terminate is not allowed together with --no-wait")
1744 if terminate is True and daemon is True:
1745 raise CommandError("--terminate is not allowed together with --daemon")
1747 if daemon is True and logfile is None:
1748 raise CommandError("--daemon is only allowed together with --logfile")
1750 if terminate is True and logfile is None:
1751 raise CommandError("--terminate is only allowed together with --logfile")
1753 if script is not None:
1754 if not os.path.exists(script):
1755 raise CommandError("script[%s] does not exist!" % script)
1757 sync_command = "%s" % os.path.abspath(script)
1761 dirsync_filter = filter
1762 if dirsync_filter is None:
1763 dirsync_filter = "(&" + \
1764 "(objectClass=user)" + \
1765 "(userAccountControl:%s:=%u)" % (
1766 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1767 "(!(sAMAccountName=krbtgt*))" + \
1770 dirsync_secret_attrs = [
1773 "supplementalCredentials",
1776 dirsync_attrs = dirsync_secret_attrs + [
1779 "userPrincipalName",
1780 "userAccountControl",
1785 password_attrs = None
1787 if cache_ldb_initialize:
1789 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1791 if decrypt_samba_gpg and not gpgme_support:
1792 raise CommandError(decrypt_samba_gpg_help)
1794 password_attrs = self.parse_attributes(attributes)
1795 lower_attrs = [x.lower() for x in password_attrs]
1796 # We always return these in order to track deletions
1797 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1798 if a.lower() not in lower_attrs:
1799 password_attrs += [a]
1801 if cache_ldb is not None:
1802 if cache_ldb.lower().startswith("ldapi://"):
1803 raise CommandError("--cache_ldb ldapi:// is not supported")
1804 elif cache_ldb.lower().startswith("ldap://"):
1805 raise CommandError("--cache_ldb ldap:// is not supported")
1806 elif cache_ldb.lower().startswith("ldaps://"):
1807 raise CommandError("--cache_ldb ldaps:// is not supported")
1808 elif cache_ldb.lower().startswith("tdb://"):
1811 if not os.path.exists(cache_ldb):
1812 cache_ldb = self.lp.private_path(cache_ldb)
1814 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1816 self.lockfile = "%s.pid" % cache_ldb
1819 if self.logfile is not None:
1821 if info.st_nlink == 0:
1822 logfile = self.logfile
1824 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1825 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
1830 log_msg("Reopened logfile[%s]\n" % (logfile))
1831 self.logfile = logfile
1832 msg = "%s: pid[%d]: %s" % (
1836 self.outf.write(msg)
1845 "passwordAttribute",
1851 self.cache = Ldb(cache_ldb)
1852 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1853 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1857 self.samdb_url = res[0]["samdbUrl"][0]
1858 except KeyError as e:
1859 self.samdb_url = None
1861 self.samdb_url = None
1862 if self.samdb_url is None and not cache_ldb_initialize:
1863 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1865 if self.samdb_url is not None and cache_ldb_initialize:
1866 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1868 if self.samdb_url is None:
1870 self.dirsync_filter = dirsync_filter
1871 self.dirsync_attrs = dirsync_attrs
1872 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"]
1873 self.password_attrs = password_attrs
1874 self.decrypt_samba_gpg = decrypt_samba_gpg
1875 self.sync_command = sync_command
1876 add_ldif = "dn: %s\n" % self.cache_dn
1877 add_ldif += "objectClass: userSyncPasswords\n"
1878 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(get_bytes(self.samdb_url)).decode('utf8')
1879 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(get_bytes(self.dirsync_filter)).decode('utf8')
1880 for a in self.dirsync_attrs:
1881 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8')
1882 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1883 for a in self.password_attrs:
1884 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8')
1885 if self.decrypt_samba_gpg:
1886 add_ldif += "decryptSambaGPG: TRUE\n"
1888 add_ldif += "decryptSambaGPG: FALSE\n"
1889 if self.sync_command is not None:
1890 add_ldif += "syncCommand: %s\n" % self.sync_command
1891 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1892 self.cache.add_ldif(add_ldif)
1893 self.current_pid = None
1894 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1895 msgs = self.cache.parse_ldif(add_ldif)
1896 changetype, msg = next(msgs)
1897 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1898 self.outf.write("%s" % ldif)
1900 self.dirsync_filter = str(res[0]["dirsyncFilter"][0])
1901 self.dirsync_attrs = []
1902 for a in res[0]["dirsyncAttribute"]:
1903 self.dirsync_attrs.append(str(a))
1904 self.dirsync_controls = [str(res[0]["dirsyncControl"][0]), "extended_dn:1:0"]
1905 self.password_attrs = []
1906 for a in res[0]["passwordAttribute"]:
1907 self.password_attrs.append(str(a))
1908 decrypt_string = str(res[0]["decryptSambaGPG"][0])
1909 assert(decrypt_string in ["TRUE", "FALSE"])
1910 if decrypt_string == "TRUE":
1911 self.decrypt_samba_gpg = True
1913 self.decrypt_samba_gpg = False
1914 if "syncCommand" in res[0]:
1915 self.sync_command = str(res[0]["syncCommand"][0])
1917 self.sync_command = None
1918 if "currentPid" in res[0]:
1919 self.current_pid = int(res[0]["currentPid"][0])
1921 self.current_pid = None
1922 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1926 def run_sync_command(dn, ldif):
1927 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1928 sync_command_p = Popen(self.sync_command,
1933 res = sync_command_p.poll()
1936 input = "%s" % (ldif)
1937 reply = sync_command_p.communicate(input)[0]
1938 log_msg("%s\n" % (reply))
1939 res = sync_command_p.poll()
1941 sync_command_p.terminate()
1942 res = sync_command_p.wait()
1944 if reply.startswith("DONE-EXIT: "):
1947 log_msg("RESULT: %s\n" % (res))
1948 raise Exception("ERROR: %s - %s\n" % (res, reply))
1950 def handle_object(idx, dirsync_obj):
1951 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1952 guid = ndr_unpack(misc.GUID, binary_guid)
1953 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1954 sid = ndr_unpack(security.dom_sid, binary_sid)
1955 domain_sid, rid = sid.split()
1956 if rid == security.DOMAIN_RID_KRBTGT:
1957 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1959 for a in list(dirsync_obj.keys()):
1960 for h in dirsync_secret_attrs:
1961 if a.lower() == h.lower():
1963 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1964 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1965 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
1966 obj = self.get_account_attributes(self.samdb,
1967 username="%s" % sid,
1968 basedn="<GUID=%s>" % guid,
1969 filter="(objectClass=user)",
1970 scope=ldb.SCOPE_BASE,
1971 attrs=self.password_attrs,
1972 decrypt=self.decrypt_samba_gpg)
1973 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1974 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1975 if self.sync_command is None:
1976 self.outf.write("%s" % (ldif))
1978 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1979 run_sync_command(obj.dn, ldif)
1981 def check_current_pid_conflict(terminate):
1987 self.lockfd = os.open(self.lockfile, flags, 0o600)
1988 except IOError as e4:
1989 (err, msg) = e4.args
1990 if err == errno.ENOENT:
1993 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1994 (self.lockfile, msg, err))
1997 got_exclusive = False
1999 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2000 got_exclusive = True
2001 except IOError as e5:
2002 (err, msg) = e5.args
2003 if err != errno.EACCES and err != errno.EAGAIN:
2004 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
2005 (self.lockfile, msg, err))
2008 if not got_exclusive:
2009 buf = os.read(self.lockfd, 64)
2010 self.current_pid = None
2012 self.current_pid = int(buf)
2013 except ValueError as e:
2015 if self.current_pid is not None:
2018 if got_exclusive and terminate:
2020 os.ftruncate(self.lockfd, 0)
2021 except IOError as e2:
2022 (err, msg) = e2.args
2023 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2024 (self.lockfile, msg, err))
2026 os.close(self.lockfd)
2031 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2032 except IOError as e6:
2033 (err, msg) = e6.args
2034 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2035 (self.lockfile, msg, err))
2037 # We leave the function with the shared lock.
2040 def update_pid(pid):
2041 if self.lockfd != -1:
2042 got_exclusive = False
2043 # Try 5 times to get the exclusiv lock.
2044 for i in range(0, 5):
2046 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2047 got_exclusive = True
2048 except IOError as e:
2050 if err != errno.EACCES and err != errno.EAGAIN:
2051 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2052 (pid, self.lockfile, msg, err))
2057 if not got_exclusive:
2058 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
2059 (pid, self.lockfile))
2060 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2061 (pid, self.lockfile))
2068 os.ftruncate(self.lockfd, 0)
2070 os.write(self.lockfd, buf)
2071 except IOError as e3:
2072 (err, msg) = e3.args
2073 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2074 (self.lockfile, msg, err))
2076 self.current_pid = pid
2077 if self.current_pid is not None:
2078 log_msg("currentPid: %d\n" % self.current_pid)
2080 modify_ldif = "dn: %s\n" % (self.cache_dn)
2081 modify_ldif += "changetype: modify\n"
2082 modify_ldif += "replace: currentPid\n"
2083 if self.current_pid is not None:
2084 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2085 modify_ldif += "replace: currentTime\n"
2086 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2087 self.cache.modify_ldif(modify_ldif)
2090 def update_cache(res_controls):
2091 assert len(res_controls) > 0
2092 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2093 res_controls[0].critical = True
2094 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2095 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2097 modify_ldif = "dn: %s\n" % (self.cache_dn)
2098 modify_ldif += "changetype: modify\n"
2099 modify_ldif += "replace: dirsyncControl\n"
2100 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2101 modify_ldif += "replace: currentTime\n"
2102 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2103 self.cache.modify_ldif(modify_ldif)
2106 def check_object(dirsync_obj, res_controls):
2107 assert len(res_controls) > 0
2108 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2110 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2111 sid = ndr_unpack(security.dom_sid, binary_sid)
2113 lastCookie = str(res_controls[0])
2115 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2116 expression="(lastCookie=%s)" % (
2117 ldb.binary_encode(lastCookie)),
2123 def update_object(dirsync_obj, res_controls):
2124 assert len(res_controls) > 0
2125 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2127 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2128 sid = ndr_unpack(security.dom_sid, binary_sid)
2130 lastCookie = str(res_controls[0])
2132 self.cache.transaction_start()
2134 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2135 expression="(objectClass=*)",
2136 attrs=["lastCookie"])
2138 add_ldif = "dn: %s\n" % (dn)
2139 add_ldif += "objectClass: userCookie\n"
2140 add_ldif += "lastCookie: %s\n" % (lastCookie)
2141 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2142 self.cache.add_ldif(add_ldif)
2144 modify_ldif = "dn: %s\n" % (dn)
2145 modify_ldif += "changetype: modify\n"
2146 modify_ldif += "replace: lastCookie\n"
2147 modify_ldif += "lastCookie: %s\n" % (lastCookie)
2148 modify_ldif += "replace: currentTime\n"
2149 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2150 self.cache.modify_ldif(modify_ldif)
2151 self.cache.transaction_commit()
2152 except Exception as e:
2153 self.cache.transaction_cancel()
2159 res = self.samdb.search(expression=str(self.dirsync_filter),
2160 scope=ldb.SCOPE_SUBTREE,
2161 attrs=self.dirsync_attrs,
2162 controls=self.dirsync_controls)
2163 log_msg("dirsync_loop(): results %d\n" % len(res))
2166 done = check_object(r, res.controls)
2168 handle_object(ri, r)
2169 update_object(r, res.controls)
2171 update_cache(res.controls)
2175 def sync_loop(wait):
2176 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2177 notify_controls = ["notification:1", "show_recycled:1"]
2178 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2179 scope=ldb.SCOPE_SUBTREE,
2181 controls=notify_controls,
2185 log_msg("Resuming monitoring\n")
2187 log_msg("Getting changes\n")
2188 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2189 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2190 self.outf.write("syncCommand: %s\n" % self.sync_command)
2193 if wait is not True:
2196 for msg in notify_handle:
2197 if not isinstance(msg, ldb.Message):
2198 self.outf.write("referal: %s\n" % msg)
2200 created = msg.get("uSNCreated")[0]
2201 changed = msg.get("uSNChanged")[0]
2202 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2203 (msg.dn, created, changed))
2207 res = notify_handle.result()
2212 orig_pid = os.getpid()
2217 if pid == 0: # Actual daemon
2219 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2224 if cache_ldb_initialize:
2226 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2231 if logfile is not None:
2232 import resource # Resource usage information.
2233 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2234 if maxfd == resource.RLIM_INFINITY:
2235 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2236 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2237 self.outf.write("Using logfile[%s]\n" % logfile)
2238 for fd in range(0, maxfd):
2249 log_msg("Attached to logfile[%s]\n" % (logfile))
2250 self.logfile = logfile
2253 conflict = check_current_pid_conflict(terminate)
2255 if self.current_pid is None:
2256 log_msg("No process running.\n")
2259 log_msg("Proccess %d is not running anymore.\n" % (
2263 log_msg("Sending SIGTERM to proccess %d.\n" % (
2265 os.kill(self.current_pid, signal.SIGTERM)
2268 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2269 os.getpid(), self.current_pid))
2273 update_pid(os.getpid())
2278 retry_sleep_max = 600
2283 retry_sleep = retry_sleep_min
2285 while self.samdb is None:
2286 if retry_sleep != 0:
2287 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2288 time.sleep(retry_sleep)
2289 retry_sleep = retry_sleep * 2
2290 if retry_sleep >= retry_sleep_max:
2291 retry_sleep = retry_sleep_max
2292 log_msg("Connecting to '%s'\n" % self.samdb_url)
2294 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2295 except Exception as msg:
2297 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2298 if wait is not True:
2303 except ldb.LdbError as e7:
2304 (enum, estr) = e7.args
2306 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2312 class cmd_user_edit(Command):
2313 """Modify User AD object.
2315 This command will allow editing of a user account in the Active Directory
2316 domain. You will then be able to add or change attributes and their values.
2318 The username specified on the command is the sAMAccountName.
2320 The command may be run from the root userid or another authorized userid.
2322 The -H or --URL= option can be used to execute the command against a remote
2326 samba-tool user edit User1 -H ldap://samba.samdom.example.com \
2327 -U administrator --password=passw1rd
2329 Example1 shows how to edit a users attributes in the domain against a remote
2332 The -H parameter is used to specify the remote target server.
2335 samba-tool user edit User2
2337 Example2 shows how to edit a users attributes in the domain against a local
2341 samba-tool user edit User3 --editor=nano
2343 Example3 shows how to edit a users attributes in the domain against a local
2344 LDAP server using the 'nano' editor.
2347 synopsis = "%prog <username> [options]"
2350 Option("-H", "--URL", help="LDB URL for database or target server",
2351 type=str, metavar="URL", dest="H"),
2352 Option("--editor", help="Editor to use instead of the system default,"
2353 " or 'vi' if no system default is set.", type=str),
2356 takes_args = ["username"]
2357 takes_optiongroups = {
2358 "sambaopts": options.SambaOptions,
2359 "credopts": options.CredentialsOptions,
2360 "versionopts": options.VersionOptions,
2363 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2364 H=None, editor=None):
2366 lp = sambaopts.get_loadparm()
2367 creds = credopts.get_credentials(lp, fallback_machine=True)
2368 samdb = SamDB(url=H, session_info=system_session(),
2369 credentials=creds, lp=lp)
2371 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2372 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2374 domaindn = samdb.domain_dn()
2377 res = samdb.search(base=domaindn,
2379 scope=ldb.SCOPE_SUBTREE)
2382 raise CommandError('Unable to find user "%s"' % (username))
2385 r_ldif = samdb.write_ldif(msg, 1)
2386 # remove 'changetype' line
2387 result_ldif = re.sub('changetype: add\n', '', r_ldif)
2390 editor = os.environ.get('EDITOR')
2394 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2395 t_file.write(result_ldif)
2398 check_call([editor, t_file.name])
2399 except CalledProcessError as e:
2400 raise CalledProcessError("ERROR: ", e)
2401 with open(t_file.name) as edited_file:
2402 edited_message = edited_file.read()
2404 if result_ldif != edited_message:
2405 diff = difflib.ndiff(result_ldif.splitlines(),
2406 edited_message.splitlines())
2410 if line.startswith('-'):
2412 minus_lines.append(line)
2413 elif line.startswith('+'):
2415 plus_lines.append(line)
2417 user_ldif = "dn: %s\n" % user_dn
2418 user_ldif += "changetype: modify\n"
2420 for line in minus_lines:
2421 attr, val = line.split(':', 1)
2422 search_attr = "%s:" % attr
2423 if not re.search(r'^' + search_attr, str(plus_lines)):
2424 user_ldif += "delete: %s\n" % attr
2425 user_ldif += "%s: %s\n" % (attr, val)
2427 for line in plus_lines:
2428 attr, val = line.split(':', 1)
2429 search_attr = "%s:" % attr
2430 if re.search(r'^' + search_attr, str(minus_lines)):
2431 user_ldif += "replace: %s\n" % attr
2432 user_ldif += "%s: %s\n" % (attr, val)
2433 if not re.search(r'^' + search_attr, str(minus_lines)):
2434 user_ldif += "add: %s\n" % attr
2435 user_ldif += "%s: %s\n" % (attr, val)
2438 samdb.modify_ldif(user_ldif)
2439 except Exception as e:
2440 raise CommandError("Failed to modify user '%s': " %
2443 self.outf.write("Modified User '%s' successfully\n" % username)
2446 class cmd_user_show(Command):
2447 """Display a user AD object.
2449 This command displays a user account and it's attributes in the Active
2451 The username specified on the command is the sAMAccountName.
2453 The command may be run from the root userid or another authorized userid.
2455 The -H or --URL= option can be used to execute the command against a remote
2459 samba-tool user show User1 -H ldap://samba.samdom.example.com \
2460 -U administrator --password=passw1rd
2462 Example1 shows how to display a users attributes in the domain against a remote
2465 The -H parameter is used to specify the remote target server.
2468 samba-tool user show User2
2470 Example2 shows how to display a users attributes in the domain against a local
2474 samba-tool user show User2 --attributes=objectSid,memberOf
2476 Example3 shows how to display a users objectSid and memberOf attributes.
2478 synopsis = "%prog <username> [options]"
2481 Option("-H", "--URL", help="LDB URL for database or target server",
2482 type=str, metavar="URL", dest="H"),
2483 Option("--attributes",
2484 help=("Comma separated list of attributes, "
2485 "which will be printed."),
2486 type=str, dest="user_attrs"),
2489 takes_args = ["username"]
2490 takes_optiongroups = {
2491 "sambaopts": options.SambaOptions,
2492 "credopts": options.CredentialsOptions,
2493 "versionopts": options.VersionOptions,
2496 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2497 H=None, user_attrs=None):
2499 lp = sambaopts.get_loadparm()
2500 creds = credopts.get_credentials(lp, fallback_machine=True)
2501 samdb = SamDB(url=H, session_info=system_session(),
2502 credentials=creds, lp=lp)
2506 attrs = user_attrs.split(",")
2508 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2509 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2511 domaindn = samdb.domain_dn()
2514 res = samdb.search(base=domaindn, expression=filter,
2515 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2518 raise CommandError('Unable to find user "%s"' % (username))
2521 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2522 self.outf.write(user_ldif)
2525 class cmd_user_move(Command):
2526 """Move a user to an organizational unit/container.
2528 This command moves a user account into the specified organizational unit
2530 The username specified on the command is the sAMAccountName.
2531 The name of the organizational unit or container can be specified as a
2532 full DN or without the domainDN component.
2534 The command may be run from the root userid or another authorized userid.
2536 The -H or --URL= option can be used to execute the command against a remote
2540 samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
2541 -H ldap://samba.samdom.example.com -U administrator
2543 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2544 unit on a remote LDAP server.
2546 The -H parameter is used to specify the remote target server.
2549 samba-tool user move User1 CN=Users
2551 Example2 shows how to move a user User1 back into the CN=Users container
2552 on the local server.
2555 synopsis = "%prog <username> <new_parent_dn> [options]"
2558 Option("-H", "--URL", help="LDB URL for database or target server",
2559 type=str, metavar="URL", dest="H"),
2562 takes_args = ["username", "new_parent_dn"]
2563 takes_optiongroups = {
2564 "sambaopts": options.SambaOptions,
2565 "credopts": options.CredentialsOptions,
2566 "versionopts": options.VersionOptions,
2569 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2570 versionopts=None, H=None):
2571 lp = sambaopts.get_loadparm()
2572 creds = credopts.get_credentials(lp, fallback_machine=True)
2573 samdb = SamDB(url=H, session_info=system_session(),
2574 credentials=creds, lp=lp)
2575 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2577 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2578 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2580 res = samdb.search(base=domain_dn,
2582 scope=ldb.SCOPE_SUBTREE)
2585 raise CommandError('Unable to find user "%s"' % (username))
2588 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2589 except Exception as e:
2590 raise CommandError('Invalid new_parent_dn "%s": %s' %
2591 (new_parent_dn, e.message))
2593 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2594 full_new_user_dn.remove_base_components(len(user_dn) - 1)
2595 full_new_user_dn.add_base(full_new_parent_dn)
2598 samdb.rename(user_dn, full_new_user_dn)
2599 except Exception as e:
2600 raise CommandError('Failed to move user "%s"' % username, e)
2601 self.outf.write('Moved user "%s" into "%s"\n' %
2602 (username, full_new_parent_dn))
2605 class cmd_user(SuperCommand):
2606 """User management."""
2609 subcommands["add"] = cmd_user_add()
2610 subcommands["create"] = cmd_user_create()
2611 subcommands["delete"] = cmd_user_delete()
2612 subcommands["disable"] = cmd_user_disable()
2613 subcommands["enable"] = cmd_user_enable()
2614 subcommands["list"] = cmd_user_list()
2615 subcommands["setexpiry"] = cmd_user_setexpiry()
2616 subcommands["password"] = cmd_user_password()
2617 subcommands["setpassword"] = cmd_user_setpassword()
2618 subcommands["getpassword"] = cmd_user_getpassword()
2619 subcommands["syncpasswords"] = cmd_user_syncpasswords()
2620 subcommands["edit"] = cmd_user_edit()
2621 subcommands["show"] = cmd_user_show()
2622 subcommands["move"] = cmd_user_move()