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