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