[PATCH] samba-tool schema attribute query_oc

William Brown william at blackhats.net.au
Sat May 19 02:28:44 UTC 2018


On Fri, 2018-05-18 at 18:51 +1200, Andrew Bartlett via samba-technical
wrote:
> On Fri, 2018-05-18 at 16:45 +1000, William Brown wrote:
> > On Fri, 2018-05-18 at 18:39 +1200, Andrew Bartlett wrote:
> > > You can just push a string value or set of string values back
> > > into
> > > the
> > > ldb message and then print it.  It isn't read-only.
> > 
> > So get the flags, change them, then push to write_ldif? This seems
> > too
> > easy ;) 
> 
> :-)

It was not so easy. See my comments in the code .... 

> 
> > > We do that for plenty of other attributes.  See lib/ldb-
> > > samba/ldif_handlers.c
> > 
> > Yes, but we'd be adding an attr that's "not in schema" technically.
> > I
> > think your solution above is better. 
> 
> It certainly is easier to keep this in python for now.
> 
> > > Before I look at these again, can you push them to some kind of
> > > CI,
> > > either github or gitlab?
> > 
> > Sure, will do. I've been careful to run make test with these, but
> > CI is
> > good too :) 


https://gitlab.com/Firstyear/samba/pipelines/22291185

Branch is:

https://gitlab.com/Firstyear/samba/commits/configure

Patches attached (with --stdout!) :) 



Thanks again for your patience reviewing this! Hoping to get started on
something else soon ;) 

William

-------------- next part --------------
From 2dd20ad0844030d6e588e1ea389de40a937a69e7 Mon Sep 17 00:00:00 2001
From: William Brown <william at blackhats.net.au>
Date: Wed, 25 Apr 2018 17:36:17 +1000
Subject: [PATCH 1/7] python/samba/netcmd/{forest.py,main.py}: add
 configuration controls

With samba-tool we should expose ways to easily administer and control
common configuration options. This adds the base framework for modifying
forest settings, generally stored in cn=configuration partition.

An example is:

samba-tool forest directory_service show
samba-tool forest directory_service dsheuristics X

Signed-off-by: William Brown <william at blackhats.net.au>
---
 docs-xml/manpages/samba-tool.8.xml      |  20 +++
 python/samba/netcmd/forest.py           | 164 ++++++++++++++++++++++++
 python/samba/netcmd/main.py             |   1 +
 python/samba/tests/samba_tool/forest.py |  63 +++++++++
 source4/selftest/tests.py               |   1 +
 5 files changed, 249 insertions(+)
 create mode 100644 python/samba/netcmd/forest.py
 create mode 100644 python/samba/tests/samba_tool/forest.py

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index fa132d85d53..fd9426ea608 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -424,6 +424,26 @@
 	<para>Add access control entries to a directory object.</para>
 </refsect3>
 
+<refsect2>
+	<title>forest</title>
+	<para>Manage Forest configuration.</para>
+</refsect2>
+
+<refsect3>
+	<title>forest directory_service</title>
+	<para>Manage directory_service behaviour for the forest.</para>
+</refsect3>
+
+<refsect3>
+	<title>forest directory_service dsheuristics <replaceable>VALUE</replaceable></title>
+	<para>Modify dsheuristics directory_service configuration for the forest.</para>
+</refsect3>
+
+<refsect3>
+	<title>forest directory_service show</title>
+	<para>Show current directory_service configuration for the forest.</para>
+</refsect3>
+
 <refsect2>
 	<title>fsmo</title>
 	<para>Manage Flexible Single Master Operations (FSMO).</para>
diff --git a/python/samba/netcmd/forest.py b/python/samba/netcmd/forest.py
new file mode 100644
index 00000000000..b56c410f2fe
--- /dev/null
+++ b/python/samba/netcmd/forest.py
@@ -0,0 +1,164 @@
+# domain management
+#
+# Copyright William Brown <william at blackhats.net.au> 2018
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import ldb
+import samba.getopt as options
+from samba.auth import system_session
+from samba.samdb import SamDB
+from samba.netcmd import (
+    Command,
+    CommandError,
+    SuperCommand,
+    Option
+    )
+
+class cmd_forest_show(Command):
+    """Display forest settings.
+
+    These settings control the behaviour of all domain controllers in this
+    forest. This displays those settings from the replicated configuration
+    partition.
+    """
+
+    synopsis = "%prog [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+        }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+                type=str, metavar="URL", dest="H"),
+        ]
+
+    def run(self, H=None, credopts=None, sambaopts=None, versionopts=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        domain_dn = samdb.domain_dn()
+        object_dn = "%s,%s" % (self.objectdn, domain_dn)
+
+        # Show all the settings we know how to set in the forest object!
+        res = samdb.search(base=object_dn, scope=ldb.SCOPE_BASE,
+                           attrs=self.attributes)
+
+        # Now we just display these attributes. The value is that
+        # we make them a bit prettier and human accessible.
+        # There should only be one response!
+        res_object = res[0]
+
+        print("Settings for %s" % object_dn)
+        for attr in self.attributes:
+            try:
+                print("%s: %s" % (attr, res_object[attr][0]))
+            except KeyError:
+                print("%s: <NO VALUE>" % attr)
+
+class cmd_forest_set(Command):
+    """Modify forest settings.
+
+    This will alter the setting specified to value.
+    """
+
+    attribute = None
+    objectdn = None
+
+    synopsis = "%prog value [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+        }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+                type=str, metavar="URL", dest="H"),
+        ]
+
+    takes_args = ["value"]
+
+    def run(self, value, H=None, credopts=None, sambaopts=None, versionopts=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        domain_dn = samdb.domain_dn()
+        object_dn = "%s,%s" % (self.objectdn, domain_dn)
+
+        # Create the modification
+        m = ldb.Message()
+        m.dn = ldb.Dn(samdb, object_dn)
+        m[self.attribute] = ldb.MessageElement(
+            value, ldb.FLAG_MOD_REPLACE, self.attribute)
+
+        samdb.modify(m)
+        print("set %s: %s" % (self.attribute, value))
+
+
+# Then you override it for each setting name:
+
+class cmd_forest_show_directory_service(cmd_forest_show):
+    """Display Directory Service settings for the forest.
+
+    These settings control how the Directory Service behaves on all domain
+    controllers in the forest.
+    """
+    objectdn = "CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration"
+    attributes = ['dsheuristics']
+
+class cmd_forest_set_directory_service_dsheuristics(cmd_forest_set):
+    """Set the value of dsheuristics on the Directory Service.
+
+    This value alters the behaviour of the Directory Service on all domain
+    controllers in the forest. Documentation related to this parameter can be
+    found here: https://msdn.microsoft.com/en-us/library/cc223560.aspx
+
+    In summary each "character" of the number-string, controls a setting.
+    A common setting is to set the value "2" in the 7th character. This controls
+    anonymous search behaviour.
+
+    Example: dsheuristics 0000002
+
+    This would allow anonymous LDAP searches to the domain (you may still need
+    to alter access controls to allow this).
+    """
+    objectdn = "CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration"
+    attribute = 'dsheuristics'
+
+class cmd_forest_directory_service(SuperCommand):
+    """Forest configuration partition management."""
+
+    subcommands = {}
+    subcommands["show"] = cmd_forest_show_directory_service()
+    subcommands["dsheuristics"] = cmd_forest_set_directory_service_dsheuristics()
+
+class cmd_forest(SuperCommand):
+    """Forest management."""
+
+    subcommands = {}
+    subcommands["directory_service"] = cmd_forest_directory_service()
+
+
diff --git a/python/samba/netcmd/main.py b/python/samba/netcmd/main.py
index 40762fabdad..56720801199 100644
--- a/python/samba/netcmd/main.py
+++ b/python/samba/netcmd/main.py
@@ -63,6 +63,7 @@ class cmd_sambatool(SuperCommand):
     subcommands["domain"] = None
     subcommands["drs"] = None
     subcommands["dsacl"] = None
+    subcommands["forest"] = None
     subcommands["fsmo"] = None
     subcommands["gpo"] = None
     subcommands["group"] = None
diff --git a/python/samba/tests/samba_tool/forest.py b/python/samba/tests/samba_tool/forest.py
new file mode 100644
index 00000000000..0f0d97b1fea
--- /dev/null
+++ b/python/samba/tests/samba_tool/forest.py
@@ -0,0 +1,63 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) William Brown <william at blackhats.net.au> 2018
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import os
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+class ForestCmdTestCase(SambaToolCmdTest):
+    """Tests for samba-tool dsacl subcommands"""
+    samdb = None
+
+    def setUp(self):
+        super(ForestCmdTestCase, self).setUp()
+        self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"],
+            "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+        self.domain_dn = self.samdb.domain_dn()
+
+    def tearDown(self):
+        super(ForestCmdTestCase, self).tearDown()
+        # Reset the values we might have changed.
+        ds_dn = "CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration"
+        m = ldb.Message()
+        m.dn = ldb.Dn(self.samdb, "%s,%s" % (ds_dn, self.domain_dn))
+        m['dsheuristics'] = ldb.MessageElement(
+            '0000000', ldb.FLAG_MOD_REPLACE, 'dsheuristics')
+
+        self.samdb.modify(m)
+
+    def test_display(self):
+        """Tests that we can display forest settings"""
+        (result, out, err) = self.runsubcmd("forest", "directory_service",
+                              "show",
+                              "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                              "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                            os.environ["DC_PASSWORD"]))
+
+        self.assertCmdSuccess(result, out, err)
+
+    def test_modify_dsheuristics(self):
+        """Test that we can modify the dsheuristics setting"""
+
+        (result, out, err) = self.runsubcmd("forest", "directory_service",
+                              "dsheuristics", "0000002",
+                              "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                              "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                            os.environ["DC_PASSWORD"]))
+
+        self.assertCmdSuccess(result, out, err)
+
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index 3757aed83aa..14e44df5247 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -612,6 +612,7 @@ planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.group")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.ou")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.computer")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.dsacl")
+planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.forest")
 planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.ntacl")
 planpythontestsuite("none", "samba.tests.samba_tool.provision_password_check")
 planpythontestsuite("none", "samba.tests.samba_tool.help")
