[PATCH] samba-tool schema attribute query_oc

William Brown william at blackhats.net.au
Mon May 28 02:31:31 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 ;) 
> 
> :-)
> 
> > > 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 :) 
> 
> Thanks,
> 
> Andrew Bartlett
> 

Here you go. Updated based on your feedback. 

Link to branch:

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

Link to CI:

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

I suspect CI will fail but running quicktest on my machine this seems
mdb related?

-- 
Sincerely,

William
-------------- next part --------------
From 7d5271aee2ad0a401a3c1ff308c97ded9f6aef86 Mon Sep 17 00:00:00 2001
From: William Brown <william at blackhats.net.au>
Date: Thu, 19 Apr 2018 14:31:16 +1000
Subject: [PATCH 1/9] python/samba/netcmd/dsacl.py: support display and remove
 of dsacls

The current dsacl command only allowed insertion of an ACE to the ACL. This
patch provides the ability to display ACLs as well as removing ACEs from them.
This makes the dsacl command much more complete and useful to administrators.

Signed-off-by: William Brown <william at blackhats.net.au>
---
 docs-xml/manpages/samba-tool.8.xml     |  12 +-
 python/samba/netcmd/dsacl.py           | 257 +++++++++++++++++++------
 python/samba/tests/samba_tool/dsacl.py |  93 +++++++++
 source4/selftest/tests.py              |   1 +
 4 files changed, 303 insertions(+), 60 deletions(-)
 create mode 100644 python/samba/tests/samba_tool/dsacl.py

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index f2154b9d8de..0ea88de748b 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -454,9 +454,19 @@
 	<para>Administer DS ACLs</para>
 </refsect2>
 
+<refsect3>
+	<title>dsacl display</title>
+	<para>Display access control entries from a directory object.</para>
+</refsect3>
+
+<refsect3>
+	<title>dsacl remove</title>
+	<para>Remove access control entries from a directory object.</para>
+</refsect3>
+
 <refsect3>
 	<title>dsacl set</title>
-	<para>Modify access list on a directory object.</para>
+	<para>Add access control entries to a directory object.</para>
 </refsect3>
 
 <refsect2>
diff --git a/python/samba/netcmd/dsacl.py b/python/samba/netcmd/dsacl.py
index 28aa843adbc..dd8bc85df21 100644
--- a/python/samba/netcmd/dsacl.py
+++ b/python/samba/netcmd/dsacl.py
@@ -1,6 +1,7 @@
 # Manipulate ACLs on directory objects
 #
 # Copyright (C) Nadezhda Ivanova <nivanova at samba.org> 2010
+# 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
@@ -42,10 +43,105 @@ from samba.netcmd import (
     Option,
     )
 
