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 (major, minor, msg):
1041 if major == gpgme.ERR_BAD_SECKEY:
1042 msg = "ERR_BAD_SECKEY: " + msg
1044 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
1045 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
1046 username or account_name, msg))
1048 def get_utf8(a, b, username):
1050 u = unicode(b, 'utf-16-le')
1051 except UnicodeDecodeError as e:
1052 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1055 u8 = u.encode('utf-8')
1058 # Extract the WDigest hash for the value specified by i.
1059 # Builds an htdigest compatible value
1061 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1062 domain, dns_domain):
1067 user = account_name.lower()
1068 realm = domain.lower()
1070 user = account_name.upper()
1071 realm = domain.upper()
1074 realm = domain.upper()
1077 realm = domain.lower()
1079 user = account_name.upper()
1080 realm = domain.lower()
1082 user = account_name.lower()
1083 realm = domain.upper()
1086 realm = dns_domain.lower()
1088 user = account_name.lower()
1089 realm = dns_domain.lower()
1091 user = account_name.upper()
1092 realm = dns_domain.upper()
1095 realm = dns_domain.upper()
1098 realm = dns_domain.lower()
1100 user = account_name.upper()
1101 realm = dns_domain.lower()
1103 user = account_name.lower()
1104 realm = dns_domain.upper()
1109 user = account_upn.lower()
1112 user = account_upn.upper()
1115 user = "%s\\%s" % (domain, account_name)
1118 user = "%s\\%s" % (domain.lower(), account_name.lower())
1121 user = "%s\\%s" % (domain.upper(), account_name.upper())
1127 user = account_name.lower()
1130 user = account_name.upper()
1136 user = account_upn.lower()
1139 user = account_upn.upper()
1142 user = "%s\\%s" % (domain, account_name)
1145 # Differs from spec, see tests
1146 user = "%s\\%s" % (domain.lower(), account_name.lower())
1149 # Differs from spec, see tests
1150 user = "%s\\%s" % (domain.upper(), account_name.upper())
1155 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1158 digest = binascii.hexlify(bytearray(digests.hashes[i-1].hash))
1159 return "%s:%s:%s" % (user, realm, digest)
1164 # get the value for a virtualCrypt attribute.
1165 # look for an exact match on algorithm and rounds in supplemental creds
1166 # if not found calculate using Primary:CLEARTEXT
1167 # if no Primary:CLEARTEXT return the first supplementalCredential
1168 # that matches the algorithm.
1169 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1172 b = get_package("Primary:userPassword")
1174 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1176 # No exact match on algorithm and number of rounds
1177 # try and calculate one from the Primary:CLEARTEXT
1178 b = get_package("Primary:CLEARTEXT")
1180 u8 = get_utf8(a, b, username or account_name)
1182 sv = get_crypt_value(str(algorithm), u8, rounds)
1184 # Unable to calculate a hash with the specified
1185 # number of rounds, fall back to the first hash using
1186 # the specified algorithm
1190 return "{CRYPT}" + sv
1192 def get_userPassword_hash(blob, algorithm, rounds):
1193 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1196 # Check that the NT hash has not been changed without updating
1197 # the user password hashes. This indicates that password has been
1198 # changed without updating the supplemental credentials.
1199 if unicodePwd != bytearray(up.current_nt_hash.hash):
1202 scheme_prefix = "$%d$" % algorithm
1203 prefix = scheme_prefix
1205 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1209 if (scheme_match is None and
1210 h.scheme == SCHEME and
1211 h.value.startswith(scheme_prefix)):
1212 scheme_match = h.value
1213 if h.scheme == SCHEME and h.value.startswith(prefix):
1214 return (h.value, scheme_match)
1216 # No match on the number of rounds, return the value of the
1217 # first matching scheme
1218 return (None, scheme_match)
1220 # We use sort here in order to have a predictable processing order
1221 for a in sorted(virtual_attributes.keys()):
1222 if not a.lower() in lower_attrs:
1225 if a == "virtualClearTextUTF8":
1226 b = get_package("Primary:CLEARTEXT")
1229 u8 = get_utf8(a, b, username or account_name)
1233 elif a == "virtualClearTextUTF16":
1234 v = get_package("Primary:CLEARTEXT")
1237 elif a == "virtualSSHA":
1238 b = get_package("Primary:CLEARTEXT")
1241 u8 = get_utf8(a, b, username or account_name)
1244 salt = get_random_bytes(4)
1248 bv = h.digest() + salt
1249 v = "{SSHA}" + base64.b64encode(bv)
1250 elif a == "virtualCryptSHA256":
1251 rounds = get_rounds(attr_opts[a])
1252 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1256 elif a == "virtualCryptSHA512":
1257 rounds = get_rounds(attr_opts[a])
1258 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1262 elif a == "virtualSambaGPG":
1263 # Samba adds 'Primary:SambaGPG' at the end.
1264 # When Windows sets the password it keeps
1265 # 'Primary:SambaGPG' and rotates it to
1266 # the begining. So we can only use the value,
1267 # if it is the last one.
1268 v = get_package("Primary:SambaGPG", min_idx=-1)
1271 elif a.startswith("virtualWDigest"):
1272 primary_wdigest = get_package("Primary:WDigest")
1273 if primary_wdigest is None:
1275 x = a[len("virtualWDigest"):]
1280 domain = self.lp.get("workgroup")
1281 dns_domain = samdb.domain_dns_name()
1282 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1287 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1290 def parse_attributes(self, attributes):
1292 if attributes is None:
1293 raise CommandError("Please specify --attributes")
1294 attrs = attributes.split(',')
1297 pa = pa.lstrip().rstrip()
1298 for da in disabled_virtual_attributes.keys():
1299 if pa.lower() == da.lower():
1300 r = disabled_virtual_attributes[da]["reason"]
1301 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1303 for va in virtual_attributes.keys():
1304 if pa.lower() == va.lower():
1305 # Take the real name
1308 password_attrs += [pa]
1310 return password_attrs
1312 class cmd_user_getpassword(GetPasswordCommand):
1313 """Get the password fields of a user/computer account.
1315 This command gets the logon password for a user/computer account.
1317 The username specified on the command is the sAMAccountName.
1318 The username may also be specified using the --filter option.
1320 The command must be run from the root user id or another authorized user id.
1321 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1322 used to adjust the local path. By default tdb:// is used by default.
1324 The '--attributes' parameter takes a comma separated list of attributes,
1325 which will be printed or given to the script specified by '--script'. If a
1326 specified attribute is not available on an object it's silently omitted.
1327 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1328 the NTHASH) and the following virtual attributes are possible (see --help
1329 for which virtual attributes are supported in your environment):
1331 virtualClearTextUTF16: The raw cleartext as stored in the
1332 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1333 with '--decrypt-samba-gpg') buffer inside of the
1334 supplementalCredentials attribute. This typically
1335 contains valid UTF-16-LE, but may contain random
1336 bytes, e.g. for computer accounts.
1338 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1339 (only from valid UTF-16-LE)
1341 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1342 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1344 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1345 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1346 with a $5$... salt, see crypt(3) on modern systems.
1347 The number of rounds used to calculate the hash can
1348 also be specified. By appending ";rounds=x" to the
1349 attribute name i.e. virtualCryptSHA256;rounds=10000
1350 will calculate a SHA256 hash with 10,000 rounds.
1351 non numeric values for rounds are silently ignored
1352 The value is calculated as follows:
1353 1) If a value exists in 'Primary:userPassword' with
1354 the specified number of rounds it is returned.
1355 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1356 '--decrypt-samba-gpg'. Calculate a hash with
1357 the specified number of rounds
1358 3) Return the first CryptSHA256 value in
1359 'Primary:userPassword'
1362 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1363 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1364 with a $6$... salt, see crypt(3) on modern systems.
1365 The number of rounds used to calculate the hash can
1366 also be specified. By appending ";rounds=x" to the
1367 attribute name i.e. virtualCryptSHA512;rounds=10000
1368 will calculate a SHA512 hash with 10,000 rounds.
1369 non numeric values for rounds are silently ignored
1370 The value is calculated as follows:
1371 1) If a value exists in 'Primary:userPassword' with
1372 the specified number of rounds it is returned.
1373 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1374 '--decrypt-samba-gpg'. Calculate a hash with
1375 the specified number of rounds
1376 3) Return the first CryptSHA512 value in
1377 'Primary:userPassword'
1379 virtualWDigestNN: The individual hash values stored in
1380 'Primary:WDigest' where NN is the hash number in
1382 NOTE: As at 22-05-2017 the documentation:
1383 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1384 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1387 virtualSambaGPG: The raw cleartext as stored in the
1388 'Primary:SambaGPG' buffer inside of the
1389 supplementalCredentials attribute.
1390 See the 'password hash gpg key ids' option in
1393 The '--decrypt-samba-gpg' option triggers decryption of the
1394 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1395 in your environment or not (the python-gpgme package is required). Please
1396 note that you might need to set the GNUPGHOME environment variable. If the
1397 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1398 environment variable has been set correctly and the passphrase is already
1399 known by the gpg-agent.
1402 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1405 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1409 super(cmd_user_getpassword, self).__init__()
1411 synopsis = "%prog (<username>|--filter <filter>) [options]"
1413 takes_optiongroups = {
1414 "sambaopts": options.SambaOptions,
1415 "versionopts": options.VersionOptions,
1419 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1420 metavar="URL", dest="H"),
1421 Option("--filter", help="LDAP Filter to set password on", type=str),
1422 Option("--attributes", type=str,
1423 help=virtual_attributes_help,
1424 metavar="ATTRIBUTELIST", dest="attributes"),
1425 Option("--decrypt-samba-gpg",
1426 help=decrypt_samba_gpg_help,
1427 action="store_true", default=False, dest="decrypt_samba_gpg"),
1430 takes_args = ["username?"]
1432 def run(self, username=None, H=None, filter=None,
1433 attributes=None, decrypt_samba_gpg=None,
1434 sambaopts=None, versionopts=None):
1435 self.lp = sambaopts.get_loadparm()
1437 if decrypt_samba_gpg and not gpgme_support:
1438 raise CommandError(decrypt_samba_gpg_help)
1440 if filter is None and username is None:
1441 raise CommandError("Either the username or '--filter' must be specified!")
1444 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1446 if attributes is None:
1447 raise CommandError("Please specify --attributes")
1449 password_attrs = self.parse_attributes(attributes)
1451 samdb = self.connect_system_samdb(url=H, allow_local=True)
1453 obj = self.get_account_attributes(samdb, username,
1456 scope=ldb.SCOPE_SUBTREE,
1457 attrs=password_attrs,
1458 decrypt=decrypt_samba_gpg)
1460 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1461 self.outf.write("%s" % ldif)
1462 self.outf.write("Got password OK\n")
1464 class cmd_user_syncpasswords(GetPasswordCommand):
1465 """Sync the password of user accounts.
1467 This syncs logon passwords for user accounts.
1469 Note that this command should run on a single domain controller only
1470 (typically the PDC-emulator). However the "password hash gpg key ids"
1471 option should to be configured on all domain controllers.
1473 The command must be run from the root user id or another authorized user id.
1474 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1475 local path. By default, ldapi:// is used with the default path to the
1476 privileged ldapi socket.
1478 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1479 "Sync Loop Terminate".
1482 Cache Initialization
1483 ====================
1485 The first time, this command needs to be called with
1486 '--cache-ldb-initialize' in order to initialize its cache.
1488 The cache initialization requires '--attributes' and allows the following
1489 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1492 The '--attributes' parameter takes a comma separated list of attributes,
1493 which will be printed or given to the script specified by '--script'. If a
1494 specified attribute is not available on an object it will be silently omitted.
1495 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1496 the NTHASH) and the following virtual attributes are possible (see '--help'
1497 for supported virtual attributes in your environment):
1499 virtualClearTextUTF16: The raw cleartext as stored in the
1500 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1501 with '--decrypt-samba-gpg') buffer inside of the
1502 supplementalCredentials attribute. This typically
1503 contains valid UTF-16-LE, but may contain random
1504 bytes, e.g. for computer accounts.
1506 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1507 (only from valid UTF-16-LE)
1509 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1510 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1512 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1513 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1514 with a $5$... salt, see crypt(3) on modern systems.
1515 The number of rounds used to calculate the hash can
1516 also be specified. By appending ";rounds=x" to the
1517 attribute name i.e. virtualCryptSHA256;rounds=10000
1518 will calculate a SHA256 hash with 10,000 rounds.
1519 non numeric values for rounds are silently ignored
1520 The value is calculated as follows:
1521 1) If a value exists in 'Primary:userPassword' with
1522 the specified number of rounds it is returned.
1523 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1524 '--decrypt-samba-gpg'. Calculate a hash with
1525 the specified number of rounds
1526 3) Return the first CryptSHA256 value in
1527 'Primary:userPassword'
1529 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1530 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1531 with a $6$... salt, see crypt(3) on modern systems.
1532 The number of rounds used to calculate the hash can
1533 also be specified. By appending ";rounds=x" to the
1534 attribute name i.e. virtualCryptSHA512;rounds=10000
1535 will calculate a SHA512 hash with 10,000 rounds.
1536 non numeric values for rounds are silently ignored
1537 The value is calculated as follows:
1538 1) If a value exists in 'Primary:userPassword' with
1539 the specified number of rounds it is returned.
1540 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1541 '--decrypt-samba-gpg'. Calculate a hash with
1542 the specified number of rounds
1543 3) Return the first CryptSHA512 value in
1544 'Primary:userPassword'
1546 virtualWDigestNN: The individual hash values stored in
1547 'Primary:WDigest' where NN is the hash number in
1549 NOTE: As at 22-05-2017 the documentation:
1550 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1551 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1554 virtualSambaGPG: The raw cleartext as stored in the
1555 'Primary:SambaGPG' buffer inside of the
1556 supplementalCredentials attribute.
1557 See the 'password hash gpg key ids' option in
1560 The '--decrypt-samba-gpg' option triggers decryption of the
1561 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1562 in your environment or not (the python-gpgme package is required). Please
1563 note that you might need to set the GNUPGHOME environment variable. If the
1564 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1565 environment variable has been set correctly and the passphrase is already
1566 known by the gpg-agent.
1568 The '--script' option specifies a custom script that is called whenever any
1569 of the dirsyncAttributes (see below) was changed. The script is called
1570 without any arguments. It gets the LDIF for exactly one object on STDIN.
1571 If the script processed the object successfully it has to respond with a
1572 single line starting with 'DONE-EXIT: ' followed by an optional message.
1574 Note that the script might be called without any password change, e.g. if
1575 the account was disabled (a userAccountControl change) or the
1576 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1577 are always returned as unique identifier of the account. It might be useful
1578 to also ask for non-password attributes like: objectSid, sAMAccountName,
1579 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1580 Depending on the object, some attributes may not be present/available,
1581 but you always get the current state (and not a diff).
1583 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1586 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1587 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1588 (!(sAMAccountName=krbtgt*)))
1589 This means only normal (non-krbtgt) user
1590 accounts are monitored. The '--filter' can modify that, e.g. if it's
1591 required to also sync computer accounts.
1597 This (default) mode runs in an endless loop waiting for password related
1598 changes in the active directory database. It makes use of the
1599 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1600 get changes in a reliable fashion. Objects are monitored for changes of the
1601 following dirsyncAttributes:
1603 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1604 userPrincipalName and userAccountControl.
1606 It recovers from LDAP disconnects and updates the cache in conservative way
1607 (in single steps after each succesfully processed change). An error from
1608 the script (specified by '--script') will result in fatal error and this
1609 command will exit. But the cache state should be still valid and can be
1610 resumed in the next "Sync Loop Run".
1612 The '--logfile' option specifies an optional (required if '--daemon' is
1613 specified) logfile that takes all output of the command. The logfile is
1614 automatically reopened if fstat returns st_nlink == 0.
1616 The optional '--daemon' option will put the command into the background.
1618 You can stop the command without the '--daemon' option, also by hitting
1621 If you specify the '--no-wait' option the command skips the
1622 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1623 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1628 In order to terminate an already running command (likely as daemon) the
1629 '--terminate' option can be used. This also requires the '--logfile' option
1634 samba-tool user syncpasswords --cache-ldb-initialize \\
1635 --attributes=virtualClearTextUTF8
1636 samba-tool user syncpasswords
1639 samba-tool user syncpasswords --cache-ldb-initialize \\
1640 --attributes=objectGUID,objectSID,sAMAccountName,\\
1641 userPrincipalName,userAccountControl,pwdLastSet,\\
1642 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1643 --script=/path/to/my-custom-syncpasswords-script.py
1644 samba-tool user syncpasswords --daemon \\
1645 --logfile=/var/log/samba/user-syncpasswords.log
1646 samba-tool user syncpasswords --terminate \\
1647 --logfile=/var/log/samba/user-syncpasswords.log
1651 super(cmd_user_syncpasswords, self).__init__()
1653 synopsis = "%prog [--cache-ldb-initialize] [options]"
1655 takes_optiongroups = {
1656 "sambaopts": options.SambaOptions,
1657 "versionopts": options.VersionOptions,
1661 Option("--cache-ldb-initialize",
1662 help="Initialize the cache for the first time",
1663 dest="cache_ldb_initialize", action="store_true"),
1664 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1665 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1666 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1667 metavar="URL", dest="H"),
1668 Option("--filter", help="optional LDAP filter to set password on", type=str,
1669 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1670 Option("--attributes", type=str,
1671 help=virtual_attributes_help,
1672 metavar="ATTRIBUTELIST", dest="attributes"),
1673 Option("--decrypt-samba-gpg",
1674 help=decrypt_samba_gpg_help,
1675 action="store_true", default=False, dest="decrypt_samba_gpg"),
1676 Option("--script", help="Script that is called for each password change", type=str,
1677 metavar="/path/to/syncpasswords.script", dest="script"),
1678 Option("--no-wait", help="Don't block waiting for changes",
1679 action="store_true", default=False, dest="nowait"),
1680 Option("--logfile", type=str,
1681 help="The logfile to use (required in --daemon mode).",
1682 metavar="/path/to/syncpasswords.log", dest="logfile"),
1683 Option("--daemon", help="daemonize after initial setup",
1684 action="store_true", default=False, dest="daemon"),
1685 Option("--terminate",
1686 help="Send a SIGTERM to an already running (daemon) process",
1687 action="store_true", default=False, dest="terminate"),
1690 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1691 H=None, filter=None,
1692 attributes=None, decrypt_samba_gpg=None,
1693 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1694 sambaopts=None, versionopts=None):
1696 self.lp = sambaopts.get_loadparm()
1698 self.samdb_url = None
1702 if not cache_ldb_initialize:
1703 if attributes is not None:
1704 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1705 if decrypt_samba_gpg:
1706 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1707 if script is not None:
1708 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1709 if filter is not None:
1710 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1712 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1714 if nowait is not False:
1715 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1716 if logfile is not None:
1717 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1718 if daemon is not False:
1719 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1720 if terminate is not False:
1721 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1725 raise CommandError("--daemon is not allowed together with --no-wait")
1726 if terminate is not False:
1727 raise CommandError("--terminate is not allowed together with --no-wait")
1729 if terminate is True and daemon is True:
1730 raise CommandError("--terminate is not allowed together with --daemon")
1732 if daemon is True and logfile is None:
1733 raise CommandError("--daemon is only allowed together with --logfile")
1735 if terminate is True and logfile is None:
1736 raise CommandError("--terminate is only allowed together with --logfile")
1738 if script is not None:
1739 if not os.path.exists(script):
1740 raise CommandError("script[%s] does not exist!" % script)
1742 sync_command = "%s" % os.path.abspath(script)
1746 dirsync_filter = filter
1747 if dirsync_filter is None:
1748 dirsync_filter = "(&" + \
1749 "(objectClass=user)" + \
1750 "(userAccountControl:%s:=%u)" % (
1751 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1752 "(!(sAMAccountName=krbtgt*))" + \
1755 dirsync_secret_attrs = [
1758 "supplementalCredentials",
1761 dirsync_attrs = dirsync_secret_attrs + [
1764 "userPrincipalName",
1765 "userAccountControl",
1770 password_attrs = None
1772 if cache_ldb_initialize:
1774 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1776 if decrypt_samba_gpg and not gpgme_support:
1777 raise CommandError(decrypt_samba_gpg_help)
1779 password_attrs = self.parse_attributes(attributes)
1780 lower_attrs = [x.lower() for x in password_attrs]
1781 # We always return these in order to track deletions
1782 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1783 if a.lower() not in lower_attrs:
1784 password_attrs += [a]
1786 if cache_ldb is not None:
1787 if cache_ldb.lower().startswith("ldapi://"):
1788 raise CommandError("--cache_ldb ldapi:// is not supported")
1789 elif cache_ldb.lower().startswith("ldap://"):
1790 raise CommandError("--cache_ldb ldap:// is not supported")
1791 elif cache_ldb.lower().startswith("ldaps://"):
1792 raise CommandError("--cache_ldb ldaps:// is not supported")
1793 elif cache_ldb.lower().startswith("tdb://"):
1796 if not os.path.exists(cache_ldb):
1797 cache_ldb = self.lp.private_path(cache_ldb)
1799 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1801 self.lockfile = "%s.pid" % cache_ldb
1804 if self.logfile is not None:
1806 if info.st_nlink == 0:
1807 logfile = self.logfile
1809 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1810 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
1815 log_msg("Reopened logfile[%s]\n" % (logfile))
1816 self.logfile = logfile
1817 msg = "%s: pid[%d]: %s" % (
1821 self.outf.write(msg)
1830 "passwordAttribute",
1836 self.cache = Ldb(cache_ldb)
1837 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1838 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1842 self.samdb_url = res[0]["samdbUrl"][0]
1843 except KeyError as e:
1844 self.samdb_url = None
1846 self.samdb_url = None
1847 if self.samdb_url is None and not cache_ldb_initialize:
1848 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1850 if self.samdb_url is not None and cache_ldb_initialize:
1851 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1853 if self.samdb_url is None:
1855 self.dirsync_filter = dirsync_filter
1856 self.dirsync_attrs = dirsync_attrs
1857 self.dirsync_controls = ["dirsync:1:0:0","extended_dn:1:0"];
1858 self.password_attrs = password_attrs
1859 self.decrypt_samba_gpg = decrypt_samba_gpg
1860 self.sync_command = sync_command
1861 add_ldif = "dn: %s\n" % self.cache_dn
1862 add_ldif += "objectClass: userSyncPasswords\n"
1863 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url)
1864 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter)
1865 for a in self.dirsync_attrs:
1866 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a)
1867 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1868 for a in self.password_attrs:
1869 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
1870 if self.decrypt_samba_gpg == True:
1871 add_ldif += "decryptSambaGPG: TRUE\n"
1873 add_ldif += "decryptSambaGPG: FALSE\n"
1874 if self.sync_command is not None:
1875 add_ldif += "syncCommand: %s\n" % self.sync_command
1876 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1877 self.cache.add_ldif(add_ldif)
1878 self.current_pid = None
1879 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1880 msgs = self.cache.parse_ldif(add_ldif)
1881 changetype,msg = msgs.next()
1882 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1883 self.outf.write("%s" % ldif)
1885 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1886 self.dirsync_attrs = []
1887 for a in res[0]["dirsyncAttribute"]:
1888 self.dirsync_attrs.append(a)
1889 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1890 self.password_attrs = []
1891 for a in res[0]["passwordAttribute"]:
1892 self.password_attrs.append(a)
1893 decrypt_string = res[0]["decryptSambaGPG"][0]
1894 assert(decrypt_string in ["TRUE", "FALSE"])
1895 if decrypt_string == "TRUE":
1896 self.decrypt_samba_gpg = True
1898 self.decrypt_samba_gpg = False
1899 if "syncCommand" in res[0]:
1900 self.sync_command = res[0]["syncCommand"][0]
1902 self.sync_command = None
1903 if "currentPid" in res[0]:
1904 self.current_pid = int(res[0]["currentPid"][0])
1906 self.current_pid = None
1907 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1911 def run_sync_command(dn, ldif):
1912 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
1913 sync_command_p = Popen(self.sync_command,
1918 res = sync_command_p.poll()
1921 input = "%s" % (ldif)
1922 reply = sync_command_p.communicate(input)[0]
1923 log_msg("%s\n" % (reply))
1924 res = sync_command_p.poll()
1926 sync_command_p.terminate()
1927 res = sync_command_p.wait()
1929 if reply.startswith("DONE-EXIT: "):
1932 log_msg("RESULT: %s\n" % (res))
1933 raise Exception("ERROR: %s - %s\n" % (res, reply))
1935 def handle_object(idx, dirsync_obj):
1936 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1937 guid = ndr_unpack(misc.GUID, binary_guid)
1938 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1939 sid = ndr_unpack(security.dom_sid, binary_sid)
1940 domain_sid, rid = sid.split()
1941 if rid == security.DOMAIN_RID_KRBTGT:
1942 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1944 for a in list(dirsync_obj.keys()):
1945 for h in dirsync_secret_attrs:
1946 if a.lower() == h.lower():
1948 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1949 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1950 log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif))
1951 obj = self.get_account_attributes(self.samdb,
1952 username="%s" % sid,
1953 basedn="<GUID=%s>" % guid,
1954 filter="(objectClass=user)",
1955 scope=ldb.SCOPE_BASE,
1956 attrs=self.password_attrs,
1957 decrypt=self.decrypt_samba_gpg)
1958 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1959 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1960 if self.sync_command is None:
1961 self.outf.write("%s" % (ldif))
1963 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1964 run_sync_command(obj.dn, ldif)
1966 def check_current_pid_conflict(terminate):
1972 self.lockfd = os.open(self.lockfile, flags, 0600)
1973 except IOError as (err, msg):
1974 if err == errno.ENOENT:
1977 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1978 (self.lockfile, msg, err))
1981 got_exclusive = False
1983 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1984 got_exclusive = True
1985 except IOError as (err, msg):
1986 if err != errno.EACCES and err != errno.EAGAIN:
1987 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
1988 (self.lockfile, msg, err))
1991 if not got_exclusive:
1992 buf = os.read(self.lockfd, 64)
1993 self.current_pid = None
1995 self.current_pid = int(buf)
1996 except ValueError as e:
1998 if self.current_pid is not None:
2001 if got_exclusive and terminate:
2003 os.ftruncate(self.lockfd, 0)
2004 except IOError as (err, msg):
2005 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2006 (self.lockfile, msg, err))
2008 os.close(self.lockfd)
2013 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2014 except IOError as (err, msg):
2015 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2016 (self.lockfile, msg, err))
2018 # We leave the function with the shared lock.
2021 def update_pid(pid):
2022 if self.lockfd != -1:
2023 got_exclusive = False
2024 # Try 5 times to get the exclusiv lock.
2025 for i in xrange(0, 5):
2027 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2028 got_exclusive = True
2029 except IOError as (err, msg):
2030 if err != errno.EACCES and err != errno.EAGAIN:
2031 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2032 (pid, self.lockfile, msg, err))
2037 if not got_exclusive:
2038 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
2039 (pid, self.lockfile))
2040 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
2041 (pid, self.lockfile))
2048 os.ftruncate(self.lockfd, 0)
2050 os.write(self.lockfd, buf)
2051 except IOError as (err, msg):
2052 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2053 (self.lockfile, msg, err))
2055 self.current_pid = pid
2056 if self.current_pid is not None:
2057 log_msg("currentPid: %d\n" % self.current_pid)
2059 modify_ldif = "dn: %s\n" % (self.cache_dn)
2060 modify_ldif += "changetype: modify\n"
2061 modify_ldif += "replace: currentPid\n"
2062 if self.current_pid is not None:
2063 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2064 modify_ldif += "replace: currentTime\n"
2065 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2066 self.cache.modify_ldif(modify_ldif)
2069 def update_cache(res_controls):
2070 assert len(res_controls) > 0
2071 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2072 res_controls[0].critical = True
2073 self.dirsync_controls = [str(res_controls[0]),"extended_dn:1:0"]
2074 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2076 modify_ldif = "dn: %s\n" % (self.cache_dn)
2077 modify_ldif += "changetype: modify\n"
2078 modify_ldif += "replace: dirsyncControl\n"
2079 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
2080 modify_ldif += "replace: currentTime\n"
2081 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2082 self.cache.modify_ldif(modify_ldif)
2085 def check_object(dirsync_obj, res_controls):
2086 assert len(res_controls) > 0
2087 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2089 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2090 sid = ndr_unpack(security.dom_sid, binary_sid)
2092 lastCookie = str(res_controls[0])
2094 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2095 expression="(lastCookie=%s)" % (
2096 ldb.binary_encode(lastCookie)),
2102 def update_object(dirsync_obj, res_controls):
2103 assert len(res_controls) > 0
2104 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2106 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2107 sid = ndr_unpack(security.dom_sid, binary_sid)
2109 lastCookie = str(res_controls[0])
2111 self.cache.transaction_start()
2113 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2114 expression="(objectClass=*)",
2115 attrs=["lastCookie"])
2117 add_ldif = "dn: %s\n" % (dn)
2118 add_ldif += "objectClass: userCookie\n"
2119 add_ldif += "lastCookie: %s\n" % (lastCookie)
2120 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2121 self.cache.add_ldif(add_ldif)
2123 modify_ldif = "dn: %s\n" % (dn)
2124 modify_ldif += "changetype: modify\n"
2125 modify_ldif += "replace: lastCookie\n"
2126 modify_ldif += "lastCookie: %s\n" % (lastCookie)
2127 modify_ldif += "replace: currentTime\n"
2128 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2129 self.cache.modify_ldif(modify_ldif)
2130 self.cache.transaction_commit()
2131 except Exception as e:
2132 self.cache.transaction_cancel()
2138 res = self.samdb.search(expression=self.dirsync_filter,
2139 scope=ldb.SCOPE_SUBTREE,
2140 attrs=self.dirsync_attrs,
2141 controls=self.dirsync_controls)
2142 log_msg("dirsync_loop(): results %d\n" % len(res))
2145 done = check_object(r, res.controls)
2147 handle_object(ri, r)
2148 update_object(r, res.controls)
2150 update_cache(res.controls)
2154 def sync_loop(wait):
2155 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2156 notify_controls = ["notification:1", "show_recycled:1"]
2157 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2158 scope=ldb.SCOPE_SUBTREE,
2160 controls=notify_controls,
2164 log_msg("Resuming monitoring\n")
2166 log_msg("Getting changes\n")
2167 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2168 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2169 self.outf.write("syncCommand: %s\n" % self.sync_command)
2172 if wait is not True:
2175 for msg in notify_handle:
2176 if not isinstance(msg, ldb.Message):
2177 self.outf.write("referal: %s\n" % msg)
2179 created = msg.get("uSNCreated")[0]
2180 changed = msg.get("uSNChanged")[0]
2181 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2182 (msg.dn, created, changed))
2186 res = notify_handle.result()
2191 orig_pid = os.getpid()
2196 if pid == 0: # Actual daemon
2198 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2203 if cache_ldb_initialize:
2205 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2210 if logfile is not None:
2211 import resource # Resource usage information.
2212 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2213 if maxfd == resource.RLIM_INFINITY:
2214 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2215 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
2216 self.outf.write("Using logfile[%s]\n" % logfile)
2217 for fd in range(0, maxfd):
2228 log_msg("Attached to logfile[%s]\n" % (logfile))
2229 self.logfile = logfile
2232 conflict = check_current_pid_conflict(terminate)
2234 if self.current_pid is None:
2235 log_msg("No process running.\n")
2238 log_msg("Proccess %d is not running anymore.\n" % (
2242 log_msg("Sending SIGTERM to proccess %d.\n" % (
2244 os.kill(self.current_pid, signal.SIGTERM)
2247 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2248 os.getpid(), self.current_pid))
2252 update_pid(os.getpid())
2257 retry_sleep_max = 600
2262 retry_sleep = retry_sleep_min
2264 while self.samdb is None:
2265 if retry_sleep != 0:
2266 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2267 time.sleep(retry_sleep)
2268 retry_sleep = retry_sleep * 2
2269 if retry_sleep >= retry_sleep_max:
2270 retry_sleep = retry_sleep_max
2271 log_msg("Connecting to '%s'\n" % self.samdb_url)
2273 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2274 except Exception as msg:
2276 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2277 if wait is not True:
2282 except ldb.LdbError as (enum, estr):
2284 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2289 class cmd_user_edit(Command):
2290 """Modify User AD object.
2292 This command will allow editing of a user account in the Active Directory
2293 domain. You will then be able to add or change attributes and their values.
2295 The username specified on the command is the sAMAccountName.
2297 The command may be run from the root userid or another authorized userid.
2299 The -H or --URL= option can be used to execute the command against a remote
2303 samba-tool user edit User1 -H ldap://samba.samdom.example.com \
2304 -U administrator --password=passw1rd
2306 Example1 shows how to edit a users attributes in the domain against a remote
2309 The -H parameter is used to specify the remote target server.
2312 samba-tool user edit User2
2314 Example2 shows how to edit a users attributes in the domain against a local
2318 samba-tool user edit User3 --editor=nano
2320 Example3 shows how to edit a users attributes in the domain against a local
2321 LDAP server using the 'nano' editor.
2324 synopsis = "%prog <username> [options]"
2327 Option("-H", "--URL", help="LDB URL for database or target server",
2328 type=str, metavar="URL", dest="H"),
2329 Option("--editor", help="Editor to use instead of the system default,"
2330 " or 'vi' if no system default is set.", type=str),
2333 takes_args = ["username"]
2334 takes_optiongroups = {
2335 "sambaopts": options.SambaOptions,
2336 "credopts": options.CredentialsOptions,
2337 "versionopts": options.VersionOptions,
2340 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2341 H=None, editor=None):
2343 lp = sambaopts.get_loadparm()
2344 creds = credopts.get_credentials(lp, fallback_machine=True)
2345 samdb = SamDB(url=H, session_info=system_session(),
2346 credentials=creds, lp=lp)
2348 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2349 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2351 domaindn = samdb.domain_dn()
2354 res = samdb.search(base=domaindn,
2356 scope=ldb.SCOPE_SUBTREE)
2359 raise CommandError('Unable to find user "%s"' % (username))
2362 r_ldif = samdb.write_ldif(msg, 1)
2363 # remove 'changetype' line
2364 result_ldif = re.sub('changetype: add\n', '', r_ldif)
2367 editor = os.environ.get('EDITOR')
2371 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2372 t_file.write(result_ldif)
2375 check_call([editor, t_file.name])
2376 except CalledProcessError as e:
2377 raise CalledProcessError("ERROR: ", e)
2378 with open(t_file.name) as edited_file:
2379 edited_message = edited_file.read()
2381 if result_ldif != edited_message:
2382 diff = difflib.ndiff(result_ldif.splitlines(),
2383 edited_message.splitlines())
2387 if line.startswith('-'):
2389 minus_lines.append(line)
2390 elif line.startswith('+'):
2392 plus_lines.append(line)
2394 user_ldif="dn: %s\n" % user_dn
2395 user_ldif += "changetype: modify\n"
2397 for line in minus_lines:
2398 attr, val = line.split(':', 1)
2399 search_attr="%s:" % attr
2400 if not re.search(r'^' + search_attr, str(plus_lines)):
2401 user_ldif += "delete: %s\n" % attr
2402 user_ldif += "%s: %s\n" % (attr, val)
2404 for line in plus_lines:
2405 attr, val = line.split(':', 1)
2406 search_attr="%s:" % attr
2407 if re.search(r'^' + search_attr, str(minus_lines)):
2408 user_ldif += "replace: %s\n" % attr
2409 user_ldif += "%s: %s\n" % (attr, val)
2410 if not re.search(r'^' + search_attr, str(minus_lines)):
2411 user_ldif += "add: %s\n" % attr
2412 user_ldif += "%s: %s\n" % (attr, val)
2415 samdb.modify_ldif(user_ldif)
2416 except Exception as e:
2417 raise CommandError("Failed to modify user '%s': " %
2420 self.outf.write("Modified User '%s' successfully\n" % username)
2422 class cmd_user_show(Command):
2423 """Display a user AD object.
2425 This command displays a user account and it's attributes in the Active
2427 The username specified on the command is the sAMAccountName.
2429 The command may be run from the root userid or another authorized userid.
2431 The -H or --URL= option can be used to execute the command against a remote
2435 samba-tool user show User1 -H ldap://samba.samdom.example.com \
2436 -U administrator --password=passw1rd
2438 Example1 shows how to display a users attributes in the domain against a remote
2441 The -H parameter is used to specify the remote target server.
2444 samba-tool user show User2
2446 Example2 shows how to display a users attributes in the domain against a local
2450 samba-tool user show User2 --attributes=objectSid,memberOf
2452 Example3 shows how to display a users objectSid and memberOf attributes.
2454 synopsis = "%prog <username> [options]"
2457 Option("-H", "--URL", help="LDB URL for database or target server",
2458 type=str, metavar="URL", dest="H"),
2459 Option("--attributes",
2460 help=("Comma separated list of attributes, "
2461 "which will be printed."),
2462 type=str, dest="user_attrs"),
2465 takes_args = ["username"]
2466 takes_optiongroups = {
2467 "sambaopts": options.SambaOptions,
2468 "credopts": options.CredentialsOptions,
2469 "versionopts": options.VersionOptions,
2472 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2473 H=None, user_attrs=None):
2475 lp = sambaopts.get_loadparm()
2476 creds = credopts.get_credentials(lp, fallback_machine=True)
2477 samdb = SamDB(url=H, session_info=system_session(),
2478 credentials=creds, lp=lp)
2482 attrs = user_attrs.split(",")
2484 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2485 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2487 domaindn = samdb.domain_dn()
2490 res = samdb.search(base=domaindn, expression=filter,
2491 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2494 raise CommandError('Unable to find user "%s"' % (username))
2497 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2498 self.outf.write(user_ldif)
2500 class cmd_user_move(Command):
2501 """Move a user to an organizational unit/container.
2503 This command moves a user account into the specified organizational unit
2505 The username specified on the command is the sAMAccountName.
2506 The name of the organizational unit or container can be specified as a
2507 full DN or without the domainDN component.
2509 The command may be run from the root userid or another authorized userid.
2511 The -H or --URL= option can be used to execute the command against a remote
2515 samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
2516 -H ldap://samba.samdom.example.com -U administrator
2518 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2519 unit on a remote LDAP server.
2521 The -H parameter is used to specify the remote target server.
2524 samba-tool user move User1 CN=Users
2526 Example2 shows how to move a user User1 back into the CN=Users container
2527 on the local server.
2530 synopsis = "%prog <username> <new_parent_dn> [options]"
2533 Option("-H", "--URL", help="LDB URL for database or target server",
2534 type=str, metavar="URL", dest="H"),
2537 takes_args = [ "username", "new_parent_dn" ]
2538 takes_optiongroups = {
2539 "sambaopts": options.SambaOptions,
2540 "credopts": options.CredentialsOptions,
2541 "versionopts": options.VersionOptions,
2544 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2545 versionopts=None, H=None):
2546 lp = sambaopts.get_loadparm()
2547 creds = credopts.get_credentials(lp, fallback_machine=True)
2548 samdb = SamDB(url=H, session_info=system_session(),
2549 credentials=creds, lp=lp)
2550 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2552 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2553 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2555 res = samdb.search(base=domain_dn,
2557 scope=ldb.SCOPE_SUBTREE)
2560 raise CommandError('Unable to find user "%s"' % (username))
2563 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2564 except Exception as e:
2565 raise CommandError('Invalid new_parent_dn "%s": %s' %
2566 (new_parent_dn, e.message))
2568 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2569 full_new_user_dn.remove_base_components(len(user_dn)-1)
2570 full_new_user_dn.add_base(full_new_parent_dn)
2573 samdb.rename(user_dn, full_new_user_dn)
2574 except Exception as e:
2575 raise CommandError('Failed to move user "%s"' % username, e)
2576 self.outf.write('Moved user "%s" into "%s"\n' %
2577 (username, full_new_parent_dn))
2579 class cmd_user(SuperCommand):
2580 """User management."""
2583 subcommands["add"] = cmd_user_add()
2584 subcommands["create"] = cmd_user_create()
2585 subcommands["delete"] = cmd_user_delete()
2586 subcommands["disable"] = cmd_user_disable()
2587 subcommands["enable"] = cmd_user_enable()
2588 subcommands["list"] = cmd_user_list()
2589 subcommands["setexpiry"] = cmd_user_setexpiry()
2590 subcommands["password"] = cmd_user_password()
2591 subcommands["setpassword"] = cmd_user_setpassword()
2592 subcommands["getpassword"] = cmd_user_getpassword()
2593 subcommands["syncpasswords"] = cmd_user_syncpasswords()
2594 subcommands["edit"] = cmd_user_edit()
2595 subcommands["show"] = cmd_user_show()
2596 subcommands["move"] = cmd_user_move()