-- 
2.17.0


From dec5dc4e4289e3e4a3a611bc936fbe5e3314c2ca Mon Sep 17 00:00:00 2001
From: William Brown <william at blackhats.net.au>
Date: Wed, 25 Apr 2018 17:37:58 +1000
Subject: [PATCH 2/7] python/samba/netcmd/domain.py: add configuration controls

With samba-tool we should expose ways to easily administer and control
common configuration options. This patch adds support to change domain
settings from the cli. These seem to be stored in the root domain object.

An example is:

samba-tool domain settings show
samba-tool domain settings account_machine_join_quota X

Signed-off-by: William Brown <william at blackhats.net.au>
---
 docs-xml/manpages/samba-tool.8.xml      |  15 ++++
 python/samba/netcmd/domain.py           | 113 ++++++++++++++++++++++++
 python/samba/tests/samba_tool/domain.py |  61 +++++++++++++
 source4/selftest/tests.py               |   1 +
 4 files changed, 190 insertions(+)
 create mode 100644 python/samba/tests/samba_tool/domain.py

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index fd9426ea608..cc02078fbb9 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -338,6 +338,21 @@
 	<para>Promote an existing domain member or NT4 PDC to an AD DC.</para>
 </refsect3>
 
+<refsect3>
+	<title>domain settings</title>
+	<para>Manage domain configuration.</para>
+</refsect3>
+
+<refsect3>
+	<title>domain settings ms-ds-machineaccountquota <replaceable>VALUE</replaceable></title>
+	<para>Modify the number of permitted machines users can join to the domain.</para>
+</refsect3>
+
+<refsect3>
+	<title>domain settings show</title>
+	<para>Show current domain configuration.</para>
+</refsect3>
+
 <refsect3>
 	<title>domain trust</title>
 	<para>Domain and forest trust management.</para>
diff --git a/python/samba/netcmd/domain.py b/python/samba/netcmd/domain.py
index d2dd06a3d48..6b96673ab14 100644
--- a/python/samba/netcmd/domain.py
+++ b/python/samba/netcmd/domain.py
@@ -7,6 +7,7 @@
 # Copyright Matthieu Patou <mat at matws.net> 2011
 # Copyright Andrew Bartlett 2008-2015
 # Copyright Stefan Metzmacher 2012
+# Copyright William Brown <william at blackhats.net.au> 2018
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -4315,6 +4316,117 @@ class cmd_domain_functional_prep(Command):
         if error_encountered:
             raise CommandError('Failed to perform functional prep')
 
+class cmd_domain_settings_show(Command):
+    """Display domain settings.
+
+    These settings control the behaviour of all domain controllers in this
+    domain. This displays those settings.
+    """
+
+    synopsis = "%prog [options]"
+
+    attributes = [
+        "ms-DS-MachineAccountQuota"
+        ]
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+        }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+                type=str, metavar="URL", dest="H"),
+        ]
+
+    def run(self, H=None, credopts=None, sambaopts=None, versionopts=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        domain_dn = samdb.domain_dn()
+
+        # Show all the settings we know how to set in the domain!
+        res = samdb.search(base=domain_dn, scope=ldb.SCOPE_BASE,
+                           attrs=self.attributes)
+
+        # Now we just display these attributes. The value is that
+        # we make them a bit prettier and human accessible.
+        # There should only be one response!
+        domain_object = res[0]
+
+        print("Settings for domain %s" % domain_dn)
+        for k in self.attributes:
+            try:
+                print("%s: %s" % (k, domain_object[k][0]))
+            except KeyError:
+                print("%s: <NO VALUE>" % k)
+
+# This is a generic class for the "set" command.
+
+class cmd_domain_settings_set(Command):
+    """Modify domain settings.
+
+    This will alter the setting specified to value.
+    """
+
+    attribute = None
+
+    synopsis = "%prog value [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+        }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+                type=str, metavar="URL", dest="H"),
+        ]
+
+    takes_args = ["value"]
+
+    def run(self, value, H=None, credopts=None, sambaopts=None, versionopts=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        domain_dn = samdb.domain_dn()
+
+        # Create the modification
+        m = ldb.Message()
+        m.dn = ldb.Dn(samdb, domain_dn)
+        m[self.attribute] = ldb.MessageElement(
+            value, ldb.FLAG_MOD_REPLACE, self.attribute)
+
+        samdb.modify(m)
+        print("set %s: %s" % (self.attribute, value))
+
+# Then you override it for each setting name:
+class cmd_domain_settings_machineaccountquota(cmd_domain_settings_set):
+    """Alter the ms-DS-MachineAccountQuota setting.
+
+    This value controls how many machines any authenticated domain user
+    can join to the domain. It's recommended you set this value to "0".
+    This does not affect domain joins performed by other authorised accounts
+    for example Administrator.
+    """
+    attribute = 'ms-DS-MachineAccountQuota'
+
+
+class cmd_domain_settings(SuperCommand):
+    """Domain setting management."""
+
+    subcommands = {}
+    subcommands["show"] = cmd_domain_settings_show()
+    subcommands["ms-ds-machineaccountquota"] = cmd_domain_settings_machineaccountquota()
+
 class cmd_domain(SuperCommand):
     """Domain management."""
 
@@ -4334,3 +4446,4 @@ class cmd_domain(SuperCommand):
     subcommands["tombstones"] = cmd_domain_tombstones()
     subcommands["schemaupgrade"] = cmd_domain_schema_upgrade()
     subcommands["functionalprep"] = cmd_domain_functional_prep()
+    subcommands["settings"] = cmd_domain_settings()
diff --git a/python/samba/tests/samba_tool/domain.py b/python/samba/tests/samba_tool/domain.py
new file mode 100644
index 00000000000..d81b20884a4
--- /dev/null
+++ b/python/samba/tests/samba_tool/domain.py
@@ -0,0 +1,61 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) William Brown <william at blackhats.net.au> 2018
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import os
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+class DomainCmdTestCase(SambaToolCmdTest):
+    """Tests for samba-tool domain subcommands"""
+    samdb = None
+
+    def setUp(self):
+        super(DomainCmdTestCase, self).setUp()
+        self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"],
+            "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+        self.domain_dn = self.samdb.domain_dn()
+
+    def tearDown(self):
+        super(DomainCmdTestCase, self).tearDown()
+        # Reset the values we might have changed.
+
+        m = ldb.Message()
+        m.dn = ldb.Dn(self.samdb, self.domain_dn)
+        m['ms-ds-machineaccountquota'] = ldb.MessageElement(
+            '10', ldb.FLAG_MOD_REPLACE, 'ms-ds-machineaccountquota')
+
+        self.samdb.modify(m)
+
+    def test_display(self):
+        """Tests that we can display domain settings"""
+        (result, out, err) = self.runsubcmd("domain", "settings", "show",
+                              "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                              "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                            os.environ["DC_PASSWORD"]))
+
+        self.assertCmdSuccess(result, out, err)
+
+    def test_modify_machine_account_quota(self):
+        """Test that we can modify the machine account quota setting"""
+
+        (result, out, err) = self.runsubcmd("domain", "settings",
+                              "ms-ds-machineaccountquota", "0",
+                              "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                              "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                            os.environ["DC_PASSWORD"]))
+
+        self.assertCmdSuccess(result, out, err)
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index 14e44df5247..72e2167b428 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -612,6 +612,7 @@ planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.group")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.ou")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.computer")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.dsacl")
+planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.domain")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.forest")
 planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.ntacl")
 planpythontestsuite("none", "samba.tests.samba_tool.provision_password_check")
