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
31 from subprocess import Popen, PIPE, STDOUT
32 from getpass import getpass
33 from samba.auth import system_session
34 from samba.samdb import SamDB
35 from samba.dcerpc import misc
36 from samba.dcerpc import security
37 from samba.dcerpc import drsblobs
38 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
43 generate_random_password,
46 from samba.net import Net
48 from samba.netcmd import (
60 decrypt_samba_gpg_help = "Decrypt the SambaGPG password as cleartext source"
61 except ImportError as e:
63 decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
64 "python-gpgme required"
66 disabled_virtual_attributes = {
69 virtual_attributes = {
70 "virtualClearTextUTF8": {
71 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
73 "virtualClearTextUTF16": {
74 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
77 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
81 get_random_bytes_fn = None
82 if get_random_bytes_fn is None:
85 get_random_bytes_fn = Crypto.Random.get_random_bytes
86 except ImportError as e:
88 if get_random_bytes_fn is None:
91 get_random_bytes_fn = M2Crypto.Rand.rand_bytes
92 except ImportError as e:
96 if get_random_bytes_fn is not None:
98 return "Crypto.Random or M2Crypto.Rand required"
100 def get_random_bytes(num):
101 random_reason = check_random()
102 if random_reason is not None:
103 raise ImportError(random_reason)
104 return get_random_bytes_fn(num)
106 def get_crypt_value(alg, utf8pw):
112 salt = get_random_bytes(16)
113 # The salt needs to be in [A-Za-z0-9./]
114 # base64 is close enough and as we had 16
115 # random bytes but only need 16 characters
116 # we can ignore the possible == at the end
117 # of the base64 string
118 # we just need to replace '+' by '.'
119 b64salt = base64.b64encode(salt)
120 crypt_salt = "$%s$%s$" % (alg, b64salt[0:16].replace('+', '.'))
121 crypt_value = crypt.crypt(utf8pw, crypt_salt)
122 if crypt_value is None:
123 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
124 expected_len = len(crypt_salt) + algs[alg]["length"]
125 if len(crypt_value) != expected_len:
126 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
127 crypt_salt, len(crypt_value), expected_len))
131 random_reason = check_random()
132 if random_reason is not None:
133 raise ImportError(random_reason)
137 virtual_attributes["virtualSSHA"] = {
139 except ImportError as e:
140 reason = "hashlib.sha1()"
142 reason += " and " + random_reason
143 reason += " required"
144 disabled_virtual_attributes["virtualSSHA"] = {
148 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
150 random_reason = check_random()
151 if random_reason is not None:
152 raise ImportError(random_reason)
154 v = get_crypt_value(alg, "")
156 virtual_attributes[attr] = {
158 except ImportError as e:
161 reason += " and " + random_reason
162 reason += " required"
163 disabled_virtual_attributes[attr] = {
166 except NotImplementedError as e:
167 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
168 disabled_virtual_attributes[attr] = {
172 virtual_attributes_help = "The attributes to display (comma separated). "
173 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
174 if len(disabled_virtual_attributes) != 0:
175 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
177 class cmd_user_create(Command):
178 """Create a new user.
180 This command creates a new user account in the Active Directory domain. The username specified on the command is the sAMaccountName.
182 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).
184 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.
186 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.
188 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.
191 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
193 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.
196 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
198 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.
201 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
203 Example3 shows how to create a new user in the OrgUnit organizational unit.
206 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
208 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'.
211 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
212 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
214 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
215 --nis-domain is set, then the other four parameters are mandatory.
218 synopsis = "%prog <username> [<password>] [options]"
221 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
222 metavar="URL", dest="H"),
223 Option("--must-change-at-next-login",
224 help="Force password to be changed on next login",
225 action="store_true"),
226 Option("--random-password",
227 help="Generate random password",
228 action="store_true"),
229 Option("--smartcard-required",
230 help="Require a smartcard for interactive logons",
231 action="store_true"),
232 Option("--use-username-as-cn",
233 help="Force use of username as user's CN",
234 action="store_true"),
236 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>'",
238 Option("--surname", help="User's surname", type=str),
239 Option("--given-name", help="User's given name", type=str),
240 Option("--initials", help="User's initials", type=str),
241 Option("--profile-path", help="User's profile path", type=str),
242 Option("--script-path", help="User's logon script path", type=str),
243 Option("--home-drive", help="User's home drive letter", type=str),
244 Option("--home-directory", help="User's home directory path", type=str),
245 Option("--job-title", help="User's job title", type=str),
246 Option("--department", help="User's department", type=str),
247 Option("--company", help="User's company", type=str),
248 Option("--description", help="User's description", type=str),
249 Option("--mail-address", help="User's email address", type=str),
250 Option("--internet-address", help="User's home page", type=str),
251 Option("--telephone-number", help="User's phone number", type=str),
252 Option("--physical-delivery-office", help="User's office location", type=str),
253 Option("--rfc2307-from-nss",
254 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
255 action="store_true"),
256 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
257 Option("--unix-home", help="User's Unix/RFC2307 home directory",
259 Option("--uid", help="User's Unix/RFC2307 username", type=str),
260 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
261 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
262 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
263 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
266 takes_args = ["username", "password?"]
268 takes_optiongroups = {
269 "sambaopts": options.SambaOptions,
270 "credopts": options.CredentialsOptions,
271 "versionopts": options.VersionOptions,
274 def run(self, username, password=None, credopts=None, sambaopts=None,
275 versionopts=None, H=None, must_change_at_next_login=False,
276 random_password=False, use_username_as_cn=False, userou=None,
277 surname=None, given_name=None, initials=None, profile_path=None,
278 script_path=None, home_drive=None, home_directory=None,
279 job_title=None, department=None, company=None, description=None,
280 mail_address=None, internet_address=None, telephone_number=None,
281 physical_delivery_office=None, rfc2307_from_nss=False,
282 nis_domain=None, unix_home=None, uid=None, uid_number=None,
283 gid_number=None, gecos=None, login_shell=None,
284 smartcard_required=False):
286 if smartcard_required:
287 if password is not None and password is not '':
288 raise CommandError('It is not allowed to specifiy '
290 'together with --smartcard-required.')
291 if must_change_at_next_login:
292 raise CommandError('It is not allowed to specifiy '
293 '--must-change-at-next-login '
294 'together with --smartcard-required.')
296 if random_password and not smartcard_required:
297 password = generate_random_password(128, 255)
300 if smartcard_required:
302 if password is not None and password is not '':
304 password = getpass("New Password: ")
305 passwordverify = getpass("Retype Password: ")
306 if not password == passwordverify:
308 self.outf.write("Sorry, passwords do not match.\n")
311 pwent = pwd.getpwnam(username)
314 if uid_number is None:
315 uid_number = pwent[2]
316 if gid_number is None:
317 gid_number = pwent[3]
320 if login_shell is None:
321 login_shell = pwent[6]
323 lp = sambaopts.get_loadparm()
324 creds = credopts.get_credentials(lp)
326 if uid_number or gid_number:
327 if not lp.get("idmap_ldb:use rfc2307"):
328 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")
330 if nis_domain is not None:
331 if None in (uid_number, login_shell, unix_home, gid_number):
332 raise CommandError('Missing parameters. To enable NIS features, '
333 'the following options have to be given: '
334 '--nis-domain=, --uidNumber=, --login-shell='
335 ', --unix-home=, --gid-number= Operation '
339 samdb = SamDB(url=H, session_info=system_session(),
340 credentials=creds, lp=lp)
341 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
342 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
343 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
344 jobtitle=job_title, department=department, company=company, description=description,
345 mailaddress=mail_address, internetaddress=internet_address,
346 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
347 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
348 uidnumber=uid_number, gidnumber=gid_number,
349 gecos=gecos, loginshell=login_shell,
350 smartcard_required=smartcard_required)
352 raise CommandError("Failed to add user '%s': " % username, e)
354 self.outf.write("User '%s' created successfully\n" % username)
357 class cmd_user_add(cmd_user_create):
358 __doc__ = cmd_user_create.__doc__
359 # take this print out after the add subcommand is removed.
360 # the add subcommand is deprecated but left in for now to allow people to
363 def run(self, *args, **kwargs):
365 "Note: samba-tool user add is deprecated. "
366 "Please use samba-tool user create for the same function.\n")
367 return super(cmd_user_add, self).run(*args, **kwargs)
370 class cmd_user_delete(Command):
373 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
375 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.
377 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.
380 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
382 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.
385 sudo samba-tool user delete User2
387 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.
390 synopsis = "%prog <username> [options]"
393 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
394 metavar="URL", dest="H"),
397 takes_args = ["username"]
398 takes_optiongroups = {
399 "sambaopts": options.SambaOptions,
400 "credopts": options.CredentialsOptions,
401 "versionopts": options.VersionOptions,
404 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
406 lp = sambaopts.get_loadparm()
407 creds = credopts.get_credentials(lp, fallback_machine=True)
409 samdb = SamDB(url=H, session_info=system_session(),
410 credentials=creds, lp=lp)
412 filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
416 res = samdb.search(base=samdb.domain_dn(),
417 scope=ldb.SCOPE_SUBTREE,
422 raise CommandError('Unable to find user "%s"' % (username))
425 samdb.delete(user_dn)
427 raise CommandError('Failed to remove user "%s"' % username, e)
428 self.outf.write("Deleted user %s\n" % username)
431 class cmd_user_list(Command):
432 """List all users."""
434 synopsis = "%prog [options]"
437 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
438 metavar="URL", dest="H"),
441 takes_optiongroups = {
442 "sambaopts": options.SambaOptions,
443 "credopts": options.CredentialsOptions,
444 "versionopts": options.VersionOptions,
447 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
448 lp = sambaopts.get_loadparm()
449 creds = credopts.get_credentials(lp, fallback_machine=True)
451 samdb = SamDB(url=H, session_info=system_session(),
452 credentials=creds, lp=lp)
454 domain_dn = samdb.domain_dn()
455 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
456 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
457 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
458 attrs=["samaccountname"])
463 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
466 class cmd_user_enable(Command):
469 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.
471 There are many reasons why an account may become disabled. These include:
472 - If a user exceeds the account policy for logon attempts
473 - If an administrator disables the account
474 - If the account expires
476 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
478 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.
480 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.
483 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
485 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.
488 su samba-tool user enable Testuser2
490 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.
493 samba-tool user enable --filter=samaccountname=Testuser3
495 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
498 synopsis = "%prog (<username>|--filter <filter>) [options]"
501 takes_optiongroups = {
502 "sambaopts": options.SambaOptions,
503 "versionopts": options.VersionOptions,
504 "credopts": options.CredentialsOptions,
508 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
509 metavar="URL", dest="H"),
510 Option("--filter", help="LDAP Filter to set password on", type=str),
513 takes_args = ["username?"]
515 def run(self, username=None, sambaopts=None, credopts=None,
516 versionopts=None, filter=None, H=None):
517 if username is None and filter is None:
518 raise CommandError("Either the username or '--filter' must be specified!")
521 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
523 lp = sambaopts.get_loadparm()
524 creds = credopts.get_credentials(lp, fallback_machine=True)
526 samdb = SamDB(url=H, session_info=system_session(),
527 credentials=creds, lp=lp)
529 samdb.enable_account(filter)
530 except Exception, msg:
531 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
532 self.outf.write("Enabled user '%s'\n" % (username or filter))
535 class cmd_user_disable(Command):
536 """Disable an user."""
538 synopsis = "%prog (<username>|--filter <filter>) [options]"
541 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
542 metavar="URL", dest="H"),
543 Option("--filter", help="LDAP Filter to set password on", type=str),
546 takes_args = ["username?"]
548 takes_optiongroups = {
549 "sambaopts": options.SambaOptions,
550 "credopts": options.CredentialsOptions,
551 "versionopts": options.VersionOptions,
554 def run(self, username=None, sambaopts=None, credopts=None,
555 versionopts=None, filter=None, H=None):
556 if username is None and filter is None:
557 raise CommandError("Either the username or '--filter' must be specified!")
560 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
562 lp = sambaopts.get_loadparm()
563 creds = credopts.get_credentials(lp, fallback_machine=True)
565 samdb = SamDB(url=H, session_info=system_session(),
566 credentials=creds, lp=lp)
568 samdb.disable_account(filter)
569 except Exception, msg:
570 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
573 class cmd_user_setexpiry(Command):
574 """Set the expiration of a user account.
576 The user can either be specified by their sAMAccountName or using the --filter option.
578 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.
580 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.
583 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
585 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.
588 su samba-tool user setexpiry User2
590 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.
593 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
595 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.
598 samba-tool user setexpiry --noexpiry User4
599 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
602 synopsis = "%prog (<username>|--filter <filter>) [options]"
604 takes_optiongroups = {
605 "sambaopts": options.SambaOptions,
606 "versionopts": options.VersionOptions,
607 "credopts": options.CredentialsOptions,
611 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
612 metavar="URL", dest="H"),
613 Option("--filter", help="LDAP Filter to set password on", type=str),
614 Option("--days", help="Days to expiry", type=int, default=0),
615 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
618 takes_args = ["username?"]
620 def run(self, username=None, sambaopts=None, credopts=None,
621 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
622 if username is None and filter is None:
623 raise CommandError("Either the username or '--filter' must be specified!")
626 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
628 lp = sambaopts.get_loadparm()
629 creds = credopts.get_credentials(lp)
631 samdb = SamDB(url=H, session_info=system_session(),
632 credentials=creds, lp=lp)
635 samdb.setexpiry(filter, days*24*3600, no_expiry_req=noexpiry)
636 except Exception, msg:
637 # FIXME: Catch more specific exception
638 raise CommandError("Failed to set expiry for user '%s': %s" % (
639 username or filter, msg))
641 self.outf.write("Expiry for user '%s' disabled.\n" % (
644 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
645 username or filter, days))
648 class cmd_user_password(Command):
649 """Change password for a user account (the one provided in authentication).
652 synopsis = "%prog [options]"
655 Option("--newpassword", help="New password", type=str),
658 takes_optiongroups = {
659 "sambaopts": options.SambaOptions,
660 "credopts": options.CredentialsOptions,
661 "versionopts": options.VersionOptions,
664 def run(self, credopts=None, sambaopts=None, versionopts=None,
667 lp = sambaopts.get_loadparm()
668 creds = credopts.get_credentials(lp)
670 # get old password now, to get the password prompts in the right order
671 old_password = creds.get_password()
673 net = Net(creds, lp, server=credopts.ipaddress)
675 password = newpassword
677 if password is not None and password is not '':
679 password = getpass("New Password: ")
680 passwordverify = getpass("Retype Password: ")
681 if not password == passwordverify:
683 self.outf.write("Sorry, passwords do not match.\n")
686 net.change_password(password.encode('utf-8'))
687 except Exception, msg:
688 # FIXME: catch more specific exception
689 raise CommandError("Failed to change password : %s" % msg)
690 self.outf.write("Changed password OK\n")
693 class cmd_user_setpassword(Command):
694 """Set or reset the password of a user account.
696 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.
698 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.
700 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.
702 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.
705 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
707 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.
710 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
712 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.
715 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
717 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
720 synopsis = "%prog (<username>|--filter <filter>) [options]"
722 takes_optiongroups = {
723 "sambaopts": options.SambaOptions,
724 "versionopts": options.VersionOptions,
725 "credopts": options.CredentialsOptions,
729 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
730 metavar="URL", dest="H"),
731 Option("--filter", help="LDAP Filter to set password on", type=str),
732 Option("--newpassword", help="Set password", type=str),
733 Option("--must-change-at-next-login",
734 help="Force password to be changed on next login",
735 action="store_true"),
736 Option("--random-password",
737 help="Generate random password",
738 action="store_true"),
739 Option("--smartcard-required",
740 help="Require a smartcard for interactive logons",
741 action="store_true"),
742 Option("--clear-smartcard-required",
743 help="Don't require a smartcard for interactive logons",
744 action="store_true"),
747 takes_args = ["username?"]
749 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
750 versionopts=None, H=None, newpassword=None,
751 must_change_at_next_login=False, random_password=False,
752 smartcard_required=False, clear_smartcard_required=False):
753 if filter is None and username is None:
754 raise CommandError("Either the username or '--filter' must be specified!")
756 password = newpassword
758 if smartcard_required:
759 if password is not None and password is not '':
760 raise CommandError('It is not allowed to specifiy '
762 'together with --smartcard-required.')
763 if must_change_at_next_login:
764 raise CommandError('It is not allowed to specifiy '
765 '--must-change-at-next-login '
766 'together with --smartcard-required.')
767 if clear_smartcard_required:
768 raise CommandError('It is not allowed to specifiy '
769 '--clear-smartcard-required '
770 'together with --smartcard-required.')
772 if random_password and not smartcard_required:
773 password = generate_random_password(128, 255)
776 if smartcard_required:
778 if password is not None and password is not '':
780 password = getpass("New Password: ")
781 passwordverify = getpass("Retype Password: ")
782 if not password == passwordverify:
784 self.outf.write("Sorry, passwords do not match.\n")
787 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
789 lp = sambaopts.get_loadparm()
790 creds = credopts.get_credentials(lp)
792 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
794 samdb = SamDB(url=H, session_info=system_session(),
795 credentials=creds, lp=lp)
797 if smartcard_required:
800 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
801 flags = dsdb.UF_SMARTCARD_REQUIRED
802 samdb.toggle_userAccountFlags(filter, flags, on=True)
803 command = "Failed to enable account for user '%s'" % (username or filter)
804 samdb.enable_account(filter)
805 except Exception, msg:
806 # FIXME: catch more specific exception
807 raise CommandError("%s: %s" % (command, msg))
808 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
812 if clear_smartcard_required:
813 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
814 flags = dsdb.UF_SMARTCARD_REQUIRED
815 samdb.toggle_userAccountFlags(filter, flags, on=False)
816 command = "Failed to set password for user '%s'" % (username or filter)
817 samdb.setpassword(filter, password,
818 force_change_at_next_login=must_change_at_next_login,
820 except Exception, msg:
821 # FIXME: catch more specific exception
822 raise CommandError("%s: %s" % (command, msg))
823 self.outf.write("Changed password OK\n")
825 class GetPasswordCommand(Command):
828 super(GetPasswordCommand, self).__init__()
831 def connect_system_samdb(self, url, allow_local=False, verbose=False):
833 # using anonymous here, results in no authentication
834 # which means we can get system privileges via
835 # the privileged ldapi socket
836 creds = credentials.Credentials()
837 creds.set_anonymous()
839 if url is None and allow_local:
841 elif url.lower().startswith("ldapi://"):
843 elif url.lower().startswith("ldap://"):
844 raise CommandError("--url ldap:// is not supported for this command")
845 elif url.lower().startswith("ldaps://"):
846 raise CommandError("--url ldaps:// is not supported for this command")
847 elif not allow_local:
848 raise CommandError("--url requires an ldapi:// url for this command")
851 self.outf.write("Connecting to '%s'\n" % url)
853 samdb = SamDB(url=url, session_info=system_session(),
854 credentials=creds, lp=self.lp)
858 # Make sure we're connected as SYSTEM
860 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
862 sids = res[0].get("tokenGroups")
863 assert len(sids) == 1
864 sid = ndr_unpack(security.dom_sid, sids[0])
865 assert str(sid) == security.SID_NT_SYSTEM
866 except Exception as msg:
867 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
868 (security.SID_NT_SYSTEM))
870 # We use sort here in order to have a predictable processing order
871 # this might not be strictly needed, but also doesn't hurt here
872 for a in sorted(virtual_attributes.keys()):
873 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
874 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
878 def get_account_attributes(self, samdb, username, basedn, filter, scope,
881 require_supplementalCredentials = False
882 search_attrs = attrs[:]
883 lower_attrs = [x.lower() for x in search_attrs]
884 for a in virtual_attributes.keys():
885 if a.lower() in lower_attrs:
886 require_supplementalCredentials = True
887 add_supplementalCredentials = False
888 add_unicodePwd = False
889 if require_supplementalCredentials:
890 a = "supplementalCredentials"
891 if a.lower() not in lower_attrs:
893 add_supplementalCredentials = True
895 if a.lower() not in lower_attrs:
897 add_unicodePwd = True
898 add_sAMAcountName = False
900 if a.lower() not in lower_attrs:
902 add_sAMAcountName = True
904 if scope == ldb.SCOPE_BASE:
905 search_controls = ["show_deleted:1", "show_recycled:1"]
909 res = samdb.search(base=basedn, expression=filter,
910 scope=scope, attrs=search_attrs,
911 controls=search_controls)
913 raise Exception('Unable to find user "%s"' % (username or filter))
915 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
916 except Exception as msg:
917 # FIXME: catch more specific exception
918 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
923 if "supplementalCredentials" in obj:
924 sc_blob = obj["supplementalCredentials"][0]
925 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
926 if add_supplementalCredentials:
927 del obj["supplementalCredentials"]
928 if "unicodePwd" in obj:
929 unicodePwd = obj["unicodePwd"][0]
931 del obj["unicodePwd"]
932 account_name = obj["sAMAccountName"][0]
933 if add_sAMAcountName:
934 del obj["sAMAccountName"]
937 def get_package(name, min_idx=0):
938 if name in calculated:
939 return calculated[name]
943 min_idx = len(sc.sub.packages) + min_idx
945 for p in sc.sub.packages:
952 return binascii.a2b_hex(p.data)
957 # Samba adds 'Primary:SambaGPG' at the end.
958 # When Windows sets the password it keeps
959 # 'Primary:SambaGPG' and rotates it to
960 # the begining. So we can only use the value,
961 # if it is the last one.
963 # In order to get more protection we verify
964 # the nthash of the decrypted utf16 password
965 # against the stored nthash in unicodePwd.
967 sgv = get_package("Primary:SambaGPG", min_idx=-1)
968 if sgv is not None and unicodePwd is not None:
969 ctx = gpgme.Context()
971 cipher_io = io.BytesIO(sgv)
972 plain_io = io.BytesIO()
974 ctx.decrypt(cipher_io, plain_io)
975 cv = plain_io.getvalue()
977 # We only use the password if it matches
978 # the current nthash stored in the unicodePwd
981 tmp = credentials.Credentials()
983 tmp.set_utf16_password(cv)
984 nthash = tmp.get_nt_hash()
985 if nthash == unicodePwd:
986 calculated["Primary:CLEARTEXT"] = cv
987 except gpgme.GpgmeError as (major, minor, msg):
988 if major == gpgme.ERR_BAD_SECKEY:
989 msg = "ERR_BAD_SECKEY: " + msg
991 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
992 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
993 username or account_name, msg))
995 def get_utf8(a, b, username):
997 u = unicode(b, 'utf-16-le')
998 except UnicodeDecodeError as e:
999 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1002 u8 = u.encode('utf-8')
1005 # We use sort here in order to have a predictable processing order
1006 for a in sorted(virtual_attributes.keys()):
1007 if not a.lower() in lower_attrs:
1010 if a == "virtualClearTextUTF8":
1011 b = get_package("Primary:CLEARTEXT")
1014 u8 = get_utf8(a, b, username or account_name)
1018 elif a == "virtualClearTextUTF16":
1019 v = get_package("Primary:CLEARTEXT")
1022 elif a == "virtualSSHA":
1023 b = get_package("Primary:CLEARTEXT")
1026 u8 = get_utf8(a, b, username or account_name)
1029 salt = get_random_bytes(4)
1033 bv = h.digest() + salt
1034 v = "{SSHA}" + base64.b64encode(bv)
1035 elif a == "virtualCryptSHA256":
1036 b = get_package("Primary:CLEARTEXT")
1039 u8 = get_utf8(a, b, username or account_name)
1042 sv = get_crypt_value("5", u8)
1044 elif a == "virtualCryptSHA512":
1045 b = get_package("Primary:CLEARTEXT")
1048 u8 = get_utf8(a, b, username or account_name)
1051 sv = get_crypt_value("6", u8)
1053 elif a == "virtualSambaGPG":
1054 # Samba adds 'Primary:SambaGPG' at the end.
1055 # When Windows sets the password it keeps
1056 # 'Primary:SambaGPG' and rotates it to
1057 # the begining. So we can only use the value,
1058 # if it is the last one.
1059 v = get_package("Primary:SambaGPG", min_idx=-1)
1064 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1067 def parse_attributes(self, attributes):
1069 if attributes is None:
1070 raise CommandError("Please specify --attributes")
1071 attrs = attributes.split(',')
1074 pa = pa.lstrip().rstrip()
1075 for da in disabled_virtual_attributes.keys():
1076 if pa.lower() == da.lower():
1077 r = disabled_virtual_attributes[da]["reason"]
1078 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1080 for va in virtual_attributes.keys():
1081 if pa.lower() == va.lower():
1082 # Take the real name
1085 password_attrs += [pa]
1087 return password_attrs
1089 class cmd_user_getpassword(GetPasswordCommand):
1090 """Get the password fields of a user/computer account.
1092 This command gets the logon password for a user/computer account.
1094 The username specified on the command is the sAMAccountName.
1095 The username may also be specified using the --filter option.
1097 The command must be run from the root user id or another authorized user id.
1098 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1099 used to adjust the local path. By default tdb:// is used by default.
1101 The '--attributes' parameter takes a comma separated list of attributes,
1102 which will be printed or given to the script specified by '--script'. If a
1103 specified attribute is not available on an object it's silently omitted.
1104 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1105 the NTHASH) and the following virtual attributes are possible (see --help
1106 for which virtual attributes are supported in your environment):
1108 virtualClearTextUTF16: The raw cleartext as stored in the
1109 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1110 with '--decrypt-samba-gpg') buffer inside of the
1111 supplementalCredentials attribute. This typically
1112 contains valid UTF-16-LE, but may contain random
1113 bytes, e.g. for computer accounts.
1115 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1116 (only from valid UTF-16-LE)
1118 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1119 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1121 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1122 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1123 with a $5$... salt, see crypt(3) on modern systems.
1125 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1126 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1127 with a $6$... salt, see crypt(3) on modern systems.
1129 virtualSambaGPG: The raw cleartext as stored in the
1130 'Primary:SambaGPG' buffer inside of the
1131 supplementalCredentials attribute.
1132 See the 'password hash gpg key ids' option in
1135 The '--decrypt-samba-gpg' option triggers decryption of the
1136 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1137 in your environment or not (the python-gpgme package is required). Please
1138 note that you might need to set the GNUPGHOME environment variable. If the
1139 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1140 environment variable has been set correctly and the passphrase is already
1141 known by the gpg-agent.
1144 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1147 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1151 super(cmd_user_getpassword, self).__init__()
1153 synopsis = "%prog (<username>|--filter <filter>) [options]"
1155 takes_optiongroups = {
1156 "sambaopts": options.SambaOptions,
1157 "versionopts": options.VersionOptions,
1161 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1162 metavar="URL", dest="H"),
1163 Option("--filter", help="LDAP Filter to set password on", type=str),
1164 Option("--attributes", type=str,
1165 help=virtual_attributes_help,
1166 metavar="ATTRIBUTELIST", dest="attributes"),
1167 Option("--decrypt-samba-gpg",
1168 help=decrypt_samba_gpg_help,
1169 action="store_true", default=False, dest="decrypt_samba_gpg"),
1172 takes_args = ["username?"]
1174 def run(self, username=None, H=None, filter=None,
1175 attributes=None, decrypt_samba_gpg=None,
1176 sambaopts=None, versionopts=None):
1177 self.lp = sambaopts.get_loadparm()
1179 if decrypt_samba_gpg and not gpgme_support:
1180 raise CommandError(decrypt_samba_gpg_help)
1182 if filter is None and username is None:
1183 raise CommandError("Either the username or '--filter' must be specified!")
1186 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1188 if attributes is None:
1189 raise CommandError("Please specify --attributes")
1191 password_attrs = self.parse_attributes(attributes)
1193 samdb = self.connect_system_samdb(url=H, allow_local=True)
1195 obj = self.get_account_attributes(samdb, username,
1198 scope=ldb.SCOPE_SUBTREE,
1199 attrs=password_attrs,
1200 decrypt=decrypt_samba_gpg)
1202 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1203 self.outf.write("%s" % ldif)
1204 self.outf.write("Got password OK\n")
1206 class cmd_user_syncpasswords(GetPasswordCommand):
1207 """Sync the password of user accounts.
1209 This syncs logon passwords for user accounts.
1211 Note that this command should run on a single domain controller only
1212 (typically the PDC-emulator). However the "password hash gpg key ids"
1213 option should to be configured on all domain controllers.
1215 The command must be run from the root user id or another authorized user id.
1216 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1217 local path. By default, ldapi:// is used with the default path to the
1218 privileged ldapi socket.
1220 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1221 "Sync Loop Terminate".
1224 Cache Initialization
1225 ====================
1227 The first time, this command needs to be called with
1228 '--cache-ldb-initialize' in order to initialize its cache.
1230 The cache initialization requires '--attributes' and allows the following
1231 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1234 The '--attributes' parameter takes a comma separated list of attributes,
1235 which will be printed or given to the script specified by '--script'. If a
1236 specified attribute is not available on an object it will be silently omitted.
1237 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1238 the NTHASH) and the following virtual attributes are possible (see '--help'
1239 for supported virtual attributes in your environment):
1241 virtualClearTextUTF16: The raw cleartext as stored in the
1242 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1243 with '--decrypt-samba-gpg') buffer inside of the
1244 supplementalCredentials attribute. This typically
1245 contains valid UTF-16-LE, but may contain random
1246 bytes, e.g. for computer accounts.
1248 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1249 (only from valid UTF-16-LE)
1251 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1252 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1254 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1255 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1256 with a $5$... salt, see crypt(3) on modern systems.
1258 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1259 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1260 with a $6$... salt, see crypt(3) on modern systems.
1262 virtualSambaGPG: The raw cleartext as stored in the
1263 'Primary:SambaGPG' buffer inside of the
1264 supplementalCredentials attribute.
1265 See the 'password hash gpg key ids' option in
1268 The '--decrypt-samba-gpg' option triggers decryption of the
1269 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1270 in your environment or not (the python-gpgme package is required). Please
1271 note that you might need to set the GNUPGHOME environment variable. If the
1272 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1273 environment variable has been set correctly and the passphrase is already
1274 known by the gpg-agent.
1276 The '--script' option specifies a custom script that is called whenever any
1277 of the dirsyncAttributes (see below) was changed. The script is called
1278 without any arguments. It gets the LDIF for exactly one object on STDIN.
1279 If the script processed the object successfully it has to respond with a
1280 single line starting with 'DONE-EXIT: ' followed by an optional message.
1282 Note that the script might be called without any password change, e.g. if
1283 the account was disabled (an userAccountControl change) or the
1284 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1285 are always returned as unique identifier of the account. It might be useful
1286 to also ask for non-password attributes like: objectSid, sAMAccountName,
1287 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1288 Depending on the object, some attributes may not be present/available,
1289 but you always get the current state (and not a diff).
1291 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1294 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1295 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1296 (!(sAMAccountName=krbtgt*)))
1297 This means only normal (non-krbtgt) user
1298 accounts are monitored. The '--filter' can modify that, e.g. if it's
1299 required to also sync computer accounts.
1305 This (default) mode runs in an endless loop waiting for password related
1306 changes in the active directory database. It makes use of the
1307 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1308 get changes in a reliable fashion. Objects are monitored for changes of the
1309 following dirsyncAttributes:
1311 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1312 userPrincipalName and userAccountControl.
1314 It recovers from LDAP disconnects and updates the cache in conservative way
1315 (in single steps after each succesfully processed change). An error from
1316 the script (specified by '--script') will result in fatal error and this
1317 command will exit. But the cache state should be still valid and can be
1318 resumed in the next "Sync Loop Run".
1320 The '--logfile' option specifies an optional (required if '--daemon' is
1321 specified) logfile that takes all output of the command. The logfile is
1322 automatically reopened if fstat returns st_nlink == 0.
1324 The optional '--daemon' option will put the command into the background.
1326 You can stop the command without the '--daemon' option, also by hitting
1329 If you specify the '--no-wait' option the command skips the
1330 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1331 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1336 In order to terminate an already running command (likely as daemon) the
1337 '--terminate' option can be used. This also requires the '--logfile' option
1342 samba-tool user syncpasswords --cache-ldb-initialize \\
1343 --attributes=virtualClearTextUTF8
1344 samba-tool user syncpasswords
1347 samba-tool user syncpasswords --cache-ldb-initialize \\
1348 --attributes=objectGUID,objectSID,sAMAccountName,\\
1349 userPrincipalName,userAccountControl,pwdLastSet,\\
1350 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1351 --script=/path/to/my-custom-syncpasswords-script.py
1352 samba-tool user syncpasswords --daemon \\
1353 --logfile=/var/log/samba/user-syncpasswords.log
1354 samba-tool user syncpasswords --terminate \\
1355 --logfile=/var/log/samba/user-syncpasswords.log
1359 super(cmd_user_syncpasswords, self).__init__()
1361 synopsis = "%prog [--cache-ldb-initialize] [options]"
1363 takes_optiongroups = {
1364 "sambaopts": options.SambaOptions,
1365 "versionopts": options.VersionOptions,
1369 Option("--cache-ldb-initialize",
1370 help="Initialize the cache for the first time",
1371 dest="cache_ldb_initialize", action="store_true"),
1372 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1373 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1374 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1375 metavar="URL", dest="H"),
1376 Option("--filter", help="optional LDAP filter to set password on", type=str,
1377 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1378 Option("--attributes", type=str,
1379 help=virtual_attributes_help,
1380 metavar="ATTRIBUTELIST", dest="attributes"),
1381 Option("--decrypt-samba-gpg",
1382 help=decrypt_samba_gpg_help,
1383 action="store_true", default=False, dest="decrypt_samba_gpg"),
1384 Option("--script", help="Script that is called for each password change", type=str,
1385 metavar="/path/to/syncpasswords.script", dest="script"),
1386 Option("--no-wait", help="Don't block waiting for changes",
1387 action="store_true", default=False, dest="nowait"),
1388 Option("--logfile", type=str,
1389 help="The logfile to use (required in --daemon mode).",
1390 metavar="/path/to/syncpasswords.log", dest="logfile"),
1391 Option("--daemon", help="daemonize after initial setup",
1392 action="store_true", default=False, dest="daemon"),
1393 Option("--terminate",
1394 help="Send a SIGTERM to an already running (daemon) process",
1395 action="store_true", default=False, dest="terminate"),
1398 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1399 H=None, filter=None,
1400 attributes=None, decrypt_samba_gpg=None,
1401 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1402 sambaopts=None, versionopts=None):
1404 self.lp = sambaopts.get_loadparm()
1406 self.samdb_url = None
1410 if not cache_ldb_initialize:
1411 if attributes is not None:
1412 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1413 if decrypt_samba_gpg:
1414 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1415 if script is not None:
1416 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1417 if filter is not None:
1418 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1420 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1422 if nowait is not False:
1423 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1424 if logfile is not None:
1425 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1426 if daemon is not False:
1427 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1428 if terminate is not False:
1429 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1433 raise CommandError("--daemon is not allowed together with --no-wait")
1434 if terminate is not False:
1435 raise CommandError("--terminate is not allowed together with --no-wait")
1437 if terminate is True and daemon is True:
1438 raise CommandError("--terminate is not allowed together with --daemon")
1440 if daemon is True and logfile is None:
1441 raise CommandError("--daemon is only allowed together with --logfile")
1443 if terminate is True and logfile is None:
1444 raise CommandError("--terminate is only allowed together with --logfile")
1446 if script is not None:
1447 if not os.path.exists(script):
1448 raise CommandError("script[%s] does not exist!" % script)
1450 sync_command = "%s" % os.path.abspath(script)
1454 dirsync_filter = filter
1455 if dirsync_filter is None:
1456 dirsync_filter = "(&" + \
1457 "(objectClass=user)" + \
1458 "(userAccountControl:%s:=%u)" % (
1459 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1460 "(!(sAMAccountName=krbtgt*))" + \
1463 dirsync_secret_attrs = [
1466 "supplementalCredentials",
1469 dirsync_attrs = dirsync_secret_attrs + [
1472 "userPrincipalName",
1473 "userAccountControl",
1478 password_attrs = None
1480 if cache_ldb_initialize:
1482 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1484 if decrypt_samba_gpg and not gpgme_support:
1485 raise CommandError(decrypt_samba_gpg_help)
1487 password_attrs = self.parse_attributes(attributes)
1488 lower_attrs = [x.lower() for x in password_attrs]
1489 # We always return these in order to track deletions
1490 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1491 if a.lower() not in lower_attrs:
1492 password_attrs += [a]
1494 if cache_ldb is not None:
1495 if cache_ldb.lower().startswith("ldapi://"):
1496 raise CommandError("--cache_ldb ldapi:// is not supported")
1497 elif cache_ldb.lower().startswith("ldap://"):
1498 raise CommandError("--cache_ldb ldap:// is not supported")
1499 elif cache_ldb.lower().startswith("ldaps://"):
1500 raise CommandError("--cache_ldb ldaps:// is not supported")
1501 elif cache_ldb.lower().startswith("tdb://"):
1504 if not os.path.exists(cache_ldb):
1505 cache_ldb = self.lp.private_path(cache_ldb)
1507 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1509 self.lockfile = "%s.pid" % cache_ldb
1512 if self.logfile is not None:
1514 if info.st_nlink == 0:
1515 logfile = self.logfile
1517 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1518 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
1523 log_msg("Reopened logfile[%s]\n" % (logfile))
1524 self.logfile = logfile
1525 msg = "%s: pid[%d]: %s" % (
1529 self.outf.write(msg)
1538 "passwordAttribute",
1544 self.cache = Ldb(cache_ldb)
1545 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1546 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1550 self.samdb_url = res[0]["samdbUrl"][0]
1551 except KeyError as e:
1552 self.samdb_url = None
1554 self.samdb_url = None
1555 if self.samdb_url is None and not cache_ldb_initialize:
1556 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1558 if self.samdb_url is not None and cache_ldb_initialize:
1559 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1561 if self.samdb_url is None:
1563 self.dirsync_filter = dirsync_filter
1564 self.dirsync_attrs = dirsync_attrs
1565 self.dirsync_controls = ["dirsync:1:0:0","extended_dn:1:0"];
1566 self.password_attrs = password_attrs
1567 self.decrypt_samba_gpg = decrypt_samba_gpg
1568 self.sync_command = sync_command
1569 add_ldif = "dn: %s\n" % self.cache_dn
1570 add_ldif += "objectClass: userSyncPasswords\n"
1571 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url)
1572 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter)
1573 for a in self.dirsync_attrs:
1574 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a)
1575 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1576 for a in self.password_attrs:
1577 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
1578 if self.decrypt_samba_gpg == True:
1579 add_ldif += "decryptSambaGPG: TRUE\n"
1581 add_ldif += "decryptSambaGPG: FALSE\n"
1582 if self.sync_command is not None:
1583 add_ldif += "syncCommand: %s\n" % self.sync_command
1584 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1585 self.cache.add_ldif(add_ldif)
1586 self.current_pid = None
1587 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1588 msgs = self.cache.parse_ldif(add_ldif)
1589 changetype,msg = msgs.next()
1590 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1591 self.outf.write("%s" % ldif)
1593 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1594 self.dirsync_attrs = []
1595 for a in res[0]["dirsyncAttribute"]:
1596 self.dirsync_attrs.append(a)
1597 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1598 self.password_attrs = []
1599 for a in res[0]["passwordAttribute"]:
1600 self.password_attrs.append(a)
1601 decrypt_string = res[0]["decryptSambaGPG"][0]
1602 assert(decrypt_string in ["TRUE", "FALSE"])
1603 if decrypt_string == "TRUE":
1604 self.decrypt_samba_gpg = True
1606 self.decrypt_samba_gpg = False
1607 if "syncCommand" in res[0]:
1608 self.sync_command = res[0]["syncCommand"][0]
1610 self.sync_command = None
1611 if "currentPid" in res[0]:
1612 self.current_pid = int(res[0]["currentPid"][0])
1614 self.current_pid = None
1615 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1619 def run_sync_command(dn, ldif):
1620 log_msg("Call Popen[%s] for %s\n" % (dn, self.sync_command))
1621 sync_command_p = Popen(self.sync_command,
1626 res = sync_command_p.poll()
1629 input = "%s" % (ldif)
1630 reply = sync_command_p.communicate(input)[0]
1631 log_msg("%s\n" % (reply))
1632 res = sync_command_p.poll()
1634 sync_command_p.terminate()
1635 res = sync_command_p.wait()
1637 if reply.startswith("DONE-EXIT: "):
1640 log_msg("RESULT: %s\n" % (res))
1641 raise Exception("ERROR: %s - %s\n" % (res, reply))
1643 def handle_object(idx, dirsync_obj):
1644 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1645 guid = ndr_unpack(misc.GUID, binary_guid)
1646 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1647 sid = ndr_unpack(security.dom_sid, binary_sid)
1648 domain_sid, rid = sid.split()
1649 if rid == security.DOMAIN_RID_KRBTGT:
1650 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1652 for a in list(dirsync_obj.keys()):
1653 for h in dirsync_secret_attrs:
1654 if a.lower() == h.lower():
1656 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1657 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1658 log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif))
1659 obj = self.get_account_attributes(self.samdb,
1660 username="%s" % sid,
1661 basedn="<GUID=%s>" % guid,
1662 filter="(objectClass=user)",
1663 scope=ldb.SCOPE_BASE,
1664 attrs=self.password_attrs,
1665 decrypt=self.decrypt_samba_gpg)
1666 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1667 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1668 if self.sync_command is None:
1669 self.outf.write("%s" % (ldif))
1671 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1672 run_sync_command(obj.dn, ldif)
1674 def check_current_pid_conflict(terminate):
1680 self.lockfd = os.open(self.lockfile, flags, 0600)
1681 except IOError as (err, msg):
1682 if err == errno.ENOENT:
1685 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1686 (self.lockfile, msg, err))
1689 got_exclusive = False
1691 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1692 got_exclusive = True
1693 except IOError as (err, msg):
1694 if err != errno.EACCES and err != errno.EAGAIN:
1695 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
1696 (self.lockfile, msg, err))
1699 if not got_exclusive:
1700 buf = os.read(self.lockfd, 64)
1701 self.current_pid = None
1703 self.current_pid = int(buf)
1704 except ValueError as e:
1706 if self.current_pid is not None:
1709 if got_exclusive and terminate:
1711 os.ftruncate(self.lockfd, 0)
1712 except IOError as (err, msg):
1713 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
1714 (self.lockfile, msg, err))
1716 os.close(self.lockfd)
1721 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
1722 except IOError as (err, msg):
1723 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
1724 (self.lockfile, msg, err))
1726 # We leave the function with the shared lock.
1729 def update_pid(pid):
1730 if self.lockfd != -1:
1731 got_exclusive = False
1732 # Try 5 times to get the exclusiv lock.
1733 for i in xrange(0, 5):
1735 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1736 got_exclusive = True
1737 except IOError as (err, msg):
1738 if err != errno.EACCES and err != errno.EAGAIN:
1739 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
1740 (pid, self.lockfile, msg, err))
1745 if not got_exclusive:
1746 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
1747 (pid, self.lockfile))
1748 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
1749 (pid, self.lockfile))
1756 os.ftruncate(self.lockfd, 0)
1758 os.write(self.lockfd, buf)
1759 except IOError as (err, msg):
1760 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
1761 (self.lockfile, msg, err))
1763 self.current_pid = pid
1764 if self.current_pid is not None:
1765 log_msg("currentPid: %d\n" % self.current_pid)
1767 modify_ldif = "dn: %s\n" % (self.cache_dn)
1768 modify_ldif += "changetype: modify\n"
1769 modify_ldif += "replace: currentPid\n"
1770 if self.current_pid is not None:
1771 modify_ldif += "currentPid: %d\n" % (self.current_pid)
1772 modify_ldif += "replace: currentTime\n"
1773 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1774 self.cache.modify_ldif(modify_ldif)
1777 def update_cache(res_controls):
1778 assert len(res_controls) > 0
1779 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
1780 res_controls[0].critical = True
1781 self.dirsync_controls = [str(res_controls[0]),"extended_dn:1:0"]
1782 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
1784 modify_ldif = "dn: %s\n" % (self.cache_dn)
1785 modify_ldif += "changetype: modify\n"
1786 modify_ldif += "replace: dirsyncControl\n"
1787 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
1788 modify_ldif += "replace: currentTime\n"
1789 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1790 self.cache.modify_ldif(modify_ldif)
1793 def check_object(dirsync_obj, res_controls):
1794 assert len(res_controls) > 0
1795 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
1797 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1798 sid = ndr_unpack(security.dom_sid, binary_sid)
1800 lastCookie = str(res_controls[0])
1802 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
1803 expression="(lastCookie=%s)" % (
1804 ldb.binary_encode(lastCookie)),
1810 def update_object(dirsync_obj, res_controls):
1811 assert len(res_controls) > 0
1812 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
1814 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1815 sid = ndr_unpack(security.dom_sid, binary_sid)
1817 lastCookie = str(res_controls[0])
1819 self.cache.transaction_start()
1821 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
1822 expression="(objectClass=*)",
1823 attrs=["lastCookie"])
1825 add_ldif = "dn: %s\n" % (dn)
1826 add_ldif += "objectClass: userCookie\n"
1827 add_ldif += "lastCookie: %s\n" % (lastCookie)
1828 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1829 self.cache.add_ldif(add_ldif)
1831 modify_ldif = "dn: %s\n" % (dn)
1832 modify_ldif += "changetype: modify\n"
1833 modify_ldif += "replace: lastCookie\n"
1834 modify_ldif += "lastCookie: %s\n" % (lastCookie)
1835 modify_ldif += "replace: currentTime\n"
1836 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1837 self.cache.modify_ldif(modify_ldif)
1838 self.cache.transaction_commit()
1839 except Exception as e:
1840 self.cache.transaction_cancel()
1846 res = self.samdb.search(expression=self.dirsync_filter,
1847 scope=ldb.SCOPE_SUBTREE,
1848 attrs=self.dirsync_attrs,
1849 controls=self.dirsync_controls)
1850 log_msg("dirsync_loop(): results %d\n" % len(res))
1853 done = check_object(r, res.controls)
1855 handle_object(ri, r)
1856 update_object(r, res.controls)
1858 update_cache(res.controls)
1862 def sync_loop(wait):
1863 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
1864 notify_controls = ["notification:1"]
1865 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
1866 scope=ldb.SCOPE_SUBTREE,
1868 controls=notify_controls,
1872 log_msg("Resuming monitoring\n")
1874 log_msg("Getting changes\n")
1875 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
1876 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
1877 self.outf.write("syncCommand: %s\n" % self.sync_command)
1880 if wait is not True:
1883 for msg in notify_handle:
1884 if not isinstance(msg, ldb.Message):
1885 self.outf.write("referal: %s\n" % msg)
1887 created = msg.get("uSNCreated")[0]
1888 changed = msg.get("uSNChanged")[0]
1889 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
1890 (msg.dn, created, changed))
1894 res = notify_handle.result()
1899 orig_pid = os.getpid()
1904 if pid == 0: # Actual daemon
1906 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
1911 if cache_ldb_initialize:
1913 self.samdb = self.connect_system_samdb(url=self.samdb_url,
1918 if logfile is not None:
1919 import resource # Resource usage information.
1920 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1921 if maxfd == resource.RLIM_INFINITY:
1922 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
1923 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
1924 self.outf.write("Using logfile[%s]\n" % logfile)
1925 for fd in range(0, maxfd):
1936 log_msg("Attached to logfile[%s]\n" % (logfile))
1937 self.logfile = logfile
1940 conflict = check_current_pid_conflict(terminate)
1942 if self.current_pid is None:
1943 log_msg("No process running.\n")
1946 log_msg("Proccess %d is not running anymore.\n" % (
1950 log_msg("Sending SIGTERM to proccess %d.\n" % (
1952 os.kill(self.current_pid, signal.SIGTERM)
1955 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
1956 os.getpid(), self.current_pid))
1960 update_pid(os.getpid())
1965 retry_sleep_max = 600
1970 retry_sleep = retry_sleep_min
1972 while self.samdb is None:
1973 if retry_sleep != 0:
1974 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
1975 time.sleep(retry_sleep)
1976 retry_sleep = retry_sleep * 2
1977 if retry_sleep >= retry_sleep_max:
1978 retry_sleep = retry_sleep_max
1979 log_msg("Connecting to '%s'\n" % self.samdb_url)
1981 self.samdb = self.connect_system_samdb(url=self.samdb_url)
1982 except Exception as msg:
1984 log_msg("Connect to samdb Exception => (%s)\n" % msg)
1985 if wait is not True:
1990 except ldb.LdbError as (enum, estr):
1992 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
1997 class cmd_user(SuperCommand):
1998 """User management."""
2001 subcommands["add"] = cmd_user_add()
2002 subcommands["create"] = cmd_user_create()
2003 subcommands["delete"] = cmd_user_delete()
2004 subcommands["disable"] = cmd_user_disable()
2005 subcommands["enable"] = cmd_user_enable()
2006 subcommands["list"] = cmd_user_list()
2007 subcommands["setexpiry"] = cmd_user_setexpiry()
2008 subcommands["password"] = cmd_user_password()
2009 subcommands["setpassword"] = cmd_user_setpassword()
2010 subcommands["getpassword"] = cmd_user_getpassword()
2011 subcommands["syncpasswords"] = cmd_user_syncpasswords()