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
33 from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError
34 from getpass import getpass
35 from samba.auth import system_session
36 from samba.samdb import SamDB, SamDBError, SamDBNotFoundError
37 from samba.dcerpc import misc
38 from samba.dcerpc import security
39 from samba.dcerpc import drsblobs
40 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
45 generate_random_password,
48 from samba.net import Net
50 from samba.netcmd import (
56 from samba.common import get_bytes
57 from samba.common import get_string
60 # python[3]-gpgme is abandoned since ubuntu 1804 and debian 9
61 # have to use python[3]-gpg instead
62 # The API is different, need to adapt.
64 def _gpgme_decrypt(encrypted_bytes):
66 Use python[3]-gpgme to decrypt GPG.
69 ctx.armor = True # use ASCII-armored
71 ctx.decrypt(io.BytesIO(encrypted_bytes), out)
75 def _gpg_decrypt(encrypted_bytes):
77 Use python[3]-gpg to decrypt GPG.
79 ciphertext = gpg.Data(string=encrypted_bytes)
80 ctx = gpg.Context(armor=True)
81 # plaintext, result, verify_result
82 plaintext, _, _ = ctx.decrypt(ciphertext)
91 gpg_decrypt = _gpgme_decrypt
98 gpg_decrypt = _gpg_decrypt
103 decrypt_samba_gpg_help = ("Decrypt the SambaGPG password as "
106 decrypt_samba_gpg_help = ("Decrypt the SambaGPG password not supported, "
107 "python[3]-gpgme or python[3]-gpg required")
110 disabled_virtual_attributes = {
113 virtual_attributes = {
114 "virtualClearTextUTF8": {
115 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
117 "virtualClearTextUTF16": {
118 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
121 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
126 def get_crypt_value(alg, utf8pw, rounds=0):
132 salt = os.urandom(16)
133 # The salt needs to be in [A-Za-z0-9./]
134 # base64 is close enough and as we had 16
135 # random bytes but only need 16 characters
136 # we can ignore the possible == at the end
137 # of the base64 string
138 # we just need to replace '+' by '.'
139 b64salt = base64.b64encode(salt)[0:16].replace(b'+', b'.').decode('utf8')
142 crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
144 crypt_salt = "$%s$%s$" % (alg, b64salt)
146 crypt_value = crypt.crypt(utf8pw, crypt_salt)
147 if crypt_value is None:
148 raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
149 expected_len = len(crypt_salt) + algs[alg]["length"]
150 if len(crypt_value) != expected_len:
151 raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % (
152 crypt_salt, len(crypt_value), expected_len))
155 # Extract the rounds value from the options of a virtualCrypt attribute
156 # i.e. options = "rounds=20;other=ignored;" will return 20
157 # if the rounds option is not found or the value is not a number, 0 is returned
158 # which indicates that the default number of rounds should be used.
161 def get_rounds(options):
165 opts = options.split(';')
167 if o.lower().startswith("rounds="):
168 (key, _, val) = o.partition('=')
180 virtual_attributes["virtualSSHA"] = {
182 except ImportError as e:
183 reason = "hashlib.sha1()"
184 reason += " required"
185 disabled_virtual_attributes["virtualSSHA"] = {
189 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
192 v = get_crypt_value(alg, "")
194 virtual_attributes[attr] = {
196 except ImportError as e:
198 reason += " required"
199 disabled_virtual_attributes[attr] = {
202 except NotImplementedError as e:
203 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
204 disabled_virtual_attributes[attr] = {
208 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
209 for x in range(1, 30):
210 virtual_attributes["virtualWDigest%02d" % x] = {}
212 # Add Kerberos virtual attributes
213 virtual_attributes["virtualKerberosSalt"] = {}
215 virtual_attributes_help = "The attributes to display (comma separated). "
216 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
217 if len(disabled_virtual_attributes) != 0:
218 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
221 class cmd_user_add(Command):
224 This command adds a new user account to the Active Directory domain. The username specified on the command is the sAMaccountName.
226 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).
228 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.
230 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.
232 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.
235 samba-tool user add User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
237 Example1 shows how to add a new user to 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.
240 sudo samba-tool user add User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
242 Example2 shows how to add a new user to 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.
245 samba-tool user add User3 passw3rd --userou='OU=OrgUnit'
247 Example3 shows how to add a new user in the OrgUnit organizational unit.
250 samba-tool user add User4 passw4rd --rfc2307-from-nss --gecos 'some text'
252 Example4 shows how to add a new user with Unix UID, GID and login-shell set from the local NSS and GECOS set to 'some text'.
255 samba-tool user add User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \\
256 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
258 Example5 shows how to add a new RFC2307/NIS domain enabled user account. If
259 --nis-domain is set, then the other four parameters are mandatory.
262 synopsis = "%prog <username> [<password>] [options]"
265 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
266 metavar="URL", dest="H"),
267 Option("--must-change-at-next-login",
268 help="Force password to be changed on next login",
269 action="store_true"),
270 Option("--random-password",
271 help="Generate random password",
272 action="store_true"),
273 Option("--smartcard-required",
274 help="Require a smartcard for interactive logons",
275 action="store_true"),
276 Option("--use-username-as-cn",
277 help="Force use of username as user's CN",
278 action="store_true"),
280 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>'",
282 Option("--surname", help="User's surname", type=str),
283 Option("--given-name", help="User's given name", type=str),
284 Option("--initials", help="User's initials", type=str),
285 Option("--profile-path", help="User's profile path", type=str),
286 Option("--script-path", help="User's logon script path", type=str),
287 Option("--home-drive", help="User's home drive letter", type=str),
288 Option("--home-directory", help="User's home directory path", type=str),
289 Option("--job-title", help="User's job title", type=str),
290 Option("--department", help="User's department", type=str),
291 Option("--company", help="User's company", type=str),
292 Option("--description", help="User's description", type=str),
293 Option("--mail-address", help="User's email address", type=str),
294 Option("--internet-address", help="User's home page", type=str),
295 Option("--telephone-number", help="User's phone number", type=str),
296 Option("--physical-delivery-office", help="User's office location", type=str),
297 Option("--rfc2307-from-nss",
298 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
299 action="store_true"),
300 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
301 Option("--unix-home", help="User's Unix/RFC2307 home directory",
303 Option("--uid", help="User's Unix/RFC2307 username", type=str),
304 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
305 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
306 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
307 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
310 takes_args = ["username", "password?"]
312 takes_optiongroups = {
313 "sambaopts": options.SambaOptions,
314 "credopts": options.CredentialsOptions,
315 "versionopts": options.VersionOptions,
318 def run(self, username, password=None, credopts=None, sambaopts=None,
319 versionopts=None, H=None, must_change_at_next_login=False,
320 random_password=False, use_username_as_cn=False, userou=None,
321 surname=None, given_name=None, initials=None, profile_path=None,
322 script_path=None, home_drive=None, home_directory=None,
323 job_title=None, department=None, company=None, description=None,
324 mail_address=None, internet_address=None, telephone_number=None,
325 physical_delivery_office=None, rfc2307_from_nss=False,
326 nis_domain=None, unix_home=None, uid=None, uid_number=None,
327 gid_number=None, gecos=None, login_shell=None,
328 smartcard_required=False):
330 if smartcard_required:
331 if password is not None and password != '':
332 raise CommandError('It is not allowed to specify '
334 'together with --smartcard-required.')
335 if must_change_at_next_login:
336 raise CommandError('It is not allowed to specify '
337 '--must-change-at-next-login '
338 'together with --smartcard-required.')
340 if random_password and not smartcard_required:
341 password = generate_random_password(128, 255)
344 if smartcard_required:
346 if password is not None and password != '':
348 password = getpass("New Password: ")
349 passwordverify = getpass("Retype Password: ")
350 if not password == passwordverify:
352 self.outf.write("Sorry, passwords do not match.\n")
355 pwent = pwd.getpwnam(username)
358 if uid_number is None:
359 uid_number = pwent[2]
360 if gid_number is None:
361 gid_number = pwent[3]
364 if login_shell is None:
365 login_shell = pwent[6]
367 lp = sambaopts.get_loadparm()
368 creds = credopts.get_credentials(lp)
370 if uid_number or gid_number:
371 if not lp.get("idmap_ldb:use rfc2307"):
372 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")
374 if nis_domain is not None:
375 if None in (uid_number, login_shell, unix_home, gid_number):
376 raise CommandError('Missing parameters. To enable NIS features, '
377 'the following options have to be given: '
378 '--nis-domain=, --uidNumber=, --login-shell='
379 ', --unix-home=, --gid-number= Operation '
383 samdb = SamDB(url=H, session_info=system_session(),
384 credentials=creds, lp=lp)
385 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
386 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
387 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
388 jobtitle=job_title, department=department, company=company, description=description,
389 mailaddress=mail_address, internetaddress=internet_address,
390 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
391 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
392 uidnumber=uid_number, gidnumber=gid_number,
393 gecos=gecos, loginshell=login_shell,
394 smartcard_required=smartcard_required)
395 except Exception as e:
396 raise CommandError("Failed to add user '%s': " % username, e)
398 self.outf.write("User '%s' added successfully\n" % username)
400 class cmd_user_delete(Command):
403 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
405 Once the account is deleted, all permissions and memberships associated with that account are deleted. If a new user account is added with the same name as a previously deleted account name, the new user does not have the previous permissions. The new account user will be assigned a new security identifier (SID) and permissions and memberships will have to be added.
407 The command may be run from the root userid or another authorized userid. The -H or --URL= option can be used to execute the command against a remote server.
410 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
412 Example1 shows how to delete a user in the domain against a remote LDAP server. The -H parameter is used to specify the remote target server. The --username= and --password= options are used to pass the username and password of a user that exists on the remote server and is authorized to issue the command on that server.
415 sudo samba-tool user delete User2
417 Example2 shows how to delete a user in the domain against the local server. sudo is used so a user may run the command as root.
420 synopsis = "%prog <username> [options]"
423 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
424 metavar="URL", dest="H"),
427 takes_args = ["username"]
428 takes_optiongroups = {
429 "sambaopts": options.SambaOptions,
430 "credopts": options.CredentialsOptions,
431 "versionopts": options.VersionOptions,
434 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
436 lp = sambaopts.get_loadparm()
437 creds = credopts.get_credentials(lp, fallback_machine=True)
439 samdb = SamDB(url=H, session_info=system_session(),
440 credentials=creds, lp=lp)
442 filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
443 ldb.binary_encode(username))
446 res = samdb.search(base=samdb.domain_dn(),
447 scope=ldb.SCOPE_SUBTREE,
452 raise CommandError('Unable to find user "%s"' % (username))
455 samdb.delete(user_dn)
456 except Exception as e:
457 raise CommandError('Failed to remove user "%s"' % username, e)
458 self.outf.write("Deleted user %s\n" % username)
461 class cmd_user_list(Command):
462 """List all users."""
464 synopsis = "%prog [options]"
467 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
468 metavar="URL", dest="H"),
469 Option("-b", "--base-dn",
470 help="Specify base DN to use",
472 Option("--full-dn", dest="full_dn",
475 help="Display DN instead of the sAMAccountName.")
478 takes_optiongroups = {
479 "sambaopts": options.SambaOptions,
480 "credopts": options.CredentialsOptions,
481 "versionopts": options.VersionOptions,
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 search_dn = samdb.domain_dn()
499 search_dn = samdb.normalize_dn_in_domain(base_dn)
501 res = samdb.search(search_dn,
502 scope=ldb.SCOPE_SUBTREE,
503 expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
504 % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
505 attrs=["samaccountname"])
511 self.outf.write("%s\n" % msg.get("dn"))
514 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
517 class cmd_user_enable(Command):
520 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.
522 There are many reasons why an account may become disabled. These include:
523 - If a user exceeds the account policy for logon attempts
524 - If an administrator disables the account
525 - If the account expires
527 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
529 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.
531 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.
534 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
536 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.
539 su samba-tool user enable Testuser2
541 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.
544 samba-tool user enable --filter=samaccountname=Testuser3
546 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
549 synopsis = "%prog (<username>|--filter <filter>) [options]"
551 takes_optiongroups = {
552 "sambaopts": options.SambaOptions,
553 "versionopts": options.VersionOptions,
554 "credopts": options.CredentialsOptions,
558 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
559 metavar="URL", dest="H"),
560 Option("--filter", help="LDAP Filter to set password on", type=str),
563 takes_args = ["username?"]
565 def run(self, username=None, sambaopts=None, credopts=None,
566 versionopts=None, filter=None, H=None):
567 if username is None and filter is None:
568 raise CommandError("Either the username or '--filter' must be specified!")
571 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
573 lp = sambaopts.get_loadparm()
574 creds = credopts.get_credentials(lp, fallback_machine=True)
576 samdb = SamDB(url=H, session_info=system_session(),
577 credentials=creds, lp=lp)
579 samdb.enable_account(filter)
580 except Exception as msg:
581 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
582 self.outf.write("Enabled user '%s'\n" % (username or filter))
585 class cmd_user_disable(Command):
586 """Disable a user."""
588 synopsis = "%prog (<username>|--filter <filter>) [options]"
591 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
592 metavar="URL", dest="H"),
593 Option("--filter", help="LDAP Filter to set password on", type=str),
596 takes_args = ["username?"]
598 takes_optiongroups = {
599 "sambaopts": options.SambaOptions,
600 "credopts": options.CredentialsOptions,
601 "versionopts": options.VersionOptions,
604 def run(self, username=None, sambaopts=None, credopts=None,
605 versionopts=None, filter=None, H=None):
606 if username is None and filter is None:
607 raise CommandError("Either the username or '--filter' must be specified!")
610 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
612 lp = sambaopts.get_loadparm()
613 creds = credopts.get_credentials(lp, fallback_machine=True)
615 samdb = SamDB(url=H, session_info=system_session(),
616 credentials=creds, lp=lp)
618 samdb.disable_account(filter)
619 except Exception as msg:
620 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
623 class cmd_user_setexpiry(Command):
624 """Set the expiration of a user account.
626 The user can either be specified by their sAMAccountName or using the --filter option.
628 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.
630 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.
633 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
635 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.
638 sudo samba-tool user setexpiry User2 --noexpiry
640 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.
643 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
645 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.
648 samba-tool user setexpiry --noexpiry User4
649 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
652 synopsis = "%prog (<username>|--filter <filter>) [options]"
654 takes_optiongroups = {
655 "sambaopts": options.SambaOptions,
656 "versionopts": options.VersionOptions,
657 "credopts": options.CredentialsOptions,
661 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
662 metavar="URL", dest="H"),
663 Option("--filter", help="LDAP Filter to set password on", type=str),
664 Option("--days", help="Days to expiry", type=int, default=0),
665 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
668 takes_args = ["username?"]
670 def run(self, username=None, sambaopts=None, credopts=None,
671 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
672 if username is None and filter is None:
673 raise CommandError("Either the username or '--filter' must be specified!")
676 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
678 lp = sambaopts.get_loadparm()
679 creds = credopts.get_credentials(lp)
681 samdb = SamDB(url=H, session_info=system_session(),
682 credentials=creds, lp=lp)
685 samdb.setexpiry(filter, days * 24 * 3600, no_expiry_req=noexpiry)
686 except Exception as msg:
687 # FIXME: Catch more specific exception
688 raise CommandError("Failed to set expiry for user '%s': %s" % (
689 username or filter, msg))
691 self.outf.write("Expiry for user '%s' disabled.\n" % (
694 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
695 username or filter, days))
698 class cmd_user_password(Command):
699 """Change password for a user account (the one provided in authentication).
702 synopsis = "%prog [options]"
705 Option("--newpassword", help="New password", type=str),
708 takes_optiongroups = {
709 "sambaopts": options.SambaOptions,
710 "credopts": options.CredentialsOptions,
711 "versionopts": options.VersionOptions,
714 def run(self, credopts=None, sambaopts=None, versionopts=None,
717 lp = sambaopts.get_loadparm()
718 creds = credopts.get_credentials(lp)
720 # get old password now, to get the password prompts in the right order
721 old_password = creds.get_password()
723 net = Net(creds, lp, server=credopts.ipaddress)
725 password = newpassword
727 if password is not None and password != '':
729 password = getpass("New Password: ")
730 passwordverify = getpass("Retype Password: ")
731 if not password == passwordverify:
733 self.outf.write("Sorry, passwords do not match.\n")
736 if not isinstance(password, str):
737 password = password.decode('utf8')
738 net.change_password(password)
739 except Exception as msg:
740 # FIXME: catch more specific exception
741 raise CommandError("Failed to change password : %s" % msg)
742 self.outf.write("Changed password OK\n")
745 class cmd_user_getgroups(Command):
746 """Get the direct group memberships of a user account.
748 The username specified on the command is the sAMAccountName."""
749 synopsis = "%prog <username> [options]"
751 takes_optiongroups = {
752 "sambaopts": options.SambaOptions,
753 "versionopts": options.VersionOptions,
754 "credopts": options.CredentialsOptions,
758 Option("-H", "--URL", help="LDB URL for database or target server",
759 type=str, metavar="URL", dest="H"),
760 Option("--full-dn", dest="full_dn",
763 help="Display DN instead of the sAMAccountName."),
766 takes_args = ["username"]
776 lp = sambaopts.get_loadparm()
777 creds = credopts.get_credentials(lp)
779 samdb = SamDB(url=H, session_info=system_session(),
780 credentials=creds, lp=lp)
782 filter = ("(&(sAMAccountName=%s)(objectClass=user))" %
783 ldb.binary_encode(username))
785 res = samdb.search(base=samdb.domain_dn(),
787 scope=ldb.SCOPE_SUBTREE,
791 user_sid_binary = res[0].get('objectSid', idx=0)
792 user_sid = ndr_unpack(security.dom_sid, user_sid_binary)
793 (user_dom_sid, user_rid) = user_sid.split()
794 user_sid_dn = "<SID=%s>" % user_sid
795 user_pgid = int(res[0].get('primaryGroupID', idx=0))
796 user_groups = res[0].get('memberOf')
797 if user_groups is None:
800 raise CommandError("Unable to find user '%s'" % (username))
802 primarygroup_sid_dn = "<SID=%s-%u>" % (user_dom_sid, user_pgid)
804 filter = "(objectClass=group)"
806 res = samdb.search(base=primarygroup_sid_dn,
808 scope=ldb.SCOPE_BASE,
809 attrs=['sAMAccountName'])
810 primary_group_dn = str(res[0].dn)
811 primary_group_name = res[0].get('sAMAccountName')
813 raise CommandError("Unable to find primary group '%s'" % (primarygroup_sid_dn))
816 self.outf.write("%s\n" % primary_group_dn)
817 for group_dn in user_groups:
818 self.outf.write("%s\n" % group_dn)
822 for gdn in user_groups:
824 res = samdb.search(base=gdn,
826 scope=ldb.SCOPE_BASE,
827 attrs=['sAMAccountName'])
828 group_names.extend(res[0].get('sAMAccountName'))
830 raise CommandError("Unable to find group '%s'" % (gdn))
832 self.outf.write("%s\n" % primary_group_name)
833 for group_name in group_names:
834 self.outf.write("%s\n" % group_name)
837 class cmd_user_setprimarygroup(Command):
838 """Set the primary group a user account.
840 This command sets the primary group a user account. The username specified on
841 the command is the sAMAccountName. The primarygroupname is the sAMAccountName
842 of the new primary group. The user must be a member of the group.
844 The command may be run from the root userid or another authorized userid. The
845 -H or --URL= option can be used to execute the command against a remote server.
848 samba-tool user setprimarygroup TestUser1 newPrimaryGroup --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
850 Example1 shows how to set the primary group for TestUser1 on a remote LDAP
851 server. The --URL parameter is used to specify the remote target server. The
852 -U option is used to pass the username and password of a user that exists on
853 the remote server and is authorized to update the server.
855 synopsis = "%prog <username> <primarygroupname> [options]"
857 takes_optiongroups = {
858 "sambaopts": options.SambaOptions,
859 "versionopts": options.VersionOptions,
860 "credopts": options.CredentialsOptions,
864 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
865 metavar="URL", dest="H"),
868 takes_args = ["username", "primarygroupname"]
870 def run(self, username, primarygroupname, credopts=None, sambaopts=None,
871 versionopts=None, H=None):
873 lp = sambaopts.get_loadparm()
874 creds = credopts.get_credentials(lp)
876 samdb = SamDB(url=H, session_info=system_session(),
877 credentials=creds, lp=lp)
879 filter = ("(&(sAMAccountName=%s)(objectClass=user))" %
880 ldb.binary_encode(username))
882 res = samdb.search(base=samdb.domain_dn(),
884 scope=ldb.SCOPE_SUBTREE,
885 controls=["extended_dn:1:1"],
889 user_sid_binary = res[0].get('objectSid', idx=0)
890 user_sid = ndr_unpack(security.dom_sid, user_sid_binary)
891 (user_dom_sid, user_rid) = user_sid.split()
892 user_sid_dn = "<SID=%s>" % user_sid
893 user_pgid = int(res[0].get('primaryGroupID', idx=0))
894 user_groups = res[0].get('memberOf')
895 if user_groups is None:
898 raise CommandError("Unable to find user '%s'" % (username))
901 for user_group in user_groups:
902 user_group_dn = ldb.Dn(samdb, str(user_group))
903 user_group_binary_sid = user_group_dn.get_extended_component("SID")
904 user_group_sid = ndr_unpack(security.dom_sid, user_group_binary_sid)
905 user_group_sids.append(user_group_sid)
907 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
908 ldb.binary_encode(primarygroupname))
910 res = samdb.search(base=samdb.domain_dn(),
912 scope=ldb.SCOPE_SUBTREE,
914 group_sid_binary = res[0].get('objectSid', idx=0)
916 raise CommandError("Unable to find group '%s'" % (primarygroupname))
918 primarygroup_sid = ndr_unpack(security.dom_sid, group_sid_binary)
919 (primarygroup_dom_sid, primarygroup_rid) = primarygroup_sid.split()
921 if user_dom_sid != primarygroup_dom_sid:
922 raise CommandError("Group '%s' does not belong to the user's "
923 "domain" % primarygroupname)
925 if primarygroup_rid != user_pgid and primarygroup_sid not in user_group_sids:
926 raise CommandError("User '%s' is not member of group '%s'" %
927 (username, primarygroupname))
929 setprimarygroup_ldif = """
932 delete: primaryGroupID
936 """ % (user_sid_dn, user_pgid, primarygroup_rid)
939 samdb.modify_ldif(setprimarygroup_ldif)
940 except Exception as msg:
941 raise CommandError("Failed to set primary group '%s' "
942 "for user '%s': %s" %
943 (primarygroupname, username, msg))
944 self.outf.write("Changed primary group to '%s'\n" % primarygroupname)
947 class cmd_user_setpassword(Command):
948 """Set or reset the password of a user account.
950 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.
952 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.
954 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.
956 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.
959 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
961 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.
964 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
966 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.
969 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
971 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
974 synopsis = "%prog (<username>|--filter <filter>) [options]"
976 takes_optiongroups = {
977 "sambaopts": options.SambaOptions,
978 "versionopts": options.VersionOptions,
979 "credopts": options.CredentialsOptions,
983 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
984 metavar="URL", dest="H"),
985 Option("--filter", help="LDAP Filter to set password on", type=str),
986 Option("--newpassword", help="Set password", type=str),
987 Option("--must-change-at-next-login",
988 help="Force password to be changed on next login",
989 action="store_true"),
990 Option("--random-password",
991 help="Generate random password",
992 action="store_true"),
993 Option("--smartcard-required",
994 help="Require a smartcard for interactive logons",
995 action="store_true"),
996 Option("--clear-smartcard-required",
997 help="Don't require a smartcard for interactive logons",
998 action="store_true"),
1001 takes_args = ["username?"]
1003 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
1004 versionopts=None, H=None, newpassword=None,
1005 must_change_at_next_login=False, random_password=False,
1006 smartcard_required=False, clear_smartcard_required=False):
1007 if filter is None and username is None:
1008 raise CommandError("Either the username or '--filter' must be specified!")
1010 password = newpassword
1012 if smartcard_required:
1013 if password is not None and password != '':
1014 raise CommandError('It is not allowed to specify '
1016 'together with --smartcard-required.')
1017 if must_change_at_next_login:
1018 raise CommandError('It is not allowed to specify '
1019 '--must-change-at-next-login '
1020 'together with --smartcard-required.')
1021 if clear_smartcard_required:
1022 raise CommandError('It is not allowed to specify '
1023 '--clear-smartcard-required '
1024 'together with --smartcard-required.')
1026 if random_password and not smartcard_required:
1027 password = generate_random_password(128, 255)
1030 if smartcard_required:
1032 if password is not None and password != '':
1034 password = getpass("New Password: ")
1035 passwordverify = getpass("Retype Password: ")
1036 if not password == passwordverify:
1038 self.outf.write("Sorry, passwords do not match.\n")
1041 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1043 lp = sambaopts.get_loadparm()
1044 creds = credopts.get_credentials(lp)
1046 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
1048 samdb = SamDB(url=H, session_info=system_session(),
1049 credentials=creds, lp=lp)
1051 if smartcard_required:
1054 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
1055 flags = dsdb.UF_SMARTCARD_REQUIRED
1056 samdb.toggle_userAccountFlags(filter, flags, on=True)
1057 command = "Failed to enable account for user '%s'" % (username or filter)
1058 samdb.enable_account(filter)
1059 except Exception as msg:
1060 # FIXME: catch more specific exception
1061 raise CommandError("%s: %s" % (command, msg))
1062 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
1066 if clear_smartcard_required:
1067 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
1068 flags = dsdb.UF_SMARTCARD_REQUIRED
1069 samdb.toggle_userAccountFlags(filter, flags, on=False)
1070 command = "Failed to set password for user '%s'" % (username or filter)
1071 samdb.setpassword(filter, password,
1072 force_change_at_next_login=must_change_at_next_login,
1074 except Exception as msg:
1075 # FIXME: catch more specific exception
1076 raise CommandError("%s: %s" % (command, msg))
1077 self.outf.write("Changed password OK\n")
1080 class GetPasswordCommand(Command):
1083 super(GetPasswordCommand, self).__init__()
1086 def connect_system_samdb(self, url, allow_local=False, verbose=False):
1088 # using anonymous here, results in no authentication
1089 # which means we can get system privileges via
1090 # the privileged ldapi socket
1091 creds = credentials.Credentials()
1092 creds.set_anonymous()
1094 if url is None and allow_local:
1096 elif url.lower().startswith("ldapi://"):
1098 elif url.lower().startswith("ldap://"):
1099 raise CommandError("--url ldap:// is not supported for this command")
1100 elif url.lower().startswith("ldaps://"):
1101 raise CommandError("--url ldaps:// is not supported for this command")
1102 elif not allow_local:
1103 raise CommandError("--url requires an ldapi:// url for this command")
1106 self.outf.write("Connecting to '%s'\n" % url)
1108 samdb = SamDB(url=url, session_info=system_session(),
1109 credentials=creds, lp=self.lp)
1113 # Make sure we're connected as SYSTEM
1115 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
1116 assert len(res) == 1
1117 sids = res[0].get("tokenGroups")
1118 assert len(sids) == 1
1119 sid = ndr_unpack(security.dom_sid, sids[0])
1120 assert str(sid) == security.SID_NT_SYSTEM
1121 except Exception as msg:
1122 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
1123 (security.SID_NT_SYSTEM))
1125 # We use sort here in order to have a predictable processing order
1126 # this might not be strictly needed, but also doesn't hurt here
1127 for a in sorted(virtual_attributes.keys()):
1128 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
1129 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
1133 def get_account_attributes(self, samdb, username, basedn, filter, scope,
1136 raw_attrs = attrs[:]
1140 (attr, _, opts) = a.partition(';')
1142 attr_opts[attr] = opts
1144 attr_opts[attr] = None
1145 search_attrs.append(attr)
1146 lower_attrs = [x.lower() for x in search_attrs]
1148 require_supplementalCredentials = False
1149 for a in virtual_attributes.keys():
1150 if a.lower() in lower_attrs:
1151 require_supplementalCredentials = True
1152 add_supplementalCredentials = False
1153 add_unicodePwd = False
1154 if require_supplementalCredentials:
1155 a = "supplementalCredentials"
1156 if a.lower() not in lower_attrs:
1158 add_supplementalCredentials = True
1160 if a.lower() not in lower_attrs:
1162 add_unicodePwd = True
1163 add_sAMAcountName = False
1164 a = "sAMAccountName"
1165 if a.lower() not in lower_attrs:
1167 add_sAMAcountName = True
1169 add_userPrincipalName = False
1170 upn = "userPrincipalName"
1171 if upn.lower() not in lower_attrs:
1172 search_attrs += [upn]
1173 add_userPrincipalName = True
1175 if scope == ldb.SCOPE_BASE:
1176 search_controls = ["show_deleted:1", "show_recycled:1"]
1178 search_controls = []
1180 res = samdb.search(base=basedn, expression=filter,
1181 scope=scope, attrs=search_attrs,
1182 controls=search_controls)
1184 raise Exception('Unable to find user "%s"' % (username or filter))
1186 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
1187 except Exception as msg:
1188 # FIXME: catch more specific exception
1189 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
1194 if "supplementalCredentials" in obj:
1195 sc_blob = obj["supplementalCredentials"][0]
1196 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
1197 if add_supplementalCredentials:
1198 del obj["supplementalCredentials"]
1199 if "unicodePwd" in obj:
1200 unicodePwd = obj["unicodePwd"][0]
1202 del obj["unicodePwd"]
1203 account_name = str(obj["sAMAccountName"][0])
1204 if add_sAMAcountName:
1205 del obj["sAMAccountName"]
1206 if "userPrincipalName" in obj:
1207 account_upn = str(obj["userPrincipalName"][0])
1209 realm = self.lp.get("realm")
1210 account_upn = "%s@%s" % (account_name, realm.lower())
1211 if add_userPrincipalName:
1212 del obj["userPrincipalName"]
1216 def get_package(name, min_idx=0):
1217 if name in calculated:
1218 return calculated[name]
1222 min_idx = len(sc.sub.packages) + min_idx
1224 for p in sc.sub.packages:
1231 return binascii.a2b_hex(p.data)
1236 # Samba adds 'Primary:SambaGPG' at the end.
1237 # When Windows sets the password it keeps
1238 # 'Primary:SambaGPG' and rotates it to
1239 # the begining. So we can only use the value,
1240 # if it is the last one.
1242 # In order to get more protection we verify
1243 # the nthash of the decrypted utf16 password
1244 # against the stored nthash in unicodePwd.
1246 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1247 if sgv is not None and unicodePwd is not None:
1249 cv = gpg_decrypt(sgv)
1251 # We only use the password if it matches
1252 # the current nthash stored in the unicodePwd
1255 tmp = credentials.Credentials()
1257 tmp.set_utf16_password(cv)
1258 nthash = tmp.get_nt_hash()
1259 if nthash == unicodePwd:
1260 calculated["Primary:CLEARTEXT"] = cv
1262 except Exception as e:
1264 "WARNING: '%s': SambaGPG can't be decrypted "
1265 "into CLEARTEXT: %s\n" % (
1266 username or account_name, e))
1269 def get_utf8(a, b, username):
1271 u = str(get_bytes(b), 'utf-16-le')
1272 except UnicodeDecodeError as e:
1273 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1276 u8 = u.encode('utf-8')
1279 # Extract the WDigest hash for the value specified by i.
1280 # Builds an htdigest compatible value
1283 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1284 domain, dns_domain):
1289 user = account_name.lower()
1290 realm = domain.lower()
1292 user = account_name.upper()
1293 realm = domain.upper()
1296 realm = domain.upper()
1299 realm = domain.lower()
1301 user = account_name.upper()
1302 realm = domain.lower()
1304 user = account_name.lower()
1305 realm = domain.upper()
1308 realm = dns_domain.lower()
1310 user = account_name.lower()
1311 realm = dns_domain.lower()
1313 user = account_name.upper()
1314 realm = dns_domain.upper()
1317 realm = dns_domain.upper()
1320 realm = dns_domain.lower()
1322 user = account_name.upper()
1323 realm = dns_domain.lower()
1325 user = account_name.lower()
1326 realm = dns_domain.upper()
1331 user = account_upn.lower()
1334 user = account_upn.upper()
1337 user = "%s\\%s" % (domain, account_name)
1340 user = "%s\\%s" % (domain.lower(), account_name.lower())
1343 user = "%s\\%s" % (domain.upper(), account_name.upper())
1349 user = account_name.lower()
1352 user = account_name.upper()
1358 user = account_upn.lower()
1361 user = account_upn.upper()
1364 user = "%s\\%s" % (domain, account_name)
1367 # Differs from spec, see tests
1368 user = "%s\\%s" % (domain.lower(), account_name.lower())
1371 # Differs from spec, see tests
1372 user = "%s\\%s" % (domain.upper(), account_name.upper())
1377 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1380 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
1381 return "%s:%s:%s" % (user, realm, get_string(digest))
1385 # get the value for a virtualCrypt attribute.
1386 # look for an exact match on algorithm and rounds in supplemental creds
1387 # if not found calculate using Primary:CLEARTEXT
1388 # if no Primary:CLEARTEXT return the first supplementalCredential
1389 # that matches the algorithm.
1390 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1393 b = get_package("Primary:userPassword")
1395 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1397 # No exact match on algorithm and number of rounds
1398 # try and calculate one from the Primary:CLEARTEXT
1399 b = get_package("Primary:CLEARTEXT")
1401 u8 = get_utf8(a, b, username or account_name)
1403 # in py2 using get_bytes should ensure u8 is unmodified
1404 # in py3 it will be decoded
1405 sv = get_crypt_value(str(algorithm), get_string(u8), rounds)
1407 # Unable to calculate a hash with the specified
1408 # number of rounds, fall back to the first hash using
1409 # the specified algorithm
1413 return "{CRYPT}" + sv
1415 def get_userPassword_hash(blob, algorithm, rounds):
1416 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1419 # Check that the NT hash has not been changed without updating
1420 # the user password hashes. This indicates that password has been
1421 # changed without updating the supplemental credentials.
1422 if unicodePwd != bytearray(up.current_nt_hash.hash):
1425 scheme_prefix = "$%d$" % algorithm
1426 prefix = scheme_prefix
1428 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1432 # in PY2 this should just do nothing and in PY3 if bytes
1433 # it will decode them
1434 h_value = get_string(h.value)
1435 if (scheme_match is None and
1436 h.scheme == SCHEME and
1437 h_value.startswith(scheme_prefix)):
1438 scheme_match = h_value
1439 if h.scheme == SCHEME and h_value.startswith(prefix):
1440 return (h_value, scheme_match)
1442 # No match on the number of rounds, return the value of the
1443 # first matching scheme
1444 return (None, scheme_match)
1446 def get_kerberos_ctr():
1447 primary_krb5 = get_package("Primary:Kerberos-Newer-Keys")
1448 if primary_krb5 is None:
1449 primary_krb5 = get_package("Primary:Kerberos")
1450 if primary_krb5 is None:
1452 krb5_blob = ndr_unpack(drsblobs.package_PrimaryKerberosBlob,
1454 return (krb5_blob.version, krb5_blob.ctr)
1456 # We use sort here in order to have a predictable processing order
1457 for a in sorted(virtual_attributes.keys()):
1458 if not a.lower() in lower_attrs:
1461 if a == "virtualClearTextUTF8":
1462 b = get_package("Primary:CLEARTEXT")
1465 u8 = get_utf8(a, b, username or account_name)
1469 elif a == "virtualClearTextUTF16":
1470 v = get_package("Primary:CLEARTEXT")
1473 elif a == "virtualSSHA":
1474 b = get_package("Primary:CLEARTEXT")
1477 u8 = get_utf8(a, b, username or account_name)
1480 salt = os.urandom(4)
1484 bv = h.digest() + salt
1485 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
1486 elif a == "virtualCryptSHA256":
1487 rounds = get_rounds(attr_opts[a])
1488 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1492 elif a == "virtualCryptSHA512":
1493 rounds = get_rounds(attr_opts[a])
1494 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1498 elif a == "virtualSambaGPG":
1499 # Samba adds 'Primary:SambaGPG' at the end.
1500 # When Windows sets the password it keeps
1501 # 'Primary:SambaGPG' and rotates it to
1502 # the begining. So we can only use the value,
1503 # if it is the last one.
1504 v = get_package("Primary:SambaGPG", min_idx=-1)
1507 elif a == "virtualKerberosSalt":
1508 (krb5_v, krb5_ctr) = get_kerberos_ctr()
1509 if krb5_v not in [3, 4]:
1511 v = krb5_ctr.salt.string
1512 elif a.startswith("virtualWDigest"):
1513 primary_wdigest = get_package("Primary:WDigest")
1514 if primary_wdigest is None:
1516 x = a[len("virtualWDigest"):]
1521 domain = self.lp.get("workgroup")
1522 dns_domain = samdb.domain_dns_name()
1523 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1528 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1531 def parse_attributes(self, attributes):
1533 if attributes is None:
1534 raise CommandError("Please specify --attributes")
1535 attrs = attributes.split(',')
1538 pa = pa.lstrip().rstrip()
1539 for da in disabled_virtual_attributes.keys():
1540 if pa.lower() == da.lower():
1541 r = disabled_virtual_attributes[da]["reason"]
1542 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1544 for va in virtual_attributes.keys():
1545 if pa.lower() == va.lower():
1546 # Take the real name
1549 password_attrs += [pa]
1551 return password_attrs
1554 class cmd_user_getpassword(GetPasswordCommand):
1555 """Get the password fields of a user/computer account.
1557 This command gets the logon password for a user/computer account.
1559 The username specified on the command is the sAMAccountName.
1560 The username may also be specified using the --filter option.
1562 The command must be run from the root user id or another authorized user id.
1563 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1564 used to adjust the local path. By default tdb:// is used by default.
1566 The '--attributes' parameter takes a comma separated list of attributes,
1567 which will be printed or given to the script specified by '--script'. If a
1568 specified attribute is not available on an object it's silently omitted.
1569 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1570 the NTHASH) and the following virtual attributes are possible (see --help
1571 for which virtual attributes are supported in your environment):
1573 virtualClearTextUTF16: The raw cleartext as stored in the
1574 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1575 with '--decrypt-samba-gpg') buffer inside of the
1576 supplementalCredentials attribute. This typically
1577 contains valid UTF-16-LE, but may contain random
1578 bytes, e.g. for computer accounts.
1580 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1581 (only from valid UTF-16-LE)
1583 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1584 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1586 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1587 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1588 with a $5$... salt, see crypt(3) on modern systems.
1589 The number of rounds used to calculate the hash can
1590 also be specified. By appending ";rounds=x" to the
1591 attribute name i.e. virtualCryptSHA256;rounds=10000
1592 will calculate a SHA256 hash with 10,000 rounds.
1593 non numeric values for rounds are silently ignored
1594 The value is calculated as follows:
1595 1) If a value exists in 'Primary:userPassword' with
1596 the specified number of rounds it is returned.
1597 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1598 '--decrypt-samba-gpg'. Calculate a hash with
1599 the specified number of rounds
1600 3) Return the first CryptSHA256 value in
1601 'Primary:userPassword'
1604 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1605 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1606 with a $6$... salt, see crypt(3) on modern systems.
1607 The number of rounds used to calculate the hash can
1608 also be specified. By appending ";rounds=x" to the
1609 attribute name i.e. virtualCryptSHA512;rounds=10000
1610 will calculate a SHA512 hash with 10,000 rounds.
1611 non numeric values for rounds are silently ignored
1612 The value is calculated as follows:
1613 1) If a value exists in 'Primary:userPassword' with
1614 the specified number of rounds it is returned.
1615 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1616 '--decrypt-samba-gpg'. Calculate a hash with
1617 the specified number of rounds
1618 3) Return the first CryptSHA512 value in
1619 'Primary:userPassword'
1621 virtualWDigestNN: The individual hash values stored in
1622 'Primary:WDigest' where NN is the hash number in
1624 NOTE: As at 22-05-2017 the documentation:
1625 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1626 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1629 virtualKerberosSalt: This results the salt string that is used to compute
1630 Kerberos keys from a UTF-8 cleartext password.
1632 virtualSambaGPG: The raw cleartext as stored in the
1633 'Primary:SambaGPG' buffer inside of the
1634 supplementalCredentials attribute.
1635 See the 'password hash gpg key ids' option in
1638 The '--decrypt-samba-gpg' option triggers decryption of the
1639 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1640 in your environment or not (the python-gpgme package is required). Please
1641 note that you might need to set the GNUPGHOME environment variable. If the
1642 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1643 environment variable has been set correctly and the passphrase is already
1644 known by the gpg-agent.
1647 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1650 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1654 super(cmd_user_getpassword, self).__init__()
1656 synopsis = "%prog (<username>|--filter <filter>) [options]"
1658 takes_optiongroups = {
1659 "sambaopts": options.SambaOptions,
1660 "versionopts": options.VersionOptions,
1664 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1665 metavar="URL", dest="H"),
1666 Option("--filter", help="LDAP Filter to set password on", type=str),
1667 Option("--attributes", type=str,
1668 help=virtual_attributes_help,
1669 metavar="ATTRIBUTELIST", dest="attributes"),
1670 Option("--decrypt-samba-gpg",
1671 help=decrypt_samba_gpg_help,
1672 action="store_true", default=False, dest="decrypt_samba_gpg"),
1675 takes_args = ["username?"]
1677 def run(self, username=None, H=None, filter=None,
1678 attributes=None, decrypt_samba_gpg=None,
1679 sambaopts=None, versionopts=None):
1680 self.lp = sambaopts.get_loadparm()
1682 if decrypt_samba_gpg and not gpg_decrypt:
1683 raise CommandError(decrypt_samba_gpg_help)
1685 if filter is None and username is None:
1686 raise CommandError("Either the username or '--filter' must be specified!")
1689 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1691 if attributes is None:
1692 raise CommandError("Please specify --attributes")
1694 password_attrs = self.parse_attributes(attributes)
1696 samdb = self.connect_system_samdb(url=H, allow_local=True)
1698 obj = self.get_account_attributes(samdb, username,
1701 scope=ldb.SCOPE_SUBTREE,
1702 attrs=password_attrs,
1703 decrypt=decrypt_samba_gpg)
1705 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1706 self.outf.write("%s" % ldif)
1707 self.outf.write("Got password OK\n")
1710 class cmd_user_syncpasswords(GetPasswordCommand):
1711 """Sync the password of user accounts.
1713 This syncs logon passwords for user accounts.
1715 Note that this command should run on a single domain controller only
1716 (typically the PDC-emulator). However the "password hash gpg key ids"
1717 option should to be configured on all domain controllers.
1719 The command must be run from the root user id or another authorized user id.
1720 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1721 local path. By default, ldapi:// is used with the default path to the
1722 privileged ldapi socket.
1724 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1725 "Sync Loop Terminate".
1728 Cache Initialization
1729 ====================
1731 The first time, this command needs to be called with
1732 '--cache-ldb-initialize' in order to initialize its cache.
1734 The cache initialization requires '--attributes' and allows the following
1735 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1738 The '--attributes' parameter takes a comma separated list of attributes,
1739 which will be printed or given to the script specified by '--script'. If a
1740 specified attribute is not available on an object it will be silently omitted.
1741 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1742 the NTHASH) and the following virtual attributes are possible (see '--help'
1743 for supported virtual attributes in your environment):
1745 virtualClearTextUTF16: The raw cleartext as stored in the
1746 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1747 with '--decrypt-samba-gpg') buffer inside of the
1748 supplementalCredentials attribute. This typically
1749 contains valid UTF-16-LE, but may contain random
1750 bytes, e.g. for computer accounts.
1752 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1753 (only from valid UTF-16-LE)
1755 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1756 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1758 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1759 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1760 with a $5$... salt, see crypt(3) on modern systems.
1761 The number of rounds used to calculate the hash can
1762 also be specified. By appending ";rounds=x" to the
1763 attribute name i.e. virtualCryptSHA256;rounds=10000
1764 will calculate a SHA256 hash with 10,000 rounds.
1765 non numeric values for rounds are silently ignored
1766 The value is calculated as follows:
1767 1) If a value exists in 'Primary:userPassword' with
1768 the specified number of rounds it is returned.
1769 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1770 '--decrypt-samba-gpg'. Calculate a hash with
1771 the specified number of rounds
1772 3) Return the first CryptSHA256 value in
1773 'Primary:userPassword'
1775 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1776 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1777 with a $6$... salt, see crypt(3) on modern systems.
1778 The number of rounds used to calculate the hash can
1779 also be specified. By appending ";rounds=x" to the
1780 attribute name i.e. virtualCryptSHA512;rounds=10000
1781 will calculate a SHA512 hash with 10,000 rounds.
1782 non numeric values for rounds are silently ignored
1783 The value is calculated as follows:
1784 1) If a value exists in 'Primary:userPassword' with
1785 the specified number of rounds it is returned.
1786 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
1787 '--decrypt-samba-gpg'. Calculate a hash with
1788 the specified number of rounds
1789 3) Return the first CryptSHA512 value in
1790 'Primary:userPassword'
1792 virtualWDigestNN: The individual hash values stored in
1793 'Primary:WDigest' where NN is the hash number in
1795 NOTE: As at 22-05-2017 the documentation:
1796 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1797 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1800 virtualKerberosSalt: This results the salt string that is used to compute
1801 Kerberos keys from a UTF-8 cleartext password.
1803 virtualSambaGPG: The raw cleartext as stored in the
1804 'Primary:SambaGPG' buffer inside of the
1805 supplementalCredentials attribute.
1806 See the 'password hash gpg key ids' option in
1809 The '--decrypt-samba-gpg' option triggers decryption of the
1810 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1811 in your environment or not (the python-gpgme package is required). Please
1812 note that you might need to set the GNUPGHOME environment variable. If the
1813 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1814 environment variable has been set correctly and the passphrase is already
1815 known by the gpg-agent.
1817 The '--script' option specifies a custom script that is called whenever any
1818 of the dirsyncAttributes (see below) was changed. The script is called
1819 without any arguments. It gets the LDIF for exactly one object on STDIN.
1820 If the script processed the object successfully it has to respond with a
1821 single line starting with 'DONE-EXIT: ' followed by an optional message.
1823 Note that the script might be called without any password change, e.g. if
1824 the account was disabled (a userAccountControl change) or the
1825 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
1826 are always returned as unique identifier of the account. It might be useful
1827 to also ask for non-password attributes like: objectSid, sAMAccountName,
1828 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
1829 Depending on the object, some attributes may not be present/available,
1830 but you always get the current state (and not a diff).
1832 If no '--script' option is specified, the LDIF will be printed on STDOUT or
1835 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
1836 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
1837 (!(sAMAccountName=krbtgt*)))
1838 This means only normal (non-krbtgt) user
1839 accounts are monitored. The '--filter' can modify that, e.g. if it's
1840 required to also sync computer accounts.
1846 This (default) mode runs in an endless loop waiting for password related
1847 changes in the active directory database. It makes use of the
1848 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
1849 get changes in a reliable fashion. Objects are monitored for changes of the
1850 following dirsyncAttributes:
1852 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
1853 userPrincipalName and userAccountControl.
1855 It recovers from LDAP disconnects and updates the cache in conservative way
1856 (in single steps after each successfully processed change). An error from
1857 the script (specified by '--script') will result in fatal error and this
1858 command will exit. But the cache state should be still valid and can be
1859 resumed in the next "Sync Loop Run".
1861 The '--logfile' option specifies an optional (required if '--daemon' is
1862 specified) logfile that takes all output of the command. The logfile is
1863 automatically reopened if fstat returns st_nlink == 0.
1865 The optional '--daemon' option will put the command into the background.
1867 You can stop the command without the '--daemon' option, also by hitting
1870 If you specify the '--no-wait' option the command skips the
1871 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
1872 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
1877 In order to terminate an already running command (likely as daemon) the
1878 '--terminate' option can be used. This also requires the '--logfile' option
1883 samba-tool user syncpasswords --cache-ldb-initialize \\
1884 --attributes=virtualClearTextUTF8
1885 samba-tool user syncpasswords
1888 samba-tool user syncpasswords --cache-ldb-initialize \\
1889 --attributes=objectGUID,objectSID,sAMAccountName,\\
1890 userPrincipalName,userAccountControl,pwdLastSet,\\
1891 msDS-KeyVersionNumber,virtualCryptSHA512 \\
1892 --script=/path/to/my-custom-syncpasswords-script.py
1893 samba-tool user syncpasswords --daemon \\
1894 --logfile=/var/log/samba/user-syncpasswords.log
1895 samba-tool user syncpasswords --terminate \\
1896 --logfile=/var/log/samba/user-syncpasswords.log
1900 super(cmd_user_syncpasswords, self).__init__()
1902 synopsis = "%prog [--cache-ldb-initialize] [options]"
1904 takes_optiongroups = {
1905 "sambaopts": options.SambaOptions,
1906 "versionopts": options.VersionOptions,
1910 Option("--cache-ldb-initialize",
1911 help="Initialize the cache for the first time",
1912 dest="cache_ldb_initialize", action="store_true"),
1913 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
1914 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
1915 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
1916 metavar="URL", dest="H"),
1917 Option("--filter", help="optional LDAP filter to set password on", type=str,
1918 metavar="LDAP-SEARCH-FILTER", dest="filter"),
1919 Option("--attributes", type=str,
1920 help=virtual_attributes_help,
1921 metavar="ATTRIBUTELIST", dest="attributes"),
1922 Option("--decrypt-samba-gpg",
1923 help=decrypt_samba_gpg_help,
1924 action="store_true", default=False, dest="decrypt_samba_gpg"),
1925 Option("--script", help="Script that is called for each password change", type=str,
1926 metavar="/path/to/syncpasswords.script", dest="script"),
1927 Option("--no-wait", help="Don't block waiting for changes",
1928 action="store_true", default=False, dest="nowait"),
1929 Option("--logfile", type=str,
1930 help="The logfile to use (required in --daemon mode).",
1931 metavar="/path/to/syncpasswords.log", dest="logfile"),
1932 Option("--daemon", help="daemonize after initial setup",
1933 action="store_true", default=False, dest="daemon"),
1934 Option("--terminate",
1935 help="Send a SIGTERM to an already running (daemon) process",
1936 action="store_true", default=False, dest="terminate"),
1939 def run(self, cache_ldb_initialize=False, cache_ldb=None,
1940 H=None, filter=None,
1941 attributes=None, decrypt_samba_gpg=None,
1942 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
1943 sambaopts=None, versionopts=None):
1945 self.lp = sambaopts.get_loadparm()
1947 self.samdb_url = None
1951 if not cache_ldb_initialize:
1952 if attributes is not None:
1953 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
1954 if decrypt_samba_gpg:
1955 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
1956 if script is not None:
1957 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
1958 if filter is not None:
1959 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
1961 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
1963 if nowait is not False:
1964 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
1965 if logfile is not None:
1966 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
1967 if daemon is not False:
1968 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
1969 if terminate is not False:
1970 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
1974 raise CommandError("--daemon is not allowed together with --no-wait")
1975 if terminate is not False:
1976 raise CommandError("--terminate is not allowed together with --no-wait")
1978 if terminate is True and daemon is True:
1979 raise CommandError("--terminate is not allowed together with --daemon")
1981 if daemon is True and logfile is None:
1982 raise CommandError("--daemon is only allowed together with --logfile")
1984 if terminate is True and logfile is None:
1985 raise CommandError("--terminate is only allowed together with --logfile")
1987 if script is not None:
1988 if not os.path.exists(script):
1989 raise CommandError("script[%s] does not exist!" % script)
1991 sync_command = "%s" % os.path.abspath(script)
1995 dirsync_filter = filter
1996 if dirsync_filter is None:
1997 dirsync_filter = "(&" + \
1998 "(objectClass=user)" + \
1999 "(userAccountControl:%s:=%u)" % (
2000 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
2001 "(!(sAMAccountName=krbtgt*))" + \
2004 dirsync_secret_attrs = [
2007 "supplementalCredentials",
2010 dirsync_attrs = dirsync_secret_attrs + [
2013 "userPrincipalName",
2014 "userAccountControl",
2019 password_attrs = None
2021 if cache_ldb_initialize:
2023 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
2025 if decrypt_samba_gpg and not gpg_decrypt:
2026 raise CommandError(decrypt_samba_gpg_help)
2028 password_attrs = self.parse_attributes(attributes)
2029 lower_attrs = [x.lower() for x in password_attrs]
2030 # We always return these in order to track deletions
2031 for a in ["objectGUID", "isDeleted", "isRecycled"]:
2032 if a.lower() not in lower_attrs:
2033 password_attrs += [a]
2035 if cache_ldb is not None:
2036 if cache_ldb.lower().startswith("ldapi://"):
2037 raise CommandError("--cache_ldb ldapi:// is not supported")
2038 elif cache_ldb.lower().startswith("ldap://"):
2039 raise CommandError("--cache_ldb ldap:// is not supported")
2040 elif cache_ldb.lower().startswith("ldaps://"):
2041 raise CommandError("--cache_ldb ldaps:// is not supported")
2042 elif cache_ldb.lower().startswith("tdb://"):
2045 if not os.path.exists(cache_ldb):
2046 cache_ldb = self.lp.private_path(cache_ldb)
2048 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
2050 self.lockfile = "%s.pid" % cache_ldb
2053 if self.logfile is not None:
2055 if info.st_nlink == 0:
2056 logfile = self.logfile
2058 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
2059 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2064 log_msg("Reopened logfile[%s]\n" % (logfile))
2065 self.logfile = logfile
2066 msg = "%s: pid[%d]: %s" % (
2070 self.outf.write(msg)
2079 "passwordAttribute",
2085 self.cache = Ldb(cache_ldb)
2086 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
2087 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
2091 self.samdb_url = str(res[0]["samdbUrl"][0])
2092 except KeyError as e:
2093 self.samdb_url = None
2095 self.samdb_url = None
2096 if self.samdb_url is None and not cache_ldb_initialize:
2097 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
2099 if self.samdb_url is not None and cache_ldb_initialize:
2100 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
2102 if self.samdb_url is None:
2104 self.dirsync_filter = dirsync_filter
2105 self.dirsync_attrs = dirsync_attrs
2106 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"]
2107 self.password_attrs = password_attrs
2108 self.decrypt_samba_gpg = decrypt_samba_gpg
2109 self.sync_command = sync_command
2110 add_ldif = "dn: %s\n" % self.cache_dn +\
2111 "objectClass: userSyncPasswords\n" +\
2112 "samdbUrl:: %s\n" % base64.b64encode(get_bytes(self.samdb_url)).decode('utf8') +\
2113 "dirsyncFilter:: %s\n" % base64.b64encode(get_bytes(self.dirsync_filter)).decode('utf8') +\
2114 "".join("dirsyncAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.dirsync_attrs) +\
2115 "dirsyncControl: %s\n" % self.dirsync_controls[0] +\
2116 "".join("passwordAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.password_attrs)
2117 if self.decrypt_samba_gpg:
2118 add_ldif += "decryptSambaGPG: TRUE\n"
2120 add_ldif += "decryptSambaGPG: FALSE\n"
2121 if self.sync_command is not None:
2122 add_ldif += "syncCommand: %s\n" % self.sync_command
2123 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2124 self.cache.add_ldif(add_ldif)
2125 self.current_pid = None
2126 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
2127 msgs = self.cache.parse_ldif(add_ldif)
2128 changetype, msg = next(msgs)
2129 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
2130 self.outf.write("%s" % ldif)
2132 self.dirsync_filter = str(res[0]["dirsyncFilter"][0])
2133 self.dirsync_attrs = []
2134 for a in res[0]["dirsyncAttribute"]:
2135 self.dirsync_attrs.append(str(a))
2136 self.dirsync_controls = [str(res[0]["dirsyncControl"][0]), "extended_dn:1:0"]
2137 self.password_attrs = []
2138 for a in res[0]["passwordAttribute"]:
2139 self.password_attrs.append(str(a))
2140 decrypt_string = str(res[0]["decryptSambaGPG"][0])
2141 assert(decrypt_string in ["TRUE", "FALSE"])
2142 if decrypt_string == "TRUE":
2143 self.decrypt_samba_gpg = True
2145 self.decrypt_samba_gpg = False
2146 if "syncCommand" in res[0]:
2147 self.sync_command = str(res[0]["syncCommand"][0])
2149 self.sync_command = None
2150 if "currentPid" in res[0]:
2151 self.current_pid = int(res[0]["currentPid"][0])
2153 self.current_pid = None
2154 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
2158 def run_sync_command(dn, ldif):
2159 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
2160 sync_command_p = Popen(self.sync_command,
2165 res = sync_command_p.poll()
2168 input = "%s" % (ldif)
2169 reply = sync_command_p.communicate(
2170 input.encode('utf-8'))[0].decode('utf-8')
2171 log_msg("%s\n" % (reply))
2172 res = sync_command_p.poll()
2174 sync_command_p.terminate()
2175 res = sync_command_p.wait()
2177 if reply.startswith("DONE-EXIT: "):
2180 log_msg("RESULT: %s\n" % (res))
2181 raise Exception("ERROR: %s - %s\n" % (res, reply))
2183 def handle_object(idx, dirsync_obj):
2184 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
2185 guid = ndr_unpack(misc.GUID, binary_guid)
2186 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2187 sid = ndr_unpack(security.dom_sid, binary_sid)
2188 domain_sid, rid = sid.split()
2189 if rid == security.DOMAIN_RID_KRBTGT:
2190 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
2192 for a in list(dirsync_obj.keys()):
2193 for h in dirsync_secret_attrs:
2194 if a.lower() == h.lower():
2196 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
2197 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
2198 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
2199 obj = self.get_account_attributes(self.samdb,
2200 username="%s" % sid,
2201 basedn="<GUID=%s>" % guid,
2202 filter="(objectClass=user)",
2203 scope=ldb.SCOPE_BASE,
2204 attrs=self.password_attrs,
2205 decrypt=self.decrypt_samba_gpg)
2206 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
2207 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
2208 if self.sync_command is None:
2209 self.outf.write("%s" % (ldif))
2211 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
2212 run_sync_command(obj.dn, ldif)
2214 def check_current_pid_conflict(terminate):
2220 self.lockfd = os.open(self.lockfile, flags, 0o600)
2221 except IOError as e4:
2222 (err, msg) = e4.args
2223 if err == errno.ENOENT:
2226 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
2227 (self.lockfile, msg, err))
2230 got_exclusive = False
2232 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2233 got_exclusive = True
2234 except IOError as e5:
2235 (err, msg) = e5.args
2236 if err != errno.EACCES and err != errno.EAGAIN:
2237 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
2238 (self.lockfile, msg, err))
2241 if not got_exclusive:
2242 buf = os.read(self.lockfd, 64)
2243 self.current_pid = None
2245 self.current_pid = int(buf)
2246 except ValueError as e:
2248 if self.current_pid is not None:
2251 if got_exclusive and terminate:
2253 os.ftruncate(self.lockfd, 0)
2254 except IOError as e2:
2255 (err, msg) = e2.args
2256 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2257 (self.lockfile, msg, err))
2259 os.close(self.lockfd)
2264 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2265 except IOError as e6:
2266 (err, msg) = e6.args
2267 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2268 (self.lockfile, msg, err))
2270 # We leave the function with the shared lock.
2273 def update_pid(pid):
2274 if self.lockfd != -1:
2275 got_exclusive = False
2276 # Try 5 times to get the exclusiv lock.
2277 for i in range(0, 5):
2279 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2280 got_exclusive = True
2281 except IOError as e:
2283 if err != errno.EACCES and err != errno.EAGAIN:
2284 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2285 (pid, self.lockfile, msg, err))
2290 if not got_exclusive:
2291 log_msg("update_pid(%r): failed to get exclusive lock[%s]" %
2292 (pid, self.lockfile))
2293 raise CommandError("update_pid(%r): failed to get "
2294 "exclusive lock[%s] after 5 seconds" %
2295 (pid, self.lockfile))
2302 os.ftruncate(self.lockfd, 0)
2304 os.write(self.lockfd, get_bytes(buf))
2305 except IOError as e3:
2306 (err, msg) = e3.args
2307 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2308 (self.lockfile, msg, err))
2310 self.current_pid = pid
2311 if self.current_pid is not None:
2312 log_msg("currentPid: %d\n" % self.current_pid)
2314 modify_ldif = "dn: %s\n" % (self.cache_dn) +\
2315 "changetype: modify\n" +\
2316 "replace: currentPid\n"
2317 if self.current_pid is not None:
2318 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2319 modify_ldif += "replace: currentTime\n" +\
2320 "currentTime: %s\n" % ldb.timestring(int(time.time()))
2321 self.cache.modify_ldif(modify_ldif)
2324 def update_cache(res_controls):
2325 assert len(res_controls) > 0
2326 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2327 res_controls[0].critical = True
2328 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2329 # This cookie can be extremely long
2330 # log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2332 modify_ldif = "dn: %s\n" % (self.cache_dn) +\
2333 "changetype: modify\n" +\
2334 "replace: dirsyncControl\n" +\
2335 "dirsyncControl: %s\n" % (self.dirsync_controls[0]) +\
2336 "replace: currentTime\n" +\
2337 "currentTime: %s\n" % ldb.timestring(int(time.time()))
2338 self.cache.modify_ldif(modify_ldif)
2341 def check_object(dirsync_obj, res_controls):
2342 assert len(res_controls) > 0
2343 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2345 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2346 sid = ndr_unpack(security.dom_sid, binary_sid)
2348 lastCookie = str(res_controls[0])
2350 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2351 expression="(lastCookie=%s)" % (
2352 ldb.binary_encode(lastCookie)),
2358 def update_object(dirsync_obj, res_controls):
2359 assert len(res_controls) > 0
2360 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2362 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2363 sid = ndr_unpack(security.dom_sid, binary_sid)
2365 lastCookie = str(res_controls[0])
2367 self.cache.transaction_start()
2369 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2370 expression="(objectClass=*)",
2371 attrs=["lastCookie"])
2373 add_ldif = "dn: %s\n" % (dn) +\
2374 "objectClass: userCookie\n" +\
2375 "lastCookie: %s\n" % (lastCookie) +\
2376 "currentTime: %s\n" % ldb.timestring(int(time.time()))
2377 self.cache.add_ldif(add_ldif)
2379 modify_ldif = "dn: %s\n" % (dn) +\
2380 "changetype: modify\n" +\
2381 "replace: lastCookie\n" +\
2382 "lastCookie: %s\n" % (lastCookie) +\
2383 "replace: currentTime\n" +\
2384 "currentTime: %s\n" % ldb.timestring(int(time.time()))
2385 self.cache.modify_ldif(modify_ldif)
2386 self.cache.transaction_commit()
2387 except Exception as e:
2388 self.cache.transaction_cancel()
2394 res = self.samdb.search(expression=str(self.dirsync_filter),
2395 scope=ldb.SCOPE_SUBTREE,
2396 attrs=self.dirsync_attrs,
2397 controls=self.dirsync_controls)
2398 log_msg("dirsync_loop(): results %d\n" % len(res))
2401 done = check_object(r, res.controls)
2403 handle_object(ri, r)
2404 update_object(r, res.controls)
2406 update_cache(res.controls)
2410 def sync_loop(wait):
2411 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2412 notify_controls = ["notification:1", "show_recycled:1"]
2413 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2414 scope=ldb.SCOPE_SUBTREE,
2416 controls=notify_controls,
2420 log_msg("Resuming monitoring\n")
2422 log_msg("Getting changes\n")
2423 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2424 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2425 self.outf.write("syncCommand: %s\n" % self.sync_command)
2428 if wait is not True:
2431 for msg in notify_handle:
2432 if not isinstance(msg, ldb.Message):
2433 self.outf.write("referal: %s\n" % msg)
2435 created = msg.get("uSNCreated")[0]
2436 changed = msg.get("uSNChanged")[0]
2437 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2438 (msg.dn, created, changed))
2442 res = notify_handle.result()
2447 orig_pid = os.getpid()
2452 if pid == 0: # Actual daemon
2454 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2459 if cache_ldb_initialize:
2461 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2466 if logfile is not None:
2467 import resource # Resource usage information.
2468 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2469 if maxfd == resource.RLIM_INFINITY:
2470 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2471 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2472 self.outf.write("Using logfile[%s]\n" % logfile)
2473 for fd in range(0, maxfd):
2484 log_msg("Attached to logfile[%s]\n" % (logfile))
2485 self.logfile = logfile
2488 conflict = check_current_pid_conflict(terminate)
2490 if self.current_pid is None:
2491 log_msg("No process running.\n")
2494 log_msg("Proccess %d is not running anymore.\n" % (
2498 log_msg("Sending SIGTERM to proccess %d.\n" % (
2500 os.kill(self.current_pid, signal.SIGTERM)
2503 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2504 os.getpid(), self.current_pid))
2508 update_pid(os.getpid())
2513 retry_sleep_max = 600
2518 retry_sleep = retry_sleep_min
2520 while self.samdb is None:
2521 if retry_sleep != 0:
2522 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2523 time.sleep(retry_sleep)
2524 retry_sleep = retry_sleep * 2
2525 if retry_sleep >= retry_sleep_max:
2526 retry_sleep = retry_sleep_max
2527 log_msg("Connecting to '%s'\n" % self.samdb_url)
2529 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2530 except Exception as msg:
2532 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2533 if wait is not True:
2538 except ldb.LdbError as e7:
2539 (enum, estr) = e7.args
2541 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2547 class cmd_user_edit(Command):
2548 """Modify User AD object.
2550 This command will allow editing of a user account in the Active Directory
2551 domain. You will then be able to add or change attributes and their values.
2553 The username specified on the command is the sAMAccountName.
2555 The command may be run from the root userid or another authorized userid.
2557 The -H or --URL= option can be used to execute the command against a remote
2561 samba-tool user edit User1 -H ldap://samba.samdom.example.com \\
2562 -U administrator --password=passw1rd
2564 Example1 shows how to edit a users attributes in the domain against a remote
2567 The -H parameter is used to specify the remote target server.
2570 samba-tool user edit User2
2572 Example2 shows how to edit a users attributes in the domain against a local
2576 samba-tool user edit User3 --editor=nano
2578 Example3 shows how to edit a users attributes in the domain against a local
2579 LDAP server using the 'nano' editor.
2582 synopsis = "%prog <username> [options]"
2585 Option("-H", "--URL", help="LDB URL for database or target server",
2586 type=str, metavar="URL", dest="H"),
2587 Option("--editor", help="Editor to use instead of the system default,"
2588 " or 'vi' if no system default is set.", type=str),
2591 takes_args = ["username"]
2592 takes_optiongroups = {
2593 "sambaopts": options.SambaOptions,
2594 "credopts": options.CredentialsOptions,
2595 "versionopts": options.VersionOptions,
2598 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2599 H=None, editor=None):
2600 lp = sambaopts.get_loadparm()
2601 creds = credopts.get_credentials(lp, fallback_machine=True)
2602 samdb = SamDB(url=H, session_info=system_session(),
2603 credentials=creds, lp=lp)
2605 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2606 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2608 domaindn = samdb.domain_dn()
2611 res = samdb.search(base=domaindn,
2613 scope=ldb.SCOPE_SUBTREE)
2616 raise CommandError('Unable to find user "%s"' % (username))
2620 result_ldif = common.get_ldif_for_editor(samdb, msg)
2623 editor = os.environ.get('EDITOR')
2627 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2628 t_file.write(get_bytes(result_ldif))
2631 check_call([editor, t_file.name])
2632 except CalledProcessError as e:
2633 raise CalledProcessError("ERROR: ", e)
2634 with open(t_file.name) as edited_file:
2635 edited_message = edited_file.read()
2638 msgs_edited = samdb.parse_ldif(edited_message)
2639 msg_edited = next(msgs_edited)[1]
2641 res_msg_diff = samdb.msg_diff(msg, msg_edited)
2642 if len(res_msg_diff) == 0:
2643 self.outf.write("Nothing to do\n")
2647 samdb.modify(res_msg_diff)
2648 except Exception as e:
2649 raise CommandError("Failed to modify user '%s': " % username, e)
2651 self.outf.write("Modified User '%s' successfully\n" % username)
2654 class cmd_user_show(Command):
2655 """Display a user AD object.
2657 This command displays a user account and it's attributes in the Active
2659 The username specified on the command is the sAMAccountName.
2661 The command may be run from the root userid or another authorized userid.
2663 The -H or --URL= option can be used to execute the command against a remote
2667 samba-tool user show User1 -H ldap://samba.samdom.example.com \\
2668 -U administrator --password=passw1rd
2670 Example1 shows how to display a users attributes in the domain against a remote
2673 The -H parameter is used to specify the remote target server.
2676 samba-tool user show User2
2678 Example2 shows how to display a users attributes in the domain against a local
2682 samba-tool user show User2 --attributes=objectSid,memberOf
2684 Example3 shows how to display a users objectSid and memberOf attributes.
2686 synopsis = "%prog <username> [options]"
2689 Option("-H", "--URL", help="LDB URL for database or target server",
2690 type=str, metavar="URL", dest="H"),
2691 Option("--attributes",
2692 help=("Comma separated list of attributes, "
2693 "which will be printed."),
2694 type=str, dest="user_attrs"),
2697 takes_args = ["username"]
2698 takes_optiongroups = {
2699 "sambaopts": options.SambaOptions,
2700 "credopts": options.CredentialsOptions,
2701 "versionopts": options.VersionOptions,
2704 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2705 H=None, user_attrs=None):
2707 lp = sambaopts.get_loadparm()
2708 creds = credopts.get_credentials(lp, fallback_machine=True)
2709 samdb = SamDB(url=H, session_info=system_session(),
2710 credentials=creds, lp=lp)
2714 attrs = user_attrs.split(",")
2716 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2717 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2719 domaindn = samdb.domain_dn()
2722 res = samdb.search(base=domaindn, expression=filter,
2723 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
2726 raise CommandError('Unable to find user "%s"' % (username))
2729 user_ldif = common.get_ldif_for_editor(samdb, msg)
2730 self.outf.write(user_ldif)
2733 class cmd_user_move(Command):
2734 """Move a user to an organizational unit/container.
2736 This command moves a user account into the specified organizational unit
2738 The username specified on the command is the sAMAccountName.
2739 The name of the organizational unit or container can be specified as a
2740 full DN or without the domainDN component.
2742 The command may be run from the root userid or another authorized userid.
2744 The -H or --URL= option can be used to execute the command against a remote
2748 samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\
2749 -H ldap://samba.samdom.example.com -U administrator
2751 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
2752 unit on a remote LDAP server.
2754 The -H parameter is used to specify the remote target server.
2757 samba-tool user move User1 CN=Users
2759 Example2 shows how to move a user User1 back into the CN=Users container
2760 on the local server.
2763 synopsis = "%prog <username> <new_parent_dn> [options]"
2766 Option("-H", "--URL", help="LDB URL for database or target server",
2767 type=str, metavar="URL", dest="H"),
2770 takes_args = ["username", "new_parent_dn"]
2771 takes_optiongroups = {
2772 "sambaopts": options.SambaOptions,
2773 "credopts": options.CredentialsOptions,
2774 "versionopts": options.VersionOptions,
2777 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
2778 versionopts=None, H=None):
2779 lp = sambaopts.get_loadparm()
2780 creds = credopts.get_credentials(lp, fallback_machine=True)
2781 samdb = SamDB(url=H, session_info=system_session(),
2782 credentials=creds, lp=lp)
2783 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2785 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2786 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2788 res = samdb.search(base=domain_dn,
2790 scope=ldb.SCOPE_SUBTREE)
2793 raise CommandError('Unable to find user "%s"' % (username))
2796 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
2797 except Exception as e:
2798 raise CommandError('Invalid new_parent_dn "%s": %s' %
2801 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
2802 full_new_user_dn.remove_base_components(len(user_dn) - 1)
2803 full_new_user_dn.add_base(full_new_parent_dn)
2806 samdb.rename(user_dn, full_new_user_dn)
2807 except Exception as e:
2808 raise CommandError('Failed to move user "%s"' % username, e)
2809 self.outf.write('Moved user "%s" into "%s"\n' %
2810 (username, full_new_parent_dn))
2813 class cmd_user_rename(Command):
2814 """Rename a user and related attributes.
2816 This command allows to set the user's name related attributes. The user's
2817 CN will be renamed automatically.
2818 The user's new CN will be made up by combining the given-name, initials
2819 and surname. A dot ('.') will be appended to the initials automatically
2821 Use the --force-new-cn option to specify the new CN manually and the
2822 --reset-cn option to reset this change.
2824 Use an empty attribute value to remove the specified attribute.
2826 The username specified on the command is the sAMAccountName.
2828 The command may be run locally from the root userid or another authorized
2831 The -H or --URL= option can be used to execute the command against a remote
2835 samba-tool user rename johndoe --surname='Bloggs'
2837 Example1 shows how to change the surname of a user 'johndoe' to 'Bloggs' on
2838 the local server. The user's CN will be renamed automatically, based on
2839 the given name, initials and surname.
2842 samba-tool user rename johndoe --force-new-cn='John Bloggs (Sales)' \\
2843 --surname=Bloggs -H ldap://samba.samdom.example.com -U administrator
2845 Example2 shows how to rename the CN of a user 'johndoe' to 'John Bloggs (Sales)'.
2846 Additionally the surname ('sn' attribute) is set to 'Bloggs'.
2847 The -H parameter is used to specify the remote target server.
2850 synopsis = "%prog <username> [options]"
2853 Option("-H", "--URL",
2854 help="LDB URL for database or target server",
2855 type=str, metavar="URL", dest="H"),
2859 Option("--given-name",
2860 help="New given name",
2862 Option("--initials",
2863 help="New initials",
2865 Option("--force-new-cn",
2866 help="Specify a new CN (RDN) instead of using a combination "
2867 "of the given name, initials and surname.",
2868 type=str, metavar="NEW_CN"),
2869 Option("--reset-cn",
2870 help="Set the CN (RDN) to the combination of the given name, "
2871 "initials and surname. Use this option to reset "
2872 "the changes made with the --force-new-cn option.",
2873 action="store_true"),
2874 Option("--display-name",
2875 help="New display name",
2877 Option("--mail-address",
2878 help="New email address",
2880 Option("--samaccountname",
2881 help="New account name (sAMAccountName/logon name)",
2884 help="New user principal name",
2888 takes_args = ["username"]
2889 takes_optiongroups = {
2890 "sambaopts": options.SambaOptions,
2891 "credopts": options.CredentialsOptions,
2892 "versionopts": options.VersionOptions,
2895 def run(self, username, credopts=None, sambaopts=None,
2896 versionopts=None, H=None, surname=None, given_name=None,
2897 initials=None, display_name=None, mail_address=None,
2898 samaccountname=None, upn=None, force_new_cn=None,
2901 if force_new_cn and reset_cn:
2902 raise CommandError("It is not allowed to specify --force-new-cn "
2903 "together with --reset-cn.")
2904 if force_new_cn == "":
2905 raise CommandError("Failed to rename user - delete protected "
2907 if samaccountname == "":
2908 raise CommandError("Failed to rename user - delete protected "
2909 "attribute 'sAMAccountName'")
2911 lp = sambaopts.get_loadparm()
2912 creds = credopts.get_credentials(lp, fallback_machine=True)
2913 samdb = SamDB(url=H, session_info=system_session(),
2914 credentials=creds, lp=lp)
2915 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
2917 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2918 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2920 res = samdb.search(base=domain_dn,
2922 scope=ldb.SCOPE_SUBTREE,
2923 attrs=["sAMAccountName",
2928 "userPrincipalName",
2932 user_dn = old_user.dn
2934 raise CommandError('Unable to find user "%s"' % (username))
2936 user_parent_dn = user_dn.parent()
2937 old_cn = old_user["cn"][0]
2939 # use the sAMAccountname as CN if no name is given
2940 new_fallback_cn = samaccountname if samaccountname is not None \
2941 else old_user["sAMAccountName"]
2943 if force_new_cn is not None:
2944 new_user_cn = force_new_cn
2946 new_user_cn = samdb.fullname_from_names(old_attrs=old_user,
2947 given_name=given_name,
2950 fallback_default=new_fallback_cn)
2952 # CN must change, if the new CN is different and the old CN is the
2953 # standard CN or the change is forced with force-new-cn or reset-cn
2954 expected_cn = samdb.fullname_from_names(old_attrs=old_user,
2955 fallback_default=old_user["sAMAccountName"])
2956 must_change_cn = str(old_cn) != str(new_user_cn) and \
2957 (str(old_cn) == str(expected_cn) or \
2958 reset_cn or bool(force_new_cn))
2960 new_user_dn = ldb.Dn(samdb, "CN=%s" % new_user_cn)
2961 new_user_dn.add_base(user_parent_dn)
2964 if self.is_valid_upn(samdb, upn) == False:
2965 raise CommandError('"%s" is not a valid upn. '
2966 'You can manage the upn '
2967 'suffixes with the "samba-tool domain '
2968 'trust namespaces" command.' % upn)
2970 user_attrs = ldb.Message()
2971 user_attrs.dn = user_dn
2972 samdb.prepare_attr_replace(user_attrs, old_user, "givenName", given_name)
2973 samdb.prepare_attr_replace(user_attrs, old_user, "initials", initials)
2974 samdb.prepare_attr_replace(user_attrs, old_user, "sn", surname)
2975 samdb.prepare_attr_replace(user_attrs, old_user, "displayName", display_name)
2976 samdb.prepare_attr_replace(user_attrs, old_user, "mail", mail_address)
2977 samdb.prepare_attr_replace(user_attrs, old_user, "sAMAccountName", samaccountname)
2978 samdb.prepare_attr_replace(user_attrs, old_user, "userPrincipalName", upn)
2980 attributes_changed = len(user_attrs) > 0
2982 samdb.transaction_start()
2984 if attributes_changed == True:
2985 samdb.modify(user_attrs)
2986 if must_change_cn == True:
2987 samdb.rename(user_dn, new_user_dn)
2988 except Exception as e:
2989 samdb.transaction_cancel()
2990 raise CommandError('Failed to rename user "%s"' % username, e)
2991 samdb.transaction_commit()
2993 if must_change_cn == True:
2994 self.outf.write('Renamed CN of user "%s" from "%s" to "%s" '
2995 'successfully\n' % (username, old_cn, new_user_cn))
2997 if attributes_changed == True:
2998 self.outf.write('Following attributes of user "%s" have been '
2999 'changed successfully:\n' % (username))
3000 for attr in user_attrs.keys():
3003 self.outf.write('%s: %s\n' % (attr, user_attrs[attr]
3004 if user_attrs[attr] else '[removed]'))
3006 def is_valid_upn(self, samdb, upn):
3007 domain_dns = samdb.domain_dns_name()
3008 forest_dns = samdb.forest_dns_name()
3009 upn_suffixes = [domain_dns, forest_dns]
3011 config_basedn = samdb.get_config_basedn()
3012 partitions_dn = "CN=Partitions,%s" % config_basedn
3015 scope=ldb.SCOPE_BASE,
3016 expression="(objectClass=crossRefContainer)",
3017 attrs=['uPNSuffixes'])
3021 if 'uPNSuffixes' in msg:
3022 for s in msg['uPNSuffixes']:
3023 upn_suffixes.append(str(s).lower())
3025 upn_suffix = upn.split('@')[-1].lower()
3026 upn_split = upn.split('@')
3027 if (len(upn_split) < 2):
3030 upn_suffix = upn_split[-1].lower()
3031 if upn_suffix not in upn_suffixes:
3037 class cmd_user_add_unix_attrs(Command):
3038 """Add RFC2307 attributes to a user.
3040 This command adds Unix attributes to a user account in the Active
3043 The username specified on the command is the sAMaccountName.
3045 You must supply a unique uidNumber.
3047 Unix (RFC2307) attributes will be added to the user account.
3049 If you supply a gidNumber with '--gid-number', this will be used for the
3050 users Unix 'gidNumber' attribute.
3052 If '--gid-number' is not supplied, the users Unix gidNumber will be set to the
3053 one found in 'Domain Users', this means Domain Users must have a gidNumber
3056 if '--unix-home' is not supplied, the users Unix home directory will be
3057 set to /home/DOMAIN/username
3059 if '--login-shell' is not supplied, the users Unix login shell will be
3062 if ---gecos' is not supplied, the users Unix gecos field will be set to the
3065 Add 'idmap_ldb:use rfc2307 = Yes' to the smb.conf on DCs, to use these
3066 attributes for UID/GID mapping.
3068 The command may be run from the root userid or another authorised userid.
3069 The -H or --URL= option can be used to execute the command against a
3073 samba-tool user addunixattrs User1 10001
3075 Example1 shows how to add RFC2307 attributes to a domain enabled user
3076 account, Domain Users will be set as the users gidNumber.
3078 The users Unix ID will be set to '10001', provided this ID isn't already
3082 samba-tool user addunixattrs User2 10002 --gid-number=10001 \
3083 --unix-home=/home/User2
3085 Example2 shows how to add RFC2307 attributes to a domain enabled user
3088 The users Unix ID will be set to '10002', provided this ID isn't already
3091 The users gidNumber attribute will be set to '10001'
3093 The users Unix home directory will be set to '/home/user2'
3096 samba-tool user addunixattrs User3 10003 --gid-number=10001 \
3097 --login-shell=/bin/false --gecos='User3 test'
3099 Example3 shows how to add RFC2307 attributes to a domain enabled user
3102 The users Unix ID will be set to '10003', provided this ID isn't already
3105 The users gidNumber attribute will be set to '10001'
3107 The users Unix login shell will be set to '/bin/false'
3109 The users gecos field will be set to 'User3 test'
3112 samba-tool user addunixattrs User4 10004 --gid-number=10001 \
3113 --unix-home=/home/User4 --login-shell=/bin/bash --gecos='User4 test'
3115 Example4 shows how to add RFC2307 attributes to a domain enabled user
3118 The users Unix ID will be set to '10004', provided this ID isn't already
3121 The users gidNumber attribute will be set to '10001'
3123 The users Unix home directory will be set to '/home/User4'
3125 The users Unix login shell will be set to '/bin/bash'
3127 The users gecos field will be set to 'User4 test'
3131 synopsis = "%prog <username> <uid-number> [options]"
3134 Option("-H", "--URL", help="LDB URL for database or target server",
3135 type=str, metavar="URL", dest="H"),
3136 Option("--gid-number", help="User's Unix/RFC2307 GID", type=str),
3137 Option("--unix-home", help="User's Unix/RFC2307 home directory",
3139 Option("--login-shell", help="User's Unix/RFC2307 login shell",
3141 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
3142 Option("--uid", help="User's Unix/RFC2307 username", type=str),
3145 takes_args = ["username", "uid-number"]
3147 takes_optiongroups = {
3148 "sambaopts": options.SambaOptions,
3149 "credopts": options.CredentialsOptions,
3150 "versionopts": options.VersionOptions,
3153 def run(self, username, uid_number, credopts=None, sambaopts=None,
3154 versionopts=None, H=None, gid_number=None, unix_home=None,
3155 login_shell=None, gecos=None, uid=None):
3157 lp = sambaopts.get_loadparm()
3158 creds = credopts.get_credentials(lp)
3160 samdb = SamDB(url=H, session_info=system_session(),
3161 credentials=creds, lp=lp)
3163 domaindn = samdb.domain_dn()
3165 # Check that uidNumber supplied isn't already in use
3166 filter = ("(&(objectClass=person)(uidNumber={}))"
3167 .format(uid_number))
3168 res = samdb.search(domaindn,
3169 scope=ldb.SCOPE_SUBTREE,
3172 raise CommandError("uidNumber {} is already being used."
3173 .format(uid_number))
3175 # Check user exists and doesn't have a uidNumber
3176 filter = "(samaccountname={})".format(ldb.binary_encode(username))
3177 res = samdb.search(domaindn,
3178 scope=ldb.SCOPE_SUBTREE,
3181 raise CommandError("Unable to find user '{}'".format(username))
3185 if "uidNumber" in res[0]:
3186 raise CommandError("User {} is already a Unix user."
3190 gecos = res[0]["cn"][0]
3193 uid = res[0]["cn"][0]
3195 if gid_number is None:
3196 search_filter = ("(samaccountname={})"
3197 .format(ldb.binary_encode('Domain Users')))
3199 res = samdb.search(domaindn,
3200 scope=ldb.SCOPE_SUBTREE,
3201 expression=search_filter)
3203 gid_number=msg.get('gidNumber')
3205 raise CommandError('Domain Users does not have a'
3206 ' gidNumber attribute')
3208 if login_shell is None:
3209 login_shell = "/bin/sh"
3211 if unix_home is None:
3212 # obtain nETBIOS Domain Name
3213 filter = "(&(objectClass=crossRef)(nETBIOSName=*))"
3214 searchdn = ("CN=Partitions,CN=Configuration," + domaindn)
3216 res = samdb.search(searchdn,
3217 scope=ldb.SCOPE_SUBTREE,
3219 unix_domain = res[0]["nETBIOSName"][0].decode()
3221 raise CommandError('Unable to find Unix domain')
3223 tmpl = lp.get('template homedir')
3224 unix_home = tmpl.replace('%D', unix_domain).replace('%U', username)
3226 if not lp.get("idmap_ldb:use rfc2307"):
3227 self.outf.write("You are setting a Unix/RFC2307 UID & GID. "
3228 "You may want to set 'idmap_ldb:use rfc2307 = Yes'"
3229 " in smb.conf to use the attributes for "
3230 "XID/SID-mapping.\n")
3245 add: unixHomeDirectory
3246 unixHomeDirectory: {6}
3247 """.format(user_dn, uid_number, gid_number, gecos, uid, login_shell, unix_home)
3249 samdb.transaction_start()
3251 samdb.modify_ldif(user_mod)
3252 except ldb.LdbError as e:
3253 raise CommandError("Failed to modify user '{0}': {1}"
3254 .format(username, e))
3256 samdb.transaction_commit()
3257 self.outf.write("Modified User '{}' successfully\n"
3260 class cmd_user_unlock(Command):
3261 """Unlock a user account.
3263 This command unlocks a user account in the Active Directory domain. The
3264 username specified on the command is the sAMAccountName. The username may
3265 also be specified using the --filter option.
3267 The command may be run from the root userid or another authorized userid.
3268 The -H or --URL= option can be used to execute the command against a remote
3272 samba-tool user unlock user1 -H ldap://samba.samdom.example.com \\
3273 --username=Administrator --password=Passw0rd
3275 The example shows how to unlock a user account in the domain against a
3276 remote LDAP server. The -H parameter is used to specify the remote target
3277 server. The --username= and --password= options are used to pass the
3278 username and password of a user that exists on the remote server and is
3279 authorized to issue the command on that server.
3282 synopsis = "%prog (<username>|--filter <filter>) [options]"
3287 help="LDB URL for database or target server",
3292 help="LDAP Filter to set password on",
3296 takes_args = ["username?"]
3298 takes_optiongroups = {
3299 "sambaopts": options.SambaOptions,
3300 "credopts": options.CredentialsOptions,
3301 "versionopts": options.VersionOptions,
3311 if username is None and filter is None:
3312 raise CommandError("Either the username or '--filter' must be "
3316 filter = ("(&(objectClass=user)(sAMAccountName=%s))" % (
3317 ldb.binary_encode(username)))
3319 lp = sambaopts.get_loadparm()
3320 creds = credopts.get_credentials(lp, fallback_machine=True)
3322 samdb = SamDB(url=H,
3323 session_info=system_session(),
3327 samdb.unlock_account(filter)
3328 except (SamDBError, ldb.LdbError) as msg:
3329 raise CommandError("Failed to unlock user '%s': %s" % (
3330 username or filter, msg))
3332 class cmd_user_sensitive(Command):
3333 """Set/unset or show UF_NOT_DELEGATED for an account."""
3335 synopsis = "%prog <accountname> [(show|on|off)] [options]"
3337 takes_optiongroups = {
3338 "sambaopts": options.SambaOptions,
3339 "credopts": options.CredentialsOptions,
3340 "versionopts": options.VersionOptions,
3344 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
3345 metavar="URL", dest="H"),
3348 takes_args = ["accountname", "cmd"]
3350 def run(self, accountname, cmd, H=None, credopts=None, sambaopts=None,
3353 if cmd not in ("show", "on", "off"):
3354 raise CommandError("invalid argument: '%s' (choose from 'show', 'on', 'off')" % cmd)
3356 lp = sambaopts.get_loadparm()
3357 creds = credopts.get_credentials(lp, fallback_machine=True)
3358 sam = SamDB(url=H, session_info=system_session(),
3359 credentials=creds, lp=lp)
3361 search_filter = "sAMAccountName=%s" % ldb.binary_encode(accountname)
3362 flag = dsdb.UF_NOT_DELEGATED;
3365 res = sam.search(scope=ldb.SCOPE_SUBTREE, expression=search_filter,
3366 attrs=["userAccountControl"])
3368 raise Exception("Unable to find account where '%s'" % search_filter)
3370 uac = int(res[0].get("userAccountControl")[0])
3372 self.outf.write("Account-DN: %s\n" % str(res[0].dn))
3373 self.outf.write("UF_NOT_DELEGATED: %s\n" % bool(uac & flag))
3383 sam.toggle_userAccountFlags(search_filter, flag, flags_str="Not-Delegated",
3385 except Exception as err:
3386 raise CommandError(err)
3389 class cmd_user(SuperCommand):
3390 """User management."""
3393 subcommands["add"] = cmd_user_add()
3394 subcommands["create"] = cmd_user_add()
3395 subcommands["delete"] = cmd_user_delete()
3396 subcommands["disable"] = cmd_user_disable()
3397 subcommands["enable"] = cmd_user_enable()
3398 subcommands["list"] = cmd_user_list()
3399 subcommands["setexpiry"] = cmd_user_setexpiry()
3400 subcommands["password"] = cmd_user_password()
3401 subcommands["getgroups"] = cmd_user_getgroups()
3402 subcommands["setprimarygroup"] = cmd_user_setprimarygroup()
3403 subcommands["setpassword"] = cmd_user_setpassword()
3404 subcommands["getpassword"] = cmd_user_getpassword()
3405 subcommands["syncpasswords"] = cmd_user_syncpasswords()
3406 subcommands["edit"] = cmd_user_edit()
3407 subcommands["show"] = cmd_user_show()
3408 subcommands["move"] = cmd_user_move()
3409 subcommands["rename"] = cmd_user_rename()
3410 subcommands["unlock"] = cmd_user_unlock()
3411 subcommands["addunixattrs"] = cmd_user_add_unix_attrs()
3412 subcommands["sensitive"] = cmd_user_sensitive()