-- 
2.17.0


From bdf87d7ad1541f2ae5315d9fbb703c5a0e2359e5 Mon Sep 17 00:00:00 2001
From: William Brown <william at blackhats.net.au>
Date: Thu, 26 Apr 2018 13:59:06 +1000
Subject: [PATCH 3/7] python/samba/netcmd/group.py: add group show

The samba-tool user command can show the ldif of a user. This is
useful for groups also, especially to determine the objectSID and
objectGUID. Add support for group show to samba-tool.

Signed-off-by: William Brown <william at blackhats.net.au>
---
 docs-xml/manpages/samba-tool.8.xml     |  5 ++
 python/samba/netcmd/group.py           | 81 ++++++++++++++++++++++++++
 python/samba/tests/samba_tool/group.py |  8 +++
 3 files changed, 94 insertions(+)

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index cc02078fbb9..19fa96f9d7f 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -590,6 +590,11 @@
 	<para>Remove members from the specified AD group.</para>
 </refsect3>
 
+<refsect3>
+	<title>group show <replaceable>groupname</replaceable> [options]</title>
+	<para>Show group object and it's attributes.</para>
+</refsect3>
+
 <refsect2>
 	<title>ldapcmp <replaceable>URL1</replaceable> <replaceable>URL2</replaceable> <replaceable>domain|configuration|schema|dnsdomain|dnsforest</replaceable> [options] </title>
 	<para>Compare two LDAP databases.</para>
diff --git a/python/samba/netcmd/group.py b/python/samba/netcmd/group.py
index a4969cc6ba9..9e1e11071f4 100644
--- a/python/samba/netcmd/group.py
+++ b/python/samba/netcmd/group.py
@@ -26,6 +26,7 @@ from getpass import getpass
 from samba.auth import system_session
 from samba.samdb import SamDB
 from samba.dsdb import (
+    ATYPE_SECURITY_GLOBAL_GROUP,
     GTYPE_SECURITY_BUILTIN_LOCAL_GROUP,
     GTYPE_SECURITY_DOMAIN_LOCAL_GROUP,
     GTYPE_SECURITY_GLOBAL_GROUP,
@@ -500,6 +501,85 @@ class cmd_group_move(Command):
         self.outf.write('Moved group "%s" into "%s"\n' %
                         (groupname, full_new_parent_dn))
 
+class cmd_group_show(Command):
+    """Display a group AD object.
+
+This command displays a group object and it's attributes in the Active
+Directory domain.
+The group name specified on the command is the sAMAccountName of the group.
+
+The command may be run from the root userid or another authorized userid.
+
+The -H or --URL= option can be used to execute the command against a remote
+server.
+
+Example1:
+samba-tool group show Group1 -H ldap://samba.samdom.example.com \
+-U administrator --password=passw1rd
+
+Example1 shows how to display a group's attributes in the domain against a remote
+LDAP server.
+
+The -H parameter is used to specify the remote target server.
+
+Example2:
+samba-tool group show Group2
+
+Example2 shows how to display a group's attributes in the domain against a local
+LDAP server.
+
+Example3:
+samba-tool group show Group3 --attributes=member,objectGUID
+
+Example3 shows how to display a users objectGUID and member attributes.
+"""
+    synopsis = "%prog <group name> [options]"
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+               type=str, metavar="URL", dest="H"),
+        Option("--attributes",
+               help=("Comma separated list of attributes, "
+                     "which will be printed."),
+               type=str, dest="group_attrs"),
+    ]
+
+    takes_args = ["groupname"]
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+        }
+
+    def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
+            H=None, group_attrs=None):
+
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp, fallback_machine=True)
+        samdb = SamDB(url=H, session_info=system_session(),
+                      credentials=creds, lp=lp)
+
+        attrs = None
+        if group_attrs:
+            attrs = group_attrs.split(",")
+
+        filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
+                     ( ATYPE_SECURITY_GLOBAL_GROUP,
+                       ldb.binary_encode(groupname)))
+
+        domaindn = samdb.domain_dn()
+
+        try:
+            res = samdb.search(base=domaindn, expression=filter,
+                               scope=ldb.SCOPE_SUBTREE, attrs=attrs)
+            user_dn = res[0].dn
+        except IndexError:
+            raise CommandError('Unable to find group "%s"' % (groupname))
+
+        for msg in res:
+            user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
+            self.outf.write(user_ldif)
+
 class cmd_group(SuperCommand):
     """Group management."""
 
@@ -511,3 +591,4 @@ class cmd_group(SuperCommand):
     subcommands["list"] = cmd_group_list()
     subcommands["listmembers"] = cmd_group_list_members()
     subcommands["move"] = cmd_group_move()
+    subcommands["show"] = cmd_group_show()
diff --git a/python/samba/tests/samba_tool/group.py b/python/samba/tests/samba_tool/group.py
index 914b8175d15..67acc1867a5 100644
--- a/python/samba/tests/samba_tool/group.py
+++ b/python/samba/tests/samba_tool/group.py
@@ -170,6 +170,14 @@ class GroupCmdTestCase(SambaToolCmdTest):
         self.assertCmdSuccess(result, out, err,
                               "Failed to delete ou '%s'" % full_ou_dn)
 
