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
28 from getpass import getpass
29 from samba.auth import system_session
30 from samba.samdb import SamDB
31 from samba.dcerpc import misc
32 from samba.dcerpc import security
33 from samba.dcerpc import drsblobs
34 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
39 generate_random_password,
41 from samba.net import Net
43 from samba.netcmd import (
50 disabled_virtual_attributes = {
53 virtual_attributes = {
54 "virtualClearTextUTF8": {
55 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
57 "virtualClearTextUTF16": {
58 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
62 get_random_bytes_fn = None
63 if get_random_bytes_fn is None:
66 get_random_bytes_fn = Crypto.Random.get_random_bytes
67 except ImportError as e:
69 if get_random_bytes_fn is None:
72 get_random_bytes_fn = M2Crypto.Rand.rand_bytes
73 except ImportError as e:
77 if get_random_bytes_fn is not None:
79 return "Crypto.Random or M2Crypto.Rand required"
81 def get_random_bytes(num):
82 random_reason = check_random()
83 if random_reason is not None:
84 raise ImportError(random_reason)
85 return get_random_bytes_fn(num)
87 def get_crypt_value(alg, utf8pw):
93 salt = get_random_bytes(16)
94 # The salt needs to be in [A-Za-z0-9./]
95 # base64 is close enough and as we had 16
96 # random bytes but only need 16 characters
97 # we can ignore the possible == at the end
98 # of the base64 string
99 # we just need to replace '+' by '.'
100 b64salt = base64.b64encode(salt)
101 crypt_salt = "$%s$%s$" % (alg, b64salt[0:16].replace('+', '.'))
102 crypt_value = crypt.crypt(utf8pw, crypt_salt)
103 if crypt_value is None:
104 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
105 expected_len = len(crypt_salt) + algs[alg]["length"]
106 if len(crypt_value) != expected_len:
107 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
108 crypt_salt, len(crypt_value), expected_len))
112 random_reason = check_random()
113 if random_reason is not None:
114 raise ImportError(random_reason)
118 virtual_attributes["virtualSSHA"] = {
120 except ImportError as e:
121 reason = "hashlib.sha1()"
123 reason += " and " + random_reason
124 reason += " required"
125 disabled_virtual_attributes["virtualSSHA"] = {
129 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
131 random_reason = check_random()
132 if random_reason is not None:
133 raise ImportError(random_reason)
135 v = get_crypt_value(alg, "")
137 virtual_attributes[attr] = {
139 except ImportError as e:
142 reason += " and " + random_reason
143 reason += " required"
144 disabled_virtual_attributes[attr] = {
147 except NotImplementedError as e:
148 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
149 disabled_virtual_attributes[attr] = {
153 virtual_attributes_help = "The attributes to display (comma separated). "
154 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
155 if len(disabled_virtual_attributes) != 0:
156 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
158 class cmd_user_create(Command):
159 """Create a new user.
161 This command creates a new user account in the Active Directory domain. The username specified on the command is the sAMaccountName.
163 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).
165 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.
167 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.
169 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.
172 samba-tool user create User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
174 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.
177 sudo samba-tool user create User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
179 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.
182 samba-tool user create User3 passw3rd --userou='OU=OrgUnit'
184 Example3 shows how to create a new user in the OrgUnit organizational unit.
187 samba-tool user create User4 passw4rd --rfc2307-from-nss --gecos 'some text'
189 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'.
192 samba-tool user create User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \
193 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
195 Example5 shows how to create an RFC2307/NIS domain enabled user account. If
196 --nis-domain is set, then the other four parameters are mandatory.
199 synopsis = "%prog <username> [<password>] [options]"
202 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
203 metavar="URL", dest="H"),
204 Option("--must-change-at-next-login",
205 help="Force password to be changed on next login",
206 action="store_true"),
207 Option("--random-password",
208 help="Generate random password",
209 action="store_true"),
210 Option("--smartcard-required",
211 help="Require a smartcard for interactive logons",
212 action="store_true"),
213 Option("--use-username-as-cn",
214 help="Force use of username as user's CN",
215 action="store_true"),
217 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>'",
219 Option("--surname", help="User's surname", type=str),
220 Option("--given-name", help="User's given name", type=str),
221 Option("--initials", help="User's initials", type=str),
222 Option("--profile-path", help="User's profile path", type=str),
223 Option("--script-path", help="User's logon script path", type=str),
224 Option("--home-drive", help="User's home drive letter", type=str),
225 Option("--home-directory", help="User's home directory path", type=str),
226 Option("--job-title", help="User's job title", type=str),
227 Option("--department", help="User's department", type=str),
228 Option("--company", help="User's company", type=str),
229 Option("--description", help="User's description", type=str),
230 Option("--mail-address", help="User's email address", type=str),
231 Option("--internet-address", help="User's home page", type=str),
232 Option("--telephone-number", help="User's phone number", type=str),
233 Option("--physical-delivery-office", help="User's office location", type=str),
234 Option("--rfc2307-from-nss",
235 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
236 action="store_true"),
237 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
238 Option("--unix-home", help="User's Unix/RFC2307 home directory",
240 Option("--uid", help="User's Unix/RFC2307 username", type=str),
241 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
242 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
243 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
244 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
247 takes_args = ["username", "password?"]
249 takes_optiongroups = {
250 "sambaopts": options.SambaOptions,
251 "credopts": options.CredentialsOptions,
252 "versionopts": options.VersionOptions,
255 def run(self, username, password=None, credopts=None, sambaopts=None,
256 versionopts=None, H=None, must_change_at_next_login=False,
257 random_password=False, use_username_as_cn=False, userou=None,
258 surname=None, given_name=None, initials=None, profile_path=None,
259 script_path=None, home_drive=None, home_directory=None,
260 job_title=None, department=None, company=None, description=None,
261 mail_address=None, internet_address=None, telephone_number=None,
262 physical_delivery_office=None, rfc2307_from_nss=False,
263 nis_domain=None, unix_home=None, uid=None, uid_number=None,
264 gid_number=None, gecos=None, login_shell=None,
265 smartcard_required=False):
267 if smartcard_required:
268 if password is not None and password is not '':
269 raise CommandError('It is not allowed to specifiy '
271 'together with --smartcard-required.')
272 if must_change_at_next_login:
273 raise CommandError('It is not allowed to specifiy '
274 '--must-change-at-next-login '
275 'together with --smartcard-required.')
277 if random_password and not smartcard_required:
278 password = generate_random_password(128, 255)
281 if smartcard_required:
283 if password is not None and password is not '':
285 password = getpass("New Password: ")
286 passwordverify = getpass("Retype Password: ")
287 if not password == passwordverify:
289 self.outf.write("Sorry, passwords do not match.\n")
292 pwent = pwd.getpwnam(username)
295 if uid_number is None:
296 uid_number = pwent[2]
297 if gid_number is None:
298 gid_number = pwent[3]
301 if login_shell is None:
302 login_shell = pwent[6]
304 lp = sambaopts.get_loadparm()
305 creds = credopts.get_credentials(lp)
307 if uid_number or gid_number:
308 if not lp.get("idmap_ldb:use rfc2307"):
309 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")
311 if nis_domain is not None:
312 if None in (uid_number, login_shell, unix_home, gid_number):
313 raise CommandError('Missing parameters. To enable NIS features, '
314 'the following options have to be given: '
315 '--nis-domain=, --uidNumber=, --login-shell='
316 ', --unix-home=, --gid-number= Operation '
320 samdb = SamDB(url=H, session_info=system_session(),
321 credentials=creds, lp=lp)
322 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
323 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
324 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
325 jobtitle=job_title, department=department, company=company, description=description,
326 mailaddress=mail_address, internetaddress=internet_address,
327 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
328 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
329 uidnumber=uid_number, gidnumber=gid_number,
330 gecos=gecos, loginshell=login_shell,
331 smartcard_required=smartcard_required)
333 raise CommandError("Failed to add user '%s': " % username, e)
335 self.outf.write("User '%s' created successfully\n" % username)
338 class cmd_user_add(cmd_user_create):
339 __doc__ = cmd_user_create.__doc__
340 # take this print out after the add subcommand is removed.
341 # the add subcommand is deprecated but left in for now to allow people to
344 def run(self, *args, **kwargs):
346 "Note: samba-tool user add is deprecated. "
347 "Please use samba-tool user create for the same function.\n")
348 return super(cmd_user_add, self).run(*args, **kwargs)
351 class cmd_user_delete(Command):
354 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
356 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.
358 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.
361 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
363 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.
366 sudo samba-tool user delete User2
368 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.
371 synopsis = "%prog <username> [options]"
374 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
375 metavar="URL", dest="H"),
378 takes_args = ["username"]
379 takes_optiongroups = {
380 "sambaopts": options.SambaOptions,
381 "credopts": options.CredentialsOptions,
382 "versionopts": options.VersionOptions,
385 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
387 lp = sambaopts.get_loadparm()
388 creds = credopts.get_credentials(lp, fallback_machine=True)
391 samdb = SamDB(url=H, session_info=system_session(),
392 credentials=creds, lp=lp)
393 samdb.deleteuser(username)
395 raise CommandError('Failed to remove user "%s"' % username, e)
396 self.outf.write("Deleted user %s\n" % username)
399 class cmd_user_list(Command):
400 """List all users."""
402 synopsis = "%prog [options]"
405 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
406 metavar="URL", dest="H"),
409 takes_optiongroups = {
410 "sambaopts": options.SambaOptions,
411 "credopts": options.CredentialsOptions,
412 "versionopts": options.VersionOptions,
415 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
416 lp = sambaopts.get_loadparm()
417 creds = credopts.get_credentials(lp, fallback_machine=True)
419 samdb = SamDB(url=H, session_info=system_session(),
420 credentials=creds, lp=lp)
422 domain_dn = samdb.domain_dn()
423 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
424 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
425 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
426 attrs=["samaccountname"])
431 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
434 class cmd_user_enable(Command):
437 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.
439 There are many reasons why an account may become disabled. These include:
440 - If a user exceeds the account policy for logon attempts
441 - If an administrator disables the account
442 - If the account expires
444 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
446 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.
448 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.
451 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
453 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.
456 su samba-tool user enable Testuser2
458 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.
461 samba-tool user enable --filter=samaccountname=Testuser3
463 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
466 synopsis = "%prog (<username>|--filter <filter>) [options]"
469 takes_optiongroups = {
470 "sambaopts": options.SambaOptions,
471 "versionopts": options.VersionOptions,
472 "credopts": options.CredentialsOptions,
476 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
477 metavar="URL", dest="H"),
478 Option("--filter", help="LDAP Filter to set password on", type=str),
481 takes_args = ["username?"]
483 def run(self, username=None, sambaopts=None, credopts=None,
484 versionopts=None, filter=None, H=None):
485 if username is None and filter is None:
486 raise CommandError("Either the username or '--filter' must be specified!")
489 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
491 lp = sambaopts.get_loadparm()
492 creds = credopts.get_credentials(lp, fallback_machine=True)
494 samdb = SamDB(url=H, session_info=system_session(),
495 credentials=creds, lp=lp)
497 samdb.enable_account(filter)
498 except Exception, msg:
499 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
500 self.outf.write("Enabled user '%s'\n" % (username or filter))
503 class cmd_user_disable(Command):
504 """Disable an user."""
506 synopsis = "%prog (<username>|--filter <filter>) [options]"
509 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
510 metavar="URL", dest="H"),
511 Option("--filter", help="LDAP Filter to set password on", type=str),
514 takes_args = ["username?"]
516 takes_optiongroups = {
517 "sambaopts": options.SambaOptions,
518 "credopts": options.CredentialsOptions,
519 "versionopts": options.VersionOptions,
522 def run(self, username=None, sambaopts=None, credopts=None,
523 versionopts=None, filter=None, H=None):
524 if username is None and filter is None:
525 raise CommandError("Either the username or '--filter' must be specified!")
528 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
530 lp = sambaopts.get_loadparm()
531 creds = credopts.get_credentials(lp, fallback_machine=True)
533 samdb = SamDB(url=H, session_info=system_session(),
534 credentials=creds, lp=lp)
536 samdb.disable_account(filter)
537 except Exception, msg:
538 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
541 class cmd_user_setexpiry(Command):
542 """Set the expiration of a user account.
544 The user can either be specified by their sAMAccountName or using the --filter option.
546 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.
548 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.
551 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
553 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.
556 su samba-tool user setexpiry User2
558 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.
561 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
563 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.
566 samba-tool user setexpiry --noexpiry User4
567 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
570 synopsis = "%prog (<username>|--filter <filter>) [options]"
572 takes_optiongroups = {
573 "sambaopts": options.SambaOptions,
574 "versionopts": options.VersionOptions,
575 "credopts": options.CredentialsOptions,
579 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
580 metavar="URL", dest="H"),
581 Option("--filter", help="LDAP Filter to set password on", type=str),
582 Option("--days", help="Days to expiry", type=int, default=0),
583 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
586 takes_args = ["username?"]
588 def run(self, username=None, sambaopts=None, credopts=None,
589 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
590 if username is None and filter is None:
591 raise CommandError("Either the username or '--filter' must be specified!")
594 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
596 lp = sambaopts.get_loadparm()
597 creds = credopts.get_credentials(lp)
599 samdb = SamDB(url=H, session_info=system_session(),
600 credentials=creds, lp=lp)
603 samdb.setexpiry(filter, days*24*3600, no_expiry_req=noexpiry)
604 except Exception, msg:
605 # FIXME: Catch more specific exception
606 raise CommandError("Failed to set expiry for user '%s': %s" % (
607 username or filter, msg))
609 self.outf.write("Expiry for user '%s' disabled.\n" % (
612 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
613 username or filter, days))
616 class cmd_user_password(Command):
617 """Change password for a user account (the one provided in authentication).
620 synopsis = "%prog [options]"
623 Option("--newpassword", help="New password", type=str),
626 takes_optiongroups = {
627 "sambaopts": options.SambaOptions,
628 "credopts": options.CredentialsOptions,
629 "versionopts": options.VersionOptions,
632 def run(self, credopts=None, sambaopts=None, versionopts=None,
635 lp = sambaopts.get_loadparm()
636 creds = credopts.get_credentials(lp)
638 # get old password now, to get the password prompts in the right order
639 old_password = creds.get_password()
641 net = Net(creds, lp, server=credopts.ipaddress)
643 password = newpassword
645 if password is not None and password is not '':
647 password = getpass("New Password: ")
648 passwordverify = getpass("Retype Password: ")
649 if not password == passwordverify:
651 self.outf.write("Sorry, passwords do not match.\n")
654 net.change_password(password)
655 except Exception, msg:
656 # FIXME: catch more specific exception
657 raise CommandError("Failed to change password : %s" % msg)
658 self.outf.write("Changed password OK\n")
661 class cmd_user_setpassword(Command):
662 """Set or reset the password of a user account.
664 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.
666 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.
668 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.
670 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.
673 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
675 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.
678 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
680 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.
683 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
685 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
688 synopsis = "%prog (<username>|--filter <filter>) [options]"
690 takes_optiongroups = {
691 "sambaopts": options.SambaOptions,
692 "versionopts": options.VersionOptions,
693 "credopts": options.CredentialsOptions,
697 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
698 metavar="URL", dest="H"),
699 Option("--filter", help="LDAP Filter to set password on", type=str),
700 Option("--newpassword", help="Set password", type=str),
701 Option("--must-change-at-next-login",
702 help="Force password to be changed on next login",
703 action="store_true"),
704 Option("--random-password",
705 help="Generate random password",
706 action="store_true"),
707 Option("--smartcard-required",
708 help="Require a smartcard for interactive logons",
709 action="store_true"),
710 Option("--clear-smartcard-required",
711 help="Don't require a smartcard for interactive logons",
712 action="store_true"),
715 takes_args = ["username?"]
717 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
718 versionopts=None, H=None, newpassword=None,
719 must_change_at_next_login=False, random_password=False,
720 smartcard_required=False, clear_smartcard_required=False):
721 if filter is None and username is None:
722 raise CommandError("Either the username or '--filter' must be specified!")
724 password = newpassword
726 if smartcard_required:
727 if password is not None and password is not '':
728 raise CommandError('It is not allowed to specifiy '
730 'together with --smartcard-required.')
731 if must_change_at_next_login:
732 raise CommandError('It is not allowed to specifiy '
733 '--must-change-at-next-login '
734 'together with --smartcard-required.')
735 if clear_smartcard_required:
736 raise CommandError('It is not allowed to specifiy '
737 '--clear-smartcard-required '
738 'together with --smartcard-required.')
740 if random_password and not smartcard_required:
741 password = generate_random_password(128, 255)
744 if smartcard_required:
746 if password is not None and password is not '':
748 password = getpass("New Password: ")
749 passwordverify = getpass("Retype Password: ")
750 if not password == passwordverify:
752 self.outf.write("Sorry, passwords do not match.\n")
755 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
757 lp = sambaopts.get_loadparm()
758 creds = credopts.get_credentials(lp)
760 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
762 samdb = SamDB(url=H, session_info=system_session(),
763 credentials=creds, lp=lp)
765 if smartcard_required:
768 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
769 flags = dsdb.UF_SMARTCARD_REQUIRED
770 samdb.toggle_userAccountFlags(filter, flags, on=True)
771 command = "Failed to enable account for user '%s'" % (username or filter)
772 samdb.enable_account(filter)
773 except Exception, msg:
774 # FIXME: catch more specific exception
775 raise CommandError("%s: %s" % (command, msg))
776 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
780 if clear_smartcard_required:
781 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
782 flags = dsdb.UF_SMARTCARD_REQUIRED
783 samdb.toggle_userAccountFlags(filter, flags, on=False)
784 command = "Failed to set password for user '%s'" % (username or filter)
785 samdb.setpassword(filter, password,
786 force_change_at_next_login=must_change_at_next_login,
788 except Exception, msg:
789 # FIXME: catch more specific exception
790 raise CommandError("%s: %s" % (command, msg))
791 self.outf.write("Changed password OK\n")
793 class GetPasswordCommand(Command):
796 super(GetPasswordCommand, self).__init__()
799 def connect_system_samdb(self, url, allow_local=False, verbose=False):
801 # using anonymous here, results in no authentication
802 # which means we can get system privileges via
803 # the privileged ldapi socket
804 creds = credentials.Credentials()
805 creds.set_anonymous()
807 if url is None and allow_local:
809 elif url.lower().startswith("ldapi://"):
811 elif url.lower().startswith("ldap://"):
812 raise CommandError("--url ldap:// is not supported for this command")
813 elif url.lower().startswith("ldaps://"):
814 raise CommandError("--url ldaps:// is not supported for this command")
815 elif not allow_local:
816 raise CommandError("--url requires an ldapi:// url for this command")
819 self.outf.write("Connecting to '%s'\n" % url)
821 samdb = SamDB(url=url, session_info=system_session(),
822 credentials=creds, lp=self.lp)
826 # Make sure we're connected as SYSTEM
828 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
830 sids = res[0].get("tokenGroups")
831 assert len(sids) == 1
832 sid = ndr_unpack(security.dom_sid, sids[0])
833 assert str(sid) == security.SID_NT_SYSTEM
834 except Exception as msg:
835 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
836 (security.SID_NT_SYSTEM))
838 # We use sort here in order to have a predictable processing order
839 # this might not be strictly needed, but also doesn't hurt here
840 for a in sorted(virtual_attributes.keys()):
841 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
842 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
846 def get_account_attributes(self, samdb, username,
847 basedn, filter, scope, attrs):
849 require_supplementalCredentials = False
850 search_attrs = attrs[:]
851 lower_attrs = [x.lower() for x in search_attrs]
852 for a in virtual_attributes.keys():
853 if a.lower() in lower_attrs:
854 require_supplementalCredentials = True
855 add_supplementalCredentials = False
856 if require_supplementalCredentials:
857 a = "supplementalCredentials"
858 if a.lower() not in lower_attrs:
860 add_supplementalCredentials = True
861 add_sAMAcountName = False
863 if a.lower() not in lower_attrs:
865 add_sAMAcountName = True
867 if scope == ldb.SCOPE_BASE:
868 search_controls = ["show_deleted:1", "show_recycled:1"]
872 res = samdb.search(base=basedn, expression=filter,
873 scope=scope, attrs=search_attrs,
874 controls=search_controls)
876 raise Exception('Unable to find user "%s"' % (username or filter))
878 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
879 except Exception as msg:
880 # FIXME: catch more specific exception
881 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
885 if "supplementalCredentials" in obj:
886 sc_blob = obj["supplementalCredentials"][0]
887 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
888 if add_supplementalCredentials:
889 del obj["supplementalCredentials"]
890 account_name = obj["sAMAccountName"][0]
891 if add_sAMAcountName:
892 del obj["sAMAccountName"]
894 def get_package(name):
897 for p in sc.sub.packages:
901 return binascii.a2b_hex(p.data)
904 def get_utf8(a, b, username):
906 u = unicode(b, 'utf-16-le')
907 except UnicodeDecodeError as e:
908 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
911 u8 = u.encode('utf-8')
914 # We use sort here in order to have a predictable processing order
915 for a in sorted(virtual_attributes.keys()):
916 if not a.lower() in lower_attrs:
919 if a == "virtualClearTextUTF8":
920 b = get_package("Primary:CLEARTEXT")
923 u8 = get_utf8(a, b, username or account_name)
927 elif a == "virtualClearTextUTF16":
928 v = get_package("Primary:CLEARTEXT")
931 elif a == "virtualSSHA":
932 b = get_package("Primary:CLEARTEXT")
935 u8 = get_utf8(a, b, username or account_name)
938 salt = get_random_bytes(4)
942 bv = h.digest() + salt
943 v = "{SSHA}" + base64.b64encode(bv)
944 elif a == "virtualCryptSHA256":
945 b = get_package("Primary:CLEARTEXT")
948 u8 = get_utf8(a, b, username or account_name)
951 sv = get_crypt_value("5", u8)
953 elif a == "virtualCryptSHA512":
954 b = get_package("Primary:CLEARTEXT")
957 u8 = get_utf8(a, b, username or account_name)
960 sv = get_crypt_value("6", u8)
964 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
967 def parse_attributes(self, attributes):
969 if attributes is None:
970 raise CommandError("Please specify --attributes")
971 attrs = attributes.split(',')
974 pa = pa.lstrip().rstrip()
975 for da in disabled_virtual_attributes.keys():
976 if pa.lower() == da.lower():
977 r = disabled_virtual_attributes[da]["reason"]
978 raise CommandError("Virtual attribute '%s' not supported: %s" % (
980 for va in virtual_attributes.keys():
981 if pa.lower() == va.lower():
985 password_attrs += [pa]
987 return password_attrs
989 class cmd_user_getpassword(GetPasswordCommand):
990 """Get the password fields of a user/computer account.
992 This command gets the logon password for a user/computer account.
994 The username specified on the command is the sAMAccountName.
995 The username may also be specified using the --filter option.
997 The command must be run from the root user id or another authorized user id.
998 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
999 used to adjust the local path. By default tdb:// is used by default.
1001 The '--attributes' parameter takes a comma separated list of attributes,
1002 which will be printed or given to the script specified by '--script'. If a
1003 specified attribute is not available on an object it's silently omitted.
1004 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1005 the NTHASH) and the following virtual attributes are possible (see --help
1006 for which virtual attributes are supported in your environment):
1008 virtualClearTextUTF16: The raw cleartext as stored in the
1009 'Primary:CLEARTEXT' buffer inside of the
1010 supplementalCredentials attribute. This typically
1011 contains valid UTF-16-LE, but may contain random
1012 bytes, e.g. for computer accounts.
1014 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1015 (only from valid UTF-16-LE)
1017 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1018 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1020 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1021 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1022 with a $5$... salt, see crypt(3) on modern systems.
1024 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1025 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1026 with a $6$... salt, see crypt(3) on modern systems.
1029 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1032 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1036 super(cmd_user_getpassword, self).__init__()
1038 synopsis = "%prog (<username>|--filter <filter>) [options]"
1040 takes_optiongroups = {
1041 "sambaopts": options.SambaOptions,
1042 "versionopts": options.VersionOptions,
1046 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1047 metavar="URL", dest="H"),
1048 Option("--filter", help="LDAP Filter to set password on", type=str),
1049 Option("--attributes", type=str,
1050 help=virtual_attributes_help,
1051 metavar="ATTRIBUTELIST", dest="attributes"),
1054 takes_args = ["username?"]
1056 def run(self, username=None, H=None, filter=None,
1058 sambaopts=None, versionopts=None):
1059 self.lp = sambaopts.get_loadparm()
1061 if filter is None and username is None:
1062 raise CommandError("Either the username or '--filter' must be specified!")
1065 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1067 if attributes is None:
1068 raise CommandError("Please specify --attributes")
1070 password_attrs = self.parse_attributes(attributes)
1072 samdb = self.connect_system_samdb(url=H, allow_local=True)
1074 obj = self.get_account_attributes(samdb, username,
1077 scope=ldb.SCOPE_SUBTREE,
1078 attrs=password_attrs)
1080 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1081 self.outf.write("%s" % ldif)
1082 self.outf.write("Got password OK\n")
1084 class cmd_user(SuperCommand):
1085 """User management."""
1088 subcommands["add"] = cmd_user_add()
1089 subcommands["create"] = cmd_user_create()
1090 subcommands["delete"] = cmd_user_delete()
1091 subcommands["disable"] = cmd_user_disable()
1092 subcommands["enable"] = cmd_user_enable()
1093 subcommands["list"] = cmd_user_list()
1094 subcommands["setexpiry"] = cmd_user_setexpiry()
1095 subcommands["password"] = cmd_user_password()
1096 subcommands["setpassword"] = cmd_user_setpassword()
1097 subcommands["getpassword"] = cmd_user_getpassword()