+def _get_trustee_sid(samdb, trusteedn):
+    res = samdb.search(base=trusteedn, expression="(objectClass=*)",
+        scope=SCOPE_BASE)
+    assert(len(res) == 1)
+    return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
+
+def _get_domain_sid(samdb):
+    res = samdb.search(base=samdb.domain_dn(),
+            expression="(objectClass=*)", scope=SCOPE_BASE)
+    return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
+
+def _get_sddl(samdb, object_dn):
+    res = samdb.search(base=object_dn, scope=SCOPE_BASE,
+            attrs=["nTSecurityDescriptor"])
+    # we should theoretically always have an SD
+    assert(len(res) == 1)
+    desc = res[0]["nTSecurityDescriptor"][0]
+    return ndr_unpack(security.descriptor, desc)
+
+def _get_sddl_as_str(samdb, desc):
+    domain_sid = _get_domain_sid(samdb)
+    desc_sddl = desc.as_sddl(domain_sid)
+    return desc_sddl
+
+def _get_sddl_as_pretty_str(samdb, desc):
+    desc_sddl = _get_sddl_as_str(samdb, desc)
+    # Apply a very simple approach to make this readable - split content to
+    # new lines.
+    desc_sddl = desc_sddl.replace('(', '\n(')
+    # The second step is for handling headers in the middle of content.
+    # This finds ) where the next char is != \n, then captures it.
+    # We then add )\n and the captured character back.
+    desc_sddl = re.sub('\)([^\n])', r')\n\1', desc_sddl)
+    return desc_sddl
+
+def _clear_sddl_inherited(desc_sddl):
+    # Could this regex be more precise?
+    desc_aces = re.findall("\(.*?\)", desc_sddl)
+    for ace in desc_aces:
+        # This isn't clear: this is removing inherited descriptors from the
+        # set so that when we resend the modification we don't tread on
+        # them.
+        if ("ID" in ace):
+            desc_sddl = desc_sddl.replace(ace, "")
+    return desc_sddl
+
+# Extra handling in libcli/security/security_descriptor.c needs work to
+# make this a data manipulation rather than string manipulation.
+def _add_ace_sddl(samdb, desc, new_ace):
+    """Add new ace explicitly."""
+    desc_sddl = _get_sddl_as_str(samdb, desc)
+    desc_sddl = _clear_sddl_inherited(desc_sddl)
+    if new_ace in desc_sddl:
+        return
+    #TODO add bindings for descriptor manipulation and get rid of this
+    # This is a bit of a "hammer". It works by taking the SDDL like:
+    #    D:(...)(...)
+    # And injecting our new ACE before the first existing ACE. The issue is
+    # if the sddl is not in an expected order, we'll inject the ACE into the
+    # incorrect location. As a result we *probably* should make desc type
+    # that can handle this case properly and is aware of the sections
+    # in the sddl.
+    if desc_sddl.find("(") >= 0:
+        desc_sddl = desc_sddl.replace('(', '%s(' % new_ace, 1)
+    else:
+        # If there are no existing ACE entries, this just appends our
+        # new content.
+        desc_sddl = desc_sddl + new_ace
+    desc = security.descriptor.from_sddl(desc_sddl, _get_domain_sid(samdb))
+    return desc
+
+def _del_ace_sddl(samdb, desc, rem_ace):
+    desc_sddl = _get_sddl_as_str(samdb, desc)
+    desc_sddl = _clear_sddl_inherited(desc_sddl)
+    # This would be better with native methods, for now we use str manipulation
+    if rem_ace in desc_sddl:
+        print('present')
+    rem_ace = rem_ace.strip()
+    if not rem_ace.startswith('(') or not rem_ace.endswith(')'):
+        # Do nothing. Invalid ace. Can we raise a better error?
+        return desc
+    desc_sddl = desc_sddl.replace(rem_ace.strip(), "")
+    if rem_ace in desc_sddl:
+        print('still present')
+    desc = security.descriptor.from_sddl(desc_sddl, _get_domain_sid(samdb))
+    return desc
+
+def _commit_descriptor(samdb, object_dn, desc, controls=None):
+    assert(isinstance(desc, security.descriptor))
+    m = ldb.Message()
+    m.dn = ldb.Dn(samdb, object_dn)
+    m["nTSecurityDescriptor"]= ldb.MessageElement(
+            (ndr_pack(desc)), ldb.FLAG_MOD_REPLACE,
+            "nTSecurityDescriptor")
+    samdb.modify(m)
 
 
 class cmd_dsacl_set(Command):
-    """Modify access list on a directory object."""
+    """Modify access entries on a directory object."""
 
     synopsis = "%prog [options]"
     car_help = """ The access control right to allow or deny """
@@ -83,58 +179,6 @@ class cmd_dsacl_set(Command):
             type="string"),
         ]
 
