s4-samba-tool: Add samba-tool ntacl sysvolcheck command
[samba.git] / source4 / scripting / 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_pack, 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     SuperCommand,
39     )
40
41 global summary
42 summary = {}
43
44 class LDAPBase(object):
45
46     def __init__(self, host, creds, lp,
47                  two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False,
48                  view="section", base="", scope="SUB",
49                  outf=sys.stdout, errf=sys.stderr):
50         ldb_options = []
51         samdb_url = host
52         if not "://" in host:
53             if os.path.isfile(host):
54                 samdb_url = "tdb://%s" % host
55             else:
56                 samdb_url = "ldap://%s" % host
57         # use 'paged_search' module when connecting remotely
58         if samdb_url.lower().startswith("ldap://"):
59             ldb_options = ["modules:paged_searches"]
60         self.outf = outf
61         self.errf = errf
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.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.summary = {}
691         self.summary["unique_attrs"] = []
692         self.summary["df_value_attrs"] = []
693         self.summary["known_ignored_dn"] = []
694         self.summary["abnormal_ignored_dn"] = []
695         self.filter_list = filter_list
696         if dn_list:
697             self.dn_list = dn_list
698         elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
699             self.context = context.upper()
700             self.dn_list = self.get_dn_list(context)
701         else:
702             raise Exception("Unknown initialization data for LDAPBundel().")
703         counter = 0
704         while counter < len(self.dn_list) and self.two_domains:
705             # Use alias reference
706             tmp = self.dn_list[counter]
707             tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
708             tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
709             if len(self.con.server_names) == 1:
710                 for x in self.con.server_names:
711                     tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
712             self.dn_list[counter] = tmp
713             counter += 1
714         self.dn_list = list(set(self.dn_list))
715         self.dn_list = sorted(self.dn_list)
716         self.size = len(self.dn_list)
717
718     def log(self, msg):
719         """
720         Log on the screen if there is no --quiet oprion set
721         """
722         if not self.quiet:
723             self.outf.write(msg+"\n")
724
725     def update_size(self):
726         self.size = len(self.dn_list)
727         self.dn_list = sorted(self.dn_list)
728
729     def __eq__(self, other):
730         res = True
731         if self.size != other.size:
732             self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
733             res = False
734         #
735         # This is the case where we want to explicitly compare two objects with different DNs.
736         # It does not matter if they are in the same DC, in two DC in one domain or in two
737         # different domains.
738         if self.search_scope != SCOPE_BASE:
739             title= "\n* DNs found only in %s:" % self.con.host
740             for x in self.dn_list:
741                 if not x.upper() in [q.upper() for q in other.dn_list]:
742                     if title:
743                         self.log( title )
744                         title = None
745                         res = False
746                     self.log( 4*" " + x )
747                     self.dn_list[self.dn_list.index(x)] = ""
748             self.dn_list = [x for x in self.dn_list if x]
749             #
750             title= "\n* DNs found only in %s:" % other.con.host
751             for x in other.dn_list:
752                 if not x.upper() in [q.upper() for q in self.dn_list]:
753                     if title:
754                         self.log( title )
755                         title = None
756                         res = False
757                     self.log( 4*" " + x )
758                     other.dn_list[other.dn_list.index(x)] = ""
759             other.dn_list = [x for x in other.dn_list if x]
760             #
761             self.update_size()
762             other.update_size()
763             assert self.size == other.size
764             assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
765         self.log( "\n* Objects to be compared: %s" % self.size )
766
767         index = 0
768         while index < self.size:
769             skip = False
770             try:
771                 object1 = LDAPObject(connection=self.con,
772                                      dn=self.dn_list[index],
773                                      summary=self.summary,
774                                      filter_list=self.filter_list,
775                                      outf=self.outf, errf=self.errf)
776             except LdbError, (enum, estr):
777                 if enum == ERR_NO_SUCH_OBJECT:
778                     self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
779                     skip = True
780                 raise
781             try:
782                 object2 = LDAPObject(connection=other.con,
783                         dn=other.dn_list[index],
784                         summary=other.summary,
785                         filter_list=self.filter_list,
786                         outf=self.outf, errf=self.errf)
787             except LdbError, (enum, estr):
788                 if enum == ERR_NO_SUCH_OBJECT:
789                     self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
790                     skip = True
791                 raise
792             if skip:
793                 index += 1
794                 continue
795             if object1 == object2:
796                 if self.con.verbose:
797                     self.log( "\nComparing:" )
798                     self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
799                     self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
800                     self.log( 4*" " + "OK" )
801             else:
802                 self.log( "\nComparing:" )
803                 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
804                 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
805                 self.log( object1.screen_output )
806                 self.log( 4*" " + "FAILED" )
807                 res = False
808             self.summary = object1.summary
809             other.summary = object2.summary
810             index += 1
811         #
812         return res
813
814     def get_dn_list(self, context):
815         """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
816             Parse all DNs and filter those that are 'strange' or abnormal.
817         """
818         if context.upper() == "DOMAIN":
819             search_base = self.con.base_dn
820         elif context.upper() == "CONFIGURATION":
821             search_base = self.con.config_dn
822         elif context.upper() == "SCHEMA":
823             search_base = self.con.schema_dn
824         elif context.upper() == "DNSDOMAIN":
825             search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
826         elif context.upper() == "DNSFOREST":
827             search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
828
829         dn_list = []
830         if not self.search_base:
831             self.search_base = search_base
832         self.search_scope = self.search_scope.upper()
833         if self.search_scope == "SUB":
834             self.search_scope = SCOPE_SUBTREE
835         elif self.search_scope == "BASE":
836             self.search_scope = SCOPE_BASE
837         elif self.search_scope == "ONE":
838             self.search_scope = SCOPE_ONELEVEL
839         else:
840             raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
841         try:
842             res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
843         except LdbError, (enum, estr):
844             self.outf.write("Failed search of base=%s\n" % self.search_base)
845             raise
846         for x in res:
847            dn_list.append(x["dn"].get_linearized())
848         #
849         global summary
850         #
851         return dn_list
852
853     def print_summary(self):
854         self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
855         self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
856         #
857         if self.summary["unique_attrs"]:
858             self.log( "\nAttributes found only in %s:" % self.con.host )
859             self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
860         #
861         if self.summary["df_value_attrs"]:
862             self.log( "\nAttributes with different values:" )
863             self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
864             self.summary["df_value_attrs"] = []
865
866
867 class cmd_ldapcmp(Command):
868     """compare two ldap databases"""
869     synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
870
871     takes_optiongroups = {
872         "sambaopts": options.SambaOptions,
873         "versionopts": options.VersionOptions,
874         "credopts": options.CredentialsOptionsDouble,
875     }
876
877     takes_optiongroups = {
878         "sambaopts": options.SambaOptions,
879         "versionopts": options.VersionOptions,
880         "credopts": options.CredentialsOptionsDouble,
881     }
882
883     takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
884
885     takes_options = [
886         Option("-w", "--two", dest="two", action="store_true", default=False,
887             help="Hosts are in two different domains"),
888         Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
889             help="Do not print anything but relay on just exit code"),
890         Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
891             help="Print all DN pairs that have been compared"),
892         Option("--sd", dest="descriptor", action="store_true", default=False,
893             help="Compare nTSecurityDescriptor attibutes only"),
894         Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
895             help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
896         Option("--view", dest="view", default="section",
897             help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
898         Option("--base", dest="base", default="",
899             help="Pass search base that will build DN list for the first DC."),
900         Option("--base2", dest="base2", default="",
901             help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
902         Option("--scope", dest="scope", default="SUB",
903             help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
904         Option("--filter", dest="filter", default="",
905             help="List of comma separated attributes to ignore in the comparision"),
906         ]
907
908     def run(self, URL1, URL2,
909             context1=None, context2=None, context3=None,
910             two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
911             view="section", base="", base2="", scope="SUB", filter="",
912             credopts=None, sambaopts=None, versionopts=None):
913
914         lp = sambaopts.get_loadparm()
915
916         using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
917
918         if using_ldap:
919             creds = credopts.get_credentials(lp, fallback_machine=True)
920         else:
921             creds = None
922         creds2 = credopts.get_credentials2(lp, guess=False)
923         if creds2.is_anonymous():
924             creds2 = creds
925         else:
926             creds2.set_domain("")
927             creds2.set_workstation("")
928         if using_ldap and not creds.authentication_requested():
929             raise CommandError("You must supply at least one username/password pair")
930
931         # make a list of contexts to compare in
932         contexts = []
933         if context1 is None:
934             if base and base2:
935                 # If search bases are specified context is defaulted to
936                 # DOMAIN so the given search bases can be verified.
937                 contexts = ["DOMAIN"]
938             else:
939                 # if no argument given, we compare all contexts
940                 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
941         else:
942             for c in [context1, context2, context3]:
943                 if c is None:
944                     continue
945                 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
946                     raise CommandError("Incorrect argument: %s" % c)
947                 contexts.append(c.upper())
948
949         if verbose and quiet:
950             raise CommandError("You cannot set --verbose and --quiet together")
951         if (not base and base2) or (base and not base2):
952             raise CommandError("You need to specify both --base and --base2 at the same time")
953         if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
954             raise CommandError("Invalid --view value. Choose from: section or collision")
955         if not scope.upper() in ["SUB", "ONE", "BASE"]:
956             raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
957
958         con1 = LDAPBase(URL1, creds, lp,
959                         two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
960                         verbose=verbose,view=view, base=base, scope=scope,
961                         outf=self.outf, errf=self.errf)
962         assert len(con1.base_dn) > 0
963
964         con2 = LDAPBase(URL2, creds2, lp,
965                         two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
966                         verbose=verbose, view=view, base=base2, scope=scope,
967                         outf=self.outf, errf=self.errf)
968         assert len(con2.base_dn) > 0
969
970         filter_list = filter.split(",")
971
972         status = 0
973         for context in contexts:
974             if not quiet:
975                 self.outf.write("\n* Comparing [%s] context...\n" % context)
976
977             b1 = LDAPBundel(con1, context=context, filter_list=filter_list,
978                             outf=self.outf, errf=self.errf)
979             b2 = LDAPBundel(con2, context=context, filter_list=filter_list,
980                             outf=self.outf, errf=self.errf)
981
982             if b1 == b2:
983                 if not quiet:
984                     self.outf.write("\n* Result for [%s]: SUCCESS\n" %
985                         context)
986             else:
987                 if not quiet:
988                     self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
989                     if not descriptor:
990                         assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
991                         b2.summary["df_value_attrs"] = []
992                         self.outf.write("\nSUMMARY\n")
993                         self.outf.write("---------\n")
994                         b1.print_summary()
995                         b2.print_summary()
996                 # mark exit status as FAILURE if a least one comparison failed
997                 status = -1
998         if status != 0:
999             raise CommandError("Compare failed: %d" % status)