--- /dev/null
+# 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()
--- /dev/null
+# 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)