-    def find_trustee_sid(self, samdb, trusteedn):
-        res = samdb.search(base=trusteedn, expression="(objectClass=*)",
-            scope=SCOPE_BASE)
-        assert(len(res) == 1)
-        return ndr_unpack( security.dom_sid,res[0]["objectSid"][0])
-
-    def modify_descriptor(self, samdb, object_dn, desc, controls=None):
-        assert(isinstance(desc, security.descriptor))
-        m = ldb.Message()
-        m.dn = ldb.Dn(samdb, object_dn)
-        m["nTSecurityDescriptor"]= ldb.MessageElement(
-                (ndr_pack(desc)), ldb.FLAG_MOD_REPLACE,
-                "nTSecurityDescriptor")
-        samdb.modify(m)
-
-    def read_descriptor(self, samdb, object_dn):
-        res = samdb.search(base=object_dn, scope=SCOPE_BASE,
-                attrs=["nTSecurityDescriptor"])
-        # we should theoretically always have an SD
-        assert(len(res) == 1)
-        desc = res[0]["nTSecurityDescriptor"][0]
-        return ndr_unpack(security.descriptor, desc)
-
-    def get_domain_sid(self, samdb):
-        res = samdb.search(base=samdb.domain_dn(),
-                expression="(objectClass=*)", scope=SCOPE_BASE)
-        return ndr_unpack( security.dom_sid,res[0]["objectSid"][0])
-
-    def add_ace(self, samdb, object_dn, new_ace):
-        """Add new ace explicitly."""
-        desc = self.read_descriptor(samdb, object_dn)
-        desc_sddl = desc.as_sddl(self.get_domain_sid(samdb))
-        #TODO add bindings for descriptor manipulation and get rid of this
-        desc_aces = re.findall("\(.*?\)", desc_sddl)
-        for ace in desc_aces:
-            if ("ID" in ace):
-                desc_sddl = desc_sddl.replace(ace, "")
-        if new_ace in desc_sddl:
-            return
-        if desc_sddl.find("(") >= 0:
-            desc_sddl = desc_sddl[:desc_sddl.index("(")] + new_ace + desc_sddl[desc_sddl.index("("):]
-        else:
-            desc_sddl = desc_sddl + new_ace
-        desc = security.descriptor.from_sddl(desc_sddl, self.get_domain_sid(samdb))
-        self.modify_descriptor(samdb, object_dn, desc)
-
-    def print_new_acl(self, samdb, object_dn):
-        desc = self.read_descriptor(samdb, object_dn)
-        desc_sddl = desc.as_sddl(self.get_domain_sid(samdb))
-        self.outf.write("new descriptor for %s:\n" % object_dn)
-        self.outf.write(desc_sddl + "\n")
-
     def run(self, car, action, objectdn, trusteedn, sddl,
             H=None, credopts=None, sambaopts=None, versionopts=None):
         lp = sambaopts.get_loadparm()
@@ -160,23 +204,118 @@ class cmd_dsacl_set(Command):
                 'repl-sync' : GUID_DRS_REPL_SYNCRONIZE,
                 'ro-repl-secret-sync' : GUID_DRS_RO_REPL_SECRET_SYNC,
                 }
-        sid = self.find_trustee_sid(samdb, trusteedn)
+
+        sid = None
+        if trusteedn is not None:
+            sid = _get_trustee_sid(samdb, trusteedn)
+
         if sddl:
             new_ace = sddl
-        elif action == "allow":
+        elif action == "allow" and sid is not None:
             new_ace = "(OA;;CR;%s;;%s)" % (cars[car], str(sid))
-        elif action == "deny":
+        elif action == "deny" and sid is not None:
             new_ace = "(OD;;CR;%s;;%s)" % (cars[car], str(sid))
         else:
             raise CommandError("Wrong argument '%s'!" % action)
 
