import ldb
import pwd
import os
+import re
+import tempfile
+import difflib
import sys
import fcntl
import signal
import time
import base64
import binascii
-from subprocess import Popen, PIPE, STDOUT
+from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError
from getpass import getpass
from samba.auth import system_session
from samba.samdb import SamDB
gensec,
generate_random_password,
Ldb,
- )
+)
from samba.net import Net
from samba.netcmd import (
CommandError,
SuperCommand,
Option,
- )
-
+)
+from samba.compat import text_type
try:
import io
except ImportError as e:
gpgme_support = False
decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
- "python-gpgme required"
+ "python-gpgme required"
disabled_virtual_attributes = {
- }
+}
virtual_attributes = {
"virtualClearTextUTF8": {
"flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
- },
+ },
"virtualClearTextUTF16": {
"flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
- },
+ },
"virtualSambaGPG": {
"flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
- },
- }
+ },
+}
get_random_bytes_fn = None
if get_random_bytes_fn is None:
raise ImportError(random_reason)
return get_random_bytes_fn(num)
-def get_crypt_value(alg, utf8pw):
+def get_crypt_value(alg, utf8pw, rounds=0):
algs = {
"5": {"length": 43},
"6": {"length": 86},
# we can ignore the possible == at the end
# of the base64 string
# we just need to replace '+' by '.'
- b64salt = base64.b64encode(salt)
- crypt_salt = "$%s$%s$" % (alg, b64salt[0:16].replace('+', '.'))
+ b64salt = base64.b64encode(salt)[0:16].replace(b'+', b'.').decode('utf8')
+ crypt_salt = ""
+ if rounds != 0:
+ crypt_salt = "$%s$rounds=%s$%s$" % (alg, rounds, b64salt)
+ else:
+ crypt_salt = "$%s$%s$" % (alg, b64salt)
+
crypt_value = crypt.crypt(utf8pw, crypt_salt)
if crypt_value is None:
raise NotImplementedError("crypt.crypt(%s) returned None" % (crypt_salt))
crypt_salt, len(crypt_value), expected_len))
return crypt_value
+# Extract the rounds value from the options of a virtualCrypt attribute
+# i.e. options = "rounds=20;other=ignored;" will return 20
+# if the rounds option is not found or the value is not a number, 0 is returned
+# which indicates that the default number of rounds should be used.
+def get_rounds(options):
+ if not options:
+ return 0
+
+ opts = options.split(';')
+ for o in opts:
+ if o.lower().startswith("rounds="):
+ (key, _, val) = o.partition('=')
+ try:
+ return int(val)
+ except ValueError:
+ return 0
+ return 0
+
try:
random_reason = check_random()
if random_reason is not None:
h = hashlib.sha1()
h = None
virtual_attributes["virtualSSHA"] = {
- }
+ }
except ImportError as e:
reason = "hashlib.sha1()"
if random_reason:
reason += " and " + random_reason
reason += " required"
disabled_virtual_attributes["virtualSSHA"] = {
- "reason" : reason,
- }
+ "reason": reason,
+ }
for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]:
try:
v = get_crypt_value(alg, "")
v = None
virtual_attributes[attr] = {
- }
+ }
except ImportError as e:
reason = "crypt"
if random_reason:
reason += " and " + random_reason
reason += " required"
disabled_virtual_attributes[attr] = {
- "reason" : reason,
- }
+ "reason": reason,
+ }
except NotImplementedError as e:
reason = "modern '$%s$' salt in crypt(3) required" % (alg)
disabled_virtual_attributes[attr] = {
- "reason" : reason,
- }
+ "reason": reason,
+ }
+
+# Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29
+for x in range(1, 30):
+ virtual_attributes["virtualWDigest%02d" % x] = {}
virtual_attributes_help = "The attributes to display (comma separated). "
virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys()))
takes_options = [
Option("-H", "--URL", help="LDB URL for database or target server", type=str,
- metavar="URL", dest="H"),
+ metavar="URL", dest="H"),
Option("--must-change-at-next-login",
- help="Force password to be changed on next login",
- action="store_true"),
+ help="Force password to be changed on next login",
+ action="store_true"),
Option("--random-password",
- help="Generate random password",
- action="store_true"),
+ help="Generate random password",
+ action="store_true"),
Option("--smartcard-required",
- help="Require a smartcard for interactive logons",
- action="store_true"),
+ help="Require a smartcard for interactive logons",
+ action="store_true"),
Option("--use-username-as-cn",
- help="Force use of username as user's CN",
- action="store_true"),
+ help="Force use of username as user's CN",
+ action="store_true"),
Option("--userou",
- 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>'",
- type=str),
+ 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>'",
+ type=str),
Option("--surname", help="User's surname", type=str),
Option("--given-name", help="User's given name", type=str),
Option("--initials", help="User's initials", type=str),
Option("--telephone-number", help="User's phone number", type=str),
Option("--physical-delivery-office", help="User's office location", type=str),
Option("--rfc2307-from-nss",
- help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
- action="store_true"),
+ help="Copy Unix user attributes from NSS (will be overridden by explicit UID/GID/GECOS/shell)",
+ action="store_true"),
Option("--nis-domain", help="User's Unix/RFC2307 NIS domain", type=str),
Option("--unix-home", help="User's Unix/RFC2307 home directory",
- type=str),
+ type=str),
Option("--uid", help="User's Unix/RFC2307 username", type=str),
Option("--uid-number", help="User's Unix/RFC2307 numeric UID", type=int),
Option("--gid-number", help="User's Unix/RFC2307 primary GID number", type=int),
"sambaopts": options.SambaOptions,
"credopts": options.CredentialsOptions,
"versionopts": options.VersionOptions,
- }
+ }
def run(self, username, password=None, credopts=None, sambaopts=None,
versionopts=None, H=None, must_change_at_next_login=False,
if smartcard_required:
if password is not None and password is not '':
- raise CommandError('It is not allowed to specifiy '
+ raise CommandError('It is not allowed to specify '
'--newpassword '
'together with --smartcard-required.')
if must_change_at_next_login:
- raise CommandError('It is not allowed to specifiy '
+ raise CommandError('It is not allowed to specify '
'--must-change-at-next-login '
'together with --smartcard-required.')
uidnumber=uid_number, gidnumber=gid_number,
gecos=gecos, loginshell=login_shell,
smartcard_required=smartcard_required)
- except Exception, e:
+ except Exception as e:
raise CommandError("Failed to add user '%s': " % username, e)
self.outf.write("User '%s' created successfully\n" % username)
"sambaopts": options.SambaOptions,
"credopts": options.CredentialsOptions,
"versionopts": options.VersionOptions,
- }
+ }
def run(self, username, credopts=None, sambaopts=None, versionopts=None,
H=None):
credentials=creds, lp=lp)
filter = ("(&(sAMAccountName=%s)(sAMAccountType=805306368))" %
- username)
+ ldb.binary_encode(username))
try:
res = samdb.search(base=samdb.domain_dn(),
try:
samdb.delete(user_dn)
- except Exception, e:
+ except Exception as e:
raise CommandError('Failed to remove user "%s"' % username, e)
self.outf.write("Deleted user %s\n" % username)
takes_options = [
Option("-H", "--URL", help="LDB URL for database or target server", type=str,
metavar="URL", dest="H"),
- ]
+ ]
takes_optiongroups = {
"sambaopts": options.SambaOptions,
"credopts": options.CredentialsOptions,
"versionopts": options.VersionOptions,
- }
+ }
def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
lp = sambaopts.get_loadparm()
creds = credopts.get_credentials(lp, fallback_machine=True)
samdb = SamDB(url=H, session_info=system_session(),
- credentials=creds, lp=lp)
+ credentials=creds, lp=lp)
domain_dn = samdb.domain_dn()
res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
- expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
- % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
- attrs=["samaccountname"])
+ expression=("(&(objectClass=user)(userAccountControl:%s:=%u))"
+ % (ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT)),
+ attrs=["samaccountname"])
if (len(res) == 0):
return
class cmd_user_enable(Command):
- """Enable an user.
+ """Enable a user.
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.
Option("-H", "--URL", help="LDB URL for database or target server", type=str,
metavar="URL", dest="H"),
Option("--filter", help="LDAP Filter to set password on", type=str),
- ]
+ ]
takes_args = ["username?"]
creds = credopts.get_credentials(lp, fallback_machine=True)
samdb = SamDB(url=H, session_info=system_session(),
- credentials=creds, lp=lp)
+ credentials=creds, lp=lp)
try:
samdb.enable_account(filter)
- except Exception, msg:
+ except Exception as msg:
raise CommandError("Failed to enable user '%s': %s" % (username or filter, msg))
self.outf.write("Enabled user '%s'\n" % (username or filter))
class cmd_user_disable(Command):
- """Disable an user."""
+ """Disable a user."""
synopsis = "%prog (<username>|--filter <filter>) [options]"
Option("-H", "--URL", help="LDB URL for database or target server", type=str,
metavar="URL", dest="H"),
Option("--filter", help="LDAP Filter to set password on", type=str),
- ]
+ ]
takes_args = ["username?"]
"sambaopts": options.SambaOptions,
"credopts": options.CredentialsOptions,
"versionopts": options.VersionOptions,
- }
+ }
def run(self, username=None, sambaopts=None, credopts=None,
versionopts=None, filter=None, H=None):
creds = credopts.get_credentials(lp, fallback_machine=True)
samdb = SamDB(url=H, session_info=system_session(),
- credentials=creds, lp=lp)
+ credentials=creds, lp=lp)
try:
samdb.disable_account(filter)
- except Exception, msg:
+ except Exception as msg:
raise CommandError("Failed to disable user '%s': %s" % (username or filter, msg))
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.
Example2:
-su samba-tool user setexpiry User2
+sudo samba-tool user setexpiry User2 --noexpiry
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.
creds = credopts.get_credentials(lp)
samdb = SamDB(url=H, session_info=system_session(),
- credentials=creds, lp=lp)
+ credentials=creds, lp=lp)
try:
samdb.setexpiry(filter, days*24*3600, no_expiry_req=noexpiry)
- except Exception, msg:
+ except Exception as msg:
# FIXME: Catch more specific exception
raise CommandError("Failed to set expiry for user '%s': %s" % (
username or filter, msg))
takes_options = [
Option("--newpassword", help="New password", type=str),
- ]
+ ]
takes_optiongroups = {
"sambaopts": options.SambaOptions,
"credopts": options.CredentialsOptions,
"versionopts": options.VersionOptions,
- }
+ }
def run(self, credopts=None, sambaopts=None, versionopts=None,
- newpassword=None):
+ newpassword=None):
lp = sambaopts.get_loadparm()
creds = credopts.get_credentials(lp)
self.outf.write("Sorry, passwords do not match.\n")
try:
+ if not isinstance(password, text_type):
+ password = password.decode('utf8')
net.change_password(password)
- except Exception, msg:
+ except Exception as msg:
# FIXME: catch more specific exception
raise CommandError("Failed to change password : %s" % msg)
self.outf.write("Changed password OK\n")
help="Force password to be changed on next login",
action="store_true"),
Option("--random-password",
- help="Generate random password",
- action="store_true"),
+ help="Generate random password",
+ action="store_true"),
Option("--smartcard-required",
- help="Require a smartcard for interactive logons",
- action="store_true"),
+ help="Require a smartcard for interactive logons",
+ action="store_true"),
Option("--clear-smartcard-required",
- help="Don't require a smartcard for interactive logons",
- action="store_true"),
- ]
+ help="Don't require a smartcard for interactive logons",
+ action="store_true"),
+ ]
takes_args = ["username?"]
if smartcard_required:
if password is not None and password is not '':
- raise CommandError('It is not allowed to specifiy '
+ raise CommandError('It is not allowed to specify '
'--newpassword '
'together with --smartcard-required.')
if must_change_at_next_login:
- raise CommandError('It is not allowed to specifiy '
+ raise CommandError('It is not allowed to specify '
'--must-change-at-next-login '
'together with --smartcard-required.')
if clear_smartcard_required:
- raise CommandError('It is not allowed to specifiy '
+ raise CommandError('It is not allowed to specify '
'--clear-smartcard-required '
'together with --smartcard-required.')
samdb.toggle_userAccountFlags(filter, flags, on=True)
command = "Failed to enable account for user '%s'" % (username or filter)
samdb.enable_account(filter)
- except Exception, msg:
+ except Exception as msg:
# FIXME: catch more specific exception
raise CommandError("%s: %s" % (command, msg))
self.outf.write("Added UF_SMARTCARD_REQUIRED OK\n")
samdb.setpassword(filter, password,
force_change_at_next_login=must_change_at_next_login,
username=username)
- except Exception, msg:
+ except Exception as msg:
# FIXME: catch more specific exception
raise CommandError("%s: %s" % (command, msg))
self.outf.write("Changed password OK\n")
def get_account_attributes(self, samdb, username, basedn, filter, scope,
attrs, decrypt):
- require_supplementalCredentials = False
- search_attrs = attrs[:]
+ raw_attrs = attrs[:]
+ search_attrs = []
+ attr_opts = {}
+ for a in raw_attrs:
+ (attr, _, opts) = a.partition(';')
+ if opts:
+ attr_opts[attr] = opts
+ else:
+ attr_opts[attr] = None
+ search_attrs.append(attr)
lower_attrs = [x.lower() for x in search_attrs]
+
+ require_supplementalCredentials = False
for a in virtual_attributes.keys():
if a.lower() in lower_attrs:
require_supplementalCredentials = True
search_attrs += [a]
add_sAMAcountName = True
+ add_userPrincipalName = False
+ upn = "usePrincipalName"
+ if upn.lower() not in lower_attrs:
+ search_attrs += [upn]
+ add_userPrincipalName = True
+
if scope == ldb.SCOPE_BASE:
search_controls = ["show_deleted:1", "show_recycled:1"]
else:
account_name = obj["sAMAccountName"][0]
if add_sAMAcountName:
del obj["sAMAccountName"]
+ if "userPrincipalName" in obj:
+ account_upn = obj["userPrincipalName"][0]
+ else:
+ realm = self.lp.get("realm")
+ account_upn = "%s@%s" % (account_name, realm.lower())
+ if add_userPrincipalName:
+ del obj["userPrincipalName"]
calculated = {}
def get_package(name, min_idx=0):
nthash = tmp.get_nt_hash()
if nthash == unicodePwd:
calculated["Primary:CLEARTEXT"] = cv
- except gpgme.GpgmeError as (major, minor, msg):
+ except gpgme.GpgmeError as e1:
+ (major, minor, msg) = e1.args
if major == gpgme.ERR_BAD_SECKEY:
msg = "ERR_BAD_SECKEY: " + msg
else:
u8 = u.encode('utf-8')
return u8
+ # Extract the WDigest hash for the value specified by i.
+ # Builds an htdigest compatible value
+ DIGEST = "Digest"
+ def get_wDigest(i, primary_wdigest, account_name, account_upn,
+ domain, dns_domain):
+ if i == 1:
+ user = account_name
+ realm = domain
+ elif i == 2:
+ user = account_name.lower()
+ realm = domain.lower()
+ elif i == 3:
+ user = account_name.upper()
+ realm = domain.upper()
+ elif i == 4:
+ user = account_name
+ realm = domain.upper()
+ elif i == 5:
+ user = account_name
+ realm = domain.lower()
+ elif i == 6:
+ user = account_name.upper()
+ realm = domain.lower()
+ elif i == 7:
+ user = account_name.lower()
+ realm = domain.upper()
+ elif i == 8:
+ user = account_name
+ realm = dns_domain.lower()
+ elif i == 9:
+ user = account_name.lower()
+ realm = dns_domain.lower()
+ elif i == 10:
+ user = account_name.upper()
+ realm = dns_domain.upper()
+ elif i == 11:
+ user = account_name
+ realm = dns_domain.upper()
+ elif i == 12:
+ user = account_name
+ realm = dns_domain.lower()
+ elif i == 13:
+ user = account_name.upper()
+ realm = dns_domain.lower()
+ elif i == 14:
+ user = account_name.lower()
+ realm = dns_domain.upper()
+ elif i == 15:
+ user = account_upn
+ realm = ""
+ elif i == 16:
+ user = account_upn.lower()
+ realm = ""
+ elif i == 17:
+ user = account_upn.upper()
+ realm = ""
+ elif i == 18:
+ user = "%s\\%s" % (domain, account_name)
+ realm = ""
+ elif i == 19:
+ user = "%s\\%s" % (domain.lower(), account_name.lower())
+ realm = ""
+ elif i == 20:
+ user = "%s\\%s" % (domain.upper(), account_name.upper())
+ realm = ""
+ elif i == 21:
+ user = account_name
+ realm = DIGEST
+ elif i == 22:
+ user = account_name.lower()
+ realm = DIGEST
+ elif i == 23:
+ user = account_name.upper()
+ realm = DIGEST
+ elif i == 24:
+ user = account_upn
+ realm = DIGEST
+ elif i == 25:
+ user = account_upn.lower()
+ realm = DIGEST
+ elif i == 26:
+ user = account_upn.upper()
+ realm = DIGEST
+ elif i == 27:
+ user = "%s\\%s" % (domain, account_name)
+ realm = DIGEST
+ elif i == 28:
+ # Differs from spec, see tests
+ user = "%s\\%s" % (domain.lower(), account_name.lower())
+ realm = DIGEST
+ elif i == 29:
+ # Differs from spec, see tests
+ user = "%s\\%s" % (domain.upper(), account_name.upper())
+ realm = DIGEST
+ else:
+ user = ""
+
+ digests = ndr_unpack(drsblobs.package_PrimaryWDigestBlob,
+ primary_wdigest)
+ try:
+ digest = binascii.hexlify(bytearray(digests.hashes[i-1].hash))
+ return "%s:%s:%s" % (user, realm, digest)
+ except IndexError:
+ return None
+
+
+ # get the value for a virtualCrypt attribute.
+ # look for an exact match on algorithm and rounds in supplemental creds
+ # if not found calculate using Primary:CLEARTEXT
+ # if no Primary:CLEARTEXT return the first supplementalCredential
+ # that matches the algorithm.
+ def get_virtual_crypt_value(a, algorithm, rounds, username, account_name):
+ sv = None
+ fb = None
+ b = get_package("Primary:userPassword")
+ if b is not None:
+ (sv, fb) = get_userPassword_hash(b, algorithm, rounds)
+ if sv is None:
+ # No exact match on algorithm and number of rounds
+ # try and calculate one from the Primary:CLEARTEXT
+ b = get_package("Primary:CLEARTEXT")
+ if b is not None:
+ u8 = get_utf8(a, b, username or account_name)
+ if u8 is not None:
+ sv = get_crypt_value(str(algorithm), u8, rounds)
+ if sv is None:
+ # Unable to calculate a hash with the specified
+ # number of rounds, fall back to the first hash using
+ # the specified algorithm
+ sv = fb
+ if sv is None:
+ return None
+ return "{CRYPT}" + sv
+
+ def get_userPassword_hash(blob, algorithm, rounds):
+ up = ndr_unpack(drsblobs.package_PrimaryUserPasswordBlob, blob)
+ SCHEME = "{CRYPT}"
+
+ # Check that the NT hash has not been changed without updating
+ # the user password hashes. This indicates that password has been
+ # changed without updating the supplemental credentials.
+ if unicodePwd != bytearray(up.current_nt_hash.hash):
+ return None
+
+ scheme_prefix = "$%d$" % algorithm
+ prefix = scheme_prefix
+ if rounds > 0:
+ prefix = "$%d$rounds=%d" % (algorithm, rounds)
+ scheme_match = None
+
+ for h in up.hashes:
+ if (scheme_match is None and
+ h.scheme == SCHEME and
+ h.value.startswith(scheme_prefix)):
+ scheme_match = h.value
+ if h.scheme == SCHEME and h.value.startswith(prefix):
+ return (h.value, scheme_match)
+
+ # No match on the number of rounds, return the value of the
+ # first matching scheme
+ return (None, scheme_match)
+
# We use sort here in order to have a predictable processing order
for a in sorted(virtual_attributes.keys()):
if not a.lower() in lower_attrs:
h.update(u8)
h.update(salt)
bv = h.digest() + salt
- v = "{SSHA}" + base64.b64encode(bv)
+ v = "{SSHA}" + base64.b64encode(bv).decode('utf8')
elif a == "virtualCryptSHA256":
- b = get_package("Primary:CLEARTEXT")
- if b is None:
- continue
- u8 = get_utf8(a, b, username or account_name)
- if u8 is None:
+ rounds = get_rounds(attr_opts[a])
+ x = get_virtual_crypt_value(a, 5, rounds, username, account_name)
+ if x is None:
continue
- sv = get_crypt_value("5", u8)
- v = "{CRYPT}" + sv
+ v = x
elif a == "virtualCryptSHA512":
- b = get_package("Primary:CLEARTEXT")
- if b is None:
+ rounds = get_rounds(attr_opts[a])
+ x = get_virtual_crypt_value(a, 6, rounds, username, account_name)
+ if x is None:
continue
- u8 = get_utf8(a, b, username or account_name)
- if u8 is None:
- continue
- sv = get_crypt_value("6", u8)
- v = "{CRYPT}" + sv
+ v = x
elif a == "virtualSambaGPG":
# Samba adds 'Primary:SambaGPG' at the end.
# When Windows sets the password it keeps
v = get_package("Primary:SambaGPG", min_idx=-1)
if v is None:
continue
+ elif a.startswith("virtualWDigest"):
+ primary_wdigest = get_package("Primary:WDigest")
+ if primary_wdigest is None:
+ continue
+ x = a[len("virtualWDigest"):]
+ try:
+ i = int(x)
+ except ValueError:
+ continue
+ domain = self.lp.get("workgroup")
+ dns_domain = samdb.domain_dns_name()
+ v = get_wDigest(i, primary_wdigest, account_name, account_upn, domain, dns_domain)
+ if v is None:
+ continue
else:
continue
obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
with a $5$... salt, see crypt(3) on modern systems.
+ The number of rounds used to calculate the hash can
+ also be specified. By appending ";rounds=x" to the
+ attribute name i.e. virtualCryptSHA256;rounds=10000
+ will calculate a SHA256 hash with 10,000 rounds.
+ non numeric values for rounds are silently ignored
+ The value is calculated as follows:
+ 1) If a value exists in 'Primary:userPassword' with
+ the specified number of rounds it is returned.
+ 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
+ '--decrypt-samba-gpg'. Calculate a hash with
+ the specified number of rounds
+ 3) Return the first CryptSHA256 value in
+ 'Primary:userPassword'
+
virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
with a $6$... salt, see crypt(3) on modern systems.
+ The number of rounds used to calculate the hash can
+ also be specified. By appending ";rounds=x" to the
+ attribute name i.e. virtualCryptSHA512;rounds=10000
+ will calculate a SHA512 hash with 10,000 rounds.
+ non numeric values for rounds are silently ignored
+ The value is calculated as follows:
+ 1) If a value exists in 'Primary:userPassword' with
+ the specified number of rounds it is returned.
+ 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
+ '--decrypt-samba-gpg'. Calculate a hash with
+ the specified number of rounds
+ 3) Return the first CryptSHA512 value in
+ 'Primary:userPassword'
+
+ virtualWDigestNN: The individual hash values stored in
+ 'Primary:WDigest' where NN is the hash number in
+ the range 01 to 29.
+ NOTE: As at 22-05-2017 the documentation:
+ 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
+ https://msdn.microsoft.com/en-us/library/cc245680.aspx
+ is incorrect
virtualSambaGPG: The raw cleartext as stored in the
'Primary:SambaGPG' buffer inside of the
Option("--decrypt-samba-gpg",
help=decrypt_samba_gpg_help,
action="store_true", default=False, dest="decrypt_samba_gpg"),
- ]
+ ]
takes_args = ["username?"]
virtualCryptSHA256: As virtualClearTextUTF8, but a salted SHA256
checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
with a $5$... salt, see crypt(3) on modern systems.
+ The number of rounds used to calculate the hash can
+ also be specified. By appending ";rounds=x" to the
+ attribute name i.e. virtualCryptSHA256;rounds=10000
+ will calculate a SHA256 hash with 10,000 rounds.
+ non numeric values for rounds are silently ignored
+ The value is calculated as follows:
+ 1) If a value exists in 'Primary:userPassword' with
+ the specified number of rounds it is returned.
+ 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
+ '--decrypt-samba-gpg'. Calculate a hash with
+ the specified number of rounds
+ 3) Return the first CryptSHA256 value in
+ 'Primary:userPassword'
virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
with a $6$... salt, see crypt(3) on modern systems.
+ The number of rounds used to calculate the hash can
+ also be specified. By appending ";rounds=x" to the
+ attribute name i.e. virtualCryptSHA512;rounds=10000
+ will calculate a SHA512 hash with 10,000 rounds.
+ non numeric values for rounds are silently ignored
+ The value is calculated as follows:
+ 1) If a value exists in 'Primary:userPassword' with
+ the specified number of rounds it is returned.
+ 2) If 'Primary:CLEARTEXT, or 'Primary:SambaGPG' with
+ '--decrypt-samba-gpg'. Calculate a hash with
+ the specified number of rounds
+ 3) Return the first CryptSHA512 value in
+ 'Primary:userPassword'
+
+ virtualWDigestNN: The individual hash values stored in
+ 'Primary:WDigest' where NN is the hash number in
+ the range 01 to 29.
+ NOTE: As at 22-05-2017 the documentation:
+ 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction
+ https://msdn.microsoft.com/en-us/library/cc245680.aspx
+ is incorrect.
virtualSambaGPG: The raw cleartext as stored in the
'Primary:SambaGPG' buffer inside of the
single line starting with 'DONE-EXIT: ' followed by an optional message.
Note that the script might be called without any password change, e.g. if
-the account was disabled (an userAccountControl change) or the
+the account was disabled (a userAccountControl change) or the
sAMAccountName was changed. The objectGUID,isDeleted,isRecycled attributes
are always returned as unique identifier of the account. It might be useful
to also ask for non-password attributes like: objectSid, sAMAccountName,
userPrincipalName and userAccountControl.
It recovers from LDAP disconnects and updates the cache in conservative way
-(in single steps after each succesfully processed change). An error from
+(in single steps after each successfully processed change). An error from
the script (specified by '--script') will result in fatal error and this
command will exit. But the cache state should be still valid and can be
resumed in the next "Sync Loop Run".
Option("--terminate",
help="Send a SIGTERM to an already running (daemon) process",
action="store_true", default=False, dest="terminate"),
- ]
+ ]
def run(self, cache_ldb_initialize=False, cache_ldb=None,
H=None, filter=None,
dirsync_filter = "(&" + \
"(objectClass=user)" + \
"(userAccountControl:%s:=%u)" % (
- ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
+ ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \
"(!(sAMAccountName=krbtgt*))" + \
")"
logfile = self.logfile
self.logfile = None
log_msg("Closing logfile[%s] (st_nlink == 0)\n" % (logfile))
- logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
+ logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
os.dup2(logfd, 0)
os.dup2(logfd, 1)
os.dup2(logfd, 2)
self.sync_command = sync_command
add_ldif = "dn: %s\n" % self.cache_dn
add_ldif += "objectClass: userSyncPasswords\n"
- add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url)
- add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter)
+ add_ldif += "samdbUrl:: %s\n" % base64.b64encode(self.samdb_url).decode('utf8')
+ add_ldif += "dirsyncFilter:: %s\n" % base64.b64encode(self.dirsync_filter).decode('utf8')
for a in self.dirsync_attrs:
- add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a)
+ add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a).decode('utf8')
add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
for a in self.password_attrs:
- add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
+ add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a).decode('utf8')
if self.decrypt_samba_gpg == True:
add_ldif += "decryptSambaGPG: TRUE\n"
else:
self.current_pid = None
self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb))
msgs = self.cache.parse_ldif(add_ldif)
- changetype,msg = msgs.next()
+ changetype,msg = next(msgs)
ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
self.outf.write("%s" % ldif)
else:
return
def run_sync_command(dn, ldif):
- log_msg("Call Popen[%s] for %s\n" % (dn, self.sync_command))
+ log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn))
sync_command_p = Popen(self.sync_command,
stdin=PIPE,
stdout=PIPE,
del dirsync_obj[a]
dirsync_obj["# %s::" % a] = ["REDACTED SECRET ATTRIBUTE"]
dirsync_ldif = self.samdb.write_ldif(dirsync_obj, ldb.CHANGETYPE_NONE)
- log_msg("# Dirsync[%d] %s %s\n%s" %(idx, guid, sid, dirsync_ldif))
+ log_msg("# Dirsync[%d] %s %s\n%s" % (idx, guid, sid, dirsync_ldif))
obj = self.get_account_attributes(self.samdb,
username="%s" % sid,
basedn="<GUID=%s>" % guid,
flags |= os.O_CREAT
try:
- self.lockfd = os.open(self.lockfile, flags, 0600)
- except IOError as (err, msg):
+ self.lockfd = os.open(self.lockfile, flags, 0o600)
+ except IOError as e4:
+ (err, msg) = e4.args
if err == errno.ENOENT:
if terminate:
return False
try:
fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
got_exclusive = True
- except IOError as (err, msg):
+ except IOError as e5:
+ (err, msg) = e5.args
if err != errno.EACCES and err != errno.EAGAIN:
log_msg("check_current_pid_conflict: failed to get exclusive lock[%s] - %s (%d)" %
(self.lockfile, msg, err))
if got_exclusive and terminate:
try:
os.ftruncate(self.lockfd, 0)
- except IOError as (err, msg):
+ except IOError as e2:
+ (err, msg) = e2.args
log_msg("check_current_pid_conflict: failed to truncate [%s] - %s (%d)" %
(self.lockfile, msg, err))
raise
try:
fcntl.lockf(self.lockfd, fcntl.LOCK_SH)
- except IOError as (err, msg):
+ except IOError as e6:
+ (err, msg) = e6.args
log_msg("check_current_pid_conflict: failed to get shared lock[%s] - %s (%d)" %
(self.lockfile, msg, err))
if self.lockfd != -1:
got_exclusive = False
# Try 5 times to get the exclusiv lock.
- for i in xrange(0, 5):
+ for i in range(0, 5):
try:
fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
got_exclusive = True
- except IOError as (err, msg):
+ except IOError as e:
+ (err, msg) = e.args
if err != errno.EACCES and err != errno.EAGAIN:
log_msg("update_pid(%r): failed to get exclusive lock[%s] - %s (%d)" %
(pid, self.lockfile, msg, err))
os.ftruncate(self.lockfd, 0)
if buf is not None:
os.write(self.lockfd, buf)
- except IOError as (err, msg):
+ except IOError as e3:
+ (err, msg) = e3.args
log_msg("check_current_pid_conflict: failed to write pid to [%s] - %s (%d)" %
(self.lockfile, msg, err))
raise
if self.current_pid is not None:
log_msg("currentPid: %d\n" % self.current_pid)
- modify_ldif = "dn: %s\n" % (self.cache_dn)
+ modify_ldif = "dn: %s\n" % (self.cache_dn)
modify_ldif += "changetype: modify\n"
modify_ldif += "replace: currentPid\n"
if self.current_pid is not None:
self.dirsync_controls = [str(res_controls[0]),"extended_dn:1:0"]
log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
- modify_ldif = "dn: %s\n" % (self.cache_dn)
+ modify_ldif = "dn: %s\n" % (self.cache_dn)
modify_ldif += "changetype: modify\n"
modify_ldif += "replace: dirsyncControl\n"
modify_ldif += "dirsyncControl: %s\n" % (self.dirsync_controls[0])
add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time()))
self.cache.add_ldif(add_ldif)
else:
- modify_ldif = "dn: %s\n" % (dn)
+ modify_ldif = "dn: %s\n" % (dn)
modify_ldif += "changetype: modify\n"
modify_ldif += "replace: lastCookie\n"
modify_ldif += "lastCookie: %s\n" % (lastCookie)
def sync_loop(wait):
notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"]
- notify_controls = ["notification:1"]
+ notify_controls = ["notification:1", "show_recycled:1"]
notify_handle = self.samdb.search_iterator(expression="objectClass=*",
scope=ldb.SCOPE_SUBTREE,
attrs=notify_attrs,
maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
if maxfd == resource.RLIM_INFINITY:
maxfd = 1024 # Rough guess at maximum number of open file descriptors.
- logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0600)
+ logfd = os.open(logfile, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600)
self.outf.write("Using logfile[%s]\n" % logfile)
for fd in range(0, maxfd):
if fd == logfd:
try:
sync_loop(wait)
- except ldb.LdbError as (enum, estr):
+ except ldb.LdbError as e7:
+ (enum, estr) = e7.args
self.samdb = None
log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
update_pid(None)
return
+class cmd_user_edit(Command):
+ """Modify User AD object.
+
+This command will allow editing of a user account in the Active Directory
+domain. You will then be able to add or change attributes and their values.
+
+The username specified on the command is the sAMAccountName.
+
+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.
+
+Example1:
+samba-tool user edit User1 -H ldap://samba.samdom.example.com \
+-U administrator --password=passw1rd
+
+Example1 shows how to edit a users attributes in the domain against a remote
+LDAP server.
+
+The -H parameter is used to specify the remote target server.
+
+Example2:
+samba-tool user edit User2
+
+Example2 shows how to edit a users attributes in the domain against a local
+LDAP server.
+
+Example3:
+samba-tool user edit User3 --editor=nano
+
+Example3 shows how to edit a users attributes in the domain against a local
+LDAP server using the 'nano' editor.
+
+"""
+ synopsis = "%prog <username> [options]"
+
+ takes_options = [
+ Option("-H", "--URL", help="LDB URL for database or target server",
+ type=str, metavar="URL", dest="H"),
+ Option("--editor", help="Editor to use instead of the system default,"
+ " or 'vi' if no system default is set.", type=str),
+ ]
+
+ takes_args = ["username"]
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "versionopts": options.VersionOptions,
+ }
+
+ def run(self, username, credopts=None, sambaopts=None, versionopts=None,
+ H=None, editor=None):
+
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp, fallback_machine=True)
+ samdb = SamDB(url=H, session_info=system_session(),
+ credentials=creds, lp=lp)
+
+ filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
+ (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
+
+ domaindn = samdb.domain_dn()
+
+ try:
+ res = samdb.search(base=domaindn,
+ expression=filter,
+ scope=ldb.SCOPE_SUBTREE)
+ user_dn = res[0].dn
+ except IndexError:
+ raise CommandError('Unable to find user "%s"' % (username))
+
+ for msg in res:
+ r_ldif = samdb.write_ldif(msg, 1)
+ # remove 'changetype' line
+ result_ldif = re.sub('changetype: add\n', '', r_ldif)
+
+ if editor is None:
+ editor = os.environ.get('EDITOR')
+ if editor is None:
+ editor = 'vi'
+
+ with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
+ t_file.write(result_ldif)
+ t_file.flush()
+ try:
+ check_call([editor, t_file.name])
+ except CalledProcessError as e:
+ raise CalledProcessError("ERROR: ", e)
+ with open(t_file.name) as edited_file:
+ edited_message = edited_file.read()
+
+ if result_ldif != edited_message:
+ diff = difflib.ndiff(result_ldif.splitlines(),
+ edited_message.splitlines())
+ minus_lines = []
+ plus_lines = []
+ for line in diff:
+ if line.startswith('-'):
+ line = line[2:]
+ minus_lines.append(line)
+ elif line.startswith('+'):
+ line = line[2:]
+ plus_lines.append(line)
+
+ user_ldif = "dn: %s\n" % user_dn
+ user_ldif += "changetype: modify\n"
+
+ for line in minus_lines:
+ attr, val = line.split(':', 1)
+ search_attr = "%s:" % attr
+ if not re.search(r'^' + search_attr, str(plus_lines)):
+ user_ldif += "delete: %s\n" % attr
+ user_ldif += "%s: %s\n" % (attr, val)
+
+ for line in plus_lines:
+ attr, val = line.split(':', 1)
+ search_attr = "%s:" % attr
+ if re.search(r'^' + search_attr, str(minus_lines)):
+ user_ldif += "replace: %s\n" % attr
+ user_ldif += "%s: %s\n" % (attr, val)
+ if not re.search(r'^' + search_attr, str(minus_lines)):
+ user_ldif += "add: %s\n" % attr
+ user_ldif += "%s: %s\n" % (attr, val)
+
+ try:
+ samdb.modify_ldif(user_ldif)
+ except Exception as e:
+ raise CommandError("Failed to modify user '%s': " %
+ username, e)
+
+ self.outf.write("Modified User '%s' successfully\n" % username)
+
+class cmd_user_show(Command):
+ """Display a user AD object.
+
+This command displays a user account and it's attributes in the Active
+Directory domain.
+The username specified on the command is the sAMAccountName.
+
+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.
+
+Example1:
+samba-tool user show User1 -H ldap://samba.samdom.example.com \
+-U administrator --password=passw1rd
+
+Example1 shows how to display a users attributes in the domain against a remote
+LDAP server.
+
+The -H parameter is used to specify the remote target server.
+
+Example2:
+samba-tool user show User2
+
+Example2 shows how to display a users attributes in the domain against a local
+LDAP server.
+
+Example3:
+samba-tool user show User2 --attributes=objectSid,memberOf
+
+Example3 shows how to display a users objectSid and memberOf attributes.
+"""
+ synopsis = "%prog <username> [options]"
+
+ takes_options = [
+ Option("-H", "--URL", help="LDB URL for database or target server",
+ type=str, metavar="URL", dest="H"),
+ Option("--attributes",
+ help=("Comma separated list of attributes, "
+ "which will be printed."),
+ type=str, dest="user_attrs"),
+ ]
+
+ takes_args = ["username"]
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "versionopts": options.VersionOptions,
+ }
+
+ def run(self, username, credopts=None, sambaopts=None, versionopts=None,
+ H=None, user_attrs=None):
+
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp, fallback_machine=True)
+ samdb = SamDB(url=H, session_info=system_session(),
+ credentials=creds, lp=lp)
+
+ attrs = None
+ if user_attrs:
+ attrs = user_attrs.split(",")
+
+ filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
+ (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
+
+ domaindn = samdb.domain_dn()
+
+ try:
+ res = samdb.search(base=domaindn, expression=filter,
+ scope=ldb.SCOPE_SUBTREE, attrs=attrs)
+ user_dn = res[0].dn
+ except IndexError:
+ raise CommandError('Unable to find user "%s"' % (username))
+
+ for msg in res:
+ user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
+ self.outf.write(user_ldif)
+
+class cmd_user_move(Command):
+ """Move a user to an organizational unit/container.
+
+ This command moves a user account into the specified organizational unit
+ or container.
+ The username specified on the command is the sAMAccountName.
+ The name of the organizational unit or container can be specified as a
+ full DN or without the domainDN component.
+
+ 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.
+
+ Example1:
+ samba-tool user move User1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
+ -H ldap://samba.samdom.example.com -U administrator
+
+ Example1 shows how to move a user User1 into the 'OrgUnit' organizational
+ unit on a remote LDAP server.
+
+ The -H parameter is used to specify the remote target server.
+
+ Example2:
+ samba-tool user move User1 CN=Users
+
+ Example2 shows how to move a user User1 back into the CN=Users container
+ on the local server.
+ """
+
+ synopsis = "%prog <username> <new_parent_dn> [options]"
+
+ takes_options = [
+ Option("-H", "--URL", help="LDB URL for database or target server",
+ type=str, metavar="URL", dest="H"),
+ ]
+
+ takes_args = ["username", "new_parent_dn"]
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "versionopts": options.VersionOptions,
+ }
+
+ def run(self, username, new_parent_dn, credopts=None, sambaopts=None,
+ versionopts=None, H=None):
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp, fallback_machine=True)
+ samdb = SamDB(url=H, session_info=system_session(),
+ credentials=creds, lp=lp)
+ domain_dn = ldb.Dn(samdb, samdb.domain_dn())
+
+ filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
+ (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username)))
+ try:
+ res = samdb.search(base=domain_dn,
+ expression=filter,
+ scope=ldb.SCOPE_SUBTREE)
+ user_dn = res[0].dn
+ except IndexError:
+ raise CommandError('Unable to find user "%s"' % (username))
+
+ try:
+ full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
+ except Exception as e:
+ raise CommandError('Invalid new_parent_dn "%s": %s' %
+ (new_parent_dn, e.message))
+
+ full_new_user_dn = ldb.Dn(samdb, str(user_dn))
+ full_new_user_dn.remove_base_components(len(user_dn)-1)
+ full_new_user_dn.add_base(full_new_parent_dn)
+
+ try:
+ samdb.rename(user_dn, full_new_user_dn)
+ except Exception as e:
+ raise CommandError('Failed to move user "%s"' % username, e)
+ self.outf.write('Moved user "%s" into "%s"\n' %
+ (username, full_new_parent_dn))
+
class cmd_user(SuperCommand):
"""User management."""
subcommands["setpassword"] = cmd_user_setpassword()
subcommands["getpassword"] = cmd_user_getpassword()
subcommands["syncpasswords"] = cmd_user_syncpasswords()
+ subcommands["edit"] = cmd_user_edit()
+ subcommands["show"] = cmd_user_show()
+ subcommands["move"] = cmd_user_move()