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