-        self.print_new_acl(samdb, objectdn)
-        self.add_ace(samdb, objectdn, new_ace)
-        self.print_new_acl(samdb, objectdn)
+        desc = _get_sddl(samdb, objectdn)
+
+        orig_desc_sddl = _get_sddl_as_str(samdb, desc)
+        self.outf.write("original descriptor for %s:\n" % objectdn)
+        self.outf.write(orig_desc_sddl + "\n")
+
+        # This returns a new descriptor
+        desc = _add_ace_sddl(samdb, desc, new_ace)
+        _commit_descriptor(samdb, objectdn, desc)
+
+        new_desc_sddl = _get_sddl_as_str(samdb, desc)
+        self.outf.write("new descriptor for %s:\n" % objectdn)
+        self.outf.write(new_desc_sddl + "\n")
+
+class cmd_dsacl_display(Command):
+    """Display access entries on a directory object."""
+    synopsis = "%prog [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+        }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+               type=str, metavar="URL", dest="H"),
+        Option("--objectdn", help="DN of the object whose SD to display",
+            type="string"),
+        ]
+
+    def run(self, objectdn, H=None, credopts=None, sambaopts=None,
+            versionopts=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        # Can't display if we don't have a target!
+        if objectdn is None :
+            return self.usage()
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        desc = _get_sddl(samdb, objectdn)
+        desc_sddl = _get_sddl_as_pretty_str(samdb, desc)
+        self.outf.write("descriptor for %s:\n" % objectdn)
+        self.outf.write(desc_sddl + "\n")
+
+class cmd_dsacl_remove(Command):
+    """Remove an access entry from an object."""
+    synopsis = "%prog [options]"
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "versionopts": options.VersionOptions,
+        }
+
+    takes_options = [
+        Option("-H", "--URL", help="LDB URL for database or target server",
+               type=str, metavar="URL", dest="H"),
+        Option("--objectdn", help="DN of the object whose SD to display",
+            type="string"),
+        Option("--sddl", help="An ACE to be removed from the object",
+            type="string"),
+        ]
+
+    def run(self, objectdn, sddl, H=None, credopts=None, sambaopts=None,
+            versionopts=None):
+        lp = sambaopts.get_loadparm()
+        creds = credopts.get_credentials(lp)
+
+        if objectdn is None or sddl is None:
+            return self.usage()
+
+        samdb = SamDB(url=H, session_info=system_session(),
+            credentials=creds, lp=lp)
+
+        # Now remove the sddl from the descriptor, or raise "not found".
+        desc = _get_sddl(samdb, objectdn)
+
+        orig_desc_sddl = _get_sddl_as_str(samdb, desc)
+        self.outf.write("original descriptor for %s:\n" % objectdn)
+        self.outf.write(orig_desc_sddl + "\n")
+
+        # This returns a new descriptor
+        desc = _del_ace_sddl(samdb, desc, sddl)
+        _commit_descriptor(samdb, objectdn, desc)
 
+        new_desc_sddl = _get_sddl_as_str(samdb, desc)
+        self.outf.write("new descriptor for %s:\n" % objectdn)
+        self.outf.write(new_desc_sddl + "\n")
 
 class cmd_dsacl(SuperCommand):
     """DS ACLs manipulation."""
 
     subcommands = {}
     subcommands["set"] = cmd_dsacl_set()
