1# LDIF helper functions for the samba_kcc tool
2#
3# Copyright (C) Dave Craft 2011
4# Copyright (C) Andrew Bartlett 2015
5#
6# Andrew Bartlett's alleged work performed by his underlings Douglas
7# Bagnall and Garming Sam.
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
22import os
23
24from samba import Ldb, ldb, read_and_sub_file
25from samba.auth import system_session
26from samba.samdb import SamDB
27from samba.common import dsdb_Dn
28
29
30class LdifError(Exception):
31    pass
32
33
34def write_search_result(samdb, f, res):
35    for msg in res:
36        lstr = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
37        f.write("%s" % lstr)
38
39
40def ldif_to_samdb(dburl, lp, ldif_file, forced_local_dsa=None):
41    """Routine to import all objects and attributes that are relevent
42    to the KCC algorithms from a previously exported LDIF file.
43
44    The point of this function is to allow a programmer/debugger to
45    import an LDIF file with non-security relevent information that
46    was previously extracted from a DC database.  The LDIF file is used
47    to create a temporary abbreviated database.  The KCC algorithm can
48    then run against this abbreviated database for debug or test
49    verification that the topology generated is computationally the
50    same between different OSes and algorithms.
51
52    :param dburl: path to the temporary abbreviated db to create
53    :param ldif_file: path to the ldif file to import
54    """
55    if os.path.exists(dburl):
56        raise LdifError("Specify a database (%s) that doesn't already exist." %
57                        dburl)
58
59    # Use ["modules:"] as we are attempting to build a sam
60    # database as opposed to start it here.
61    tmpdb = Ldb(url=dburl, session_info=system_session(),
62                lp=lp, options=["modules:"])
63
64    tmpdb.transaction_start()
65    try:
66        data = read_and_sub_file(ldif_file, None)
67        tmpdb.add_ldif(data, None)
68        if forced_local_dsa:
69            tmpdb.modify_ldif("""dn: @ROOTDSE
70changetype: modify
71replace: dsServiceName
72dsServiceName: CN=NTDS Settings,%s
73            """ % forced_local_dsa)
74
75        tmpdb.add_ldif("""dn: @MODULES
76@LIST: rootdse,extended_dn_in,extended_dn_out_ldb,objectguid
77-
78""")
79
80    except Exception as estr:
81        tmpdb.transaction_cancel()
82        raise LdifError("Failed to import %s: %s" % (ldif_file, estr))
83
84    tmpdb.transaction_commit()
85
86    # We have an abbreviated list of options here because we have built
87    # an abbreviated database.  We use the rootdse and extended-dn
88    # modules only during this re-open
89    samdb = SamDB(url=dburl, session_info=system_session(), lp=lp)
90    return samdb
91
92
93def samdb_to_ldif_file(samdb, dburl, lp, creds, ldif_file):
94    """Routine to extract all objects and attributes that are relevent
95    to the KCC algorithms from a DC database.
96
97    The point of this function is to allow a programmer/debugger to
98    extract an LDIF file with non-security relevent information from
99    a DC database.  The LDIF file can then be used to "import" via
100    the import_ldif() function this file into a temporary abbreviated
101    database.  The KCC algorithm can then run against this abbreviated
102    database for debug or test verification that the topology generated
103    is computationally the same between different OSes and algorithms.
104
105    :param dburl: LDAP database URL to extract info from
106    :param ldif_file: output LDIF file name to create
107    """
108    try:
109        samdb = SamDB(url=dburl,
110                      session_info=system_session(),
111                      credentials=creds, lp=lp)
112    except ldb.LdbError as e:
113        (enum, estr) = e.args
114        raise LdifError("Unable to open sam database (%s) : %s" %
115                        (dburl, estr))
116
117    if os.path.exists(ldif_file):
118        raise LdifError("Specify a file (%s) that doesn't already exist." %
119                        ldif_file)
120
121    try:
122        f = open(ldif_file, "w")
123    except IOError as ioerr:
124        raise LdifError("Unable to open (%s) : %s" % (ldif_file, str(ioerr)))
125
126    try:
127        # Query Partitions
128        attrs = ["objectClass",
129                 "objectGUID",
130                 "cn",
131                 "whenChanged",
132                 "objectSid",
133                 "Enabled",
134                 "systemFlags",
135                 "dnsRoot",
136                 "nCName",
137                 "msDS-NC-Replica-Locations",
138                 "msDS-NC-RO-Replica-Locations"]
139
140        sstr = "CN=Partitions,%s" % samdb.get_config_basedn()
141        res = samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
142                           attrs=attrs,
143                           expression="(objectClass=crossRef)")
144
145        # Write partitions output
146        write_search_result(samdb, f, res)
147
148        # Query cross reference container
149        attrs = ["objectClass",
150                 "objectGUID",
151                 "cn",
152                 "whenChanged",
153                 "fSMORoleOwner",
154                 "systemFlags",
155                 "msDS-Behavior-Version",
156                 "msDS-EnabledFeature"]
157
158        sstr = "CN=Partitions,%s" % samdb.get_config_basedn()
159        res = samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
160                           attrs=attrs,
161                           expression="(objectClass=crossRefContainer)")
162
163        # Write cross reference container output
164        write_search_result(samdb, f, res)
165
166        # Query Sites
167        attrs = ["objectClass",
168                 "objectGUID",
169                 "cn",
170                 "whenChanged",
171                 "systemFlags"]
172
173        sstr = "CN=Sites,%s" % samdb.get_config_basedn()
174        sites = samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
175                             attrs=attrs,
176                             expression="(objectClass=site)")
177
178        # Write sites output
179        write_search_result(samdb, f, sites)
180
181        # Query NTDS Site Settings
182        for msg in sites:
183            sitestr = str(msg.dn)
184
185            attrs = ["objectClass",
186                     "objectGUID",
187                     "cn",
188                     "whenChanged",
189                     "interSiteTopologyGenerator",
190                     "interSiteTopologyFailover",
191                     "schedule",
192                     "options"]
193
194            sstr = "CN=NTDS Site Settings,%s" % sitestr
195            res = samdb.search(base=sstr, scope=ldb.SCOPE_BASE,
196                               attrs=attrs)
197
198            # Write Site Settings output
199            write_search_result(samdb, f, res)
200
201        # Naming context list
202        nclist = []
203
204        # Query Directory Service Agents
205        for msg in sites:
206            sstr = str(msg.dn)
207
208            ncattrs = ["hasMasterNCs",
209                       "msDS-hasMasterNCs",
210                       "hasPartialReplicaNCs",
211                       "msDS-HasDomainNCs",
212                       "msDS-hasFullReplicaNCs",
213                       "msDS-HasInstantiatedNCs"]
214            attrs = ["objectClass",
215                     "objectGUID",
216                     "cn",
217                     "whenChanged",
218                     "invocationID",
219                     "options",
220                     "msDS-isRODC",
221                     "msDS-Behavior-Version"]
222
223            res = samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
224                               attrs=attrs + ncattrs,
225                               expression="(objectClass=nTDSDSA)")
226
227            # Spin thru all the DSAs looking for NC replicas
228            # and build a list of all possible Naming Contexts
229            # for subsequent retrieval below
230            for msg in res:
231                for k in msg.keys():
232                    if k in ncattrs:
233                        for value in msg[k]:
234                            # Some of these have binary DNs so
235                            # use dsdb_Dn to split out relevent parts
236                            dsdn = dsdb_Dn(samdb, value.decode('utf8'))
237                            dnstr = str(dsdn.dn)
238                            if dnstr not in nclist:
239                                nclist.append(dnstr)
240
241            # Write DSA output
242            write_search_result(samdb, f, res)
243
244        # Query NTDS Connections
245        for msg in sites:
246            sstr = str(msg.dn)
247
248            attrs = ["objectClass",
249                     "objectGUID",
250                     "cn",
251                     "whenChanged",
252                     "options",
253                     "whenCreated",
254                     "enabledConnection",
255                     "schedule",
256                     "transportType",
257                     "fromServer",
258                     "systemFlags"]
259
260            res = samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
261                               attrs=attrs,
262                               expression="(objectClass=nTDSConnection)")
263            # Write NTDS Connection output
264            write_search_result(samdb, f, res)
265
266        # Query Intersite transports
267        attrs = ["objectClass",
268                 "objectGUID",
269                 "cn",
270                 "whenChanged",
271                 "options",
272                 "name",
273                 "bridgeheadServerListBL",
274                 "transportAddressAttribute"]
275
276        sstr = "CN=Inter-Site Transports,CN=Sites,%s" % \
277               samdb.get_config_basedn()
278        res = samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
279                           attrs=attrs,
280                           expression="(objectClass=interSiteTransport)")
281
282        # Write inter-site transport output
283        write_search_result(samdb, f, res)
284
285        # Query siteLink
286        attrs = ["objectClass",
287                 "objectGUID",
288                 "cn",
289                 "whenChanged",
290                 "systemFlags",
291                 "options",
292                 "schedule",
293                 "replInterval",
294                 "siteList",
295                 "cost"]
296
297        sstr = "CN=Sites,%s" % \
298               samdb.get_config_basedn()
299        res = samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
300                           attrs=attrs,
301                           expression="(objectClass=siteLink)",
302                           controls=['extended_dn:0'])
303
304        # Write siteLink output
305        write_search_result(samdb, f, res)
306
307        # Query siteLinkBridge
308        attrs = ["objectClass",
309                 "objectGUID",
310                 "cn",
311                 "whenChanged",
312                 "siteLinkList"]
313
314        sstr = "CN=Sites,%s" % samdb.get_config_basedn()
315        res = samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
316                           attrs=attrs,
317                           expression="(objectClass=siteLinkBridge)")
318
319        # Write siteLinkBridge output
320        write_search_result(samdb, f, res)
321
322        # Query servers containers
323        # Needed for samdb.server_site_name()
324        attrs = ["objectClass",
325                 "objectGUID",
326                 "cn",
327                 "whenChanged",
328                 "systemFlags"]
329
330        sstr = "CN=Sites,%s" % samdb.get_config_basedn()
331        res = samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
332                           attrs=attrs,
333                           expression="(objectClass=serversContainer)")
334
335        # Write servers container output
336        write_search_result(samdb, f, res)
337
338        # Query servers
339        # Needed because some transport interfaces refer back to
340        # attributes found in the server object.   Also needed
341        # so extended-dn will be happy with dsServiceName in rootDSE
342        attrs = ["objectClass",
343                 "objectGUID",
344                 "cn",
345                 "whenChanged",
346                 "systemFlags",
347                 "dNSHostName",
348                 "mailAddress"]
349
350        sstr = "CN=Sites,%s" % samdb.get_config_basedn()
351        res = samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
352                           attrs=attrs,
353                           expression="(objectClass=server)")
354
355        # Write server output
356        write_search_result(samdb, f, res)
357
358        # Query Naming Context replicas
359        attrs = ["objectClass",
360                 "objectGUID",
361                 "cn",
362                 "whenChanged",
363                 "objectSid",
364                 "fSMORoleOwner",
365                 "msDS-Behavior-Version",
366                 "repsFrom",
367                 "repsTo"]
368
369        for sstr in nclist:
370            res = samdb.search(sstr, scope=ldb.SCOPE_BASE,
371                               attrs=attrs)
372
373            # Write naming context output
374            write_search_result(samdb, f, res)
375
376        # Query rootDSE replicas
377        attrs = ["objectClass",
378                 "objectGUID",
379                 "cn",
380                 "whenChanged",
381                 "rootDomainNamingContext",
382                 "configurationNamingContext",
383                 "schemaNamingContext",
384                 "defaultNamingContext",
385                 "dsServiceName"]
386
387        sstr = ""
388        res = samdb.search(sstr, scope=ldb.SCOPE_BASE,
389                           attrs=attrs)
390
391        # Record the rootDSE object as a dn as it
392        # would appear in the base ldb file.  We have
393        # to save it this way because we are going to
394        # be importing as an abbreviated database.
395        res[0].dn = ldb.Dn(samdb, "@ROOTDSE")
396
397        # Write rootdse output
398        write_search_result(samdb, f, res)
399
400    except ldb.LdbError as e1:
401        (enum, estr) = e1.args
402        raise LdifError("Error processing (%s) : %s" % (sstr, estr))
403
404    f.close()
405