s4:kdc: adjust formatting of samba_kdc_update_pac() documentation
[Samba.git] / python / samba / netcmd / user.py
blobc292def0985d407b3eacfad1e5a799372fb65446
1 # user management
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 builtins
21 import samba.getopt as options
22 import ldb
23 import pwd
24 import os
25 import io
26 import fcntl
27 import signal
28 import errno
29 import time
30 import base64
31 import binascii
32 from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError
33 from getpass import getpass
34 from samba.auth import system_session
35 from samba.samdb import SamDB, SamDBError
36 from samba.dcerpc import misc
37 from samba.dcerpc import security
38 from samba.dcerpc import drsblobs
39 from samba.ndr import ndr_unpack
40 from samba import (
41 credentials,
42 dsdb,
43 gensec,
44 generate_random_password,
45 Ldb,
46 nttime2float,
48 from samba.net import Net
50 from samba.netcmd import (
51 Command,
52 CommandError,
53 SuperCommand,
54 Option,
56 from samba.common import get_bytes
57 from samba.common import get_string
58 from . import common
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):
65 """
66 Use python[3]-gpgme to decrypt GPG.
67 """
68 ctx = gpgme.Context()
69 ctx.armor = True # use ASCII-armored
70 out = io.BytesIO()
71 ctx.decrypt(io.BytesIO(encrypted_bytes), out)
72 return out.getvalue()
75 def _gpg_decrypt(encrypted_bytes):
76 """
77 Use python[3]-gpg to decrypt GPG.
78 """
79 ciphertext = gpg.Data(string=encrypted_bytes)
80 ctx = gpg.Context(armor=True)
81 # plaintext, result, verify_result
82 plaintext, _, _ = ctx.decrypt(ciphertext)
83 return plaintext
86 gpg_decrypt = None
88 if not gpg_decrypt:
89 try:
90 import gpgme
91 gpg_decrypt = _gpgme_decrypt
92 except ImportError:
93 pass
95 if not gpg_decrypt:
96 try:
97 import gpg
98 gpg_decrypt = _gpg_decrypt
99 except ImportError:
100 pass
102 if gpg_decrypt:
103 decrypt_samba_gpg_help = ("Decrypt the SambaGPG password as "
104 "cleartext source")
105 else:
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,
120 "virtualSambaGPG": {
121 "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
126 def get_crypt_value(alg, utf8pw, rounds=0):
127 algs = {
128 "5": {"length": 43},
129 "6": {"length": 86},
131 assert alg in algs
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')
140 crypt_salt = ""
141 if rounds != 0:
142 crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
143 else:
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))
153 return crypt_value
155 try:
156 import hashlib
157 hashlib.sha1()
158 virtual_attributes["virtualSSHA"] = {
160 except ImportError as e:
161 reason = "hashlib.sha1()"
162 reason += " required"
163 disabled_virtual_attributes["virtualSSHA"] = {
164 "reason": reason,
167 for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
168 try:
169 import crypt
170 get_crypt_value(alg, "")
171 virtual_attributes[attr] = {
173 except ImportError as e:
174 reason = "crypt"
175 reason += " required"
176 disabled_virtual_attributes[attr] = {
177 "reason": reason,
179 except NotImplementedError as e:
180 reason = "modern '$%s$' salt in crypt(3) required" % (alg)
181 disabled_virtual_attributes[attr] = {
182 "reason": reason,
185 # Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
186 for x in range(1, 30):
187 virtual_attributes["virtualWDigest%02d" % x] = {}
189 # Add Kerberos virtual attributes
190 virtual_attributes["virtualKerberosSalt"] = {}
192 virtual_attributes_help = "The attributes to display (comma separated). "
193 virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
194 if len(disabled_virtual_attributes) != 0:
195 virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys()))
198 class cmd_user_add(Command):
199 """Add a new user.
201 This command adds a new user account to the Active Directory domain. The username specified on the command is the sAMaccountName.
203 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).
205 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.
207 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.
209 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.
211 Example1:
212 samba-tool user add User1 passw0rd --given-name=John --surname=Smith --must-change-at-next-login -H ldap://samba.samdom.example.com -Uadministrator%passw1rd
214 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.
216 Example2:
217 sudo samba-tool user add User2 passw2rd --given-name=Jane --surname=Doe --must-change-at-next-login
219 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.
221 Example3:
222 samba-tool user add User3 passw3rd --userou='OU=OrgUnit'
224 Example3 shows how to add a new user in the OrgUnit organizational unit.
226 Example4:
227 samba-tool user add User4 passw4rd --rfc2307-from-nss --gecos 'some text'
229 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'.
231 Example5:
232 samba-tool user add User5 passw5rd --nis-domain=samdom --unix-home=/home/User5 \\
233 --uid-number=10005 --login-shell=/bin/false --gid-number=10000
235 Example5 shows how to add a new RFC2307/NIS domain enabled user account. If
236 --nis-domain is set, then the other four parameters are mandatory.
239 synopsis = "%prog <username> [<password>] [options]"
241 takes_options = [
242 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
243 metavar="URL", dest="H"),
244 Option("--must-change-at-next-login",
245 help="Force password to be changed on next login",
246 action="store_true"),
247 Option("--random-password",
248 help="Generate random password",
249 action="store_true"),
250 Option("--smartcard-required",
251 help="Require a smartcard for interactive logons",
252 action="store_true"),
253 Option("--use-username-as-cn",
254 help="Force use of username as user's CN",
255 action="store_true"),
256 Option("--userou",
257 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>'",
258 type=str),
259 Option("--surname", help="User's surname", type=str),
260 Option("--given-name", help="User's given name", type=str),
261 Option("--initials", help="User's initials", type=str),
262 Option("--profile-path", help="User's profile path", type=str),
263 Option("--script-path", help="User's logon script path", type=str),
264 Option("--home-drive", help="User's home drive letter", type=str),
265 Option("--home-directory", help="User's home directory path", type=str),
266 Option("--job-title", help="User's job title", type=str),
267 Option("--department", help="User's department", type=str),
268 Option("--company", help="User's company", type=str),
269 Option("--description", help="User's description", type=str),
270 Option("--mail-address", help="User's email address", type=str),
271 Option("--internet-address", help="User's home page", type=str),
272 Option("--telephone-number", help="User's phone number", type=str),
273 Option("--physical-delivery-office", help="User's office location", type=str),
274 Option("--rfc2307-from-nss",
275 help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
276 action="store_true"),
277 Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
278 Option("--unix-home", help="User's Unix/RFC2307 home directory",
279 type=str),
280 Option("--uid", help="User's Unix/RFC2307 username", type=str),
281 Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
282 Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
283 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
284 Option("--login-shell", help="User's Unix/RFC2307 login shell", type=str),
287 takes_args = ["username", "password?"]
289 takes_optiongroups = {
290 "sambaopts": options.SambaOptions,
291 "credopts": options.CredentialsOptions,
292 "versionopts": options.VersionOptions,
295 def run(self, username, password=None, credopts=None, sambaopts=None,
296 versionopts=None, H=None, must_change_at_next_login=False,
297 random_password=False, use_username_as_cn=False, userou=None,
298 surname=None, given_name=None, initials=None, profile_path=None,
299 script_path=None, home_drive=None, home_directory=None,
300 job_title=None, department=None, company=None, description=None,
301 mail_address=None, internet_address=None, telephone_number=None,
302 physical_delivery_office=None, rfc2307_from_nss=False,
303 nis_domain=None, unix_home=None, uid=None, uid_number=None,
304 gid_number=None, gecos=None, login_shell=None,
305 smartcard_required=False):
307 if smartcard_required:
308 if password is not None and password != '':
309 raise CommandError('It is not allowed to specify '
310 '--newpassword '
311 'together with --smartcard-required.')
312 if must_change_at_next_login:
313 raise CommandError('It is not allowed to specify '
314 '--must-change-at-next-login '
315 'together with --smartcard-required.')
317 if random_password and not smartcard_required:
318 password = generate_random_password(128, 255)
320 while True:
321 if smartcard_required:
322 break
323 if password is not None and password != '':
324 break
325 password = getpass("New Password: ")
326 passwordverify = getpass("Retype Password: ")
327 if not password == passwordverify:
328 password = None
329 self.outf.write("Sorry, passwords do not match.\n")
331 if rfc2307_from_nss:
332 pwent = pwd.getpwnam(username)
333 if uid is None:
334 uid = username
335 if uid_number is None:
336 uid_number = pwent[2]
337 if gid_number is None:
338 gid_number = pwent[3]
339 if gecos is None:
340 gecos = pwent[4]
341 if login_shell is None:
342 login_shell = pwent[6]
344 lp = sambaopts.get_loadparm()
345 creds = credopts.get_credentials(lp)
347 if uid_number or gid_number:
348 if not lp.get("idmap_ldb:use rfc2307"):
349 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")
351 if nis_domain is not None:
352 if None in (uid_number, login_shell, unix_home, gid_number):
353 raise CommandError('Missing parameters. To enable NIS features, '
354 'the following options have to be given: '
355 '--nis-domain=, --uidNumber=, --login-shell='
356 ', --unix-home=, --gid-number= Operation '
357 'cancelled.')
359 try:
360 samdb = SamDB(url=H, session_info=system_session(),
361 credentials=creds, lp=lp)
362 samdb.newuser(username, password, force_password_change_at_next_login_req=must_change_at_next_login,
363 useusernameascn=use_username_as_cn, userou=userou, surname=surname, givenname=given_name, initials=initials,
364 profilepath=profile_path, homedrive=home_drive, scriptpath=script_path, homedirectory=home_directory,
365 jobtitle=job_title, department=department, company=company, description=description,
366 mailaddress=mail_address, internetaddress=internet_address,
367 telephonenumber=telephone_number, physicaldeliveryoffice=physical_delivery_office,
368 nisdomain=nis_domain, unixhome=unix_home, uid=uid,
369 uidnumber=uid_number, gidnumber=gid_number,
370 gecos=gecos, loginshell=login_shell,
371 smartcard_required=smartcard_required)
372 except Exception as e:
373 raise CommandError("Failed to add user '%s': " % username, e)
375 self.outf.write("User '%s' added successfully\n" % username)
377 class cmd_user_delete(Command):
378 """Delete a user.
380 This command deletes a user account from the Active Directory domain. The username specified on the command is the sAMAccountName.
382 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.
384 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.
386 Example1:
387 samba-tool user delete User1 -H ldap://samba.samdom.example.com --username=administrator --password=passw1rd
389 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.
391 Example2:
392 sudo samba-tool user delete User2
394 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.
397 synopsis = "%prog <username> [options]"
399 takes_options = [
400 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
401 metavar="URL", dest="H"),
404 takes_args = ["username"]
405 takes_optiongroups = {
406 "sambaopts": options.SambaOptions,
407 "credopts": options.CredentialsOptions,
408 "versionopts": options.VersionOptions,
411 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
412 H=None):
413 lp = sambaopts.get_loadparm()
414 creds = credopts.get_credentials(lp, fallback_machine=True)
416 samdb = SamDB(url=H, session_info=system_session(),
417 credentials=creds, lp=lp)
419 filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
420 ldb.binary_encode(username))
422 try:
423 res = samdb.search(base=samdb.domain_dn(),
424 scope=ldb.SCOPE_SUBTREE,
425 expression=filter,
426 attrs=["dn"])
427 user_dn = res[0].dn
428 except IndexError:
429 raise CommandError('Unable to find user "%s"' % (username))
431 try:
432 samdb.delete(user_dn)
433 except Exception as e:
434 raise CommandError('Failed to remove user "%s"' % username, e)
435 self.outf.write("Deleted user %s\n" % username)
438 class cmd_user_list(Command):
439 """List all users."""
441 synopsis = "%prog [options]"
443 takes_options = [
444 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
445 metavar="URL", dest="H"),
446 Option("--hide-expired",
447 help="Do not list expired user accounts",
448 default=False,
449 action='store_true'),
450 Option("--hide-disabled",
451 default=False,
452 action='store_true',
453 help="Do not list disabled user accounts"),
454 Option("-b", "--base-dn",
455 help="Specify base DN to use",
456 type=str),
457 Option("--full-dn", dest="full_dn",
458 default=False,
459 action='store_true',
460 help="Display DN instead of the sAMAccountName.")
463 takes_optiongroups = {
464 "sambaopts": options.SambaOptions,
465 "credopts": options.CredentialsOptions,
466 "versionopts": options.VersionOptions,
469 def run(self,
470 sambaopts=None,
471 credopts=None,
472 versionopts=None,
473 H=None,
474 hide_expired=False,
475 hide_disabled=False,
476 base_dn=None,
477 full_dn=False):
478 lp = sambaopts.get_loadparm()
479 creds = credopts.get_credentials(lp, fallback_machine=True)
481 samdb = SamDB(url=H, session_info=system_session(),
482 credentials=creds, lp=lp)
484 search_dn = samdb.domain_dn()
485 if base_dn:
486 search_dn = samdb.normalize_dn_in_domain(base_dn)
488 filter_expires = ""
489 if hide_expired is True:
490 current_nttime = samdb.get_nttime()
491 filter_expires = "(|(accountExpires=0)(accountExpires>=%u))" % (
492 current_nttime)
494 filter_disabled = ""
495 if hide_disabled is True:
496 filter_disabled = "(!(userAccountControl:%s:=%u))" % (
497 ldb.OID_COMPARATOR_AND, dsdb.UF_ACCOUNTDISABLE)
499 filter = "(&(objectClass=user)(userAccountControl:%s:=%u)%s%s)" % (
500 ldb.OID_COMPARATOR_AND,
501 dsdb.UF_NORMAL_ACCOUNT,
502 filter_disabled,
503 filter_expires)
505 res = samdb.search(search_dn,
506 scope=ldb.SCOPE_SUBTREE,
507 expression=filter,
508 attrs=["samaccountname"])
509 if (len(res) == 0):
510 return
512 for msg in res:
513 if full_dn:
514 self.outf.write("%s\n" % msg.get("dn"))
515 continue
517 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
520 class cmd_user_enable(Command):
521 """Enable a user.
523 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.
525 There are many reasons why an account may become disabled. These include:
526 - If a user exceeds the account policy for logon attempts
527 - If an administrator disables the account
528 - If the account expires
530 The samba-tool user enable command allows an administrator to enable an account which has become disabled.
532 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.
534 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.
536 Example1:
537 samba-tool user enable Testuser1 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
539 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.
541 Example2:
542 su samba-tool user enable Testuser2
544 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.
546 Example3:
547 samba-tool user enable --filter=samaccountname=Testuser3
549 Example3 shows how to enable a user in the domain against a local LDAP server. It uses the --filter=samaccountname to specify the username.
552 synopsis = "%prog (<username>|--filter <filter>) [options]"
554 takes_optiongroups = {
555 "sambaopts": options.SambaOptions,
556 "versionopts": options.VersionOptions,
557 "credopts": options.CredentialsOptions,
560 takes_options = [
561 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
562 metavar="URL", dest="H"),
563 Option("--filter", help="LDAP Filter to set password on", type=str),
566 takes_args = ["username?"]
568 def run(self, username=None, sambaopts=None, credopts=None,
569 versionopts=None, filter=None, H=None):
570 if username is None and filter is None:
571 raise CommandError("Either the username or '--filter' must be specified!")
573 if filter is None:
574 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
576 lp = sambaopts.get_loadparm()
577 creds = credopts.get_credentials(lp, fallback_machine=True)
579 samdb = SamDB(url=H, session_info=system_session(),
580 credentials=creds, lp=lp)
581 try:
582 samdb.enable_account(filter)
583 except Exception as msg:
584 raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
585 self.outf.write("Enabled user '%s'\n" % (username or filter))
588 class cmd_user_disable(Command):
589 """Disable a user."""
591 synopsis = "%prog (<username>|--filter <filter>) [options]"
593 takes_options = [
594 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
595 metavar="URL", dest="H"),
596 Option("--filter", help="LDAP Filter to set password on", type=str),
599 takes_args = ["username?"]
601 takes_optiongroups = {
602 "sambaopts": options.SambaOptions,
603 "credopts": options.CredentialsOptions,
604 "versionopts": options.VersionOptions,
607 def run(self, username=None, sambaopts=None, credopts=None,
608 versionopts=None, filter=None, H=None):
609 if username is None and filter is None:
610 raise CommandError("Either the username or '--filter' must be specified!")
612 if filter is None:
613 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
615 lp = sambaopts.get_loadparm()
616 creds = credopts.get_credentials(lp, fallback_machine=True)
618 samdb = SamDB(url=H, session_info=system_session(),
619 credentials=creds, lp=lp)
620 try:
621 samdb.disable_account(filter)
622 except Exception as msg:
623 raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
626 class cmd_user_setexpiry(Command):
627 """Set the expiration of a user account.
629 The user can either be specified by their sAMAccountName or using the --filter option.
631 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.
633 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.
635 Example1:
636 samba-tool user setexpiry User1 --days=20 --URL=ldap://samba.samdom.example.com --username=administrator --password=passw1rd
638 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.
640 Example2:
641 sudo samba-tool user setexpiry User2 --noexpiry
643 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.
645 Example3:
646 samba-tool user setexpiry --days=20 --filter=samaccountname=User3
648 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.
650 Example4:
651 samba-tool user setexpiry --noexpiry User4
652 Example4 shows how to set the account expiration so that it will never expire. The username and sAMAccountName in this example is User4.
655 synopsis = "%prog (<username>|--filter <filter>) [options]"
657 takes_optiongroups = {
658 "sambaopts": options.SambaOptions,
659 "versionopts": options.VersionOptions,
660 "credopts": options.CredentialsOptions,
663 takes_options = [
664 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
665 metavar="URL", dest="H"),
666 Option("--filter", help="LDAP Filter to set password on", type=str),
667 Option("--days", help="Days to expiry", type=int, default=0),
668 Option("--noexpiry", help="Password does never expire", action="store_true", default=False),
671 takes_args = ["username?"]
673 def run(self, username=None, sambaopts=None, credopts=None,
674 versionopts=None, H=None, filter=None, days=None, noexpiry=None):
675 if username is None and filter is None:
676 raise CommandError("Either the username or '--filter' must be specified!")
678 if filter is None:
679 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
681 lp = sambaopts.get_loadparm()
682 creds = credopts.get_credentials(lp)
684 samdb = SamDB(url=H, session_info=system_session(),
685 credentials=creds, lp=lp)
687 try:
688 samdb.setexpiry(filter, days * 24 * 3600, no_expiry_req=noexpiry)
689 except Exception as msg:
690 # FIXME: Catch more specific exception
691 raise CommandError("Failed to set expiry for user '%s': %s" % (
692 username or filter, msg))
693 if noexpiry:
694 self.outf.write("Expiry for user '%s' disabled.\n" % (
695 username or filter))
696 else:
697 self.outf.write("Expiry for user '%s' set to %u days.\n" % (
698 username or filter, days))
701 class cmd_user_password(Command):
702 """Change password for a user account (the one provided in authentication).
705 synopsis = "%prog [options]"
707 takes_options = [
708 Option("--newpassword", help="New password", type=str),
711 takes_optiongroups = {
712 "sambaopts": options.SambaOptions,
713 "credopts": options.CredentialsOptions,
714 "versionopts": options.VersionOptions,
717 def run(self, credopts=None, sambaopts=None, versionopts=None,
718 newpassword=None):
720 lp = sambaopts.get_loadparm()
721 creds = credopts.get_credentials(lp)
723 # get old password now, to get the password prompts in the right order
724 old_password = creds.get_password()
726 net = Net(creds, lp, server=credopts.ipaddress)
728 password = newpassword
729 while True:
730 if password is not None and password != '':
731 break
732 password = getpass("New Password: ")
733 passwordverify = getpass("Retype Password: ")
734 if not password == passwordverify:
735 password = None
736 self.outf.write("Sorry, passwords do not match.\n")
738 try:
739 if not isinstance(password, str):
740 password = password.decode('utf8')
741 net.change_password(password)
742 except Exception as msg:
743 # FIXME: catch more specific exception
744 raise CommandError("Failed to change password : %s" % msg)
745 self.outf.write("Changed password OK\n")
748 class cmd_user_getgroups(Command):
749 """Get the direct group memberships of a user account.
751 The username specified on the command is the sAMAccountName."""
752 synopsis = "%prog <username> [options]"
754 takes_optiongroups = {
755 "sambaopts": options.SambaOptions,
756 "versionopts": options.VersionOptions,
757 "credopts": options.CredentialsOptions,
760 takes_options = [
761 Option("-H", "--URL", help="LDB URL for database or target server",
762 type=str, metavar="URL", dest="H"),
763 Option("--full-dn", dest="full_dn",
764 default=False,
765 action='store_true',
766 help="Display DN instead of the sAMAccountName."),
769 takes_args = ["username"]
771 def run(self,
772 username,
773 credopts=None,
774 sambaopts=None,
775 versionopts=None,
776 H=None,
777 full_dn=False):
779 lp = sambaopts.get_loadparm()
780 creds = credopts.get_credentials(lp)
782 samdb = SamDB(url=H, session_info=system_session(),
783 credentials=creds, lp=lp)
785 filter = ("(&(sAMAccountName=%s)(objectClass=user))" %
786 ldb.binary_encode(username))
787 try:
788 res = samdb.search(base=samdb.domain_dn(),
789 expression=filter,
790 scope=ldb.SCOPE_SUBTREE,
791 attrs=["objectSid",
792 "memberOf",
793 "primaryGroupID"])
794 user_sid_binary = res[0].get('objectSid', idx=0)
795 user_sid = ndr_unpack(security.dom_sid, user_sid_binary)
796 (user_dom_sid, user_rid) = user_sid.split()
797 user_sid_dn = "<SID=%s>" % user_sid
798 user_pgid = int(res[0].get('primaryGroupID', idx=0))
799 user_groups = res[0].get('memberOf')
800 if user_groups is None:
801 user_groups = []
802 except IndexError:
803 raise CommandError("Unable to find user '%s'" % (username))
805 primarygroup_sid_dn = "<SID=%s-%u>" % (user_dom_sid, user_pgid)
807 filter = "(objectClass=group)"
808 try:
809 res = samdb.search(base=primarygroup_sid_dn,
810 expression=filter,
811 scope=ldb.SCOPE_BASE,
812 attrs=['sAMAccountName'])
813 primary_group_dn = str(res[0].dn)
814 primary_group_name = res[0].get('sAMAccountName')
815 except IndexError:
816 raise CommandError("Unable to find primary group '%s'" % (primarygroup_sid_dn))
818 if full_dn:
819 self.outf.write("%s\n" % primary_group_dn)
820 for group_dn in user_groups:
821 self.outf.write("%s\n" % group_dn)
822 return
824 group_names = []
825 for gdn in user_groups:
826 try:
827 res = samdb.search(base=gdn,
828 expression=filter,
829 scope=ldb.SCOPE_BASE,
830 attrs=['sAMAccountName'])
831 group_names.extend(res[0].get('sAMAccountName'))
832 except IndexError:
833 raise CommandError("Unable to find group '%s'" % (gdn))
835 self.outf.write("%s\n" % primary_group_name)
836 for group_name in group_names:
837 self.outf.write("%s\n" % group_name)
840 class cmd_user_setprimarygroup(Command):
841 """Set the primary group a user account.
843 This command sets the primary group a user account. The username specified on
844 the command is the sAMAccountName. The primarygroupname is the sAMAccountName
845 of the new primary group. The user must be a member of the group.
847 The command may be run from the root userid or another authorized userid. The
848 -H or --URL= option can be used to execute the command against a remote server.
850 Example1:
851 samba-tool user setprimarygroup TestUser1 newPrimaryGroup --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
853 Example1 shows how to set the primary group for TestUser1 on a remote LDAP
854 server. The --URL parameter is used to specify the remote target server. The
855 -U option is used to pass the username and password of a user that exists on
856 the remote server and is authorized to update the server.
858 synopsis = "%prog <username> <primarygroupname> [options]"
860 takes_optiongroups = {
861 "sambaopts": options.SambaOptions,
862 "versionopts": options.VersionOptions,
863 "credopts": options.CredentialsOptions,
866 takes_options = [
867 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
868 metavar="URL", dest="H"),
871 takes_args = ["username", "primarygroupname"]
873 def run(self, username, primarygroupname, credopts=None, sambaopts=None,
874 versionopts=None, H=None):
876 lp = sambaopts.get_loadparm()
877 creds = credopts.get_credentials(lp)
879 samdb = SamDB(url=H, session_info=system_session(),
880 credentials=creds, lp=lp)
882 filter = ("(&(sAMAccountName=%s)(objectClass=user))" %
883 ldb.binary_encode(username))
884 try:
885 res = samdb.search(base=samdb.domain_dn(),
886 expression=filter,
887 scope=ldb.SCOPE_SUBTREE,
888 controls=["extended_dn:1:1"],
889 attrs=["objectSid",
890 "memberOf",
891 "primaryGroupID"])
892 user_sid_binary = res[0].get('objectSid', idx=0)
893 user_sid = ndr_unpack(security.dom_sid, user_sid_binary)
894 (user_dom_sid, user_rid) = user_sid.split()
895 user_sid_dn = "<SID=%s>" % user_sid
896 user_pgid = int(res[0].get('primaryGroupID', idx=0))
897 user_groups = res[0].get('memberOf')
898 if user_groups is None:
899 user_groups = []
900 except IndexError:
901 raise CommandError("Unable to find user '%s'" % (username))
903 user_group_sids = []
904 for user_group in user_groups:
905 user_group_dn = ldb.Dn(samdb, str(user_group))
906 user_group_binary_sid = user_group_dn.get_extended_component("SID")
907 user_group_sid = ndr_unpack(security.dom_sid, user_group_binary_sid)
908 user_group_sids.append(user_group_sid)
910 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
911 ldb.binary_encode(primarygroupname))
912 try:
913 res = samdb.search(base=samdb.domain_dn(),
914 expression=filter,
915 scope=ldb.SCOPE_SUBTREE,
916 attrs=["objectSid"])
917 group_sid_binary = res[0].get('objectSid', idx=0)
918 except IndexError:
919 raise CommandError("Unable to find group '%s'" % (primarygroupname))
921 primarygroup_sid = ndr_unpack(security.dom_sid, group_sid_binary)
922 (primarygroup_dom_sid, primarygroup_rid) = primarygroup_sid.split()
924 if user_dom_sid != primarygroup_dom_sid:
925 raise CommandError("Group '%s' does not belong to the user's "
926 "domain" % primarygroupname)
928 if primarygroup_rid != user_pgid and primarygroup_sid not in user_group_sids:
929 raise CommandError("User '%s' is not member of group '%s'" %
930 (username, primarygroupname))
932 setprimarygroup_ldif = """
933 dn: %s
934 changetype: modify
935 delete: primaryGroupID
936 primaryGroupID: %u
937 add: primaryGroupID
938 primaryGroupID: %u
939 """ % (user_sid_dn, user_pgid, primarygroup_rid)
941 try:
942 samdb.modify_ldif(setprimarygroup_ldif)
943 except Exception as msg:
944 raise CommandError("Failed to set primary group '%s' "
945 "for user '%s': %s" %
946 (primarygroupname, username, msg))
947 self.outf.write("Changed primary group to '%s'\n" % primarygroupname)
950 class cmd_user_setpassword(Command):
951 """Set or reset the password of a user account.
953 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.
955 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.
957 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.
959 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.
961 Example1:
962 samba-tool user setpassword TestUser1 --newpassword=passw0rd --URL=ldap://samba.samdom.example.com -Uadministrator%passw1rd
964 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.
966 Example2:
967 sudo samba-tool user setpassword TestUser2 --newpassword=passw0rd --must-change-at-next-login
969 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.
971 Example3:
972 samba-tool user setpassword --filter=samaccountname=TestUser3 --newpassword=passw0rd
974 Example3 shows how an administrator would reset TestUser3 user's password to passw0rd using the --filter= option to specify the username.
977 synopsis = "%prog (<username>|--filter <filter>) [options]"
979 takes_optiongroups = {
980 "sambaopts": options.SambaOptions,
981 "versionopts": options.VersionOptions,
982 "credopts": options.CredentialsOptions,
985 takes_options = [
986 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
987 metavar="URL", dest="H"),
988 Option("--filter", help="LDAP Filter to set password on", type=str),
989 Option("--newpassword", help="Set password", type=str),
990 Option("--must-change-at-next-login",
991 help="Force password to be changed on next login",
992 action="store_true"),
993 Option("--random-password",
994 help="Generate random password",
995 action="store_true"),
996 Option("--smartcard-required",
997 help="Require a smartcard for interactive logons",
998 action="store_true"),
999 Option("--clear-smartcard-required",
1000 help="Don't require a smartcard for interactive logons",
1001 action="store_true"),
1004 takes_args = ["username?"]
1006 def run(self, username=None, filter=None, credopts=None, sambaopts=None,
1007 versionopts=None, H=None, newpassword=None,
1008 must_change_at_next_login=False, random_password=False,
1009 smartcard_required=False, clear_smartcard_required=False):
1010 if filter is None and username is None:
1011 raise CommandError("Either the username or '--filter' must be specified!")
1013 password = newpassword
1015 if smartcard_required:
1016 if password is not None and password != '':
1017 raise CommandError('It is not allowed to specify '
1018 '--newpassword '
1019 'together with --smartcard-required.')
1020 if must_change_at_next_login:
1021 raise CommandError('It is not allowed to specify '
1022 '--must-change-at-next-login '
1023 'together with --smartcard-required.')
1024 if clear_smartcard_required:
1025 raise CommandError('It is not allowed to specify '
1026 '--clear-smartcard-required '
1027 'together with --smartcard-required.')
1029 if random_password and not smartcard_required:
1030 password = generate_random_password(128, 255)
1032 while True:
1033 if smartcard_required:
1034 break
1035 if password is not None and password != '':
1036 break
1037 password = getpass("New Password: ")
1038 passwordverify = getpass("Retype Password: ")
1039 if not password == passwordverify:
1040 password = None
1041 self.outf.write("Sorry, passwords do not match.\n")
1043 if filter is None:
1044 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1046 lp = sambaopts.get_loadparm()
1047 creds = credopts.get_credentials(lp)
1049 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
1051 samdb = SamDB(url=H, session_info=system_session(),
1052 credentials=creds, lp=lp)
1054 if smartcard_required:
1055 command = ""
1056 try:
1057 command = "Failed to set UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
1058 flags = dsdb.UF_SMARTCARD_REQUIRED
1059 samdb.toggle_userAccountFlags(filter, flags, on=True)
1060 command = "Failed to enable account for user '%s'" % (username or filter)
1061 samdb.enable_account(filter)
1062 except Exception as msg:
1063 # FIXME: catch more specific exception
1064 raise CommandError("%s: %s" % (command, msg))
1065 self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
1066 else:
1067 command = ""
1068 try:
1069 if clear_smartcard_required:
1070 command = "Failed to remove UF_SMARTCARD_REQUIRED for user '%s'" % (username or filter)
1071 flags = dsdb.UF_SMARTCARD_REQUIRED
1072 samdb.toggle_userAccountFlags(filter, flags, on=False)
1073 command = "Failed to set password for user '%s'" % (username or filter)
1074 samdb.setpassword(filter, password,
1075 force_change_at_next_login=must_change_at_next_login,
1076 username=username)
1077 except Exception as msg:
1078 # FIXME: catch more specific exception
1079 raise CommandError("%s: %s" % (command, msg))
1080 self.outf.write("Changed password OK\n")
1083 class GetPasswordCommand(Command):
1085 def __init__(self):
1086 super(GetPasswordCommand, self).__init__()
1087 self.lp = None
1089 def inject_virtual_attributes(self, samdb):
1090 # We use sort here in order to have a predictable processing order
1091 # this might not be strictly needed, but also doesn't hurt here
1092 for a in sorted(virtual_attributes.keys()):
1093 flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0)
1094 samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING)
1096 def connect_system_samdb(self, url, allow_local=False, verbose=False):
1098 # using anonymous here, results in no authentication
1099 # which means we can get system privileges via
1100 # the privileged ldapi socket
1101 creds = credentials.Credentials()
1102 creds.set_anonymous()
1104 if url is None and allow_local:
1105 pass
1106 elif url.lower().startswith("ldapi://"):
1107 pass
1108 elif url.lower().startswith("ldap://"):
1109 raise CommandError("--url ldap:// is not supported for this command")
1110 elif url.lower().startswith("ldaps://"):
1111 raise CommandError("--url ldaps:// is not supported for this command")
1112 elif not allow_local:
1113 raise CommandError("--url requires an ldapi:// url for this command")
1115 if verbose:
1116 self.outf.write("Connecting to '%s'\n" % url)
1118 samdb = SamDB(url=url, session_info=system_session(),
1119 credentials=creds, lp=self.lp)
1121 try:
1123 # Make sure we're connected as SYSTEM
1125 res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
1126 assert len(res) == 1
1127 sids = res[0].get("tokenGroups")
1128 assert len(sids) == 1
1129 sid = ndr_unpack(security.dom_sid, sids[0])
1130 assert str(sid) == security.SID_NT_SYSTEM
1131 except Exception as msg:
1132 raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" %
1133 (security.SID_NT_SYSTEM))
1135 self.inject_virtual_attributes(samdb)
1137 return samdb
1139 def get_account_attributes(self, samdb, username, basedn, filter, scope,
1140 attrs, decrypt, support_pw_attrs=True):
1142 def get_option(opts, name):
1143 if not opts:
1144 return None
1145 for o in opts:
1146 if o.lower().startswith("%s=" % name.lower()):
1147 (key, _, val) = o.partition('=')
1148 return val
1149 return None
1151 def get_virtual_attr_definition(attr):
1152 for van in sorted(virtual_attributes.keys()):
1153 if van.lower() != attr.lower():
1154 continue
1155 return virtual_attributes[van]
1156 return None
1158 formats = [
1159 "GeneralizedTime",
1160 "UnixTime",
1161 "TimeSpec",
1164 def get_virtual_format_definition(opts):
1165 formatname = get_option(opts, "format")
1166 if formatname is None:
1167 return None
1168 for fm in formats:
1169 if fm.lower() != formatname.lower():
1170 continue
1171 return fm
1172 return None
1174 def parse_raw_attr(raw_attr, is_hidden=False):
1175 (attr, _, fullopts) = raw_attr.partition(';')
1176 if fullopts:
1177 opts = fullopts.split(';')
1178 else:
1179 opts = []
1180 a = {}
1181 a["raw_attr"] = raw_attr
1182 a["attr"] = attr
1183 a["opts"] = opts
1184 a["vattr"] = get_virtual_attr_definition(attr)
1185 a["vformat"] = get_virtual_format_definition(opts)
1186 a["is_hidden"] = is_hidden
1187 return a
1189 raw_attrs = attrs[:]
1190 has_wildcard_attr = "*" in raw_attrs
1191 has_virtual_attrs = False
1192 requested_attrs = []
1193 implicit_attrs = []
1195 for raw_attr in raw_attrs:
1196 a = parse_raw_attr(raw_attr)
1197 requested_attrs.append(a)
1199 search_attrs = []
1200 has_virtual_attrs = False
1201 for a in requested_attrs:
1202 if a["vattr"] is not None:
1203 has_virtual_attrs = True
1204 continue
1205 if a["vformat"] is not None:
1206 # also add it as implicit attr,
1207 # where we just do
1208 # search_attrs.append(a["attr"])
1209 # later on
1210 implicit_attrs.append(a)
1211 continue
1212 if a["raw_attr"] in search_attrs:
1213 continue
1214 search_attrs.append(a["raw_attr"])
1216 if not has_wildcard_attr:
1217 required_attrs = [
1218 "sAMAccountName",
1219 "userPrincipalName"
1221 for required_attr in required_attrs:
1222 a = parse_raw_attr(required_attr)
1223 implicit_attrs.append(a)
1225 if has_virtual_attrs:
1226 if support_pw_attrs:
1227 required_attrs = [
1228 "supplementalCredentials",
1229 "unicodePwd",
1231 for required_attr in required_attrs:
1232 a = parse_raw_attr(required_attr, is_hidden=True)
1233 implicit_attrs.append(a)
1235 for a in implicit_attrs:
1236 if a["attr"] in search_attrs:
1237 continue
1238 search_attrs.append(a["attr"])
1240 if scope == ldb.SCOPE_BASE:
1241 search_controls = ["show_deleted:1", "show_recycled:1"]
1242 else:
1243 search_controls = []
1244 try:
1245 res = samdb.search(base=basedn, expression=filter,
1246 scope=scope, attrs=search_attrs,
1247 controls=search_controls)
1248 if len(res) == 0:
1249 raise Exception('Unable to find user "%s"' % (username or filter))
1250 if len(res) > 1:
1251 raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter))
1252 except Exception as msg:
1253 # FIXME: catch more specific exception
1254 raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
1255 obj = res[0]
1257 sc = None
1258 unicodePwd = None
1259 if "supplementalCredentials" in obj:
1260 sc_blob = obj["supplementalCredentials"][0]
1261 sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
1262 if "unicodePwd" in obj:
1263 unicodePwd = obj["unicodePwd"][0]
1264 account_name = str(obj["sAMAccountName"][0])
1265 if "userPrincipalName" in obj:
1266 account_upn = str(obj["userPrincipalName"][0])
1267 else:
1268 realm = samdb.domain_dns_name()
1269 account_upn = "%s@%s" % (account_name, realm.lower())
1271 calculated = {}
1273 def get_package(name, min_idx=0):
1274 if name in calculated:
1275 return calculated[name]
1276 if sc is None:
1277 return None
1278 if min_idx < 0:
1279 min_idx = len(sc.sub.packages) + min_idx
1280 idx = 0
1281 for p in sc.sub.packages:
1282 idx += 1
1283 if idx <= min_idx:
1284 continue
1285 if name != p.name:
1286 continue
1288 return binascii.a2b_hex(p.data)
1289 return None
1291 def get_kerberos_ctr():
1292 primary_krb5 = get_package("Primary:Kerberos-Newer-Keys")
1293 if primary_krb5 is None:
1294 primary_krb5 = get_package("Primary:Kerberos")
1295 if primary_krb5 is None:
1296 return (0, None)
1297 krb5_blob = ndr_unpack(drsblobs.package_PrimaryKerberosBlob,
1298 primary_krb5)
1299 return (krb5_blob.version, krb5_blob.ctr)
1301 aes256_key = None
1302 kerberos_salt = None
1304 (krb5_v, krb5_ctr) = get_kerberos_ctr()
1305 if krb5_v in [3, 4]:
1306 kerberos_salt = krb5_ctr.salt.string
1308 if krb5_ctr.keys:
1309 def is_aes256(k):
1310 return k.keytype == 18
1311 aes256_key = next(builtins.filter(is_aes256, krb5_ctr.keys),
1312 None)
1314 if decrypt:
1316 # Samba adds 'Primary:SambaGPG' at the end.
1317 # When Windows sets the password it keeps
1318 # 'Primary:SambaGPG' and rotates it to
1319 # the beginning. So we can only use the value,
1320 # if it is the last one.
1322 # In order to get more protection we verify
1323 # the nthash of the decrypted utf16 password
1324 # against the stored nthash in unicodePwd if
1325 # available, otherwise against the first 16
1326 # bytes of the AES256 key.
1328 sgv = get_package("Primary:SambaGPG", min_idx=-1)
1329 if sgv is not None:
1330 try:
1331 cv = gpg_decrypt(sgv)
1333 # We only use the password if it matches
1334 # the current nthash stored in the unicodePwd
1335 # attribute, or the current AES256 key.
1337 tmp = credentials.Credentials()
1338 tmp.set_anonymous()
1339 tmp.set_utf16_password(cv)
1341 decrypted = None
1342 current_hash = None
1344 if unicodePwd is not None:
1345 decrypted = tmp.get_nt_hash()
1346 current_hash = unicodePwd
1347 elif aes256_key is not None and kerberos_salt is not None:
1348 decrypted = tmp.get_aes256_key(kerberos_salt)
1349 current_hash = aes256_key.value
1351 if current_hash is not None and current_hash == decrypted:
1352 calculated["Primary:CLEARTEXT"] = cv
1354 except Exception as e:
1355 self.outf.write(
1356 "WARNING: '%s': SambaGPG can't be decrypted "
1357 "into CLEARTEXT: %s\n" % (
1358 username or account_name, e))
1361 def get_utf8(a, b, username):
1362 try:
1363 u = str(get_bytes(b), 'utf-16-le')
1364 except UnicodeDecodeError as e:
1365 self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
1366 username, a))
1367 return None
1368 u8 = u.encode('utf-8')
1369 return u8
1371 # Extract the WDigest hash for the value specified by i.
1372 # Builds an htdigest compatible value
1373 DIGEST = "Digest"
1375 def get_wDigest(i, primary_wdigest, account_name, account_upn,
1376 domain, dns_domain):
1377 if i == 1:
1378 user = account_name
1379 realm = domain
1380 elif i == 2:
1381 user = account_name.lower()
1382 realm = domain.lower()
1383 elif i == 3:
1384 user = account_name.upper()
1385 realm = domain.upper()
1386 elif i == 4:
1387 user = account_name
1388 realm = domain.upper()
1389 elif i == 5:
1390 user = account_name
1391 realm = domain.lower()
1392 elif i == 6:
1393 user = account_name.upper()
1394 realm = domain.lower()
1395 elif i == 7:
1396 user = account_name.lower()
1397 realm = domain.upper()
1398 elif i == 8:
1399 user = account_name
1400 realm = dns_domain.lower()
1401 elif i == 9:
1402 user = account_name.lower()
1403 realm = dns_domain.lower()
1404 elif i == 10:
1405 user = account_name.upper()
1406 realm = dns_domain.upper()
1407 elif i == 11:
1408 user = account_name
1409 realm = dns_domain.upper()
1410 elif i == 12:
1411 user = account_name
1412 realm = dns_domain.lower()
1413 elif i == 13:
1414 user = account_name.upper()
1415 realm = dns_domain.lower()
1416 elif i == 14:
1417 user = account_name.lower()
1418 realm = dns_domain.upper()
1419 elif i == 15:
1420 user = account_upn
1421 realm = ""
1422 elif i == 16:
1423 user = account_upn.lower()
1424 realm = ""
1425 elif i == 17:
1426 user = account_upn.upper()
1427 realm = ""
1428 elif i == 18:
1429 user = "%s\\%s" % (domain, account_name)
1430 realm = ""
1431 elif i == 19:
1432 user = "%s\\%s" % (domain.lower(), account_name.lower())
1433 realm = ""
1434 elif i == 20:
1435 user = "%s\\%s" % (domain.upper(), account_name.upper())
1436 realm = ""
1437 elif i == 21:
1438 user = account_name
1439 realm = DIGEST
1440 elif i == 22:
1441 user = account_name.lower()
1442 realm = DIGEST
1443 elif i == 23:
1444 user = account_name.upper()
1445 realm = DIGEST
1446 elif i == 24:
1447 user = account_upn
1448 realm = DIGEST
1449 elif i == 25:
1450 user = account_upn.lower()
1451 realm = DIGEST
1452 elif i == 26:
1453 user = account_upn.upper()
1454 realm = DIGEST
1455 elif i == 27:
1456 user = "%s\\%s" % (domain, account_name)
1457 realm = DIGEST
1458 elif i == 28:
1459 # Differs from spec, see tests
1460 user = "%s\\%s" % (domain.lower(), account_name.lower())
1461 realm = DIGEST
1462 elif i == 29:
1463 # Differs from spec, see tests
1464 user = "%s\\%s" % (domain.upper(), account_name.upper())
1465 realm = DIGEST
1466 else:
1467 user = ""
1469 digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
1470 primary_wdigest)
1471 try:
1472 digest = binascii.hexlify(bytearray(digests.hashes[i - 1].hash))
1473 return "%s:%s:%s" % (user, realm, get_string(digest))
1474 except IndexError:
1475 return None
1477 # get the value for a virtualCrypt attribute.
1478 # look for an exact match on algorithm and rounds in supplemental creds
1479 # if not found calculate using Primary:CLEARTEXT
1480 # if no Primary:CLEARTEXT return the first supplementalCredential
1481 # that matches the algorithm.
1482 def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
1483 sv = None
1484 fb = None
1485 b = get_package("Primary:userPassword")
1486 if b is not None:
1487 (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
1488 if sv is None:
1489 # No exact match on algorithm and number of rounds
1490 # try and calculate one from the Primary:CLEARTEXT
1491 b = get_package("Primary:CLEARTEXT")
1492 if b is not None:
1493 u8 = get_utf8(a, b, username or account_name)
1494 if u8 is not None:
1495 # in py2 using get_bytes should ensure u8 is unmodified
1496 # in py3 it will be decoded
1497 sv = get_crypt_value(str(algorithm), get_string(u8), rounds)
1498 if sv is None:
1499 # Unable to calculate a hash with the specified
1500 # number of rounds, fall back to the first hash using
1501 # the specified algorithm
1502 sv = fb
1503 if sv is None:
1504 return None
1505 return "{CRYPT}" + sv
1507 def get_userPassword_hash(blob, algorithm, rounds):
1508 up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
1509 SCHEME = "{CRYPT}"
1511 # Check that the NT hash or AES256 key have not been changed
1512 # without updating the user password hashes. This indicates that
1513 # password has been changed without updating the supplemental
1514 # credentials.
1515 if unicodePwd is not None:
1516 current_hash = unicodePwd
1517 elif aes256_key is not None:
1518 current_hash = aes256_key.value[:16]
1519 else:
1520 return None, None
1522 if current_hash != bytearray(up.current_nt_hash.hash):
1523 return None, None
1525 scheme_prefix = "$%d$" % algorithm
1526 prefix = scheme_prefix
1527 if rounds > 0:
1528 prefix = "$%d$rounds=%d" % (algorithm, rounds)
1529 scheme_match = None
1531 for h in up.hashes:
1532 # in PY2 this should just do nothing and in PY3 if bytes
1533 # it will decode them
1534 h_value = get_string(h.value)
1535 if (scheme_match is None and
1536 h.scheme == SCHEME and
1537 h_value.startswith(scheme_prefix)):
1538 scheme_match = h_value
1539 if h.scheme == SCHEME and h_value.startswith(prefix):
1540 return (h_value, scheme_match)
1542 # No match on the number of rounds, return the value of the
1543 # first matching scheme
1544 return (None, scheme_match)
1546 # Extract the rounds value from the options of a virtualCrypt attribute
1547 # i.e. options = "rounds=20;other=ignored;" will return 20
1548 # if the rounds option is not found or the value is not a number, 0 is returned
1549 # which indicates that the default number of rounds should be used.
1550 def get_rounds(opts):
1551 val = get_option(opts, "rounds")
1552 if val is None:
1553 return 0
1554 try:
1555 return int(val)
1556 except ValueError:
1557 return 0
1559 # We use sort here in order to have a predictable processing order
1560 for a in sorted(virtual_attributes.keys()):
1561 vattr = None
1562 for ra in requested_attrs:
1563 if ra["vattr"] is None:
1564 continue
1565 if ra["attr"].lower() != a.lower():
1566 continue
1567 vattr = ra
1568 break
1569 if vattr is None:
1570 continue
1571 attr_opts = vattr["opts"]
1573 if a == "virtualClearTextUTF8":
1574 b = get_package("Primary:CLEARTEXT")
1575 if b is None:
1576 continue
1577 u8 = get_utf8(a, b, username or account_name)
1578 if u8 is None:
1579 continue
1580 v = u8
1581 elif a == "virtualClearTextUTF16":
1582 v = get_package("Primary:CLEARTEXT")
1583 if v is None:
1584 continue
1585 elif a == "virtualSSHA":
1586 b = get_package("Primary:CLEARTEXT")
1587 if b is None:
1588 continue
1589 u8 = get_utf8(a, b, username or account_name)
1590 if u8 is None:
1591 continue
1592 salt = os.urandom(4)
1593 h = hashlib.sha1()
1594 h.update(u8)
1595 h.update(salt)
1596 bv = h.digest() + salt
1597 v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
1598 elif a == "virtualCryptSHA256":
1599 rounds = get_rounds(attr_opts)
1600 x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
1601 if x is None:
1602 continue
1603 v = x
1604 elif a == "virtualCryptSHA512":
1605 rounds = get_rounds(attr_opts)
1606 x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
1607 if x is None:
1608 continue
1609 v = x
1610 elif a == "virtualSambaGPG":
1611 # Samba adds 'Primary:SambaGPG' at the end.
1612 # When Windows sets the password it keeps
1613 # 'Primary:SambaGPG' and rotates it to
1614 # the beginning. So we can only use the value,
1615 # if it is the last one.
1616 v = get_package("Primary:SambaGPG", min_idx=-1)
1617 if v is None:
1618 continue
1619 elif a == "virtualKerberosSalt":
1620 v = kerberos_salt
1621 if v is None:
1622 continue
1623 elif a.startswith("virtualWDigest"):
1624 primary_wdigest = get_package("Primary:WDigest")
1625 if primary_wdigest is None:
1626 continue
1627 x = a[len("virtualWDigest"):]
1628 try:
1629 i = int(x)
1630 except ValueError:
1631 continue
1632 domain = samdb.domain_netbios_name()
1633 dns_domain = samdb.domain_dns_name()
1634 v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
1635 if v is None:
1636 continue
1637 else:
1638 continue
1639 obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
1641 def get_src_attrname(srcattrg):
1642 srcattrl = srcattrg.lower()
1643 srcattr = None
1644 for k in obj.keys():
1645 if srcattrl != k.lower():
1646 continue
1647 srcattr = k
1648 break
1649 return srcattr
1651 def get_src_time_float(srcattr):
1652 if srcattr not in obj:
1653 return None
1654 vstr = str(obj[srcattr][0])
1655 if vstr.endswith(".0Z"):
1656 vut = ldb.string_to_time(vstr)
1657 vfl = float(vut)
1658 return vfl
1660 try:
1661 vnt = int(vstr)
1662 except ValueError as e:
1663 return None
1664 # 0 or 9223372036854775807 mean no value too
1665 if vnt == 0:
1666 return None
1667 if vnt >= 0x7FFFFFFFFFFFFFFF:
1668 return None
1669 vfl = nttime2float(vnt)
1670 return vfl
1672 def get_generalizedtime(srcattr):
1673 vfl = get_src_time_float(srcattr)
1674 if vfl is None:
1675 return None
1676 vut = int(vfl)
1677 try:
1678 v = "%s" % ldb.timestring(vut)
1679 except OSError as e:
1680 if e.errno == errno.EOVERFLOW:
1681 return None
1682 raise
1683 return v
1685 def get_unixepoch(srcattr):
1686 vfl = get_src_time_float(srcattr)
1687 if vfl is None:
1688 return None
1689 vut = int(vfl)
1690 v = "%d" % vut
1691 return v
1693 def get_timespec(srcattr):
1694 vfl = get_src_time_float(srcattr)
1695 if vfl is None:
1696 return None
1697 v = "%.9f" % vfl
1698 return v
1700 generated_formats = {}
1701 for fm in formats:
1702 for ra in requested_attrs:
1703 if ra["vformat"] is None:
1704 continue
1705 if ra["vformat"] != fm:
1706 continue
1707 srcattr = get_src_attrname(ra["attr"])
1708 if srcattr is None:
1709 continue
1710 an = "%s;format=%s" % (srcattr, fm)
1711 if an in generated_formats:
1712 continue
1713 generated_formats[an] = fm
1715 v = None
1716 if fm == "GeneralizedTime":
1717 v = get_generalizedtime(srcattr)
1718 elif fm == "UnixTime":
1719 v = get_unixepoch(srcattr)
1720 elif fm == "TimeSpec":
1721 v = get_timespec(srcattr)
1722 if v is None:
1723 continue
1724 obj[an] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, an)
1726 # Now filter out implicit attributes
1727 for delname in obj.keys():
1728 keep = False
1729 for ra in requested_attrs:
1730 if delname.lower() != ra["raw_attr"].lower():
1731 continue
1732 keep = True
1733 break
1734 if keep:
1735 continue
1737 dattr = None
1738 for ia in implicit_attrs:
1739 if delname.lower() != ia["attr"].lower():
1740 continue
1741 dattr = ia
1742 break
1743 if dattr is None:
1744 continue
1746 if has_wildcard_attr and not dattr["is_hidden"]:
1747 continue
1748 del obj[delname]
1749 return obj
1751 def parse_attributes(self, attributes):
1753 if attributes is None:
1754 raise CommandError("Please specify --attributes")
1755 attrs = attributes.split(',')
1756 password_attrs = []
1757 for pa in attrs:
1758 pa = pa.lstrip().rstrip()
1759 for da in disabled_virtual_attributes.keys():
1760 if pa.lower() == da.lower():
1761 r = disabled_virtual_attributes[da]["reason"]
1762 raise CommandError("Virtual attribute '%s' not supported: %s" % (
1763 da, r))
1764 for va in virtual_attributes.keys():
1765 if pa.lower() == va.lower():
1766 # Take the real name
1767 pa = va
1768 break
1769 password_attrs += [pa]
1771 return password_attrs
1774 class cmd_user_getpassword(GetPasswordCommand):
1775 """Get the password fields of a user/computer account.
1777 This command gets the logon password for a user/computer account.
1779 The username specified on the command is the sAMAccountName.
1780 The username may also be specified using the --filter option.
1782 The command must be run from the root user id or another authorized user id.
1783 The '-H' or '--URL' option only supports ldapi:// or [tdb://] and can be
1784 used to adjust the local path. By default tdb:// is used by default.
1786 The '--attributes' parameter takes a comma separated list of attributes,
1787 which will be printed or given to the script specified by '--script'. If a
1788 specified attribute is not available on an object it's silently omitted.
1789 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1790 the NTHASH) and the following virtual attributes are possible (see --help
1791 for which virtual attributes are supported in your environment):
1793 virtualClearTextUTF16: The raw cleartext as stored in the
1794 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1795 with '--decrypt-samba-gpg') buffer inside of the
1796 supplementalCredentials attribute. This typically
1797 contains valid UTF-16-LE, but may contain random
1798 bytes, e.g. for computer accounts.
1800 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1801 (only from valid UTF-16-LE).
1803 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1804 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1806 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1807 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1808 with a $5$... salt, see crypt(3) on modern systems.
1809 The number of rounds used to calculate the hash can
1810 also be specified. By appending ";rounds=x" to the
1811 attribute name i.e. virtualCryptSHA256;rounds=10000
1812 will calculate a SHA256 hash with 10,000 rounds.
1813 Non numeric values for rounds are silently ignored.
1814 The value is calculated as follows:
1815 1) If a value exists in 'Primary:userPassword' with
1816 the specified number of rounds it is returned.
1817 2) If 'Primary:CLEARTEXT', or 'Primary:SambaGPG'
1818 with '--decrypt-samba-gpg'. Calculate a hash with
1819 the specified number of rounds.
1820 3) Return the first CryptSHA256 value in
1821 'Primary:userPassword'.
1824 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
1825 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1826 with a $6$... salt, see crypt(3) on modern systems.
1827 The number of rounds used to calculate the hash can
1828 also be specified. By appending ";rounds=x" to the
1829 attribute name i.e. virtualCryptSHA512;rounds=10000
1830 will calculate a SHA512 hash with 10,000 rounds.
1831 Non numeric values for rounds are silently ignored.
1832 The value is calculated as follows:
1833 1) If a value exists in 'Primary:userPassword' with
1834 the specified number of rounds it is returned.
1835 2) If 'Primary:CLEARTEXT', or 'Primary:SambaGPG'
1836 with '--decrypt-samba-gpg'. Calculate a hash with
1837 the specified number of rounds.
1838 3) Return the first CryptSHA512 value in
1839 'Primary:userPassword'.
1841 virtualWDigestNN: The individual hash values stored in
1842 'Primary:WDigest' where NN is the hash number in
1843 the range 01 to 29.
1844 NOTE: As at 22-05-2017 the documentation:
1845 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
1846 https://msdn.microsoft.com/en-us/library/cc245680.aspx
1847 is incorrect.
1849 virtualKerberosSalt: This results the salt string that is used to compute
1850 Kerberos keys from a UTF-8 cleartext password.
1852 virtualSambaGPG: The raw cleartext as stored in the
1853 'Primary:SambaGPG' buffer inside of the
1854 supplementalCredentials attribute.
1855 See the 'password hash gpg key ids' option in
1856 smb.conf.
1858 The '--decrypt-samba-gpg' option triggers decryption of the
1859 Primary:SambaGPG buffer. Check with '--help' if this feature is available
1860 in your environment or not (the python-gpgme package is required). Please
1861 note that you might need to set the GNUPGHOME environment variable. If the
1862 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
1863 environment variable has been set correctly and the passphrase is already
1864 known by the gpg-agent.
1866 Attributes with time values can take an additional format specifier, which
1867 converts the time value into the requested format. The format can be specified
1868 by adding ";format=formatSpecifier" to the requested attribute name, whereby
1869 "formatSpecifier" must be a valid specifier. The syntax looks like:
1871 --attributes=attributeName;format=formatSpecifier
1873 The following format specifiers are available:
1874 - GeneralizedTime (e.g. 20210224113259.0Z)
1875 - UnixTime (e.g. 1614166392)
1876 - TimeSpec (e.g. 161416639.267546892)
1878 Attributes with an original NTTIME value of 0 and 9223372036854775807 are
1879 treated as non-existing value.
1881 Example1:
1882 samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
1884 Example2:
1885 samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
1888 def __init__(self):
1889 super(cmd_user_getpassword, self).__init__()
1891 synopsis = "%prog (<username>|--filter <filter>) [options]"
1893 takes_optiongroups = {
1894 "sambaopts": options.SambaOptions,
1895 "versionopts": options.VersionOptions,
1898 takes_options = [
1899 Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
1900 metavar="URL", dest="H"),
1901 Option("--filter", help="LDAP Filter to set password on", type=str),
1902 Option("--attributes", type=str,
1903 help=virtual_attributes_help,
1904 metavar="ATTRIBUTELIST", dest="attributes"),
1905 Option("--decrypt-samba-gpg",
1906 help=decrypt_samba_gpg_help,
1907 action="store_true", default=False, dest="decrypt_samba_gpg"),
1910 takes_args = ["username?"]
1912 def run(self, username=None, H=None, filter=None,
1913 attributes=None, decrypt_samba_gpg=None,
1914 sambaopts=None, versionopts=None):
1915 self.lp = sambaopts.get_loadparm()
1917 if decrypt_samba_gpg and not gpg_decrypt:
1918 raise CommandError(decrypt_samba_gpg_help)
1920 if filter is None and username is None:
1921 raise CommandError("Either the username or '--filter' must be specified!")
1923 if filter is None:
1924 filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username))
1926 if attributes is None:
1927 raise CommandError("Please specify --attributes")
1929 password_attrs = self.parse_attributes(attributes)
1931 samdb = self.connect_system_samdb(url=H, allow_local=True)
1933 obj = self.get_account_attributes(samdb, username,
1934 basedn=None,
1935 filter=filter,
1936 scope=ldb.SCOPE_SUBTREE,
1937 attrs=password_attrs,
1938 decrypt=decrypt_samba_gpg)
1940 ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
1941 self.outf.write("%s" % ldif)
1942 self.outf.write("Got password OK\n")
1945 class cmd_user_syncpasswords(GetPasswordCommand):
1946 """Sync the password of user accounts.
1948 This syncs logon passwords for user accounts.
1950 Note that this command should run on a single domain controller only
1951 (typically the PDC-emulator). However the "password hash gpg key ids"
1952 option should to be configured on all domain controllers.
1954 The command must be run from the root user id or another authorized user id.
1955 The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the
1956 local path. By default, ldapi:// is used with the default path to the
1957 privileged ldapi socket.
1959 This command has three modes: "Cache Initialization", "Sync Loop Run" and
1960 "Sync Loop Terminate".
1963 Cache Initialization
1964 ====================
1966 The first time, this command needs to be called with
1967 '--cache-ldb-initialize' in order to initialize its cache.
1969 The cache initialization requires '--attributes' and allows the following
1970 optional options: '--decrypt-samba-gpg', '--script', '--filter' or
1971 '-H/--URL'.
1973 The '--attributes' parameter takes a comma separated list of attributes,
1974 which will be printed or given to the script specified by '--script'. If a
1975 specified attribute is not available on an object it will be silently omitted.
1976 All attributes defined in the schema (e.g. the unicodePwd attribute holds
1977 the NTHASH) and the following virtual attributes are possible (see '--help'
1978 for supported virtual attributes in your environment):
1980 virtualClearTextUTF16: The raw cleartext as stored in the
1981 'Primary:CLEARTEXT' (or 'Primary:SambaGPG'
1982 with '--decrypt-samba-gpg') buffer inside of the
1983 supplementalCredentials attribute. This typically
1984 contains valid UTF-16-LE, but may contain random
1985 bytes, e.g. for computer accounts.
1987 virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8
1988 (only from valid UTF-16-LE).
1990 virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1
1991 checksum, useful for OpenLDAP's '{SSHA}' algorithm.
1993 virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
1994 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
1995 with a $5$... salt, see crypt(3) on modern systems.
1996 The number of rounds used to calculate the hash can
1997 also be specified. By appending ";rounds=x" to the
1998 attribute name i.e. virtualCryptSHA256;rounds=10000
1999 will calculate a SHA256 hash with 10,000 rounds.
2000 Non numeric values for rounds are silently ignored.
2001 The value is calculated as follows:
2002 1) If a value exists in 'Primary:userPassword' with
2003 the specified number of rounds it is returned.
2004 2) If 'Primary:CLEARTEXT', or 'Primary:SambaGPG' with
2005 '--decrypt-samba-gpg'. Calculate a hash with
2006 the specified number of rounds
2007 3) Return the first CryptSHA256 value in
2008 'Primary:userPassword'.
2010 virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
2011 checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
2012 with a $6$... salt, see crypt(3) on modern systems.
2013 The number of rounds used to calculate the hash can
2014 also be specified. By appending ";rounds=x" to the
2015 attribute name i.e. virtualCryptSHA512;rounds=10000
2016 will calculate a SHA512 hash with 10,000 rounds.
2017 Non numeric values for rounds are silently ignored.
2018 The value is calculated as follows:
2019 1) If a value exists in 'Primary:userPassword' with
2020 the specified number of rounds it is returned.
2021 2) If 'Primary:CLEARTEXT', or 'Primary:SambaGPG' with
2022 '--decrypt-samba-gpg'. Calculate a hash with
2023 the specified number of rounds.
2024 3) Return the first CryptSHA512 value in
2025 'Primary:userPassword'.
2027 virtualWDigestNN: The individual hash values stored in
2028 'Primary:WDigest' where NN is the hash number in
2029 the range 01 to 29.
2030 NOTE: As at 22-05-2017 the documentation:
2031 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
2032 https://msdn.microsoft.com/en-us/library/cc245680.aspx
2033 is incorrect.
2035 virtualKerberosSalt: This results the salt string that is used to compute
2036 Kerberos keys from a UTF-8 cleartext password.
2038 virtualSambaGPG: The raw cleartext as stored in the
2039 'Primary:SambaGPG' buffer inside of the
2040 supplementalCredentials attribute.
2041 See the 'password hash gpg key ids' option in
2042 smb.conf.
2044 The '--decrypt-samba-gpg' option triggers decryption of the
2045 Primary:SambaGPG buffer. Check with '--help' if this feature is available
2046 in your environment or not (the python-gpgme package is required). Please
2047 note that you might need to set the GNUPGHOME environment variable. If the
2048 decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO
2049 environment variable has been set correctly and the passphrase is already
2050 known by the gpg-agent.
2052 The '--script' option specifies a custom script that is called whenever any
2053 of the dirsyncAttributes (see below) was changed. The script is called
2054 without any arguments. It gets the LDIF for exactly one object on STDIN.
2055 If the script processed the object successfully it has to respond with a
2056 single line starting with 'DONE-EXIT: ' followed by an optional message.
2058 Note that the script might be called without any password change, e.g. if
2059 the account was disabled (a userAccountControl change) or the
2060 sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
2061 are always returned as unique identifier of the account. It might be useful
2062 to also ask for non-password attributes like: objectSid, sAMAccountName,
2063 userPrincipalName, userAccountControl, pwdLastSet and msDS-KeyVersionNumber.
2064 Depending on the object, some attributes may not be present/available,
2065 but you always get the current state (and not a diff).
2067 If no '--script' option is specified, the LDIF will be printed on STDOUT or
2068 into the logfile.
2070 The default filter for the LDAP_SERVER_DIRSYNC_OID search is:
2071 (&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\
2072 (!(sAMAccountName=krbtgt*)))
2073 This means only normal (non-krbtgt) user
2074 accounts are monitored. The '--filter' can modify that, e.g. if it's
2075 required to also sync computer accounts.
2078 Sync Loop Run
2079 =============
2081 This (default) mode runs in an endless loop waiting for password related
2082 changes in the active directory database. It makes use of the
2083 LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order
2084 get changes in a reliable fashion. Objects are monitored for changes of the
2085 following dirsyncAttributes:
2087 unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName,
2088 userPrincipalName and userAccountControl.
2090 It recovers from LDAP disconnects and updates the cache in conservative way
2091 (in single steps after each successfully processed change). An error from
2092 the script (specified by '--script') will result in fatal error and this
2093 command will exit. But the cache state should be still valid and can be
2094 resumed in the next "Sync Loop Run".
2096 The '--logfile' option specifies an optional (required if '--daemon' is
2097 specified) logfile that takes all output of the command. The logfile is
2098 automatically reopened if fstat returns st_nlink == 0.
2100 The optional '--daemon' option will put the command into the background.
2102 You can stop the command without the '--daemon' option, also by hitting
2103 strg+c.
2105 If you specify the '--no-wait' option the command skips the
2106 LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once
2107 all LDAP_SERVER_DIRSYNC_OID changes are consumed.
2109 Sync Loop Terminate
2110 ===================
2112 In order to terminate an already running command (likely as daemon) the
2113 '--terminate' option can be used. This also requires the '--logfile' option
2114 to be specified.
2117 Example1:
2118 samba-tool user syncpasswords --cache-ldb-initialize \\
2119 --attributes=virtualClearTextUTF8
2120 samba-tool user syncpasswords
2122 Example2:
2123 samba-tool user syncpasswords --cache-ldb-initialize \\
2124 --attributes=objectGUID,objectSID,sAMAccountName,\\
2125 userPrincipalName,userAccountControl,pwdLastSet,\\
2126 msDS-KeyVersionNumber,virtualCryptSHA512 \\
2127 --script=/path/to/my-custom-syncpasswords-script.py
2128 samba-tool user syncpasswords --daemon \\
2129 --logfile=/var/log/samba/user-syncpasswords.log
2130 samba-tool user syncpasswords --terminate \\
2131 --logfile=/var/log/samba/user-syncpasswords.log
2134 def __init__(self):
2135 super(cmd_user_syncpasswords, self).__init__()
2137 synopsis = "%prog [--cache-ldb-initialize] [options]"
2139 takes_optiongroups = {
2140 "sambaopts": options.SambaOptions,
2141 "versionopts": options.VersionOptions,
2144 takes_options = [
2145 Option("--cache-ldb-initialize",
2146 help="Initialize the cache for the first time",
2147 dest="cache_ldb_initialize", action="store_true"),
2148 Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str,
2149 metavar="CACHE-LDB-PATH", dest="cache_ldb"),
2150 Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str,
2151 metavar="URL", dest="H"),
2152 Option("--filter", help="optional LDAP filter to set password on", type=str,
2153 metavar="LDAP-SEARCH-FILTER", dest="filter"),
2154 Option("--attributes", type=str,
2155 help=virtual_attributes_help,
2156 metavar="ATTRIBUTELIST", dest="attributes"),
2157 Option("--decrypt-samba-gpg",
2158 help=decrypt_samba_gpg_help,
2159 action="store_true", default=False, dest="decrypt_samba_gpg"),
2160 Option("--script", help="Script that is called for each password change", type=str,
2161 metavar="/path/to/syncpasswords.script", dest="script"),
2162 Option("--no-wait", help="Don't block waiting for changes",
2163 action="store_true", default=False, dest="nowait"),
2164 Option("--logfile", type=str,
2165 help="The logfile to use (required in --daemon mode).",
2166 metavar="/path/to/syncpasswords.log", dest="logfile"),
2167 Option("--daemon", help="daemonize after initial setup",
2168 action="store_true", default=False, dest="daemon"),
2169 Option("--terminate",
2170 help="Send a SIGTERM to an already running (daemon) process",
2171 action="store_true", default=False, dest="terminate"),
2174 def run(self, cache_ldb_initialize=False, cache_ldb=None,
2175 H=None, filter=None,
2176 attributes=None, decrypt_samba_gpg=None,
2177 script=None, nowait=None, logfile=None, daemon=None, terminate=None,
2178 sambaopts=None, versionopts=None):
2180 self.lp = sambaopts.get_loadparm()
2181 self.logfile = None
2182 self.samdb_url = None
2183 self.samdb = None
2184 self.cache = None
2186 if not cache_ldb_initialize:
2187 if attributes is not None:
2188 raise CommandError("--attributes is only allowed together with --cache-ldb-initialize")
2189 if decrypt_samba_gpg:
2190 raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize")
2191 if script is not None:
2192 raise CommandError("--script is only allowed together with --cache-ldb-initialize")
2193 if filter is not None:
2194 raise CommandError("--filter is only allowed together with --cache-ldb-initialize")
2195 if H is not None:
2196 raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize")
2197 else:
2198 if nowait is not False:
2199 raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize")
2200 if logfile is not None:
2201 raise CommandError("--logfile is not allowed together with --cache-ldb-initialize")
2202 if daemon is not False:
2203 raise CommandError("--daemon is not allowed together with --cache-ldb-initialize")
2204 if terminate is not False:
2205 raise CommandError("--terminate is not allowed together with --cache-ldb-initialize")
2207 if nowait is True:
2208 if daemon is True:
2209 raise CommandError("--daemon is not allowed together with --no-wait")
2210 if terminate is not False:
2211 raise CommandError("--terminate is not allowed together with --no-wait")
2213 if terminate is True and daemon is True:
2214 raise CommandError("--terminate is not allowed together with --daemon")
2216 if daemon is True and logfile is None:
2217 raise CommandError("--daemon is only allowed together with --logfile")
2219 if terminate is True and logfile is None:
2220 raise CommandError("--terminate is only allowed together with --logfile")
2222 if script is not None:
2223 if not os.path.exists(script):
2224 raise CommandError("script[%s] does not exist!" % script)
2226 sync_command = "%s" % os.path.abspath(script)
2227 else:
2228 sync_command = None
2230 dirsync_filter = filter
2231 if dirsync_filter is None:
2232 dirsync_filter = "(&" + \
2233 "(objectClass=user)" + \
2234 "(userAccountControl:%s:=%u)" % (
2235 ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
2236 "(!(sAMAccountName=krbtgt*))" + \
2239 dirsync_secret_attrs = [
2240 "unicodePwd",
2241 "dBCSPwd",
2242 "supplementalCredentials",
2245 dirsync_attrs = dirsync_secret_attrs + [
2246 "pwdLastSet",
2247 "sAMAccountName",
2248 "userPrincipalName",
2249 "userAccountControl",
2250 "isDeleted",
2251 "isRecycled",
2254 password_attrs = None
2256 if cache_ldb_initialize:
2257 if H is None:
2258 H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
2260 if decrypt_samba_gpg and not gpg_decrypt:
2261 raise CommandError(decrypt_samba_gpg_help)
2263 password_attrs = self.parse_attributes(attributes)
2264 lower_attrs = [x.lower() for x in password_attrs]
2265 # We always return these in order to track deletions
2266 for a in ["objectGUID", "isDeleted", "isRecycled"]:
2267 if a.lower() not in lower_attrs:
2268 password_attrs += [a]
2270 if cache_ldb is not None:
2271 if cache_ldb.lower().startswith("ldapi://"):
2272 raise CommandError("--cache_ldb ldapi:// is not supported")
2273 elif cache_ldb.lower().startswith("ldap://"):
2274 raise CommandError("--cache_ldb ldap:// is not supported")
2275 elif cache_ldb.lower().startswith("ldaps://"):
2276 raise CommandError("--cache_ldb ldaps:// is not supported")
2277 elif cache_ldb.lower().startswith("tdb://"):
2278 pass
2279 else:
2280 if not os.path.exists(cache_ldb):
2281 cache_ldb = self.lp.private_path(cache_ldb)
2282 else:
2283 cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb")
2285 self.lockfile = "%s.pid" % cache_ldb
2287 def log_msg(msg):
2288 if self.logfile is not None:
2289 info = os.fstat(0)
2290 if info.st_nlink == 0:
2291 logfile = self.logfile
2292 self.logfile = None
2293 log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
2294 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2295 os.dup2(logfd, 0)
2296 os.dup2(logfd, 1)
2297 os.dup2(logfd, 2)
2298 os.close(logfd)
2299 log_msg("Reopened logfile[%s]\n" % (logfile))
2300 self.logfile = logfile
2301 msg = "%s: pid[%d]: %s" % (
2302 time.ctime(),
2303 os.getpid(),
2304 msg)
2305 self.outf.write(msg)
2306 return
2308 def load_cache():
2309 cache_attrs = [
2310 "samdbUrl",
2311 "dirsyncFilter",
2312 "dirsyncAttribute",
2313 "dirsyncControl",
2314 "passwordAttribute",
2315 "decryptSambaGPG",
2316 "syncCommand",
2317 "currentPid",
2320 self.cache = Ldb(cache_ldb)
2321 self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS")
2322 res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE,
2323 attrs=cache_attrs)
2324 if len(res) == 1:
2325 try:
2326 self.samdb_url = str(res[0]["samdbUrl"][0])
2327 except KeyError as e:
2328 self.samdb_url = None
2329 else:
2330 self.samdb_url = None
2331 if self.samdb_url is None and not cache_ldb_initialize:
2332 raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % (
2333 cache_ldb))
2334 if self.samdb_url is not None and cache_ldb_initialize:
2335 raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % (
2336 cache_ldb))
2337 if self.samdb_url is None:
2338 self.samdb_url = H
2339 self.dirsync_filter = dirsync_filter
2340 self.dirsync_attrs = dirsync_attrs
2341 self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"]
2342 self.password_attrs = password_attrs
2343 self.decrypt_samba_gpg = decrypt_samba_gpg
2344 self.sync_command = sync_command
2345 add_ldif = "dn: %s\n" % self.cache_dn +\
2346 "objectClass: userSyncPasswords\n" +\
2347 "samdbUrl:: %s\n" % base64.b64encode(get_bytes(self.samdb_url)).decode('utf8') +\
2348 "dirsyncFilter:: %s\n" % base64.b64encode(get_bytes(self.dirsync_filter)).decode('utf8') +\
2349 "".join("dirsyncAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.dirsync_attrs) +\
2350 "dirsyncControl: %s\n" % self.dirsync_controls[0] +\
2351 "".join("passwordAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.password_attrs)
2352 if self.decrypt_samba_gpg:
2353 add_ldif += "decryptSambaGPG: TRUE\n"
2354 else:
2355 add_ldif += "decryptSambaGPG: FALSE\n"
2356 if self.sync_command is not None:
2357 add_ldif += "syncCommand: %s\n" % self.sync_command
2358 add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
2359 self.cache.add_ldif(add_ldif)
2360 self.current_pid = None
2361 self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
2362 msgs = self.cache.parse_ldif(add_ldif)
2363 changetype, msg = next(msgs)
2364 ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
2365 self.outf.write("%s" % ldif)
2366 else:
2367 self.dirsync_filter = str(res[0]["dirsyncFilter"][0])
2368 self.dirsync_attrs = []
2369 for a in res[0]["dirsyncAttribute"]:
2370 self.dirsync_attrs.append(str(a))
2371 self.dirsync_controls = [str(res[0]["dirsyncControl"][0]), "extended_dn:1:0"]
2372 self.password_attrs = []
2373 for a in res[0]["passwordAttribute"]:
2374 self.password_attrs.append(str(a))
2375 decrypt_string = str(res[0]["decryptSambaGPG"][0])
2376 assert(decrypt_string in ["TRUE", "FALSE"])
2377 if decrypt_string == "TRUE":
2378 self.decrypt_samba_gpg = True
2379 else:
2380 self.decrypt_samba_gpg = False
2381 if "syncCommand" in res[0]:
2382 self.sync_command = str(res[0]["syncCommand"][0])
2383 else:
2384 self.sync_command = None
2385 if "currentPid" in res[0]:
2386 self.current_pid = int(res[0]["currentPid"][0])
2387 else:
2388 self.current_pid = None
2389 log_msg("Using cache_ldb[%s]\n" % (cache_ldb))
2391 return
2393 def run_sync_command(dn, ldif):
2394 log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
2395 sync_command_p = Popen(self.sync_command,
2396 stdin=PIPE,
2397 stdout=PIPE,
2398 stderr=STDOUT)
2400 res = sync_command_p.poll()
2401 assert res is None
2403 input = "%s" % (ldif)
2404 reply = sync_command_p.communicate(
2405 input.encode('utf-8'))[0].decode('utf-8')
2406 log_msg("%s\n" % (reply))
2407 res = sync_command_p.poll()
2408 if res is None:
2409 sync_command_p.terminate()
2410 res = sync_command_p.wait()
2412 if reply.startswith("DONE-EXIT: "):
2413 return
2415 log_msg("RESULT: %s\n" % (res))
2416 raise Exception("ERROR: %s - %s\n" % (res, reply))
2418 def handle_object(idx, dirsync_obj):
2419 binary_guid = dirsync_obj.dn.get_extended_component("GUID")
2420 guid = ndr_unpack(misc.GUID, binary_guid)
2421 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2422 sid = ndr_unpack(security.dom_sid, binary_sid)
2423 domain_sid, rid = sid.split()
2424 if rid == security.DOMAIN_RID_KRBTGT:
2425 log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx))
2426 return
2427 for a in list(dirsync_obj.keys()):
2428 for h in dirsync_secret_attrs:
2429 if a.lower() == h.lower():
2430 del dirsync_obj[a]
2431 dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
2432 dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
2433 log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
2434 obj = self.get_account_attributes(self.samdb,
2435 username="%s" % sid,
2436 basedn="<GUID=%s>" % guid,
2437 filter="(objectClass=user)",
2438 scope=ldb.SCOPE_BASE,
2439 attrs=self.password_attrs,
2440 decrypt=self.decrypt_samba_gpg)
2441 ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
2442 log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid))
2443 if self.sync_command is None:
2444 self.outf.write("%s" % (ldif))
2445 return
2446 self.outf.write("# attrs=%s\n" % (sorted(obj.keys())))
2447 run_sync_command(obj.dn, ldif)
2449 def check_current_pid_conflict(terminate):
2450 flags = os.O_RDWR
2451 if not terminate:
2452 flags |= os.O_CREAT
2454 try:
2455 self.lockfd = os.open(self.lockfile, flags, 0o600)
2456 except IOError as e4:
2457 (err, msg) = e4.args
2458 if err == errno.ENOENT:
2459 if terminate:
2460 return False
2461 log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" %
2462 (self.lockfile, msg, err))
2463 raise
2465 got_exclusive = False
2466 try:
2467 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2468 got_exclusive = True
2469 except IOError as e5:
2470 (err, msg) = e5.args
2471 if err != errno.EACCES and err != errno.EAGAIN:
2472 log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
2473 (self.lockfile, msg, err))
2474 raise
2476 if not got_exclusive:
2477 buf = os.read(self.lockfd, 64)
2478 self.current_pid = None
2479 try:
2480 self.current_pid = int(buf)
2481 except ValueError as e:
2482 pass
2483 if self.current_pid is not None:
2484 return True
2486 if got_exclusive and terminate:
2487 try:
2488 os.ftruncate(self.lockfd, 0)
2489 except IOError as e2:
2490 (err, msg) = e2.args
2491 log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
2492 (self.lockfile, msg, err))
2493 raise
2494 os.close(self.lockfd)
2495 self.lockfd = -1
2496 return False
2498 try:
2499 fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
2500 except IOError as e6:
2501 (err, msg) = e6.args
2502 log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
2503 (self.lockfile, msg, err))
2505 # We leave the function with the shared lock.
2506 return False
2508 def update_pid(pid):
2509 if self.lockfd != -1:
2510 got_exclusive = False
2511 # Try 5 times to get the exclusive lock.
2512 for i in range(0, 5):
2513 try:
2514 fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2515 got_exclusive = True
2516 except IOError as e:
2517 (err, msg) = e.args
2518 if err != errno.EACCES and err != errno.EAGAIN:
2519 log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
2520 (pid, self.lockfile, msg, err))
2521 raise
2522 if got_exclusive:
2523 break
2524 time.sleep(1)
2525 if not got_exclusive:
2526 log_msg("update_pid(%r): failed to get exclusive lock[%s]" %
2527 (pid, self.lockfile))
2528 raise CommandError("update_pid(%r): failed to get "
2529 "exclusive lock[%s] after 5 seconds" %
2530 (pid, self.lockfile))
2532 if pid is not None:
2533 buf = "%d\n" % pid
2534 else:
2535 buf = None
2536 try:
2537 os.ftruncate(self.lockfd, 0)
2538 if buf is not None:
2539 os.write(self.lockfd, get_bytes(buf))
2540 except IOError as e3:
2541 (err, msg) = e3.args
2542 log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
2543 (self.lockfile, msg, err))
2544 raise
2545 self.current_pid = pid
2546 if self.current_pid is not None:
2547 log_msg("currentPid: %d\n" % self.current_pid)
2549 modify_ldif = "dn: %s\n" % (self.cache_dn) +\
2550 "changetype: modify\n" +\
2551 "replace: currentPid\n"
2552 if self.current_pid is not None:
2553 modify_ldif += "currentPid: %d\n" % (self.current_pid)
2554 modify_ldif += "replace: currentTime\n" +\
2555 "currentTime: %s\n" % ldb.timestring(int(time.time()))
2556 self.cache.modify_ldif(modify_ldif)
2557 return
2559 def update_cache(res_controls):
2560 assert len(res_controls) > 0
2561 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2562 res_controls[0].critical = True
2563 self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"]
2564 # This cookie can be extremely long
2565 # log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
2567 modify_ldif = "dn: %s\n" % (self.cache_dn) +\
2568 "changetype: modify\n" +\
2569 "replace: dirsyncControl\n" +\
2570 "dirsyncControl: %s\n" % (self.dirsync_controls[0]) +\
2571 "replace: currentTime\n" +\
2572 "currentTime: %s\n" % ldb.timestring(int(time.time()))
2573 self.cache.modify_ldif(modify_ldif)
2574 return
2576 def check_object(dirsync_obj, res_controls):
2577 assert len(res_controls) > 0
2578 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2580 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2581 sid = ndr_unpack(security.dom_sid, binary_sid)
2582 dn = "KEY=%s" % sid
2583 lastCookie = str(res_controls[0])
2585 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2586 expression="(lastCookie=%s)" % (
2587 ldb.binary_encode(lastCookie)),
2588 attrs=[])
2589 if len(res) == 1:
2590 return True
2591 return False
2593 def update_object(dirsync_obj, res_controls):
2594 assert len(res_controls) > 0
2595 assert res_controls[0].oid == "1.2.840.113556.1.4.841"
2597 binary_sid = dirsync_obj.dn.get_extended_component("SID")
2598 sid = ndr_unpack(security.dom_sid, binary_sid)
2599 dn = "KEY=%s" % sid
2600 lastCookie = str(res_controls[0])
2602 self.cache.transaction_start()
2603 try:
2604 res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE,
2605 expression="(objectClass=*)",
2606 attrs=["lastCookie"])
2607 if len(res) == 0:
2608 add_ldif = "dn: %s\n" % (dn) +\
2609 "objectClass: userCookie\n" +\
2610 "lastCookie: %s\n" % (lastCookie) +\
2611 "currentTime: %s\n" % ldb.timestring(int(time.time()))
2612 self.cache.add_ldif(add_ldif)
2613 else:
2614 modify_ldif = "dn: %s\n" % (dn) +\
2615 "changetype: modify\n" +\
2616 "replace: lastCookie\n" +\
2617 "lastCookie: %s\n" % (lastCookie) +\
2618 "replace: currentTime\n" +\
2619 "currentTime: %s\n" % ldb.timestring(int(time.time()))
2620 self.cache.modify_ldif(modify_ldif)
2621 self.cache.transaction_commit()
2622 except Exception as e:
2623 self.cache.transaction_cancel()
2625 return
2627 def dirsync_loop():
2628 while True:
2629 res = self.samdb.search(expression=str(self.dirsync_filter),
2630 scope=ldb.SCOPE_SUBTREE,
2631 attrs=self.dirsync_attrs,
2632 controls=self.dirsync_controls)
2633 log_msg("dirsync_loop(): results %d\n" % len(res))
2634 ri = 0
2635 for r in res:
2636 done = check_object(r, res.controls)
2637 if not done:
2638 handle_object(ri, r)
2639 update_object(r, res.controls)
2640 ri += 1
2641 update_cache(res.controls)
2642 if len(res) == 0:
2643 break
2645 def sync_loop(wait):
2646 notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
2647 notify_controls = ["notification:1", "show_recycled:1"]
2648 notify_handle = self.samdb.search_iterator(expression="objectClass=*",
2649 scope=ldb.SCOPE_SUBTREE,
2650 attrs=notify_attrs,
2651 controls=notify_controls,
2652 timeout=-1)
2654 if wait is True:
2655 log_msg("Resuming monitoring\n")
2656 else:
2657 log_msg("Getting changes\n")
2658 self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter)
2659 self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls)
2660 self.outf.write("syncCommand: %s\n" % self.sync_command)
2661 dirsync_loop()
2663 if wait is not True:
2664 return
2666 for msg in notify_handle:
2667 if not isinstance(msg, ldb.Message):
2668 self.outf.write("referral: %s\n" % msg)
2669 continue
2670 created = msg.get("uSNCreated")[0]
2671 changed = msg.get("uSNChanged")[0]
2672 log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" %
2673 (msg.dn, created, changed))
2675 dirsync_loop()
2677 res = notify_handle.result()
2679 def daemonize():
2680 self.samdb = None
2681 self.cache = None
2682 orig_pid = os.getpid()
2683 pid = os.fork()
2684 if pid == 0:
2685 os.setsid()
2686 pid = os.fork()
2687 if pid == 0: # Actual daemon
2688 pid = os.getpid()
2689 log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid))
2690 load_cache()
2691 return
2692 os._exit(0)
2694 if cache_ldb_initialize:
2695 self.samdb_url = H
2696 self.samdb = self.connect_system_samdb(url=self.samdb_url,
2697 verbose=True)
2698 load_cache()
2699 return
2701 if logfile is not None:
2702 import resource # Resource usage information.
2703 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
2704 if maxfd == resource.RLIM_INFINITY:
2705 maxfd = 1024 # Rough guess at maximum number of open file descriptors.
2706 logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
2707 self.outf.write("Using logfile[%s]\n" % logfile)
2708 for fd in range(0, maxfd):
2709 if fd == logfd:
2710 continue
2711 try:
2712 os.close(fd)
2713 except OSError:
2714 pass
2715 os.dup2(logfd, 0)
2716 os.dup2(logfd, 1)
2717 os.dup2(logfd, 2)
2718 os.close(logfd)
2719 log_msg("Attached to logfile[%s]\n" % (logfile))
2720 self.logfile = logfile
2722 load_cache()
2723 conflict = check_current_pid_conflict(terminate)
2724 if terminate:
2725 if self.current_pid is None:
2726 log_msg("No process running.\n")
2727 return
2728 if not conflict:
2729 log_msg("Process %d is not running anymore.\n" % (
2730 self.current_pid))
2731 update_pid(None)
2732 return
2733 log_msg("Sending SIGTERM to process %d.\n" % (
2734 self.current_pid))
2735 os.kill(self.current_pid, signal.SIGTERM)
2736 return
2737 if conflict:
2738 raise CommandError("Exiting pid %d, command is already running as pid %d" % (
2739 os.getpid(), self.current_pid))
2741 if daemon is True:
2742 daemonize()
2743 update_pid(os.getpid())
2745 wait = True
2746 while wait is True:
2747 retry_sleep_min = 1
2748 retry_sleep_max = 600
2749 if nowait is True:
2750 wait = False
2751 retry_sleep = 0
2752 else:
2753 retry_sleep = retry_sleep_min
2755 while self.samdb is None:
2756 if retry_sleep != 0:
2757 log_msg("Wait before connect - sleep(%d)\n" % retry_sleep)
2758 time.sleep(retry_sleep)
2759 retry_sleep = retry_sleep * 2
2760 if retry_sleep >= retry_sleep_max:
2761 retry_sleep = retry_sleep_max
2762 log_msg("Connecting to '%s'\n" % self.samdb_url)
2763 try:
2764 self.samdb = self.connect_system_samdb(url=self.samdb_url)
2765 except Exception as msg:
2766 self.samdb = None
2767 log_msg("Connect to samdb Exception => (%s)\n" % msg)
2768 if wait is not True:
2769 raise
2771 try:
2772 sync_loop(wait)
2773 except ldb.LdbError as e7:
2774 (enum, estr) = e7.args
2775 self.samdb = None
2776 log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
2778 update_pid(None)
2779 return
2782 class cmd_user_edit(Command):
2783 """Modify User AD object.
2785 This command will allow editing of a user account in the Active Directory
2786 domain. You will then be able to add or change attributes and their values.
2788 The username specified on the command is the sAMAccountName.
2790 The command may be run from the root userid or another authorized userid.
2792 The -H or --URL= option can be used to execute the command against a remote
2793 server.
2795 Example1:
2796 samba-tool user edit User1 -H ldap://samba.samdom.example.com \\
2797 -U administrator --password=passw1rd
2799 Example1 shows how to edit a users attributes in the domain against a remote
2800 LDAP server.
2802 The -H parameter is used to specify the remote target server.
2804 Example2:
2805 samba-tool user edit User2
2807 Example2 shows how to edit a users attributes in the domain against a local
2808 LDAP server.
2810 Example3:
2811 samba-tool user edit User3 --editor=nano
2813 Example3 shows how to edit a users attributes in the domain against a local
2814 LDAP server using the 'nano' editor.
2817 synopsis = "%prog <username> [options]"
2819 takes_options = [
2820 Option("-H", "--URL", help="LDB URL for database or target server",
2821 type=str, metavar="URL", dest="H"),
2822 Option("--editor", help="Editor to use instead of the system default,"
2823 " or 'vi' if no system default is set.", type=str),
2826 takes_args = ["username"]
2827 takes_optiongroups = {
2828 "sambaopts": options.SambaOptions,
2829 "credopts": options.CredentialsOptions,
2830 "versionopts": options.VersionOptions,
2833 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2834 H=None, editor=None):
2835 lp = sambaopts.get_loadparm()
2836 creds = credopts.get_credentials(lp, fallback_machine=True)
2837 samdb = SamDB(url=H, session_info=system_session(),
2838 credentials=creds, lp=lp)
2840 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2841 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2843 domaindn = samdb.domain_dn()
2845 try:
2846 res = samdb.search(base=domaindn,
2847 expression=filter,
2848 scope=ldb.SCOPE_SUBTREE)
2849 user_dn = res[0].dn
2850 except IndexError:
2851 raise CommandError('Unable to find user "%s"' % (username))
2853 import tempfile
2854 for msg in res:
2855 result_ldif = common.get_ldif_for_editor(samdb, msg)
2857 if editor is None:
2858 editor = os.environ.get('EDITOR')
2859 if editor is None:
2860 editor = 'vi'
2862 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
2863 t_file.write(get_bytes(result_ldif))
2864 t_file.flush()
2865 try:
2866 check_call([editor, t_file.name])
2867 except CalledProcessError as e:
2868 raise CalledProcessError("ERROR: ", e)
2869 with open(t_file.name) as edited_file:
2870 edited_message = edited_file.read()
2873 msgs_edited = samdb.parse_ldif(edited_message)
2874 msg_edited = next(msgs_edited)[1]
2876 res_msg_diff = samdb.msg_diff(msg, msg_edited)
2877 if len(res_msg_diff) == 0:
2878 self.outf.write("Nothing to do\n")
2879 return
2881 try:
2882 samdb.modify(res_msg_diff)
2883 except Exception as e:
2884 raise CommandError("Failed to modify user '%s': " % username, e)
2886 self.outf.write("Modified User '%s' successfully\n" % username)
2889 class cmd_user_show(GetPasswordCommand):
2890 """Display a user AD object.
2892 This command displays a user account and it's attributes in the Active
2893 Directory domain.
2894 The username specified on the command is the sAMAccountName.
2896 The command may be run from the root userid or another authorized userid.
2898 The -H or --URL= option can be used to execute the command against a remote
2899 server.
2901 The '--attributes' parameter takes a comma separated list of the requested
2902 attributes. Without '--attributes' or with '--attributes=*' all usually
2903 available attributes are selected.
2904 Hidden attributes in addition to all usually available attributes can be
2905 selected with e.g. '--attributes=*,msDS-UserPasswordExpiryTimeComputed'.
2906 If a specified attribute is not available on a user object it's silently
2907 omitted.
2909 Attributes with time values can take an additional format specifier, which
2910 converts the time value into the requested format. The format can be specified
2911 by adding ";format=formatSpecifier" to the requested attribute name, whereby
2912 "formatSpecifier" must be a valid specifier. The syntax looks like:
2914 --attributes=attributeName;format=formatSpecifier
2916 The following format specifiers are available:
2917 - GeneralizedTime (e.g. 20210224113259.0Z)
2918 - UnixTime (e.g. 1614166392)
2919 - TimeSpec (e.g. 161416639.267546892)
2921 Attributes with an original NTTIME value of 0 and 9223372036854775807 are
2922 treated as non-existing value.
2924 Example1:
2925 samba-tool user show User1 -H ldap://samba.samdom.example.com \\
2926 -U administrator --password=passw1rd
2928 Example1 shows how to display a users attributes in the domain against a remote
2929 LDAP server.
2931 The -H parameter is used to specify the remote target server.
2933 Example2:
2934 samba-tool user show User2
2936 Example2 shows how to display a users attributes in the domain against a local
2937 LDAP server.
2939 Example3:
2940 samba-tool user show User2 --attributes=objectSid,memberOf
2942 Example3 shows how to display a users objectSid and memberOf attributes.
2944 Example4:
2945 samba-tool user show User2 \\
2946 --attributes='pwdLastSet;format=GeneralizedTime,pwdLastSet;format=UnixTime'
2948 The result of Example 4 provides the pwdLastSet attribute values in the
2949 specified format:
2950 dn: CN=User2,CN=Users,DC=samdom,DC=example,DC=com
2951 pwdLastSet;format=GeneralizedTime: 20210120105207.0Z
2952 pwdLastSet;format=UnixTime: 1611139927
2954 synopsis = "%prog <username> [options]"
2956 takes_options = [
2957 Option("-H", "--URL", help="LDB URL for database or target server",
2958 type=str, metavar="URL", dest="H"),
2959 Option("--attributes",
2960 help=("Comma separated list of attributes, "
2961 "which will be printed. "
2962 "Possible supported virtual attributes: "
2963 "virtualGeneralizedTime, virtualUnixTime, virtualTimeSpec."),
2964 type=str, dest="user_attrs"),
2967 takes_args = ["username"]
2968 takes_optiongroups = {
2969 "sambaopts": options.SambaOptions,
2970 "credopts": options.CredentialsOptions,
2971 "versionopts": options.VersionOptions,
2974 def run(self, username, credopts=None, sambaopts=None, versionopts=None,
2975 H=None, user_attrs=None):
2977 lp = sambaopts.get_loadparm()
2978 creds = credopts.get_credentials(lp, fallback_machine=True)
2979 samdb = SamDB(url=H, session_info=system_session(),
2980 credentials=creds, lp=lp)
2982 self.inject_virtual_attributes(samdb)
2984 if user_attrs:
2985 attrs = self.parse_attributes(user_attrs)
2986 else:
2987 attrs = ["*"]
2989 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
2990 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
2992 domaindn = samdb.domain_dn()
2994 obj = self.get_account_attributes(samdb, username,
2995 basedn=domaindn,
2996 filter=filter,
2997 scope=ldb.SCOPE_SUBTREE,
2998 attrs=attrs,
2999 decrypt=False,
3000 support_pw_attrs=False)
3001 user_ldif = common.get_ldif_for_editor(samdb, obj)
3002 self.outf.write(user_ldif)
3004 class cmd_user_move(Command):
3005 """Move a user to an organizational unit/container.
3007 This command moves a user account into the specified organizational unit
3008 or container.
3009 The username specified on the command is the sAMAccountName.
3010 The name of the organizational unit or container can be specified as a
3011 full DN or without the domainDN component.
3013 The command may be run from the root userid or another authorized userid.
3015 The -H or --URL= option can be used to execute the command against a remote
3016 server.
3018 Example1:
3019 samba-tool user move User1 'OU=OrgUnit,DC=samdom,DC=example,DC=com' \\
3020 -H ldap://samba.samdom.example.com -U administrator
3022 Example1 shows how to move a user User1 into the 'OrgUnit' organizational
3023 unit on a remote LDAP server.
3025 The -H parameter is used to specify the remote target server.
3027 Example2:
3028 samba-tool user move User1 CN=Users
3030 Example2 shows how to move a user User1 back into the CN=Users container
3031 on the local server.
3034 synopsis = "%prog <username> <new_parent_dn> [options]"
3036 takes_options = [
3037 Option("-H", "--URL", help="LDB URL for database or target server",
3038 type=str, metavar="URL", dest="H"),
3041 takes_args = ["username", "new_parent_dn"]
3042 takes_optiongroups = {
3043 "sambaopts": options.SambaOptions,
3044 "credopts": options.CredentialsOptions,
3045 "versionopts": options.VersionOptions,
3048 def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
3049 versionopts=None, H=None):
3050 lp = sambaopts.get_loadparm()
3051 creds = credopts.get_credentials(lp, fallback_machine=True)
3052 samdb = SamDB(url=H, session_info=system_session(),
3053 credentials=creds, lp=lp)
3054 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
3056 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
3057 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
3058 try:
3059 res = samdb.search(base=domain_dn,
3060 expression=filter,
3061 scope=ldb.SCOPE_SUBTREE)
3062 user_dn = res[0].dn
3063 except IndexError:
3064 raise CommandError('Unable to find user "%s"' % (username))
3066 try:
3067 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
3068 except Exception as e:
3069 raise CommandError('Invalid new_parent_dn "%s": %s' %
3070 (new_parent_dn, e))
3072 full_new_user_dn = ldb.Dn(samdb, str(user_dn))
3073 full_new_user_dn.remove_base_components(len(user_dn) - 1)
3074 full_new_user_dn.add_base(full_new_parent_dn)
3076 try:
3077 samdb.rename(user_dn, full_new_user_dn)
3078 except Exception as e:
3079 raise CommandError('Failed to move user "%s"' % username, e)
3080 self.outf.write('Moved user "%s" into "%s"\n' %
3081 (username, full_new_parent_dn))
3084 class cmd_user_rename(Command):
3085 """Rename a user and related attributes.
3087 This command allows to set the user's name related attributes. The user's
3088 CN will be renamed automatically.
3089 The user's new CN will be made up by combining the given-name, initials
3090 and surname. A dot ('.') will be appended to the initials automatically
3091 if required.
3092 Use the --force-new-cn option to specify the new CN manually and the
3093 --reset-cn option to reset this change.
3095 Use an empty attribute value to remove the specified attribute.
3097 The username specified on the command is the sAMAccountName.
3099 The command may be run locally from the root userid or another authorized
3100 userid.
3102 The -H or --URL= option can be used to execute the command against a remote
3103 server.
3105 Example1:
3106 samba-tool user rename johndoe --surname='Bloggs'
3108 Example1 shows how to change the surname of a user 'johndoe' to 'Bloggs' on
3109 the local server. The user's CN will be renamed automatically, based on
3110 the given name, initials and surname.
3112 Example2:
3113 samba-tool user rename johndoe --force-new-cn='John Bloggs (Sales)' \\
3114 --surname=Bloggs -H ldap://samba.samdom.example.com -U administrator
3116 Example2 shows how to rename the CN of a user 'johndoe' to 'John Bloggs (Sales)'.
3117 Additionally the surname ('sn' attribute) is set to 'Bloggs'.
3118 The -H parameter is used to specify the remote target server.
3121 synopsis = "%prog <username> [options]"
3123 takes_options = [
3124 Option("-H", "--URL",
3125 help="LDB URL for database or target server",
3126 type=str, metavar="URL", dest="H"),
3127 Option("--surname",
3128 help="New surname",
3129 type=str),
3130 Option("--given-name",
3131 help="New given name",
3132 type=str),
3133 Option("--initials",
3134 help="New initials",
3135 type=str),
3136 Option("--force-new-cn",
3137 help="Specify a new CN (RDN) instead of using a combination "
3138 "of the given name, initials and surname.",
3139 type=str, metavar="NEW_CN"),
3140 Option("--reset-cn",
3141 help="Set the CN (RDN) to the combination of the given name, "
3142 "initials and surname. Use this option to reset "
3143 "the changes made with the --force-new-cn option.",
3144 action="store_true"),
3145 Option("--display-name",
3146 help="New display name",
3147 type=str),
3148 Option("--mail-address",
3149 help="New email address",
3150 type=str),
3151 Option("--samaccountname",
3152 help="New account name (sAMAccountName/logon name)",
3153 type=str),
3154 Option("--upn",
3155 help="New user principal name",
3156 type=str),
3159 takes_args = ["username"]
3160 takes_optiongroups = {
3161 "sambaopts": options.SambaOptions,
3162 "credopts": options.CredentialsOptions,
3163 "versionopts": options.VersionOptions,
3166 def run(self, username, credopts=None, sambaopts=None,
3167 versionopts=None, H=None, surname=None, given_name=None,
3168 initials=None, display_name=None, mail_address=None,
3169 samaccountname=None, upn=None, force_new_cn=None,
3170 reset_cn=None):
3171 # illegal options
3172 if force_new_cn and reset_cn:
3173 raise CommandError("It is not allowed to specify --force-new-cn "
3174 "together with --reset-cn.")
3175 if force_new_cn == "":
3176 raise CommandError("Failed to rename user - delete protected "
3177 "attribute 'CN'")
3178 if samaccountname == "":
3179 raise CommandError("Failed to rename user - delete protected "
3180 "attribute 'sAMAccountName'")
3182 lp = sambaopts.get_loadparm()
3183 creds = credopts.get_credentials(lp, fallback_machine=True)
3184 samdb = SamDB(url=H, session_info=system_session(),
3185 credentials=creds, lp=lp)
3186 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
3188 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
3189 (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
3190 try:
3191 res = samdb.search(base=domain_dn,
3192 expression=filter,
3193 scope=ldb.SCOPE_SUBTREE,
3194 attrs=["sAMAccountName",
3195 "givenName",
3196 "initials",
3197 "sn",
3198 "mail",
3199 "userPrincipalName",
3200 "displayName",
3201 "cn"])
3202 old_user = res[0]
3203 user_dn = old_user.dn
3204 except IndexError:
3205 raise CommandError('Unable to find user "%s"' % (username))
3207 user_parent_dn = user_dn.parent()
3208 old_cn = old_user["cn"][0]
3210 # use the sAMAccountname as CN if no name is given
3211 new_fallback_cn = samaccountname if samaccountname is not None \
3212 else old_user["sAMAccountName"]
3214 if force_new_cn is not None:
3215 new_user_cn = force_new_cn
3216 else:
3217 new_user_cn = samdb.fullname_from_names(old_attrs=old_user,
3218 given_name=given_name,
3219 initials=initials,
3220 surname=surname,
3221 fallback_default=new_fallback_cn)
3223 # CN must change, if the new CN is different and the old CN is the
3224 # standard CN or the change is forced with force-new-cn or reset-cn
3225 expected_cn = samdb.fullname_from_names(old_attrs=old_user,
3226 fallback_default=old_user["sAMAccountName"])
3227 must_change_cn = str(old_cn) != str(new_user_cn) and \
3228 (str(old_cn) == str(expected_cn) or \
3229 reset_cn or bool(force_new_cn))
3231 new_user_dn = ldb.Dn(samdb, "CN=%s" % new_user_cn)
3232 new_user_dn.add_base(user_parent_dn)
3234 if upn is not None:
3235 if self.is_valid_upn(samdb, upn) == False:
3236 raise CommandError('"%s" is not a valid upn. '
3237 'You can manage the upn '
3238 'suffixes with the "samba-tool domain '
3239 'trust namespaces" command.' % upn)
3241 user_attrs = ldb.Message()
3242 user_attrs.dn = user_dn
3243 samdb.prepare_attr_replace(user_attrs, old_user, "givenName", given_name)
3244 samdb.prepare_attr_replace(user_attrs, old_user, "initials", initials)
3245 samdb.prepare_attr_replace(user_attrs, old_user, "sn", surname)
3246 samdb.prepare_attr_replace(user_attrs, old_user, "displayName", display_name)
3247 samdb.prepare_attr_replace(user_attrs, old_user, "mail", mail_address)
3248 samdb.prepare_attr_replace(user_attrs, old_user, "sAMAccountName", samaccountname)
3249 samdb.prepare_attr_replace(user_attrs, old_user, "userPrincipalName", upn)
3251 attributes_changed = len(user_attrs) > 0
3253 samdb.transaction_start()
3254 try:
3255 if attributes_changed == True:
3256 samdb.modify(user_attrs)
3257 if must_change_cn == True:
3258 samdb.rename(user_dn, new_user_dn)
3259 except Exception as e:
3260 samdb.transaction_cancel()
3261 raise CommandError('Failed to rename user "%s"' % username, e)
3262 samdb.transaction_commit()
3264 if must_change_cn == True:
3265 self.outf.write('Renamed CN of user "%s" from "%s" to "%s" '
3266 'successfully\n' % (username, old_cn, new_user_cn))
3268 if attributes_changed == True:
3269 self.outf.write('Following attributes of user "%s" have been '
3270 'changed successfully:\n' % (username))
3271 for attr in user_attrs.keys():
3272 if (attr == "dn"):
3273 continue
3274 self.outf.write('%s: %s\n' % (attr, user_attrs[attr]
3275 if user_attrs[attr] else '[removed]'))
3277 def is_valid_upn(self, samdb, upn):
3278 domain_dns = samdb.domain_dns_name()
3279 forest_dns = samdb.forest_dns_name()
3280 upn_suffixes = [domain_dns, forest_dns]
3282 config_basedn = samdb.get_config_basedn()
3283 partitions_dn = "CN=Partitions,%s" % config_basedn
3284 res = samdb.search(
3285 base=partitions_dn,
3286 scope=ldb.SCOPE_BASE,
3287 expression="(objectClass=crossRefContainer)",
3288 attrs=['uPNSuffixes'])
3290 if (len(res) >= 1):
3291 msg = res[0]
3292 if 'uPNSuffixes' in msg:
3293 for s in msg['uPNSuffixes']:
3294 upn_suffixes.append(str(s).lower())
3296 upn_split = upn.split('@')
3297 if (len(upn_split) < 2):
3298 return False
3300 upn_suffix = upn_split[-1].lower()
3301 if upn_suffix not in upn_suffixes:
3302 return False
3304 return True
3307 class cmd_user_add_unix_attrs(Command):
3308 """Add RFC2307 attributes to a user.
3310 This command adds Unix attributes to a user account in the Active
3311 Directory domain.
3313 The username specified on the command is the sAMaccountName.
3315 You must supply a unique uidNumber.
3317 Unix (RFC2307) attributes will be added to the user account.
3319 If you supply a gidNumber with '--gid-number', this will be used for the
3320 users Unix 'gidNumber' attribute.
3322 If '--gid-number' is not supplied, the users Unix gidNumber will be set to the
3323 one found in 'Domain Users', this means Domain Users must have a gidNumber
3324 attribute.
3326 if '--unix-home' is not supplied, the users Unix home directory will be
3327 set to /home/DOMAIN/username
3329 if '--login-shell' is not supplied, the users Unix login shell will be
3330 set to '/bin/sh'
3332 if ---gecos' is not supplied, the users Unix gecos field will be set to the
3333 users 'CN'
3335 Add 'idmap_ldb:use rfc2307 = Yes' to the smb.conf on DCs, to use these
3336 attributes for UID/GID mapping.
3338 The command may be run from the root userid or another authorised userid.
3339 The -H or --URL= option can be used to execute the command against a
3340 remote server.
3342 Example1:
3343 samba-tool user addunixattrs User1 10001
3345 Example1 shows how to add RFC2307 attributes to a domain enabled user
3346 account, Domain Users will be set as the users gidNumber.
3348 The users Unix ID will be set to '10001', provided this ID isn't already
3349 in use.
3351 Example2:
3352 samba-tool user addunixattrs User2 10002 --gid-number=10001 \
3353 --unix-home=/home/User2
3355 Example2 shows how to add RFC2307 attributes to a domain enabled user
3356 account.
3358 The users Unix ID will be set to '10002', provided this ID isn't already
3359 in use.
3361 The users gidNumber attribute will be set to '10001'
3363 The users Unix home directory will be set to '/home/user2'
3365 Example3:
3366 samba-tool user addunixattrs User3 10003 --gid-number=10001 \
3367 --login-shell=/bin/false --gecos='User3 test'
3369 Example3 shows how to add RFC2307 attributes to a domain enabled user
3370 account.
3372 The users Unix ID will be set to '10003', provided this ID isn't already
3373 in use.
3375 The users gidNumber attribute will be set to '10001'
3377 The users Unix login shell will be set to '/bin/false'
3379 The users gecos field will be set to 'User3 test'
3381 Example4:
3382 samba-tool user addunixattrs User4 10004 --gid-number=10001 \
3383 --unix-home=/home/User4 --login-shell=/bin/bash --gecos='User4 test'
3385 Example4 shows how to add RFC2307 attributes to a domain enabled user
3386 account.
3388 The users Unix ID will be set to '10004', provided this ID isn't already
3389 in use.
3391 The users gidNumber attribute will be set to '10001'
3393 The users Unix home directory will be set to '/home/User4'
3395 The users Unix login shell will be set to '/bin/bash'
3397 The users gecos field will be set to 'User4 test'
3401 synopsis = "%prog <username> <uid-number> [options]"
3403 takes_options = [
3404 Option("-H", "--URL", help="LDB URL for database or target server",
3405 type=str, metavar="URL", dest="H"),
3406 Option("--gid-number", help="User's Unix/RFC2307 GID", type=str),
3407 Option("--unix-home", help="User's Unix/RFC2307 home directory",
3408 type=str),
3409 Option("--login-shell", help="User's Unix/RFC2307 login shell",
3410 type=str),
3411 Option("--gecos", help="User's Unix/RFC2307 GECOS field", type=str),
3412 Option("--uid", help="User's Unix/RFC2307 username", type=str),
3415 takes_args = ["username", "uid-number"]
3417 takes_optiongroups = {
3418 "sambaopts": options.SambaOptions,
3419 "credopts": options.CredentialsOptions,
3420 "versionopts": options.VersionOptions,
3423 def run(self, username, uid_number, credopts=None, sambaopts=None,
3424 versionopts=None, H=None, gid_number=None, unix_home=None,
3425 login_shell=None, gecos=None, uid=None):
3427 lp = sambaopts.get_loadparm()
3428 creds = credopts.get_credentials(lp)
3430 samdb = SamDB(url=H, session_info=system_session(),
3431 credentials=creds, lp=lp)
3433 domaindn = samdb.domain_dn()
3435 # Check that uidNumber supplied isn't already in use
3436 filter = ("(&(objectClass=person)(uidNumber={}))"
3437 .format(uid_number))
3438 res = samdb.search(domaindn,
3439 scope=ldb.SCOPE_SUBTREE,
3440 expression=filter)
3441 if (len(res) != 0):
3442 raise CommandError("uidNumber {} is already being used."
3443 .format(uid_number))
3445 # Check user exists and doesn't have a uidNumber
3446 filter = "(samaccountname={})".format(ldb.binary_encode(username))
3447 res = samdb.search(domaindn,
3448 scope=ldb.SCOPE_SUBTREE,
3449 expression=filter)
3450 if (len(res) == 0):
3451 raise CommandError("Unable to find user '{}'".format(username))
3453 user_dn = res[0].dn
3455 if "uidNumber" in res[0]:
3456 raise CommandError("User {} is already a Unix user."
3457 .format(username))
3459 if gecos is None:
3460 gecos = res[0]["cn"][0]
3462 if uid is None:
3463 uid = res[0]["cn"][0]
3465 if gid_number is None:
3466 search_filter = ("(samaccountname={})"
3467 .format(ldb.binary_encode('Domain Users')))
3468 try:
3469 res = samdb.search(domaindn,
3470 scope=ldb.SCOPE_SUBTREE,
3471 expression=search_filter)
3472 for msg in res:
3473 gid_number=msg.get('gidNumber')
3474 except IndexError:
3475 raise CommandError('Domain Users does not have a'
3476 ' gidNumber attribute')
3478 if login_shell is None:
3479 login_shell = "/bin/sh"
3481 if unix_home is None:
3482 # obtain nETBIOS Domain Name
3483 unix_domain = samdb.domain_netbios_name()
3484 if unix_domain is None:
3485 raise CommandError('Unable to find Unix domain')
3487 tmpl = lp.get('template homedir')
3488 unix_home = tmpl.replace('%D', unix_domain).replace('%U', username)
3490 if not lp.get("idmap_ldb:use rfc2307"):
3491 self.outf.write("You are setting a Unix/RFC2307 UID & GID. "
3492 "You may want to set 'idmap_ldb:use rfc2307 = Yes'"
3493 " in smb.conf to use the attributes for "
3494 "XID/SID-mapping.\n")
3496 user_mod = """
3497 dn: {0}
3498 changetype: modify
3499 add: uidNumber
3500 uidNumber: {1}
3501 add: gidnumber
3502 gidNumber: {2}
3503 add: gecos
3504 gecos: {3}
3505 add: uid
3506 uid: {4}
3507 add: loginshell
3508 loginShell: {5}
3509 add: unixHomeDirectory
3510 unixHomeDirectory: {6}
3511 """.format(user_dn, uid_number, gid_number, gecos, uid, login_shell, unix_home)
3513 samdb.transaction_start()
3514 try:
3515 samdb.modify_ldif(user_mod)
3516 except ldb.LdbError as e:
3517 raise CommandError("Failed to modify user '{0}': {1}"
3518 .format(username, e))
3519 else:
3520 samdb.transaction_commit()
3521 self.outf.write("Modified User '{}' successfully\n"
3522 .format(username))
3524 class cmd_user_unlock(Command):
3525 """Unlock a user account.
3527 This command unlocks a user account in the Active Directory domain. The
3528 username specified on the command is the sAMAccountName. The username may
3529 also be specified using the --filter option.
3531 The command may be run from the root userid or another authorized userid.
3532 The -H or --URL= option can be used to execute the command against a remote
3533 server.
3535 Example:
3536 samba-tool user unlock user1 -H ldap://samba.samdom.example.com \\
3537 --username=Administrator --password=Passw0rd
3539 The example shows how to unlock a user account in the domain against a
3540 remote LDAP server. The -H parameter is used to specify the remote target
3541 server. The --username= and --password= options are used to pass the
3542 username and password of a user that exists on the remote server and is
3543 authorized to issue the command on that server.
3546 synopsis = "%prog (<username>|--filter <filter>) [options]"
3548 takes_options = [
3549 Option("-H",
3550 "--URL",
3551 help="LDB URL for database or target server",
3552 type=str,
3553 metavar="URL",
3554 dest="H"),
3555 Option("--filter",
3556 help="LDAP Filter to set password on",
3557 type=str),
3560 takes_args = ["username?"]
3562 takes_optiongroups = {
3563 "sambaopts": options.SambaOptions,
3564 "credopts": options.CredentialsOptions,
3565 "versionopts": options.VersionOptions,
3568 def run(self,
3569 username=None,
3570 sambaopts=None,
3571 credopts=None,
3572 versionopts=None,
3573 filter=None,
3574 H=None):
3575 if username is None and filter is None:
3576 raise CommandError("Either the username or '--filter' must be "
3577 "specified!")
3579 if filter is None:
3580 filter = ("(&(objectClass=user)(sAMAccountName=%s))" % (
3581 ldb.binary_encode(username)))
3583 lp = sambaopts.get_loadparm()
3584 creds = credopts.get_credentials(lp, fallback_machine=True)
3586 samdb = SamDB(url=H,
3587 session_info=system_session(),
3588 credentials=creds,
3589 lp=lp)
3590 try:
3591 samdb.unlock_account(filter)
3592 except (SamDBError, ldb.LdbError) as msg:
3593 raise CommandError("Failed to unlock user '%s': %s" % (
3594 username or filter, msg))
3596 class cmd_user_sensitive(Command):
3597 """Set/unset or show UF_NOT_DELEGATED for an account."""
3599 synopsis = "%prog <accountname> [(show|on|off)] [options]"
3601 takes_optiongroups = {
3602 "sambaopts": options.SambaOptions,
3603 "credopts": options.CredentialsOptions,
3604 "versionopts": options.VersionOptions,
3607 takes_options = [
3608 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
3609 metavar="URL", dest="H"),
3612 takes_args = ["accountname", "cmd"]
3614 def run(self, accountname, cmd, H=None, credopts=None, sambaopts=None,
3615 versionopts=None):
3617 if cmd not in ("show", "on", "off"):
3618 raise CommandError("invalid argument: '%s' (choose from 'show', 'on', 'off')" % cmd)
3620 lp = sambaopts.get_loadparm()
3621 creds = credopts.get_credentials(lp, fallback_machine=True)
3622 sam = SamDB(url=H, session_info=system_session(),
3623 credentials=creds, lp=lp)
3625 search_filter = "sAMAccountName=%s" % ldb.binary_encode(accountname)
3626 flag = dsdb.UF_NOT_DELEGATED;
3628 if cmd == "show":
3629 res = sam.search(scope=ldb.SCOPE_SUBTREE, expression=search_filter,
3630 attrs=["userAccountControl"])
3631 if len(res) == 0:
3632 raise Exception("Unable to find account where '%s'" % search_filter)
3634 uac = int(res[0].get("userAccountControl")[0])
3636 self.outf.write("Account-DN: %s\n" % str(res[0].dn))
3637 self.outf.write("UF_NOT_DELEGATED: %s\n" % bool(uac & flag))
3639 return
3641 if cmd == "on":
3642 on = True
3643 elif cmd == "off":
3644 on = False
3646 try:
3647 sam.toggle_userAccountFlags(search_filter, flag, flags_str="Not-Delegated",
3648 on=on, strict=True)
3649 except Exception as err:
3650 raise CommandError(err)
3653 class cmd_user(SuperCommand):
3654 """User management."""
3656 subcommands = {}
3657 subcommands["add"] = cmd_user_add()
3658 subcommands["create"] = cmd_user_add()
3659 subcommands["delete"] = cmd_user_delete()
3660 subcommands["disable"] = cmd_user_disable()
3661 subcommands["enable"] = cmd_user_enable()
3662 subcommands["list"] = cmd_user_list()
3663 subcommands["setexpiry"] = cmd_user_setexpiry()
3664 subcommands["password"] = cmd_user_password()
3665 subcommands["getgroups"] = cmd_user_getgroups()
3666 subcommands["setprimarygroup"] = cmd_user_setprimarygroup()
3667 subcommands["setpassword"] = cmd_user_setpassword()
3668 subcommands["getpassword"] = cmd_user_getpassword()
3669 subcommands["syncpasswords"] = cmd_user_syncpasswords()
3670 subcommands["edit"] = cmd_user_edit()
3671 subcommands["show"] = cmd_user_show()
3672 subcommands["move"] = cmd_user_move()
3673 subcommands["rename"] = cmd_user_rename()
3674 subcommands["unlock"] = cmd_user_unlock()
3675 subcommands["addunixattrs"] = cmd_user_add_unix_attrs()
3676 subcommands["sensitive"] = cmd_user_sensitive()