diff options
Diffstat (limited to 'python/samba/netcmd/user/readpasswords')
-rw-r--r-- | python/samba/netcmd/user/readpasswords/__init__.py | 25 | ||||
-rw-r--r-- | python/samba/netcmd/user/readpasswords/common.py | 907 | ||||
-rw-r--r-- | python/samba/netcmd/user/readpasswords/get_kerberos_ticket.py | 146 | ||||
-rw-r--r-- | python/samba/netcmd/user/readpasswords/getpassword.py | 210 | ||||
-rw-r--r-- | python/samba/netcmd/user/readpasswords/show.py | 144 | ||||
-rw-r--r-- | python/samba/netcmd/user/readpasswords/syncpasswords.py | 878 |
6 files changed, 2310 insertions, 0 deletions
diff --git a/python/samba/netcmd/user/readpasswords/__init__.py b/python/samba/netcmd/user/readpasswords/__init__.py new file mode 100644 index 0000000..75ba313 --- /dev/null +++ b/python/samba/netcmd/user/readpasswords/__init__.py @@ -0,0 +1,25 @@ +# user management +# +# user readpasswords commands +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from .getpassword import cmd_user_getpassword +from .show import cmd_user_show +from .syncpasswords import cmd_user_syncpasswords +from .get_kerberos_ticket import cmd_user_get_kerberos_ticket diff --git a/python/samba/netcmd/user/readpasswords/common.py b/python/samba/netcmd/user/readpasswords/common.py new file mode 100644 index 0000000..6d44881 --- /dev/null +++ b/python/samba/netcmd/user/readpasswords/common.py @@ -0,0 +1,907 @@ +# user management +# +# common code +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import base64 +import builtins +import binascii +import errno +import io +import os + +import ldb +from samba import credentials, nttime2float +from samba.auth import system_session +from samba.common import get_bytes, get_string +from samba.dcerpc import drsblobs, security, gmsa +from samba.ndr import ndr_unpack +from samba.netcmd import Command, CommandError +from samba.samdb import SamDB + + +# python[3]-gpgme is abandoned since ubuntu 1804 and debian 9 +# have to use python[3]-gpg instead +# The API is different, need to adapt. + +def _gpgme_decrypt(encrypted_bytes): + """ + Use python[3]-gpgme to decrypt GPG. + """ + ctx = gpgme.Context() + ctx.armor = True # use ASCII-armored + out = io.BytesIO() + ctx.decrypt(io.BytesIO(encrypted_bytes), out) + return out.getvalue() + + +def _gpg_decrypt(encrypted_bytes): + """ + Use python[3]-gpg to decrypt GPG. + """ + ciphertext = gpg.Data(string=encrypted_bytes) + ctx = gpg.Context(armor=True) + # plaintext, result, verify_result + plaintext, _, _ = ctx.decrypt(ciphertext) + return plaintext + + +gpg_decrypt = None + +if not gpg_decrypt: + try: + import gpgme + gpg_decrypt = _gpgme_decrypt + except ImportError: + pass + +if not gpg_decrypt: + try: + import gpg + gpg_decrypt = _gpg_decrypt + except ImportError: + pass + +if gpg_decrypt: + decrypt_samba_gpg_help = ("Decrypt the SambaGPG password as " + "cleartext source") +else: + decrypt_samba_gpg_help = ("Decrypt the SambaGPG password not supported, " + "python[3]-gpgme or python[3]-gpg 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, + }, + "unicodePwd": { + "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF, + }, +} + + +def get_crypt_value(alg, utf8pw, rounds=0): + algs = { + "5": {"length": 43}, + "6": {"length": 86}, + } + assert alg in algs + salt = os.urandom(16) + # The salt needs to be in [A-Za-z0-9./] + # base64 is close enough and as we had 16 + # random bytes but only need 16 characters + # we can ignore the possible == at the end + # of the base64 string + # we just need to replace '+' by '.' + 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)) + expected_len = len(crypt_salt) + algs[alg]["length"] + if len(crypt_value) != expected_len: + raise NotImplementedError("crypt.crypt(%s) returned a value with length %d, expected length is %d" % ( + crypt_salt, len(crypt_value), expected_len)) + return crypt_value + + +try: + import hashlib + hashlib.sha1() + virtual_attributes["virtualSSHA"] = { + } +except ImportError as e: + reason = "hashlib.sha1()" + reason += " required" + disabled_virtual_attributes["virtualSSHA"] = { + "reason": reason, + } + +for (alg, attr) in [("5", "virtualCryptSHA256"), ("6", "virtualCryptSHA512")]: + try: + import crypt + get_crypt_value(alg, "") + virtual_attributes[attr] = { + } + except ImportError as e: + reason = "crypt" + reason += " required" + disabled_virtual_attributes[attr] = { + "reason": reason, + } + except NotImplementedError as e: + reason = "modern '$%s$' salt in crypt(3) required" % (alg) + disabled_virtual_attributes[attr] = { + "reason": reason, + } + +# Add the wDigest virtual attributes, virtualWDigest01 to virtualWDigest29 +for x in range(1, 30): + virtual_attributes["virtualWDigest%02d" % x] = {} + +# Add Kerberos virtual attributes +virtual_attributes["virtualKerberosSalt"] = {} + +virtual_attributes_help = "The attributes to display (comma separated). " +virtual_attributes_help += "Possible supported virtual attributes: %s" % ", ".join(sorted(virtual_attributes.keys())) +if len(disabled_virtual_attributes) != 0: + virtual_attributes_help += "Unsupported virtual attributes: %s" % ", ".join(sorted(disabled_virtual_attributes.keys())) + + +class GetPasswordCommand(Command): + + def __init__(self): + super().__init__() + self.lp = None + + def inject_virtual_attributes(self, samdb): + # We use sort here in order to have a predictable processing order + # this might not be strictly needed, but also doesn't hurt here + for a in sorted(virtual_attributes.keys()): + flags = ldb.ATTR_FLAG_HIDDEN | virtual_attributes[a].get("flags", 0) + samdb.schema_attribute_add(a, flags, ldb.SYNTAX_OCTET_STRING) + + def connect_for_passwords(self, url, + creds=None, + require_ldapi=True, + verbose=False): + + # using anonymous here, results in no authentication + # which means we can get system privileges via + # the privileged ldapi socket + anon_creds = credentials.Credentials() + anon_creds.set_anonymous() + + if url is None and not require_ldapi: + pass + elif url.lower().startswith("ldapi://"): + creds = anon_creds + pass + elif require_ldapi: + raise CommandError("--url requires an ldapi:// url for this command") + + if verbose: + self.outf.write("Connecting to '%s'\n" % url) + + samdb = SamDB(url=url, session_info=system_session(), + credentials=creds, lp=self.lp) + + if require_ldapi or url is None: + try: + # + # Make sure we're connected as SYSTEM + # + res = samdb.search(base='', scope=ldb.SCOPE_BASE, attrs=["tokenGroups"]) + assert len(res) == 1 + sids = res[0].get("tokenGroups") + assert len(sids) == 1 + sid = ndr_unpack(security.dom_sid, sids[0]) + assert str(sid) == security.SID_NT_SYSTEM + except Exception as msg: + raise CommandError("You need to specify an URL that gives privileges as SID_NT_SYSTEM(%s)" % + (security.SID_NT_SYSTEM)) + + self.inject_virtual_attributes(samdb) + + return samdb + + def get_account_attributes(self, samdb, username, basedn, filter, scope, + attrs, decrypt, support_pw_attrs=True): + + def get_option(opts, name): + if not opts: + return None + for o in opts: + if o.lower().startswith("%s=" % name.lower()): + (key, _, val) = o.partition('=') + return val + return None + + def get_virtual_attr_definition(attr): + for van in sorted(virtual_attributes.keys()): + if van.lower() != attr.lower(): + continue + return virtual_attributes[van] + return None + + formats = [ + "GeneralizedTime", + "UnixTime", + "TimeSpec", + ] + + def get_virtual_format_definition(opts): + formatname = get_option(opts, "format") + if formatname is None: + return None + for fm in formats: + if fm.lower() != formatname.lower(): + continue + return fm + return None + + def parse_raw_attr(raw_attr, is_hidden=False): + (attr, _, fullopts) = raw_attr.partition(';') + if fullopts: + opts = fullopts.split(';') + else: + opts = [] + a = {} + a["raw_attr"] = raw_attr + a["attr"] = attr + a["opts"] = opts + a["vattr"] = get_virtual_attr_definition(attr) + a["vformat"] = get_virtual_format_definition(opts) + a["is_hidden"] = is_hidden + return a + + raw_attrs = attrs[:] + has_wildcard_attr = "*" in raw_attrs + has_virtual_attrs = False + requested_attrs = [] + implicit_attrs = [] + + for raw_attr in raw_attrs: + a = parse_raw_attr(raw_attr) + requested_attrs.append(a) + + search_attrs = [] + has_virtual_attrs = False + for a in requested_attrs: + if a["vattr"] is not None: + has_virtual_attrs = True + continue + if a["vformat"] is not None: + # also add it as implicit attr, + # where we just do + # search_attrs.append(a["attr"]) + # later on + implicit_attrs.append(a) + continue + if a["raw_attr"] in search_attrs: + continue + search_attrs.append(a["raw_attr"]) + + if not has_wildcard_attr: + required_attrs = [ + "sAMAccountName", + "userPrincipalName" + ] + for required_attr in required_attrs: + a = parse_raw_attr(required_attr) + implicit_attrs.append(a) + + if has_virtual_attrs: + if support_pw_attrs: + required_attrs = [ + "supplementalCredentials", + "unicodePwd", + "msDS-ManagedPassword", + ] + for required_attr in required_attrs: + a = parse_raw_attr(required_attr, is_hidden=True) + implicit_attrs.append(a) + + for a in implicit_attrs: + if a["attr"] in search_attrs: + continue + search_attrs.append(a["attr"]) + + if scope == ldb.SCOPE_BASE: + search_controls = ["show_deleted:1", "show_recycled:1"] + else: + search_controls = [] + try: + res = samdb.search(base=basedn, expression=filter, + scope=scope, attrs=search_attrs, + controls=search_controls) + if len(res) == 0: + raise Exception('Unable to find user "%s"' % (username or filter)) + if len(res) > 1: + raise Exception('Matched %u multiple users with filter "%s"' % (len(res), filter)) + except Exception as msg: + # FIXME: catch more specific exception + raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg)) + obj = res[0] + + calculated = {} + + sc = None + unicodePwd = None + if "supplementalCredentials" in obj: + sc_blob = obj["supplementalCredentials"][0] + sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob) + if "unicodePwd" in obj: + unicodePwd = obj["unicodePwd"][0] + if "msDS-ManagedPassword" in obj: + # unpack a GMSA managed password as if we could read the + # hidden password attributes. + managed_password = obj["msDS-ManagedPassword"][0] + unpacked_managed_password = ndr_unpack(gmsa.MANAGEDPASSWORD_BLOB, + managed_password) + calculated["Primary:CLEARTEXT"] = \ + unpacked_managed_password.passwords.current + calculated["OLDCLEARTEXT"] = \ + unpacked_managed_password.passwords.previous + + account_name = str(obj["sAMAccountName"][0]) + if "userPrincipalName" in obj: + account_upn = str(obj["userPrincipalName"][0]) + else: + realm = samdb.domain_dns_name() + account_upn = "%s@%s" % (account_name, realm.lower()) + + def get_package(name, min_idx=0): + if name in calculated: + return calculated[name] + if sc is None: + return None + if min_idx < 0: + min_idx = len(sc.sub.packages) + min_idx + idx = 0 + for p in sc.sub.packages: + idx += 1 + if idx <= min_idx: + continue + if name != p.name: + continue + + return binascii.a2b_hex(p.data) + return None + + def get_cleartext(attr_opts): + param = get_option(attr_opts, "previous") + if param: + if param != "1": + raise CommandError( + f"Invalid attribute parameter ;previous={param}, " + "only supported value is previous=1") + return calculated.get("OLDCLEARTEXT") + else: + return get_package("Primary:CLEARTEXT") + + def get_kerberos_ctr(): + primary_krb5 = get_package("Primary:Kerberos-Newer-Keys") + if primary_krb5 is None: + primary_krb5 = get_package("Primary:Kerberos") + if primary_krb5 is None: + return (0, None) + krb5_blob = ndr_unpack(drsblobs.package_PrimaryKerberosBlob, + primary_krb5) + return (krb5_blob.version, krb5_blob.ctr) + + aes256_key = None + kerberos_salt = None + + (krb5_v, krb5_ctr) = get_kerberos_ctr() + if krb5_v in [3, 4]: + kerberos_salt = krb5_ctr.salt.string + + if krb5_ctr.keys: + def is_aes256(k): + return k.keytype == 18 + aes256_key = next(builtins.filter(is_aes256, krb5_ctr.keys), + None) + + if decrypt: + # + # Samba adds 'Primary:SambaGPG' at the end. + # When Windows sets the password it keeps + # 'Primary:SambaGPG' and rotates it to + # the beginning. So we can only use the value, + # if it is the last one. + # + # In order to get more protection we verify + # the nthash of the decrypted utf16 password + # against the stored nthash in unicodePwd if + # available, otherwise against the first 16 + # bytes of the AES256 key. + # + sgv = get_package("Primary:SambaGPG", min_idx=-1) + if sgv is not None: + try: + cv = gpg_decrypt(sgv) + # + # We only use the password if it matches + # the current nthash stored in the unicodePwd + # attribute, or the current AES256 key. + # + tmp = credentials.Credentials() + tmp.set_anonymous() + tmp.set_utf16_password(cv) + + decrypted = None + current_hash = None + + if unicodePwd is not None: + decrypted = tmp.get_nt_hash() + current_hash = unicodePwd + elif aes256_key is not None and kerberos_salt is not None: + decrypted = tmp.get_aes256_key(kerberos_salt) + current_hash = aes256_key.value + + if current_hash is not None and current_hash == decrypted: + calculated["Primary:CLEARTEXT"] = cv + + except Exception as e: + self.outf.write( + "WARNING: '%s': SambaGPG can't be decrypted " + "into CLEARTEXT: %s\n" % ( + username or account_name, e)) + + def get_utf8(a, b, username): + creds_for_charcnv = credentials.Credentials() + creds_for_charcnv.set_anonymous() + creds_for_charcnv.set_utf16_password(get_bytes(b)) + + # This can't fail due to character conversion issues as it + # includes a built-in fallback (UTF16_MUNGED) matching + # exactly what we need. + return creds_for_charcnv.get_password().encode() + + # 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, get_string(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_cleartext(attr_opts) + if b is not None: + u8 = get_utf8(a, b, username or account_name) + if u8 is not None: + # in py2 using get_bytes should ensure u8 is unmodified + # in py3 it will be decoded + sv = get_crypt_value(str(algorithm), get_string(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 or AES256 key have not been changed + # without updating the user password hashes. This indicates that + # password has been changed without updating the supplemental + # credentials. + if unicodePwd is not None: + current_hash = unicodePwd + elif aes256_key is not None: + current_hash = aes256_key.value[:16] + else: + return None, None + + if current_hash != bytearray(up.current_nt_hash.hash): + return None, 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: + # in PY2 this should just do nothing and in PY3 if bytes + # it will decode them + h_value = get_string(h.value) + 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) + + # 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(opts): + val = get_option(opts, "rounds") + if val is None: + return 0 + try: + return int(val) + except ValueError: + return 0 + + def get_unicode_pwd_hash(pwd): + # We can't read unicodePwd directly, but we can regenerate + # it from msDS-ManagedPassword + tmp = credentials.Credentials() + tmp.set_anonymous() + tmp.set_utf16_password(pwd) + return tmp.get_nt_hash() + + # We use sort here in order to have a predictable processing order + for a in sorted(virtual_attributes.keys()): + vattr = None + for ra in requested_attrs: + if ra["vattr"] is None: + continue + if ra["attr"].lower() != a.lower(): + continue + vattr = ra + break + if vattr is None: + continue + attr_opts = vattr["opts"] + + if a == "virtualClearTextUTF8": + b = get_cleartext(attr_opts) + if b is None: + continue + u8 = get_utf8(a, b, username or account_name) + if u8 is None: + continue + v = u8 + elif a == "virtualClearTextUTF16": + v = get_cleartext(attr_opts) + if v is None: + continue + elif a == "virtualSSHA": + b = get_cleartext(attr_opts) + if b is None: + continue + u8 = get_utf8(a, b, username or account_name) + if u8 is None: + continue + salt = os.urandom(4) + h = hashlib.sha1() + h.update(u8) + h.update(salt) + bv = h.digest() + salt + v = "{SSHA}" + base64.b64encode(bv).decode('utf8') + elif a == "virtualCryptSHA256": + rounds = get_rounds(attr_opts) + x = get_virtual_crypt_value(a, 5, rounds, username, account_name) + if x is None: + continue + v = x + elif a == "virtualCryptSHA512": + rounds = get_rounds(attr_opts) + x = get_virtual_crypt_value(a, 6, rounds, username, account_name) + if x is None: + continue + v = x + elif a == "virtualSambaGPG": + # Samba adds 'Primary:SambaGPG' at the end. + # When Windows sets the password it keeps + # 'Primary:SambaGPG' and rotates it to + # the beginning. So we can only use the value, + # if it is the last one. + v = get_package("Primary:SambaGPG", min_idx=-1) + if v is None: + continue + elif a == "virtualKerberosSalt": + v = kerberos_salt + if v is None: + continue + elif a == "unicodePwd" and unicodePwd is None: + if "Primary:CLEARTEXT" in calculated and not get_option(attr_opts, "previous"): + v = get_unicode_pwd_hash(calculated["Primary:CLEARTEXT"]) + elif "OLDCLEARTEXT" in calculated and get_option(attr_opts, "previous"): + v = get_unicode_pwd_hash(calculated["OLDCLEARTEXT"]) + else: + 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 = samdb.domain_netbios_name() + 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, vattr["raw_attr"]) + + def get_src_attrname(srcattrg): + srcattrl = srcattrg.lower() + srcattr = None + for k in obj.keys(): + if srcattrl != k.lower(): + continue + srcattr = k + break + return srcattr + + def get_src_time_float(srcattr): + if srcattr not in obj: + return None + vstr = str(obj[srcattr][0]) + if vstr.endswith(".0Z"): + vut = ldb.string_to_time(vstr) + vfl = float(vut) + return vfl + + try: + vnt = int(vstr) + except ValueError as e: + return None + # 0 or 9223372036854775807 mean no value too + if vnt == 0: + return None + if vnt >= 0x7FFFFFFFFFFFFFFF: + return None + vfl = nttime2float(vnt) + return vfl + + def get_generalizedtime(srcattr): + vfl = get_src_time_float(srcattr) + if vfl is None: + return None + vut = int(vfl) + try: + v = "%s" % ldb.timestring(vut) + except OSError as e: + if e.errno == errno.EOVERFLOW: + return None + raise + return v + + def get_unixepoch(srcattr): + vfl = get_src_time_float(srcattr) + if vfl is None: + return None + vut = int(vfl) + v = "%d" % vut + return v + + def get_timespec(srcattr): + vfl = get_src_time_float(srcattr) + if vfl is None: + return None + v = "%.9f" % vfl + return v + + generated_formats = {} + for fm in formats: + for ra in requested_attrs: + if ra["vformat"] is None: + continue + if ra["vformat"] != fm: + continue + srcattr = get_src_attrname(ra["attr"]) + if srcattr is None: + continue + an = "%s;format=%s" % (srcattr, fm) + if an in generated_formats: + continue + generated_formats[an] = fm + + v = None + if fm == "GeneralizedTime": + v = get_generalizedtime(srcattr) + elif fm == "UnixTime": + v = get_unixepoch(srcattr) + elif fm == "TimeSpec": + v = get_timespec(srcattr) + if v is None: + continue + obj[an] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, an) + + # Now filter out implicit attributes + for delname in obj.keys(): + keep = False + for ra in requested_attrs: + if delname.lower() != ra["raw_attr"].lower(): + continue + keep = True + break + if keep: + continue + + dattr = None + for ia in implicit_attrs: + if delname.lower() != ia["attr"].lower(): + continue + dattr = ia + break + if dattr is None: + continue + + if has_wildcard_attr and not dattr["is_hidden"]: + continue + del obj[delname] + return obj + + def parse_attributes(self, attributes): + + if attributes is None: + raise CommandError("Please specify --attributes") + attrs = attributes.split(',') + password_attrs = [] + for pa in attrs: + pa = pa.lstrip().rstrip() + for da in disabled_virtual_attributes.keys(): + if pa.lower() == da.lower(): + r = disabled_virtual_attributes[da]["reason"] + raise CommandError("Virtual attribute '%s' not supported: %s" % ( + da, r)) + for va in virtual_attributes.keys(): + if pa.lower() == va.lower(): + # Take the real name + pa = va + break + password_attrs += [pa] + + return password_attrs diff --git a/python/samba/netcmd/user/readpasswords/get_kerberos_ticket.py b/python/samba/netcmd/user/readpasswords/get_kerberos_ticket.py new file mode 100644 index 0000000..3a8296b --- /dev/null +++ b/python/samba/netcmd/user/readpasswords/get_kerberos_ticket.py @@ -0,0 +1,146 @@ +# user management +# +# user get-kerberos-ticket command - obtain a TGT for a database user +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com> +# Copyright Andrew Bartlett 2023 <abartlet@samba.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import ldb +import samba.getopt as options +from samba.netcmd import CommandError, Option +from samba.credentials import Credentials +from .common import ( + GetPasswordCommand, + gpg_decrypt, + decrypt_samba_gpg_help, +) +from samba.dcerpc import samr + +class cmd_user_get_kerberos_ticket(GetPasswordCommand): + """Get a Kerberos Ticket Granting Ticket as a user + +This command gets a Kerberos TGT using the password for a user/computer account. + +The username specified on the command is the sAMAccountName. +The username may also be specified using the --filter option. + +The command must be run from the root user id or another authorized +user id. The '-H' or '--URL' option supports ldap:// for remote Group +Managed Service accounts, and ldapi:// or tdb:// can be used to +adjust the local path. tdb:// is used by default for a bare path. + +The --output-krb5-ccache option should point to a location for the +credentials cache. The default is a FILE: type cache if no prefix is +specified. + +The '--decrypt-samba-gpg' option triggers decryption of the +Primary:SambaGPG buffer to get the password. + +Check with '--help' if this feature is available +in your environment or not (the python-gpgme package is required). Please +note that you might need to set the GNUPGHOME environment variable. If the +decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO +environment variable has been set correctly and the passphrase is already +known by the gpg-agent. + +Example1: +samba-tool user get-kerberos-ticket TestUser1 --output-krb5-ccache=/srv/service/krb5_ccache + +Example2: +samba-tool user get-kerberos-ticket --filter='(samAccountName=TestUser3)' --output-krb5-ccache=FILE:/srv/service/krb5_ccache + + """ + synopsis = "%prog (<username>|--filter <filter>) [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + "hostopts": options.HostOptions, + } + + takes_options = [ + Option("--filter", help="LDAP Filter to get Kerberos ticket for (must match single account)", type=str), + Option("--output-krb5-ccache", type=str, + help="Location of Kerberos credentials cache to write ticket into", + metavar="CCACHE", dest="output_krb5_ccache"), + Option("--decrypt-samba-gpg", + help=decrypt_samba_gpg_help, + action="store_true", default=False, dest="decrypt_samba_gpg"), + ] + + takes_args = ["username?"] + + def run(self, username=None, H=None, filter=None, + attributes=None, decrypt_samba_gpg=None, + sambaopts=None, versionopts=None, hostopts=None, + credopts=None, output_krb5_ccache=None): + self.lp = sambaopts.get_loadparm() + + if decrypt_samba_gpg and not gpg_decrypt: + raise CommandError(decrypt_samba_gpg_help) + + if filter is None and username is None: + raise CommandError("Either the username or '--filter' must be specified!") + + if filter is None: + filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username)) + + password_attrs = ["virtualClearTextUTF16", "samAccountName", "unicodePwd"] + + creds = credopts.get_credentials(self.lp) + samdb = self.connect_for_passwords(url=hostopts.H, require_ldapi=False, creds=creds) + + obj = self.get_account_attributes(samdb, username, + basedn=None, + filter=filter, + scope=ldb.SCOPE_SUBTREE, + attrs=password_attrs, + decrypt=decrypt_samba_gpg) + + lp_ctx = sambaopts.get_loadparm() + + creds = Credentials() + creds.set_username(str(obj["samAccountName"][0])) + creds.set_realm(samdb.domain_dns_name()) + + utf16_pw = None + nt_pass = None + try: + utf16_pw = obj["virtualClearTextUTF16"][0] + creds.set_utf16_password(utf16_pw) + except KeyError: + pass + + if utf16_pw is None: + try: + nt_pass = samr.Password() + nt_pass.hash = list(obj["unicodePwd"][0]) + creds.set_nt_hash(nt_pass) + except KeyError: + pass + + if nt_pass is None and utf16_pw is None: + if samdb.url.startswith("ldap://") or samdb.url.startswith("ldaps://"): + raise CommandError("No password was available for this user. " + "Only Group Managed Service accounts allow access to passwords over LDAP, " + "you may need to access the sam.ldb directly on the Samba AD DC and export the file.") + else: + raise CommandError("No password was available for this user") + creds.guess(lp_ctx) + creds.get_named_ccache(lp_ctx, output_krb5_ccache) diff --git a/python/samba/netcmd/user/readpasswords/getpassword.py b/python/samba/netcmd/user/readpasswords/getpassword.py new file mode 100644 index 0000000..f962412 --- /dev/null +++ b/python/samba/netcmd/user/readpasswords/getpassword.py @@ -0,0 +1,210 @@ +# user management +# +# user getpassword command +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import ldb +import samba.getopt as options +from samba.netcmd import CommandError, Option + +from .common import ( + GetPasswordCommand, + gpg_decrypt, + decrypt_samba_gpg_help, + virtual_attributes_help +) + + +class cmd_user_getpassword(GetPasswordCommand): + """Get the password fields of a user/computer account. + +This command gets the logon password for a user/computer account. + +The username specified on the command is the sAMAccountName. +The username may also be specified using the --filter option. + +The command must be run from the root user id or another authorized user id. +The '-H' or '--URL' option normally only supports ldapi:// or [tdb://] and +can be used to adjust the local path. By default, tdb:// is used. +if the target account is a group managed service account, then in this +case the -H can point to a remote AD DC LDAP server. + +The '--attributes' parameter takes a comma separated list of attributes, +which will be printed or given to the script specified by '--script'. If a +specified attribute is not available on an object it's silently omitted. +All attributes defined in the schema (e.g. the unicodePwd attribute holds +the NTHASH) and the following virtual attributes are possible (see --help +for which virtual attributes are supported in your environment): + + virtualClearTextUTF16: The raw cleartext as stored in the + 'Primary:CLEARTEXT' (or 'Primary:SambaGPG' + with '--decrypt-samba-gpg') buffer inside the + supplementalCredentials attribute. This typically + contains valid UTF-16-LE, but may contain random + bytes, e.g. for computer and gMSA accounts. + When the account is a group managed service account, + and the user is permitted to access + msDS-ManagedPassword then the current and previous + password can be read over LDAP. Add ;previous=1 + to read the previous password. + + virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8 + (invalid UTF-16-LE is mapped in the same way as + Windows). + + virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1 + checksum, useful for OpenLDAP's '{SSHA}' algorithm. + + 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. + + virtualKerberosSalt: This results the salt string that is used to compute + Kerberos keys from a UTF-8 cleartext password. + + virtualSambaGPG: The raw cleartext as stored in the + 'Primary:SambaGPG' buffer inside the + supplementalCredentials attribute. + See the 'password hash gpg key ids' option in + smb.conf. + +The '--decrypt-samba-gpg' option triggers decryption of the +Primary:SambaGPG buffer. Check with '--help' if this feature is available +in your environment or not (the python-gpgme package is required). Please +note that you might need to set the GNUPGHOME environment variable. If the +decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO +environment variable has been set correctly and the passphrase is already +known by the gpg-agent. + +Attributes with time values can take an additional format specifier, which +converts the time value into the requested format. The format can be specified +by adding ";format=formatSpecifier" to the requested attribute name, whereby +"formatSpecifier" must be a valid specifier. The syntax looks like: + + --attributes=attributeName;format=formatSpecifier + +The following format specifiers are available: + - GeneralizedTime (e.g. 20210224113259.0Z) + - UnixTime (e.g. 1614166392) + - TimeSpec (e.g. 161416639.267546892) + +Attributes with an original NTTIME value of 0 and 9223372036854775807 are +treated as non-existing value. + +Example1: +samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8 + +Example2: +samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16 + +""" + + synopsis = "%prog (<username>|--filter <filter>) [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + "hostopts": options.HostOptions, + } + + takes_options = [ + Option("--filter", help="LDAP Filter to get password for (must match single account)", type=str), + Option("--attributes", type=str, + help=virtual_attributes_help, + metavar="ATTRIBUTELIST", dest="attributes"), + Option("--decrypt-samba-gpg", + help=decrypt_samba_gpg_help, + action="store_true", default=False, dest="decrypt_samba_gpg"), + ] + + takes_args = ["username?"] + + def run(self, username=None, H=None, filter=None, + attributes=None, decrypt_samba_gpg=None, + sambaopts=None, versionopts=None, hostopts=None, + credopts=None): + self.lp = sambaopts.get_loadparm() + + if decrypt_samba_gpg and not gpg_decrypt: + raise CommandError(decrypt_samba_gpg_help) + + if filter is None and username is None: + raise CommandError("Either the username or '--filter' must be specified!") + + if filter is None: + filter = "(&(objectClass=user)(sAMAccountName=%s))" % (ldb.binary_encode(username)) + + if attributes is None: + raise CommandError("Please specify --attributes") + + password_attrs = self.parse_attributes(attributes) + + creds = credopts.get_credentials(self.lp) + samdb = self.connect_for_passwords(url=hostopts.H, require_ldapi=False, creds=creds) + + obj = self.get_account_attributes(samdb, username, + basedn=None, + filter=filter, + scope=ldb.SCOPE_SUBTREE, + attrs=password_attrs, + decrypt=decrypt_samba_gpg) + + ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE) + self.outf.write("%s" % ldif) + self.errf.write("Got password OK\n") diff --git a/python/samba/netcmd/user/readpasswords/show.py b/python/samba/netcmd/user/readpasswords/show.py new file mode 100644 index 0000000..1cdec89 --- /dev/null +++ b/python/samba/netcmd/user/readpasswords/show.py @@ -0,0 +1,144 @@ +# user management +# +# user show command +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import samba.getopt as options +from samba import dsdb, ldb +from samba.auth import system_session +from samba.netcmd import Option, common +from samba.samdb import SamDB + +from .common import GetPasswordCommand + + +class cmd_user_show(GetPasswordCommand): + """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. + +The '--attributes' parameter takes a comma separated list of the requested +attributes. Without '--attributes' or with '--attributes=*' all usually +available attributes are selected. +Hidden attributes in addition to all usually available attributes can be +selected with e.g. '--attributes=*,msDS-UserPasswordExpiryTimeComputed'. +If a specified attribute is not available on a user object it's silently +omitted. + +Attributes with time values can take an additional format specifier, which +converts the time value into the requested format. The format can be specified +by adding ";format=formatSpecifier" to the requested attribute name, whereby +"formatSpecifier" must be a valid specifier. The syntax looks like: + + --attributes=attributeName;format=formatSpecifier + +The following format specifiers are available: + - GeneralizedTime (e.g. 20210224113259.0Z) + - UnixTime (e.g. 1614166392) + - TimeSpec (e.g. 161416639.267546892) + +Attributes with an original NTTIME value of 0 and 9223372036854775807 are +treated as non-existing value. + +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. + +Example4: +samba-tool user show User2 \\ + --attributes='pwdLastSet;format=GeneralizedTime,pwdLastSet;format=UnixTime' + +The result of Example 4 provides the pwdLastSet attribute values in the +specified format: + dn: CN=User2,CN=Users,DC=samdom,DC=example,DC=com + pwdLastSet;format=GeneralizedTime: 20210120105207.0Z + pwdLastSet;format=UnixTime: 1611139927 +""" + 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. " + "Possible supported virtual attributes: " + "virtualGeneralizedTime, virtualUnixTime, virtualTimeSpec."), + 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) + + self.inject_virtual_attributes(samdb) + + if user_attrs: + attrs = self.parse_attributes(user_attrs) + else: + attrs = ["*"] + + filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" % + (dsdb.ATYPE_NORMAL_ACCOUNT, ldb.binary_encode(username))) + + domaindn = samdb.domain_dn() + + obj = self.get_account_attributes(samdb, username, + basedn=domaindn, + filter=filter, + scope=ldb.SCOPE_SUBTREE, + attrs=attrs, + decrypt=False, + support_pw_attrs=False) + user_ldif = common.get_ldif_for_editor(samdb, obj) + self.outf.write(user_ldif) diff --git a/python/samba/netcmd/user/readpasswords/syncpasswords.py b/python/samba/netcmd/user/readpasswords/syncpasswords.py new file mode 100644 index 0000000..a909123 --- /dev/null +++ b/python/samba/netcmd/user/readpasswords/syncpasswords.py @@ -0,0 +1,878 @@ +# user management +# +# user syncpasswords command +# +# Copyright Jelmer Vernooij 2010 <jelmer@samba.org> +# Copyright Theresa Halloran 2011 <theresahalloran@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import base64 +import errno +import fcntl +import os +import signal +import time +from subprocess import Popen, PIPE, STDOUT + +import ldb +import samba.getopt as options +from samba import Ldb, dsdb +from samba.dcerpc import misc, security +from samba.ndr import ndr_unpack +from samba.common import get_bytes +from samba.netcmd import CommandError, Option + +from .common import ( + GetPasswordCommand, + gpg_decrypt, + decrypt_samba_gpg_help, + virtual_attributes_help +) + + +class cmd_user_syncpasswords(GetPasswordCommand): + """Sync the password of user accounts. + +This syncs logon passwords for user accounts. + +Note that this command should run on a single domain controller only +(typically the PDC-emulator). However the "password hash gpg key ids" +option should to be configured on all domain controllers. + +The command must be run from the root user id or another authorized user id. +The '-H' or '--URL' option only supports ldapi:// and can be used to adjust the +local path. By default, ldapi:// is used with the default path to the +privileged ldapi socket. + +This command has three modes: "Cache Initialization", "Sync Loop Run" and +"Sync Loop Terminate". + + +Cache Initialization +==================== + +The first time, this command needs to be called with +'--cache-ldb-initialize' in order to initialize its cache. + +The cache initialization requires '--attributes' and allows the following +optional options: '--decrypt-samba-gpg', '--script', '--filter' or +'-H/--URL'. + +The '--attributes' parameter takes a comma separated list of attributes, +which will be printed or given to the script specified by '--script'. If a +specified attribute is not available on an object it will be silently omitted. +All attributes defined in the schema (e.g. the unicodePwd attribute holds +the NTHASH) and the following virtual attributes are possible (see '--help' +for supported virtual attributes in your environment): + + virtualClearTextUTF16: The raw cleartext as stored in the + 'Primary:CLEARTEXT' (or 'Primary:SambaGPG' + with '--decrypt-samba-gpg') buffer inside of the + supplementalCredentials attribute. This typically + contains valid UTF-16-LE, but may contain random + bytes, e.g. for computer accounts. + + virtualClearTextUTF8: As virtualClearTextUTF16, but converted to UTF-8 + (only from valid UTF-16-LE). + + virtualSSHA: As virtualClearTextUTF8, but a salted SHA-1 + checksum, useful for OpenLDAP's '{SSHA}' algorithm. + + 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. + + virtualKerberosSalt: This results the salt string that is used to compute + Kerberos keys from a UTF-8 cleartext password. + + virtualSambaGPG: The raw cleartext as stored in the + 'Primary:SambaGPG' buffer inside of the + supplementalCredentials attribute. + See the 'password hash gpg key ids' option in + smb.conf. + +The '--decrypt-samba-gpg' option triggers decryption of the +Primary:SambaGPG buffer. Check with '--help' if this feature is available +in your environment or not (the python-gpgme package is required). Please +note that you might need to set the GNUPGHOME environment variable. If the +decryption key has a passphrase you have to make sure that the GPG_AGENT_INFO +environment variable has been set correctly and the passphrase is already +known by the gpg-agent. + +The '--script' option specifies a custom script that is called whenever any +of the dirsyncAttributes (see below) was changed. The script is called +without any arguments. It gets the LDIF for exactly one object on STDIN. +If the script processed the object successfully it has to respond with a +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 (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, userAccountControl, pwdLastSet and msDS-KeyVersionNumber. +Depending on the object, some attributes may not be present/available, +but you always get the current state (and not a diff). + +If no '--script' option is specified, the LDIF will be printed on STDOUT or +into the logfile. + +The default filter for the LDAP_SERVER_DIRSYNC_OID search is: +(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512)\\ + (!(sAMAccountName=krbtgt*))) +This means only normal (non-krbtgt) user +accounts are monitored. The '--filter' can modify that, e.g. if it's +required to also sync computer accounts. + + +Sync Loop Run +============= + +This (default) mode runs in an endless loop waiting for password related +changes in the active directory database. It makes use of the +LDAP_SERVER_DIRSYNC_OID and LDAP_SERVER_NOTIFICATION_OID controls in order +get changes in a reliable fashion. Objects are monitored for changes of the +following dirsyncAttributes: + + unicodePwd, dBCSPwd, supplementalCredentials, pwdLastSet, sAMAccountName, + userPrincipalName and userAccountControl. + +It recovers from LDAP disconnects and updates the cache in conservative way +(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". + +The '--logfile' option specifies an optional (required if '--daemon' is +specified) logfile that takes all output of the command. The logfile is +automatically reopened if fstat returns st_nlink == 0. + +The optional '--daemon' option will put the command into the background. + +You can stop the command without the '--daemon' option, also by hitting +strg+c. + +If you specify the '--no-wait' option the command skips the +LDAP_SERVER_NOTIFICATION_OID 'waiting' step and exit once +all LDAP_SERVER_DIRSYNC_OID changes are consumed. + +Sync Loop Terminate +=================== + +In order to terminate an already running command (likely as daemon) the +'--terminate' option can be used. This also requires the '--logfile' option +to be specified. + + +Example1: +samba-tool user syncpasswords --cache-ldb-initialize \\ + --attributes=virtualClearTextUTF8 +samba-tool user syncpasswords + +Example2: +samba-tool user syncpasswords --cache-ldb-initialize \\ + --attributes=objectGUID,objectSID,sAMAccountName,\\ + userPrincipalName,userAccountControl,pwdLastSet,\\ + msDS-KeyVersionNumber,virtualCryptSHA512 \\ + --script=/path/to/my-custom-syncpasswords-script.py +samba-tool user syncpasswords --daemon \\ + --logfile=/var/log/samba/user-syncpasswords.log +samba-tool user syncpasswords --terminate \\ + --logfile=/var/log/samba/user-syncpasswords.log + +""" + + synopsis = "%prog [--cache-ldb-initialize] [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + } + + takes_options = [ + Option("--cache-ldb-initialize", + help="Initialize the cache for the first time", + dest="cache_ldb_initialize", action="store_true"), + Option("--cache-ldb", help="optional LDB URL user-syncpasswords-cache.ldb", type=str, + metavar="CACHE-LDB-PATH", dest="cache_ldb"), + Option("-H", "--URL", help="optional LDB URL for a local ldapi server", type=str, + metavar="URL", dest="H"), + Option("--filter", help="optional LDAP filter to set password on", type=str, + metavar="LDAP-SEARCH-FILTER", dest="filter"), + Option("--attributes", type=str, + help=virtual_attributes_help, + metavar="ATTRIBUTELIST", dest="attributes"), + Option("--decrypt-samba-gpg", + help=decrypt_samba_gpg_help, + action="store_true", default=False, dest="decrypt_samba_gpg"), + Option("--script", help="Script that is called for each password change", type=str, + metavar="/path/to/syncpasswords.script", dest="script"), + Option("--no-wait", help="Don't block waiting for changes", + action="store_true", default=False, dest="nowait"), + Option("--logfile", type=str, + help="The logfile to use (required in --daemon mode).", + metavar="/path/to/syncpasswords.log", dest="logfile"), + Option("--daemon", help="daemonize after initial setup", + action="store_true", default=False, dest="daemon"), + 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, + attributes=None, decrypt_samba_gpg=None, + script=None, nowait=None, logfile=None, daemon=None, terminate=None, + sambaopts=None, versionopts=None): + + self.lp = sambaopts.get_loadparm() + self.logfile = None + self.samdb_url = None + self.samdb = None + self.cache = None + + if not cache_ldb_initialize: + if attributes is not None: + raise CommandError("--attributes is only allowed together with --cache-ldb-initialize") + if decrypt_samba_gpg: + raise CommandError("--decrypt-samba-gpg is only allowed together with --cache-ldb-initialize") + if script is not None: + raise CommandError("--script is only allowed together with --cache-ldb-initialize") + if filter is not None: + raise CommandError("--filter is only allowed together with --cache-ldb-initialize") + if H is not None: + raise CommandError("-H/--URL is only allowed together with --cache-ldb-initialize") + else: + if nowait is not False: + raise CommandError("--no-wait is not allowed together with --cache-ldb-initialize") + if logfile is not None: + raise CommandError("--logfile is not allowed together with --cache-ldb-initialize") + if daemon is not False: + raise CommandError("--daemon is not allowed together with --cache-ldb-initialize") + if terminate is not False: + raise CommandError("--terminate is not allowed together with --cache-ldb-initialize") + + if nowait is True: + if daemon is True: + raise CommandError("--daemon is not allowed together with --no-wait") + if terminate is not False: + raise CommandError("--terminate is not allowed together with --no-wait") + + if terminate is True and daemon is True: + raise CommandError("--terminate is not allowed together with --daemon") + + if daemon is True and logfile is None: + raise CommandError("--daemon is only allowed together with --logfile") + + if terminate is True and logfile is None: + raise CommandError("--terminate is only allowed together with --logfile") + + if script is not None: + if not os.path.exists(script): + raise CommandError("script[%s] does not exist!" % script) + + sync_command = "%s" % os.path.abspath(script) + else: + sync_command = None + + dirsync_filter = filter + if dirsync_filter is None: + dirsync_filter = "(&" + \ + "(objectClass=user)" + \ + "(userAccountControl:%s:=%u)" % ( + ldb.OID_COMPARATOR_AND, dsdb.UF_NORMAL_ACCOUNT) + \ + "(!(sAMAccountName=krbtgt*))" + \ + ")" + + dirsync_secret_attrs = [ + "unicodePwd", + "dBCSPwd", + "supplementalCredentials", + ] + + dirsync_attrs = dirsync_secret_attrs + [ + "pwdLastSet", + "sAMAccountName", + "userPrincipalName", + "userAccountControl", + "isDeleted", + "isRecycled", + ] + + password_attrs = None + + if cache_ldb_initialize: + if H is None: + H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi")) + + if decrypt_samba_gpg and not gpg_decrypt: + raise CommandError(decrypt_samba_gpg_help) + + password_attrs = self.parse_attributes(attributes) + lower_attrs = [x.lower() for x in password_attrs] + # We always return these in order to track deletions + for a in ["objectGUID", "isDeleted", "isRecycled"]: + if a.lower() not in lower_attrs: + password_attrs += [a] + + if cache_ldb is not None: + if cache_ldb.lower().startswith("ldapi://"): + raise CommandError("--cache_ldb ldapi:// is not supported") + elif cache_ldb.lower().startswith("ldap://"): + raise CommandError("--cache_ldb ldap:// is not supported") + elif cache_ldb.lower().startswith("ldaps://"): + raise CommandError("--cache_ldb ldaps:// is not supported") + elif cache_ldb.lower().startswith("tdb://"): + pass + else: + if not os.path.exists(cache_ldb): + cache_ldb = self.lp.private_path(cache_ldb) + else: + cache_ldb = self.lp.private_path("user-syncpasswords-cache.ldb") + + self.lockfile = "%s.pid" % cache_ldb + + def log_msg(msg): + if self.logfile is not None: + info = os.fstat(0) + if info.st_nlink == 0: + 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, 0o600) + os.dup2(logfd, 0) + os.dup2(logfd, 1) + os.dup2(logfd, 2) + os.close(logfd) + log_msg("Reopened logfile[%s]\n" % (logfile)) + self.logfile = logfile + msg = "%s: pid[%d]: %s" % ( + time.ctime(), + os.getpid(), + msg) + self.outf.write(msg) + return + + def load_cache(): + cache_attrs = [ + "samdbUrl", + "dirsyncFilter", + "dirsyncAttribute", + "dirsyncControl", + "passwordAttribute", + "decryptSambaGPG", + "syncCommand", + "currentPid", + ] + + self.cache = Ldb(cache_ldb) + self.cache_dn = ldb.Dn(self.cache, "KEY=USERSYNCPASSWORDS") + res = self.cache.search(base=self.cache_dn, scope=ldb.SCOPE_BASE, + attrs=cache_attrs) + if len(res) == 1: + try: + self.samdb_url = str(res[0]["samdbUrl"][0]) + except KeyError as e: + self.samdb_url = None + else: + self.samdb_url = None + if self.samdb_url is None and not cache_ldb_initialize: + raise CommandError("cache_ldb[%s] not initialized, use --cache-ldb-initialize the first time" % ( + cache_ldb)) + if self.samdb_url is not None and cache_ldb_initialize: + raise CommandError("cache_ldb[%s] already initialized, --cache-ldb-initialize not allowed" % ( + cache_ldb)) + if self.samdb_url is None: + self.samdb_url = H + self.dirsync_filter = dirsync_filter + self.dirsync_attrs = dirsync_attrs + self.dirsync_controls = ["dirsync:1:0:0", "extended_dn:1:0"] + self.password_attrs = password_attrs + self.decrypt_samba_gpg = decrypt_samba_gpg + self.sync_command = sync_command + add_ldif = "dn: %s\n" % self.cache_dn +\ + "objectClass: userSyncPasswords\n" +\ + "samdbUrl:: %s\n" % base64.b64encode(get_bytes(self.samdb_url)).decode('utf8') +\ + "dirsyncFilter:: %s\n" % base64.b64encode(get_bytes(self.dirsync_filter)).decode('utf8') +\ + "".join("dirsyncAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.dirsync_attrs) +\ + "dirsyncControl: %s\n" % self.dirsync_controls[0] +\ + "".join("passwordAttribute:: %s\n" % base64.b64encode(get_bytes(a)).decode('utf8') for a in self.password_attrs) + if self.decrypt_samba_gpg: + add_ldif += "decryptSambaGPG: TRUE\n" + else: + add_ldif += "decryptSambaGPG: FALSE\n" + if self.sync_command is not None: + add_ldif += "syncCommand: %s\n" % self.sync_command + add_ldif += "currentTime: %s\n" % ldb.timestring(int(time.time())) + self.cache.add_ldif(add_ldif) + self.current_pid = None + self.outf.write("Initialized cache_ldb[%s]\n" % (cache_ldb)) + msgs = self.cache.parse_ldif(add_ldif) + changetype, msg = next(msgs) + ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE) + self.outf.write("%s" % ldif) + else: + self.dirsync_filter = str(res[0]["dirsyncFilter"][0]) + self.dirsync_attrs = [] + for a in res[0]["dirsyncAttribute"]: + self.dirsync_attrs.append(str(a)) + self.dirsync_controls = [str(res[0]["dirsyncControl"][0]), "extended_dn:1:0"] + self.password_attrs = [] + for a in res[0]["passwordAttribute"]: + self.password_attrs.append(str(a)) + decrypt_string = str(res[0]["decryptSambaGPG"][0]) + assert(decrypt_string in ["TRUE", "FALSE"]) + if decrypt_string == "TRUE": + self.decrypt_samba_gpg = True + else: + self.decrypt_samba_gpg = False + if "syncCommand" in res[0]: + self.sync_command = str(res[0]["syncCommand"][0]) + else: + self.sync_command = None + if "currentPid" in res[0]: + self.current_pid = int(res[0]["currentPid"][0]) + else: + self.current_pid = None + log_msg("Using cache_ldb[%s]\n" % (cache_ldb)) + + return + + def run_sync_command(dn, ldif): + log_msg("Call Popen[%s] for %s\n" % (self.sync_command, dn)) + sync_command_p = Popen(self.sync_command, + stdin=PIPE, + stdout=PIPE, + stderr=STDOUT) + + res = sync_command_p.poll() + assert res is None + + input = "%s" % (ldif) + reply = sync_command_p.communicate( + input.encode('utf-8'))[0].decode('utf-8') + log_msg("%s\n" % (reply)) + res = sync_command_p.poll() + if res is None: + sync_command_p.terminate() + res = sync_command_p.wait() + + if reply.startswith("DONE-EXIT: "): + return + + log_msg("RESULT: %s\n" % (res)) + raise Exception("ERROR: %s - %s\n" % (res, reply)) + + def handle_object(idx, dirsync_obj): + binary_guid = dirsync_obj.dn.get_extended_component("GUID") + guid = ndr_unpack(misc.GUID, binary_guid) + binary_sid = dirsync_obj.dn.get_extended_component("SID") + sid = ndr_unpack(security.dom_sid, binary_sid) + domain_sid, rid = sid.split() + if rid == security.DOMAIN_RID_KRBTGT: + log_msg("# Dirsync[%d] SKIP: DOMAIN_RID_KRBTGT\n\n" % (idx)) + return + for a in list(dirsync_obj.keys()): + for h in dirsync_secret_attrs: + if a.lower() == h.lower(): + 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)) + obj = self.get_account_attributes(self.samdb, + username="%s" % sid, + basedn="<GUID=%s>" % guid, + filter="(objectClass=user)", + scope=ldb.SCOPE_BASE, + attrs=self.password_attrs, + decrypt=self.decrypt_samba_gpg) + ldif = self.samdb.write_ldif(obj, ldb.CHANGETYPE_NONE) + log_msg("# Passwords[%d] %s %s\n" % (idx, guid, sid)) + if self.sync_command is None: + self.outf.write("%s" % (ldif)) + return + self.outf.write("# attrs=%s\n" % (sorted(obj.keys()))) + run_sync_command(obj.dn, ldif) + + def check_current_pid_conflict(terminate): + flags = os.O_RDWR + if not terminate: + flags |= os.O_CREAT + + try: + self.lockfd = os.open(self.lockfile, flags, 0o600) + except IOError as e4: + (err, msg) = e4.args + if err == errno.ENOENT: + if terminate: + return False + log_msg("check_current_pid_conflict: failed to open[%s] - %s (%d)" % + (self.lockfile, msg, err)) + raise + + got_exclusive = False + try: + fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB) + got_exclusive = True + 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)) + raise + + if not got_exclusive: + buf = os.read(self.lockfd, 64) + self.current_pid = None + try: + self.current_pid = int(buf) + except ValueError as e: + pass + if self.current_pid is not None: + return True + + if got_exclusive and terminate: + try: + os.ftruncate(self.lockfd, 0) + 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 + os.close(self.lockfd) + self.lockfd = -1 + return False + + try: + fcntl.lockf(self.lockfd, fcntl.LOCK_SH) + 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)) + + # We leave the function with the shared lock. + return False + + def update_pid(pid): + if self.lockfd != -1: + got_exclusive = False + # Try 5 times to get the exclusive lock. + for i in range(0, 5): + try: + fcntl.lockf(self.lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB) + got_exclusive = True + 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)) + raise + if got_exclusive: + break + time.sleep(1) + if not got_exclusive: + log_msg("update_pid(%r): failed to get exclusive lock[%s]" % + (pid, self.lockfile)) + raise CommandError("update_pid(%r): failed to get " + "exclusive lock[%s] after 5 seconds" % + (pid, self.lockfile)) + + if pid is not None: + buf = "%d\n" % pid + else: + buf = None + try: + os.ftruncate(self.lockfd, 0) + if buf is not None: + os.write(self.lockfd, get_bytes(buf)) + 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 + self.current_pid = pid + if self.current_pid is not None: + log_msg("currentPid: %d\n" % self.current_pid) + + modify_ldif = "dn: %s\n" % (self.cache_dn) +\ + "changetype: modify\n" +\ + "replace: currentPid\n" + if self.current_pid is not None: + modify_ldif += "currentPid: %d\n" % (self.current_pid) + modify_ldif += "replace: currentTime\n" +\ + "currentTime: %s\n" % ldb.timestring(int(time.time())) + self.cache.modify_ldif(modify_ldif) + return + + def update_cache(res_controls): + assert len(res_controls) > 0 + assert res_controls[0].oid == "1.2.840.113556.1.4.841" + res_controls[0].critical = True + self.dirsync_controls = [str(res_controls[0]), "extended_dn:1:0"] + # This cookie can be extremely long + # log_msg("dirsyncControls: %r\n" % self.dirsync_controls) + + modify_ldif = "dn: %s\n" % (self.cache_dn) +\ + "changetype: modify\n" +\ + "replace: dirsyncControl\n" +\ + "dirsyncControl: %s\n" % (self.dirsync_controls[0]) +\ + "replace: currentTime\n" +\ + "currentTime: %s\n" % ldb.timestring(int(time.time())) + self.cache.modify_ldif(modify_ldif) + return + + def check_object(dirsync_obj, res_controls): + assert len(res_controls) > 0 + assert res_controls[0].oid == "1.2.840.113556.1.4.841" + + binary_sid = dirsync_obj.dn.get_extended_component("SID") + sid = ndr_unpack(security.dom_sid, binary_sid) + dn = "KEY=%s" % sid + lastCookie = str(res_controls[0]) + + res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE, + expression="(lastCookie=%s)" % ( + ldb.binary_encode(lastCookie)), + attrs=[]) + if len(res) == 1: + return True + return False + + def update_object(dirsync_obj, res_controls): + assert len(res_controls) > 0 + assert res_controls[0].oid == "1.2.840.113556.1.4.841" + + binary_sid = dirsync_obj.dn.get_extended_component("SID") + sid = ndr_unpack(security.dom_sid, binary_sid) + dn = "KEY=%s" % sid + lastCookie = str(res_controls[0]) + + self.cache.transaction_start() + try: + res = self.cache.search(base=dn, scope=ldb.SCOPE_BASE, + expression="(objectClass=*)", + attrs=["lastCookie"]) + if len(res) == 0: + add_ldif = "dn: %s\n" % (dn) +\ + "objectClass: userCookie\n" +\ + "lastCookie: %s\n" % (lastCookie) +\ + "currentTime: %s\n" % ldb.timestring(int(time.time())) + self.cache.add_ldif(add_ldif) + else: + modify_ldif = "dn: %s\n" % (dn) +\ + "changetype: modify\n" +\ + "replace: lastCookie\n" +\ + "lastCookie: %s\n" % (lastCookie) +\ + "replace: currentTime\n" +\ + "currentTime: %s\n" % ldb.timestring(int(time.time())) + self.cache.modify_ldif(modify_ldif) + self.cache.transaction_commit() + except Exception as e: + self.cache.transaction_cancel() + + return + + def dirsync_loop(): + while True: + res = self.samdb.search(expression=str(self.dirsync_filter), + scope=ldb.SCOPE_SUBTREE, + attrs=self.dirsync_attrs, + controls=self.dirsync_controls) + log_msg("dirsync_loop(): results %d\n" % len(res)) + ri = 0 + for r in res: + done = check_object(r, res.controls) + if not done: + handle_object(ri, r) + update_object(r, res.controls) + ri += 1 + update_cache(res.controls) + if len(res) == 0: + break + + def sync_loop(wait): + notify_attrs = ["name", "uSNCreated", "uSNChanged", "objectClass"] + notify_controls = ["notification:1", "show_recycled:1"] + notify_handle = self.samdb.search_iterator(expression="objectClass=*", + scope=ldb.SCOPE_SUBTREE, + attrs=notify_attrs, + controls=notify_controls, + timeout=-1) + + if wait is True: + log_msg("Resuming monitoring\n") + else: + log_msg("Getting changes\n") + self.outf.write("dirsyncFilter: %s\n" % self.dirsync_filter) + self.outf.write("dirsyncControls: %r\n" % self.dirsync_controls) + self.outf.write("syncCommand: %s\n" % self.sync_command) + dirsync_loop() + + if wait is not True: + return + + for msg in notify_handle: + if not isinstance(msg, ldb.Message): + self.outf.write("referral: %s\n" % msg) + continue + created = msg.get("uSNCreated")[0] + changed = msg.get("uSNChanged")[0] + log_msg("# Notify %s uSNCreated[%s] uSNChanged[%s]\n" % + (msg.dn, created, changed)) + + dirsync_loop() + + res = notify_handle.result() + + def daemonize(): + self.samdb = None + self.cache = None + orig_pid = os.getpid() + pid = os.fork() + if pid == 0: + os.setsid() + pid = os.fork() + if pid == 0: # Actual daemon + pid = os.getpid() + log_msg("Daemonized as pid %d (from %d)\n" % (pid, orig_pid)) + load_cache() + return + os._exit(0) + + if cache_ldb_initialize: + self.samdb_url = H + self.samdb = self.connect_for_passwords(url=self.samdb_url, + verbose=True) + load_cache() + return + + if logfile is not None: + import resource # Resource usage information. + 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, 0o600) + self.outf.write("Using logfile[%s]\n" % logfile) + for fd in range(0, maxfd): + if fd == logfd: + continue + try: + os.close(fd) + except OSError: + pass + os.dup2(logfd, 0) + os.dup2(logfd, 1) + os.dup2(logfd, 2) + os.close(logfd) + log_msg("Attached to logfile[%s]\n" % (logfile)) + self.logfile = logfile + + load_cache() + conflict = check_current_pid_conflict(terminate) + if terminate: + if self.current_pid is None: + log_msg("No process running.\n") + return + if not conflict: + log_msg("Process %d is not running anymore.\n" % ( + self.current_pid)) + update_pid(None) + return + log_msg("Sending SIGTERM to process %d.\n" % ( + self.current_pid)) + os.kill(self.current_pid, signal.SIGTERM) + return + if conflict: + raise CommandError("Exiting pid %d, command is already running as pid %d" % ( + os.getpid(), self.current_pid)) + + if daemon is True: + daemonize() + update_pid(os.getpid()) + + wait = True + while wait is True: + retry_sleep_min = 1 + retry_sleep_max = 600 + if nowait is True: + wait = False + retry_sleep = 0 + else: + retry_sleep = retry_sleep_min + + while self.samdb is None: + if retry_sleep != 0: + log_msg("Wait before connect - sleep(%d)\n" % retry_sleep) + time.sleep(retry_sleep) + retry_sleep = retry_sleep * 2 + if retry_sleep >= retry_sleep_max: + retry_sleep = retry_sleep_max + log_msg("Connecting to '%s'\n" % self.samdb_url) + try: + self.samdb = self.connect_for_passwords(url=self.samdb_url) + except Exception as msg: + self.samdb = None + log_msg("Connect to samdb Exception => (%s)\n" % msg) + if wait is not True: + raise + + try: + sync_loop(wait) + 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 |