PEP8: fix E222: multiple spaces after operator
[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 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 as e2:
126             (enum, estr) = e2.args
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 as 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_sid_map(self):
254         """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
255         """
256         self.sid_map = {}
257         res = self.ldb.search(base=self.base_dn,
258                               expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
259         for item in res:
260             try:
261                 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
262             except KeyError:
263                 pass
264
265 class Descriptor(object):
266     def __init__(self, connection, dn, outf=sys.stdout, errf=sys.stderr):
267         self.outf = outf
268         self.errf = errf
269         self.con = connection
270         self.dn = dn
271         self.sddl = self.con.get_descriptor_sddl(self.dn)
272         self.dacl_list = self.extract_dacl()
273         if self.con.sort_aces:
274             self.dacl_list.sort()
275
276     def extract_dacl(self):
277         """ Extracts the DACL as a list of ACE string (with the brakets).
278         """
279         try:
280             if "S:" in self.sddl:
281                 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
282             else:
283                 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
284         except AttributeError:
285             return []
286         return re.findall("(\(.*?\))", res)
287
288     def fix_sid(self, ace):
289         res = "%s" % ace
290         sids = re.findall("S-[-0-9]+", res)
291         # If there are not SIDs to replace return the same ACE
292         if len(sids) == 0:
293             return res
294         for sid in sids:
295             try:
296                 name = self.con.sid_map[sid]
297                 res = res.replace(sid, name)
298             except KeyError:
299                 # Do not bother if the SID is not found in baseDN
300                 pass
301         return res
302
303     def diff_1(self, other):
304         res = ""
305         if len(self.dacl_list) != len(other.dacl_list):
306             res += 4*" " + "Difference in ACE count:\n"
307             res += 8*" " + "=> %s\n" % len(self.dacl_list)
308             res += 8*" " + "=> %s\n" % len(other.dacl_list)
309         #
310         i = 0
311         flag = True
312         while True:
313             self_ace = None
314             other_ace = None
315             try:
316                 self_ace = "%s" % self.dacl_list[i]
317             except IndexError:
318                 self_ace = ""
319             #
320             try:
321                 other_ace = "%s" % other.dacl_list[i]
322             except IndexError:
323                 other_ace = ""
324             if len(self_ace) + len(other_ace) == 0:
325                 break
326             self_ace_fixed = "%s" % self.fix_sid(self_ace)
327             other_ace_fixed = "%s" % other.fix_sid(other_ace)
328             if self_ace_fixed != other_ace_fixed:
329                 res += "%60s * %s\n" % (self_ace_fixed, other_ace_fixed)
330                 flag = False
331             else:
332                 res += "%60s | %s\n" % (self_ace_fixed, other_ace_fixed)
333             i += 1
334         return (flag, res)
335
336     def diff_2(self, other):
337         res = ""
338         if len(self.dacl_list) != len(other.dacl_list):
339             res += 4*" " + "Difference in ACE count:\n"
340             res += 8*" " + "=> %s\n" % len(self.dacl_list)
341             res += 8*" " + "=> %s\n" % len(other.dacl_list)
342         #
343         common_aces = []
344         self_aces = []
345         other_aces = []
346         self_dacl_list_fixed = []
347         other_dacl_list_fixed = []
348         [self_dacl_list_fixed.append(self.fix_sid(ace)) for ace in self.dacl_list]
349         [other_dacl_list_fixed.append(other.fix_sid(ace)) for ace in other.dacl_list]
350         for ace in self_dacl_list_fixed:
351             try:
352                 other_dacl_list_fixed.index(ace)
353             except ValueError:
354                 self_aces.append(ace)
355             else:
356                 common_aces.append(ace)
357         self_aces = sorted(self_aces)
358         if len(self_aces) > 0:
359             res += 4*" " + "ACEs found only in %s:\n" % self.con.host
360             for ace in self_aces:
361                 res += 8*" " + ace + "\n"
362         #
363         for ace in other_dacl_list_fixed:
364             try:
365                 self_dacl_list_fixed.index(ace)
366             except ValueError:
367                 other_aces.append(ace)
368             else:
369                 common_aces.append(ace)
370         other_aces = sorted(other_aces)
371         if len(other_aces) > 0:
372             res += 4*" " + "ACEs found only in %s:\n" % other.con.host
373             for ace in other_aces:
374                 res += 8*" " + ace + "\n"
375         #
376         common_aces = sorted(list(set(common_aces)))
377         if self.con.verbose:
378             res += 4*" " + "ACEs found in both:\n"
379             for ace in common_aces:
380                 res += 8*" " + ace + "\n"
381         return (self_aces == [] and other_aces == [], res)
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 not x 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 not x 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 LDAPBundel(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 LDAPBundel().")
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 __eq__(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         # This is the case where we want to explicitly compare two objects with different DNs.
748         # It does not matter if they are in the same DC, in two DC in one domain or in two
749         # different domains.
750         if self.search_scope != SCOPE_BASE:
751             title= "\n* DNs found only in %s:" % self.con.host
752             for x in self.dn_list:
753                 if not x.upper() in [q.upper() for q in other.dn_list]:
754                     if title and not self.skip_missing_dn:
755                         self.log(title)
756                         title = None
757                         res = False
758                     self.log(4*" " + x)
759                     self.dn_list[self.dn_list.index(x)] = ""
760             self.dn_list = [x for x in self.dn_list if x]
761             #
762             title= "\n* DNs found only in %s:" % other.con.host
763             for x in other.dn_list:
764                 if not x.upper() in [q.upper() for q in self.dn_list]:
765                     if title and not self.skip_missing_dn:
766                         self.log(title)
767                         title = None
768                         res = False
769                     self.log(4*" " + x)
770                     other.dn_list[other.dn_list.index(x)] = ""
771             other.dn_list = [x for x in other.dn_list if x]
772             #
773             self.update_size()
774             other.update_size()
775             assert self.size == other.size
776             assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
777         self.log("\n* Objects to be compared: %s" % self.size)
778
779         index = 0
780         while index < self.size:
781             skip = False
782             try:
783                 object1 = LDAPObject(connection=self.con,
784                                      dn=self.dn_list[index],
785                                      summary=self.summary,
786                                      filter_list=self.filter_list,
787                                      outf=self.outf, errf=self.errf)
788             except LdbError as e:
789                 (enum, estr) = e.args
790                 if enum == ERR_NO_SUCH_OBJECT:
791                     self.log("\n!!! Object not found: %s" % self.dn_list[index])
792                     skip = True
793                 raise
794             try:
795                 object2 = LDAPObject(connection=other.con,
796                                      dn=other.dn_list[index],
797                                      summary=other.summary,
798                                      filter_list=self.filter_list,
799                                      outf=self.outf, errf=self.errf)
800             except LdbError as e1:
801                 (enum, estr) = e1.args
802                 if enum == ERR_NO_SUCH_OBJECT:
803                     self.log("\n!!! Object not found: %s" % other.dn_list[index])
804                     skip = True
805                 raise
806             if skip:
807                 index += 1
808                 continue
809             if object1 == object2:
810                 if self.con.verbose:
811                     self.log("\nComparing:")
812                     self.log("'%s' [%s]" % (object1.dn, object1.con.host))
813                     self.log("'%s' [%s]" % (object2.dn, object2.con.host))
814                     self.log(4*" " + "OK")
815             else:
816                 self.log("\nComparing:")
817                 self.log("'%s' [%s]" % (object1.dn, object1.con.host))
818                 self.log("'%s' [%s]" % (object2.dn, object2.con.host))
819                 self.log(object1.screen_output)
820                 self.log(4*" " + "FAILED")
821                 res = False
822             self.summary = object1.summary
823             other.summary = object2.summary
824             index += 1
825         #
826         return res
827
828     def get_dn_list(self, context):
829         """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
830             Parse all DNs and filter those that are 'strange' or abnormal.
831         """
832         if context.upper() == "DOMAIN":
833             search_base = self.con.base_dn
834         elif context.upper() == "CONFIGURATION":
835             search_base = self.con.config_dn
836         elif context.upper() == "SCHEMA":
837             search_base = self.con.schema_dn
838         elif context.upper() == "DNSDOMAIN":
839             search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
840         elif context.upper() == "DNSFOREST":
841             search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
842
843         dn_list = []
844         if not self.search_base:
845             self.search_base = search_base
846         self.search_scope = self.search_scope.upper()
847         if self.search_scope == "SUB":
848             self.search_scope = SCOPE_SUBTREE
849         elif self.search_scope == "BASE":
850             self.search_scope = SCOPE_BASE
851         elif self.search_scope == "ONE":
852             self.search_scope = SCOPE_ONELEVEL
853         else:
854             raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
855         try:
856             res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
857         except LdbError as e3:
858             (enum, estr) = e3.args
859             self.outf.write("Failed search of base=%s\n" % self.search_base)
860             raise
861         for x in res:
862             dn_list.append(x["dn"].get_linearized())
863         #
864         global summary
865         #
866         return dn_list
867
868     def print_summary(self):
869         self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
870         self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
871         #
872         if self.summary["unique_attrs"]:
873             self.log("\nAttributes found only in %s:" % self.con.host)
874             self.log("".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]))
875         #
876         if self.summary["df_value_attrs"]:
877             self.log("\nAttributes with different values:")
878             self.log("".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]))
879             self.summary["df_value_attrs"] = []
880
881
882 class cmd_ldapcmp(Command):
883     """Compare two ldap databases."""
884     synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
885
886     takes_optiongroups = {
887         "sambaopts": options.SambaOptions,
888         "versionopts": options.VersionOptions,
889         "credopts": options.CredentialsOptionsDouble,
890     }
891
892     takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?", "context4?", "context5?"]
893
894     takes_options = [
895         Option("-w", "--two", dest="two", action="store_true", default=False,
896                help="Hosts are in two different domains"),
897         Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
898                help="Do not print anything but relay on just exit code"),
899         Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
900                help="Print all DN pairs that have been compared"),
901         Option("--sd", dest="descriptor", action="store_true", default=False,
902                help="Compare nTSecurityDescriptor attibutes only"),
903         Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
904                help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
905         Option("--view", dest="view", default="section",
906                help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
907         Option("--base", dest="base", default="",
908                help="Pass search base that will build DN list for the first DC."),
909         Option("--base2", dest="base2", default="",
910                help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
911         Option("--scope", dest="scope", default="SUB",
912                help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
913         Option("--filter", dest="filter", default="",
914                help="List of comma separated attributes to ignore in the comparision"),
915         Option("--skip-missing-dn", dest="skip_missing_dn", action="store_true", default=False,
916                help="Skip report and failure due to missing DNs in one server or another"),
917     ]
918
919     def run(self, URL1, URL2,
920             context1=None, context2=None, context3=None, context4=None, context5=None,
921             two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
922             view="section", base="", base2="", scope="SUB", filter="",
923             credopts=None, sambaopts=None, versionopts=None, skip_missing_dn=False):
924
925         lp = sambaopts.get_loadparm()
926
927         using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
928
929         if using_ldap:
930             creds = credopts.get_credentials(lp, fallback_machine=True)
931         else:
932             creds = None
933         creds2 = credopts.get_credentials2(lp, guess=False)
934         if creds2.is_anonymous():
935             creds2 = creds
936         else:
937             creds2.set_domain("")
938             creds2.set_workstation("")
939         if using_ldap and not creds.authentication_requested():
940             raise CommandError("You must supply at least one username/password pair")
941
942         # make a list of contexts to compare in
943         contexts = []
944         if context1 is None:
945             if base and base2:
946                 # If search bases are specified context is defaulted to
947                 # DOMAIN so the given search bases can be verified.
948                 contexts = ["DOMAIN"]
949             else:
950                 # if no argument given, we compare all contexts
951                 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]
952         else:
953             for c in [context1, context2, context3, context4, context5]:
954                 if c is None:
955                     continue
956                 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
957                     raise CommandError("Incorrect argument: %s" % c)
958                 contexts.append(c.upper())
959
960         if verbose and quiet:
961             raise CommandError("You cannot set --verbose and --quiet together")
962         if (not base and base2) or (base and not base2):
963             raise CommandError("You need to specify both --base and --base2 at the same time")
964         if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
965             raise CommandError("Invalid --view value. Choose from: section or collision")
966         if not scope.upper() in ["SUB", "ONE", "BASE"]:
967             raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
968
969         con1 = LDAPBase(URL1, creds, lp,
970                         two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
971                         verbose=verbose,view=view, base=base, scope=scope,
972                         outf=self.outf, errf=self.errf)
973         assert len(con1.base_dn) > 0
974
975         con2 = LDAPBase(URL2, creds2, lp,
976                         two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
977                         verbose=verbose, view=view, base=base2, scope=scope,
978                         outf=self.outf, errf=self.errf)
979         assert len(con2.base_dn) > 0
980
981         filter_list = filter.split(",")
982
983         status = 0
984         for context in contexts:
985             if not quiet:
986                 self.outf.write("\n* Comparing [%s] context...\n" % context)
987
988             b1 = LDAPBundel(con1, context=context, filter_list=filter_list,
989                             outf=self.outf, errf=self.errf)
990             b2 = LDAPBundel(con2, context=context, filter_list=filter_list,
991                             outf=self.outf, errf=self.errf)
992
993             if b1 == b2:
994                 if not quiet:
995                     self.outf.write("\n* Result for [%s]: SUCCESS\n" %
996                                     context)
997             else:
998                 if not quiet:
999                     self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
1000                     if not descriptor:
1001                         assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
1002                         b2.summary["df_value_attrs"] = []
1003                         self.outf.write("\nSUMMARY\n")
1004                         self.outf.write("---------\n")
1005                         b1.print_summary()
1006                         b2.print_summary()
1007                 # mark exit status as FAILURE if a least one comparison failed
1008                 status = -1
1009         if status != 0:
1010             raise CommandError("Compare failed: %d" % status)