LDAPCmp feature to compare nTSecurityDescriptors
[kai/samba.git] / source4 / scripting / devel / ldapcmp
1 #!/usr/bin/env python
2 #
3 # Unix SMB/CIFS implementation.
4 # A script to compare differences of objects and attributes between
5 # two LDAP servers both running at the same time. It generally compares
6 # one of the three pratitions DOMAIN, CONFIGURATION or SCHEMA. Users
7 # that have to be provided sheould be able to read objects in any of the
8 # above partitions.
9
10 # Copyright (C) Zahari Zahariev <zahari.zahariev@postpath.com> 2009, 2010
11 #
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 3 of the License, or
15 # (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
24 #
25
26 import os
27 import re
28 import sys
29 from optparse import OptionParser
30
31 sys.path.insert(0, "bin/python")
32
33 import samba
34 import samba.getopt as options
35 from samba import Ldb
36 from samba.ndr import ndr_pack, ndr_unpack
37 from samba.dcerpc import security
38 from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, ERR_NO_SUCH_OBJECT, LdbError
39
40 global summary
41 summary = {}
42
43 class LDAPBase(object):
44
45     def __init__(self, host, cmd_opts, creds, lp):
46         ldb_options = []
47         samdb_url = host
48         if not "://" in host:
49             if os.path.isfile(host):
50                 samdb_url = "tdb://%s" % host
51             else:
52                 samdb_url = "ldap://%s:389" % host
53         # use 'paged_search' module when connecting remotely
54         if samdb_url.lower().startswith("ldap://"):
55             ldb_options = ["modules:paged_searches"]
56         self.ldb = Ldb(url=samdb_url,
57                        credentials=creds,
58                        lp=lp,
59                        options=ldb_options)
60         self.two_domains = cmd_opts.two
61         self.quiet = cmd_opts.quiet
62         self.descriptor = cmd_opts.descriptor
63         self.view = cmd_opts.view
64         self.verbose = cmd_opts.verbose
65         self.host = host
66         self.base_dn = self.find_basedn()
67         self.domain_netbios = self.find_netbios()
68         self.server_names = self.find_servers()
69         self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
70         self.domain_sid = self.find_domain_sid()
71         self.get_guid_map()
72         self.get_sid_map()
73         #
74         # Log some domain controller specific place-holers that are being used
75         # when compare content of two DCs. Uncomment for DEBUG purposes.
76         if self.two_domains and not self.quiet:
77             print "\n* Place-holders for %s:" % self.host
78             print 4*" " + "${DOMAIN_DN}      => %s" % self.base_dn
79             print 4*" " + "${DOMAIN_NETBIOS} => %s" % self.domain_netbios
80             print 4*" " + "${SERVER_NAME}     => %s" % self.server_names
81             print 4*" " + "${DOMAIN_NAME}    => %s" % self.domain_name
82
83     def find_domain_sid(self):
84         res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
85         return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
86
87     def find_servers(self):
88         """
89         """
90         res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, \
91                 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
92         assert len(res) > 0
93         srv = []
94         for x in res:
95             srv.append(x["cn"][0])
96         return srv
97
98     def find_netbios(self):
99         res = self.ldb.search(base="CN=Partitions,CN=Configuration,%s" % self.base_dn, \
100                 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
101         assert len(res) > 0
102         for x in res:
103             if "nETBIOSName" in x.keys():
104                 return x["nETBIOSName"][0]
105
106     def find_basedn(self):
107         res = self.ldb.search(base="", expression="(objectClass=*)", scope=SCOPE_BASE,
108                 attrs=["defaultNamingContext"])
109         assert len(res) == 1
110         return res[0]["defaultNamingContext"][0]
111
112     def object_exists(self, object_dn):
113         res = None
114         try:
115             res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, expression="(objectClass=*)")
116         except LdbError, (ERR_NO_SUCH_OBJECT, _):
117             return False
118         return len(res) == 1
119
120     def get_object_sid(self, object_dn):
121         try:
122             res = self.ldb.search(base=object_dn, expression="(objectClass=*)", scope=SCOPE_BASE, attrs=["objectSid"])
123         except LdbError, (ERR_NO_SUCH_OBJECT, _):
124             raise Exception("DN sintax is wrong or object does't exist: " + object_dn)
125         assert len(res) == 1
126         return res[0]["objectSid"][0]
127
128     def delete_force(self, object_dn):
129         try:
130             self.ldb.delete(object_dn)
131         except Ldb.LdbError, e:
132             assert "No such object" in str(e)
133
134     def get_attributes(self, object_dn):
135         """ Returns dict with all default visible attributes
136         """
137         res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
138         assert len(res) == 1
139         res = dict(res[0])
140         # 'Dn' element is not iterable and we have it as 'distinguishedName'
141         del res["dn"]
142         for key in res.keys():
143             res[key] = list(res[key])
144         return res
145
146     def get_descriptor_sddl(self, object_dn):
147         res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
148         desc = res[0]["nTSecurityDescriptor"][0]
149         desc = ndr_unpack(security.descriptor, desc)
150         return desc.as_sddl(self.domain_sid)
151
152     def guid_as_string(self, guid_blob):
153         """ Translate binary representation of schemaIDGUID to standard string representation.
154             @gid_blob: binary schemaIDGUID
155         """
156         blob = "%s" % guid_blob
157         stops = [4, 2, 2, 2, 6]
158         index = 0
159         res = ""
160         x = 0
161         while x < len(stops):
162             tmp = ""
163             y = 0
164             while y < stops[x]:
165                 c = hex(ord(blob[index])).replace("0x", "")
166                 c = [None, "0" + c, c][len(c)]
167                 if 2 * index < len(blob):
168                     tmp = c + tmp
169                 else:
170                     tmp += c
171                 index += 1
172                 y += 1
173             res += tmp + " "
174             x += 1
175         assert index == len(blob)
176         return res.strip().replace(" ", "-")
177
178     def get_guid_map(self):
179         """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
180         """
181         self.guid_map = {}
182         res = self.ldb.search(base="cn=schema,cn=configuration,%s" % self.base_dn, \
183                 expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
184         for item in res:
185             self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
186         #
187         res = self.ldb.search(base="cn=extended-rights,cn=configuration,%s" % self.base_dn, \
188                 expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
189         for item in res:
190             self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
191
192     def get_sid_map(self):
193         """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
194         """
195         self.sid_map = {}
196         res = self.ldb.search(base="%s" % self.base_dn, \
197                 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
198         for item in res:
199             try:
200                 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
201             except KeyError:
202                 pass
203
204 class Descriptor(object):
205     def __init__(self, connection, dn):
206         self.con = connection
207         self.dn = dn
208         self.sddl = self.con.get_descriptor_sddl(self.dn)
209         self.dacl_list = self.extract_dacl()
210
211     def extract_dacl(self):
212         """ Extracts the DACL as a list of ACE string (with the brakets).
213         """
214         try:
215             res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
216         except AttributeError:
217             return []
218         return re.findall("(\(.*?\))", res)
219
220     def fix_guid(self, ace):
221         res = "%s" % ace
222         guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
223         # If there are not GUIDs to replace return the same ACE
224         if len(guids) == 0:
225             return res
226         for guid in guids:
227             try:
228                 name = self.con.guid_map[guid.lower()]
229                 res = res.replace(guid, name)
230             except KeyError:
231                 # Do not bother if the GUID is not found in
232                 # cn=Schema or cn=Extended-Rights
233                 pass
234         return res
235
236     def fix_sid(self, ace):
237         res = "%s" % ace
238         sids = re.findall("S-[-0-9]+", res)
239         # If there are not SIDs to replace return the same ACE
240         if len(sids) == 0:
241             return res
242         for sid in sids:
243             try:
244                 name = self.con.sid_map[sid]
245                 res = res.replace(sid, name)
246             except KeyError:
247                 # Do not bother if the SID is not found in baseDN
248                 pass
249         return res
250
251     def fixit(self, ace):
252         """ Combine all replacement methods in one
253         """
254         res = "%s" % ace
255         res = self.fix_guid(res)
256         res = self.fix_sid(res)
257         return res
258
259     def diff_1(self, other):
260         res = ""
261         if len(self.dacl_list) != len(other.dacl_list):
262             res += 4*" " + "Difference in ACE count:\n"
263             res += 8*" " + "=> %s\n" % len(self.dacl_list)
264             res += 8*" " + "=> %s\n" % len(other.dacl_list)
265         #
266         i = 0
267         flag = True
268         while True:
269             self_ace = None
270             other_ace = None
271             try:
272                 self_ace = "%s" % self.dacl_list[i]
273             except IndexError:
274                 self_ace = ""
275             #
276             try:
277                 other_ace = "%s" % other.dacl_list[i]
278             except IndexError:
279                 other_ace = ""
280             if len(self_ace) + len(other_ace) == 0:
281                 break
282             self_ace_fixed = "%s" % self.fixit(self_ace)
283             other_ace_fixed = "%s" % other.fixit(other_ace)
284             if self_ace_fixed != other_ace_fixed:
285                 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
286                 flag = False
287             else:
288                 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
289             i += 1
290         return (flag, res)
291
292     def diff_2(self, other):
293         res = ""
294         if len(self.dacl_list) != len(other.dacl_list):
295             res += 4*" " + "Difference in ACE count:\n"
296             res += 8*" " + "=> %s\n" % len(self.dacl_list)
297             res += 8*" " + "=> %s\n" % len(other.dacl_list)
298         #
299         common_aces = []
300         self_aces = []
301         other_aces = []
302         self_dacl_list_fixed = []
303         other_dacl_list_fixed = []
304         [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
305         [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
306         for ace in self_dacl_list_fixed:
307             try:
308                 other_dacl_list_fixed.index(ace)
309             except ValueError:
310                 self_aces.append(ace)
311             else:
312                 common_aces.append(ace)
313         self_aces = sorted(self_aces)
314         if len(self_aces) > 0:
315             res += 4*" " + "ACEs found only in %s:\n" % self.con.host
316             for ace in self_aces:
317                 res += 8*" " + ace + "\n"
318         #
319         for ace in other_dacl_list_fixed:
320             try:
321                 self_dacl_list_fixed.index(ace)
322             except ValueError:
323                 other_aces.append(ace)
324             else:
325                 common_aces.append(ace)
326         other_aces = sorted(other_aces)
327         if len(other_aces) > 0:
328             res += 4*" " + "ACEs found only in %s:\n" % other.con.host
329             for ace in other_aces:
330                 res += 8*" " + ace + "\n"
331         #
332         common_aces = sorted(list(set(common_aces)))
333         if self.con.verbose:
334             res += 4*" " + "ACEs found in both:\n"
335             for ace in common_aces:
336                 res += 8*" " + ace + "\n"
337         return (self_aces == [] and other_aces == [], res)
338
339 class LDAPObject(object):
340     def __init__(self, connection, dn, summary):
341         self.con = connection
342         self.two_domains = self.con.two_domains
343         self.quiet = self.con.quiet
344         self.verbose = self.con.verbose
345         self.summary = summary
346         self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
347         self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
348         for x in self.con.server_names:
349             self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
350         self.attributes = self.con.get_attributes(self.dn)
351         # Attributes that are considered always to be different e.g based on timestamp etc.
352         #
353         # One domain - two domain controllers
354         self.ignore_attributes =  [
355                 # Default Naming Context
356                 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
357                 "operatingSystemVersion","oEMInformation",
358                 # Configuration Naming Context
359                 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
360                 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
361                 # Schema Naming Context
362                 "prefixMap",]
363         self.dn_attributes = []
364         self.domain_attributes = []
365         self.servername_attributes = []
366         self.netbios_attributes = []
367         self.other_attributes = []
368         # Two domains - two domain controllers
369
370         if self.two_domains:
371             self.ignore_attributes +=  [
372                 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
373                 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
374                 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
375                 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
376                 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
377                 # After Exchange preps
378                 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
379             #
380             # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
381             self.dn_attributes = [
382                 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
383                 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
384                 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
385                 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
386                 # After Exchange preps
387                 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
388                 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
389                 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
390                 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
391             self.dn_attributes = [x.upper() for x in self.dn_attributes]
392             #
393             # Attributes that contain the Domain name e.g. 'samba.org'
394             self.domain_attributes = [
395                 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
396                 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
397             self.domain_attributes = [x.upper() for x in self.domain_attributes]
398             #
399             # May contain DOMAIN_NETBIOS and SERVER_NAME
400             self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
401                 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
402                 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
403             self.servername_attributes = [x.upper() for x in self.servername_attributes]
404             #
405             self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
406             self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
407             #
408             self.other_attributes = [ "name", "DC",]
409             self.other_attributes = [x.upper() for x in self.other_attributes]
410         #
411         self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
412
413     def log(self, msg):
414         """
415         Log on the screen if there is no --quiet oprion set
416         """
417         if not self.quiet:
418             print msg
419
420     def fix_dn(self, s):
421         res = "%s" % s
422         if not self.two_domains:
423             return res
424         if res.upper().endswith(self.con.base_dn.upper()):
425             res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
426         return res
427
428     def fix_domain_name(self, s):
429         res = "%s" % s
430         if not self.two_domains:
431             return res
432         res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
433         res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
434         return res
435
436     def fix_domain_netbios(self, s):
437         res = "%s" % s
438         if not self.two_domains:
439             return res
440         res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
441         res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
442         return res
443
444     def fix_server_name(self, s):
445         res = "%s" % s
446         if not self.two_domains or len(self.con.server_names) > 1:
447             return res
448         for x in self.con.server_names:
449             res = res.upper().replace(x, "${SERVER_NAME}")
450         return res
451
452     def __eq__(self, other):
453         if self.con.descriptor:
454             return self.cmp_desc(other)
455         return self.cmp_attrs(other)
456
457     def cmp_desc(self, other):
458         d1 = Descriptor(self.con, self.dn)
459         d2 = Descriptor(other.con, other.dn)
460         if self.con.view == "section":
461             res = d1.diff_2(d2)
462         elif self.con.view == "collision":
463             res = d1.diff_1(d2)
464         else:
465             raise Exception("Unknown --view option value.")
466         #
467         self.screen_output = res[1][:-1]
468         other.screen_output = res[1][:-1]
469         #
470         return res[0]
471
472     def cmp_attrs(self, other):
473         res = ""
474         self.unique_attrs = []
475         self.df_value_attrs = []
476         other.unique_attrs = []
477         if self.attributes.keys() != other.attributes.keys():
478             #
479             title = 4*" " + "Attributes found only in %s:" % self.con.host
480             for x in self.attributes.keys():
481                 if not x in other.attributes.keys() and \
482                 not x.upper() in [q.upper() for q in other.ignore_attributes]:
483                     if title:
484                         res += title + "\n"
485                         title = None
486                     res += 8*" " + x + "\n"
487                     self.unique_attrs.append(x)
488             #
489             title = 4*" " + "Attributes found only in %s:" % other.con.host
490             for x in other.attributes.keys():
491                 if not x in self.attributes.keys() and \
492                 not x.upper() in [q.upper() for q in self.ignore_attributes]:
493                     if title:
494                         res += title + "\n"
495                         title = None
496                     res += 8*" " + x + "\n"
497                     other.unique_attrs.append(x)
498         #
499         missing_attrs = [x.upper() for x in self.unique_attrs]
500         missing_attrs += [x.upper() for x in other.unique_attrs]
501         title = 4*" " + "Difference in attribute values:"
502         for x in self.attributes.keys():
503             if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
504                 continue
505             if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
506                 self.attributes[x] = sorted(self.attributes[x])
507                 other.attributes[x] = sorted(other.attributes[x])
508             if self.attributes[x] != other.attributes[x]:
509                 p = None
510                 q = None
511                 m = None
512                 n = None
513                 # First check if the difference can be fixed but shunting the first part
514                 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
515                 if x.upper() in self.other_attributes:
516                     p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
517                     q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
518                     if p == q:
519                         continue
520                 # Attribute values that are list that contain DN based values that may differ
521                 elif x.upper() in self.dn_attributes:
522                     m = p
523                     n = q
524                     if not p and not q:
525                         m = self.attributes[x]
526                         n = other.attributes[x]
527                     p = [self.fix_dn(j) for j in m]
528                     q = [other.fix_dn(j) for j in n]
529                     if p == q:
530                         continue
531                 # Attributes that contain the Domain name in them
532                 if x.upper() in self.domain_attributes:
533                     m = p
534                     n = q
535                     if not p and not q:
536                         m = self.attributes[x]
537                         n = other.attributes[x]
538                     p = [self.fix_domain_name(j) for j in m]
539                     q = [other.fix_domain_name(j) for j in n]
540                     if p == q:
541                         continue
542                 #
543                 if x.upper() in self.servername_attributes:
544                     # Attributes with SERVER_NAME
545                     m = p
546                     n = q
547                     if not p and not q:
548                         m = self.attributes[x]
549                         n = other.attributes[x]
550                     p = [self.fix_server_name(j) for j in m]
551                     q = [other.fix_server_name(j) for j in n]
552                     if p == q:
553                         continue
554                 #
555                 if x.upper() in self.netbios_attributes:
556                     # Attributes with NETBIOS Domain name
557                     m = p
558                     n = q
559                     if not p and not q:
560                         m = self.attributes[x]
561                         n = other.attributes[x]
562                     p = [self.fix_domain_netbios(j) for j in m]
563                     q = [other.fix_domain_netbios(j) for j in n]
564                     if p == q:
565                         continue
566                 #
567                 if title:
568                     res += title + "\n"
569                     title = None
570                 if p and q:
571                     res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
572                 else:
573                     res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
574                 self.df_value_attrs.append(x)
575         #
576         if self.unique_attrs + other.unique_attrs != []:
577             assert self.unique_attrs != other.unique_attrs
578         self.summary["unique_attrs"] += self.unique_attrs
579         self.summary["df_value_attrs"] += self.df_value_attrs
580         other.summary["unique_attrs"] += other.unique_attrs
581         other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
582         #
583         self.screen_output = res[:-1]
584         other.screen_output = res[:-1]
585         #
586         return res == ""
587
588
589 class LDAPBundel(object):
590     def __init__(self, connection, context, dn_list=None):
591         self.con = connection
592         self.two_domains = self.con.two_domains
593         self.quiet = self.con.quiet
594         self.verbose = self.con.verbose
595         self.summary = {}
596         self.summary["unique_attrs"] = []
597         self.summary["df_value_attrs"] = []
598         self.summary["known_ignored_dn"] = []
599         self.summary["abnormal_ignored_dn"] = []
600         if dn_list:
601             self.dn_list = dn_list
602         elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
603             self.context = context.upper()
604             self.dn_list = self.get_dn_list(context)
605         else:
606             raise Exception("Unknown initialization data for LDAPBundel().")
607         counter = 0
608         while counter < len(self.dn_list) and self.two_domains:
609             # Use alias reference
610             tmp = self.dn_list[counter]
611             tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
612             tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
613             if len(self.con.server_names) == 1:
614                 for x in self.con.server_names:
615                     tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
616             self.dn_list[counter] = tmp
617             counter += 1
618         self.dn_list = list(set(self.dn_list))
619         self.dn_list = sorted(self.dn_list)
620         self.size = len(self.dn_list)
621
622     def log(self, msg):
623         """
624         Log on the screen if there is no --quiet oprion set
625         """
626         if not self.quiet:
627             print msg
628
629     def update_size(self):
630         self.size = len(self.dn_list)
631         self.dn_list = sorted(self.dn_list)
632
633     def __eq__(self, other):
634         res = True
635         if self.size != other.size:
636             self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
637             res = False
638         #
639         title= "\n* DNs found only in %s:" % self.con.host
640         for x in self.dn_list:
641             if not x.upper() in [q.upper() for q in other.dn_list]:
642                 if title:
643                     self.log( title )
644                     title = None
645                     res = False
646                 self.log( 4*" " + x )
647                 self.dn_list[self.dn_list.index(x)] = ""
648         self.dn_list = [x for x in self.dn_list if x]
649         #
650         title= "\n* DNs found only in %s:" % other.con.host
651         for x in other.dn_list:
652             if not x.upper() in [q.upper() for q in self.dn_list]:
653                 if title:
654                     self.log( title )
655                     title = None
656                     res = False
657                 self.log( 4*" " + x )
658                 other.dn_list[other.dn_list.index(x)] = ""
659         other.dn_list = [x for x in other.dn_list if x]
660         #
661         self.update_size()
662         other.update_size()
663         assert self.size == other.size
664         assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
665         self.log( "\n* Objects to be compared: %s" % self.size )
666
667         index = 0
668         while index < self.size:
669             skip = False
670             try:
671                 object1 = LDAPObject(connection=self.con,
672                         dn=self.dn_list[index],
673                         summary=self.summary)
674             except LdbError, (ERR_NO_SUCH_OBJECT, _):
675                 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
676                 skip = True
677             try:
678                 object2 = LDAPObject(connection=other.con,
679                         dn=other.dn_list[index],
680                         summary=other.summary)
681             except LdbError, (ERR_NO_SUCH_OBJECT, _):
682                 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
683                 skip = True
684             if skip:
685                 index += 1
686                 continue
687             if object1 == object2:
688                 if self.con.verbose:
689                     self.log( "\nComparing:" )
690                     self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
691                     self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
692                     self.log( 4*" " + "OK" )
693             else:
694                 self.log( "\nComparing:" )
695                 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
696                 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
697                 self.log( object1.screen_output )
698                 self.log( 4*" " + "FAILED" )
699                 res = False
700             self.summary = object1.summary
701             other.summary = object2.summary
702             index += 1
703         #
704         return res
705
706     def get_dn_list(self, context):
707         """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
708             Parse all DNs and filter those that are 'strange' or abnormal.
709         """
710         if context.upper() == "DOMAIN":
711             search_base = "%s" % self.con.base_dn
712         elif context.upper() == "CONFIGURATION":
713             search_base = "CN=Configuration,%s" % self.con.base_dn
714         elif context.upper() == "SCHEMA":
715             search_base = "CN=Schema,CN=Configuration,%s" % self.con.base_dn
716
717         dn_list = []
718         res = self.con.ldb.search(base=search_base, scope=SCOPE_SUBTREE, attrs=["dn"])
719         for x in res:
720            dn_list.append(x["dn"].get_linearized())
721
722         #
723         global summary
724         #
725         return dn_list
726
727     def print_summary(self):
728         self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
729         self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
730         #
731         if self.summary["unique_attrs"]:
732             self.log( "\nAttributes found only in %s:" % self.con.host )
733             self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
734         #
735         if self.summary["df_value_attrs"]:
736             self.log( "\nAttributes with different values:" )
737             self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
738             self.summary["df_value_attrs"] = []
739
740 ###
741
742 if __name__ == "__main__":
743     parser = OptionParser("ldapcmp [options] domain|configuration|schema")
744     sambaopts = options.SambaOptions(parser)
745     parser.add_option_group(sambaopts)
746     credopts = options.CredentialsOptionsDouble(parser)
747     parser.add_option_group(credopts)
748
749     parser.add_option("", "--host", dest="host",
750                               help="IP of the first LDAP server",)
751     parser.add_option("", "--host2", dest="host2",
752                               help="IP of the second LDAP server",)
753     parser.add_option("-w", "--two", dest="two", action="store_true", default=False,
754                               help="Hosts are in two different domains",)
755     parser.add_option("-q", "--quiet", dest="quiet", action="store_true", default=False,
756                               help="Do not print anything but relay on just exit code",)
757     parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
758                               help="Print all DN pairs that have been compared",)
759     parser.add_option("", "--sd", dest="descriptor", action="store_true", default=False,
760                               help="Compare nTSecurityDescriptor attibutes only",)
761     parser.add_option("", "--view", dest="view", default="section",
762             help="Display mode for nTSecurityDescriptor results. Possible values: section or collision.",)
763     (opts, args) = parser.parse_args()
764
765     lp = sambaopts.get_loadparm()
766     creds = credopts.get_credentials(lp)
767     creds2 = credopts.get_credentials2(lp)
768     if creds2.is_anonymous():
769         creds2 = creds
770
771     if creds.is_anonymous():
772         parser.error("You must supply at least one username/password pair")
773
774     # make a list of contexts to compare in
775     contexts = []
776     if len(args) == 0:
777         # if no argument given, we compare all contexts
778         contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
779     else:
780         for context in args:
781             if not context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
782                 parser.error("Incorrect argument: %s" % context)
783             contexts.append(context.upper())
784
785     if opts.verbose and opts.quiet:
786         parser.error("You cannot set --verbose and --quiet together")
787     if opts.descriptor and opts.view.upper() not in ["SECTION", "COLLISION"]:
788         parser.error("Unknown --view option value. Choose from: section or collision.")
789
790     con1 = LDAPBase(opts.host, opts, creds, lp)
791     assert len(con1.base_dn) > 0
792
793     con2 = LDAPBase(opts.host2, opts, creds2, lp)
794     assert len(con2.base_dn) > 0
795
796     status = 0
797     for context in contexts:
798         if not opts.quiet:
799             print "\n* Comparing [%s] context..." % context
800
801         b1 = LDAPBundel(con1, context=context)
802         b2 = LDAPBundel(con2, context=context)
803
804         if b1 == b2:
805             if not opts.quiet:
806                 print "\n* Result for [%s]: SUCCESS" % context
807         else:
808             if not opts.quiet:
809                 print "\n* Result for [%s]: FAILURE" % context
810                 if not opts.descriptor:
811                     assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
812                     b2.summary["df_value_attrs"] = []
813                     print "\nSUMMARY"
814                     print "---------"
815                     b1.print_summary()
816                     b2.print_summary()
817             # mark exit status as FAILURE if a least one comparison failed
818             status = -1
819
820     sys.exit(status)