+    def test_show(self):
+        """Assert that we can show a group correctly."""
+        (result, out, err) = self.runsubcmd("group", "show", "Domain Users",
+                                            "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                                            "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                                          os.environ["DC_PASSWORD"]))
+        self.assertCmdSuccess(result, out, err)
+
     def _randomGroup(self, base={}):
         """create a group with random attribute values, you can specify base attributes"""
         group = {
-- 
2.17.0


From 96b160cde6721ab0ff1242ffbc27871a0cb2dc8d Mon Sep 17 00:00:00 2001
From: William Brown <william at blackhats.net.au>
Date: Sat, 28 Apr 2018 15:22:29 +1000
Subject: [PATCH 4/7] python/samba/netcmd/schema.py: add schema query and
 management.

Schema management in active directory is complex and dangerous. Having
a tool that safely wraps administrative tasks as well as allowing query
of the schema will make this complex topic more accessible to administrators.

Signed-off-by: William Brown <william at blackhats.net.au>
---
 docs-xml/manpages/samba-tool.8.xml      |  20 ++
 python/samba/ms_schema.py               |   9 +-
 python/samba/netcmd/main.py             |   1 +
 python/samba/netcmd/schema.py           | 262 ++++++++++++++++++++++++
 python/samba/samdb.py                   |   4 +
 python/samba/tests/samba_tool/schema.py |  90 ++++++++
 source4/selftest/tests.py               |   1 +
 7 files changed, 384 insertions(+), 3 deletions(-)
 create mode 100644 python/samba/netcmd/schema.py
 create mode 100644 python/samba/tests/samba_tool/schema.py

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index 19fa96f9d7f..0466e125100 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -717,6 +717,26 @@
 	<para>Preload one account for an RODC.</para>
 </refsect3>
 
+<refsect2>
+	<title>schema</title>
+	<para>Manage and query schema.</para>
+</refsect2>
+
+<refsect3>
+	<title>schema attribute modify <replaceable>attribute</replaceable> [options]</title>
+	<para>Modify the behaviour of an attribute in schema.</para>
+</refsect3>
+
+<refsect3>
+	<title>schema attribute show <replaceable>attribute</replaceable> [options]</title>
+	<para>Display an attribute schema definition.</para>
+</refsect3>
+
+<refsect3>
+	<title>schema objectclass show <replaceable>objectclass</replaceable> [options]</title>
+	<para>Display an objectclass schema definition.</para>
+</refsect3>
+
 <refsect2>
 	<title>sites</title>
 	<para>Manage sites.</para>
diff --git a/python/samba/ms_schema.py b/python/samba/ms_schema.py
index 889b7f5ef22..0cfcd6b824a 100644
--- a/python/samba/ms_schema.py
+++ b/python/samba/ms_schema.py
@@ -35,14 +35,17 @@ bitFields["searchflags"] = {
     'fTUPLEINDEX': 26,       # TP
     'fSUBTREEATTINDEX': 25,  # ST
     'fCONFIDENTIAL': 24,     # CF
+    'fCONFIDENTAIL': 24, # typo
     'fNEVERVALUEAUDIT': 23,  # NV
     'fRODCAttribute': 22,    # RO
 
 
     # missing in ADTS but required by LDIF
-    'fRODCFilteredAttribute': 22,    # RO ?
-    'fCONFIDENTAIL': 24, # typo
-    'fRODCFILTEREDATTRIBUTE': 22 # case
+    'fRODCFilteredAttribute': 22,    # RO
+    'fRODCFILTEREDATTRIBUTE': 22, # case
+    'fEXTENDEDLINKTRACKING': 21,  # XL
+    'fBASEONLY': 20,  # BO
+    'fPARTITIONSECRET': 19,  # SE
     }
 
 # ADTS: 2.2.10
diff --git a/python/samba/netcmd/main.py b/python/samba/netcmd/main.py
index 56720801199..83797662083 100644
--- a/python/samba/netcmd/main.py
+++ b/python/samba/netcmd/main.py
@@ -70,6 +70,7 @@ class cmd_sambatool(SuperCommand):
     subcommands["ldapcmp"] = None
     subcommands["ntacl"] = None
     subcommands["rodc"] = None
+    subcommands["schema"] = None
     subcommands["sites"] = None
     subcommands["spn"] = None
     subcommands["testparm"] = None
diff --git a/python/samba/netcmd/schema.py b/python/samba/netcmd/schema.py
new file mode 100644
index 00000000000..f162a5f09e8
--- /dev/null
+++ b/python/samba/netcmd/schema.py
@@ -0,0 +1,262 @@
+# Manipulate ACLs on directory objects
+#
+# Copyright (C) William Brown <william at blackhats.net.au> 2018
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import ldb
+import samba.getopt as options
+from samba.ms_schema import bitFields
+from samba.auth import system_session
+from samba.samdb import SamDB
+from samba.netcmd import (
+    Command,
+    CommandError,
+    SuperCommand,
+    Option
+    )
+
+class cmd_schema_attribute_modify(Command):
+    """Modify attribute settings in the schema partition.
+
+    This commands allows minor modifications to attributes in the schema. Active
+    Directory does not allow many changes to schema, but important modifications
+    are related to indexing. This command overwrites the value of searchflags,
+    so be sure to view the current content before making changes.
+
+    Example1:
+    samba-tool schema attribute modify uid \
+        --searchflags="fATTINDEX,fPRESERVEONDELETE"
+
+    This alters the uid attribute to be indexed and to be preserved when
+    converted to a tombstone.
+
+    Important search flag values are:
+
+    fATTINDEX: create an equality index for this attribute.
+    fPDNTATTINDEX: create a container index for this attribute (ie OU).
+    fANR: specify that this attribute is a member of the ambiguous name
+         resolution set.
+    fPRESERVEONDELETE: indicate that the value of this attribute should be
+         preserved when the object is converted to a tombstone (deleted).
+    fCOPY: hint to clients that this attribute should be copied.
+    fTUPLEINDEX: create a tuple index for this attribute. This is used in
+          substring queries.
+    fSUBTREEATTINDEX: create a browsing index for this attribute. VLV searches
+          require this.
+    fCONFIDENTIAL: indicate that the attribute is confidental and requires
+          special access checks.
+    fNEVERVALUEAUDIT: indicate that changes to this value should NOT be audited.
+    fRODCFILTEREDATTRIBUTE: indicate that this value should not be replicated to
+          RODCs.
+    fEXTENDEDLINKTRACKING: indicate to the DC to perform extra link tracking.
+    fBASEONLY: indicate that this attribute should only be displayed when the
+           search scope of the query is SCOPE_BASE or a single object result.
+    fPARTITIONSECRET: indicate that this attribute is a partition secret and
+           requires special access checks.
+
+    The authoritative source of this information is the MS-ADTS.
+    """
+    synopsis = "%prog attribute [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+        }
+
+    takes_options = [
+        Option("--searchflags", help="Search Flags for the attribute", type=str),
+        Option("-H", "--URL", help="LDB URL for database or target server",
+                type=str, metavar="URL", dest="H"),
+        ]
+
+    takes_args = ["attribute"]
+
+    def run(self, attribute, H=None, credopts=None, sambaopts=None,
+            versionopts=None, searchflags=None):
+
+        if searchflags is None:
+            raise CommandError('A value to modify must be provided.')
+
+        # Parse the search flags to a set of bits to modify.
+
+        searchflags_int = None
+        if searchflags is not None:
+            searchflags_int = 0
+            flags = searchflags.split(',')
+            # We have to normalise all the values. To achieve this predictably
+            # we title case (Fattrindex), then swapcase (fATTINDEX)
+            flags = [ x.capitalize().swapcase() for x in flags ]
+            for flag in flags:
+                if flag not in bitFields['searchflags'].keys():
+                    raise CommandError("Unknown flag '%s', please see --help" % flag)
+                bit_loc = 31 - bitFields['searchflags'][flag]
+                # Now apply the bit.
+                searchflags_int = searchflags_int | (1 << bit_loc)
+
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        schema_dn = samdb.schema_dn()
+        # For now we make assumptions about the CN
+        attr_dn = 'cn=%s,%s' % (attribute, schema_dn)
+
+        m = ldb.Message()
+        m.dn = ldb.Dn(samdb, attr_dn)
+
+        if searchflags_int is not None:
+            m['searchFlags'] = ldb.MessageElement(
+                str(searchflags_int), ldb.FLAG_MOD_REPLACE, 'searchFlags')
+
+        samdb.modify(m)
+        print("modified %s" % attr_dn)
+
+class cmd_schema_attribute_show(Command):
+    """Show details about an attribute from the schema.
+
+    Schema attribute definitions define and control the behaviour of directory
+    attributes on objects. This displays the details of a single attribute.
+    """
+    synopsis = "%prog attribute [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+        }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+                type=str, metavar="URL", dest="H"),
+        ]
+
+    takes_args = ["attribute"]
+
+    def run(self, attribute, H=None, credopts=None, sambaopts=None, versionopts=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        schema_dn = samdb.schema_dn()
+
+        filt = '(&(objectClass=attributeSchema)(|(lDAPDisplayName={0})(cn={0})(name={0})))'.format(attribute)
+
+        res = samdb.search(base=schema_dn, scope=ldb.SCOPE_SUBTREE,
+                           expression=filt)
+
+        if len(res) == 0:
+            raise CommandError('No schema objects matched "%s"' % attribute)
+        if len(res) > 1:
+            raise CommandError('Multiple schema objects matched "%s": this is a serious issue you should report!' % attribute)
+
+        # Get the content of searchFlags (if any) and manipulate them to
+        # show our friendly names.
+
+        # WARNING: If you are reading this in the future trying to change an
+        # ldb message dynamically, and wondering why you get an operations
+        # error, it's related to talloc references.
+        #
+        # When you create *any* python reference, IE:
+        # flags = res[0]['attr']
+        # this creates a talloc_reference that may live forever due to pythons
+        # memory management model. However, when you create this reference it
+        # blocks talloc_realloc from functions in msg.add(element).
+        #
+        # As a result, you MUST avoid ALL new variable references UNTIL you have
+        # modified the message as required, even if it makes your code more
+        # verbose.
+
+        if 'searchFlags' in res[0].keys():
+            flags_i = None
+            try:
+                # See above
+                flags_i = int(str(res[0]['searchFlags']))
+            except ValueError:
+                raise CommandError('Invalid schemaFlags value "%s": this is a serious issue you should report!' % res[0]['searchFlags'])
+            # Work out what keys we have.
+            out = []
+            for flag in bitFields['searchflags'].keys():
+                if flags_i & (1 << (31 - bitFields['searchflags'][flag])) != 0:
+                    out.append(flag)
+            if len(out) > 0:
+                res[0].add(ldb.MessageElement(out, ldb.FLAG_MOD_ADD, 'searchFlagsDecoded'))
+
+        user_ldif = samdb.write_ldif(res[0], ldb.CHANGETYPE_NONE)
+        self.outf.write(user_ldif)
+
+class cmd_schema_objectclass_show(Command):
+    """Show details about an objectClass from the schema.
+
+    Schema objectClass definitions define and control the behaviour of directory
+    objects including what attributes they may contain. This displays the
+    details of an objectClass.
+    """
+    synopsis = "%prog objectclass [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+        }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+                type=str, metavar="URL", dest="H"),
+        ]
+
+    takes_args = ["objectclass"]
+
+    def run(self, objectclass, H=None, credopts=None, sambaopts=None, versionopts=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        schema_dn = samdb.schema_dn()
+
+        filt = '(&(objectClass=classSchema)(|(lDAPDisplayName={0})(cn={0})(name={0})))'.format(objectclass)
+
+        res = samdb.search(base=schema_dn, scope=ldb.SCOPE_SUBTREE,
+                           expression=filt)
+
+        for msg in res:
+            user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
+            self.outf.write(user_ldif)
+
+class cmd_schema_attribute(SuperCommand):
+    """Query and manage attributes in the schema partition."""
+    subcommands = {}
+    subcommands["modify"] = cmd_schema_attribute_modify()
+    subcommands["show"] = cmd_schema_attribute_show()
+
+class cmd_schema_objectclass(SuperCommand):
+    """Query and manage objectclasses in the schema partition."""
+    subcommands = {}
+    subcommands["show"] = cmd_schema_objectclass_show()
+
+class cmd_schema(SuperCommand):
+    """Schema querying and management."""
+
+    subcommands = {}
+    subcommands["attribute"] = cmd_schema_attribute()
+    subcommands["objectclass"] = cmd_schema_objectclass()
+
diff --git a/python/samba/samdb.py b/python/samba/samdb.py
index b66afb7431c..a4212ea9c0d 100644
--- a/python/samba/samdb.py
+++ b/python/samba/samdb.py
@@ -90,6 +90,10 @@ class SamDB(samba.Ldb):
         '''return the domain DN'''
         return str(self.get_default_basedn())
 
+    def schema_dn(self):
+        '''return the schema partition dn'''
+        return str(self.get_schema_basedn())
+
     def disable_account(self, search_filter):
         """Disables an account
 
diff --git a/python/samba/tests/samba_tool/schema.py b/python/samba/tests/samba_tool/schema.py
new file mode 100644
index 00000000000..8a610d88cf4
--- /dev/null
+++ b/python/samba/tests/samba_tool/schema.py
@@ -0,0 +1,90 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) William Brown <william at blackhats.net.au> 2018
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import os
+import ldb
+from samba.tests.samba_tool.base import SambaToolCmdTest
+
+class SchemaCmdTestCase(SambaToolCmdTest):
+    """Tests for samba-tool dsacl subcommands"""
+    samdb = None
+
+    def setUp(self):
+        super(SchemaCmdTestCase, self).setUp()
+        self.samdb = self.getSamDB("-H", "ldap://%s" % os.environ["DC_SERVER"],
+            "-U%s%%%s" % (os.environ["DC_USERNAME"], os.environ["DC_PASSWORD"]))
+
+    def tearDown(self):
+        super(SchemaCmdTestCase, self).tearDown()
+
+    def test_display_attribute(self):
+        """Tests that we can display schema attributes"""
+        (result, out, err) = self.runsubcmd("schema", "attribute",
+                              "show", "uid",
+                              "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                              "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                            os.environ["DC_PASSWORD"]))
+
+        self.assertCmdSuccess(result, out, err)
+
+    def test_modify_attribute_searchflags(self):
+        """Tests that we can modify searchFlags of an attribute"""
+        (result, out, err) = self.runsubcmd("schema", "attribute",
+                              "modify", "uid", "--searchflags=9",
+                              "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                              "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                            os.environ["DC_PASSWORD"]))
+
+        self.assertCmdFail(result, 'Unknown flag 9, please see --help')
+
+        (result, out, err) = self.runsubcmd("schema", "attribute",
+                              "modify", "uid", "--searchflags=fATTINDEX",
+                              "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                              "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                            os.environ["DC_PASSWORD"]))
+
+        self.assertCmdSuccess(result, out, err)
+
+        (result, out, err) = self.runsubcmd("schema", "attribute",
+                              "modify", "uid",
+                              "--searchflags=fATTINDEX,fSUBTREEATTINDEX",
+                              "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                              "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                            os.environ["DC_PASSWORD"]))
+
+        self.assertCmdSuccess(result, out, err)
+
+        (result, out, err) = self.runsubcmd("schema", "attribute",
+                              "modify", "uid",
+                              "--searchflags=fAtTiNdEx,fPRESERVEONDELETE",
+                              "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                              "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                            os.environ["DC_PASSWORD"]))
+
+        self.assertCmdSuccess(result, out, err)
+
+    def test_display_objectclass(self):
+        """Tests that we can display schema objectclasses"""
+        (result, out, err) = self.runsubcmd("schema", "objectclass",
+                              "show", "person",
+                              "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                              "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                            os.environ["DC_PASSWORD"]))
+
+        self.assertCmdSuccess(result, out, err)
+
+
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index 72e2167b428..5f1bd3a6480 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -614,6 +614,7 @@ planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.computer")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.dsacl")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.domain")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.forest")
+planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.schema")
 planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.ntacl")
 planpythontestsuite("none", "samba.tests.samba_tool.provision_password_check")
 planpythontestsuite("none", "samba.tests.samba_tool.help")
-- 
2.17.0


From 6c5562605eed6e432b28e0e937e50e38d5411abb Mon Sep 17 00:00:00 2001
From: William Brown <william at blackhats.net.au>
Date: Sun, 29 Apr 2018 13:28:42 +1200
Subject: [PATCH 5/7] python/samba/netcmd/schema.py: add schema show_oc for
 attribute

Often administrators need to add a specific attribute to an object, but
it may not be possible with the objectClasses present. This tool allows
searching "what objectclasses must or may?" take an attribute to help hint
to an administrator what objectclasses can be added to objects to achieve
the changes they want.

Signed-off-by: William Brown <william at blackhats.net.au>
---
 docs-xml/manpages/samba-tool.8.xml      |  5 +++
 python/samba/netcmd/schema.py           | 48 +++++++++++++++++++++++++
 python/samba/tests/samba_tool/schema.py | 10 ++++++
 3 files changed, 63 insertions(+)

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index 0466e125100..7c8b92ebc99 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -732,6 +732,11 @@
 	<para>Display an attribute schema definition.</para>
 </refsect3>
 
+<refsect3>
+	<title>schema attribute show_oc <replaceable>attribute</replaceable> [options]</title>
+	<para>Show objectclasses that MAY or MUST contain this attribute.</para>
+</refsect3>
+
 <refsect3>
 	<title>schema objectclass show <replaceable>objectclass</replaceable> [options]</title>
 	<para>Display an objectclass schema definition.</para>
diff --git a/python/samba/netcmd/schema.py b/python/samba/netcmd/schema.py
index f162a5f09e8..5c129198e63 100644
--- a/python/samba/netcmd/schema.py
+++ b/python/samba/netcmd/schema.py
@@ -202,6 +202,53 @@ class cmd_schema_attribute_show(Command):
         user_ldif = samdb.write_ldif(res[0], ldb.CHANGETYPE_NONE)
         self.outf.write(user_ldif)
 
+class cmd_schema_attribute_show_oc(Command):
+    """Show what objectclasses MAY or MUST contain an attribute.
+
+    This is useful to determine "if I need uid, what objectclasses could be
+    applied to achieve this."
+    """
+    synopsis = "%prog attribute [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+        }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+                type=str, metavar="URL", dest="H"),
+        ]
+
+    takes_args = ["attribute"]
+
+    def run(self, attribute, H=None, credopts=None, sambaopts=None, versionopts=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        schema_dn = samdb.schema_dn()
+
+        may_filt = '(&(objectClass=classSchema)(|(mayContain={0})(systemMayContain={0})))'.format(attribute)
+        must_filt = '(&(objectClass=classSchema)(|(mustContain={0})(systemMustContain={0})))'.format(attribute)
+
+        may_res = samdb.search(base=schema_dn, scope=ldb.SCOPE_SUBTREE,
+                           expression=may_filt, attrs=['cn'])
+        must_res = samdb.search(base=schema_dn, scope=ldb.SCOPE_SUBTREE,
+                           expression=must_filt, attrs=['cn'])
+
+        self.outf.write('--- MAY contain ---\n')
+        for msg in may_res:
+            self.outf.write('%s\n' % msg['cn'][0])
+
+        self.outf.write('--- MUST contain ---\n')
+        for msg in must_res:
+            self.outf.write('%s\n' % msg['cn'][0])
+
+
 class cmd_schema_objectclass_show(Command):
     """Show details about an objectClass from the schema.
 
@@ -247,6 +294,7 @@ class cmd_schema_attribute(SuperCommand):
     subcommands = {}
     subcommands["modify"] = cmd_schema_attribute_modify()
     subcommands["show"] = cmd_schema_attribute_show()
+    subcommands["show_oc"] = cmd_schema_attribute_show_oc()
 
 class cmd_schema_objectclass(SuperCommand):
     """Query and manage objectclasses in the schema partition."""
diff --git a/python/samba/tests/samba_tool/schema.py b/python/samba/tests/samba_tool/schema.py
index 8a610d88cf4..5484e32fe47 100644
--- a/python/samba/tests/samba_tool/schema.py
+++ b/python/samba/tests/samba_tool/schema.py
@@ -77,6 +77,16 @@ class SchemaCmdTestCase(SambaToolCmdTest):
 
         self.assertCmdSuccess(result, out, err)
 
+    def test_show_oc_attribute(self):
+        """Tests that we can modify searchFlags of an attribute"""
+        (result, out, err) = self.runsubcmd("schema", "attribute",
+                              "show_oc", "cn",
+                              "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                              "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                            os.environ["DC_PASSWORD"]))
+
+        self.assertCmdSuccess(result, out, err)
+
     def test_display_objectclass(self):
         """Tests that we can display schema objectclasses"""
         (result, out, err) = self.runsubcmd("schema", "objectclass",
-- 
2.17.0


From 14937bb4baa919fcfe6c7183a31e4240796b69b9 Mon Sep 17 00:00:00 2001
From: William Brown <william at blackhats.net.au>
Date: Thu, 3 May 2018 16:07:07 +1200
Subject: [PATCH 6/7] source4/setup/external-schema: Add ns compat and
 sshpubkey

Add externally provided schema files that can be applied to a domain. This
prevents admins needing to apply "random ldifs" from the internet. The two
external schemas are for sshpublic key storage in LDAP, and the second is
a 389 Directory Server compatability attribute for UUID mapping.

Signed-off-by: William Brown <william at blackhats.net.au>
---
 python/samba/netcmd/schema.py                 | 75 +++++++++++++++
 python/samba/schema.py                        | 94 +++++++++++++++++++
 python/samba/tests/samba_tool/schema.py       | 25 +++++
 source4/setup/external-schema/README.txt      | 11 +++
 source4/setup/external-schema/ldapcompat.ldif | 50 ++++++++++
 source4/setup/external-schema/sshpubkey.ldif  | 32 +++++++
 source4/setup/schema_samba4.ldif              |  1 +
 source4/setup/wscript_build                   |  3 +
 8 files changed, 291 insertions(+)
 create mode 100644 source4/setup/external-schema/README.txt
 create mode 100644 source4/setup/external-schema/ldapcompat.ldif
 create mode 100644 source4/setup/external-schema/sshpubkey.ldif

diff --git a/python/samba/netcmd/schema.py b/python/samba/netcmd/schema.py
index 5c129198e63..a6c63329e99 100644
--- a/python/samba/netcmd/schema.py
+++ b/python/samba/netcmd/schema.py
@@ -28,6 +28,9 @@ from samba.netcmd import (
     Option
     )
 
+# For external schema
+from samba.schema import list_external_schema, get_external_schema
+
 class cmd_schema_attribute_modify(Command):
     """Modify attribute settings in the schema partition.
 
@@ -249,6 +252,70 @@ class cmd_schema_attribute_show_oc(Command):
             self.outf.write('%s\n' % msg['cn'][0])
 
 
+class cmd_schema_external_list(Command):
+    """List available external schemas that can be applied.
+
+    Samba ships with a number of external schemas that extend the functionality
+    of the Directory Server.
+    """
+
+    synopsis = "%prog"
+
+    def run(self):
+        schemas = list_external_schema()
+        for schema in schemas:
+            self.outf.write('%s - %s\n' % (schema.name, schema.desc))
+
+class cmd_schema_external_show(Command):
+    """Show content of external schemas that can be applied.
+
+    This displays what would be changed if the schema was applied to the server.
+    """
+
+    synopsis = "%prog <schemaname>"
+
+    takes_args = ["schemaname"]
+
+    def run(self, schemaname):
+        schema = get_external_schema(schemaname)
+        if schema is None:
+            raise CommandError('No external schema named %s' % schemaname)
+        self.outf.write(schema.show())
+
+class cmd_schema_external_apply(Command):
+    """Apply an external schema to the directory.
+
+    This is a non-reversible action, schema modifications in AD are permanent!
+    """
+
+    synopsis = "%prog <schemaname> [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "versionopts": options.VersionOptions,
+        "credopts": options.CredentialsOptions,
+        }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+                type=str, metavar="URL", dest="H"),
+        ]
+
+    takes_args = ["schemaname"]
+
+    def run(self, schemaname, H=None, credopts=None, sambaopts=None, versionopts=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        schema = get_external_schema(schemaname)
+        if schema is None:
+            raise CommandError('No external schema named %s' % schemaname)
+        schema.apply(samdb)
+        self.outf.write("Success: applied schema %s\n" % schemaname)
+
 class cmd_schema_objectclass_show(Command):
     """Show details about an objectClass from the schema.
 
@@ -296,6 +363,13 @@ class cmd_schema_attribute(SuperCommand):
     subcommands["show"] = cmd_schema_attribute_show()
     subcommands["show_oc"] = cmd_schema_attribute_show_oc()
 
+class cmd_schema_external(SuperCommand):
+    """Manage external schema from the Samba project."""
+    subcommands = {}
+    subcommands["list"] = cmd_schema_external_list()
+    subcommands["show"] = cmd_schema_external_show()
+    subcommands["apply"] = cmd_schema_external_apply()
+
 class cmd_schema_objectclass(SuperCommand):
     """Query and manage objectclasses in the schema partition."""
     subcommands = {}
@@ -306,5 +380,6 @@ class cmd_schema(SuperCommand):
 
     subcommands = {}
     subcommands["attribute"] = cmd_schema_attribute()
+    subcommands["external"] = cmd_schema_external()
     subcommands["objectclass"] = cmd_schema_objectclass()
 
diff --git a/python/samba/schema.py b/python/samba/schema.py
index 67ec357a285..7415c7c656e 100644
--- a/python/samba/schema.py
+++ b/python/samba/schema.py
@@ -31,6 +31,7 @@ from samba.samdb import SamDB
 from samba import dsdb
 from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL
 import os
+import ldb
 
 def get_schema_descriptor(domain_sid, name_map={}):
     sddl = "O:SAG:SAD:AI(OA;;CR;e12b56b6-0a95-11d1-adbb-00c04fd8d5cd;;SA)" \
@@ -195,6 +196,99 @@ dn: @INDEXLIST
         return dsdb._dsdb_convert_schema_to_openldap(self.ldb, target, mapping)
 
 
+class ExternalSchema(object):
+    """Class to manipulate and apply external schemas shipped with samba."""
+    def __init__(self):
+        pass
+
+    def show(self):
+        """Return a formated ldif of what we would add"""
+        data = read_and_sub_file(self.path, {"SCHEMADN": "CN=Schema,..."})
+        for oc in self.auxiliary:
+            data += "# Extend classSchema CN='%s' with auxiliaryClass: '%s'\n" % (oc, self.auxiliary[oc])
+        return data
+
+    def apply(self, samdb):
+        """Apply the external schema to the directory.
+
+        """
+        schemadn = str(samdb.get_schema_basedn())
+        data = read_and_sub_file(self.path, {"SCHEMADN": schemadn})
+
+        # Rather than directly applying this ldif, we'll spit it to
+        # ldb messages and apply them one at a time. This allows partially
+        # applied schema to be "skipped over" so we can do fix ups.
+        # ldb.modify_ldif(data, controls)
+        for changetype, msg in samdb.parse_ldif(data):
+            try:
+                if changetype == ldb.CHANGETYPE_ADD:
+                    samdb.add(msg)
+                else:
+                    samdb.modify(msg)
+
+            except ldb.LdbError as err:
+                if err.args[0] != ldb.ERR_ENTRY_ALREADY_EXISTS:
+                    raise err
+
+        # Finally, apply any supplemental auxiliary class changes.
+        for oc in self.auxiliary:
+            filt = '(&(objectClass=classSchema)(cn={0}))'.format(oc)
+            # Get the original
+            res = samdb.search(base=schemadn, scope=ldb.SCOPE_SUBTREE,
+                               expression=filt)
+            # extend the auxiliaryClass:
+            aux = [str(x) for x in res[0]['auxiliaryClass']]
+
+            # do we already exist? We have to compare as all lower.
+            aux_lower = [x.lower() for x in aux]
+            if self.auxiliary[oc].lower() in aux_lower:
+                continue
+
+            # Okay, so add our aux, proper case.
+            aux.append(self.auxiliary[oc])
+
+            # commit
+            m = ldb.Message()
+            m.dn = ldb.Dn(samdb, str(res[0]['dn']))
+            m['auxiliaryClass'] = ldb.MessageElement(
+                aux, ldb.FLAG_MOD_REPLACE, 'auxiliaryClass')
+            samdb.modify(m)
+            # done!
+
+
+class LdapCompatExSchema(ExternalSchema):
+    def __init__(self):
+        super(LdapCompatExSchema, self).__init__()
+        from samba.provision import setup_path
+        self.name = 'ldapcompat'
+        self.desc = "Unix LDAP server compatability attributes and classes"
+        self.path = setup_path('external-schema/ldapcompat.ldif')
+        self.auxiliary = {
+            'User': 'ldapCompatPerson'
+        }
+
+class SshPubKeyExSchema(ExternalSchema):
+    def __init__(self):
+        super(SshPubKeyExSchema, self).__init__()
+        from samba.provision import setup_path
+        self.name = 'sshpubkey'
+        self.desc = "Allow storage and distribution of ssh public keys"
+        self.path = setup_path('external-schema/sshpubkey.ldif')
+        self.auxiliary = {
+            'User': 'ldapPublicKey'
+        }
+
+
+def list_external_schema():
+    return [LdapCompatExSchema(), SshPubKeyExSchema()]
+
+def get_external_schema(name):
+    schemas = {
+        'ldapcompat': LdapCompatExSchema(),
+        'sshpubkey' : SshPubKeyExSchema(),
+    }
+    return schemas.get(name, None)
+
 # Return a hash with the forward attribute as a key and the back as the value
 def get_linked_attributes(schemadn, schemaldb):
     attrs = ["linkID", "lDAPDisplayName"]
diff --git a/python/samba/tests/samba_tool/schema.py b/python/samba/tests/samba_tool/schema.py
index 5484e32fe47..df807d88bf0 100644
--- a/python/samba/tests/samba_tool/schema.py
+++ b/python/samba/tests/samba_tool/schema.py
@@ -98,3 +98,28 @@ class SchemaCmdTestCase(SambaToolCmdTest):
         self.assertCmdSuccess(result, out, err)
 
 
+    def test_display_external(self):
+        """Tests that we can display the external schemas"""
+        (result, out, err) = self.runsubcmd("schema", "external",
+                              "list")
+
+        self.assertCmdSuccess(result, out, err)
+
+        (result, out, err) = self.runsubcmd("schema", "external",
+                              "show", "sshpubkey")
+
+        self.assertCmdSuccess(result, out, err)
+
+    def test_apply_external(self):
+        """Test application of the external schema. Important to know
+        we apply this twice to show it's safe to run multiple times.
+        """
+        for i in range(0,2):
+            (result, out, err) = self.runsubcmd("schema", "external",
+                                  "apply", "sshpubkey",
+                                  "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                                  "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                                os.environ["DC_PASSWORD"]))
+
+            self.assertCmdSuccess(result, out, err)
+
diff --git a/source4/setup/external-schema/README.txt b/source4/setup/external-schema/README.txt
new file mode 100644
index 00000000000..844246d4dab
--- /dev/null
+++ b/source4/setup/external-schema/README.txt
@@ -0,0 +1,11 @@
+This is a set of external LDIF schemas that are useful - but not installed
+by default.
+
+They exist so that rather than applying random internet LDIF's we can guide
+people to use these instead.
+
+To apply these, you need to copy them and replace 'DC=X' with your domain DN.
+
+They can then be applied with ldapmodify -f <name>.ldif. You will need to
+authenticate with an account that is a member of Schema Admins.
+
diff --git a/source4/setup/external-schema/ldapcompat.ldif b/source4/setup/external-schema/ldapcompat.ldif
new file mode 100644
index 00000000000..661bf32d4e0
--- /dev/null
+++ b/source4/setup/external-schema/ldapcompat.ldif
@@ -0,0 +1,50 @@
+
+dn: CN=nsUniqueId,${SCHEMADN}
+changetype: add
+objectClass: top
+objectClass: attributeSchema
+attributeID: 2.16.840.1.113730.3.1.542
+cn: nsUniqueId
+name: nsUniqueId
+lDAPDisplayName: nsUniqueId
+description: MANDATORY: nsUniqueId compatability
+attributeSyntax: 2.5.5.10
+oMSyntax: 4
+isSingleValued: TRUE
+searchFlags: 9
+objectCategory: CN=Attribute-Schema,${SCHEMADN}
+schemaIDGUID:: PTIIe1afdUKi0To2hxU1zg==
+
+dn: CN=entryUUID,${SCHEMADN}
+changetype: add
+objectClass: top
+objectClass: attributeSchema
+attributeID: 1.3.6.1.1.16.4
+cn: entryUUID
+name: entryUUID
+lDAPDisplayName: entryUUID
+description: MANDATORY: entryUUID compatability
+attributeSyntax: 2.5.5.10
+oMSyntax: 4
+isSingleValued: TRUE
+searchFlags: 9
+objectCategory: CN=Attribute-Schema,${SCHEMADN}
+schemaIDGUID:: mqcwg4e++kmQIA2VphN1oQ==
+
+dn: CN=ldapCompatPerson,${SCHEMADN}
+changetype: add
+objectClass: top
+objectClass: classSchema
+governsID: 1.3.6.1.4.1.7165.4.2.3
+cn: ldapCompatPerson
+name: ldapCompatPerson
+description: MANDATORY: Unix LDAP compat person
+lDAPDisplayName: ldapCompatPerson
+subClassOf: top
+objectClassCategory: 3
+objectCategory: CN=Class-Schema,${SCHEMADN}
+defaultObjectCategory: CN=ldapCompatPerson,${SCHEMADN}
+mayContain: nsUniqueId
+mayContain: entryUUID
+schemaIDGUID:: 86ZQNhW9JE6cXG/Mb03K4Q==
+
diff --git a/source4/setup/external-schema/sshpubkey.ldif b/source4/setup/external-schema/sshpubkey.ldif
new file mode 100644
index 00000000000..cc61a3b105c
--- /dev/null
+++ b/source4/setup/external-schema/sshpubkey.ldif
@@ -0,0 +1,32 @@
+dn: CN=sshPublicKey,${SCHEMADN}
+changetype: add
+objectClass: top
+objectClass: attributeSchema
+attributeID: 1.3.6.1.4.1.24552.500.1.1.1.13
+cn: sshPublicKey
+name: sshPublicKey
+lDAPDisplayName: sshPublicKey
+description: MANDATORY: OpenSSH Public key
+attributeSyntax: 2.5.5.10
+oMSyntax: 4
+isSingleValued: FALSE
+objectCategory: CN=Attribute-Schema,${SCHEMADN}
+searchFlags: 8
+schemaIDGUID:: cjDAZyEXzU+/akI0EGDW+g==
+
+dn: CN=ldapPublicKey,${SCHEMADN}
+changetype: add
+objectClass: top
+objectClass: classSchema
+governsID: 1.3.6.1.4.1.24552.500.1.1.2.0
+cn: ldapPublicKey
+name: ldapPublicKey
+description: MANDATORY: OpenSSH LPK objectclass
+lDAPDisplayName: ldapPublicKey
+subClassOf: top
+objectClassCategory: 3
+objectCategory: CN=Class-Schema,${SCHEMADN}
+defaultObjectCategory: CN=ldapPublicKey,${SCHEMADN}
+mayContain: sshPublicKey
+schemaIDGUID:: +8nFQ43rpkWTOgbCCcSkqA==
+
diff --git a/source4/setup/schema_samba4.ldif b/source4/setup/schema_samba4.ldif
index 6aafc9e5f49..85df49e5c04 100644
--- a/source4/setup/schema_samba4.ldif
+++ b/source4/setup/schema_samba4.ldif
@@ -393,3 +393,4 @@ defaultHidingValue: TRUE
 objectCategory: CN=Class-Schema,${SCHEMADN}
 defaultObjectCategory: CN=Samba4Top,${SCHEMADN}
 
