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