+    subcommands["display"] = cmd_dsacl_display()
+    subcommands["remove"] = cmd_dsacl_remove()
+    # subcommands["replace"] = cmd_dsacl_replace()
diff --git a/python/samba/tests/samba_tool/dsacl.py b/python/samba/tests/samba_tool/dsacl.py
new file mode 100644
index 00000000000..0bd4c38f0ab
--- /dev/null
+++ b/python/samba/tests/samba_tool/dsacl.py
@@ -0,0 +1,93 @@
+# 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 DsaclCmdTestCase(SambaToolCmdTest):
+    """Tests for samba-tool dsacl subcommands"""
+    samdb = None
+
+    def setUp(self):
+        super(DsaclCmdTestCase, 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.runsubcmd("dsacl", "set", "")
+        # we add a group to manipulate the acl on
+        self.runsubcmd("group", "add", "dsacl_test",
+                              "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                              "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                            os.environ["DC_PASSWORD"]))
+
+    def tearDown(self):
+        super(DsaclCmdTestCase, self).tearDown()
+        # Remove our test group
+        if self._find_group():
+            self.runsubcmd("group", "delete", "dsacl_test",
+                                  "-H", "ldap://%s" % os.environ["DC_SERVER"],
+                                  "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                                                os.environ["DC_PASSWORD"]))
+
+    def _find_group(self):
+        search_filter = ("(&(sAMAccountName=%s)(objectCategory=%s,%s))" %
+                         (ldb.binary_encode("dsacl_test"),
+                         "CN=Group,CN=Schema,CN=Configuration",
+                         self.samdb.domain_dn()))
+        grouplist = self.samdb.search(base=self.samdb.domain_dn(),
+                                      scope=ldb.SCOPE_SUBTREE,
+                                      expression=search_filter,
+                                      attrs=[])
+        if grouplist:
+            return grouplist[0]
+        else:
+            return None
+
+    def test_display(self):
+        """Tests that we can display ACE"""
+        x = self._find_group()
+
+        (result, out, err) = self.runsubcmd("dsacl", "display",
+                              "--objectdn=%s" % x['dn'],
+                              "-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_add_remove(self):
+        """Tests a forced error"""
+        x = self._find_group()
+
+        (result, out, err) = self.runsubcmd("dsacl", "set",
+                              "--objectdn=%s" % x['dn'],
+                              "--sddl=(A;;RPLCLORC;;;AN)",
+                              "-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("dsacl", "remove",
+                              "--objectdn=%s" % x['dn'],
+                              "--sddl=(A;;RPLCLORC;;;AN)",
+                              "-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 b1d91ef9935..817e86a5f82 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -619,6 +619,7 @@ planpythontestsuite("chgdcpass:local", "samba.tests.samba_tool.user_check_passwo
 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: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 30fa7ea01bf6e10db73b23dfd27b43cef4a879f2 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/9] 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 |  65 ++++++++++++++
 source4/selftest/tests.py               |   1 +
 4 files changed, 194 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 0ea88de748b..b2d7a740c88 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -383,6 +383,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 6698fc9583a..acf9bc62ecc 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
@@ -4320,6 +4321,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]
+
+        self.outf.write("Settings for domain %s\n" % domain_dn)
+        for k in self.attributes:
+            try:
+                self.outf.write("%s: %s\n" % (k, domain_object[k][0]))
+            except KeyError:
+                self.outf.write("%s: <NO VALUE>\n" % 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)
+        self.outf.write("set %s: %s\n" % (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."""
 
@@ -4339,3 +4451,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..687e4b835ec
--- /dev/null
+++ b/python/samba/tests/samba_tool/domain.py
@@ -0,0 +1,65 @@
+# 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.runsublevelcmd("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)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("Settings for domain DC=samba,DC=example,DC=com", out)
+
+    def test_modify_machine_account_quota(self):
+        """Test that we can modify the machine account quota setting"""
+
+        (result, out, err) = self.runsublevelcmd("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)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("set ms-DS-MachineAccountQuota: 0", out)
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index 817e86a5f82..00ed7065785 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -620,6 +620,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: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 70bd8a83b50b954b28d2db92b32a61b989c89311 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 3/9] 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 |  67 ++++++++++
 source4/selftest/tests.py               |   1 +
 5 files changed, 253 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 b2d7a740c88..6c5d529ac11 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -484,6 +484,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..49296704701
--- /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]
+
+        self.outf.write("Settings for %s\n" % object_dn)
+        for attr in self.attributes:
+            try:
+                self.outf.write("%s: %s\n" % (attr, res_object[attr][0]))
+            except KeyError:
+                self.outf.write("%s: <NO VALUE>\n" % 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)
+        self.outf.write("set %s: %s\n" % (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..93796712258
--- /dev/null
+++ b/python/samba/tests/samba_tool/forest.py
@@ -0,0 +1,67 @@
+# 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.runsublevelcmd("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)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("dsheuristics: <NO VALUE>", out)
+
+    def test_modify_dsheuristics(self):
+        """Test that we can modify the dsheuristics setting"""
+
+        (result, out, err) = self.runsublevelcmd("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)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("set dsheuristics: 0000002", out)
+
diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py
index 00ed7065785..ecd1a2781da 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -621,6 +621,7 @@ 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")
 planpythontestsuite("none", "samba.tests.samba_tool.help")
-- 
2.17.0


From f28883ea5a9ba17d76ecd2c07f8b69f5e03fba4a 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 4/9] 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 | 10 ++++
 3 files changed, 96 insertions(+)

diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml
index 6c5d529ac11..89922d2d9c2 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -635,6 +635,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..06226717ab1 100644
--- a/python/samba/tests/samba_tool/group.py
+++ b/python/samba/tests/samba_tool/group.py
@@ -170,6 +170,16 @@ 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)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("dn: CN=Domain Users,CN=Users,DC=samba,DC=example,DC=com", out)
+
     def _randomGroup(self, base={}):
         """create a group with random attribute values, you can specify base attributes"""
         group = {
-- 
2.17.0


From 2800bcfad284655ac16ac723f483911bd4abbc97 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 5/9] 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 89922d2d9c2..3e5153cb8e3 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -762,6 +762,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 de6e4b28cdc..e8363754281 100644
--- a/python/samba/ms_schema.py
+++ b/python/samba/ms_schema.py
@@ -36,14 +36,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 abe434c8578..2b5c43faa54 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 ecd1a2781da..34818e3c777 100755
--- a/source4/selftest/tests.py
+++ b/source4/selftest/tests.py
@@ -622,6 +622,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 0e92055b5e10dcd37874fca2bd21f28634346b7a 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 6/9] 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 3e5153cb8e3..d41330152e8 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -777,6 +777,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 d6504100f8075328e5bed40fb7faa16b79e7d9d7 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 7/9] 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  | 34 +++++++
 source4/setup/schema_samba4.ldif              |  5 +-
 source4/setup/wscript_build                   |  3 +
 8 files changed, 296 insertions(+), 1 deletion(-)
 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 c5537080296..b373ee175dc 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..a364c41b1e7
--- /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: nsUniqueId compatability attribute.
+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: entryUUID compatability attribute.
+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: Unix LDAP compatability person for migration.
+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..8cdbd511d6e
--- /dev/null
+++ b/source4/setup/external-schema/sshpubkey.ldif
@@ -0,0 +1,34 @@
+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: OpenSSH Public key storage. Takes public key strings (.pub file
+ content)
+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: OpenSSH ldapPublicKey objectclass. Allows publicKeys associated
+ to objects for distribution.
+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 5b26dc0bee5..a0b1579e555 100644
--- a/source4/setup/schema_samba4.ldif
+++ b/source4/setup/schema_samba4.ldif
@@ -292,6 +292,10 @@
 #defaultObjectCategory: CN=Samba4-Local-Domain,${SCHEMADN}
 
 
+#Allocated: (ldapCompatPerson) governsID: 1.3.6.1.4.1.7165.4.2.3
+# WARNING: Don't allocate below this line, as the excess comments will break
+# ldif parsing.
+
 dn: CN=Samba4Top,${SCHEMADN}
 objectClass: top
 objectClass: classSchema
@@ -393,4 +397,3 @@ systemFlags: 16
 defaultHidingValue: TRUE
 objectCategory: CN=Class-Schema,${SCHEMADN}
 defaultObjectCategory: CN=Samba4Top,${SCHEMADN}
-
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 f59f55ad3a9ed1e0616c637e718870a323e2efb6 Mon Sep 17 00:00:00 2001
From: William Brown <william at blackhats.net.au>
Date: Mon, 28 May 2018 10:30:39 +1000
Subject: [PATCH 8/9] python/samba/tests/samba_tool/schema.py: Improve schema
 command testing

Assert the correct stdout content of the schema test commands.

Signed-off-by: William Brown <william at blackhats.net.au>
---
 python/samba/netcmd/schema.py           |  2 +-
 python/samba/tests/samba_tool/schema.py | 59 ++++++++++++++++---------
 2 files changed, 40 insertions(+), 21 deletions(-)

diff --git a/python/samba/netcmd/schema.py b/python/samba/netcmd/schema.py
index a6c63329e99..9988a15be3e 100644
--- a/python/samba/netcmd/schema.py
+++ b/python/samba/netcmd/schema.py
@@ -128,7 +128,7 @@ class cmd_schema_attribute_modify(Command):
                 str(searchflags_int), ldb.FLAG_MOD_REPLACE, 'searchFlags')
 
         samdb.modify(m)
-        print("modified %s" % attr_dn)
+        self.outf.write("modified %s" % attr_dn)
 
 class cmd_schema_attribute_show(Command):
     """Show details about an attribute from the schema.
diff --git a/python/samba/tests/samba_tool/schema.py b/python/samba/tests/samba_tool/schema.py
index df807d88bf0..ebd772341a1 100644
--- a/python/samba/tests/samba_tool/schema.py
+++ b/python/samba/tests/samba_tool/schema.py
@@ -33,93 +33,112 @@ class SchemaCmdTestCase(SambaToolCmdTest):
 
     def test_display_attribute(self):
         """Tests that we can display schema attributes"""
-        (result, out, err) = self.runsubcmd("schema", "attribute",
-                              "show", "uid",
+        (result, out, err) = self.runsublevelcmd("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)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("dn: CN=uid,CN=Schema,CN=Configuration,DC=samba,DC=example,DC=com", out)
 
     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",
+        (result, out, err) = self.runsublevelcmd("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",
+        (result, out, err) = self.runsublevelcmd("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)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("modified cn=uid,CN=Schema,CN=Configuration,DC=samba,DC=example,DC=com", out)
 
-        (result, out, err) = self.runsubcmd("schema", "attribute",
-                              "modify", "uid",
+        (result, out, err) = self.runsublevelcmd("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)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("modified cn=uid,CN=Schema,CN=Configuration,DC=samba,DC=example,DC=com", out)
 
-        (result, out, err) = self.runsubcmd("schema", "attribute",
-                              "modify", "uid",
+        (result, out, err) = self.runsublevelcmd("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)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("modified cn=uid,CN=Schema,CN=Configuration,DC=samba,DC=example,DC=com", out)
 
     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",
+        (result, out, err) = self.runsublevelcmd("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)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("--- MAY contain ---", out)
+        self.assertIn("--- MUST contain ---", out)
 
     def test_display_objectclass(self):
         """Tests that we can display schema objectclasses"""
-        (result, out, err) = self.runsubcmd("schema", "objectclass",
-                              "show", "person",
+        (result, out, err) = self.runsublevelcmd("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)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("dn: CN=Person,CN=Schema,CN=Configuration,DC=samba,DC=example,DC=com", out)
 
 
     def test_display_external(self):
         """Tests that we can display the external schemas"""
-        (result, out, err) = self.runsubcmd("schema", "external",
-                              "list")
+        (result, out, err) = self.runsublevelcmd("schema", ("external",
+                              "list"))
 
         self.assertCmdSuccess(result, out, err)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("ldapcompat", out)
 
-        (result, out, err) = self.runsubcmd("schema", "external",
-                              "show", "sshpubkey")
+        (result, out, err) = self.runsublevelcmd("schema", ("external",
+                              "show"), "sshpubkey")
 
         self.assertCmdSuccess(result, out, err)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("dn: CN=sshPublicKey,CN=Schema,...", out)
 
     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",
+            (result, out, err) = self.runsublevelcmd("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)
+            self.assertEquals(err,"","Shouldn't be any error messages")
+            self.assertIn("Success: applied schema sshpubkey", out)
 
-- 
2.17.0


From 7bbd236cdb5733a7531d9334931b50fe4975a9ef 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 9/9] 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 acf9bc62ecc..5aa1b8949b9 100644
--- a/python/samba/netcmd/domain.py
+++ b/python/samba/netcmd/domain.py
@@ -3947,7 +3947,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 9988a15be3e..531b89dc7de 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()
         self.outf.write("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 2b5c43faa54..7184fcfa4b0 100644
--- a/python/samba/samdb.py
+++ b/python/samba/samdb.py
@@ -762,6 +762,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