3 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
4 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import samba.getopt as options
34 from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError
35 from getpass import getpass
36 from samba.auth import system_session
37 from samba.samdb import SamDB
38 from samba.dcerpc import misc
39 from samba.dcerpc import security
40 from samba.dcerpc import drsblobs
41 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
46 generate_random_password,
49 from samba.net import Net
51 from samba.netcmd import (
57 from samba.compat import text_type
63 decrypt_samba_gpg_help = "Decrypt the SambaGPG password as cleartext source"
64 except ImportError as e:
66 decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
67 "python-gpgme required"
69 disabled_virtual_attributes = {
72 virtual_attributes = {
73 "virtualClearTextUTF8": {
74 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
76 "virtualClearTextUTF16": {
77 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
80 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
84 get_random_bytes_fn = None
85 if get_random_bytes_fn is None:
88 get_random_bytes_fn = Crypto.Random.get_random_bytes
89 except ImportError as e:
91 if get_random_bytes_fn is None:
94 get_random_bytes_fn = M2Crypto.Rand.rand_bytes
95 except ImportError as e:
99 if get_random_bytes_fn is not None:
101 return "Crypto.Random or M2Crypto.Rand required"
103 def get_random_bytes(num):
104 random_reason = check_random()
105 if random_reason is not None:
106 raise ImportError(random_reason)
107 return get_random_bytes_fn(num)
109 def get_crypt_value(alg, utf8pw, rounds=0):
115 salt = get_random_bytes(16)
116 # The salt needs to be in [A-Za-z0-9./]
117 # base64 is close enough and as we had 16
118 # random bytes but only need 16 characters
119 # we can ignore the possible == at the end
120 # of the base64 string
121 # we just need to replace '+' by '.'
122 b64salt = base64.b64encode(salt)[0:16].replace(b'+', b'.').decode('utf8')
125 crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
127 crypt_salt = "$%s$%s$" % (alg, b64salt)
129 crypt_value = crypt.crypt(utf8pw, crypt_salt)
130 if crypt_value is None:
131 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
132 expected_len = len(crypt_salt) + algs[alg]["length"]
133 if len(crypt_value) != expected_len:
134 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
135 crypt_salt, len(crypt_value), expected_len))
138 # Extract the rounds value from the options of a virtualCrypt attribute
139 # i.e. options = "rounds=20;other=ignored;" will return 20
140 # if the rounds option is not found or the value is not a number, 0 is returned
141 # which indicates that the default number of rounds should be used.
142 def get_rounds(options):
146 opts = options.split(';')
148 if o.lower().startswith("rounds="):
149 (key, _, val) = o.partition('=')
157 random_reason = check_random()
158 if random_reason is not None:
159 raise ImportError(random_reason)
163 virtual_attributes["virtualSSHA"] = {
165 except ImportError as e:
166 reason = "hashlib.sha1()"
168 reason += " and " + random_reason
169 reason += " required"
170 disabled_virtual_attributes["virtualSSHA"] = {
174 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
176 random_reason = check_random()
177 if random_reason is not None:
178 raise ImportError(random_reason)
180 v = get_crypt_value(alg, "")
182 virtual_attributes[attr] = {
184 except ImportError as e:
187 reason += " and " + random_reason
188 reason += " required"
189 disabled_virtual_attributes[attr] = {
192 except NotImplementedError as e:
193 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
194 disabled_virtual_attributes[attr] = {
198 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
199 for x in range(1, 30):
200 virtual_attributes["virtualWDigest%02d" % x] = {}
202 virtual_attributes_help = "The attributes to display (comma separated). "
203 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
204 if len(disabled_virtual_attributes) != 0:
205 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
207 class cmd_user_create(Command):
208 """Create a new user.
210 This command creates a new user account in the Active Directory domain. The username specified on the command is the sAMaccountName.
212 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).
214 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.
216 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.
218 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.
221 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
223 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.
226 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
228 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.
231 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
233 Example3 shows how to create a new user in the OrgUnit organizational unit.
236 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
238 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'.
241 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
242 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
244 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
245 --nis-domain is set, then the other four parameters are mandatory.
248 synopsis = "%prog <username> [<password>] [options]"
251 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
252 metavar="URL", dest="H"),
253 Option("--must-change-at-next-login",
254 help="Force password to be changed on next login",
255 action="store_true"),
256 Option("--random-password",
257 help="Generate random password",
258 action="store_true"),
259 Option("--smartcard-required",
260 help="Require a smartcard for interactive logons",
261 action="store_true"),
262 Option("--use-username-as-cn",
263 help="Force use of username as user's CN",
264 action="store_true"),
266 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>'",
268 Option("--surname", help="User's surname", type=str),
269 Option("--given-name", help="User's given name", type=str),
270 Option("--initials", help="User's initials", type=str),
271 Option("--profile-path", help="User's profile path", type=str),
272 Option("--script-path", help="User's logon script path", type=str),
273 Option("--home-drive", help="User's home drive letter", type=str),
274 Option("--home-directory", help="User's home directory path", type=str),
275 Option("--job-title", help="User's job title", type=str),
276 Option("--department", help="User's department", type=str),
277 Option("--company", help="User's company", type=str),
278 Option("--description", help="User's description", type=str),
279 Option("--mail-address", help="User's email address", type=str),
280 Option("--internet-address", help="User's home page", type=str),
281 Option("--telephone-number", help="User's phone number", type=str),
282 Option("--physical-delivery-office", help="User's office location", type=str),
283 Option("--rfc2307-from-nss",
284 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
285 action="store_true"),
286 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
287 Option("--unix-home", help="User's Unix/RFC2307 home directory",
289 Option("--uid", help="User's Unix/RFC2307 username", type=str),
290 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
291 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
292 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
293 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
296 takes_args = ["username", "password?"]
298 takes_optiongroups = {
299 "sambaopts": options.SambaOptions,
300 "credopts": options.CredentialsOptions,
301 "versionopts": options.VersionOptions,
304 def run(self, username, password=None, credopts=None, sambaopts=None,
305 versionopts=None, H=None, must_change_at_next_login=False,
306 random_password=False, use_username_as_cn=False, userou=None,
307 surname=None, given_name=None, initials=None, profile_path=None,
308 script_path=None, home_drive=None, home_directory=None,
309 job_title=None, department=None, company=None, description=None,
310 mail_address=None, internet_address=None, telephone_number=None,
311 physical_delivery_office=None, rfc2307_from_nss=False,
312 nis_domain=None, unix_home=None, uid=None, uid_number=None,
313 gid_number=None, gecos=None, login_shell=None,
314 smartcard_required=False):
316 if smartcard_required:
317 if password is not None and password is not '':
318 raise CommandError('It is not allowed to specify '
320 'together with --smartcard-required.')
321 if must_change_at_next_login:
322 raise CommandError('It is not allowed to specify '
323 '--must-change-at-next-login '
324 'together with --smartcard-required.')
326 if random_password and not smartcard_required:
327 password = generate_random_password(128, 255)
330 if smartcard_required:
332 if password is not None and password is not '':
334 password = getpass("New Password: ")
335 passwordverify = getpass("Retype Password: ")
336 if not password == passwordverify:
338 self.outf.write("Sorry, passwords do not match.\n")
341 pwent = pwd.getpwnam(username)
344 if uid_number is None:
345 uid_number = pwent[2]
346 if gid_number is None:
347 gid_number = pwent[3]
350 if login_shell is None:
351 login_shell = pwent[6]
353 lp = sambaopts.get_loadparm()
354 creds = credopts.get_credentials(lp)
356 if uid_number or gid_number:
357 if not lp.get("idmap_ldb:use rfc2307"):
358 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")
360 if nis_domain is not None:
361 if None in (uid_number, login_shell, unix_home, gid_number):
362 raise CommandError('Missing parameters. To enable NIS features, '
363 'the following options have to be given: '
364 '--nis-domain=, --uidNumber=, --login-shell='
365 ', --unix-home=, --gid-number= Operation '
369 samdb = SamDB(url=H, session_info=system_session(),
370 credentials=creds, lp=lp)
371 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
372 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
373 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
374 jobtitle=job_title, department=department, company=company, description=description,
375 mailaddress=mail_address, internetaddress=internet_address,
376 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
377 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
378 uidnumber=uid_number, gidnumber=gid_number,
379 gecos=gecos, loginshell=login_shell,
380 smartcard_required=smartcard_required)
381 except Exception as e:
382 raise CommandError("Failed to add user '%s': " % username, e)
384 self.outf.write("User '%s' created successfully\n" % username)
387 class cmd_user_add(cmd_user_create):
388 __doc__ = cmd_user_create.__doc__
389 # take this print out after the add subcommand is removed.
390 # the add subcommand is deprecated but left in for now to allow people to
393 def run(self, *args, **kwargs):
395 "Note: samba-tool user add is deprecated. "
396 "Please use samba-tool user create for the same function.\n")
397 return super(cmd_user_add, self).run(*args, **kwargs)
400 class cmd_user_delete(Command):
403 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
405 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.
407 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.
410 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
412 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.
415 sudo samba-tool user delete User2
417 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.
420 synopsis = "%prog <username> [options]"
423 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
424 metavar="URL", dest="H"),
427 takes_args = ["username"]
428 takes_optiongroups = {
429 "sambaopts": options.SambaOptions,
430 "credopts": options.CredentialsOptions,
431 "versionopts": options.VersionOptions,
434 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
436 lp = sambaopts.get_loadparm()
437 creds = credopts.get_credentials(lp, fallback_machine=True)
439 samdb = SamDB(url=H, session_info=system_session(),
440 credentials=creds, lp=lp)
442 filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
443 ldb.binary_encode(username))
446 res = samdb.search(base=samdb.domain_dn(),
447 scope=ldb.SCOPE_SUBTREE,
452 raise CommandError('Unable to find user "%s"' % (username))
455 samdb.delete(user_dn)
456 except Exception as e:
457 raise CommandError('Failed to remove user "%s"' % username, e)
458 self.outf.write("Deleted user %s\n" % username)
461 class cmd_user_list(Command):
462 """List all users."""
464 synopsis = "%prog [options]"
467 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
468 metavar="URL", dest="H"),
471 takes_optiongroups = {
472 "sambaopts": options.SambaOptions,
473 "credopts": options.CredentialsOptions,
474 "versionopts": options.VersionOptions,
477 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
478 lp = sambaopts.get_loadparm()
479 creds = credopts.get_credentials(lp, fallback_machine=True)
481 samdb = SamDB(url=H, session_info=system_session(),
482 credentials=creds, lp=lp)
484 domain_dn = samdb.domain_dn()
485 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
486 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
487 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
488 attrs=["samaccountname"])
493 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
496 class cmd_user_enable(Command):
499 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.
501 There are many reasons why an account may become disabled. These include:
502 - If a user exceeds the account policy for logon attempts
503 - If an administrator disables the account
504 - If the account expires
506 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
508 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.
510 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.
513 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
515 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.
518 su samba-tool user enable Testuser2
520 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.
523 samba-tool user enable --filter=samaccountname=Testuser3
525 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
528 synopsis = "%prog (<username>|--filter <filter>) [options]"
531 takes_optiongroups = {
532 "sambaopts": options.SambaOptions,
533 "versionopts": options.VersionOptions,
534 "credopts": options.CredentialsOptions,
538 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
539 metavar="URL", dest="H"),
540 Option("--filter", help="LDAP Filter to set password on", type=str),
543 takes_args = ["username?"]
545 def run(self, username=None, sambaopts=None, credopts=None,
546 versionopts=None, filter=None, H=None):
547 if username is None and filter is None:
548 raise CommandError("Either the username or '--filter' must be specified!")
551 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
553 lp = sambaopts.get_loadparm()
554 creds = credopts.get_credentials(lp, fallback_machine=True)
556 samdb = SamDB(url=H, session_info=system_session(),
557 credentials=creds, lp=lp)
559 samdb.enable_account(filter)
560 except Exception as msg:
561 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
562 self.outf.write("Enabled user '%s'\n" % (username or filter))
565 class cmd_user_disable(Command):
566 """Disable a user."""
568 synopsis = "%prog (<username>|--filter <filter>) [options]"
571 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
572 metavar="URL", dest="H"),
573 Option("--filter", help="LDAP Filter to set password on", type=str),
576 takes_args = ["username?"]
578 takes_optiongroups = {
579 "sambaopts": options.SambaOptions,
580 "credopts": options.CredentialsOptions,
581 "versionopts": options.VersionOptions,
584 def run(self, username=None, sambaopts=None, credopts=None,
585 versionopts=None, filter=None, H=None):
586 if username is None and filter is None:
587 raise CommandError("Either the username or '--filter' must be specified!")
590 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
592 lp = sambaopts.get_loadparm()
593 creds = credopts.get_credentials(lp, fallback_machine=True)
595 samdb = SamDB(url=H, session_info=system_session(),
596 credentials=creds, lp=lp)
598 samdb.disable_account(filter)
599 except Exception as msg:
600 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
603 class cmd_user_setexpiry(Command):
604 """Set the expiration of a user account.
606 The user can either be specified by their sAMAccountName or using the --filter option.
608 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.
610 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.
613 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
615 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.
618 sudo samba-tool user setexpiry User2 --noexpiry
620 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.
623 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
625 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.
628 samba-tool user setexpiry --noexpiry User4
629 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
632 synopsis = "%prog (<username>|--filter <filter>) [options]"
634 takes_optiongroups = {
635 "sambaopts": options.SambaOptions,
636 "versionopts": options.VersionOptions,
637 "credopts": options.CredentialsOptions,
641 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
642 metavar="URL", dest="H"),
643 Option("--filter", help="LDAP Filter to set password on", type=str),
644 Option("--days", help="Days to expiry", type=int, default=0),
645 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
648 takes_args = ["username?"]
650 def run(self, username=None, sambaopts=None, credopts=None,
651 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
652 if username is None and filter is None:
653 raise CommandError("Either the username or '--filter' must be specified!")
656 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
658 lp = sambaopts.get_loadparm()
659 creds = credopts.get_credentials(lp)
661 samdb = SamDB(url=H, session_info=system_session(),
662 credentials=creds, lp=lp)
665 samdb.setexpiry(filter, days * 24 * 3600, no_expiry_req=noexpiry)
666 except Exception as msg:
667 # FIXME: Catch more specific exception
668 raise CommandError("Failed to set expiry for user '%s': %s" % (
669 username or filter, msg))
671 self.outf.write("Expiry for user '%s' disabled.\n" % (
674 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
675 username or filter, days))
678 class cmd_user_password(Command):
679 """Change password for a user account (the one provided in authentication).
682 synopsis = "%prog [options]"
685 Option("--newpassword", help="New password", type=str),
688 takes_optiongroups = {
689 "sambaopts": options.SambaOptions,
690 "credopts": options.CredentialsOptions,
691 "versionopts": options.VersionOptions,
694 def run(self, credopts=None, sambaopts=None, versionopts=None,
697 lp = sambaopts.get_loadparm()
698 creds = credopts.get_credentials(lp)
700 # get old password now, to get the password prompts in the right order
701 old_password = creds.get_password()
703 net = Net(creds, lp, server=credopts.ipaddress)
705 password = newpassword
707 if password is not None and password is not '':
709 password = getpass("New Password: ")
710 passwordverify = getpass("Retype Password: ")
711 if not password == passwordverify:
713 self.outf.write("Sorry, passwords do not match.\n")
716 if not isinstance(password, text_type):
717 password = password.decode('utf8')
718 net.change_password(password)
719 except Exception as msg:
720 # FIXME: catch more specific exception
721 raise CommandError("Failed to change password : %s" % msg)
722 self.outf.write("Changed password OK\n")
725 class cmd_user_setpassword(Command):
726 """Set or reset the password of a user account.
728 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.
730 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.
732 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.
734 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.
737 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
739 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.
742 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
744 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.
747 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
749 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
752 synopsis = "%prog (<username>|--filter <filter>) [options]"
754 takes_optiongroups = {
755 "sambaopts": options.SambaOptions,
756 "versionopts": options.VersionOptions,
757 "credopts": options.CredentialsOptions,
761 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
762 metavar="URL", dest="H"),
763 Option("--filter", help="LDAP Filter to set password on", type=str),
764 Option("--newpassword", help="Set password", type=str),
765 Option("--must-change-at-next-login",
766 help="Force password to be changed on next login",
767 action="store_true"),
768 Option("--random-password",
769 help="Generate random password",
770 action="store_true"),
771 Option("--smartcard-required",
772 help="Require a smartcard for interactive logons",
773 action="store_true"),
774 Option("--clear-smartcard-required",
775 help="Don't require a smartcard for interactive logons",
776 action="store_true"),
779 takes_args = ["username?"]
781 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
782 versionopts=None, H=None, newpassword=None,
783 must_change_at_next_login=False, random_password=False,
784 smartcard_required=False, clear_smartcard_required=False):
785 if filter is None and username is None:
786 raise CommandError("Either the username or '--filter' must be specified!")
788 password = newpassword
790 if smartcard_required:
791 if password is not None and password is not '':
792 raise CommandError('It is not allowed to specify '
794 'together with --smartcard-required.')
795 if must_change_at_next_login:
796 raise CommandError('It is not allowed to specify '
797 '--must-change-at-next-login '
798 'together with --smartcard-required.')
799 if clear_smartcard_required:
800 raise CommandError('It is not allowed to specify '
801 '--clear-smartcard-required '
802 'together with --smartcard-required.')
804 if random_password and not smartcard_required:
805 password = generate_random_password(128, 255)
808 if smartcard_required:
810 if password is not None and password is not '':
812 password = getpass("New Password: ")
813 passwordverify = getpass("Retype Password: ")
814 if not password == passwordverify:
816 self.outf.write("Sorry, passwords do not match.\n")
819 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
821 lp = sambaopts.get_loadparm()
822 creds = credopts.get_credentials(lp)
824 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
826 samdb = SamDB(url=H, session_info=system_session(),
827 credentials=creds, lp=lp)
829 if smartcard_required:
832 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
833 flags = dsdb.UF_SMARTCARD_REQUIRED
834 samdb.toggle_userAccountFlags(filter, flags, on=True)
835 command = "Failed to enable account for user '%s'" % (username or filter)
836 samdb.enable_account(filter)
837 except Exception as msg:
838 # FIXME: catch more specific exception
839 raise CommandError("%s: %s" % (command, msg))
840 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
844 if clear_smartcard_required:
845 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
846 flags = dsdb.UF_SMARTCARD_REQUIRED
847 samdb.toggle_userAccountFlags(filter, flags, on=False)
848 command = "Failed to set password for user '%s'" % (username or filter)
849 samdb.setpassword(filter, password,
850 force_change_at_next_login=must_change_at_next_login,
852 except Exception as msg:
853 # FIXME: catch more specific exception
854 raise CommandError("%s: %s" % (command, msg))
855 self.outf.write("Changed password OK\n")
857 class GetPasswordCommand(Command):
860 super(GetPasswordCommand, self).__init__()
863 def connect_system_samdb(self, url, allow_local=False, verbose=False):
865 # using anonymous here, results in no authentication
866 # which means we can get system privileges via
867 # the privileged ldapi socket
868 creds = credentials.Credentials()
869 creds.set_anonymous()
871 if url is None and allow_local:
873 elif url.lower().startswith("ldapi://"):
875 elif url.lower().startswith("ldap://"):
876 raise CommandError("--url ldap:// is not supported for this command")
877 elif url.lower().startswith("ldaps://"):
878 raise CommandError("--url ldaps:// is not supported for this command")
879 elif not allow_local:
880 raise CommandError("--url requires an ldapi:// url for this command")
883 self.outf.write("Connecting to '%s'\n" % url)
885 samdb = SamDB(url=url, session_info=system_session(),
886 credentials=creds, lp=self.lp)
890 # Make sure we're connected as SYSTEM
892 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
894 sids = res[0].get("tokenGroups")
895 assert len(sids) == 1
896 sid = ndr_unpack(security.dom_sid, sids[0])
897 assert str(sid) == security.SID_NT_SYSTEM
898 except Exception as msg:
899 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
900 (security.SID_NT_SYSTEM))
902 # We use sort here in order to have a predictable processing order
903 # this might not be strictly needed, but also doesn't hurt here
904 for a in sorted(virtual_attributes.keys()):
905 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
906 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
910 def get_account_attributes(self, samdb, username, basedn, filter, scope,
917 (attr, _, opts) = a.partition(';')
919 attr_opts[attr] = opts
921 attr_opts[attr] = None
922 search_attrs.append(attr)
923 lower_attrs = [x.lower() for x in search_attrs]
925 require_supplementalCredentials = False
926 for a in virtual_attributes.keys():
927 if a.lower() in lower_attrs:
928 require_supplementalCredentials = True
929 add_supplementalCredentials = False
930 add_unicodePwd = False
931 if require_supplementalCredentials:
932 a = "supplementalCredentials"
933 if a.lower() not in lower_attrs:
935 add_supplementalCredentials = True
937 if a.lower() not in lower_attrs:
939 add_unicodePwd = True
940 add_sAMAcountName = False
942 if a.lower() not in lower_attrs:
944 add_sAMAcountName = True
946 add_userPrincipalName = False
947 upn = "usePrincipalName"
948 if upn.lower() not in lower_attrs:
949 search_attrs += [upn]
950 add_userPrincipalName = True
952 if scope == ldb.SCOPE_BASE:
953 search_controls = ["show_deleted:1", "show_recycled:1"]
957 res = samdb.search(base=basedn, expression=filter,
958 scope=scope, attrs=search_attrs,
959 controls=search_controls)
961 raise Exception('Unable to find user "%s"' % (username or filter))
963 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
964 except Exception as msg:
965 # FIXME: catch more specific exception
966 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
971 if "supplementalCredentials" in obj:
972 sc_blob = obj["supplementalCredentials"][0]
973 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
974 if add_supplementalCredentials:
975 del obj["supplementalCredentials"]
976 if "unicodePwd" in obj:
977 unicodePwd = obj["unicodePwd"][0]
979 del obj["unicodePwd"]
980 account_name = obj["sAMAccountName"][0]
981 if add_sAMAcountName:
982 del obj["sAMAccountName"]
983 if "userPrincipalName" in obj:
984 account_upn = obj["userPrincipalName"][0]
986 realm = self.lp.get("realm")
987 account_upn = "%s@%s" % (account_name, realm.lower())
988 if add_userPrincipalName:
989 del obj["userPrincipalName"]
992 def get_package(name, min_idx=0):
993 if name in calculated:
994 return calculated[name]
998 min_idx = len(sc.sub.packages) + min_idx
1000 for p in sc.sub.packages:
1007 return binascii.a2b_hex(p.data)
1012 # Samba adds 'Primary:SambaGPG' at the end.
1013 # When Windows sets the password it keeps
1014 # 'Primary:SambaGPG' and rotates it to
1015 # the begining. So we can only use the value,
1016 # if it is the last one.
1018 # In order to get more protection we verify
1019 # the nthash of the decrypted utf16 password
1020 # against the stored nthash in unicodePwd.
1022 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1023 if sgv is not None and unicodePwd is not None:
1024 ctx = gpgme.Context()
1026 cipher_io = io.BytesIO(sgv)
1027 plain_io = io.BytesIO()
1029 ctx.decrypt(cipher_io, plain_io)
1030 cv = plain_io.getvalue()
1032 # We only use the password if it matches
1033 # the current nthash stored in the unicodePwd
1036 tmp = credentials.Credentials()
1038 tmp.set_utf16_password(cv)
1039 nthash = tmp.get_nt_hash()
1040 if nthash == unicodePwd:
1041 calculated["Primary:CLEARTEXT"] = cv
1042 except gpgme.GpgmeError as e1:
1043 (major, minor, msg) = e1.args
1044 if major == gpgme.ERR_BAD_SECKEY:
1045 msg = "ERR_BAD_SECKEY: " + msg
1047 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1048 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1049 username or account_name, msg))
1051 def get_utf8(a, b, username):
1053 u = unicode(b, 'utf-16-le')
1054 except UnicodeDecodeError as e:
1055 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1058 u8 = u.encode('utf-8')
1061 # Extract the WDigest hash for the value specified by i.
1062 # Builds an htdigest compatible value
1064 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1065 domain, dns_domain):
1070 user = account_name.lower()
1071 realm = domain.lower()
1073 user = account_name.upper()
1074 realm = domain.upper()
1077 realm = domain.upper()
1080 realm = domain.lower()
1082 user = account_name.upper()
1083 realm = domain.lower()
1085 user = account_name.lower()
1086 realm = domain.upper()
1089 realm = dns_domain.lower()
1091 user = account_name.lower()
1092 realm = dns_domain.lower()
1094 user = account_name.upper()
1095 realm = dns_domain.upper()
1098 realm = dns_domain.upper()
1101 realm = dns_domain.lower()
1103 user = account_name.upper()
1104 realm = dns_domain.lower()
1106 user = account_name.lower()
1107 realm = dns_domain.upper()
1112 user = account_upn.lower()
1115 user = account_upn.upper()
1118 user = "%s\\%s" % (domain, account_name)
1121 user = "%s\\%s" % (domain.lower(), account_name.lower())
1124 user = "%s\\%s" % (domain.upper(), account_name.upper())
1130 user = account_name.lower()
1133 user = account_name.upper()
1139 user = account_upn.lower()
1142 user = account_upn.upper()
1145 user = "%s\\%s" % (domain, account_name)
1148 # Differs from spec, see tests
1149 user = "%s\\%s" % (domain.lower(), account_name.lower())
1152 # Differs from spec, see tests
1153 user = "%s\\%s" % (domain.upper(), account_name.upper())
1158 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1161 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
1162 return "%s:%s:%s" % (user, realm, digest)
1167 # get the value for a virtualCrypt attribute.
1168 # look for an exact match on algorithm and rounds in supplemental creds
1169 # if not found calculate using Primary:CLEARTEXT
1170 # if no Primary:CLEARTEXT return the first supplementalCredential
1171 # that matches the algorithm.
1172 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1175 b = get_package("Primary:userPassword")
1177 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1179 # No exact match on algorithm and number of rounds
1180 # try and calculate one from the Primary:CLEARTEXT
1181 b = get_package("Primary:CLEARTEXT")
1183 u8 = get_utf8(a, b, username or account_name)
1185 sv = get_crypt_value(str(algorithm), u8, rounds)
1187 # Unable to calculate a hash with the specified
1188 # number of rounds, fall back to the first hash using
1189 # the specified algorithm
1193 return "{CRYPT}" + sv
1195 def get_userPassword_hash(blob, algorithm, rounds):
1196 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1199 # Check that the NT hash has not been changed without updating
1200 # the user password hashes. This indicates that password has been
1201 # changed without updating the supplemental credentials.
1202 if unicodePwd != bytearray(up.current_nt_hash.hash):
1205 scheme_prefix = "$%d$" % algorithm
1206 prefix = scheme_prefix
1208 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1212 if (scheme_match is None and
1213 h.scheme == SCHEME and
1214 h.value.startswith(scheme_prefix)):
1215 scheme_match = h.value
1216 if h.scheme == SCHEME and h.value.startswith(prefix):
1217 return (h.value, scheme_match)
1219 # No match on the number of rounds, return the value of the
1220 # first matching scheme
1221 return (None, scheme_match)
1223 # We use sort here in order to have a predictable processing order
1224 for a in sorted(virtual_attributes.keys()):
1225 if not a.lower() in lower_attrs:
1228 if a == "virtualClearTextUTF8":
1229 b = get_package("Primary:CLEARTEXT")
1232 u8 = get_utf8(a, b, username or account_name)
1236 elif a == "virtualClearTextUTF16":
1237 v = get_package("Primary:CLEARTEXT")
1240 elif a == "virtualSSHA":
1241 b = get_package("Primary:CLEARTEXT")
1244 u8 = get_utf8(a, b, username or account_name)
1247 salt = get_random_bytes(4)
1251 bv = h.digest() + salt
1252 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
1253 elif a == "virtualCryptSHA256":
1254 rounds = get_rounds(attr_opts[a])
1255 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1259 elif a == "virtualCryptSHA512":
1260 rounds = get_rounds(attr_opts[a])
1261 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1265 elif a == "virtualSambaGPG":
1266 # Samba adds 'Primary:SambaGPG' at the end.
1267 # When Windows sets the password it keeps
1268 # 'Primary:SambaGPG' and rotates it to
1269 # the begining. So we can only use the value,
1270 # if it is the last one.
1271 v = get_package("Primary:SambaGPG", min_idx=-1)
1274 elif a.startswith("virtualWDigest"):
1275 primary_wdigest = get_package("Primary:WDigest")
1276 if primary_wdigest is None:
1278 x = a[len("virtualWDigest"):]
1283 domain = self.lp.get("workgroup")
1284 dns_domain = samdb.domain_dns_name()
1285 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1290 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1293 def parse_attributes(self, attributes):
1295 if attributes is None:
1296 raise CommandError("Please specify --attributes")
1297 attrs = attributes.split(',')
1300 pa = pa.lstrip().rstrip()
1301 for da in disabled_virtual_attributes.keys():
1302 if pa.lower() == da.lower():
1303 r = disabled_virtual_attributes[da]["reason"]
1304 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1306 for va in virtual_attributes.keys():
1307 if pa.lower() == va.lower():
1308 # Take the real name
1311 password_attrs += [pa]
1313 return password_attrs
1315 class cmd_user_getpassword(GetPasswordCommand):
1316 """Get the password fields of a user/computer account.
1318 This command gets the logon password for a user/computer account.
1320 The username specified on the command is the sAMAccountName.
1321 The username may also be specified using the --filter option.
1323 The command must be run from the root user id or another authorized user id.
1324 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1325 used to adjust the local path. By default tdb:// is used by default.
1327 The '--attributes' parameter takes a comma separated list of attributes,
1328 which will be printed or given to the script specified by '--script'. If a
1329 specified attribute is not available on an object it's silently omitted.
1330 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1331 the NTHASH) and the following virtual attributes are possible (see --help
1332 for which virtual attributes are supported in your environment):
1334 virtualClearTextUTF16: The raw cleartext as stored in the
1335 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1336 with '--decrypt-samba-gpg') buffer inside of the
1337 supplementalCredentials attribute. This typically
1338 contains valid UTF-16-LE, but may contain random
1339 bytes, e.g. for computer accounts.
1341 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1342 (only from valid UTF-16-LE)
1344 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1345 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1347 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1348 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1349 with a $5$... salt, see crypt(3) on modern systems.
1350 The number of rounds used to calculate the hash can
1351 also be specified. By appending ";rounds=x" to the
1352 attribute name i.e. virtualCryptSHA256;rounds=10000
1353 will calculate a SHA256 hash with 10,000 rounds.
1354 non numeric values for rounds are silently ignored
1355 The value is calculated as follows:
1356 1) If a value exists in 'Primary:userPassword' with
1357 the specified number of rounds it is returned.
1358 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1359 '--decrypt-samba-gpg'. Calculate a hash with
1360 the specified number of rounds
1361 3) Return the first CryptSHA256 value in
1362 'Primary:userPassword'
1365 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1366 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1367 with a $6$... salt, see crypt(3) on modern systems.
1368 The number of rounds used to calculate the hash can
1369 also be specified. By appending ";rounds=x" to the
1370 attribute name i.e. virtualCryptSHA512;rounds=10000
1371 will calculate a SHA512 hash with 10,000 rounds.
1372 non numeric values for rounds are silently ignored
1373 The value is calculated as follows:
1374 1) If a value exists in 'Primary:userPassword' with
1375 the specified number of rounds it is returned.
1376 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1377 '--decrypt-samba-gpg'. Calculate a hash with
1378 the specified number of rounds
1379 3) Return the first CryptSHA512 value in
1380 'Primary:userPassword'
1382 virtualWDigestNN: The individual hash values stored in
1383 'Primary:WDigest' where NN is the hash number in
1385 NOTE: As at 22-05-2017 the documentation:
1386 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1387 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1390 virtualSambaGPG: The raw cleartext as stored in the
1391 'Primary:SambaGPG' buffer inside of the
1392 supplementalCredentials attribute.
1393 See the 'password hash gpg key ids' option in
1396 The '--decrypt-samba-gpg' option triggers decryption of the
1397 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1398 in your environment or not (the python-gpgme package is required). Please
1399 note that you might need to set the GNUPGHOME environment variable. If the
1400 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1401 environment variable has been set correctly and the passphrase is already
1402 known by the gpg-agent.
1405 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1408 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1412 super(cmd_user_getpassword, self).__init__()
1414 synopsis = "%prog (<username>|--filter <filter>) [options]"
1416 takes_optiongroups = {
1417 "sambaopts": options.SambaOptions,
1418 "versionopts": options.VersionOptions,
1422 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1423 metavar="URL", dest="H"),
1424 Option("--filter", help="LDAP Filter to set password on", type=str),
1425 Option("--attributes", type=str,
1426 help=virtual_attributes_help,
1427 metavar="ATTRIBUTELIST", dest="attributes"),
1428 Option("--decrypt-samba-gpg",
1429 help=decrypt_samba_gpg_help,
1430 action="store_true", default=False, dest="decrypt_samba_gpg"),
1433 takes_args = ["username?"]
1435 def run(self, username=None, H=None, filter=None,
1436 attributes=None, decrypt_samba_gpg=None,
1437 sambaopts=None, versionopts=None):
1438 self.lp = sambaopts.get_loadparm()
1440 if decrypt_samba_gpg and not gpgme_support:
1441 raise CommandError(decrypt_samba_gpg_help)
1443 if filter is None and username is None:
1444 raise CommandError("Either the username or '--filter' must be specified!")
1447 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1449 if attributes is None:
1450 raise CommandError("Please specify --attributes")
1452 password_attrs = self.parse_attributes(attributes)
1454 samdb = self.connect_system_samdb(url=H, allow_local=True)
1456 obj = self.get_account_attributes(samdb, username,
1459 scope=ldb.SCOPE_SUBTREE,
1460 attrs=password_attrs,
1461 decrypt=decrypt_samba_gpg)
1463 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1464 self.outf.write("%s" % ldif)
1465 self.outf.write("Got password OK\n")
1467 class cmd_user_syncpasswords(GetPasswordCommand):
1468 """Sync the password of user accounts.
1470 This syncs logon passwords for user accounts.
1472 Note that this command should run on a single domain controller only
1473 (typically the PDC-emulator). However the "password hash gpg key ids"
1474 option should to be configured on all domain controllers.
1476 The command must be run from the root user id or another authorized user id.
1477 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1478 local path. By default, ldapi:// is used with the default path to the
1479 privileged ldapi socket.
1481 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1482 "Sync Loop Terminate".
1485 Cache Initialization
1486 ====================
1488 The first time, this command needs to be called with
1489 '--cache-ldb-initialize' in order to initialize its cache.
1491 The cache initialization requires '--attributes' and allows the following
1492 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1495 The '--attributes' parameter takes a comma separated list of attributes,
1496 which will be printed or given to the script specified by '--script'. If a
1497 specified attribute is not available on an object it will be silently omitted.
1498 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1499 the NTHASH) and the following virtual attributes are possible (see '--help'
1500 for supported virtual attributes in your environment):
1502 virtualClearTextUTF16: The raw cleartext as stored in the
1503 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1504 with '--decrypt-samba-gpg') buffer inside of the
1505 supplementalCredentials attribute. This typically
1506 contains valid UTF-16-LE, but may contain random
1507 bytes, e.g. for computer accounts.
1509 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1510 (only from valid UTF-16-LE)
1512 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1513 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1515 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1516 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1517 with a $5$... salt, see crypt(3) on modern systems.
1518 The number of rounds used to calculate the hash can
1519 also be specified. By appending ";rounds=x" to the
1520 attribute name i.e. virtualCryptSHA256;rounds=10000
1521 will calculate a SHA256 hash with 10,000 rounds.
1522 non numeric values for rounds are silently ignored
1523 The value is calculated as follows:
1524 1) If a value exists in 'Primary:userPassword' with
1525 the specified number of rounds it is returned.
1526 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1527 '--decrypt-samba-gpg'. Calculate a hash with
1528 the specified number of rounds
1529 3) Return the first CryptSHA256 value in
1530 'Primary:userPassword'
1532 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1533 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1534 with a $6$... salt, see crypt(3) on modern systems.
1535 The number of rounds used to calculate the hash can
1536 also be specified. By appending ";rounds=x" to the
1537 attribute name i.e. virtualCryptSHA512;rounds=10000
1538 will calculate a SHA512 hash with 10,000 rounds.
1539 non numeric values for rounds are silently ignored
1540 The value is calculated as follows:
1541 1) If a value exists in 'Primary:userPassword' with
1542 the specified number of rounds it is returned.
1543 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1544 '--decrypt-samba-gpg'. Calculate a hash with
1545 the specified number of rounds
1546 3) Return the first CryptSHA512 value in
1547 'Primary:userPassword'
1549 virtualWDigestNN: The individual hash values stored in
1550 'Primary:WDigest' where NN is the hash number in
1552 NOTE: As at 22-05-2017 the documentation:
1553 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1554 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1557 virtualSambaGPG: The raw cleartext as stored in the
1558 'Primary:SambaGPG' buffer inside of the
1559 supplementalCredentials attribute.
1560 See the 'password hash gpg key ids' option in
1563 The '--decrypt-samba-gpg' option triggers decryption of the
1564 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1565 in your environment or not (the python-gpgme package is required). Please
1566 note that you might need to set the GNUPGHOME environment variable. If the
1567 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1568 environment variable has been set correctly and the passphrase is already
1569 known by the gpg-agent.
1571 The '--script' option specifies a custom script that is called whenever any
1572 of the dirsyncAttributes (see below) was changed. The script is called
1573 without any arguments. It gets the LDIF for exactly one object on STDIN.
1574 If the script processed the object successfully it has to respond with a
1575 single line starting with 'DONE-EXIT: ' followed by an optional message.
1577 Note that the script might be called without any password change, e.g. if
1578 the account was disabled (a userAccountControl change) or the
1579 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1580 are always returned as unique identifier of the account. It might be useful
1581 to also ask for non-password attributes like: objectSid, sAMAccountName,
1582 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1583 Depending on the object, some attributes may not be present/available,
1584 but you always get the current state (and not a diff).
1586 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1589 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1590 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1591 (!(sAMAccountName=krbtgt*)))
1592 This means only normal (non-krbtgt) user
1593 accounts are monitored. The '--filter' can modify that, e.g. if it's
1594 required to also sync computer accounts.
1600 This (default) mode runs in an endless loop waiting for password related
1601 changes in the active directory database. It makes use of the
1602 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1603 get changes in a reliable fashion. Objects are monitored for changes of the
1604 following dirsyncAttributes:
1606 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1607 userPrincipalName and userAccountControl.
1609 It recovers from LDAP disconnects and updates the cache in conservative way
1610 (in single steps after each successfully processed change). An error from
1611 the script (specified by '--script') will result in fatal error and this
1612 command will exit. But the cache state should be still valid and can be
1613 resumed in the next "Sync Loop Run".
1615 The '--logfile' option specifies an optional (required if '--daemon' is
1616 specified) logfile that takes all output of the command. The logfile is
1617 automatically reopened if fstat returns st_nlink == 0.
1619 The optional '--daemon' option will put the command into the background.
1621 You can stop the command without the '--daemon' option, also by hitting
1624 If you specify the '--no-wait' option the command skips the
1625 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1626 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1631 In order to terminate an already running command (likely as daemon) the
1632 '--terminate' option can be used. This also requires the '--logfile' option
1637 samba-tool user syncpasswords --cache-ldb-initialize \\
1638 --attributes=virtualClearTextUTF8
1639 samba-tool user syncpasswords
1642 samba-tool user syncpasswords --cache-ldb-initialize \\
1643 --attributes=objectGUID,objectSID,sAMAccountName,\\
1644 userPrincipalName,userAccountControl,pwdLastSet,\\
1645 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1646 --script=/path/to/my-custom-syncpasswords-script.py
1647 samba-tool user syncpasswords --daemon \\
1648 --logfile=/var/log/samba/user-syncpasswords.log
1649 samba-tool user syncpasswords --terminate \\
1650 --logfile=/var/log/samba/user-syncpasswords.log
1654 super(cmd_user_syncpasswords, self).__init__()
1656 synopsis = "%prog [--cache-ldb-initialize] [options]"
1658 takes_optiongroups = {
1659 "sambaopts": options.SambaOptions,
1660 "versionopts": options.VersionOptions,
1664 Option("--cache-ldb-initialize",
1665 help="Initialize the cache for the first time",
1666 dest="cache_ldb_initialize", action="store_true"),
1667 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1668 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1669 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1670 metavar="URL", dest="H"),
1671 Option("--filter", help="optional LDAP filter to set password on", type=str,
1672 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1673 Option("--attributes", type=str,
1674 help=virtual_attributes_help,
1675 metavar="ATTRIBUTELIST", dest="attributes"),
1676 Option("--decrypt-samba-gpg",
1677 help=decrypt_samba_gpg_help,
1678 action="store_true", default=False, dest="decrypt_samba_gpg"),
1679 Option("--script", help="Script that is called for each password change", type=str,
1680 metavar="/path/to/syncpasswords.script", dest="script"),
1681 Option("--no-wait", help="Don't block waiting for changes",
1682 action="store_true", default=False, dest="nowait"),
1683 Option("--logfile", type=str,
1684 help="The logfile to use (required in --daemon mode).",
1685 metavar="/path/to/syncpasswords.log", dest="logfile"),
1686 Option("--daemon", help="daemonize after initial setup",
1687 action="store_true", default=False, dest="daemon"),
1688 Option("--terminate",
1689 help="Send a SIGTERM to an already running (daemon) process",
1690 action="store_true", default=False, dest="terminate"),
1693 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1694 H=None, filter=None,
1695 attributes=None, decrypt_samba_gpg=None,
1696 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1697 sambaopts=None, versionopts=None):
1699 self.lp = sambaopts.get_loadparm()
1701 self.samdb_url = None
1705 if not cache_ldb_initialize:
1706 if attributes is not None:
1707 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1708 if decrypt_samba_gpg:
1709 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1710 if script is not None:
1711 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1712 if filter is not None:
1713 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1715 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1717 if nowait is not False:
1718 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1719 if logfile is not None:
1720 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1721 if daemon is not False:
1722 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1723 if terminate is not False:
1724 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1728 raise CommandError("--daemon is not allowed together with --no-wait")
1729 if terminate is not False:
1730 raise CommandError("--terminate is not allowed together with --no-wait")
1732 if terminate is True and daemon is True:
1733 raise CommandError("--terminate is not allowed together with --daemon")
1735 if daemon is True and logfile is None:
1736 raise CommandError("--daemon is only allowed together with --logfile")
1738 if terminate is True and logfile is None:
1739 raise CommandError("--terminate is only allowed together with --logfile")
1741 if script is not None:
1742 if not os.path.exists(script):
1743 raise CommandError("script[%s] does not exist!" % script)
1745 sync_command = "%s" % os.path.abspath(script)
1749 dirsync_filter = filter
1750 if dirsync_filter is None:
1751 dirsync_filter = "(&" + \
1752 "(objectClass=user)" + \
1753 "(userAccountControl:%s:=%u)" % (
1754 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1755 "(!(sAMAccountName=krbtgt*))" + \
1758 dirsync_secret_attrs = [
1761 "supplementalCredentials",
1764 dirsync_attrs = dirsync_secret_attrs + [
1767 "userPrincipalName",
1768 "userAccountControl",
1773 password_attrs = None
1775 if cache_ldb_initialize:
1777 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1779 if decrypt_samba_gpg and not gpgme_support:
1780 raise CommandError(decrypt_samba_gpg_help)
1782 password_attrs = self.parse_attributes(attributes)
1783 lower_attrs = [x.lower() for x in password_attrs]
1784 # We always return these in order to track deletions
1785 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1786 if a.lower() not in lower_attrs:
1787 password_attrs += [a]
1789 if cache_ldb is not None:
1790 if cache_ldb.lower().startswith("ldapi://"):
1791 raise CommandError("--cache_ldb ldapi:// is not supported")
1792 elif cache_ldb.lower().startswith("ldap://"):
1793 raise CommandError("--cache_ldb ldap:// is not supported")
1794 elif cache_ldb.lower().startswith("ldaps://"):
1795 raise CommandError("--cache_ldb ldaps:// is not supported")
1796 elif cache_ldb.lower().startswith("tdb://"):
1799 if not os.path.exists(cache_ldb):
1800 cache_ldb = self.lp.private_path(cache_ldb)
1802 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1804 self.lockfile = "%s.pid" % cache_ldb
1807 if self.logfile is not None:
1809 if info.st_nlink == 0:
1810 logfile = self.logfile
1812 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1813 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
1818 log_msg("Reopened logfile[%s]\n" % (logfile))
1819 self.logfile = logfile
1820 msg = "%s: pid[%d]: %s" % (
1824 self.outf.write(msg)
1833 "passwordAttribute",
1839 self.cache = Ldb(cache_ldb)
1840 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1841 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1845 self.samdb_url = res[0]["samdbUrl"][0]
1846 except KeyError as e:
1847 self.samdb_url = None
1849 self.samdb_url = None
1850 if self.samdb_url is None and not cache_ldb_initialize:
1851 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1853 if self.samdb_url is not None and cache_ldb_initialize:
1854 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1856 if self.samdb_url is None:
1858 self.dirsync_filter = dirsync_filter
1859 self.dirsync_attrs = dirsync_attrs
1860 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"];
1861 self.password_attrs = password_attrs
1862 self.decrypt_samba_gpg = decrypt_samba_gpg
1863 self.sync_command = sync_command
1864 add_ldif = "dn: %s\n" % self.cache_dn
1865 add_ldif += "objectClass: userSyncPasswords\n"
1866 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url).decode('utf8')
1867 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter).decode('utf8')
1868 for a in self.dirsync_attrs:
1869 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a).decode('utf8')
1870 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1871 for a in self.password_attrs:
1872 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a).decode('utf8')
1873 if self.decrypt_samba_gpg == True:
1874 add_ldif += "decryptSambaGPG: TRUE\n"
1876 add_ldif += "decryptSambaGPG: FALSE\n"
1877 if self.sync_command is not None:
1878 add_ldif += "syncCommand: %s\n" % self.sync_command
1879 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1880 self.cache.add_ldif(add_ldif)
1881 self.current_pid = None
1882 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1883 msgs = self.cache.parse_ldif(add_ldif)
1884 changetype, msg = next(msgs)
1885 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1886 self.outf.write("%s" % ldif)
1888 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1889 self.dirsync_attrs = []
1890 for a in res[0]["dirsyncAttribute"]:
1891 self.dirsync_attrs.append(a)
1892 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1893 self.password_attrs = []
1894 for a in res[0]["passwordAttribute"]:
1895 self.password_attrs.append(a)
1896 decrypt_string = res[0]["decryptSambaGPG"][0]
1897 assert(decrypt_string in ["TRUE", "FALSE"])
1898 if decrypt_string == "TRUE":
1899 self.decrypt_samba_gpg = True
1901 self.decrypt_samba_gpg = False
1902 if "syncCommand" in res[0]:
1903 self.sync_command = res[0]["syncCommand"][0]
1905 self.sync_command = None
1906 if "currentPid" in res[0]:
1907 self.current_pid = int(res[0]["currentPid"][0])
1909 self.current_pid = None
1910 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1914 def run_sync_command(dn, ldif):
1915 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1916 sync_command_p = Popen(self.sync_command,
1921 res = sync_command_p.poll()
1924 input = "%s" % (ldif)
1925 reply = sync_command_p.communicate(input)[0]
1926 log_msg("%s\n" % (reply))
1927 res = sync_command_p.poll()
1929 sync_command_p.terminate()
1930 res = sync_command_p.wait()
1932 if reply.startswith("DONE-EXIT: "):
1935 log_msg("RESULT: %s\n" % (res))
1936 raise Exception("ERROR: %s - %s\n" % (res, reply))
1938 def handle_object(idx, dirsync_obj):
1939 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1940 guid = ndr_unpack(misc.GUID, binary_guid)
1941 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1942 sid = ndr_unpack(security.dom_sid, binary_sid)
1943 domain_sid, rid = sid.split()
1944 if rid == security.DOMAIN_RID_KRBTGT:
1945 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1947 for a in list(dirsync_obj.keys()):
1948 for h in dirsync_secret_attrs:
1949 if a.lower() == h.lower():
1951 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1952 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1953 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
1954 obj = self.get_account_attributes(self.samdb,
1955 username="%s" % sid,
1956 basedn="<GUID=%s>" % guid,
1957 filter="(objectClass=user)",
1958 scope=ldb.SCOPE_BASE,
1959 attrs=self.password_attrs,
1960 decrypt=self.decrypt_samba_gpg)
1961 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1962 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1963 if self.sync_command is None:
1964 self.outf.write("%s" % (ldif))
1966 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1967 run_sync_command(obj.dn, ldif)
1969 def check_current_pid_conflict(terminate):
1975 self.lockfd = os.open(self.lockfile, flags, 0o600)
1976 except IOError as e4:
1977 (err, msg) = e4.args
1978 if err == errno.ENOENT:
1981 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1982 (self.lockfile, msg, err))
1985 got_exclusive = False
1987 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1988 got_exclusive = True
1989 except IOError as e5:
1990 (err, msg) = e5.args
1991 if err != errno.EACCES and err != errno.EAGAIN:
1992 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
1993 (self.lockfile, msg, err))
1996 if not got_exclusive:
1997 buf = os.read(self.lockfd, 64)
1998 self.current_pid = None
2000 self.current_pid = int(buf)
2001 except ValueError as e:
2003 if self.current_pid is not None:
2006 if got_exclusive and terminate:
2008 os.ftruncate(self.lockfd, 0)
2009 except IOError as e2:
2010 (err, msg) = e2.args
2011 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2012 (self.lockfile, msg, err))
2014 os.close(self.lockfd)
2019 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2020 except IOError as e6:
2021 (err, msg) = e6.args
2022 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2023 (self.lockfile, msg, err))
2025 # We leave the function with the shared lock.
2028 def update_pid(pid):
2029 if self.lockfd != -1:
2030 got_exclusive = False
2031 # Try 5 times to get the exclusiv lock.
2032 for i in range(0, 5):
2034 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2035 got_exclusive = True
2036 except IOError as e:
2038 if err != errno.EACCES and err != errno.EAGAIN:
2039 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2040 (pid, self.lockfile, msg, err))
2045 if not got_exclusive:
2046 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
2047 (pid, self.lockfile))
2048 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2049 (pid, self.lockfile))
2056 os.ftruncate(self.lockfd, 0)
2058 os.write(self.lockfd, buf)
2059 except IOError as e3:
2060 (err, msg) = e3.args
2061 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2062 (self.lockfile, msg, err))
2064 self.current_pid = pid
2065 if self.current_pid is not None:
2066 log_msg("currentPid: %d\n" % self.current_pid)
2068 modify_ldif = "dn: %s\n" % (self.cache_dn)
2069 modify_ldif += "changetype: modify\n"
2070 modify_ldif += "replace: currentPid\n"
2071 if self.current_pid is not None:
2072 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2073 modify_ldif += "replace: currentTime\n"
2074 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2075 self.cache.modify_ldif(modify_ldif)
2078 def update_cache(res_controls):
2079 assert len(res_controls) > 0
2080 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2081 res_controls[0].critical = True
2082 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2083 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2085 modify_ldif = "dn: %s\n" % (self.cache_dn)
2086 modify_ldif += "changetype: modify\n"
2087 modify_ldif += "replace: dirsyncControl\n"
2088 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2089 modify_ldif += "replace: currentTime\n"
2090 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2091 self.cache.modify_ldif(modify_ldif)
2094 def check_object(dirsync_obj, res_controls):
2095 assert len(res_controls) > 0
2096 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2098 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2099 sid = ndr_unpack(security.dom_sid, binary_sid)
2101 lastCookie = str(res_controls[0])
2103 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2104 expression="(lastCookie=%s)" % (
2105 ldb.binary_encode(lastCookie)),
2111 def update_object(dirsync_obj, res_controls):
2112 assert len(res_controls) > 0
2113 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2115 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2116 sid = ndr_unpack(security.dom_sid, binary_sid)
2118 lastCookie = str(res_controls[0])
2120 self.cache.transaction_start()
2122 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2123 expression="(objectClass=*)",
2124 attrs=["lastCookie"])
2126 add_ldif = "dn: %s\n" % (dn)
2127 add_ldif += "objectClass: userCookie\n"
2128 add_ldif += "lastCookie: %s\n" % (lastCookie)
2129 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2130 self.cache.add_ldif(add_ldif)
2132 modify_ldif = "dn: %s\n" % (dn)
2133 modify_ldif += "changetype: modify\n"
2134 modify_ldif += "replace: lastCookie\n"
2135 modify_ldif += "lastCookie: %s\n" % (lastCookie)
2136 modify_ldif += "replace: currentTime\n"
2137 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2138 self.cache.modify_ldif(modify_ldif)
2139 self.cache.transaction_commit()
2140 except Exception as e:
2141 self.cache.transaction_cancel()
2147 res = self.samdb.search(expression=self.dirsync_filter,
2148 scope=ldb.SCOPE_SUBTREE,
2149 attrs=self.dirsync_attrs,
2150 controls=self.dirsync_controls)
2151 log_msg("dirsync_loop(): results %d\n" % len(res))
2154 done = check_object(r, res.controls)
2156 handle_object(ri, r)
2157 update_object(r, res.controls)
2159 update_cache(res.controls)
2163 def sync_loop(wait):
2164 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2165 notify_controls = ["notification:1", "show_recycled:1"]
2166 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2167 scope=ldb.SCOPE_SUBTREE,
2169 controls=notify_controls,
2173 log_msg("Resuming monitoring\n")
2175 log_msg("Getting changes\n")
2176 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2177 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2178 self.outf.write("syncCommand: %s\n" % self.sync_command)
2181 if wait is not True:
2184 for msg in notify_handle:
2185 if not isinstance(msg, ldb.Message):
2186 self.outf.write("referal: %s\n" % msg)
2188 created = msg.get("uSNCreated")[0]
2189 changed = msg.get("uSNChanged")[0]
2190 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2191 (msg.dn, created, changed))
2195 res = notify_handle.result()
2200 orig_pid = os.getpid()
2205 if pid == 0: # Actual daemon
2207 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2212 if cache_ldb_initialize:
2214 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2219 if logfile is not None:
2220 import resource # Resource usage information.
2221 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2222 if maxfd == resource.RLIM_INFINITY:
2223 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2224 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2225 self.outf.write("Using logfile[%s]\n" % logfile)
2226 for fd in range(0, maxfd):
2237 log_msg("Attached to logfile[%s]\n" % (logfile))
2238 self.logfile = logfile
2241 conflict = check_current_pid_conflict(terminate)
2243 if self.current_pid is None:
2244 log_msg("No process running.\n")
2247 log_msg("Proccess %d is not running anymore.\n" % (
2251 log_msg("Sending SIGTERM to proccess %d.\n" % (
2253 os.kill(self.current_pid, signal.SIGTERM)
2256 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2257 os.getpid(), self.current_pid))
2261 update_pid(os.getpid())
2266 retry_sleep_max = 600
2271 retry_sleep = retry_sleep_min
2273 while self.samdb is None:
2274 if retry_sleep != 0:
2275 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2276 time.sleep(retry_sleep)
2277 retry_sleep = retry_sleep * 2
2278 if retry_sleep >= retry_sleep_max:
2279 retry_sleep = retry_sleep_max
2280 log_msg("Connecting to '%s'\n" % self.samdb_url)
2282 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2283 except Exception as msg:
2285 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2286 if wait is not True:
2291 except ldb.LdbError as e7:
2292 (enum, estr) = e7.args
2294 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2299 class cmd_user_edit(Command):
2300 """Modify User AD object.
2302 This command will allow editing of a user account in the Active Directory
2303 domain. You will then be able to add or change attributes and their values.
2305 The username specified on the command is the sAMAccountName.
2307 The command may be run from the root userid or another authorized userid.
2309 The -H or --URL= option can be used to execute the command against a remote
2313 samba-tool user edit User1 -H ldap://samba.samdom.example.com \
2314 -U administrator --password=passw1rd
2316 Example1 shows how to edit a users attributes in the domain against a remote
2319 The -H parameter is used to specify the remote target server.
2322 samba-tool user edit User2
2324 Example2 shows how to edit a users attributes in the domain against a local
2328 samba-tool user edit User3 --editor=nano
2330 Example3 shows how to edit a users attributes in the domain against a local
2331 LDAP server using the 'nano' editor.
2334 synopsis = "%prog <username> [options]"
2337 Option("-H", "--URL", help="LDB URL for database or target server",
2338 type=str, metavar="URL", dest="H"),
2339 Option("--editor", help="Editor to use instead of the system default,"
2340 " or 'vi' if no system default is set.", type=str),
2343 takes_args = ["username"]
2344 takes_optiongroups = {
2345 "sambaopts": options.SambaOptions,
2346 "credopts": options.CredentialsOptions,
2347 "versionopts": options.VersionOptions,
2350 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2351 H=None, editor=None):
2353 lp = sambaopts.get_loadparm()
2354 creds = credopts.get_credentials(lp, fallback_machine=True)
2355 samdb = SamDB(url=H, session_info=system_session(),
2356 credentials=creds, lp=lp)
2358 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2359 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2361 domaindn = samdb.domain_dn()
2364 res = samdb.search(base=domaindn,
2366 scope=ldb.SCOPE_SUBTREE)
2369 raise CommandError('Unable to find user "%s"' % (username))
2372 r_ldif = samdb.write_ldif(msg, 1)
2373 # remove 'changetype' line
2374 result_ldif = re.sub('changetype: add\n', '', r_ldif)
2377 editor = os.environ.get('EDITOR')
2381 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2382 t_file.write(result_ldif)
2385 check_call([editor, t_file.name])
2386 except CalledProcessError as e:
2387 raise CalledProcessError("ERROR: ", e)
2388 with open(t_file.name) as edited_file:
2389 edited_message = edited_file.read()
2391 if result_ldif != edited_message:
2392 diff = difflib.ndiff(result_ldif.splitlines(),
2393 edited_message.splitlines())
2397 if line.startswith('-'):
2399 minus_lines.append(line)
2400 elif line.startswith('+'):
2402 plus_lines.append(line)
2404 user_ldif = "dn: %s\n" % user_dn
2405 user_ldif += "changetype: modify\n"
2407 for line in minus_lines:
2408 attr, val = line.split(':', 1)
2409 search_attr = "%s:" % attr
2410 if not re.search(r'^' + search_attr, str(plus_lines)):
2411 user_ldif += "delete: %s\n" % attr
2412 user_ldif += "%s: %s\n" % (attr, val)
2414 for line in plus_lines:
2415 attr, val = line.split(':', 1)
2416 search_attr = "%s:" % attr
2417 if re.search(r'^' + search_attr, str(minus_lines)):
2418 user_ldif += "replace: %s\n" % attr
2419 user_ldif += "%s: %s\n" % (attr, val)
2420 if not re.search(r'^' + search_attr, str(minus_lines)):
2421 user_ldif += "add: %s\n" % attr
2422 user_ldif += "%s: %s\n" % (attr, val)
2425 samdb.modify_ldif(user_ldif)
2426 except Exception as e:
2427 raise CommandError("Failed to modify user '%s': " %
2430 self.outf.write("Modified User '%s' successfully\n" % username)
2432 class cmd_user_show(Command):
2433 """Display a user AD object.
2435 This command displays a user account and it's attributes in the Active
2437 The username specified on the command is the sAMAccountName.
2439 The command may be run from the root userid or another authorized userid.
2441 The -H or --URL= option can be used to execute the command against a remote
2445 samba-tool user show User1 -H ldap://samba.samdom.example.com \
2446 -U administrator --password=passw1rd
2448 Example1 shows how to display a users attributes in the domain against a remote
2451 The -H parameter is used to specify the remote target server.
2454 samba-tool user show User2
2456 Example2 shows how to display a users attributes in the domain against a local
2460 samba-tool user show User2 --attributes=objectSid,memberOf
2462 Example3 shows how to display a users objectSid and memberOf attributes.
2464 synopsis = "%prog <username> [options]"
2467 Option("-H", "--URL", help="LDB URL for database or target server",
2468 type=str, metavar="URL", dest="H"),
2469 Option("--attributes",
2470 help=("Comma separated list of attributes, "
2471 "which will be printed."),
2472 type=str, dest="user_attrs"),
2475 takes_args = ["username"]
2476 takes_optiongroups = {
2477 "sambaopts": options.SambaOptions,
2478 "credopts": options.CredentialsOptions,
2479 "versionopts": options.VersionOptions,
2482 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2483 H=None, user_attrs=None):
2485 lp = sambaopts.get_loadparm()
2486 creds = credopts.get_credentials(lp, fallback_machine=True)
2487 samdb = SamDB(url=H, session_info=system_session(),
2488 credentials=creds, lp=lp)
2492 attrs = user_attrs.split(",")
2494 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2495 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2497 domaindn = samdb.domain_dn()
2500 res = samdb.search(base=domaindn, expression=filter,
2501 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2504 raise CommandError('Unable to find user "%s"' % (username))
2507 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2508 self.outf.write(user_ldif)
2510 class cmd_user_move(Command):
2511 """Move a user to an organizational unit/container.
2513 This command moves a user account into the specified organizational unit
2515 The username specified on the command is the sAMAccountName.
2516 The name of the organizational unit or container can be specified as a
2517 full DN or without the domainDN component.
2519 The command may be run from the root userid or another authorized userid.
2521 The -H or --URL= option can be used to execute the command against a remote
2525 samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
2526 -H ldap://samba.samdom.example.com -U administrator
2528 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2529 unit on a remote LDAP server.
2531 The -H parameter is used to specify the remote target server.
2534 samba-tool user move User1 CN=Users
2536 Example2 shows how to move a user User1 back into the CN=Users container
2537 on the local server.
2540 synopsis = "%prog <username> <new_parent_dn> [options]"
2543 Option("-H", "--URL", help="LDB URL for database or target server",
2544 type=str, metavar="URL", dest="H"),
2547 takes_args = ["username", "new_parent_dn"]
2548 takes_optiongroups = {
2549 "sambaopts": options.SambaOptions,
2550 "credopts": options.CredentialsOptions,
2551 "versionopts": options.VersionOptions,
2554 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2555 versionopts=None, H=None):
2556 lp = sambaopts.get_loadparm()
2557 creds = credopts.get_credentials(lp, fallback_machine=True)
2558 samdb = SamDB(url=H, session_info=system_session(),
2559 credentials=creds, lp=lp)
2560 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2562 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2563 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2565 res = samdb.search(base=domain_dn,
2567 scope=ldb.SCOPE_SUBTREE)
2570 raise CommandError('Unable to find user "%s"' % (username))
2573 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2574 except Exception as e:
2575 raise CommandError('Invalid new_parent_dn "%s": %s' %
2576 (new_parent_dn, e.message))
2578 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2579 full_new_user_dn.remove_base_components(len(user_dn) - 1)
2580 full_new_user_dn.add_base(full_new_parent_dn)
2583 samdb.rename(user_dn, full_new_user_dn)
2584 except Exception as e:
2585 raise CommandError('Failed to move user "%s"' % username, e)
2586 self.outf.write('Moved user "%s" into "%s"\n' %
2587 (username, full_new_parent_dn))
2589 class cmd_user(SuperCommand):
2590 """User management."""
2593 subcommands["add"] = cmd_user_add()
2594 subcommands["create"] = cmd_user_create()
2595 subcommands["delete"] = cmd_user_delete()
2596 subcommands["disable"] = cmd_user_disable()
2597 subcommands["enable"] = cmd_user_enable()
2598 subcommands["list"] = cmd_user_list()
2599 subcommands["setexpiry"] = cmd_user_setexpiry()
2600 subcommands["password"] = cmd_user_password()
2601 subcommands["setpassword"] = cmd_user_setpassword()
2602 subcommands["getpassword"] = cmd_user_getpassword()
2603 subcommands["syncpasswords"] = cmd_user_syncpasswords()
2604 subcommands["edit"] = cmd_user_edit()
2605 subcommands["show"] = cmd_user_show()
2606 subcommands["move"] = cmd_user_move()