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