summaryrefslogtreecommitdiff
path: root/source4/scripting/devel
diff options
context:
space:
mode:
authorZahari Zahariev <zahari.zahariev@postpath.com>2010-09-30 04:13:02 +0300
committerAnatoliy Atanasov <anatoliy.atanasov@postpath.com>2010-09-30 09:41:20 -0700
commit73763b367862121fb1175e829e863daef55a07bd (patch)
treecc29d0482730ee8ac7b0d2fd8127f8084425489f /source4/scripting/devel
parentbad98e37e7e4077a74c7b32d74499c78810192c5 (diff)
downloadsamba-73763b367862121fb1175e829e863daef55a07bd.tar.gz
samba-73763b367862121fb1175e829e863daef55a07bd.tar.bz2
samba-73763b367862121fb1175e829e863daef55a07bd.zip
LDAPCmp feature to compare nTSecurityDescriptors
New feature that enables LDAPCmp users to find unmatched or missing ACEs in objects for the three naming contexts between DCs in one domain (default) or different domains. Comparing security descriptors is not the default action but attribute compatison. So to activate the new mode there is --sd switch. However there are two view modes to the new --sd action which are 'section' (default) or 'collision'. In 'section' mode you can only find differences connected to missing or value unmatched ACEs but not disorder unmatch if ACE values and count are the same. All of the mentioned differences plus disorder ACE unmatch you can observe under 'collision' view however it is more verbose. Signed-off-by: Anatoliy Atanasov <anatoliy.atanasov@postpath.com>
Diffstat (limited to 'source4/scripting/devel')
-rwxr-xr-xsource4/scripting/devel/ldapcmp286
1 files changed, 252 insertions, 34 deletions
diff --git a/source4/scripting/devel/ldapcmp b/source4/scripting/devel/ldapcmp
index 74a22bf33b..58b187a039 100755
--- a/source4/scripting/devel/ldapcmp
+++ b/source4/scripting/devel/ldapcmp
@@ -59,12 +59,17 @@ class LDAPBase(object):
options=ldb_options)
self.two_domains = cmd_opts.two
self.quiet = cmd_opts.quiet
+ self.descriptor = cmd_opts.descriptor
+ self.view = cmd_opts.view
+ self.verbose = cmd_opts.verbose
self.host = host
self.base_dn = self.find_basedn()
self.domain_netbios = self.find_netbios()
self.server_names = self.find_servers()
self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
- self.domain_sid_bin = self.get_object_sid(self.base_dn)
+ self.domain_sid = self.find_domain_sid()
+ self.get_guid_map()
+ self.get_sid_map()
#
# Log some domain controller specific place-holers that are being used
# when compare content of two DCs. Uncomment for DEBUG purposes.
@@ -72,9 +77,13 @@ class LDAPBase(object):
print "\n* Place-holders for %s:" % self.host
print 4*" " + "${DOMAIN_DN} => %s" % self.base_dn
print 4*" " + "${DOMAIN_NETBIOS} => %s" % self.domain_netbios
- print 4*" " + "${SERVERNAME} => %s" % self.server_names
+ print 4*" " + "${SERVER_NAME} => %s" % self.server_names
print 4*" " + "${DOMAIN_NAME} => %s" % self.domain_name
+ def find_domain_sid(self):
+ res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
+ return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
+
def find_servers(self):
"""
"""
@@ -134,22 +143,210 @@ class LDAPBase(object):
res[key] = list(res[key])
return res
- def get_descriptor(self, object_dn):
+ def get_descriptor_sddl(self, object_dn):
res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
- return res[0]["nTSecurityDescriptor"][0]
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ return desc.as_sddl(self.domain_sid)
+
+ def guid_as_string(self, guid_blob):
+ """ Translate binary representation of schemaIDGUID to standard string representation.
+ @gid_blob: binary schemaIDGUID
+ """
+ blob = "%s" % guid_blob
+ stops = [4, 2, 2, 2, 6]
+ index = 0
+ res = ""
+ x = 0
+ while x < len(stops):
+ tmp = ""
+ y = 0
+ while y < stops[x]:
+ c = hex(ord(blob[index])).replace("0x", "")
+ c = [None, "0" + c, c][len(c)]
+ if 2 * index < len(blob):
+ tmp = c + tmp
+ else:
+ tmp += c
+ index += 1
+ y += 1
+ res += tmp + " "
+ x += 1
+ assert index == len(blob)
+ return res.strip().replace(" ", "-")
+
+ def get_guid_map(self):
+ """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
+ """
+ self.guid_map = {}
+ res = self.ldb.search(base="cn=schema,cn=configuration,%s" % self.base_dn, \
+ expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
+ for item in res:
+ self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
+ #
+ res = self.ldb.search(base="cn=extended-rights,cn=configuration,%s" % self.base_dn, \
+ expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
+ for item in res:
+ self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
+
+ def get_sid_map(self):
+ """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
+ """
+ self.sid_map = {}
+ res = self.ldb.search(base="%s" % self.base_dn, \
+ expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
+ for item in res:
+ try:
+ self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
+ except KeyError:
+ pass
+
+class Descriptor(object):
+ def __init__(self, connection, dn):
+ self.con = connection
+ self.dn = dn
+ self.sddl = self.con.get_descriptor_sddl(self.dn)
+ self.dacl_list = self.extract_dacl()
+
+ def extract_dacl(self):
+ """ Extracts the DACL as a list of ACE string (with the brakets).
+ """
+ try:
+ res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
+ except AttributeError:
+ return []
+ return re.findall("(\(.*?\))", res)
+
+ def fix_guid(self, ace):
+ res = "%s" % ace
+ guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
+ # If there are not GUIDs to replace return the same ACE
+ if len(guids) == 0:
+ return res
+ for guid in guids:
+ try:
+ name = self.con.guid_map[guid.lower()]
+ res = res.replace(guid, name)
+ except KeyError:
+ # Do not bother if the GUID is not found in
+ # cn=Schema or cn=Extended-Rights
+ pass
+ return res
+
+ def fix_sid(self, ace):
+ res = "%s" % ace
+ sids = re.findall("S-[-0-9]+", res)
+ # If there are not SIDs to replace return the same ACE
+ if len(sids) == 0:
+ return res
+ for sid in sids:
+ try:
+ name = self.con.sid_map[sid]
+ res = res.replace(sid, name)
+ except KeyError:
+ # Do not bother if the SID is not found in baseDN
+ pass
+ return res
+
+ def fixit(self, ace):
+ """ Combine all replacement methods in one
+ """
+ res = "%s" % ace
+ res = self.fix_guid(res)
+ res = self.fix_sid(res)
+ return res
+ def diff_1(self, other):
+ res = ""
+ if len(self.dacl_list) != len(other.dacl_list):
+ res += 4*" " + "Difference in ACE count:\n"
+ res += 8*" " + "=> %s\n" % len(self.dacl_list)
+ res += 8*" " + "=> %s\n" % len(other.dacl_list)
+ #
+ i = 0
+ flag = True
+ while True:
+ self_ace = None
+ other_ace = None
+ try:
+ self_ace = "%s" % self.dacl_list[i]
+ except IndexError:
+ self_ace = ""
+ #
+ try:
+ other_ace = "%s" % other.dacl_list[i]
+ except IndexError:
+ other_ace = ""
+ if len(self_ace) + len(other_ace) == 0:
+ break
+ self_ace_fixed = "%s" % self.fixit(self_ace)
+ other_ace_fixed = "%s" % other.fixit(other_ace)
+ if self_ace_fixed != other_ace_fixed:
+ res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
+ flag = False
+ else:
+ res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
+ i += 1
+ return (flag, res)
+
+ def diff_2(self, other):
+ res = ""
+ if len(self.dacl_list) != len(other.dacl_list):
+ res += 4*" " + "Difference in ACE count:\n"
+ res += 8*" " + "=> %s\n" % len(self.dacl_list)
+ res += 8*" " + "=> %s\n" % len(other.dacl_list)
+ #
+ common_aces = []
+ self_aces = []
+ other_aces = []
+ self_dacl_list_fixed = []
+ other_dacl_list_fixed = []
+ [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
+ [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
+ for ace in self_dacl_list_fixed:
+ try:
+ other_dacl_list_fixed.index(ace)
+ except ValueError:
+ self_aces.append(ace)
+ else:
+ common_aces.append(ace)
+ self_aces = sorted(self_aces)
+ if len(self_aces) > 0:
+ res += 4*" " + "ACEs found only in %s:\n" % self.con.host
+ for ace in self_aces:
+ res += 8*" " + ace + "\n"
+ #
+ for ace in other_dacl_list_fixed:
+ try:
+ self_dacl_list_fixed.index(ace)
+ except ValueError:
+ other_aces.append(ace)
+ else:
+ common_aces.append(ace)
+ other_aces = sorted(other_aces)
+ if len(other_aces) > 0:
+ res += 4*" " + "ACEs found only in %s:\n" % other.con.host
+ for ace in other_aces:
+ res += 8*" " + ace + "\n"
+ #
+ common_aces = sorted(list(set(common_aces)))
+ if self.con.verbose:
+ res += 4*" " + "ACEs found in both:\n"
+ for ace in common_aces:
+ res += 8*" " + ace + "\n"
+ return (self_aces == [] and other_aces == [], res)
class LDAPObject(object):
- def __init__(self, connection, dn, summary, cmd_opts):
+ def __init__(self, connection, dn, summary):
self.con = connection
- self.two_domains = cmd_opts.two
- self.quiet = cmd_opts.quiet
- self.verbose = cmd_opts.verbose
+ self.two_domains = self.con.two_domains
+ self.quiet = self.con.quiet
+ self.verbose = self.con.verbose
self.summary = summary
self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
for x in self.con.server_names:
- self.dn = self.dn.replace("CN=${SERVERNAME}", "CN=%s" % x)
+ self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
self.attributes = self.con.get_attributes(self.dn)
# Attributes that are considered always to be different e.g based on timestamp etc.
#
@@ -199,7 +396,7 @@ class LDAPObject(object):
"dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
self.domain_attributes = [x.upper() for x in self.domain_attributes]
#
- # May contain DOMAIN_NETBIOS and SERVERNAME
+ # May contain DOMAIN_NETBIOS and SERVER_NAME
self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
"servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
"msDS-IsDomainFor", "interSiteTopologyGenerator",]
@@ -249,10 +446,30 @@ class LDAPObject(object):
if not self.two_domains or len(self.con.server_names) > 1:
return res
for x in self.con.server_names:
- res = res.upper().replace(x, "${SERVERNAME}")
+ res = res.upper().replace(x, "${SERVER_NAME}")
return res
def __eq__(self, other):
+ if self.con.descriptor:
+ return self.cmp_desc(other)
+ return self.cmp_attrs(other)
+
+ def cmp_desc(self, other):
+ d1 = Descriptor(self.con, self.dn)
+ d2 = Descriptor(other.con, other.dn)
+ if self.con.view == "section":
+ res = d1.diff_2(d2)
+ elif self.con.view == "collision":
+ res = d1.diff_1(d2)
+ else:
+ raise Exception("Unknown --view option value.")
+ #
+ self.screen_output = res[1][:-1]
+ other.screen_output = res[1][:-1]
+ #
+ return res[0]
+
+ def cmp_attrs(self, other):
res = ""
self.unique_attrs = []
self.df_value_attrs = []
@@ -324,7 +541,7 @@ class LDAPObject(object):
continue
#
if x.upper() in self.servername_attributes:
- # Attributes with SERVERNAME
+ # Attributes with SERVER_NAME
m = p
n = q
if not p and not q:
@@ -370,12 +587,11 @@ class LDAPObject(object):
class LDAPBundel(object):
- def __init__(self, connection, context, cmd_opts, dn_list=None):
+ def __init__(self, connection, context, dn_list=None):
self.con = connection
- self.cmd_opts = cmd_opts
- self.two_domains = cmd_opts.two
- self.quiet = cmd_opts.quiet
- self.verbose = cmd_opts.verbose
+ self.two_domains = self.con.two_domains
+ self.quiet = self.con.quiet
+ self.verbose = self.con.verbose
self.summary = {}
self.summary["unique_attrs"] = []
self.summary["df_value_attrs"] = []
@@ -396,7 +612,7 @@ class LDAPBundel(object):
tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
if len(self.con.server_names) == 1:
for x in self.con.server_names:
- tmp = tmp.replace("CN=%s" % x, "CN=${SERVERNAME}")
+ tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
self.dn_list[counter] = tmp
counter += 1
self.dn_list = list(set(self.dn_list))
@@ -454,16 +670,14 @@ class LDAPBundel(object):
try:
object1 = LDAPObject(connection=self.con,
dn=self.dn_list[index],
- summary=self.summary,
- cmd_opts = self.cmd_opts)
+ summary=self.summary)
except LdbError, (ERR_NO_SUCH_OBJECT, _):
self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
skip = True
try:
object2 = LDAPObject(connection=other.con,
dn=other.dn_list[index],
- summary=other.summary,
- cmd_opts = self.cmd_opts)
+ summary=other.summary)
except LdbError, (ERR_NO_SUCH_OBJECT, _):
self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
skip = True
@@ -471,7 +685,7 @@ class LDAPBundel(object):
index += 1
continue
if object1 == object2:
- if self.verbose:
+ if self.con.verbose:
self.log( "\nComparing:" )
self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
@@ -542,6 +756,10 @@ if __name__ == "__main__":
help="Do not print anything but relay on just exit code",)
parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
help="Print all DN pairs that have been compared",)
+ parser.add_option("", "--sd", dest="descriptor", action="store_true", default=False,
+ help="Compare nTSecurityDescriptor attibutes only",)
+ parser.add_option("", "--view", dest="view", default="section",
+ help="Display mode for nTSecurityDescriptor results. Possible values: section or collision.",)
(opts, args) = parser.parse_args()
lp = sambaopts.get_loadparm()
@@ -566,6 +784,8 @@ if __name__ == "__main__":
if opts.verbose and opts.quiet:
parser.error("You cannot set --verbose and --quiet together")
+ if opts.descriptor and opts.view.upper() not in ["SECTION", "COLLISION"]:
+ parser.error("Unknown --view option value. Choose from: section or collision.")
con1 = LDAPBase(opts.host, opts, creds, lp)
assert len(con1.base_dn) > 0
@@ -578,8 +798,8 @@ if __name__ == "__main__":
if not opts.quiet:
print "\n* Comparing [%s] context..." % context
- b1 = LDAPBundel(con1, context=context, cmd_opts=opts)
- b2 = LDAPBundel(con2, context=context, cmd_opts=opts)
+ b1 = LDAPBundel(con1, context=context)
+ b2 = LDAPBundel(con2, context=context)
if b1 == b2:
if not opts.quiet:
@@ -587,16 +807,14 @@ if __name__ == "__main__":
else:
if not opts.quiet:
print "\n* Result for [%s]: FAILURE" % context
- print "\nSUMMARY"
- print "---------"
+ if not opts.descriptor:
+ assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
+ b2.summary["df_value_attrs"] = []
+ print "\nSUMMARY"
+ print "---------"
+ b1.print_summary()
+ b2.print_summary()
# mark exit status as FAILURE if a least one comparison failed
status = -1
- assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
- b2.summary["df_value_attrs"] = []
-
- if not opts.quiet:
- b1.print_summary()
- b2.print_summary()
-
sys.exit(status)