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)
410 samdb = SamDB(url=H, session_info=system_session(),
411 credentials=creds, lp=lp)
412 samdb.deleteuser(username)
414 raise CommandError('Failed to remove user "%s"' % username, e)
415 self.outf.write("Deleted user %s\n" % username)
418 class cmd_user_list(Command):
419 """List all users."""
421 synopsis = "%prog [options]"
424 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
425 metavar="URL", dest="H"),
428 takes_optiongroups = {
429 "sambaopts": options.SambaOptions,
430 "credopts": options.CredentialsOptions,
431 "versionopts": options.VersionOptions,
434 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
435 lp = sambaopts.get_loadparm()
436 creds = credopts.get_credentials(lp, fallback_machine=True)
438 samdb = SamDB(url=H, session_info=system_session(),
439 credentials=creds, lp=lp)
441 domain_dn = samdb.domain_dn()
442 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
443 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
444 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
445 attrs=["samaccountname"])
450 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
453 class cmd_user_enable(Command):
456 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.
458 There are many reasons why an account may become disabled. These include:
459 - If a user exceeds the account policy for logon attempts
460 - If an administrator disables the account
461 - If the account expires
463 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
465 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.
467 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.
470 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
472 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.
475 su samba-tool user enable Testuser2
477 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.
480 samba-tool user enable --filter=samaccountname=Testuser3
482 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
485 synopsis = "%prog (<username>|--filter <filter>) [options]"
488 takes_optiongroups = {
489 "sambaopts": options.SambaOptions,
490 "versionopts": options.VersionOptions,
491 "credopts": options.CredentialsOptions,
495 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
496 metavar="URL", dest="H"),
497 Option("--filter", help="LDAP Filter to set password on", type=str),
500 takes_args = ["username?"]
502 def run(self, username=None, sambaopts=None, credopts=None,
503 versionopts=None, filter=None, H=None):
504 if username is None and filter is None:
505 raise CommandError("Either the username or '--filter' must be specified!")
508 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
510 lp = sambaopts.get_loadparm()
511 creds = credopts.get_credentials(lp, fallback_machine=True)
513 samdb = SamDB(url=H, session_info=system_session(),
514 credentials=creds, lp=lp)
516 samdb.enable_account(filter)
517 except Exception, msg:
518 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
519 self.outf.write("Enabled user '%s'\n" % (username or filter))
522 class cmd_user_disable(Command):
523 """Disable an user."""
525 synopsis = "%prog (<username>|--filter <filter>) [options]"
528 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
529 metavar="URL", dest="H"),
530 Option("--filter", help="LDAP Filter to set password on", type=str),
533 takes_args = ["username?"]
535 takes_optiongroups = {
536 "sambaopts": options.SambaOptions,
537 "credopts": options.CredentialsOptions,
538 "versionopts": options.VersionOptions,
541 def run(self, username=None, sambaopts=None, credopts=None,
542 versionopts=None, filter=None, H=None):
543 if username is None and filter is None:
544 raise CommandError("Either the username or '--filter' must be specified!")
547 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
549 lp = sambaopts.get_loadparm()
550 creds = credopts.get_credentials(lp, fallback_machine=True)
552 samdb = SamDB(url=H, session_info=system_session(),
553 credentials=creds, lp=lp)
555 samdb.disable_account(filter)
556 except Exception, msg:
557 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
560 class cmd_user_setexpiry(Command):
561 """Set the expiration of a user account.
563 The user can either be specified by their sAMAccountName or using the --filter option.
565 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.
567 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.
570 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
572 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.
575 su samba-tool user setexpiry User2
577 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.
580 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
582 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.
585 samba-tool user setexpiry --noexpiry User4
586 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
589 synopsis = "%prog (<username>|--filter <filter>) [options]"
591 takes_optiongroups = {
592 "sambaopts": options.SambaOptions,
593 "versionopts": options.VersionOptions,
594 "credopts": options.CredentialsOptions,
598 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
599 metavar="URL", dest="H"),
600 Option("--filter", help="LDAP Filter to set password on", type=str),
601 Option("--days", help="Days to expiry", type=int, default=0),
602 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
605 takes_args = ["username?"]
607 def run(self, username=None, sambaopts=None, credopts=None,
608 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
609 if username is None and filter is None:
610 raise CommandError("Either the username or '--filter' must be specified!")
613 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
615 lp = sambaopts.get_loadparm()
616 creds = credopts.get_credentials(lp)
618 samdb = SamDB(url=H, session_info=system_session(),
619 credentials=creds, lp=lp)
622 samdb.setexpiry(filter, days*24*3600, no_expiry_req=noexpiry)
623 except Exception, msg:
624 # FIXME: Catch more specific exception
625 raise CommandError("Failed to set expiry for user '%s': %s" % (
626 username or filter, msg))
628 self.outf.write("Expiry for user '%s' disabled.\n" % (
631 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
632 username or filter, days))
635 class cmd_user_password(Command):
636 """Change password for a user account (the one provided in authentication).
639 synopsis = "%prog [options]"
642 Option("--newpassword", help="New password", type=str),
645 takes_optiongroups = {
646 "sambaopts": options.SambaOptions,
647 "credopts": options.CredentialsOptions,
648 "versionopts": options.VersionOptions,
651 def run(self, credopts=None, sambaopts=None, versionopts=None,
654 lp = sambaopts.get_loadparm()
655 creds = credopts.get_credentials(lp)
657 # get old password now, to get the password prompts in the right order
658 old_password = creds.get_password()
660 net = Net(creds, lp, server=credopts.ipaddress)
662 password = newpassword
664 if password is not None and password is not '':
666 password = getpass("New Password: ")
667 passwordverify = getpass("Retype Password: ")
668 if not password == passwordverify:
670 self.outf.write("Sorry, passwords do not match.\n")
673 net.change_password(password)
674 except Exception, msg:
675 # FIXME: catch more specific exception
676 raise CommandError("Failed to change password : %s" % msg)
677 self.outf.write("Changed password OK\n")
680 class cmd_user_setpassword(Command):
681 """Set or reset the password of a user account.
683 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.
685 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.
687 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.
689 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.
692 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
694 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.
697 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
699 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.
702 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
704 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
707 synopsis = "%prog (<username>|--filter <filter>) [options]"
709 takes_optiongroups = {
710 "sambaopts": options.SambaOptions,
711 "versionopts": options.VersionOptions,
712 "credopts": options.CredentialsOptions,
716 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
717 metavar="URL", dest="H"),
718 Option("--filter", help="LDAP Filter to set password on", type=str),
719 Option("--newpassword", help="Set password", type=str),
720 Option("--must-change-at-next-login",
721 help="Force password to be changed on next login",
722 action="store_true"),
723 Option("--random-password",
724 help="Generate random password",
725 action="store_true"),
726 Option("--smartcard-required",
727 help="Require a smartcard for interactive logons",
728 action="store_true"),
729 Option("--clear-smartcard-required",
730 help="Don't require a smartcard for interactive logons",
731 action="store_true"),
734 takes_args = ["username?"]
736 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
737 versionopts=None, H=None, newpassword=None,
738 must_change_at_next_login=False, random_password=False,
739 smartcard_required=False, clear_smartcard_required=False):
740 if filter is None and username is None:
741 raise CommandError("Either the username or '--filter' must be specified!")
743 password = newpassword
745 if smartcard_required:
746 if password is not None and password is not '':
747 raise CommandError('It is not allowed to specifiy '
749 'together with --smartcard-required.')
750 if must_change_at_next_login:
751 raise CommandError('It is not allowed to specifiy '
752 '--must-change-at-next-login '
753 'together with --smartcard-required.')
754 if clear_smartcard_required:
755 raise CommandError('It is not allowed to specifiy '
756 '--clear-smartcard-required '
757 'together with --smartcard-required.')
759 if random_password and not smartcard_required:
760 password = generate_random_password(128, 255)
763 if smartcard_required:
765 if password is not None and password is not '':
767 password = getpass("New Password: ")
768 passwordverify = getpass("Retype Password: ")
769 if not password == passwordverify:
771 self.outf.write("Sorry, passwords do not match.\n")
774 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
776 lp = sambaopts.get_loadparm()
777 creds = credopts.get_credentials(lp)
779 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
781 samdb = SamDB(url=H, session_info=system_session(),
782 credentials=creds, lp=lp)
784 if smartcard_required:
787 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
788 flags = dsdb.UF_SMARTCARD_REQUIRED
789 samdb.toggle_userAccountFlags(filter, flags, on=True)
790 command = "Failed to enable account for user '%s'" % (username or filter)
791 samdb.enable_account(filter)
792 except Exception, msg:
793 # FIXME: catch more specific exception
794 raise CommandError("%s: %s" % (command, msg))
795 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
799 if clear_smartcard_required:
800 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
801 flags = dsdb.UF_SMARTCARD_REQUIRED
802 samdb.toggle_userAccountFlags(filter, flags, on=False)
803 command = "Failed to set password for user '%s'" % (username or filter)
804 samdb.setpassword(filter, password,
805 force_change_at_next_login=must_change_at_next_login,
807 except Exception, msg:
808 # FIXME: catch more specific exception
809 raise CommandError("%s: %s" % (command, msg))
810 self.outf.write("Changed password OK\n")
812 class GetPasswordCommand(Command):
815 super(GetPasswordCommand, self).__init__()
818 def connect_system_samdb(self, url, allow_local=False, verbose=False):
820 # using anonymous here, results in no authentication
821 # which means we can get system privileges via
822 # the privileged ldapi socket
823 creds = credentials.Credentials()
824 creds.set_anonymous()
826 if url is None and allow_local:
828 elif url.lower().startswith("ldapi://"):
830 elif url.lower().startswith("ldap://"):
831 raise CommandError("--url ldap:// is not supported for this command")
832 elif url.lower().startswith("ldaps://"):
833 raise CommandError("--url ldaps:// is not supported for this command")
834 elif not allow_local:
835 raise CommandError("--url requires an ldapi:// url for this command")
838 self.outf.write("Connecting to '%s'\n" % url)
840 samdb = SamDB(url=url, session_info=system_session(),
841 credentials=creds, lp=self.lp)
845 # Make sure we're connected as SYSTEM
847 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
849 sids = res[0].get("tokenGroups")
850 assert len(sids) == 1
851 sid = ndr_unpack(security.dom_sid, sids[0])
852 assert str(sid) == security.SID_NT_SYSTEM
853 except Exception as msg:
854 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
855 (security.SID_NT_SYSTEM))
857 # We use sort here in order to have a predictable processing order
858 # this might not be strictly needed, but also doesn't hurt here
859 for a in sorted(virtual_attributes.keys()):
860 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
861 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
865 def get_account_attributes(self, samdb, username, basedn, filter, scope,
868 require_supplementalCredentials = False
869 search_attrs = attrs[:]
870 lower_attrs = [x.lower() for x in search_attrs]
871 for a in virtual_attributes.keys():
872 if a.lower() in lower_attrs:
873 require_supplementalCredentials = True
874 add_supplementalCredentials = False
875 add_unicodePwd = False
876 if require_supplementalCredentials:
877 a = "supplementalCredentials"
878 if a.lower() not in lower_attrs:
880 add_supplementalCredentials = True
882 if a.lower() not in lower_attrs:
884 add_unicodePwd = True
885 add_sAMAcountName = False
887 if a.lower() not in lower_attrs:
889 add_sAMAcountName = True
891 if scope == ldb.SCOPE_BASE:
892 search_controls = ["show_deleted:1", "show_recycled:1"]
896 res = samdb.search(base=basedn, expression=filter,
897 scope=scope, attrs=search_attrs,
898 controls=search_controls)
900 raise Exception('Unable to find user "%s"' % (username or filter))
902 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
903 except Exception as msg:
904 # FIXME: catch more specific exception
905 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
910 if "supplementalCredentials" in obj:
911 sc_blob = obj["supplementalCredentials"][0]
912 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
913 if add_supplementalCredentials:
914 del obj["supplementalCredentials"]
915 if "unicodePwd" in obj:
916 unicodePwd = obj["unicodePwd"][0]
918 del obj["unicodePwd"]
919 account_name = obj["sAMAccountName"][0]
920 if add_sAMAcountName:
921 del obj["sAMAccountName"]
924 def get_package(name, min_idx=0):
925 if name in calculated:
926 return calculated[name]
930 min_idx = len(sc.sub.packages) + min_idx
932 for p in sc.sub.packages:
939 return binascii.a2b_hex(p.data)
944 # Samba adds 'Primary:SambaGPG' at the end.
945 # When Windows sets the password it keeps
946 # 'Primary:SambaGPG' and rotates it to
947 # the begining. So we can only use the value,
948 # if it is the last one.
950 # In order to get more protection we verify
951 # the nthash of the decrypted utf16 password
952 # against the stored nthash in unicodePwd.
954 sgv = get_package("Primary:SambaGPG", min_idx=-1)
955 if sgv is not None and unicodePwd is not None:
956 ctx = gpgme.Context()
958 cipher_io = io.BytesIO(sgv)
959 plain_io = io.BytesIO()
961 ctx.decrypt(cipher_io, plain_io)
962 cv = plain_io.getvalue()
964 # We only use the password if it matches
965 # the current nthash stored in the unicodePwd
968 tmp = credentials.Credentials()
970 tmp.set_utf16_password(cv)
971 nthash = tmp.get_nt_hash()
972 if nthash == unicodePwd:
973 calculated["Primary:CLEARTEXT"] = cv
974 except gpgme.GpgmeError as (major, minor, msg):
975 if major == gpgme.ERR_BAD_SECKEY:
976 msg = "ERR_BAD_SECKEY: " + msg
978 msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
979 self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
980 username or account_name, msg))
982 def get_utf8(a, b, username):
984 u = unicode(b, 'utf-16-le')
985 except UnicodeDecodeError as e:
986 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
989 u8 = u.encode('utf-8')
992 # We use sort here in order to have a predictable processing order
993 for a in sorted(virtual_attributes.keys()):
994 if not a.lower() in lower_attrs:
997 if a == "virtualClearTextUTF8":
998 b = get_package("Primary:CLEARTEXT")
1001 u8 = get_utf8(a, b, username or account_name)
1005 elif a == "virtualClearTextUTF16":
1006 v = get_package("Primary:CLEARTEXT")
1009 elif a == "virtualSSHA":
1010 b = get_package("Primary:CLEARTEXT")
1013 u8 = get_utf8(a, b, username or account_name)
1016 salt = get_random_bytes(4)
1020 bv = h.digest() + salt
1021 v = "{SSHA}" + base64.b64encode(bv)
1022 elif a == "virtualCryptSHA256":
1023 b = get_package("Primary:CLEARTEXT")
1026 u8 = get_utf8(a, b, username or account_name)
1029 sv = get_crypt_value("5", u8)
1031 elif a == "virtualCryptSHA512":
1032 b = get_package("Primary:CLEARTEXT")
1035 u8 = get_utf8(a, b, username or account_name)
1038 sv = get_crypt_value("6", u8)
1040 elif a == "virtualSambaGPG":
1041 # Samba adds 'Primary:SambaGPG' at the end.
1042 # When Windows sets the password it keeps
1043 # 'Primary:SambaGPG' and rotates it to
1044 # the begining. So we can only use the value,
1045 # if it is the last one.
1046 v = get_package("Primary:SambaGPG", min_idx=-1)
1051 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1054 def parse_attributes(self, attributes):
1056 if attributes is None:
1057 raise CommandError("Please specify --attributes")
1058 attrs = attributes.split(',')
1061 pa = pa.lstrip().rstrip()
1062 for da in disabled_virtual_attributes.keys():
1063 if pa.lower() == da.lower():
1064 r = disabled_virtual_attributes[da]["reason"]
1065 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1067 for va in virtual_attributes.keys():
1068 if pa.lower() == va.lower():
1069 # Take the real name
1072 password_attrs += [pa]
1074 return password_attrs
1076 class cmd_user_getpassword(GetPasswordCommand):
1077 """Get the password fields of a user/computer account.
1079 This command gets the logon password for a user/computer account.
1081 The username specified on the command is the sAMAccountName.
1082 The username may also be specified using the --filter option.
1084 The command must be run from the root user id or another authorized user id.
1085 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1086 used to adjust the local path. By default tdb:// is used by default.
1088 The '--attributes' parameter takes a comma separated list of attributes,
1089 which will be printed or given to the script specified by '--script'. If a
1090 specified attribute is not available on an object it's silently omitted.
1091 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1092 the NTHASH) and the following virtual attributes are possible (see --help
1093 for which virtual attributes are supported in your environment):
1095 virtualClearTextUTF16: The raw cleartext as stored in the
1096 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1097 with '--decrypt-samba-gpg') buffer inside of the
1098 supplementalCredentials attribute. This typically
1099 contains valid UTF-16-LE, but may contain random
1100 bytes, e.g. for computer accounts.
1102 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1103 (only from valid UTF-16-LE)
1105 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1106 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1108 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1109 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1110 with a $5$... salt, see crypt(3) on modern systems.
1112 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1113 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1114 with a $6$... salt, see crypt(3) on modern systems.
1116 virtualSambaGPG: The raw cleartext as stored in the
1117 'Primary:SambaGPG' buffer inside of the
1118 supplementalCredentials attribute.
1119 See the 'password hash gpg key ids' option in
1122 The '--decrypt-samba-gpg' option triggers decryption of the
1123 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1124 in your environment or not (the python-gpgme package is required). Please
1125 note that you might need to set the GNUPGHOME environment variable. If the
1126 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1127 environment variable has been set correctly and the passphrase is already
1128 known by the gpg-agent.
1131 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1134 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1138 super(cmd_user_getpassword, self).__init__()
1140 synopsis = "%prog (<username>|--filter <filter>) [options]"
1142 takes_optiongroups = {
1143 "sambaopts": options.SambaOptions,
1144 "versionopts": options.VersionOptions,
1148 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1149 metavar="URL", dest="H"),
1150 Option("--filter", help="LDAP Filter to set password on", type=str),
1151 Option("--attributes", type=str,
1152 help=virtual_attributes_help,
1153 metavar="ATTRIBUTELIST", dest="attributes"),
1154 Option("--decrypt-samba-gpg",
1155 help=decrypt_samba_gpg_help,
1156 action="store_true", default=False, dest="decrypt_samba_gpg"),
1159 takes_args = ["username?"]
1161 def run(self, username=None, H=None, filter=None,
1162 attributes=None, decrypt_samba_gpg=None,
1163 sambaopts=None, versionopts=None):
1164 self.lp = sambaopts.get_loadparm()
1166 if decrypt_samba_gpg and not gpgme_support:
1167 raise CommandError(decrypt_samba_gpg_help)
1169 if filter is None and username is None:
1170 raise CommandError("Either the username or '--filter' must be specified!")
1173 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1175 if attributes is None:
1176 raise CommandError("Please specify --attributes")
1178 password_attrs = self.parse_attributes(attributes)
1180 samdb = self.connect_system_samdb(url=H, allow_local=True)
1182 obj = self.get_account_attributes(samdb, username,
1185 scope=ldb.SCOPE_SUBTREE,
1186 attrs=password_attrs,
1187 decrypt=decrypt_samba_gpg)
1189 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1190 self.outf.write("%s" % ldif)
1191 self.outf.write("Got password OK\n")
1193 class cmd_user_syncpasswords(GetPasswordCommand):
1194 """Sync the password of user accounts.
1196 This syncs logon passwords for user accounts.
1198 Note that this command should run on a single domain controller only
1199 (typically the PDC-emulator). However the "password hash gpg key ids"
1200 option should to be configured on all domain controllers.
1202 The command must be run from the root user id or another authorized user id.
1203 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1204 local path. By default, ldapi:// is used with the default path to the
1205 privileged ldapi socket.
1207 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1208 "Sync Loop Terminate".
1211 Cache Initialization
1212 ====================
1214 The first time, this command needs to be called with
1215 '--cache-ldb-initialize' in order to initialize its cache.
1217 The cache initialization requires '--attributes' and allows the following
1218 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1221 The '--attributes' parameter takes a comma separated list of attributes,
1222 which will be printed or given to the script specified by '--script'. If a
1223 specified attribute is not available on an object it will be silently omitted.
1224 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1225 the NTHASH) and the following virtual attributes are possible (see '--help'
1226 for supported virtual attributes in your environment):
1228 virtualClearTextUTF16: The raw cleartext as stored in the
1229 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1230 with '--decrypt-samba-gpg') buffer inside of the
1231 supplementalCredentials attribute. This typically
1232 contains valid UTF-16-LE, but may contain random
1233 bytes, e.g. for computer accounts.
1235 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1236 (only from valid UTF-16-LE)
1238 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1239 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1241 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1242 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1243 with a $5$... salt, see crypt(3) on modern systems.
1245 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1246 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1247 with a $6$... salt, see crypt(3) on modern systems.
1249 virtualSambaGPG: The raw cleartext as stored in the
1250 'Primary:SambaGPG' buffer inside of the
1251 supplementalCredentials attribute.
1252 See the 'password hash gpg key ids' option in
1255 The '--decrypt-samba-gpg' option triggers decryption of the
1256 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1257 in your environment or not (the python-gpgme package is required). Please
1258 note that you might need to set the GNUPGHOME environment variable. If the
1259 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1260 environment variable has been set correctly and the passphrase is already
1261 known by the gpg-agent.
1263 The '--script' option specifies a custom script that is called whenever any
1264 of the dirsyncAttributes (see below) was changed. The script is called
1265 without any arguments. It gets the LDIF for exactly one object on STDIN.
1266 If the script processed the object successfully it has to respond with a
1267 single line starting with 'DONE-EXIT: ' followed by an optional message.
1269 Note that the script might be called without any password change, e.g. if
1270 the account was disabled (an userAccountControl change) or the
1271 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1272 are always returned as unique identifier of the account. It might be useful
1273 to also ask for non-password attributes like: objectSid, sAMAccountName,
1274 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1275 Depending on the object, some attributes may not be present/available,
1276 but you always get the current state (and not a diff).
1278 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1281 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1282 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1283 (!(sAMAccountName=krbtgt*)))
1284 This means only normal (non-krbtgt) user
1285 accounts are monitored. The '--filter' can modify that, e.g. if it's
1286 required to also sync computer accounts.
1292 This (default) mode runs in an endless loop waiting for password related
1293 changes in the active directory database. It makes use of the
1294 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1295 get changes in a reliable fashion. Objects are monitored for changes of the
1296 following dirsyncAttributes:
1298 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1299 userPrincipalName and userAccountControl.
1301 It recovers from LDAP disconnects and updates the cache in conservative way
1302 (in single steps after each succesfully processed change). An error from
1303 the script (specified by '--script') will result in fatal error and this
1304 command will exit. But the cache state should be still valid and can be
1305 resumed in the next "Sync Loop Run".
1307 The '--logfile' option specifies an optional (required if '--daemon' is
1308 specified) logfile that takes all output of the command. The logfile is
1309 automatically reopened if fstat returns st_nlink == 0.
1311 The optional '--daemon' option will put the command into the background.
1313 You can stop the command without the '--daemon' option, also by hitting
1316 If you specify the '--no-wait' option the command skips the
1317 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1318 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1323 In order to terminate an already running command (likely as daemon) the
1324 '--terminate' option can be used. This also requires the '--logfile' option
1329 samba-tool user syncpasswords --cache-ldb-initialize \\
1330 --attributes=virtualClearTextUTF8
1331 samba-tool user syncpasswords
1334 samba-tool user syncpasswords --cache-ldb-initialize \\
1335 --attributes=objectGUID,objectSID,sAMAccountName,\\
1336 userPrincipalName,userAccountControl,pwdLastSet,\\
1337 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1338 --script=/path/to/my-custom-syncpasswords-script.py
1339 samba-tool user syncpasswords --daemon \\
1340 --logfile=/var/log/samba/user-syncpasswords.log
1341 samba-tool user syncpasswords --terminate \\
1342 --logfile=/var/log/samba/user-syncpasswords.log
1346 super(cmd_user_syncpasswords, self).__init__()
1348 synopsis = "%prog [--cache-ldb-initialize] [options]"
1350 takes_optiongroups = {
1351 "sambaopts": options.SambaOptions,
1352 "versionopts": options.VersionOptions,
1356 Option("--cache-ldb-initialize",
1357 help="Initialize the cache for the first time",
1358 dest="cache_ldb_initialize", action="store_true"),
1359 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1360 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1361 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1362 metavar="URL", dest="H"),
1363 Option("--filter", help="optional LDAP filter to set password on", type=str,
1364 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1365 Option("--attributes", type=str,
1366 help=virtual_attributes_help,
1367 metavar="ATTRIBUTELIST", dest="attributes"),
1368 Option("--decrypt-samba-gpg",
1369 help=decrypt_samba_gpg_help,
1370 action="store_true", default=False, dest="decrypt_samba_gpg"),
1371 Option("--script", help="Script that is called for each password change", type=str,
1372 metavar="/path/to/syncpasswords.script", dest="script"),
1373 Option("--no-wait", help="Don't block waiting for changes",
1374 action="store_true", default=False, dest="nowait"),
1375 Option("--logfile", type=str,
1376 help="The logfile to use (required in --daemon mode).",
1377 metavar="/path/to/syncpasswords.log", dest="logfile"),
1378 Option("--daemon", help="daemonize after initial setup",
1379 action="store_true", default=False, dest="daemon"),
1380 Option("--terminate",
1381 help="Send a SIGTERM to an already running (daemon) process",
1382 action="store_true", default=False, dest="terminate"),
1385 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1386 H=None, filter=None,
1387 attributes=None, decrypt_samba_gpg=None,
1388 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1389 sambaopts=None, versionopts=None):
1391 self.lp = sambaopts.get_loadparm()
1393 self.samdb_url = None
1397 if not cache_ldb_initialize:
1398 if attributes is not None:
1399 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1400 if decrypt_samba_gpg:
1401 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1402 if script is not None:
1403 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1404 if filter is not None:
1405 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1407 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1409 if nowait is not False:
1410 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1411 if logfile is not None:
1412 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1413 if daemon is not False:
1414 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1415 if terminate is not False:
1416 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1420 raise CommandError("--daemon is not allowed together with --no-wait")
1421 if terminate is not False:
1422 raise CommandError("--terminate is not allowed together with --no-wait")
1424 if terminate is True and daemon is True:
1425 raise CommandError("--terminate is not allowed together with --daemon")
1427 if daemon is True and logfile is None:
1428 raise CommandError("--daemon is only allowed together with --logfile")
1430 if terminate is True and logfile is None:
1431 raise CommandError("--terminate is only allowed together with --logfile")
1433 if script is not None:
1434 if not os.path.exists(script):
1435 raise CommandError("script[%s] does not exist!" % script)
1437 sync_command = "%s" % os.path.abspath(script)
1441 dirsync_filter = filter
1442 if dirsync_filter is None:
1443 dirsync_filter = "(&" + \
1444 "(objectClass=user)" + \
1445 "(userAccountControl:%s:=%u)" % (
1446 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
1447 "(!(sAMAccountName=krbtgt*))" + \
1450 dirsync_secret_attrs = [
1453 "supplementalCredentials",
1456 dirsync_attrs = dirsync_secret_attrs + [
1459 "userPrincipalName",
1460 "userAccountControl",
1465 password_attrs = None
1467 if cache_ldb_initialize:
1469 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
1471 if decrypt_samba_gpg and not gpgme_support:
1472 raise CommandError(decrypt_samba_gpg_help)
1474 password_attrs = self.parse_attributes(attributes)
1475 lower_attrs = [x.lower() for x in password_attrs]
1476 # We always return these in order to track deletions
1477 for a in ["objectGUID", "isDeleted", "isRecycled"]:
1478 if a.lower() not in lower_attrs:
1479 password_attrs += [a]
1481 if cache_ldb is not None:
1482 if cache_ldb.lower().startswith("ldapi://"):
1483 raise CommandError("--cache_ldb ldapi:// is not supported")
1484 elif cache_ldb.lower().startswith("ldap://"):
1485 raise CommandError("--cache_ldb ldap:// is not supported")
1486 elif cache_ldb.lower().startswith("ldaps://"):
1487 raise CommandError("--cache_ldb ldaps:// is not supported")
1488 elif cache_ldb.lower().startswith("tdb://"):
1491 if not os.path.exists(cache_ldb):
1492 cache_ldb = self.lp.private_path(cache_ldb)
1494 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
1496 self.lockfile = "%s.pid" % cache_ldb
1499 if self.logfile is not None:
1501 if info.st_nlink == 0:
1502 logfile = self.logfile
1504 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
1505 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
1510 log_msg("Reopened logfile[%s]\n" % (logfile))
1511 self.logfile = logfile
1512 msg = "%s: pid[%d]: %s" % (
1516 self.outf.write(msg)
1525 "passwordAttribute",
1531 self.cache = Ldb(cache_ldb)
1532 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
1533 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
1537 self.samdb_url = res[0]["samdbUrl"][0]
1538 except KeyError as e:
1539 self.samdb_url = None
1541 self.samdb_url = None
1542 if self.samdb_url is None and not cache_ldb_initialize:
1543 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
1545 if self.samdb_url is not None and cache_ldb_initialize:
1546 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
1548 if self.samdb_url is None:
1550 self.dirsync_filter = dirsync_filter
1551 self.dirsync_attrs = dirsync_attrs
1552 self.dirsync_controls = ["dirsync:1:0:0","extended_dn:1:0"];
1553 self.password_attrs = password_attrs
1554 self.decrypt_samba_gpg = decrypt_samba_gpg
1555 self.sync_command = sync_command
1556 add_ldif = "dn: %s\n" % self.cache_dn
1557 add_ldif += "objectClass: userSyncPasswords\n"
1558 add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url)
1559 add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter)
1560 for a in self.dirsync_attrs:
1561 add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a)
1562 add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
1563 for a in self.password_attrs:
1564 add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
1565 if self.decrypt_samba_gpg == True:
1566 add_ldif += "decryptSambaGPG: TRUE\n"
1568 add_ldif += "decryptSambaGPG: FALSE\n"
1569 if self.sync_command is not None:
1570 add_ldif += "syncCommand: %s\n" % self.sync_command
1571 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1572 self.cache.add_ldif(add_ldif)
1573 self.current_pid = None
1574 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
1575 msgs = self.cache.parse_ldif(add_ldif)
1576 changetype,msg = msgs.next()
1577 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
1578 self.outf.write("%s" % ldif)
1580 self.dirsync_filter = res[0]["dirsyncFilter"][0]
1581 self.dirsync_attrs = []
1582 for a in res[0]["dirsyncAttribute"]:
1583 self.dirsync_attrs.append(a)
1584 self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
1585 self.password_attrs = []
1586 for a in res[0]["passwordAttribute"]:
1587 self.password_attrs.append(a)
1588 decrypt_string = res[0]["decryptSambaGPG"][0]
1589 assert(decrypt_string in ["TRUE", "FALSE"])
1590 if decrypt_string == "TRUE":
1591 self.decrypt_samba_gpg = True
1593 self.decrypt_samba_gpg = False
1594 if "syncCommand" in res[0]:
1595 self.sync_command = res[0]["syncCommand"][0]
1597 self.sync_command = None
1598 if "currentPid" in res[0]:
1599 self.current_pid = int(res[0]["currentPid"][0])
1601 self.current_pid = None
1602 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
1606 def run_sync_command(dn, ldif):
1607 log_msg("Call Popen[%s] for %s\n" % (dn, self.sync_command))
1608 sync_command_p = Popen(self.sync_command,
1613 res = sync_command_p.poll()
1616 input = "%s" % (ldif)
1617 reply = sync_command_p.communicate(input)[0]
1618 log_msg("%s\n" % (reply))
1619 res = sync_command_p.poll()
1621 sync_command_p.terminate()
1622 res = sync_command_p.wait()
1624 if reply.startswith("DONE-EXIT: "):
1627 log_msg("RESULT: %s\n" % (res))
1628 raise Exception("ERROR: %s - %s\n" % (res, reply))
1630 def handle_object(idx, dirsync_obj):
1631 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
1632 guid = ndr_unpack(misc.GUID, binary_guid)
1633 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1634 sid = ndr_unpack(security.dom_sid, binary_sid)
1635 domain_sid, rid = sid.split()
1636 if rid == security.DOMAIN_RID_KRBTGT:
1637 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
1639 for a in list(dirsync_obj.keys()):
1640 for h in dirsync_secret_attrs:
1641 if a.lower() == h.lower():
1643 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
1644 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
1645 log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif))
1646 obj = self.get_account_attributes(self.samdb,
1647 username="%s" % sid,
1648 basedn="<GUID=%s>" % guid,
1649 filter="(objectClass=user)",
1650 scope=ldb.SCOPE_BASE,
1651 attrs=self.password_attrs,
1652 decrypt=self.decrypt_samba_gpg)
1653 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1654 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
1655 if self.sync_command is None:
1656 self.outf.write("%s" % (ldif))
1658 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
1659 run_sync_command(obj.dn, ldif)
1661 def check_current_pid_conflict(terminate):
1667 self.lockfd = os.open(self.lockfile, flags, 0600)
1668 except IOError as (err, msg):
1669 if err == errno.ENOENT:
1672 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
1673 (self.lockfile, msg, err))
1676 got_exclusive = False
1678 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1679 got_exclusive = True
1680 except IOError as (err, msg):
1681 if err != errno.EACCES and err != errno.EAGAIN:
1682 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
1683 (self.lockfile, msg, err))
1686 if not got_exclusive:
1687 buf = os.read(self.lockfd, 64)
1688 self.current_pid = None
1690 self.current_pid = int(buf)
1691 except ValueError as e:
1693 if self.current_pid is not None:
1696 if got_exclusive and terminate:
1698 os.ftruncate(self.lockfd, 0)
1699 except IOError as (err, msg):
1700 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
1701 (self.lockfile, msg, err))
1703 os.close(self.lockfd)
1708 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
1709 except IOError as (err, msg):
1710 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
1711 (self.lockfile, msg, err))
1713 # We leave the function with the shared lock.
1716 def update_pid(pid):
1717 if self.lockfd != -1:
1718 got_exclusive = False
1719 # Try 5 times to get the exclusiv lock.
1720 for i in xrange(0, 5):
1722 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
1723 got_exclusive = True
1724 except IOError as (err, msg):
1725 if err != errno.EACCES and err != errno.EAGAIN:
1726 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
1727 (pid, self.lockfile, msg, err))
1732 if not got_exclusive:
1733 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s" %
1734 (pid, self.lockfile))
1735 raise CommandError("update_pid(%r): failed to get exclusive lock[%s] after 5 seconds" %
1736 (pid, self.lockfile))
1743 os.ftruncate(self.lockfd, 0)
1745 os.write(self.lockfd, buf)
1746 except IOError as (err, msg):
1747 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
1748 (self.lockfile, msg, err))
1750 self.current_pid = pid
1751 if self.current_pid is not None:
1752 log_msg("currentPid: %d\n" % self.current_pid)
1754 modify_ldif = "dn: %s\n" % (self.cache_dn)
1755 modify_ldif += "changetype: modify\n"
1756 modify_ldif += "replace: currentPid\n"
1757 if self.current_pid is not None:
1758 modify_ldif += "currentPid: %d\n" % (self.current_pid)
1759 modify_ldif += "replace: currentTime\n"
1760 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1761 self.cache.modify_ldif(modify_ldif)
1764 def update_cache(res_controls):
1765 assert len(res_controls) > 0
1766 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
1767 res_controls[0].critical = True
1768 self.dirsync_controls = [str(res_controls[0]),"extended_dn:1:0"]
1769 log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
1771 modify_ldif = "dn: %s\n" % (self.cache_dn)
1772 modify_ldif += "changetype: modify\n"
1773 modify_ldif += "replace: dirsyncControl\n"
1774 modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
1775 modify_ldif += "replace: currentTime\n"
1776 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1777 self.cache.modify_ldif(modify_ldif)
1780 def check_object(dirsync_obj, res_controls):
1781 assert len(res_controls) > 0
1782 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
1784 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1785 sid = ndr_unpack(security.dom_sid, binary_sid)
1787 lastCookie = str(res_controls[0])
1789 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
1790 expression="(lastCookie=%s)" % (
1791 ldb.binary_encode(lastCookie)),
1797 def update_object(dirsync_obj, res_controls):
1798 assert len(res_controls) > 0
1799 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
1801 binary_sid = dirsync_obj.dn.get_extended_component("SID")
1802 sid = ndr_unpack(security.dom_sid, binary_sid)
1804 lastCookie = str(res_controls[0])
1806 self.cache.transaction_start()
1808 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
1809 expression="(objectClass=*)",
1810 attrs=["lastCookie"])
1812 add_ldif = "dn: %s\n" % (dn)
1813 add_ldif += "objectClass: userCookie\n"
1814 add_ldif += "lastCookie: %s\n" % (lastCookie)
1815 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1816 self.cache.add_ldif(add_ldif)
1818 modify_ldif = "dn: %s\n" % (dn)
1819 modify_ldif += "changetype: modify\n"
1820 modify_ldif += "replace: lastCookie\n"
1821 modify_ldif += "lastCookie: %s\n" % (lastCookie)
1822 modify_ldif += "replace: currentTime\n"
1823 modify_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
1824 self.cache.modify_ldif(modify_ldif)
1825 self.cache.transaction_commit()
1826 except Exception as e:
1827 self.cache.transaction_cancel()
1833 res = self.samdb.search(expression=self.dirsync_filter,
1834 scope=ldb.SCOPE_SUBTREE,
1835 attrs=self.dirsync_attrs,
1836 controls=self.dirsync_controls)
1837 log_msg("dirsync_loop(): results %d\n" % len(res))
1840 done = check_object(r, res.controls)
1842 handle_object(ri, r)
1843 update_object(r, res.controls)
1845 update_cache(res.controls)
1849 def sync_loop(wait):
1850 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
1851 notify_controls = ["notification:1"]
1852 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
1853 scope=ldb.SCOPE_SUBTREE,
1855 controls=notify_controls,
1859 log_msg("Resuming monitoring\n")
1861 log_msg("Getting changes\n")
1862 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
1863 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
1864 self.outf.write("syncCommand: %s\n" % self.sync_command)
1867 if wait is not True:
1870 for msg in notify_handle:
1871 if not isinstance(msg, ldb.Message):
1872 self.outf.write("referal: %s\n" % msg)
1874 created = msg.get("uSNCreated")[0]
1875 changed = msg.get("uSNChanged")[0]
1876 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
1877 (msg.dn, created, changed))
1881 res = notify_handle.result()
1886 orig_pid = os.getpid()
1891 if pid == 0: # Actual daemon
1893 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
1898 if cache_ldb_initialize:
1900 self.samdb = self.connect_system_samdb(url=self.samdb_url,
1905 if logfile is not None:
1906 import resource # Resource usage information.
1907 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1908 if maxfd == resource.RLIM_INFINITY:
1909 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
1910 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
1911 self.outf.write("Using logfile[%s]\n" % logfile)
1912 for fd in range(0, maxfd):
1923 log_msg("Attached to logfile[%s]\n" % (logfile))
1924 self.logfile = logfile
1927 conflict = check_current_pid_conflict(terminate)
1929 if self.current_pid is None:
1930 log_msg("No process running.\n")
1933 log_msg("Proccess %d is not running anymore.\n" % (
1937 log_msg("Sending SIGTERM to proccess %d.\n" % (
1939 os.kill(self.current_pid, signal.SIGTERM)
1942 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
1943 os.getpid(), self.current_pid))
1947 update_pid(os.getpid())
1952 retry_sleep_max = 600
1957 retry_sleep = retry_sleep_min
1959 while self.samdb is None:
1960 if retry_sleep != 0:
1961 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
1962 time.sleep(retry_sleep)
1963 retry_sleep = retry_sleep * 2
1964 if retry_sleep >= retry_sleep_max:
1965 retry_sleep = retry_sleep_max
1966 log_msg("Connecting to '%s'\n" % self.samdb_url)
1968 self.samdb = self.connect_system_samdb(url=self.samdb_url)
1969 except Exception as msg:
1971 log_msg("Connect to samdb Exception => (%s)\n" % msg)
1972 if wait is not True:
1977 except ldb.LdbError as (enum, estr):
1979 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
1984 class cmd_user(SuperCommand):
1985 """User management."""
1988 subcommands["add"] = cmd_user_add()
1989 subcommands["create"] = cmd_user_create()
1990 subcommands["delete"] = cmd_user_delete()
1991 subcommands["disable"] = cmd_user_disable()
1992 subcommands["enable"] = cmd_user_enable()
1993 subcommands["list"] = cmd_user_list()
1994 subcommands["setexpiry"] = cmd_user_setexpiry()
1995 subcommands["password"] = cmd_user_password()
1996 subcommands["setpassword"] = cmd_user_setpassword()
1997 subcommands["getpassword"] = cmd_user_getpassword()
1998 subcommands["syncpasswords"] = cmd_user_syncpasswords()