+#Allocated: (ldapCompatPerson) governsID: 1.3.6.1.4.1.7165.4.2.3
diff --git a/source4/setup/wscript_build b/source4/setup/wscript_build
index 6bd48843938..ffaa88f73f1 100644
--- a/source4/setup/wscript_build
+++ b/source4/setup/wscript_build
@@ -8,6 +8,9 @@ bld.INSTALL_WILDCARD('${SETUPDIR}', 'adprep/WindowsServerDocs/Schema-Updates.md'
 bld.INSTALL_WILDCARD('${SETUPDIR}', 'adprep/WindowsServerDocs/Forest-Wide-Updates.md')
 bld.INSTALL_WILDCARD('${SETUPDIR}', 'adprep/WindowsServerDocs/*.diff')
 
+bld.INSTALL_WILDCARD('${SETUPDIR}', 'external-schema/*.txt')
+bld.INSTALL_WILDCARD('${SETUPDIR}', 'external-schema/*.ldif')
+
 bld.INSTALL_FILES('${SETUPDIR}', 'dns_update_list')
 bld.INSTALL_FILES('${SETUPDIR}', 'spn_update_list')
 
-- 
2.17.0


From bdab40f786618992050f04bf67737a4629e35017 Mon Sep 17 00:00:00 2001
From: William Brown <william at blackhats.net.au>
Date: Sat, 19 May 2018 12:19:58 +1000
Subject: [PATCH 7/7] python/samba/netcmd/schema.py: samdb schema update now

When we change schema values, we should trigger a schema update to refresh
the changes applied. This is called after a change is made. A helper to
samdb is added so that it's easier for other locations to call additionally.

Signed-off-by: William Brown <william at blackhats.net.au>
---
 python/samba/netcmd/domain.py | 2 +-
 python/samba/netcmd/schema.py | 2 ++
 python/samba/samdb.py         | 9 +++++++++
 3 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/python/samba/netcmd/domain.py b/python/samba/netcmd/domain.py
index 6b96673ab14..e7dc48bf8ca 100644
--- a/python/samba/netcmd/domain.py
+++ b/python/samba/netcmd/domain.py
@@ -3942,7 +3942,7 @@ schemaUpdateNow: 1
                     # Otherwise the OID-to-attribute mapping in
                     # _apply_updates_in_file() won't work, because it
                     # can't lookup the new OID in the schema
-                    self._ldap_schemaUpdateNow(samdb)
+                    samdb.set_schema_update_now()
 
                     samdb.modify_ldif(self.ldif, controls=['relax:0'])
                 else:
diff --git a/python/samba/netcmd/schema.py b/python/samba/netcmd/schema.py
index a6c63329e99..ad6e6c40e00 100644
--- a/python/samba/netcmd/schema.py
+++ b/python/samba/netcmd/schema.py
@@ -128,6 +128,7 @@ class cmd_schema_attribute_modify(Command):
                 str(searchflags_int), ldb.FLAG_MOD_REPLACE, 'searchFlags')
 
         samdb.modify(m)
