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