python/samba/netcmd/schema.py: add schema query and management.
authorWilliam Brown <william@blackhats.net.au>
Sat, 28 Apr 2018 05:22:29 +0000 (15:22 +1000)
committerAndrew Bartlett <abartlet@samba.org>
Tue, 29 May 2018 03:34:08 +0000 (05:34 +0200)
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@blackhats.net.au>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Garming Sam <garming@catalyst.net.nz>
docs-xml/manpages/samba-tool.8.xml
python/samba/ms_schema.py
python/samba/netcmd/main.py
python/samba/netcmd/schema.py [new file with mode: 0644]
python/samba/samdb.py
python/samba/tests/samba_tool/schema.py [new file with mode: 0644]
source4/selftest/tests.py

index fd58b1b1941e9eb8272dcbb2e2e73dd8b3cf08a4..19c18645171d9d64337a2963648b534038e33d92 100644 (file)
        <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>
index de6e4b28cdc04a2d87f47471fba2930f3d8d9a63..e836375428186cc8071fa0bf5c79c1a76a74a79b 100644 (file)
@@ -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
index 56720801199a05f509824908e2bda61dbb4090f6..83797662083373576bed761fc6b7a800e8b477a2 100644 (file)
@@ -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 (file)
index 0000000..dbefe79
--- /dev/null
@@ -0,0 +1,262 @@
+# Manipulate ACLs on directory objects
+#
+# Copyright (C) William Brown <william@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()
index abe434c8578853ec72aa92666a60d42ef348b2e2..2b5c43faa540a7909530ef24c2759f11a584e9af 100644 (file)
@@ -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 (file)
index 0000000..373ae16
--- /dev/null
@@ -0,0 +1,88 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) William Brown <william@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)
index 66b669e30f8f32318a13ee62591b6d9a4adf3935..535931663b8dac80fd0beb86866e2b6fdefaefe2 100755 (executable)
@@ -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.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")