~ubuntu-branches/ubuntu/vivid/samba/vivid

« back to all changes in this revision

Viewing changes to source4/scripting/python/samba/netcmd/ldapcmp.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2011-12-21 13:18:04 UTC
  • mfrom: (0.39.21 sid)
  • Revision ID: package-import@ubuntu.com-20111221131804-xtlr39wx6njehxxr
Tags: 2:3.6.1-3ubuntu1
* Merge from Debian testing.  Remaining changes:
  + debian/patches/VERSION.patch:
    - set SAMBA_VERSION_SUFFIX to Ubuntu.
  + debian/patches/error-trans.fix-276472:
    - Add the translation of Unix Error code -ENOTSUP to NT Error Code
    - NT_STATUS_NOT_SUPPORTED to prevent the Permission denied error.
  + debian/smb.conf:
    - add "(Samba, Ubuntu)" to server string.
    - comment out the default [homes] share, and add a comment about
      "valid users = %S" to show users how to restrict access to
      \\server\username to only username.
    - Set 'usershare allow guests', so that usershare admins are 
      allowed to create public shares in addition to authenticated
      ones.
    - add map to guest = Bad user, maps bad username to guest access.
  + debian/samba-common.config:
    - Do not change priority to high if dhclient3 is installed.
    - Use priority medium instead of high for the workgroup question.
  + debian/control:
    - Don't build against or suggest ctdb.
    - Add dependency on samba-common-bin to samba.
  + Add ufw integration:
    - Created debian/samba.ufw.profile
    - debian/rules, debian/samba.dirs, debian/samba.files: install
      profile
    - debian/control: have samba suggest ufw
  + Add apport hook:
    - Created debian/source_samba.py.
    - debian/rules, debian/samba.dirs, debian/samba-common-bin.files: install
  + Switch to upstart:
    - Add debian/samba.{nmbd,smbd}.upstart.
  + debian/samba.logrotate, debian/samba-common.dhcp, debian/samba.if-up:
    - Make them upstart compatible
  + debian/samba.postinst: 
    - Avoid scary pdbedit warnings on first import.
  + debian/samba-common.postinst: Add more informative error message for
    the case where smb.conf was manually deleted
  + debian/patches/fix-debuglevel-name-conflict.patch: don't use 'debug_level'
    as a global variable name in an NSS module 
  + Dropped:
    - debian/patches/error-trans.fix-276472
    - debian/patches/fix-debuglevel-name-conflict.patch

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
        lp = sambaopts.get_loadparm()
 
878
        creds = credopts.get_credentials(lp, fallback_machine=True)
 
879
        creds2 = credopts.get_credentials2(lp, guess=False)
 
880
        if creds2.is_anonymous():
 
881
            creds2 = creds
 
882
        else:
 
883
            creds2.set_domain("")
 
884
            creds2.set_workstation("")
 
885
        if not creds.authentication_requested():
 
886
            raise CommandError("You must supply at least one username/password pair")
 
887
 
 
888
        # make a list of contexts to compare in
 
889
        contexts = []
 
890
        if context1 is None:
 
891
            if base and base2:
 
892
                # If search bases are specified context is defaulted to
 
893
                # DOMAIN so the given search bases can be verified.
 
894
                contexts = ["DOMAIN"]
 
895
            else:
 
896
                # if no argument given, we compare all contexts
 
897
                contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
 
898
        else:
 
899
            for c in [context1, context2, context3]:
 
900
                if c is None:
 
901
                    continue
 
902
                if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
 
903
                    raise CommandError("Incorrect argument: %s" % c)
 
904
                contexts.append(c.upper())
 
905
 
 
906
        if verbose and quiet:
 
907
            raise CommandError("You cannot set --verbose and --quiet together")
 
908
        if (not base and base2) or (base and not base2):
 
909
            raise CommandError("You need to specify both --base and --base2 at the same time")
 
910
        if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
 
911
            raise CommandError("Invalid --view value. Choose from: section or collision")
 
912
        if not scope.upper() in ["SUB", "ONE", "BASE"]:
 
913
            raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
 
914
 
 
915
        con1 = LDAPBase(URL1, creds, lp,
 
916
                        two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
 
917
                        verbose=verbose,view=view, base=base, scope=scope)
 
918
        assert len(con1.base_dn) > 0
 
919
 
 
920
        con2 = LDAPBase(URL2, creds2, lp,
 
921
                        two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
 
922
                        verbose=verbose, view=view, base=base2, scope=scope)
 
923
        assert len(con2.base_dn) > 0
 
924
 
 
925
        status = 0
 
926
        for context in contexts:
 
927
            if not quiet:
 
928
                print "\n* Comparing [%s] context..." % context
 
929
 
 
930
            b1 = LDAPBundel(con1, context=context)
 
931
            b2 = LDAPBundel(con2, context=context)
 
932
 
 
933
            if b1 == b2:
 
934
                if not quiet:
 
935
                    print "\n* Result for [%s]: SUCCESS" % context
 
936
            else:
 
937
                if not quiet:
 
938
                    print "\n* Result for [%s]: FAILURE" % context
 
939
                    if not descriptor:
 
940
                        assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
 
941
                        b2.summary["df_value_attrs"] = []
 
942
                        print "\nSUMMARY"
 
943
                        print "---------"
 
944
                        b1.print_summary()
 
945
                        b2.print_summary()
 
946
                # mark exit status as FAILURE if a least one comparison failed
 
947
                status = -1
 
948
        if status != 0:
 
949
            raise CommandError("Compare failed: %d" % status)