PATCHES: Password sync as active directory domain controller
Alexander Bokovoy
ab at samba.org
Wed Mar 2 07:50:26 UTC 2016
Hi Metze,
thanks for these patches. They are an impressive amount of work. I have
few comments below mostly related to Python 2/3 compatibility and some
idiomatic use of Python.
On Mon, 29 Feb 2016, Stefan Metzmacher wrote:
> From 9e74157546880f003f87a39de3b973c45b6df1df Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Fri, 22 Jan 2016 21:52:26 +0100
> Subject: [PATCH 04/22] samba-tool: add 'user getpassword' command
>
> This provides an easy way to get the passwords of a user
> including the cleartext passwords (if stored) and derived
> hashes. This is done by providing virtual attributes like:
> virtualClearTextUTF16, virtualClearTextUTF8,
> virtualCryptSHA256, virtualCryptSHA512, virtualSSHA
>
> This is much easier than using ldbsearch and manually parsing
> the supplementalCredentials attribute.
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> python/samba/netcmd/user.py | 432 ++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 432 insertions(+)
>
> diff --git a/python/samba/netcmd/user.py b/python/samba/netcmd/user.py
> index cf640b0..04e1a50 100644
> --- a/python/samba/netcmd/user.py
> +++ b/python/samba/netcmd/user.py
> @@ -20,10 +20,20 @@
> import samba.getopt as options
> import ldb
> import pwd
> +import os
> +import sys
> +import errno
> +import base64
> +import binascii
> from getpass import getpass
> from samba.auth import system_session
> from samba.samdb import SamDB
> +from samba.dcerpc import misc
> +from samba.dcerpc import security
> +from samba.dcerpc import drsblobs
> +from samba.ndr import ndr_unpack, ndr_pack, ndr_print
> from samba import (
> + credentials,
> dsdb,
> gensec,
> generate_random_password,
> @@ -37,6 +47,141 @@ from samba.netcmd import (
> Option,
> )
>
> +disabled_virtual_attributes = {
> + }
> +
> +virtual_attributes = {
> + "virtualClearTextUTF8": {
> + "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
> + },
> + "virtualClearTextUTF16": {
> + "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
> + },
> + }
> +
> +def check_random():
> + try:
> + import Crypto.Random
> + return None
> + except ImportError as e:
> + pass
> + try:
> + import M2Crypto.Rand
> + return None
> + except ImportError as e:
> + pass
> + return "Crypto.Random or M2Crypto.Rand required"
> +
> +def get_random_bytes(num):
> + try:
> + import Crypto.Random
> + return Crypto.Random.get_random_bytes(num)
> + except ImportError as e:
> + pass
> + try:
> + import M2Crypto.Rand
> + return M2Crypto.Rand.rand_bytes(num)
> + except ImportError as e:
> + pass
> + raise ImportError("Crypto.Random or M2Crypto.Rand required")
> +
> +def get_crypt_value(alg, utf8pw):
> + algs = {
> + "5": {"length": 43},
> + "6": {"length": 86},
> + }
> + assert alg in algs.keys()
Please replace it with
assert alg in algs
It is more Python idiomatic and also avoids creating a list every time
only to drop it.
> + salt = get_random_bytes(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)
> + crypt_salt = "$%s$%s$" % (alg, b64salt[0:16].replace('+', '.'))
> + 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:
> + random_reason = check_random()
> + if random_reason is not None:
> + raise ImportError(random_reason)
> + import hashlib
> + 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,
> + }
> + pass
You don't need 'pass' here as there are already other statements under
'except'.
> +
> +try:
> + random_reason = check_random()
> + if random_reason is not None:
> + raise ImportError(random_reason)
> + import crypt
> + v = get_crypt_value("5", "")
> + v = None
> + virtual_attributes["virtualCryptSHA256"] = {
> + }
> +except ImportError as e:
> + reason = "crypt"
> + if random_reason:
> + reason += " and " + random_reason
> + reason += " required"
> + disabled_virtual_attributes["virtualCryptSHA256"] = {
> + "reason" : reason,
> + }
> + pass
Same here.
> +except NotImplementedError as e:
> + reason = "modern '$5$' salt in crypt(3) required"
> + disabled_virtual_attributes["virtualCryptSHA256"] = {
> + "reason" : reason,
> + }
> + pass
Same here.
> +
> +try:
> + random_reason = check_random()
> + if random_reason is not None:
> + raise ImportError(random_reason)
> + import crypt
> + v = get_crypt_value("6", "")
> + v = None
> + virtual_attributes["virtualCryptSHA512"] = {
> + }
> +except ImportError as e:
> + reason = "crypt"
> + if random_reason is not None:
> + reason += " and " + random_reason
> + reason += " required"
> + disabled_virtual_attributes["virtualCryptSHA512"] = {
> + "reason" : reason,
> + }
> + pass
Same here.
> +except NotImplementedError as e:
> + reason = "modern '$6$' salt in crypt(3) required"
> + disabled_virtual_attributes["virtualCryptSHA512"] = {
> + "reason" : reason,
> + }
> + pass
Same here.
> +
> +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 cmd_user_create(Command):
> """Create a new user.
> @@ -610,6 +755,292 @@ Example3 shows how an administrator would reset TestUser3 user's password to pas
> raise CommandError("Failed to set password for user '%s': %s" % (username or filter, msg))
> self.outf.write("Changed password OK\n")
>
> +class GetPasswordCommand(Command):
> +
> + def __init__(self):
> + Command.__init__(self)
A nit-pick:
It would be good to write all constructors in idiomatic way for 'new'
classes (derived from 'object')
super(GetPasswordCommand, self).__init__()
> + self.lp = None
> +
> + def connect_system_samdb(self, url, allow_local=False, verbose=False):
> +
> + # using anonymous here, results in no authentication
> + # which means we can get system privileges via
> + # the privileged ldapi socket
> + creds = credentials.Credentials()
> + creds.set_anonymous()
> +
> + if url is None and allow_local:
> + pass
> + elif url.lower().startswith("ldapi://"):
> + pass
> + elif url.lower().startswith("ldap://"):
> + raise CommandError("--url ldap:// is not supported for this command")
> + elif url.lower().startswith("ldaps://"):
> + raise CommandError("--url ldaps:// is not supported for this command")
> + elif not allow_local:
> + 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)
> +
> + 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, msg:
Please use 'except Exception as e' like in the other code above because
this makes the code Python 2 and 3 compatible ('except Exception, msg'
is not compatible with Python 3).
> + raise CommandError("You need to speficy an URL that gives privileges as SID_NT_SYSTEM(%s)" %
> + (security.SID_NT_SYSTEM))
> +
> + 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)
> +
> + return samdb
> +
> + def get_account_attributes(self, samdb, username,
> + basedn, filter, scope, attrs):
> +
> + require_supplementalCredentials = False
> + search_attrs = attrs[:]
> + lower_attrs = [x.lower() for x in search_attrs]
> + for a in virtual_attributes.keys():
> + if a.lower() in lower_attrs:
> + require_supplementalCredentials = True
> + add_supplementalCredentials = False
> + if require_supplementalCredentials:
> + a = "supplementalCredentials"
> + if a.lower() not in lower_attrs:
> + search_attrs += [a]
> + add_supplementalCredentials = True
> + add_sAMAcountName = False
> + a = "sAMAccountName"
> + if a.lower() not in lower_attrs:
> + search_attrs += [a]
> + add_sAMAcountName = True
> +
> + 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, msg:
Same here.
> + # FIXME: catch more specific exception
> + raise CommandError("Failed to get password for user '%s': %s" % (username or filter, msg))
> + obj = res[0]
> +
> + sc = None
> + if "supplementalCredentials" in obj:
> + sc_blob = obj["supplementalCredentials"][0]
> + sc = ndr_unpack(drsblobs.supplementalCredentialsBlob, sc_blob)
> + if add_supplementalCredentials:
> + del obj["supplementalCredentials"]
> + account_name = obj["sAMAccountName"][0]
> + if add_sAMAcountName:
> + del obj["sAMAccountName"]
> +
> + def get_package(name):
> + if sc is None:
> + return None
> + for p in sc.sub.packages:
> + if name != p.name:
> + continue
> +
> + return binascii.a2b_hex(p.data)
> + return None
> +
> + def get_utf8(a, b, username):
> + try:
> + u = unicode(b, 'utf-16-le')
> + except UnicodeDecodeError as e:
> + self.outf.write("WARNING: '%s': CLEARTEXT is invalid UTF-16-LE unable to generate %s\n" % (
> + username, a))
> + return None
> + u8 = u.encode('utf-8')
> + return u8
> +
> + for a in sorted(virtual_attributes.keys()):
> + if not a.lower() in lower_attrs:
> + continue
> +
> + if a == "virtualClearTextUTF8":
> + b = get_package("Primary:CLEARTEXT")
> + 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_package("Primary:CLEARTEXT")
> + if v is None:
> + continue
> + elif a == "virtualSSHA":
> + b = get_package("Primary:CLEARTEXT")
> + if b is None:
> + continue
> + u8 = get_utf8(a, b, username or account_name)
> + if u8 is None:
> + continue
> + salt = get_random_bytes(4)
> + h = hashlib.sha1()
> + h.update(u8)
> + h.update(salt)
> + bv = h.digest() + salt
> + v = "{SSHA}" + base64.b64encode(bv)
> + 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:
> + continue
> + sv = get_crypt_value("5", u8)
> + v = "{CRYPT}" + sv
> + elif a == "virtualCryptSHA512":
> + b = get_package("Primary:CLEARTEXT")
> + if b 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
> + else:
> + continue
> + obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
> + 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:
> + 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
> +
> +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 only supports ldapi:// or [tdb://] and can be
> +used to adjust the local path. By default tdb:// is used by default.
> +
> +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' 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.
> +
> + virtualCryptSHA512: As virtualClearTextUTF8, but a salted SHA512
> + checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
> + with a $6$... salt, see crypt(3) on modern systems.
> +
> +Example1:
> +samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
> +
> +Example2:
> +samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-KeyVersionNumber,unicodePwd,virtualClearTextUTF16
> +
> +"""
> + def __init__(self):
> + GetPasswordCommand.__init__(self)
Again, better to use
super(cmd_user_getpassword, self).__init__()
here
> +
> + synopsis = "%prog (<username>|--filter <filter>) [options]"
> +
> + takes_optiongroups = {
> + "sambaopts": options.SambaOptions,
> + "versionopts": options.VersionOptions,
> + }
> +
> + takes_options = [
> + Option("-H", "--URL", help="LDB URL for sam.ldb database or local ldapi server", type=str,
> + metavar="URL", dest="H"),
> + Option("--filter", help="LDAP Filter to set password on", type=str),
> + Option("--attributes", type=str,
> + help=virtual_attributes_help,
> + metavar="ATTRIBUTELIST", dest="attributes"),
> + ]
> +
> + takes_args = ["username?"]
> +
> + def run(self, username=None, H=None, filter=None,
> + attributes=None,
> + sambaopts=None, versionopts=None):
> + self.lp = sambaopts.get_loadparm()
> +
> + 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)
> +
> + samdb = self.connect_system_samdb(url=H, allow_local=True)
> +
> + obj = self.get_account_attributes(samdb, username,
> + basedn=None,
> + filter=filter,
> + scope=ldb.SCOPE_SUBTREE,
> + attrs=password_attrs)
> +
> + ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
> + self.outf.write("%s" % ldif)
> + self.outf.write("Got password OK\n")
>
> class cmd_user(SuperCommand):
> """User management."""
> @@ -624,3 +1055,4 @@ class cmd_user(SuperCommand):
> subcommands["setexpiry"] = cmd_user_setexpiry()
> subcommands["password"] = cmd_user_password()
> subcommands["setpassword"] = cmd_user_setpassword()
> + subcommands["getpassword"] = cmd_user_getpassword()
> --
> 1.9.1
>
>
> From c42135b4cd0393bdf4b922b42db82eead7cc0b3b Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 16 Feb 2016 03:19:58 +0100
> Subject: [PATCH 05/22] python:samba/tests: add simple 'samba-tool user
> getpassword' test
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> python/samba/tests/samba_tool/user.py | 24 +++++++++++++++++++++++-
> 1 file changed, 23 insertions(+), 1 deletion(-)
>
> diff --git a/python/samba/tests/samba_tool/user.py b/python/samba/tests/samba_tool/user.py
> index 645eb40..2542a73 100644
> --- a/python/samba/tests/samba_tool/user.py
> +++ b/python/samba/tests/samba_tool/user.py
> @@ -17,9 +17,11 @@
>
> import os
> import time
> +import base64
> import ldb
> from samba.tests.samba_tool.base import SambaToolCmdTest
> from samba import (
> + credentials,
> nttime2unix,
> dsdb
> )
> @@ -114,13 +116,33 @@ class UserCmdTestCase(SambaToolCmdTest):
>
> for user in self.users:
> newpasswd = self.randomPass()
> + creds = credentials.Credentials()
> + creds.set_anonymous()
> + creds.set_password(newpasswd)
> + nthash = creds.get_nt_hash()
> + attributes = "sAMAccountName,unicodePwd,supplementalCredentials"
> + unicodePwd = base64.b64encode(creds.get_nt_hash())
> +
> (result, out, err) = self.runsubcmd("user", "setpassword",
> user["name"],
> "--newpassword=%s" % newpasswd)
> - # self.assertCmdSuccess(result, "Ensure setpassword runs")
> + self.assertCmdSuccess(result, "Ensure setpassword runs")
> self.assertEquals(err,"","setpassword without url")
> self.assertMatch(out, "Changed password OK", "setpassword without url")
>
> + (result, out, err) = self.runsubcmd("user", "getpassword",
> + user["name"],
> + "--attributes=%s" % attributes)
> + self.assertCmdSuccess(result, "Ensure getpassword runs")
> + self.assertEqual(err,"","getpassword without url")
> + self.assertMatch(out, "Got password OK", "getpassword without url")
> + self.assertMatch(out, "sAMAccountName: %s" % (user["name"]),
> + "syncpasswords --no-wait: 'sAMAccountName': %s out[%s]" % (user["name"], out))
> + self.assertMatch(out, "unicodePwd:: %s" % unicodePwd,
> + "getpassword unicodePwd: out[%s]" % out)
> + self.assertMatch(out, "supplementalCredentials:: ",
> + "getpassword supplementalCredentials: out[%s]" % out)
> +
> for user in self.users:
> newpasswd = self.randomPass()
> (result, out, err) = self.runsubcmd("user", "setpassword",
> --
> 1.9.1
>
>
> From deb1cf71b6a8907fd118ef5e9ad6f70a4f7bf654 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Mon, 15 Feb 2016 09:15:38 +0100
> Subject: [PATCH 06/22] docs-xml:samba-tool.8: document "user getpassword"
> command
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> docs-xml/manpages/samba-tool.8.xml | 5 +++++
> 1 file changed, 5 insertions(+)
>
> diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
> index 3416ecf..024ffb6 100644
> --- a/docs-xml/manpages/samba-tool.8.xml
> +++ b/docs-xml/manpages/samba-tool.8.xml
> @@ -587,6 +587,11 @@
> <para>Sets or resets the password of an user account.</para>
> </refsect3>
>
> +<refsect3>
> + <title>user getpassword <replaceable>username</replaceable> [options]</title>
> + <para>Gets the password of an user account.</para>
> +</refsect3>
> +
> <refsect2>
> <title>vampire [options] <replaceable>domain</replaceable></title>
> <para>Join and synchronise a remote AD domain to the local server.
> --
> 1.9.1
>
>
> From 41af9a5379c1dde2f142bf6dab94911c4d61890d Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Fri, 22 Jan 2016 21:52:26 +0100
> Subject: [PATCH 07/22] samba-tool: add 'user syncpasswords' command
>
> This provides an easy way to keep passwords in sync with
> another account database, e.g. an OpenLDAP server.
>
> It provides a functionality like the "passwd program"
> for the "unix password sync" feature of a standalone, member
> and classic (NT4) server, but for an active directory domain
> controller.
>
> The provided script is called for each account/password related
> change.
>
> Like the 'user getpassword' command it allows virtual attributes like:
> virtualClearTextUTF16, virtualClearTextUTF8,
> virtualCryptSHA256, virtualCryptSHA512, virtualSSHA
>
> Note that this command should just run on a single domain controller
> (typically the PDC-emulator).
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> python/samba/netcmd/user.py | 688 ++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 688 insertions(+)
>
> diff --git a/python/samba/netcmd/user.py b/python/samba/netcmd/user.py
> index 04e1a50..0fe7c54 100644
> --- a/python/samba/netcmd/user.py
> +++ b/python/samba/netcmd/user.py
> @@ -23,8 +23,10 @@ import pwd
> import os
> import sys
> import errno
> +import time
> import base64
> import binascii
> +from subprocess import call, check_call, Popen, PIPE, STDOUT
> from getpass import getpass
> from samba.auth import system_session
> from samba.samdb import SamDB
> @@ -37,6 +39,7 @@ from samba import (
> dsdb,
> gensec,
> generate_random_password,
> + Ldb,
> )
> from samba.net import Net
>
> @@ -1042,6 +1045,690 @@ samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-
> self.outf.write("%s" % ldif)
> self.outf.write("Got password OK\n")
>
> +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).
> +
> +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: '--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' 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.
> +
> + 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 '--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 (an 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 succesfully 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("--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,
> + 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 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"))
> +
> + 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")
> +
> + 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, 0600)
> + 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",
> + "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 = 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.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)
> + for a in self.dirsync_attrs:
> + add_ldif += "dirsyncAttribute:: %s\n" % base64.b64encode(a)
> + add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
> + for a in self.password_attrs:
> + add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
> + 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 = msgs.next()
> + ldif = self.cache.write_ldif(msg, ldb.CHANGETYPE_NONE)
> + self.outf.write("%s" % ldif)
> + else:
> + self.dirsync_filter = res[0]["dirsyncFilter"][0]
> + self.dirsync_attrs = []
> + for a in res[0]["dirsyncAttribute"]:
> + self.dirsync_attrs.append(a)
> + self.dirsync_controls = [res[0]["dirsyncControl"][0], "extended_dn:1:0"]
> + self.password_attrs = []
> + for a in res[0]["passwordAttribute"]:
> + self.password_attrs.append(a)
> + if "syncCommand" in res[0]:
> + self.sync_command = 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" % (dn, self.sync_command))
> + 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)[0]
> + 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 iteration over keys it is better to use iterator objects which don't
allocate the whole list at the same time:
for a in dirsync_obj:
....
> + 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)
> + 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(pid):
> + if pid is None:
> + return False
> +
> + mypid = os.getpid()
> + if pid == mypid:
> + return False
> +
> + try:
> + os.kill(pid, 0)
> + except OSError as (num, msg):
> + if num != errno.ESRCH:
> + raise
> + return False
> +
> + p = Popen("grep -q 'syncpasswords' /proc/%d/cmdline" % pid, shell=True)
> + res = p.wait()
> + if res != 0:
> + return False
> + return True
> +
> + def update_pid(pid):
> + 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)
> + modify_ldif += "changetype: modify\n"
> + modify_ldif += "replace: currentPid\n"
> + if self.current_pid is not None:
> + modify_ldif += "currentPid: %d\n" % (self.current_pid)
> + modify_ldif += "replace: currentTime\n"
> + modify_ldif += "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"]
> + log_msg("dirsyncControls: %r\n" % self.dirsync_controls)
> +
> + 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])
> + modify_ldif += "replace: currentTime\n"
> + modify_ldif += "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)
> + add_ldif += "objectClass: userCookie\n"
> + add_ldif += "lastCookie: %s\n" % (lastCookie)
> + 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 += "changetype: modify\n"
> + modify_ldif += "replace: lastCookie\n"
> + modify_ldif += "lastCookie: %s\n" % (lastCookie)
> + modify_ldif += "replace: currentTime\n"
> + modify_ldif += "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=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"]
> + 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("referal: %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()
> + update_pid(os.getpid())
> + return
> + os._exit(0)
> +
> + if cache_ldb_initialize:
> + self.samdb_url = H
> + self.samdb = self.connect_system_samdb(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, 0600)
> + 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(self.current_pid)
> + if terminate:
> + if self.current_pid is None:
> + log_msg("No process running.\n")
> + return
> + if not conflict:
> + log_msg("Proccess %d is not running anymore.\n" % (
> + self.current_pid))
> + update_pid(None)
> + return
> + log_msg("Sending SIGTERM to proccess %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))
> + update_pid(os.getpid())
> + if daemon is True:
> + daemonize()
> +
> + 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_system_samdb(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
> + pass
> +
> + try:
> + sync_loop(wait)
> + except ldb.LdbError as (enum, estr):
> + self.samdb = None
> + log_msg("ldb.LdbError(%d) => (%s)\n" % (enum, estr))
> + pass
> +
> + # not reached
> + return
> +
> class cmd_user(SuperCommand):
> """User management."""
>
> @@ -1056,3 +1743,4 @@ class cmd_user(SuperCommand):
> subcommands["password"] = cmd_user_password()
> subcommands["setpassword"] = cmd_user_setpassword()
> subcommands["getpassword"] = cmd_user_getpassword()
> + subcommands["syncpasswords"] = cmd_user_syncpasswords()
> --
> 1.9.1
>
>
> From 941197011381493a604ecff1b4f77dd8ad338404 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 16 Feb 2016 03:19:58 +0100
> Subject: [PATCH 08/22] python:samba/tests: add simple 'samba-tool user
> syncpasswords' test
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> python/samba/tests/samba_tool/user.py | 46 ++++++++++++++++++++++++++++++++++-
> 1 file changed, 45 insertions(+), 1 deletion(-)
>
> diff --git a/python/samba/tests/samba_tool/user.py b/python/samba/tests/samba_tool/user.py
> index 2542a73..aad323d 100644
> --- a/python/samba/tests/samba_tool/user.py
> +++ b/python/samba/tests/samba_tool/user.py
> @@ -114,13 +114,41 @@ class UserCmdTestCase(SambaToolCmdTest):
> self.assertEquals(err,"","setpassword with url")
> self.assertMatch(out, "Changed password OK", "setpassword with url")
>
> + attributes = "sAMAccountName,unicodePwd,supplementalCredentials"
> + (result, out, err) = self.runsubcmd("user", "syncpasswords",
> + "--cache-ldb-initialize",
> + "--attributes=%s" % attributes)
> + self.assertCmdSuccess(result, "Ensure syncpasswords --cache-ldb-initialize runs")
> + self.assertEqual(err,"","getpassword without url")
> + cache_attrs = {
> + "objectClass": { "value": "userSyncPasswords" },
> + "samdbUrl": { },
> + "dirsyncFilter": { },
> + "dirsyncAttribute": { },
> + "dirsyncControl": { "value": "dirsync:1:0:0"},
> + "passwordAttribute": { },
> + "currentTime": { },
> + }
> + for a in cache_attrs.keys():
> + v = cache_attrs[a].get("value", "")
> + self.assertMatch(out, "%s: %s" % (a, v),
> + "syncpasswords --cache-ldb-initialize: %s: %s out[%s]" % (a, v, out))
> +
> + (result, out, err) = self.runsubcmd("user", "syncpasswords", "--no-wait")
> + self.assertCmdSuccess(result, "Ensure syncpasswords --no-wait runs")
> + self.assertEqual(err,"","syncpasswords --no-wait")
> + self.assertMatch(out, "dirsync_loop(): results 0",
> + "syncpasswords --no-wait: 'dirsync_loop(): results 0': out[%s]" % (out))
> + for user in self.users:
> + self.assertMatch(out, "sAMAccountName: %s" % (user["name"]),
> + "syncpasswords --no-wait: 'sAMAccountName': %s out[%s]" % (user["name"], out))
> +
> for user in self.users:
> newpasswd = self.randomPass()
> creds = credentials.Credentials()
> creds.set_anonymous()
> creds.set_password(newpasswd)
> nthash = creds.get_nt_hash()
> - attributes = "sAMAccountName,unicodePwd,supplementalCredentials"
> unicodePwd = base64.b64encode(creds.get_nt_hash())
>
> (result, out, err) = self.runsubcmd("user", "setpassword",
> @@ -130,6 +158,22 @@ class UserCmdTestCase(SambaToolCmdTest):
> self.assertEquals(err,"","setpassword without url")
> self.assertMatch(out, "Changed password OK", "setpassword without url")
>
> + (result, out, err) = self.runsubcmd("user", "syncpasswords", "--no-wait")
> + self.assertCmdSuccess(result, "Ensure syncpasswords --no-wait runs")
> + self.assertEqual(err,"","syncpasswords --no-wait")
> + self.assertMatch(out, "dirsync_loop(): results 0",
> + "syncpasswords --no-wait: 'dirsync_loop(): results 0': out[%s]" % (out))
> + self.assertMatch(out, "sAMAccountName: %s" % (user["name"]),
> + "syncpasswords --no-wait: 'sAMAccountName': %s out[%s]" % (user["name"], out))
> + self.assertMatch(out, "# unicodePwd::: REDACTED SECRET ATTRIBUTE",
> + "getpassword '# unicodePwd::: REDACTED SECRET ATTRIBUTE': out[%s]" % out)
> + self.assertMatch(out, "unicodePwd:: %s" % unicodePwd,
> + "getpassword unicodePwd: out[%s]" % out)
> + self.assertMatch(out, "# supplementalCredentials::: REDACTED SECRET ATTRIBUTE",
> + "getpassword '# supplementalCredentials::: REDACTED SECRET ATTRIBUTE': out[%s]" % out)
> + self.assertMatch(out, "supplementalCredentials:: ",
> + "getpassword supplementalCredentials: out[%s]" % out)
> +
> (result, out, err) = self.runsubcmd("user", "getpassword",
> user["name"],
> "--attributes=%s" % attributes)
> --
> 1.9.1
>
>
> From cb6f2a7d60f6b9ba074f56131c667889874648ac Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Mon, 15 Feb 2016 09:15:38 +0100
> Subject: [PATCH 09/22] docs-xml:samba-tool.8: document "user syncpasswords"
> command
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> docs-xml/manpages/samba-tool.8.xml | 7 +++++++
> 1 file changed, 7 insertions(+)
>
> diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
> index 024ffb6..dea984f 100644
> --- a/docs-xml/manpages/samba-tool.8.xml
> +++ b/docs-xml/manpages/samba-tool.8.xml
> @@ -592,6 +592,13 @@
> <para>Gets the password of an user account.</para>
> </refsect3>
>
> +<refsect3>
> + <title>user syncpasswords <replaceable>--cache-ldb-initialize</replaceable> [options]</title>
> + <para>Syncs the passwords of all user accounts, using an optional script.</para>
> + <para>Note that this command should run on a single domain controller only
> + (typically the PDC-emulator).</para>
> +</refsect3>
> +
> <refsect2>
> <title>vampire [options] <replaceable>domain</replaceable></title>
> <para>Join and synchronise a remote AD domain to the local server.
> --
> 1.9.1
>
>
> From 97b8b98ed8faf202f94ba4685ed11391b4bf7ed8 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Mon, 15 Feb 2016 09:56:03 +0100
> Subject: [PATCH 10/22] docs-xml/smbdotconf: reference "unix password sync"
> with "samba-tool user syncpasswords"
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> docs-xml/smbdotconf/security/unixpasswordsync.xml | 8 ++++++--
> 1 file changed, 6 insertions(+), 2 deletions(-)
>
> diff --git a/docs-xml/smbdotconf/security/unixpasswordsync.xml b/docs-xml/smbdotconf/security/unixpasswordsync.xml
> index 321ece5..75c8916 100644
> --- a/docs-xml/smbdotconf/security/unixpasswordsync.xml
> +++ b/docs-xml/smbdotconf/security/unixpasswordsync.xml
> @@ -9,8 +9,12 @@
> If this is set to <constant>yes</constant> the program specified in the <parameter moreinfo="none">passwd
> program</parameter> parameter is called <emphasis>AS ROOT</emphasis> -
> to allow the new UNIX password to be set without access to the
> - old UNIX password (as the SMB password change code has no
> - access to the old password cleartext, only the new).</para>
> + old UNIX password (as the SMB password change code has no
> + access to the old password cleartext, only the new).</para>
> +
> + <para>This option has no effect if <command moreinfo="none">samba</command>
> + is running as an active directory domain controller, in that case have a
> + look at the <command moreinfo="none">samba-tool user syncpasswords</command> command.</para>
> </description>
>
> <related>passwd program</related>
> --
> 1.9.1
>
>
> From 3273fc157363a73d58e93992abb730843775391c Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 16 Feb 2016 07:01:18 +0100
> Subject: [PATCH 11/22] .travis.yml: install libgpgme11-dev python[3]-gpgme
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> .travis.yml | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/.travis.yml b/.travis.yml
> index b930cfe..6c6934e 100644
> --- a/.travis.yml
> +++ b/.travis.yml
> @@ -24,7 +24,7 @@ matrix:
>
> before_install:
> - sudo apt-get update -qq
> - - sudo apt-get install --assume-yes acl attr autoconf bison build-essential debhelper dnsutils docbook-xml docbook-xsl flex gdb git krb5-user libacl1-dev libaio-dev libattr1-dev libblkid-dev libbsd-dev libcap-dev libcups2-dev libgnutls-dev libldap2-dev libncurses5-dev libpam0g-dev libparse-yapp-perl libpopt-dev libreadline-dev perl perl-modules pkg-config python-crypto python-dev python-dnspython python3-crypto python3-dev python3-dnspython realpath screen xsltproc zlib1g-dev
> + - sudo apt-get install --assume-yes acl attr autoconf bison build-essential debhelper dnsutils docbook-xml docbook-xsl flex gdb git krb5-user libacl1-dev libaio-dev libattr1-dev libblkid-dev libbsd-dev libcap-dev libcups2-dev libgnutls-dev libgpgme11-dev libldap2-dev libncurses5-dev libpam0g-dev libparse-yapp-perl libpopt-dev libreadline-dev perl perl-modules pkg-config python-crypto python-dev python-dnspython python-gpgme python3-crypto python3-dev python3-dnspython python3-gpgme realpath screen xsltproc zlib1g-dev
>
> script:
> - git fetch --unshallow
> --
> 1.9.1
>
>
> From 0127066d0f34737d1752e2ad7811ff55fc144b76 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Mon, 15 Feb 2016 09:10:54 +0100
> Subject: [PATCH 12/22] docs-xml/smbdotconf: add "password hash gpg key ids"
> option
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> .../smbdotconf/security/passwordhashgpgkeyids.xml | 45 ++++++++++++++++++++++
> 1 file changed, 45 insertions(+)
> create mode 100644 docs-xml/smbdotconf/security/passwordhashgpgkeyids.xml
>
> diff --git a/docs-xml/smbdotconf/security/passwordhashgpgkeyids.xml b/docs-xml/smbdotconf/security/passwordhashgpgkeyids.xml
> new file mode 100644
> index 0000000..48fbc79
> --- /dev/null
> +++ b/docs-xml/smbdotconf/security/passwordhashgpgkeyids.xml
> @@ -0,0 +1,45 @@
> +<samba:parameter name="password hash gpg key ids"
> + context="G"
> + type="cmdlist"
> + xmlns:samba="http://www.samba.org/samba/DTD/samba-doc">
> +<description>
> + <para>If <command moreinfo="none">samba</command> is running as an
> + active directory domain controller, it is possible to store the
> + cleartext password of accounts in a PGP/OpenGPG encrypted form.</para>
> +
> + <para>You can specify one or more recipients by key id or user id.</para>
> +
> + <para>The value is stored as 'Primary:SambaGPG' in the
> + <command moreinfo="none">supplementalCredentials</command> attribute.</para>
> +
> + <para>As password changes can occur on any domain controller,
> + you should configure this on each of them. Note that this feature is currently
> + available only on Samba domain controllers.</para>
> +
> + <para>This option is only available if <command moreinfo="none">samba</command>
> + was compiled with <command moreinfo="none">gpgme</command> support.</para>
> +
> + <para>You may need to export the <command moreinfo="none">GNUPGHOME</command>
> + environment variable before starting <command moreinfo="none">samba</command>.
> + <emphasis>It is strongly recommended to only store the public key in this
> + location. The private key is not used for encryption and should be
> + only stored where decryption is required.</emphasis></para>
> +
> + <para>Being able to restore the cleartext password helps, when they need to be imported
> + into other authentication systems later (see <command moreinfo="none">samba-tool user getpassword</command>)
> + or you want to keep the passwords in sync with another system, e.g. an OpenLDAP server
> + (see <command moreinfo="none">samba-tool user syncpasswords</command>).</para>
> +
> + <para>While this option needs to be configured on all domain controllers, the
> + <command moreinfo="none">samba-tool user syncpasswords</command> command should
> + run on a single domain controller only (typically the PDC-emulator).</para>
> +</description>
> +
> +<related>unix password sync</related>
> +
> +<value type="default"></value>
> +<value type="example">01FAB41A</value>
> +<value type="example">4952E40301FAB41A</value>
> +<value type="example">selftest at samba.example.com</value>
> +<value type="example">selftest at samba.example.com, 4952E40301FAB41A</value>
> +</samba:parameter>
> --
> 1.9.1
>
>
> From 270ef6c74bd9929f31f1d8eb8b3485197f305101 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Mon, 15 Feb 2016 09:56:03 +0100
> Subject: [PATCH 13/22] docs-xml/smbdotconf: reference "unix password sync"
> with "password hash gpg key ids"
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> docs-xml/smbdotconf/security/unixpasswordsync.xml | 4 +++-
> 1 file changed, 3 insertions(+), 1 deletion(-)
>
> diff --git a/docs-xml/smbdotconf/security/unixpasswordsync.xml b/docs-xml/smbdotconf/security/unixpasswordsync.xml
> index 75c8916..89b0158 100644
> --- a/docs-xml/smbdotconf/security/unixpasswordsync.xml
> +++ b/docs-xml/smbdotconf/security/unixpasswordsync.xml
> @@ -14,11 +14,13 @@
>
> <para>This option has no effect if <command moreinfo="none">samba</command>
> is running as an active directory domain controller, in that case have a
> - look at the <command moreinfo="none">samba-tool user syncpasswords</command> command.</para>
> + look at the <smbconfoption name="password hash gpg key ids"/> option and the
> + <command moreinfo="none">samba-tool user syncpasswords</command> command.</para>
> </description>
>
> <related>passwd program</related>
> <related>passwd chat</related>
> +<related>password hash gpg key ids</related>
>
> <value type="default">no</value>
> </samba:parameter>
> --
> 1.9.1
>
>
> From 3ff64ac992474ee31425b4735d713be3f4037249 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 12 Jan 2016 10:51:38 +0100
> Subject: [PATCH 14/22] s4:dsdb/samdb: add configure checks for libgpgme
>
> This will be used to store the cleartext utf16 password
> GPG encrypted as 'Primary:SambaGPG' in the
> supplementalCredentials attribute.
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> source4/dsdb/samdb/ldb_modules/wscript | 28 ++++++++++++++++++++++++++++
> wscript | 2 ++
> 2 files changed, 30 insertions(+)
> create mode 100644 source4/dsdb/samdb/ldb_modules/wscript
>
> diff --git a/source4/dsdb/samdb/ldb_modules/wscript b/source4/dsdb/samdb/ldb_modules/wscript
> new file mode 100644
> index 0000000..2aed0a0
> --- /dev/null
> +++ b/source4/dsdb/samdb/ldb_modules/wscript
> @@ -0,0 +1,28 @@
> +
> +import Logs, Options, sys
> +import samba3
> +
> +def set_options(opt):
> + opt.SAMBA3_ADD_OPTION('gpgme', default=None)
> +
> + return
> +
> +def configure(conf):
> + conf.SET_TARGET_TYPE('gpgme', 'EMPTY')
> +
> + if Options.options.with_gpgme != False:
> + conf.find_program('gpgme-config', var='GPGME_CONFIG')
> +
> + if conf.env.GPGME_CONFIG:
> + conf.CHECK_CFG(path=conf.env.GPGME_CONFIG, args="--cflags --libs",
> + package="", uselib_store="gpgme",
> + msg='Checking for gpgme support')
> +
> + if conf.CHECK_FUNCS_IN('gpgme_new', 'gpgme', headers='gpgme.h'):
> + conf.DEFINE('ENABLE_GPGME', '1')
> +
> + if not conf.CONFIG_SET('ENABLE_GPGME'):
> + if Options.options.with_gpgme == True:
> + conf.fatal('GPGME support requested, but no suitable GPGME library found')
> + else:
> + Logs.warn('no suitable GPGME library found')
> diff --git a/wscript b/wscript
> index 41ed5da..ef6eb7a 100644
> --- a/wscript
> +++ b/wscript
> @@ -39,6 +39,7 @@ def set_options(opt):
> opt.RECURSE('lib/ldb')
> opt.RECURSE('selftest')
> opt.RECURSE('source4/lib/tls')
> + opt.RECURSE('source4/dsdb/samdb/ldb_modules')
> opt.RECURSE('pidl')
> opt.RECURSE('source3')
> opt.RECURSE('lib/util')
> @@ -149,6 +150,7 @@ def configure(conf):
> if conf.CONFIG_GET('KRB5_VENDOR') in (None, 'heimdal'):
> conf.RECURSE('source4/heimdal_build')
> conf.RECURSE('source4/lib/tls')
> + conf.RECURSE('source4/dsdb/samdb/ldb_modules')
> conf.RECURSE('source4/ntvfs/sysdep')
> conf.RECURSE('lib/util')
> conf.RECURSE('lib/util/charset')
> --
> 1.9.1
>
>
> From aa6956ddea7a74f9eab2efec9680755cda57d9a9 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 12 Jan 2016 10:51:38 +0100
> Subject: [PATCH 15/22] drsblobs.idl: add package_PrimarySambaGPGBlob
>
> This will be used to store the cleartext utf16 password
> GPG encrypted in the supplementalCredentials attribute.
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> librpc/idl/drsblobs.idl | 8 ++++++++
> 1 file changed, 8 insertions(+)
>
> diff --git a/librpc/idl/drsblobs.idl b/librpc/idl/drsblobs.idl
> index 499febb..0f421a0 100644
> --- a/librpc/idl/drsblobs.idl
> +++ b/librpc/idl/drsblobs.idl
> @@ -445,6 +445,14 @@ interface drsblobs {
> [in] package_PrimaryWDigestBlob blob
> );
>
> + typedef [public] struct {
> + [flag(NDR_REMAINING)] DATA_BLOB gpg_blob;
> + } package_PrimarySambaGPGBlob;
> +
> + void decode_PrimarySambaGPG(
> + [in] package_PrimarySambaGPGBlob blob
> + );
> +
> typedef struct {
> [value(0)] uint32 size;
> } AuthInfoNone;
> --
> 1.9.1
>
>
> From 47e26c134dc77f4da4dd24c420af3ae11c05f50b Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 12 Jan 2016 10:51:38 +0100
> Subject: [PATCH 16/22] s4:dsdb/samdb: optionally store
> package_PrimarySambaGPGBlob in supplementalCredentials
>
> It's important that Primary:SambaGPG is added as the last element.
> This is the indication that it matches the current password.
> When a password change happens on a Windows DC,
> it will keep the old Primary:SambaGPG value, but as the first element.
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> source4/dsdb/samdb/ldb_modules/password_hash.c | 226 ++++++++++++++++++++-
> .../dsdb/samdb/ldb_modules/wscript_build_server | 2 +-
> 2 files changed, 221 insertions(+), 7 deletions(-)
>
> diff --git a/source4/dsdb/samdb/ldb_modules/password_hash.c b/source4/dsdb/samdb/ldb_modules/password_hash.c
> index 05b0854..14dc268 100644
> --- a/source4/dsdb/samdb/ldb_modules/password_hash.c
> +++ b/source4/dsdb/samdb/ldb_modules/password_hash.c
> @@ -45,6 +45,11 @@
> #include "param/param.h"
> #include "lib/krb5_wrap/krb5_samba.h"
>
> +#ifdef ENABLE_GPGME
> +#undef class
> +#include <gpgme.h>
> +#endif
> +
> /* If we have decided there is a reason to work on this request, then
> * setup all the password hash types correctly.
> *
> @@ -97,6 +102,8 @@ struct ph_context {
> bool hash_values;
> bool userPassword;
> bool pwd_last_set_bypass;
> +
> + const char **gpg_key_ids;
> };
>
>
> @@ -1393,17 +1400,137 @@ static int setup_primary_wdigest(struct setup_password_fields_io *io,
> return LDB_SUCCESS;
> }
>
> +static int setup_primary_samba_gpg(struct setup_password_fields_io *io,
> + struct package_PrimarySambaGPGBlob *pgb)
> +{
> +#ifdef ENABLE_GPGME
> + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
> + gpgme_error_t gret;
> + gpgme_ctx_t ctx = NULL;
> + size_t num_keys = str_list_length(io->ac->gpg_key_ids);
> + gpgme_key_t keys[num_keys+1];
> + size_t ki = 0;
> + size_t kr = 0;
> + gpgme_data_t plain_data = NULL;
> + gpgme_data_t crypt_data = NULL;
> + size_t crypt_length = 0;
> + char *crypt_mem = NULL;
> +
> + gret = gpgme_new(&ctx);
> + if (gret != GPG_ERR_NO_ERROR) {
> + ldb_debug(ldb, LDB_DEBUG_ERROR,
> + "%s:%s: gret[%u] %s\n",
> + __location__, __func__,
> + gret, gpgme_strerror(gret));
> + return ldb_module_operr(io->ac->module);
> + }
> +
> + gpgme_set_armor(ctx, 1);
> +
> + gret = gpgme_data_new_from_mem(&plain_data,
> + (const char *)io->n.cleartext_utf16->data,
> + io->n.cleartext_utf16->length,
> + 0 /* no copy */);
> + if (gret != GPG_ERR_NO_ERROR) {
> + ldb_debug(ldb, LDB_DEBUG_ERROR,
> + "%s:%s: gret[%u] %s\n",
> + __location__, __func__,
> + gret, gpgme_strerror(gret));
> + gpgme_release(ctx);
> + return ldb_module_operr(io->ac->module);
> + }
> + gret = gpgme_data_new(&crypt_data);
> + if (gret != GPG_ERR_NO_ERROR) {
> + ldb_debug(ldb, LDB_DEBUG_ERROR,
> + "%s:%s: gret[%u] %s\n",
> + __location__, __func__,
> + gret, gpgme_strerror(gret));
> + gpgme_data_release(plain_data);
> + gpgme_release(ctx);
> + return ldb_module_operr(io->ac->module);
> + }
> +
> + for (ki = 0; ki < num_keys; ki++) {
> + const char *key_id = io->ac->gpg_key_ids[ki];
> +
> + keys[ki] = NULL;
> +
> + gret = gpgme_get_key(ctx, key_id, &keys[ki], 0 /* public key */);
> + if (gret != GPG_ERR_NO_ERROR) {
> + keys[ki] = NULL;
> + ldb_debug(ldb, LDB_DEBUG_ERROR,
> + "%s:%s: ki[%zu] key_id[%s] gret[%u] %s\n",
> + __location__, __func__,
> + ki, io->ac->gpg_key_ids[ki],
> + gret, gpgme_strerror(gret));
> + for (kr = 0; keys[kr] != NULL; kr++) {
> + gpgme_key_release(keys[kr]);
> + }
> + gpgme_data_release(crypt_data);
> + gpgme_data_release(plain_data);
> + gpgme_release(ctx);
> + return ldb_module_operr(io->ac->module);
> + }
> + }
> + keys[ki] = NULL;
> +
> + gret = gpgme_op_encrypt(ctx, keys,
> + GPGME_ENCRYPT_ALWAYS_TRUST,
> + plain_data, crypt_data);
> + gpgme_data_release(plain_data);
> + plain_data = NULL;
> + for (kr = 0; keys[kr] != NULL; kr++) {
> + gpgme_key_release(keys[kr]);
> + keys[kr] = NULL;
> + }
> + gpgme_release(ctx);
> + ctx = NULL;
> + if (gret != GPG_ERR_NO_ERROR) {
> + ldb_debug(ldb, LDB_DEBUG_ERROR,
> + "%s:%s: gret[%u] %s\n",
> + __location__, __func__,
> + gret, gpgme_strerror(gret));
> + gpgme_data_release(crypt_data);
> + return ldb_module_operr(io->ac->module);
> + }
> +
> + crypt_mem = gpgme_data_release_and_get_mem(crypt_data, &crypt_length);
> + crypt_data = NULL;
> + if (crypt_mem == NULL) {
> + return ldb_module_oom(io->ac->module);
> + }
> +
> + pgb->gpg_blob = data_blob_talloc(io->ac,
> + (const uint8_t *)crypt_mem,
> + crypt_length);
> + gpgme_free(crypt_mem);
> + crypt_mem = NULL;
> + crypt_length = 0;
> + if (pgb->gpg_blob.data == NULL) {
> + return ldb_module_oom(io->ac->module);
> + }
> +
> + return LDB_SUCCESS;
> +#else /* ENABLE_GPGME */
> + ldb_debug_set(ldb, LDB_DEBUG_FATAL,
> + "You configured 'password hash gpg key ids', "
> + "but GPGME support is missing. (%s:%d)",
> + __FILE__, __LINE__);
> + return LDB_ERR_UNWILLING_TO_PERFORM;
> +#endif /* else ENABLE_GPGME */
> +}
> +
> static int setup_supplemental_field(struct setup_password_fields_io *io)
> {
> struct ldb_context *ldb;
> struct supplementalCredentialsBlob scb;
> struct supplementalCredentialsBlob _old_scb;
> struct supplementalCredentialsBlob *old_scb = NULL;
> - /* Packages + (Kerberos-Newer-Keys, Kerberos, WDigest and CLEARTEXT) */
> + /* Packages + (Kerberos-Newer-Keys, Kerberos, WDigest, CLEARTEXT, SambaGPG) */
> uint32_t num_names = 0;
> - const char *names[1+4];
> + const char *names[1+5];
> uint32_t num_packages = 0;
> - struct supplementalCredentialsPackage packages[1+4];
> + struct supplementalCredentialsPackage packages[1+5];
> /* Packages */
> struct supplementalCredentialsPackage *pp = NULL;
> struct package_PackagesBlob pb;
> @@ -1433,14 +1560,22 @@ static int setup_supplemental_field(struct setup_password_fields_io *io)
> struct package_PrimaryCLEARTEXTBlob pcb;
> DATA_BLOB pcb_blob;
> char *pcb_hexstr;
> + /* Primary:SambaGPG */
> + const char **ng = NULL;
> + struct supplementalCredentialsPackage *pg = NULL;
> + struct package_PrimarySambaGPGBlob pgb;
> + DATA_BLOB pgb_blob;
> + char *pgb_hexstr;
> int ret;
> enum ndr_err_code ndr_err;
> uint8_t zero16[16];
> bool do_newer_keys = false;
> bool do_cleartext = false;
> + bool do_samba_gpg = false;
>
> ZERO_STRUCT(zero16);
> ZERO_STRUCT(names);
> + ZERO_STRUCT(packages);
>
> ldb = ldb_module_get_ctx(io->ac->module);
>
> @@ -1483,6 +1618,10 @@ static int setup_supplemental_field(struct setup_password_fields_io *io)
> do_cleartext = true;
> }
>
> + if (io->ac->gpg_key_ids != NULL) {
> + do_samba_gpg = true;
> + }
> +
> /*
> * The ordering is this
> *
> @@ -1490,9 +1629,16 @@ static int setup_supplemental_field(struct setup_password_fields_io *io)
> * Primary:Kerberos
> * Primary:WDigest
> * Primary:CLEARTEXT (optional)
> + * Primary:SambaGPG (optional)
> *
> * And the 'Packages' package is insert before the last
> * other package.
> + *
> + * Note: it's important that Primary:SambaGPG is added as
> + * the last element. This is the indication that it matches
> + * the current password. When a password change happens on
> + * a Windows DC, it will keep the old Primary:SambaGPG value,
> + * but as the first element.
> */
> if (do_newer_keys) {
> /* Primary:Kerberos-Newer-Keys */
> @@ -1504,7 +1650,7 @@ static int setup_supplemental_field(struct setup_password_fields_io *io)
> nk = &names[num_names++];
> pk = &packages[num_packages++];
>
> - if (!do_cleartext) {
> + if (!do_cleartext && !do_samba_gpg) {
> /* Packages */
> pp = &packages[num_packages++];
> }
> @@ -1514,14 +1660,25 @@ static int setup_supplemental_field(struct setup_password_fields_io *io)
> pd = &packages[num_packages++];
>
> if (do_cleartext) {
> - /* Packages */
> - pp = &packages[num_packages++];
> + if (!do_samba_gpg) {
> + /* Packages */
> + pp = &packages[num_packages++];
> + }
>
> /* Primary:CLEARTEXT */
> nc = &names[num_names++];
> pc = &packages[num_packages++];
> }
>
> + if (do_samba_gpg) {
> + /* Packages */
> + pp = &packages[num_packages++];
> +
> + /* Primary:SambaGPG */
> + ng = &names[num_names++];
> + pg = &packages[num_packages++];
> + }
> +
> if (pkn) {
> /*
> * setup 'Primary:Kerberos-Newer-Keys' element
> @@ -1640,6 +1797,36 @@ static int setup_supplemental_field(struct setup_password_fields_io *io)
> }
>
> /*
> + * setup 'Primary:SambaGPG' element
> + */
> + if (pg) {
> + *ng = "SambaGPG";
> +
> + ret = setup_primary_samba_gpg(io, &pgb);
> + if (ret != LDB_SUCCESS) {
> + return ret;
> + }
> +
> + ndr_err = ndr_push_struct_blob(&pgb_blob, io->ac, &pgb,
> + (ndr_push_flags_fn_t)ndr_push_package_PrimarySambaGPGBlob);
> + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
> + NTSTATUS status = ndr_map_error2ntstatus(ndr_err);
> + ldb_asprintf_errstring(ldb,
> + "setup_supplemental_field: failed to "
> + "push package_PrimarySambaGPGBlob: %s",
> + nt_errstr(status));
> + return LDB_ERR_OPERATIONS_ERROR;
> + }
> + pgb_hexstr = data_blob_hex_string_upper(io->ac, &pgb_blob);
> + if (!pgb_hexstr) {
> + return ldb_oom(ldb);
> + }
> + pg->name = "Primary:SambaGPG";
> + pg->reserved = 1;
> + pg->data = pgb_hexstr;
> + }
> +
> + /*
> * setup 'Packages' element
> */
> pb.names = names;
> @@ -2595,6 +2782,7 @@ static struct ph_context *ph_init_context(struct ldb_module *module,
> {
> struct ldb_context *ldb;
> struct ph_context *ac;
> + struct loadparm_context *lp_ctx = NULL;
>
> ldb = ldb_module_get_ctx(module);
>
> @@ -2608,6 +2796,10 @@ static struct ph_context *ph_init_context(struct ldb_module *module,
> ac->req = req;
> ac->userPassword = userPassword;
>
> + lp_ctx = talloc_get_type_abort(ldb_get_opaque(ldb, "loadparm"),
> + struct loadparm_context);
> + ac->gpg_key_ids = lpcfg_password_hash_gpg_key_ids(lp_ctx);
> +
> return ac;
> }
>
> @@ -3518,6 +3710,28 @@ static const struct ldb_module_ops ldb_password_hash_module_ops = {
>
> int ldb_password_hash_module_init(const char *version)
> {
> +#ifdef ENABLE_GPGME
> + const char *gversion = NULL;
> +#endif /* ENABLE_GPGME */
> +
> LDB_MODULE_CHECK_VERSION(version);
> +
> +#ifdef ENABLE_GPGME
> + /*
> + * Note: this sets a SIGPIPE handler
> + * if none is active already. See:
> + * https://www.gnupg.org/documentation/manuals/gpgme/Signal-Handling.html#Signal-Handling
> + */
> + gversion = gpgme_check_version(GPGME_VERSION);
> + if (gversion == NULL) {
> + fprintf(stderr, "%s() in %s version[%s]: "
> + "gpgme_check_version(%s) not available, "
> + "gpgme_check_version(NULL) => '%s'\n",
> + __func__, __FILE__, version,
> + GPGME_VERSION, gpgme_check_version(NULL));
> + return LDB_ERR_UNAVAILABLE;
> + }
> +#endif /* ENABLE_GPGME */
> +
> return ldb_register_module(&ldb_password_hash_module_ops);
> }
> diff --git a/source4/dsdb/samdb/ldb_modules/wscript_build_server b/source4/dsdb/samdb/ldb_modules/wscript_build_server
> index aba2d87..bc6903b 100755
> --- a/source4/dsdb/samdb/ldb_modules/wscript_build_server
> +++ b/source4/dsdb/samdb/ldb_modules/wscript_build_server
> @@ -116,7 +116,7 @@ bld.SAMBA_MODULE('ldb_password_hash',
> init_function='ldb_password_hash_module_init',
> module_init_name='ldb_init_module',
> internal_module=False,
> - deps='talloc samdb LIBCLI_AUTH NDR_DRSBLOBS authkrb5 krb5 DSDB_MODULE_HELPERS'
> + deps='talloc samdb LIBCLI_AUTH NDR_DRSBLOBS authkrb5 krb5 gpgme DSDB_MODULE_HELPERS'
> )
>
>
> --
> 1.9.1
>
>
> From ccfa0938bc4994633f52a33caf17803f55fef3aa Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Fri, 22 Jan 2016 21:52:26 +0100
> Subject: [PATCH 17/22] samba-tool: add --decrypt-samba-gpg support to 'user
> getpasswords' and 'user syncpasswords'
>
> This get's the cleartext passwords by decrypting
> the 'Primary:SambaGPG' value in order to provide the
> virtual attributes: virtualClearTextUTF16, virtualClearTextUTF8,
> virtualCryptSHA256, virtualCryptSHA512, virtualSSHA
>
> The virtual attribute virtualSambaGPG provides the raw
> (encrypted) value of the 'Primary:SambaGPG' value.
>
> See the "password hash gpg key ids" option for the encryption part
> of this feature.
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> python/samba/netcmd/user.py | 139 ++++++++++++++++++++++++++++++++++++++++----
> 1 file changed, 128 insertions(+), 11 deletions(-)
>
> diff --git a/python/samba/netcmd/user.py b/python/samba/netcmd/user.py
> index 0fe7c54..0378c5b 100644
> --- a/python/samba/netcmd/user.py
> +++ b/python/samba/netcmd/user.py
> @@ -50,6 +50,18 @@ from samba.netcmd import (
> Option,
> )
>
> +
> +try:
> + import io
> + import gpgme
> + gpgme_support = True
> + decrypt_samba_gpg_help = "Decrypt the SambaGPG password as cleartext source"
> +except ImportError as e:
> + gpgme_support = False
> + decrypt_samba_gpg_help = "Decrypt the SambaGPG password not supported, " + \
> + "python-gpgme requires"
> + pass
> +
> disabled_virtual_attributes = {
> }
>
> @@ -60,6 +72,9 @@ virtual_attributes = {
> "virtualClearTextUTF16": {
> "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
> },
> + "virtualSambaGPG": {
> + "flags": ldb.ATTR_FLAG_FORCE_BASE64_LDIF,
> + },
> }
>
> def check_random():
> @@ -809,8 +824,8 @@ class GetPasswordCommand(Command):
>
> return samdb
>
> - def get_account_attributes(self, samdb, username,
> - basedn, filter, scope, attrs):
> + def get_account_attributes(self, samdb, username, basedn, filter, scope,
> + attrs, decrypt):
>
> require_supplementalCredentials = False
> search_attrs = attrs[:]
> @@ -857,16 +872,50 @@ class GetPasswordCommand(Command):
> if add_sAMAcountName:
> del obj["sAMAccountName"]
>
> - def get_package(name):
> + calculated = {}
> + 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
>
> + if decrypt:
> + # Samba add 'Primary:SambaGPG' at the end.
> + # When Windows sets the password it keeps
> + # 'Primary:SambaGPG' and rotates it to
> + # the begining. So we can only use the value,
> + # if it is the last one.
> + sgv = get_package("Primary:SambaGPG", min_idx=-1)
> + if sgv is not None:
> + ctx = gpgme.Context()
> + ctx.armor = True
> + cipher_io = io.BytesIO(sgv)
> + plain_io = io.BytesIO()
> + try:
> + ctx.decrypt(cipher_io, plain_io)
> + cv = plain_io.getvalue()
> + calculated["Primary:CLEARTEXT"] = cv
> + except gpgme.GpgmeError as (major, minor, msg):
> + if major == gpgme.ERR_BAD_SECKEY:
> + msg = "ERR_BAD_SECKEY: " + msg
> + else:
> + msg = "MAJOR:%d, MINOR:%d: %s" % (major, minor, msg)
> + self.outf.write("WARNING: '%s': SambaGPG can't be decrypted into CLEARTEXT: %s\n" % (
> + username or account_name, msg))
> + pass
> +
> def get_utf8(a, b, username):
> try:
> u = unicode(b, 'utf-16-le')
> @@ -924,6 +973,15 @@ class GetPasswordCommand(Command):
> continue
> sv = get_crypt_value("6", u8)
> v = "{CRYPT}" + sv
> + elif a == "virtualSambaGPG":
> + # Samba add 'Primary:SambaGPG' at the end.
> + # When Windows sets the password it keeps
> + # 'Primary:SambaGPG' and rotates it to
> + # the begining. 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
> else:
> continue
> obj[a] = ldb.MessageElement(v, ldb.FLAG_MOD_REPLACE, a)
> @@ -970,7 +1028,8 @@ 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' buffer inside of 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.
> @@ -989,6 +1048,20 @@ for which virtual attributes are supported in your environment):
> checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
> with a $6$... salt, see crypt(3) on modern systems.
>
> + 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.
> +
> Example1:
> samba-tool user getpassword TestUser1 --attributes=pwdLastSet,virtualClearTextUTF8
>
> @@ -1013,15 +1086,21 @@ samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-
> 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,
> + attributes=None, decrypt_samba_gpg=None,
> sambaopts=None, versionopts=None):
> self.lp = sambaopts.get_loadparm()
>
> + if decrypt_samba_gpg and not gpgme_support:
> + raise CommandError(decrypt_samba_gpg_help)
> +
> if filter is None and username is None:
> raise CommandError("Either the username or '--filter' must be specified!")
>
> @@ -1039,7 +1118,8 @@ samba-tool user getpassword --filter=samaccountname=TestUser3 --attributes=msDS-
> basedn=None,
> filter=filter,
> scope=ldb.SCOPE_SUBTREE,
> - attrs=password_attrs)
> + attrs=password_attrs,
> + decrypt=decrypt_samba_gpg)
>
> ldif = samdb.write_ldif(obj, ldb.CHANGETYPE_NONE)
> self.outf.write("%s" % ldif)
> @@ -1051,7 +1131,8 @@ class cmd_user_syncpasswords(GetPasswordCommand):
> This syncs logon passwords for user accounts.
>
> Note that this command should run on a single domain controller only
> -(typically the PDC-emulator).
> +(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
> @@ -1069,7 +1150,7 @@ 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: '--script', '--filter' or
> +optional options: '--decrypt-samba-gpg', '--script', '--filter' or
> '-H/--URL'.
>
> The '--attributes' parameter takes a comma separated list of attributes,
> @@ -1080,7 +1161,8 @@ 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' buffer inside of 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.
> @@ -1099,6 +1181,20 @@ for supported virtual attributes in your environment):
> checksum, useful for OpenLDAP's '{CRYPT}' algorithm,
> with a $6$... salt, see crypt(3) on modern systems.
>
> + 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.
> @@ -1201,6 +1297,9 @@ samba-tool user syncpasswords --terminate \\
> 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",
> @@ -1217,7 +1316,7 @@ samba-tool user syncpasswords --terminate \\
>
> def run(self, cache_ldb_initialize=False, cache_ldb=None,
> H=None, filter=None,
> - attributes=None,
> + attributes=None, decrypt_samba_gpg=None,
> script=None, nowait=None, logfile=None, daemon=None, terminate=None,
> sambaopts=None, versionopts=None):
>
> @@ -1230,6 +1329,8 @@ samba-tool user syncpasswords --terminate \\
> 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:
> @@ -1299,6 +1400,9 @@ samba-tool user syncpasswords --terminate \\
> if H is None:
> H = "ldapi://%s" % os.path.abspath(self.lp.private_path("ldap_priv/ldapi"))
>
> + if decrypt_samba_gpg and not gpgme_support:
> + 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
> @@ -1349,6 +1453,7 @@ samba-tool user syncpasswords --terminate \\
> "dirsyncAttribute",
> "dirsyncControl",
> "passwordAttribute",
> + "decryptSambaGPG",
> "syncCommand",
> "currentPid",
> ]
> @@ -1376,6 +1481,7 @@ samba-tool user syncpasswords --terminate \\
> 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
> add_ldif += "objectClass: userSyncPasswords\n"
> @@ -1386,6 +1492,10 @@ samba-tool user syncpasswords --terminate \\
> add_ldif += "dirsyncControl: %s\n" % self.dirsync_controls[0]
> for a in self.password_attrs:
> add_ldif += "passwordAttribute:: %s\n" % base64.b64encode(a)
> + if self.decrypt_samba_gpg == True:
> + 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()))
> @@ -1405,6 +1515,12 @@ samba-tool user syncpasswords --terminate \\
> self.password_attrs = []
> for a in res[0]["passwordAttribute"]:
> self.password_attrs.append(a)
> + decrypt_string = 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 = res[0]["syncCommand"][0]
> else:
> @@ -1462,7 +1578,8 @@ samba-tool user syncpasswords --terminate \\
> basedn="<GUID=%s>" % guid,
> filter="(objectClass=user)",
> scope=ldb.SCOPE_BASE,
> - attrs=self.password_attrs)
> + 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:
> --
> 1.9.1
>
>
> From b5e6f0b273d3431388a4e206998ee50a1723d188 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 12 Jan 2016 13:51:00 +0100
> Subject: [PATCH 18/22] selftest:gnupg: add a gpg key for Samba Selftest
> <selftest at samba.example.com>
>
> This key doesn't have a passphrase and allows automatic testing
> of decryption.
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> .gitignore | 1 +
> selftest/gnupg/gpg.conf | 4 ++++
> selftest/gnupg/pubring.gpg | Bin 0 -> 1214 bytes
> selftest/gnupg/secring.gpg | Bin 0 -> 2516 bytes
> selftest/gnupg/trustdb.gpg | Bin 0 -> 1280 bytes
> 5 files changed, 5 insertions(+)
> create mode 100644 selftest/gnupg/gpg.conf
> create mode 100644 selftest/gnupg/pubring.gpg
> create mode 100644 selftest/gnupg/secring.gpg
> create mode 100644 selftest/gnupg/trustdb.gpg
>
> diff --git a/.gitignore b/.gitignore
> index a4c2a69..5870140 100644
> --- a/.gitignore
> +++ b/.gitignore
> @@ -16,6 +16,7 @@ source3/.clang_complete
> *.patch
> *.pyc
> semantic.cache
> +selftest/gnupg/random_seed
> pidl/blib
> pidl/cover_db
> pidl/Makefile
> diff --git a/selftest/gnupg/gpg.conf b/selftest/gnupg/gpg.conf
> new file mode 100644
> index 0000000..33b9f9f
> --- /dev/null
> +++ b/selftest/gnupg/gpg.conf
> @@ -0,0 +1,4 @@
> +
> +keyid-format long
> +fingerprint
> +default-key 4952E40301FAB41A
> diff --git a/selftest/gnupg/pubring.gpg b/selftest/gnupg/pubring.gpg
> new file mode 100644
> index 0000000000000000000000000000000000000000..b3fa9ccc547cd125ddae5fc00a2e2075ef669964
> GIT binary patch
> literal 1214
> zc-jHJ1VQ_m0SyFJm1KMY2ms-X8f0hnqKH(%6n51&uP3(6+vtkL2;*?1DyswNmT~eI
> zE0Kjt7tO%@7h}c<IeIf9OYxcgsg|f>J$~2Bs+|JIzRsidEo7<Qp?ijk$+QGtOTIZL
> z;-)ql`Lh~H<Kr*AttnVgZdHEm4WSD8+nAn$=^KW8S#GRW1Y8QxYNcf0SFSP&L9h2n
> z^lTW}6N+;We1FFeUIPuF_svb}15Xv#xAk`3L6qva?GUYQ%VRjmAAWqf+Ry)PA2G53
> zw1q?a{_cuS27>Fzm>k-tolUY9k#dnB at 1>D>GTP02BT4;5qG?lSGz|xm25tAJ9fzia
> zS*1YoCkdP)j4>lM)HeVT0RRECD^p=@VqqXtWo%}2Wpi{OJac7iW^`q9bU<@qZDL_A
> zWq4t2aBO8RV{dIfi2*(Y69EDMC<Ovmm1KMZ8v_LiF8&9A1`7!Y2Ll2I6$k<e3JU}l
> z0s{d89svRufB*^!5J^(x0|EN98hfM&0Ebl;?X7H@<<^6Nfj3=<D5_;pcxvM6a!S#M
> za9rS#d4?Det$A)hG+n?-R`Yx1z*K;-6sqhuVsrphMT-<?MJSuzcRXE#{AwvcCJp~g
> zYY3RqifioJe#CV&jz at Q3g$^E&8y at MHe6&^8#b!teLqI=Ee>+~Grh(*TA|DQe7*odR
> z3&spq3+?1<k9dEgEBlTr{zDab6y(<{g|=%Zxi=c)BcHokpEnHvc}K4TL^xI+QPL(0
> zzU00vK4_(jRH+tc+p9((SoJ&va6Bk|%uwQszx1f&oq5|v8cG5vgLCI2Ma07l*XI<d
> z{m`%LwQ`RyzGe+Sa?Ppy7hKcSX|MtS1GxbW1Xh(~d;tgm=X at b)M5Uv*vGqB8c+_OP
> z0NFlSx9Je^m%Q*U3BeVP8HkgMw+R~4oXp}0iz_#wrd~<uSU+MVBkXe|J={-gn`Gb}
> zYP(F%+m53*<=12L0(H{mo|_xn4%FMQyF(gRdh2trF_?g<ZNCmkO*bxC1T~nnq5T0o
> z*4IQJ53f=5BB)$ermWYsHXR!m_YuvsG29|UzHa!l7_zSmTQ23#%#0T=6y+3?FxCi%
> zj|m4&QlRIEco<l$$)gB4_(aW at Y;`%bZ35E7s_0=WA|XA#9rn8B0n#IL<F>SPSyO4K
> z2{BC_ZJed?GWO1V at pb5I$l^92Z6iJ6!A|0n at F!W>01*KI0f_-61Q-DV01pKMR+VIY
> z0vikk2`>HzfB*^!5J^(x0|EN98XOS^`U6>eKq2q%Uiu}q$vxpjk#NGYTmh_ at hBvsp
> zy^ebF=y%PnZJePn+hJjksXjcAh1(<6O#Nkngqc3YnN7iTgHQtpLaoV%o#vvC7xm4i
> zoOen+#${$}vG9?~NX at 5DTO-R&W_5|qRK~M&=fYx;;{I<;2A9s={!@IO9DMxsLg7Ru
> zhevTy3Co~w_W9#DV(*f){B1R18Zw>Wwt&-9Im2h4NYAGrMt%w(EEdfG9cw*#X^{{G
> z;0uLtjd5(Dr&%WO7Jln5<f_qQz63|oP+CZw<lG9@&zXHRVo_c^c75%FEKp7!UenV{
> cJdkshVP{+3B`5IdDqLdAgp#ju39teH11G#8&Hw-a
>
> literal 0
> Hc-jL100001
>
> diff --git a/selftest/gnupg/secring.gpg b/selftest/gnupg/secring.gpg
> new file mode 100644
> index 0000000000000000000000000000000000000000..09dd9fd9fa75494fdad1ff9b39fc29ccbb58755c
> GIT binary patch
> literal 2516
> zc-jHf2`l!M1DFI>m1KMY2ms-X8f0hnqKH(%6n51&uP3(6+vtkL2;*?1DyswNmT~eI
> zE0Kjt7tO%@7h}c<IeIf9OYxcgsg|f>J$~2Bs+|JIzRsidEo7<Qp?ijk$+QGtOTIZL
> z;-)ql`Lh~H<Kr*AttnVgZdHEm4WSD8+nAn$=^KW8S#GRW1Y8QxYNcf0SFSP&L9h2n
> z^lTW}6N+;We1FFeUIPuF_svb}15Xv#xAk`3L6qva?GUYQ%VRjmAAWqf+Ry)PA2G53
> zw1q?a{_cuS27>Fzm>k-tolUY9k#dnB at 1>D>GTP02BT4;5qG?lSGz|xm25tAJ9fzia
> zS*1YoCkdP)j4>lM)HeVT0RRC22mT;956!|@tMEZjEsq8snh@>7s2I^>MsP);(2d`}
> z<h+04fN=7&JO!82pl1)rXZ$bx84%867Ium>-4a%_3b&9Q at qnc7kw{0#c0cAcZH^yt
> z8Y!U?G_Z1-fHm#|nsroy3J;aOi0oHO%&8o7#}}OW7z~lMvI%<=$=;)C)T5LKr`Xe-
> z;(I`ciVV*jI4$yE#Gd1Mf1%ioFM7Gfq5I^qCmn^RlD?8VZ>?IHzEeVG45i!1l|TlG
> zb{hxwKNf$Y)vwmVbTm8p9M`&QD4 at Uuos<!f+><VM4g at H?X7!+jg(}tc2B<m$f6bCY
> zE~1pHKw65tLrW*vkc6hcE`0<5<gvnTNx}3>6OnFk9h4Q*R^6`y3f;GM6`;_un)1{Y
> zn4vko;>Y3VnI$#Hy7a<v`270L1P00-FzQ!s0%h4XNClYh-*u`B{|6T>tvub8w;%I7
> zLs0WOzxPvTU1i&8KC_4w!H1ZYQdyl;>y%j1H4r4Oj?#d`bON3bBbZ1X1OWVcCSfS@
> zpL+6?fK&NAZt5-<FV%&h8CCi-b=)B9UUUw$qV%L_=$Tci3 at -OzzfjSWNdb_q#c1;E
> z(vaU)R&yXJt_cEzx~v;4dyEd;siLjeMSj3C<EFLHU}D~LH-0OomX}B at RC791dF&WK
> z&#;nhpOMRJDS=tGt4Api<zoZ>Gcxu5Jvp_jQes5^F2+o&s^fu at izPNmM7Rs{7qUGx
> z2$gj7914BcY!qW)V6W#Xid;#N3d3<&Jvq6>$63AQ^@KU|$wzb}wd|xnhOk=a;&wKY
> zt!Tt{W?mo))g$)nqr%IuDwm`cLxkAFn4y5G?Dpj{aEfJdQvLAUEDX6BLJzbnQ(<jl
> zVIWgwY-V(2b95j)b7gF1bY*jNKyzVjVqq?2cwudDY-KKEZ*4w_0X_s10RjLh1p-!;
> zWPAb}0|f~#{s({t3ke7Z0|EvW2m%QT3j`Jd0|5da0Rk6*0162ZNmAqk0s6EWd!z^e
> zhgBBst!$d*)`Nk8H(iJ*s%21kYU1j0O3{aKT;P#;h8Pg7d2T>7UBF6K^LypMRDiJ*
> zs_ZvnbO2OEixg)?D4X7QJY9qQYAHY_4gXAQ2$<4}YwX&7#C0@|M|WU_4jzyj9_g5T
> zv{lx{W=IJ`KtD@=J6 at rtf#hW(9}a^UQ^x2E#tc>q?c{5Zcz>cR`;IF9Llt-w<ku^O
> zwreK2HyYz3pSxP0Hw^%JN3Q}zI947}(k2VO<i0FEXr+r(sTOD3t41JL^*jV{JSct4
> zP~wZf^r+;WdD})BN&+Z at bLS&P#KR2N=M<>@(68&Ya*r>*W(_}b&8hqsT+`HPumS)8
> zodcKzR+VIY0SEx+d?9H>rK7j8^*MZa)MUH>**;mf=@9Xkyznjw!4-}fh?9%A2^!O!
> z%;E`)D>tF0UP<X#KVl^#>~kbN+)rzpWZ)fYyG+j8j-xl_*JJYnb<*XYn;Y8>)Z4JT
> zLmF3l>vOO%n1HEmzYa)EH!fKOHJG%a{Q*4I*F+!>uTk_Os9aX2tk<<R9UB+-5zVwQ
> z+#*B1ZuqkpvabtUF6Gb6j2ABy<rI at J)(D4>2?tG5py!Bq7+9>yqX;?pM9r6Mbvd+c
> z0 at B2)=wT}&Aw9kw_PXW)(j#- at wzPCvQ)#FPF-;w9oTc$H_Rf6qb?9u!;x-^{BR%55
> zPU4gBCt2A55di=J00;dTQ6XPq4>oEOA1oP1#v};c+w@^iVFTYKE&E#%x;{<qpS#2-
> z2c=R<d-+(vPFrg?LtAH1R%A4Lm>8y8dpo+7ncTA~WuKLCS_BGMvMx0?a&se`>@`|v
> zE%YE3Bj4@?qZ>txh&3nII0ZwJ9Q0W9h`!Kf-Qktz3AqK=FZUluscw)R=0Fc|`+QK|
> z!$l_7*w<$u{ie8aC3Y-$=5lPaXtr(=^uk0_le%G7*Y>0Dn)Gss40zJ at o=Hm(xsJ?T
> zN++L-p%0sgkY~%S##^0m at LPnqp)8%Hf=j%iQApsyI$x1EfN%qW7AZw%g50(1A}i;?
> zX75T6mN|MvIT~vO0P-bY-qO`m(A)k0u8!NuqK2yGXI(-(Q%}oK(s0{1x#B1tF!A|D
> zqLS_d2hxab%w`;@yhp*?$Es?0Feu0a0&G<1F~3<#TG9dpiz^SN6&F->|ETk$t`J>?
> zID`u1^7&GS8Zn+(YUN0nisOm{@W<Q#^_}T97I~MQ`tx$gPXqw;x|>Lcl$CFwb)X!X
> zJM%bFlG==doVuw2$Rn*$_T)3}7y0(3+Y4C)`zM_dBz`um$s8m;H6uQp82T_c&oMx*
> zCbz3ITU>e6ma at J5M)|%x>90C5n+_Hla=mj;q{Lm)bXLx_KWN0x!xN~LzmH8n>sSzF
> zj`;CSX}lo{9f?;20HV$9ZV&MrqW at 8awWFpo{cP7sX<;lX%nu-^CiCw><nW<<TxgM-
> zD!-<?adqfeTNpuf-&B98(pgHJdf6LA+yV*K at -FXhxO4mROOz`D)`7yWH#lopE<txZ
> zXx<ZRTpWJH^CJcv9ol~bvN#hxAxnwn_Wm5q0!sT at H}MrwtvP#%0VM<&0RjLI1p-!;
> zWPAb}3<U`;{s({n3JDNNQse^x`m`Dx5eNDMS$jYs at 9$pvCAG;t;Y5*e!m?ZeteA#3
> zxV*iNdh+OZ&8=;mp)lKFVUMXkJdlOkBi2m)Wr2j5KE;_$!E}RA0|!E_$%mchqK_B#
> z&8D1pN<GG9W^1wVk;+KTr%ziW%S~o=iOy8UvvcRdVvyqgZ%hW4&fWe~e4ZS9{PjZN
> zL?wquaZw4&pl|m1<2YjPlC=D7HDMYuo#3{B(^EOaXP-#Vryxdt3Lh*M%>W&1J$Y%7
> z5Cz~1g>a2=Y at w%FCh!)1>o4T0(PO>@N77JQNSx%{3f9k=eKcZGUOaYv?Sd>&P99#<
> e(@Q*%bCqFdTizun at aZaCV#|b*uW<>m0ssItu%B-L
>
> literal 0
> Hc-jL100001
>
> diff --git a/selftest/gnupg/trustdb.gpg b/selftest/gnupg/trustdb.gpg
> new file mode 100644
> index 0000000000000000000000000000000000000000..bfe8f0689daf367f65de4e9054107d9c9c74a1c3
> GIT binary patch
> literal 1280
> zc-mu3FGy!*W at Ke#VqgfHnyS}2Ir|R-CSc at ZAP$VG8&x-|Zd4sv>f{Kk<6-#Yv6{<s
> zvCXUNUp-PigPt%m{@NnN29cCUsN-e05F6a)zQJJY#s0`D?vRZVU(Awf*)Yrl01lrS
> A<^TWy
>
> literal 0
> Hc-jL100001
>
> --
> 1.9.1
>
>
> From 7ce8c7c7bf4ebbe94ad09ca0a42c1e7e701c3387 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 16 Feb 2016 10:04:40 +0100
> Subject: [PATCH 19/22] s4:selftest: run samba.tests.samba_tool.user also
> against ad_dc:local
>
> In future ad_dc_ntvfs and ad_dc will differ regarding the Primary:SambaGPG
> password feature. So we should test both.
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> source4/selftest/tests.py | 1 +
> 1 file changed, 1 insertion(+)
>
> diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
> index 32d47a0..633e31f 100755
> --- a/source4/selftest/tests.py
> +++ b/source4/selftest/tests.py
> @@ -486,6 +486,7 @@ planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.gpo")
>
> planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.processes")
> planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.user")
> +planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.user")
> planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.group")
> planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.ntacl")
>
> --
> 1.9.1
>
>
> From 7a7b76ae4b3d99b8d5135c7f54341437ab3122d6 Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 12 Jan 2016 13:51:00 +0100
> Subject: [PATCH 20/22] selftest:Samba4: configure "password hash gpg key ids"
> for ad_dc (if available)
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> selftest/selftest.pl | 1 +
> selftest/target/Samba4.pm | 32 ++++++++++++++++++++++++++++++++
> 2 files changed, 33 insertions(+)
>
> diff --git a/selftest/selftest.pl b/selftest/selftest.pl
> index 0827376..012c4fc 100755
> --- a/selftest/selftest.pl
> +++ b/selftest/selftest.pl
> @@ -320,6 +320,7 @@ $ENV{KRB5CCNAME} = "$prefix/krb5ticket";
> $ENV{PREFIX_ABS} = $prefix_abs;
> $ENV{SRCDIR} = $srcdir;
> $ENV{SRCDIR_ABS} = $srcdir_abs;
> +$ENV{GNUPGHOME} = "$srcdir_abs/selftest/gnupg";
> $ENV{BINDIR} = $bindir_abs;
>
> my $tls_enabled = not $opt_quick;
> diff --git a/selftest/target/Samba4.pm b/selftest/target/Samba4.pm
> index c1c3967..7df8a27 100755
> --- a/selftest/target/Samba4.pm
> +++ b/selftest/target/Samba4.pm
> @@ -1784,6 +1784,27 @@ sub provision_rodc($$$)
> return $ret;
> }
>
> +sub read_config_h($)
> +{
> + my ($name) = @_;
> + my %ret = {};
> + open(LF, "<$name") or die("unable to read $name: $!");
> + while (<LF>) {
> + chomp;
> + next if not (/^#define /);
> + if (/^#define (.*?)[ \t]+(.*?)$/) {
> + $ret{$1} = $2;
> + next;
> + }
> + if (/^#define (.*?)[ \t]+$/) {
> + $ret{$1} = 1;;
> + next;
> + }
> + }
> + close(LF);
> + return \%ret;
> +}
> +
> sub provision_ad_dc($$)
> {
> my ($self, $prefix) = @_;
> @@ -1797,6 +1818,15 @@ sub provision_ad_dc($$)
> my $require_mutexes = "dbwrap_tdb_require_mutexes:* = yes";
> $require_mutexes = "" if ($ENV{SELFTEST_DONT_REQUIRE_TDB_MUTEX_SUPPORT} eq "1");
>
> + my $config_h = {};
> +
> + if (defined($ENV{CONFIG_H})) {
> + $config_h = read_config_h($ENV{CONFIG_H});
> + }
> +
> + my $password_hash_gpg_key_ids = "password hash gpg key ids = selftest\@samba.example.com";
> + $password_hash_gpg_key_ids = "" unless defined($config_h->{HAVE_GPGME});
> +
> my $extra_smbconf_options = "
> server services = -smb +s3fs
> xattr_tdb:file = $prefix_abs/statedir/xattr.tdb
> @@ -1804,6 +1834,8 @@ sub provision_ad_dc($$)
> dbwrap_tdb_mutexes:* = yes
> ${require_mutexes}
>
> + ${password_hash_gpg_key_ids}
> +
> kernel oplocks = no
> kernel change notify = no
>
> --
> 1.9.1
>
>
> From 0f2058630330dae690749e5f12c9868f9abf420f Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Tue, 16 Feb 2016 03:19:58 +0100
> Subject: [PATCH 21/22] python:samba/tests: use 'samba-tool user
> {getpassword,syncpasswords}' with --decrypt-samba-gpg
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> python/samba/tests/samba_tool/user.py | 25 ++++++++++++++++++++++---
> 1 file changed, 22 insertions(+), 3 deletions(-)
>
> diff --git a/python/samba/tests/samba_tool/user.py b/python/samba/tests/samba_tool/user.py
> index aad323d..b9f318e 100644
> --- a/python/samba/tests/samba_tool/user.py
> +++ b/python/samba/tests/samba_tool/user.py
> @@ -114,10 +114,11 @@ class UserCmdTestCase(SambaToolCmdTest):
> self.assertEquals(err,"","setpassword with url")
> self.assertMatch(out, "Changed password OK", "setpassword with url")
>
> - attributes = "sAMAccountName,unicodePwd,supplementalCredentials"
> + attributes = "sAMAccountName,unicodePwd,supplementalCredentials,virtualClearTextUTF8,virtualClearTextUTF16,virtualSSHA,virtualSambaGPG"
> (result, out, err) = self.runsubcmd("user", "syncpasswords",
> "--cache-ldb-initialize",
> - "--attributes=%s" % attributes)
> + "--attributes=%s" % attributes,
> + "--decrypt-samba-gpg")
> self.assertCmdSuccess(result, "Ensure syncpasswords --cache-ldb-initialize runs")
> self.assertEqual(err,"","getpassword without url")
> cache_attrs = {
> @@ -127,6 +128,7 @@ class UserCmdTestCase(SambaToolCmdTest):
> "dirsyncAttribute": { },
> "dirsyncControl": { "value": "dirsync:1:0:0"},
> "passwordAttribute": { },
> + "decryptSambaGPG": { },
> "currentTime": { },
> }
> for a in cache_attrs.keys():
> @@ -150,6 +152,8 @@ class UserCmdTestCase(SambaToolCmdTest):
> creds.set_password(newpasswd)
> nthash = creds.get_nt_hash()
> unicodePwd = base64.b64encode(creds.get_nt_hash())
> + virtualClearTextUTF8 = base64.b64encode(newpasswd)
> + virtualClearTextUTF16 = base64.b64encode(unicode(newpasswd, 'utf-8').encode('utf-16-le'))
>
> (result, out, err) = self.runsubcmd("user", "setpassword",
> user["name"],
> @@ -173,10 +177,18 @@ class UserCmdTestCase(SambaToolCmdTest):
> "getpassword '# supplementalCredentials::: REDACTED SECRET ATTRIBUTE': out[%s]" % out)
> self.assertMatch(out, "supplementalCredentials:: ",
> "getpassword supplementalCredentials: out[%s]" % out)
> + if "virtualSambaGPG:: " in out:
> + self.assertMatch(out, "virtualClearTextUTF8:: %s" % virtualClearTextUTF8,
> + "getpassword virtualClearTextUTF8: out[%s]" % out)
> + self.assertMatch(out, "virtualClearTextUTF16:: %s" % virtualClearTextUTF16,
> + "getpassword virtualClearTextUTF16: out[%s]" % out)
> + self.assertMatch(out, "virtualSSHA: ",
> + "getpassword virtualSSHA: out[%s]" % out)
>
> (result, out, err) = self.runsubcmd("user", "getpassword",
> user["name"],
> - "--attributes=%s" % attributes)
> + "--attributes=%s" % attributes,
> + "--decrypt-samba-gpg")
> self.assertCmdSuccess(result, "Ensure getpassword runs")
> self.assertEqual(err,"","getpassword without url")
> self.assertMatch(out, "Got password OK", "getpassword without url")
> @@ -186,6 +198,13 @@ class UserCmdTestCase(SambaToolCmdTest):
> "getpassword unicodePwd: out[%s]" % out)
> self.assertMatch(out, "supplementalCredentials:: ",
> "getpassword supplementalCredentials: out[%s]" % out)
> + if "virtualSambaGPG:: " in out:
> + self.assertMatch(out, "virtualClearTextUTF8:: %s" % virtualClearTextUTF8,
> + "getpassword virtualClearTextUTF8: out[%s]" % out)
> + self.assertMatch(out, "virtualClearTextUTF16:: %s" % virtualClearTextUTF16,
> + "getpassword virtualClearTextUTF16: out[%s]" % out)
> + self.assertMatch(out, "virtualSSHA: ",
> + "getpassword virtualSSHA: out[%s]" % out)
>
> for user in self.users:
> newpasswd = self.randomPass()
> --
> 1.9.1
>
>
> From d3252f6b41506f456ddcf4e17c13259ca95539bf Mon Sep 17 00:00:00 2001
> From: Stefan Metzmacher <metze at samba.org>
> Date: Wed, 17 Feb 2016 10:07:27 +0100
> Subject: [PATCH 22/22] WHATSNEW: add 'Password sync as active directory domain
> controller'
>
> Signed-off-by: Stefan Metzmacher <metze at samba.org>
> ---
> WHATSNEW.txt | 14 ++++++++++++++
> 1 file changed, 14 insertions(+)
>
> diff --git a/WHATSNEW.txt b/WHATSNEW.txt
> index e7579b9..37b2d05 100644
> --- a/WHATSNEW.txt
> +++ b/WHATSNEW.txt
> @@ -33,6 +33,19 @@ The ldap server has support for the LDAP_SERVER_NOTIFICATION_OID
> control. This can be used to monitor the active directory database
> for changes.
>
> +Password sync as active directory domain controller
> +---------------------------------------------------
> +
> +The new commands 'samba-tool user getpassword'
> +and 'samba-tool user syncpasswords' provide
> +access and syncing of various password fields.
> +
> +If compiled with GPGME support (--with-gpgme) it's
> +possible to store cleartext passwords in a PGP/OpenGPG
> +encrypted form by configuring the new "password hash gpg key ids"
> +option.
> +
> +
> TODO...
>
>
> @@ -47,6 +60,7 @@ smb.conf changes
>
> Parameter Name Description Default
> -------------- ----------- -------
> + password hash gpg key ids New
>
>
> KNOWN ISSUES
> --
> 1.9.1
>
--
/ Alexander Bokovoy
More information about the samba-technical
mailing list