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 (
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('+', '.')
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))" %
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 net.change_password(password.encode('utf-8'))
717 except Exception as msg:
718 # FIXME: catch more specific exception
719 raise CommandError("Failed to change password : %s" % msg)
720 self.outf.write("Changed password OK\n")
723 class cmd_user_setpassword(Command):
724 """Set or reset the password of a user account.
726 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.
728 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.
730 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.
732 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.
735 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
737 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.
740 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
742 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.
745 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
747 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
750 synopsis = "%prog (<username>|--filter <filter>) [options]"
752 takes_optiongroups = {
753 "sambaopts": options.SambaOptions,
754 "versionopts": options.VersionOptions,
755 "credopts": options.CredentialsOptions,
759 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
760 metavar="URL", dest="H"),
761 Option("--filter", help="LDAP Filter to set password on", type=str),
762 Option("--newpassword", help="Set password", type=str),
763 Option("--must-change-at-next-login",
764 help="Force password to be changed on next login",
765 action="store_true"),
766 Option("--random-password",
767 help="Generate random password",
768 action="store_true"),
769 Option("--smartcard-required",
770 help="Require a smartcard for interactive logons",
771 action="store_true"),
772 Option("--clear-smartcard-required",
773 help="Don't require a smartcard for interactive logons",
774 action="store_true"),
777 takes_args = ["username?"]
779 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
780 versionopts=None, H=None, newpassword=None,
781 must_change_at_next_login=False, random_password=False,
782 smartcard_required=False, clear_smartcard_required=False):
783 if filter is None and username is None:
784 raise CommandError("Either the username or '--filter' must be specified!")
786 password = newpassword
788 if smartcard_required:
789 if password is not None and password is not '':
790 raise CommandError('It is not allowed to specify '
792 'together with --smartcard-required.')
793 if must_change_at_next_login:
794 raise CommandError('It is not allowed to specify '
795 '--must-change-at-next-login '
796 'together with --smartcard-required.')
797 if clear_smartcard_required:
798 raise CommandError('It is not allowed to specify '
799 '--clear-smartcard-required '
800 'together with --smartcard-required.')
802 if random_password and not smartcard_required:
803 password = generate_random_password(128, 255)
806 if smartcard_required:
808 if password is not None and password is not '':
810 password = getpass("New Password: ")
811 passwordverify = getpass("Retype Password: ")
812 if not password == passwordverify:
814 self.outf.write("Sorry, passwords do not match.\n")
817 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
819 lp = sambaopts.get_loadparm()
820 creds = credopts.get_credentials(lp)
822 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
824 samdb = SamDB(url=H, session_info=system_session(),
825 credentials=creds, lp=lp)
827 if smartcard_required:
830 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
831 flags = dsdb.UF_SMARTCARD_REQUIRED
832 samdb.toggle_userAccountFlags(filter, flags, on=True)
833 command = "Failed to enable account for user '%s'" % (username or filter)
834 samdb.enable_account(filter)
835 except Exception as msg:
836 # FIXME: catch more specific exception
837 raise CommandError("%s: %s" % (command, msg))
838 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
842 if clear_smartcard_required:
843 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
844 flags = dsdb.UF_SMARTCARD_REQUIRED
845 samdb.toggle_userAccountFlags(filter, flags, on=False)
846 command = "Failed to set password for user '%s'" % (username or filter)
847 samdb.setpassword(filter, password,
848 force_change_at_next_login=must_change_at_next_login,
850 except Exception as msg:
851 # FIXME: catch more specific exception
852 raise CommandError("%s: %s" % (command, msg))
853 self.outf.write("Changed password OK\n")
855 class GetPasswordCommand(Command):
858 super(GetPasswordCommand, self).__init__()
861 def connect_system_samdb(self, url, allow_local=False, verbose=False):
863 # using anonymous here, results in no authentication
864 # which means we can get system privileges via
865 # the privileged ldapi socket
866 creds = credentials.Credentials()
867 creds.set_anonymous()
869 if url is None and allow_local:
871 elif url.lower().startswith("ldapi://"):
873 elif url.lower().startswith("ldap://"):
874 raise CommandError("--url ldap:// is not supported for this command")
875 elif url.lower().startswith("ldaps://"):
876 raise CommandError("--url ldaps:// is not supported for this command")
877 elif not allow_local:
878 raise CommandError("--url requires an ldapi:// url for this command")
881 self.outf.write("Connecting to '%s'\n" % url)
883 samdb = SamDB(url=url, session_info=system_session(),
884 credentials=creds, lp=self.lp)
888 # Make sure we're connected as SYSTEM
890 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
892 sids = res[0].get("tokenGroups")
893 assert len(sids) == 1
894 sid = ndr_unpack(security.dom_sid, sids[0])
895 assert str(sid) == security.SID_NT_SYSTEM
896 except Exception as msg:
897 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
898 (security.SID_NT_SYSTEM))
900 # We use sort here in order to have a predictable processing order
901 # this might not be strictly needed, but also doesn't hurt here
902 for a in sorted(virtual_attributes.keys()):
903 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
904 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
908 def get_account_attributes(self, samdb, username, basedn, filter, scope,
915 (attr, _, opts) = a.partition(';')
917 attr_opts[attr] = opts
919 attr_opts[attr] = None
920 search_attrs.append(attr)
921 lower_attrs = [x.lower() for x in search_attrs]
923 require_supplementalCredentials = False
924 for a in virtual_attributes.keys():
925 if a.lower() in lower_attrs:
926 require_supplementalCredentials = True
927 add_supplementalCredentials = False
928 add_unicodePwd = False
929 if require_supplementalCredentials:
930 a = "supplementalCredentials"
931 if a.lower() not in lower_attrs:
933 add_supplementalCredentials = True
935 if a.lower() not in lower_attrs:
937 add_unicodePwd = True
938 add_sAMAcountName = False
940 if a.lower() not in lower_attrs:
942 add_sAMAcountName = True
944 add_userPrincipalName = False
945 upn = "usePrincipalName"
946 if upn.lower() not in lower_attrs:
947 search_attrs += [upn]
948 add_userPrincipalName = True
950 if scope == ldb.SCOPE_BASE:
951 search_controls = ["show_deleted:1", "show_recycled:1"]
955 res = samdb.search(base=basedn, expression=filter,
956 scope=scope, attrs=search_attrs,
957 controls=search_controls)
959 raise Exception('Unable to find user "%s"' % (username or filter))
961 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
962 except Exception as msg:
963 # FIXME: catch more specific exception
964 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
969 if "supplementalCredentials" in obj:
970 sc_blob = obj["supplementalCredentials"][0]
971 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
972 if add_supplementalCredentials:
973 del obj["supplementalCredentials"]
974 if "unicodePwd" in obj:
975 unicodePwd = obj["unicodePwd"][0]
977 del obj["unicodePwd"]
978 account_name = obj["sAMAccountName"][0]
979 if add_sAMAcountName:
980 del obj["sAMAccountName"]
981 if "userPrincipalName" in obj:
982 account_upn = obj["userPrincipalName"][0]
984 realm = self.lp.get("realm")
985 account_upn = "%s@%s" % (account_name, realm.lower())
986 if add_userPrincipalName:
987 del obj["userPrincipalName"]
990 def get_package(name, min_idx=0):
991 if name in calculated:
992 return calculated[name]
996 min_idx = len(sc.sub.packages) + min_idx
998 for p in sc.sub.packages:
1005 return binascii.a2b_hex(p.data)
1010 # Samba adds 'Primary:SambaGPG' at the end.
1011 # When Windows sets the password it keeps
1012 # 'Primary:SambaGPG' and rotates it to
1013 # the begining. So we can only use the value,
1014 # if it is the last one.
1016 # In order to get more protection we verify
1017 # the nthash of the decrypted utf16 password
1018 # against the stored nthash in unicodePwd.
1020 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1021 if sgv is not None and unicodePwd is not None:
1022 ctx = gpgme.Context()
1024 cipher_io = io.BytesIO(sgv)
1025 plain_io = io.BytesIO()
1027 ctx.decrypt(cipher_io, plain_io)
1028 cv = plain_io.getvalue()
1030 # We only use the password if it matches
1031 # the current nthash stored in the unicodePwd
1034 tmp = credentials.Credentials()
1036 tmp.set_utf16_password(cv)
1037 nthash = tmp.get_nt_hash()
1038 if nthash == unicodePwd:
1039 calculated["Primary:CLEARTEXT"] = cv
1040 except gpgme.GpgmeError as e1:
1041 (major, minor, msg) = e1.args
1042 if major == gpgme.ERR_BAD_SECKEY:
1043 msg = "ERR_BAD_SECKEY: " + msg
1045 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1046 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1047 username or account_name, msg))
1049 def get_utf8(a, b, username):
1051 u = unicode(b, 'utf-16-le')
1052 except UnicodeDecodeError as e:
1053 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1056 u8 = u.encode('utf-8')
1059 # Extract the WDigest hash for the value specified by i.
1060 # Builds an htdigest compatible value
1062 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1063 domain, dns_domain):
1068 user = account_name.lower()
1069 realm = domain.lower()
1071 user = account_name.upper()
1072 realm = domain.upper()
1075 realm = domain.upper()
1078 realm = domain.lower()
1080 user = account_name.upper()
1081 realm = domain.lower()
1083 user = account_name.lower()
1084 realm = domain.upper()
1087 realm = dns_domain.lower()
1089 user = account_name.lower()
1090 realm = dns_domain.lower()
1092 user = account_name.upper()
1093 realm = dns_domain.upper()
1096 realm = dns_domain.upper()
1099 realm = dns_domain.lower()
1101 user = account_name.upper()
1102 realm = dns_domain.lower()
1104 user = account_name.lower()
1105 realm = dns_domain.upper()
1110 user = account_upn.lower()
1113 user = account_upn.upper()
1116 user = "%s\\%s" % (domain, account_name)
1119 user = "%s\\%s" % (domain.lower(), account_name.lower())
1122 user = "%s\\%s" % (domain.upper(), account_name.upper())
1128 user = account_name.lower()
1131 user = account_name.upper()
1137 user = account_upn.lower()
1140 user = account_upn.upper()
1143 user = "%s\\%s" % (domain, account_name)
1146 # Differs from spec, see tests
1147 user = "%s\\%s" % (domain.lower(), account_name.lower())
1150 # Differs from spec, see tests
1151 user = "%s\\%s" % (domain.upper(), account_name.upper())
1156 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1159 digest = binascii.hexlify(bytearray(digests.hashes[i-1].hash))
1160 return "%s:%s:%s" % (user, realm, digest)
1165 # get the value for a virtualCrypt attribute.
1166 # look for an exact match on algorithm and rounds in supplemental creds
1167 # if not found calculate using Primary:CLEARTEXT
1168 # if no Primary:CLEARTEXT return the first supplementalCredential
1169 # that matches the algorithm.
1170 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1173 b = get_package("Primary:userPassword")
1175 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1177 # No exact match on algorithm and number of rounds
1178 # try and calculate one from the Primary:CLEARTEXT
1179 b = get_package("Primary:CLEARTEXT")
1181 u8 = get_utf8(a, b, username or account_name)
1183 sv = get_crypt_value(str(algorithm), u8, rounds)
1185 # Unable to calculate a hash with the specified
1186 # number of rounds, fall back to the first hash using
1187 # the specified algorithm
1191 return "{CRYPT}" + sv
1193 def get_userPassword_hash(blob, algorithm, rounds):
1194 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1197 # Check that the NT hash has not been changed without updating
1198 # the user password hashes. This indicates that password has been
1199 # changed without updating the supplemental credentials.
1200 if unicodePwd != bytearray(up.current_nt_hash.hash):
1203 scheme_prefix = "$%d$" % algorithm
1204 prefix = scheme_prefix
1206 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1210 if (scheme_match is None and
1211 h.scheme == SCHEME and
1212 h.value.startswith(scheme_prefix)):
1213 scheme_match = h.value
1214 if h.scheme == SCHEME and h.value.startswith(prefix):
1215 return (h.value, scheme_match)
1217 # No match on the number of rounds, return the value of the
1218 # first matching scheme
1219 return (None, scheme_match)
1221 # We use sort here in order to have a predictable processing order
1222 for a in sorted(virtual_attributes.keys()):
1223 if not a.lower() in lower_attrs:
1226 if a == "virtualClearTextUTF8":
1227 b = get_package("Primary:CLEARTEXT")
1230 u8 = get_utf8(a, b, username or account_name)
1234 elif a == "virtualClearTextUTF16":
1235 v = get_package("Primary:CLEARTEXT")
1238 elif a == "virtualSSHA":
1239 b = get_package("Primary:CLEARTEXT")
1242 u8 = get_utf8(a, b, username or account_name)
1245 salt = get_random_bytes(4)
1249 bv = h.digest() + salt
1250 v = "{SSHA}" + base64.b64encode(bv)
1251 elif a == "virtualCryptSHA256":
1252 rounds = get_rounds(attr_opts[a])
1253 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1257 elif a == "virtualCryptSHA512":
1258 rounds = get_rounds(attr_opts[a])
1259 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1263 elif a == "virtualSambaGPG":
1264 # Samba adds 'Primary:SambaGPG' at the end.
1265 # When Windows sets the password it keeps
1266 # 'Primary:SambaGPG' and rotates it to
1267 # the begining. So we can only use the value,
1268 # if it is the last one.
1269 v = get_package("Primary:SambaGPG", min_idx=-1)
1272 elif a.startswith("virtualWDigest"):
1273 primary_wdigest = get_package("Primary:WDigest")
1274 if primary_wdigest is None:
1276 x = a[len("virtualWDigest"):]
1281 domain = self.lp.get("workgroup")
1282 dns_domain = samdb.domain_dns_name()
1283 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1288 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1291 def parse_attributes(self, attributes):
1293 if attributes is None:
1294 raise CommandError("Please specify --attributes")
1295 attrs = attributes.split(',')
1298 pa = pa.lstrip().rstrip()
1299 for da in disabled_virtual_attributes.keys():
1300 if pa.lower() == da.lower():
1301 r = disabled_virtual_attributes[da]["reason"]
1302 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1304 for va in virtual_attributes.keys():
1305 if pa.lower() == va.lower():
1306 # Take the real name
1309 password_attrs += [pa]
1311 return password_attrs
1313 class cmd_user_getpassword(GetPasswordCommand):
1314 """Get the password fields of a user/computer account.
1316 This command gets the logon password for a user/computer account.
1318 The username specified on the command is the sAMAccountName.
1319 The username may also be specified using the --filter option.
1321 The command must be run from the root user id or another authorized user id.
1322 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1323 used to adjust the local path. By default tdb:// is used by default.
1325 The '--attributes' parameter takes a comma separated list of attributes,
1326 which will be printed or given to the script specified by '--script'. If a
1327 specified attribute is not available on an object it's silently omitted.
1328 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1329 the NTHASH) and the following virtual attributes are possible (see --help
1330 for which virtual attributes are supported in your environment):
1332 virtualClearTextUTF16: The raw cleartext as stored in the
1333 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1334 with '--decrypt-samba-gpg') buffer inside of the
1335 supplementalCredentials attribute. This typically
1336 contains valid UTF-16-LE, but may contain random
1337 bytes, e.g. for computer accounts.
1339 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1340 (only from valid UTF-16-LE)
1342 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1343 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1345 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1346 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1347 with a $5$... salt, see crypt(3) on modern systems.
1348 The number of rounds used to calculate the hash can
1349 also be specified. By appending ";rounds=x" to the
1350 attribute name i.e. virtualCryptSHA256;rounds=10000
1351 will calculate a SHA256 hash with 10,000 rounds.
1352 non numeric values for rounds are silently ignored
1353 The value is calculated as follows:
1354 1) If a value exists in 'Primary:userPassword' with
1355 the specified number of rounds it is returned.
1356 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1357 '--decrypt-samba-gpg'. Calculate a hash with
1358 the specified number of rounds
1359 3) Return the first CryptSHA256 value in
1360 'Primary:userPassword'
1363 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1364 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1365 with a $6$... salt, see crypt(3) on modern systems.
1366 The number of rounds used to calculate the hash can
1367 also be specified. By appending ";rounds=x" to the
1368 attribute name i.e. virtualCryptSHA512;rounds=10000
1369 will calculate a SHA512 hash with 10,000 rounds.
1370 non numeric values for rounds are silently ignored
1371 The value is calculated as follows:
1372 1) If a value exists in 'Primary:userPassword' with
1373 the specified number of rounds it is returned.
1374 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1375 '--decrypt-samba-gpg'. Calculate a hash with
1376 the specified number of rounds
1377 3) Return the first CryptSHA512 value in
1378 'Primary:userPassword'
1380 virtualWDigestNN: The individual hash values stored in
1381 'Primary:WDigest' where NN is the hash number in
1383 NOTE: As at 22-05-2017 the documentation:
1384 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1385 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1388 virtualSambaGPG: The raw cleartext as stored in the
1389 'Primary:SambaGPG' buffer inside of the
1390 supplementalCredentials attribute.
1391 See the 'password hash gpg key ids' option in
1394 The '--decrypt-samba-gpg' option triggers decryption of the
1395 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1396 in your environment or not (the python-gpgme package is required). Please
1397 note that you might need to set the GNUPGHOME environment variable. If the
1398 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1399 environment variable has been set correctly and the passphrase is already
1400 known by the gpg-agent.
1403 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1406 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1410 super(cmd_user_getpassword, self).__init__()
1412 synopsis = "%prog (<username>|--filter <filter>) [options]"
1414 takes_optiongroups = {
1415 "sambaopts": options.SambaOptions,
1416 "versionopts": options.VersionOptions,
1420 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1421 metavar="URL", dest="H"),
1422 Option("--filter", help="LDAP Filter to set password on", type=str),
1423 Option("--attributes", type=str,
1424 help=virtual_attributes_help,
1425 metavar="ATTRIBUTELIST", dest="attributes"),
1426 Option("--decrypt-samba-gpg",
1427 help=decrypt_samba_gpg_help,
1428 action="store_true", default=False, dest="decrypt_samba_gpg"),
1431 takes_args = ["username?"]
1433 def run(self, username=None, H=None, filter=None,
1434 attributes=None, decrypt_samba_gpg=None,
1435 sambaopts=None, versionopts=None):
1436 self.lp = sambaopts.get_loadparm()
1438 if decrypt_samba_gpg and not gpgme_support:
1439 raise CommandError(decrypt_samba_gpg_help)
1441 if filter is None and username is None:
1442 raise CommandError("Either the username or '--filter' must be specified!")
1445 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1447 if attributes is None:
1448 raise CommandError("Please specify --attributes")
1450 password_attrs = self.parse_attributes(attributes)
1452 samdb = self.connect_system_samdb(url=H, allow_local=True)
1454 obj = self.get_account_attributes(samdb, username,
1457 scope=ldb.SCOPE_SUBTREE,
1458 attrs=password_attrs,
1459 decrypt=decrypt_samba_gpg)
1461 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1462 self.outf.write("%s" % ldif)
1463 self.outf.write("Got password OK\n")
1465 class cmd_user_syncpasswords(GetPasswordCommand):
1466 """Sync the password of user accounts.
1468 This syncs logon passwords for user accounts.
1470 Note that this command should run on a single domain controller only
1471 (typically the PDC-emulator). However the "password hash gpg key ids"
1472 option should to be configured on all domain controllers.
1474 The command must be run from the root user id or another authorized user id.
1475 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1476 local path. By default, ldapi:// is used with the default path to the
1477 privileged ldapi socket.
1479 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1480 "Sync Loop Terminate".
1483 Cache Initialization
1484 ====================
1486 The first time, this command needs to be called with
1487 '--cache-ldb-initialize' in order to initialize its cache.
1489 The cache initialization requires '--attributes' and allows the following
1490 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1493 The '--attributes' parameter takes a comma separated list of attributes,
1494 which will be printed or given to the script specified by '--script'. If a
1495 specified attribute is not available on an object it will be silently omitted.
1496 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1497 the NTHASH) and the following virtual attributes are possible (see '--help'
1498 for supported virtual attributes in your environment):
1500 virtualClearTextUTF16: The raw cleartext as stored in the
1501 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1502 with '--decrypt-samba-gpg') buffer inside of the
1503 supplementalCredentials attribute. This typically
1504 contains valid UTF-16-LE, but may contain random
1505 bytes, e.g. for computer accounts.
1507 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1508 (only from valid UTF-16-LE)
1510 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1511 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1513 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1514 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1515 with a $5$... salt, see crypt(3) on modern systems.
1516 The number of rounds used to calculate the hash can
1517 also be specified. By appending ";rounds=x" to the
1518 attribute name i.e. virtualCryptSHA256;rounds=10000
1519 will calculate a SHA256 hash with 10,000 rounds.
1520 non numeric values for rounds are silently ignored
1521 The value is calculated as follows:
1522 1) If a value exists in 'Primary:userPassword' with
1523 the specified number of rounds it is returned.
1524 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1525 '--decrypt-samba-gpg'. Calculate a hash with
1526 the specified number of rounds
1527 3) Return the first CryptSHA256 value in
1528 'Primary:userPassword'
1530 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1531 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1532 with a $6$... salt, see crypt(3) on modern systems.
1533 The number of rounds used to calculate the hash can
1534 also be specified. By appending ";rounds=x" to the
1535 attribute name i.e. virtualCryptSHA512;rounds=10000
1536 will calculate a SHA512 hash with 10,000 rounds.
1537 non numeric values for rounds are silently ignored
1538 The value is calculated as follows:
1539 1) If a value exists in 'Primary:userPassword' with
1540 the specified number of rounds it is returned.
1541 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1542 '--decrypt-samba-gpg'. Calculate a hash with
1543 the specified number of rounds
1544 3) Return the first CryptSHA512 value in
1545 'Primary:userPassword'
1547 virtualWDigestNN: The individual hash values stored in
1548 'Primary:WDigest' where NN is the hash number in
1550 NOTE: As at 22-05-2017 the documentation:
1551 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1552 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1555 virtualSambaGPG: The raw cleartext as stored in the
1556 'Primary:SambaGPG' buffer inside of the
1557 supplementalCredentials attribute.
1558 See the 'password hash gpg key ids' option in
1561 The '--decrypt-samba-gpg' option triggers decryption of the
1562 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1563 in your environment or not (the python-gpgme package is required). Please
1564 note that you might need to set the GNUPGHOME environment variable. If the
1565 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1566 environment variable has been set correctly and the passphrase is already
1567 known by the gpg-agent.
1569 The '--script' option specifies a custom script that is called whenever any
1570 of the dirsyncAttributes (see below) was changed. The script is called
1571 without any arguments. It gets the LDIF for exactly one object on STDIN.
1572 If the script processed the object successfully it has to respond with a
1573 single line starting with 'DONE-EXIT: ' followed by an optional message.
1575 Note that the script might be called without any password change, e.g. if
1576 the account was disabled (a userAccountControl change) or the
1577 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1578 are always returned as unique identifier of the account. It might be useful
1579 to also ask for non-password attributes like: objectSid, sAMAccountName,
1580 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1581 Depending on the object, some attributes may not be present/available,
1582 but you always get the current state (and not a diff).
1584 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1587 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1588 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1589 (!(sAMAccountName=krbtgt*)))
1590 This means only normal (non-krbtgt) user
1591 accounts are monitored. The '--filter' can modify that, e.g. if it's
1592 required to also sync computer accounts.
1598 This (default) mode runs in an endless loop waiting for password related
1599 changes in the active directory database. It makes use of the
1600 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1601 get changes in a reliable fashion. Objects are monitored for changes of the
1602 following dirsyncAttributes:
1604 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1605 userPrincipalName and userAccountControl.
1607 It recovers from LDAP disconnects and updates the cache in conservative way
1608 (in single steps after each succesfully processed change). An error from
1609 the script (specified by '--script') will result in fatal error and this
1610 command will exit. But the cache state should be still valid and can be
1611 resumed in the next "Sync Loop Run".
1613 The '--logfile' option specifies an optional (required if '--daemon' is
1614 specified) logfile that takes all output of the command. The logfile is
1615 automatically reopened if fstat returns st_nlink == 0.
1617 The optional '--daemon' option will put the command into the background.
1619 You can stop the command without the '--daemon' option, also by hitting
1622 If you specify the '--no-wait' option the command skips the
1623 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1624 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1629 In order to terminate an already running command (likely as daemon) the
1630 '--terminate' option can be used. This also requires the '--logfile' option
1635 samba-tool user syncpasswords --cache-ldb-initialize \\
1636 --attributes=virtualClearTextUTF8
1637 samba-tool user syncpasswords
1640 samba-tool user syncpasswords --cache-ldb-initialize \\
1641 --attributes=objectGUID,objectSID,sAMAccountName,\\
1642 userPrincipalName,userAccountControl,pwdLastSet,\\
1643 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1644 --script=/path/to/my-custom-syncpasswords-script.py
1645 samba-tool user syncpasswords --daemon \\
1646 --logfile=/var/log/samba/user-syncpasswords.log
1647 samba-tool user syncpasswords --terminate \\
1648 --logfile=/var/log/samba/user-syncpasswords.log
1652 super(cmd_user_syncpasswords, self).__init__()
1654 synopsis = "%prog [--cache-ldb-initialize] [options]"
1656 takes_optiongroups = {
1657 "sambaopts": options.SambaOptions,
1658 "versionopts": options.VersionOptions,
1662 Option("--cache-ldb-initialize",
1663 help="Initialize the cache for the first time",
1664 dest="cache_ldb_initialize", action="store_true"),
1665 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1666 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1667 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1668 metavar="URL", dest="H"),
1669 Option("--filter", help="optional LDAP filter to set password on", type=str,
1670 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1671 Option("--attributes", type=str,
1672 help=virtual_attributes_help,
1673 metavar="ATTRIBUTELIST", dest="attributes"),
1674 Option("--decrypt-samba-gpg",
1675 help=decrypt_samba_gpg_help,
1676 action="store_true", default=False, dest="decrypt_samba_gpg"),
1677 Option("--script", help="Script that is called for each password change", type=str,
1678 metavar="/path/to/syncpasswords.script", dest="script"),
1679 Option("--no-wait", help="Don't block waiting for changes",
1680 action="store_true", default=False, dest="nowait"),
1681 Option("--logfile", type=str,
1682 help="The logfile to use (required in --daemon mode).",
1683 metavar="/path/to/syncpasswords.log", dest="logfile"),
1684 Option("--daemon", help="daemonize after initial setup",
1685 action="store_true", default=False, dest="daemon"),
1686 Option("--terminate",
1687 help="Send a SIGTERM to an already running (daemon) process",
1688 action="store_true", default=False, dest="terminate"),
1691 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1692 H=None, filter=None,
1693 attributes=None, decrypt_samba_gpg=None,
1694 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1695 sambaopts=None, versionopts=None):
1697 self.lp = sambaopts.get_loadparm()
1699 self.samdb_url = None
1703 if not cache_ldb_initialize:
1704 if attributes is not None:
1705 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1706 if decrypt_samba_gpg:
1707 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1708 if script is not None:
1709 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1710 if filter is not None:
1711 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1713 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1715 if nowait is not False:
1716 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1717 if logfile is not None:
1718 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1719 if daemon is not False:
1720 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1721 if terminate is not False:
1722 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1726 raise CommandError("--daemon is not allowed together with --no-wait")
1727 if terminate is not False:
1728 raise CommandError("--terminate is not allowed together with --no-wait")
1730 if terminate is True and daemon is True:
1731 raise CommandError("--terminate is not allowed together with --daemon")
1733 if daemon is True and logfile is None:
1734 raise CommandError("--daemon is only allowed together with --logfile")
1736 if terminate is True and logfile is None:
1737 raise CommandError("--terminate is only allowed together with --logfile")
1739 if script is not None:
1740 if not os.path.exists(script):
1741 raise CommandError("script[%s] does not exist!" % script)
1743 sync_command = "%s" % os.path.abspath(script)
1747 dirsync_filter = filter
1748 if dirsync_filter is None:
1749 dirsync_filter = "(&" + \
1750 "(objectClass=user)" + \
1751 "(userAccountControl:%s:=%u)" % (
1752 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1753 "(!(sAMAccountName=krbtgt*))" + \
1756 dirsync_secret_attrs = [
1759 "supplementalCredentials",
1762 dirsync_attrs = dirsync_secret_attrs + [
1765 "userPrincipalName",
1766 "userAccountControl",
1771 password_attrs = None
1773 if cache_ldb_initialize:
1775 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1777 if decrypt_samba_gpg and not gpgme_support:
1778 raise CommandError(decrypt_samba_gpg_help)
1780 password_attrs = self.parse_attributes(attributes)
1781 lower_attrs = [x.lower() for x in password_attrs]
1782 # We always return these in order to track deletions
1783 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1784 if a.lower() not in lower_attrs:
1785 password_attrs += [a]
1787 if cache_ldb is not None:
1788 if cache_ldb.lower().startswith("ldapi://"):
1789 raise CommandError("--cache_ldb ldapi:// is not supported")
1790 elif cache_ldb.lower().startswith("ldap://"):
1791 raise CommandError("--cache_ldb ldap:// is not supported")
1792 elif cache_ldb.lower().startswith("ldaps://"):
1793 raise CommandError("--cache_ldb ldaps:// is not supported")
1794 elif cache_ldb.lower().startswith("tdb://"):
1797 if not os.path.exists(cache_ldb):
1798 cache_ldb = self.lp.private_path(cache_ldb)
1800 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1802 self.lockfile = "%s.pid" % cache_ldb
1805 if self.logfile is not None:
1807 if info.st_nlink == 0:
1808 logfile = self.logfile
1810 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1811 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
1816 log_msg("Reopened logfile[%s]\n" % (logfile))
1817 self.logfile = logfile
1818 msg = "%s: pid[%d]: %s" % (
1822 self.outf.write(msg)
1831 "passwordAttribute",
1837 self.cache = Ldb(cache_ldb)
1838 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1839 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1843 self.samdb_url = res[0]["samdbUrl"][0]
1844 except KeyError as e:
1845 self.samdb_url = None
1847 self.samdb_url = None
1848 if self.samdb_url is None and not cache_ldb_initialize:
1849 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1851 if self.samdb_url is not None and cache_ldb_initialize:
1852 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1854 if self.samdb_url is None:
1856 self.dirsync_filter = dirsync_filter
1857 self.dirsync_attrs = dirsync_attrs
1858 self.dirsync_controls = ["dirsync:1:0:0","extended_dn:1:0"];
1859 self.password_attrs = password_attrs
1860 self.decrypt_samba_gpg = decrypt_samba_gpg
1861 self.sync_command = sync_command
1862 add_ldif = "dn: %s\n" % self.cache_dn
1863 add_ldif += "objectClass: userSyncPasswords\n"
1864 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url)
1865 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter)
1866 for a in self.dirsync_attrs:
1867 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a)
1868 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1869 for a in self.password_attrs:
1870 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
1871 if self.decrypt_samba_gpg == True:
1872 add_ldif += "decryptSambaGPG: TRUE\n"
1874 add_ldif += "decryptSambaGPG: FALSE\n"
1875 if self.sync_command is not None:
1876 add_ldif += "syncCommand: %s\n" % self.sync_command
1877 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1878 self.cache.add_ldif(add_ldif)
1879 self.current_pid = None
1880 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1881 msgs = self.cache.parse_ldif(add_ldif)
1882 changetype,msg = msgs.next()
1883 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1884 self.outf.write("%s" % ldif)
1886 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1887 self.dirsync_attrs = []
1888 for a in res[0]["dirsyncAttribute"]:
1889 self.dirsync_attrs.append(a)
1890 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1891 self.password_attrs = []
1892 for a in res[0]["passwordAttribute"]:
1893 self.password_attrs.append(a)
1894 decrypt_string = res[0]["decryptSambaGPG"][0]
1895 assert(decrypt_string in ["TRUE", "FALSE"])
1896 if decrypt_string == "TRUE":
1897 self.decrypt_samba_gpg = True
1899 self.decrypt_samba_gpg = False
1900 if "syncCommand" in res[0]:
1901 self.sync_command = res[0]["syncCommand"][0]
1903 self.sync_command = None
1904 if "currentPid" in res[0]:
1905 self.current_pid = int(res[0]["currentPid"][0])
1907 self.current_pid = None
1908 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1912 def run_sync_command(dn, ldif):
1913 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1914 sync_command_p = Popen(self.sync_command,
1919 res = sync_command_p.poll()
1922 input = "%s" % (ldif)
1923 reply = sync_command_p.communicate(input)[0]
1924 log_msg("%s\n" % (reply))
1925 res = sync_command_p.poll()
1927 sync_command_p.terminate()
1928 res = sync_command_p.wait()
1930 if reply.startswith("DONE-EXIT: "):
1933 log_msg("RESULT: %s\n" % (res))
1934 raise Exception("ERROR: %s - %s\n" % (res, reply))
1936 def handle_object(idx, dirsync_obj):
1937 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1938 guid = ndr_unpack(misc.GUID, binary_guid)
1939 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1940 sid = ndr_unpack(security.dom_sid, binary_sid)
1941 domain_sid, rid = sid.split()
1942 if rid == security.DOMAIN_RID_KRBTGT:
1943 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1945 for a in list(dirsync_obj.keys()):
1946 for h in dirsync_secret_attrs:
1947 if a.lower() == h.lower():
1949 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1950 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1951 log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif))
1952 obj = self.get_account_attributes(self.samdb,
1953 username="%s" % sid,
1954 basedn="<GUID=%s>" % guid,
1955 filter="(objectClass=user)",
1956 scope=ldb.SCOPE_BASE,
1957 attrs=self.password_attrs,
1958 decrypt=self.decrypt_samba_gpg)
1959 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1960 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1961 if self.sync_command is None:
1962 self.outf.write("%s" % (ldif))
1964 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1965 run_sync_command(obj.dn, ldif)
1967 def check_current_pid_conflict(terminate):
1973 self.lockfd = os.open(self.lockfile, flags, 0o600)
1974 except IOError as e4:
1975 (err, msg) = e4.args
1976 if err == errno.ENOENT:
1979 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1980 (self.lockfile, msg, err))
1983 got_exclusive = False
1985 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1986 got_exclusive = True
1987 except IOError as e5:
1988 (err, msg) = e5.args
1989 if err != errno.EACCES and err != errno.EAGAIN:
1990 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
1991 (self.lockfile, msg, err))
1994 if not got_exclusive:
1995 buf = os.read(self.lockfd, 64)
1996 self.current_pid = None
1998 self.current_pid = int(buf)
1999 except ValueError as e:
2001 if self.current_pid is not None:
2004 if got_exclusive and terminate:
2006 os.ftruncate(self.lockfd, 0)
2007 except IOError as e2:
2008 (err, msg) = e2.args
2009 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2010 (self.lockfile, msg, err))
2012 os.close(self.lockfd)
2017 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2018 except IOError as e6:
2019 (err, msg) = e6.args
2020 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2021 (self.lockfile, msg, err))
2023 # We leave the function with the shared lock.
2026 def update_pid(pid):
2027 if self.lockfd != -1:
2028 got_exclusive = False
2029 # Try 5 times to get the exclusiv lock.
2030 for i in xrange(0, 5):
2032 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2033 got_exclusive = True
2034 except IOError as e:
2036 if err != errno.EACCES and err != errno.EAGAIN:
2037 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2038 (pid, self.lockfile, msg, err))
2043 if not got_exclusive:
2044 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
2045 (pid, self.lockfile))
2046 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2047 (pid, self.lockfile))
2054 os.ftruncate(self.lockfd, 0)
2056 os.write(self.lockfd, buf)
2057 except IOError as e3:
2058 (err, msg) = e3.args
2059 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2060 (self.lockfile, msg, err))
2062 self.current_pid = pid
2063 if self.current_pid is not None:
2064 log_msg("currentPid: %d\n" % self.current_pid)
2066 modify_ldif = "dn: %s\n" % (self.cache_dn)
2067 modify_ldif += "changetype: modify\n"
2068 modify_ldif += "replace: currentPid\n"
2069 if self.current_pid is not None:
2070 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2071 modify_ldif += "replace: currentTime\n"
2072 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2073 self.cache.modify_ldif(modify_ldif)
2076 def update_cache(res_controls):
2077 assert len(res_controls) > 0
2078 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2079 res_controls[0].critical = True
2080 self.dirsync_controls = [str(res_controls[0]),"extended_dn:1:0"]
2081 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2083 modify_ldif = "dn: %s\n" % (self.cache_dn)
2084 modify_ldif += "changetype: modify\n"
2085 modify_ldif += "replace: dirsyncControl\n"
2086 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2087 modify_ldif += "replace: currentTime\n"
2088 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2089 self.cache.modify_ldif(modify_ldif)
2092 def check_object(dirsync_obj, res_controls):
2093 assert len(res_controls) > 0
2094 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2096 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2097 sid = ndr_unpack(security.dom_sid, binary_sid)
2099 lastCookie = str(res_controls[0])
2101 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2102 expression="(lastCookie=%s)" % (
2103 ldb.binary_encode(lastCookie)),
2109 def update_object(dirsync_obj, res_controls):
2110 assert len(res_controls) > 0
2111 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2113 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2114 sid = ndr_unpack(security.dom_sid, binary_sid)
2116 lastCookie = str(res_controls[0])
2118 self.cache.transaction_start()
2120 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2121 expression="(objectClass=*)",
2122 attrs=["lastCookie"])
2124 add_ldif = "dn: %s\n" % (dn)
2125 add_ldif += "objectClass: userCookie\n"
2126 add_ldif += "lastCookie: %s\n" % (lastCookie)
2127 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2128 self.cache.add_ldif(add_ldif)
2130 modify_ldif = "dn: %s\n" % (dn)
2131 modify_ldif += "changetype: modify\n"
2132 modify_ldif += "replace: lastCookie\n"
2133 modify_ldif += "lastCookie: %s\n" % (lastCookie)
2134 modify_ldif += "replace: currentTime\n"
2135 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2136 self.cache.modify_ldif(modify_ldif)
2137 self.cache.transaction_commit()
2138 except Exception as e:
2139 self.cache.transaction_cancel()
2145 res = self.samdb.search(expression=self.dirsync_filter,
2146 scope=ldb.SCOPE_SUBTREE,
2147 attrs=self.dirsync_attrs,
2148 controls=self.dirsync_controls)
2149 log_msg("dirsync_loop(): results %d\n" % len(res))
2152 done = check_object(r, res.controls)
2154 handle_object(ri, r)
2155 update_object(r, res.controls)
2157 update_cache(res.controls)
2161 def sync_loop(wait):
2162 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2163 notify_controls = ["notification:1", "show_recycled:1"]
2164 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2165 scope=ldb.SCOPE_SUBTREE,
2167 controls=notify_controls,
2171 log_msg("Resuming monitoring\n")
2173 log_msg("Getting changes\n")
2174 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2175 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2176 self.outf.write("syncCommand: %s\n" % self.sync_command)
2179 if wait is not True:
2182 for msg in notify_handle:
2183 if not isinstance(msg, ldb.Message):
2184 self.outf.write("referal: %s\n" % msg)
2186 created = msg.get("uSNCreated")[0]
2187 changed = msg.get("uSNChanged")[0]
2188 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2189 (msg.dn, created, changed))
2193 res = notify_handle.result()
2198 orig_pid = os.getpid()
2203 if pid == 0: # Actual daemon
2205 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2210 if cache_ldb_initialize:
2212 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2217 if logfile is not None:
2218 import resource # Resource usage information.
2219 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2220 if maxfd == resource.RLIM_INFINITY:
2221 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2222 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2223 self.outf.write("Using logfile[%s]\n" % logfile)
2224 for fd in range(0, maxfd):
2235 log_msg("Attached to logfile[%s]\n" % (logfile))
2236 self.logfile = logfile
2239 conflict = check_current_pid_conflict(terminate)
2241 if self.current_pid is None:
2242 log_msg("No process running.\n")
2245 log_msg("Proccess %d is not running anymore.\n" % (
2249 log_msg("Sending SIGTERM to proccess %d.\n" % (
2251 os.kill(self.current_pid, signal.SIGTERM)
2254 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2255 os.getpid(), self.current_pid))
2259 update_pid(os.getpid())
2264 retry_sleep_max = 600
2269 retry_sleep = retry_sleep_min
2271 while self.samdb is None:
2272 if retry_sleep != 0:
2273 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2274 time.sleep(retry_sleep)
2275 retry_sleep = retry_sleep * 2
2276 if retry_sleep >= retry_sleep_max:
2277 retry_sleep = retry_sleep_max
2278 log_msg("Connecting to '%s'\n" % self.samdb_url)
2280 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2281 except Exception as msg:
2283 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2284 if wait is not True:
2289 except ldb.LdbError as e7:
2290 (enum, estr) = e7.args
2292 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2297 class cmd_user_edit(Command):
2298 """Modify User AD object.
2300 This command will allow editing of a user account in the Active Directory
2301 domain. You will then be able to add or change attributes and their values.
2303 The username specified on the command is the sAMAccountName.
2305 The command may be run from the root userid or another authorized userid.
2307 The -H or --URL= option can be used to execute the command against a remote
2311 samba-tool user edit User1 -H ldap://samba.samdom.example.com \
2312 -U administrator --password=passw1rd
2314 Example1 shows how to edit a users attributes in the domain against a remote
2317 The -H parameter is used to specify the remote target server.
2320 samba-tool user edit User2
2322 Example2 shows how to edit a users attributes in the domain against a local
2326 samba-tool user edit User3 --editor=nano
2328 Example3 shows how to edit a users attributes in the domain against a local
2329 LDAP server using the 'nano' editor.
2332 synopsis = "%prog <username> [options]"
2335 Option("-H", "--URL", help="LDB URL for database or target server",
2336 type=str, metavar="URL", dest="H"),
2337 Option("--editor", help="Editor to use instead of the system default,"
2338 " or 'vi' if no system default is set.", type=str),
2341 takes_args = ["username"]
2342 takes_optiongroups = {
2343 "sambaopts": options.SambaOptions,
2344 "credopts": options.CredentialsOptions,
2345 "versionopts": options.VersionOptions,
2348 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2349 H=None, editor=None):
2351 lp = sambaopts.get_loadparm()
2352 creds = credopts.get_credentials(lp, fallback_machine=True)
2353 samdb = SamDB(url=H, session_info=system_session(),
2354 credentials=creds, lp=lp)
2356 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2357 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2359 domaindn = samdb.domain_dn()
2362 res = samdb.search(base=domaindn,
2364 scope=ldb.SCOPE_SUBTREE)
2367 raise CommandError('Unable to find user "%s"' % (username))
2370 r_ldif = samdb.write_ldif(msg, 1)
2371 # remove 'changetype' line
2372 result_ldif = re.sub('changetype: add\n', '', r_ldif)
2375 editor = os.environ.get('EDITOR')
2379 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2380 t_file.write(result_ldif)
2383 check_call([editor, t_file.name])
2384 except CalledProcessError as e:
2385 raise CalledProcessError("ERROR: ", e)
2386 with open(t_file.name) as edited_file:
2387 edited_message = edited_file.read()
2389 if result_ldif != edited_message:
2390 diff = difflib.ndiff(result_ldif.splitlines(),
2391 edited_message.splitlines())
2395 if line.startswith('-'):
2397 minus_lines.append(line)
2398 elif line.startswith('+'):
2400 plus_lines.append(line)
2402 user_ldif="dn: %s\n" % user_dn
2403 user_ldif += "changetype: modify\n"
2405 for line in minus_lines:
2406 attr, val = line.split(':', 1)
2407 search_attr="%s:" % attr
2408 if not re.search(r'^' + search_attr, str(plus_lines)):
2409 user_ldif += "delete: %s\n" % attr
2410 user_ldif += "%s: %s\n" % (attr, val)
2412 for line in plus_lines:
2413 attr, val = line.split(':', 1)
2414 search_attr="%s:" % attr
2415 if re.search(r'^' + search_attr, str(minus_lines)):
2416 user_ldif += "replace: %s\n" % attr
2417 user_ldif += "%s: %s\n" % (attr, val)
2418 if not re.search(r'^' + search_attr, str(minus_lines)):
2419 user_ldif += "add: %s\n" % attr
2420 user_ldif += "%s: %s\n" % (attr, val)
2423 samdb.modify_ldif(user_ldif)
2424 except Exception as e:
2425 raise CommandError("Failed to modify user '%s': " %
2428 self.outf.write("Modified User '%s' successfully\n" % username)
2430 class cmd_user_show(Command):
2431 """Display a user AD object.
2433 This command displays a user account and it's attributes in the Active
2435 The username specified on the command is the sAMAccountName.
2437 The command may be run from the root userid or another authorized userid.
2439 The -H or --URL= option can be used to execute the command against a remote
2443 samba-tool user show User1 -H ldap://samba.samdom.example.com \
2444 -U administrator --password=passw1rd
2446 Example1 shows how to display a users attributes in the domain against a remote
2449 The -H parameter is used to specify the remote target server.
2452 samba-tool user show User2
2454 Example2 shows how to display a users attributes in the domain against a local
2458 samba-tool user show User2 --attributes=objectSid,memberOf
2460 Example3 shows how to display a users objectSid and memberOf attributes.
2462 synopsis = "%prog <username> [options]"
2465 Option("-H", "--URL", help="LDB URL for database or target server",
2466 type=str, metavar="URL", dest="H"),
2467 Option("--attributes",
2468 help=("Comma separated list of attributes, "
2469 "which will be printed."),
2470 type=str, dest="user_attrs"),
2473 takes_args = ["username"]
2474 takes_optiongroups = {
2475 "sambaopts": options.SambaOptions,
2476 "credopts": options.CredentialsOptions,
2477 "versionopts": options.VersionOptions,
2480 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2481 H=None, user_attrs=None):
2483 lp = sambaopts.get_loadparm()
2484 creds = credopts.get_credentials(lp, fallback_machine=True)
2485 samdb = SamDB(url=H, session_info=system_session(),
2486 credentials=creds, lp=lp)
2490 attrs = user_attrs.split(",")
2492 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2493 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2495 domaindn = samdb.domain_dn()
2498 res = samdb.search(base=domaindn, expression=filter,
2499 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2502 raise CommandError('Unable to find user "%s"' % (username))
2505 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2506 self.outf.write(user_ldif)
2508 class cmd_user_move(Command):
2509 """Move a user to an organizational unit/container.
2511 This command moves a user account into the specified organizational unit
2513 The username specified on the command is the sAMAccountName.
2514 The name of the organizational unit or container can be specified as a
2515 full DN or without the domainDN component.
2517 The command may be run from the root userid or another authorized userid.
2519 The -H or --URL= option can be used to execute the command against a remote
2523 samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
2524 -H ldap://samba.samdom.example.com -U administrator
2526 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2527 unit on a remote LDAP server.
2529 The -H parameter is used to specify the remote target server.
2532 samba-tool user move User1 CN=Users
2534 Example2 shows how to move a user User1 back into the CN=Users container
2535 on the local server.
2538 synopsis = "%prog <username> <new_parent_dn> [options]"
2541 Option("-H", "--URL", help="LDB URL for database or target server",
2542 type=str, metavar="URL", dest="H"),
2545 takes_args = [ "username", "new_parent_dn" ]
2546 takes_optiongroups = {
2547 "sambaopts": options.SambaOptions,
2548 "credopts": options.CredentialsOptions,
2549 "versionopts": options.VersionOptions,
2552 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2553 versionopts=None, H=None):
2554 lp = sambaopts.get_loadparm()
2555 creds = credopts.get_credentials(lp, fallback_machine=True)
2556 samdb = SamDB(url=H, session_info=system_session(),
2557 credentials=creds, lp=lp)
2558 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2560 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2561 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2563 res = samdb.search(base=domain_dn,
2565 scope=ldb.SCOPE_SUBTREE)
2568 raise CommandError('Unable to find user "%s"' % (username))
2571 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2572 except Exception as e:
2573 raise CommandError('Invalid new_parent_dn "%s": %s' %
2574 (new_parent_dn, e.message))
2576 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2577 full_new_user_dn.remove_base_components(len(user_dn)-1)
2578 full_new_user_dn.add_base(full_new_parent_dn)
2581 samdb.rename(user_dn, full_new_user_dn)
2582 except Exception as e:
2583 raise CommandError('Failed to move user "%s"' % username, e)
2584 self.outf.write('Moved user "%s" into "%s"\n' %
2585 (username, full_new_parent_dn))
2587 class cmd_user(SuperCommand):
2588 """User management."""
2591 subcommands["add"] = cmd_user_add()
2592 subcommands["create"] = cmd_user_create()
2593 subcommands["delete"] = cmd_user_delete()
2594 subcommands["disable"] = cmd_user_disable()
2595 subcommands["enable"] = cmd_user_enable()
2596 subcommands["list"] = cmd_user_list()
2597 subcommands["setexpiry"] = cmd_user_setexpiry()
2598 subcommands["password"] = cmd_user_password()
2599 subcommands["setpassword"] = cmd_user_setpassword()
2600 subcommands["getpassword"] = cmd_user_getpassword()
2601 subcommands["syncpasswords"] = cmd_user_syncpasswords()
2602 subcommands["edit"] = cmd_user_edit()
2603 subcommands["show"] = cmd_user_show()
2604 subcommands["move"] = cmd_user_move()