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