+        samdb.set_schema_update_now()
         print("modified %s" % attr_dn)
 
 class cmd_schema_attribute_show(Command):
@@ -314,6 +315,7 @@ class cmd_schema_external_apply(Command):
         if schema is None:
             raise CommandError('No external schema named %s' % schemaname)
         schema.apply(samdb)
+        samdb.set_schema_update_now()
         self.outf.write("Success: applied schema %s\n" % schemaname)
 
 class cmd_schema_objectclass_show(Command):
diff --git a/python/samba/samdb.py b/python/samba/samdb.py
index a4212ea9c0d..fcaa7585eac 100644
--- a/python/samba/samdb.py
+++ b/python/samba/samdb.py
@@ -758,6 +758,15 @@ accountExpires: %u
     def set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes=True):
         dsdb._dsdb_set_schema_from_ldb(self, ldb_conn, write_indices_and_attributes)
 
+    def set_schema_update_now(self):
+        ldif = """
+dn:
+changetype: modify
+add: schemaUpdateNow
+schemaUpdateNow: 1
+"""
+        self.modify_ldif(ldif)
+
     def dsdb_DsReplicaAttribute(self, ldb, ldap_display_name, ldif_elements):
         '''convert a list of attribute values to a DRSUAPI DsReplicaAttribute'''
         return dsdb._dsdb_DsReplicaAttribute(ldb, ldap_display_name, ldif_elements)
-- 
2.17.0



More information about the samba-technical mailing list