source4/scripting/python/samba/samba3: handle ntdb files.
[metze/samba/wip.git] / python / samba / samba3 / __init__.py
1 # Unix SMB/CIFS implementation.
2 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 #
17
18 """Support for reading Samba 3 data files."""
19
20 __docformat__ = "restructuredText"
21
22 REGISTRY_VALUE_PREFIX = "SAMBA_REGVAL"
23 REGISTRY_DB_VERSION = 1
24
25 import os
26 import struct
27 import tdb
28 import ntdb
29
30 import passdb
31 import param as s3param
32
33
34 def fetch_uint32(db, key):
35     try:
36         data = db[key]
37     except KeyError:
38         return None
39     assert len(data) == 4
40     return struct.unpack("<L", data)[0]
41
42
43 def fetch_int32(db, key):
44     try:
45         data = db[key]
46     except KeyError:
47         return None
48     assert len(data) == 4
49     return struct.unpack("<l", data)[0]
50
51
52 class DbDatabase(object):
53     """Simple Samba 3 TDB/NTDB database reader."""
54     def __init__(self, file):
55         """Open a file.
56
57         :param file: Path of the file to open, appending .tdb or .ntdb.
58         """
59         if os.path.exists(file + ".ntdb"):
60             self.db = ntdb.Ntdb(file + ".ntdb", flags=os.O_RDONLY)
61         else:
62             self.db = tdb.Tdb(file + ".tdb", flags=os.O_RDONLY)
63         self._check_version()
64
65     def _check_version(self):
66         pass
67
68     def close(self):
69         """Close resources associated with this object."""
70         self.db.close()
71
72
73 class Registry(DbDatabase):
74     """Simple read-only support for reading the Samba3 registry.
75
76     :note: This object uses the same syntax for registry key paths as
77         Samba 3. This particular format uses forward slashes for key path
78         separators and abbreviations for the predefined key names.
79         e.g.: HKLM/Software/Bar.
80     """
81     def __len__(self):
82         """Return the number of keys."""
83         return len(self.keys())
84
85     def keys(self):
86         """Return list with all the keys."""
87         return [k.rstrip("\x00") for k in self.db.iterkeys() if not k.startswith(REGISTRY_VALUE_PREFIX)]
88
89     def subkeys(self, key):
90         """Retrieve the subkeys for the specified key.
91
92         :param key: Key path.
93         :return: list with key names
94         """
95         data = self.db.get("%s\x00" % key)
96         if data is None:
97             return []
98         (num, ) = struct.unpack("<L", data[0:4])
99         keys = data[4:].split("\0")
100         assert keys[-1] == ""
101         keys.pop()
102         assert len(keys) == num
103         return keys
104
105     def values(self, key):
106         """Return a dictionary with the values set for a specific key.
107
108         :param key: Key to retrieve values for.
109         :return: Dictionary with value names as key, tuple with type and
110             data as value."""
111         data = self.db.get("%s/%s\x00" % (REGISTRY_VALUE_PREFIX, key))
112         if data is None:
113             return {}
114         ret = {}
115         (num, ) = struct.unpack("<L", data[0:4])
116         data = data[4:]
117         for i in range(num):
118             # Value name
119             (name, data) = data.split("\0", 1)
120
121             (type, ) = struct.unpack("<L", data[0:4])
122             data = data[4:]
123             (value_len, ) = struct.unpack("<L", data[0:4])
124             data = data[4:]
125
126             ret[name] = (type, data[:value_len])
127             data = data[value_len:]
128
129         return ret
130
131
132 # High water mark keys
133 IDMAP_HWM_GROUP = "GROUP HWM\0"
134 IDMAP_HWM_USER = "USER HWM\0"
135
136 IDMAP_GROUP_PREFIX = "GID "
137 IDMAP_USER_PREFIX = "UID "
138
139 # idmap version determines auto-conversion
140 IDMAP_VERSION_V2 = 2
141
142 class IdmapDatabase(DbDatabase):
143     """Samba 3 ID map database reader."""
144
145     def _check_version(self):
146         assert fetch_int32(self.db, "IDMAP_VERSION\0") == IDMAP_VERSION_V2
147
148     def ids(self):
149         """Retrieve a list of all ids in this database."""
150         for k in self.db.iterkeys():
151             if k.startswith(IDMAP_USER_PREFIX):
152                 yield k.rstrip("\0").split(" ")
153             if k.startswith(IDMAP_GROUP_PREFIX):
154                 yield k.rstrip("\0").split(" ")
155
156     def uids(self):
157         """Retrieve a list of all uids in this database."""
158         for k in self.db.iterkeys():
159             if k.startswith(IDMAP_USER_PREFIX):
160                 yield int(k[len(IDMAP_USER_PREFIX):].rstrip("\0"))
161
162     def gids(self):
163         """Retrieve a list of all gids in this database."""
164         for k in self.db.iterkeys():
165             if k.startswith(IDMAP_GROUP_PREFIX):
166                 yield int(k[len(IDMAP_GROUP_PREFIX):].rstrip("\0"))
167
168     def get_sid(self, xid, id_type):
169         """Retrive SID associated with a particular id and type.
170
171         :param xid: UID or GID to retrive SID for.
172         :param id_type: Type of id specified - 'UID' or 'GID'
173         """
174         data = self.db.get("%s %s\0" % (id_type, str(xid)))
175         if data is None:
176             return data
177         return data.rstrip("\0")
178
179     def get_user_sid(self, uid):
180         """Retrieve the SID associated with a particular uid.
181
182         :param uid: UID to retrieve SID for.
183         :return: A SID or None if no mapping was found.
184         """
185         data = self.db.get("%s%d\0" % (IDMAP_USER_PREFIX, uid))
186         if data is None:
187             return data
188         return data.rstrip("\0")
189
190     def get_group_sid(self, gid):
191         data = self.db.get("%s%d\0" % (IDMAP_GROUP_PREFIX, gid))
192         if data is None:
193             return data
194         return data.rstrip("\0")
195
196     def get_user_hwm(self):
197         """Obtain the user high-water mark."""
198         return fetch_uint32(self.db, IDMAP_HWM_USER)
199
200     def get_group_hwm(self):
201         """Obtain the group high-water mark."""
202         return fetch_uint32(self.db, IDMAP_HWM_GROUP)
203
204
205 class SecretsDatabase(DbDatabase):
206     """Samba 3 Secrets database reader."""
207
208     def get_auth_password(self):
209         return self.db.get("SECRETS/AUTH_PASSWORD")
210
211     def get_auth_domain(self):
212         return self.db.get("SECRETS/AUTH_DOMAIN")
213
214     def get_auth_user(self):
215         return self.db.get("SECRETS/AUTH_USER")
216
217     def get_domain_guid(self, host):
218         return self.db.get("SECRETS/DOMGUID/%s" % host)
219
220     def ldap_dns(self):
221         for k in self.db.iterkeys():
222             if k.startswith("SECRETS/LDAP_BIND_PW/"):
223                 yield k[len("SECRETS/LDAP_BIND_PW/"):].rstrip("\0")
224
225     def domains(self):
226         """Iterate over domains in this database.
227
228         :return: Iterator over the names of domains in this database.
229         """
230         for k in self.db.iterkeys():
231             if k.startswith("SECRETS/SID/"):
232                 yield k[len("SECRETS/SID/"):].rstrip("\0")
233
234     def get_ldap_bind_pw(self, host):
235         return self.db.get("SECRETS/LDAP_BIND_PW/%s" % host)
236
237     def get_afs_keyfile(self, host):
238         return self.db.get("SECRETS/AFS_KEYFILE/%s" % host)
239
240     def get_machine_sec_channel_type(self, host):
241         return fetch_uint32(self.db, "SECRETS/MACHINE_SEC_CHANNEL_TYPE/%s" % host)
242
243     def get_machine_last_change_time(self, host):
244         return fetch_uint32(self.db, "SECRETS/MACHINE_LAST_CHANGE_TIME/%s" % host)
245
246     def get_machine_password(self, host):
247         return self.db.get("SECRETS/MACHINE_PASSWORD/%s" % host)
248
249     def get_machine_acc(self, host):
250         return self.db.get("SECRETS/$MACHINE.ACC/%s" % host)
251
252     def get_domtrust_acc(self, host):
253         return self.db.get("SECRETS/$DOMTRUST.ACC/%s" % host)
254
255     def trusted_domains(self):
256         for k in self.db.iterkeys():
257             if k.startswith("SECRETS/$DOMTRUST.ACC/"):
258                 yield k[len("SECRETS/$DOMTRUST.ACC/"):].rstrip("\0")
259
260     def get_random_seed(self):
261         return self.db.get("INFO/random_seed")
262
263     def get_sid(self, host):
264         return self.db.get("SECRETS/SID/%s" % host.upper())
265
266
267 SHARE_DATABASE_VERSION_V1 = 1
268 SHARE_DATABASE_VERSION_V2 = 2
269
270
271 class ShareInfoDatabase(DbDatabase):
272     """Samba 3 Share Info database reader."""
273
274     def _check_version(self):
275         assert fetch_int32(self.db, "INFO/version\0") in (SHARE_DATABASE_VERSION_V1, SHARE_DATABASE_VERSION_V2)
276
277     def get_secdesc(self, name):
278         """Obtain the security descriptor on a particular share.
279
280         :param name: Name of the share
281         """
282         secdesc = self.db.get("SECDESC/%s" % name)
283         # FIXME: Run ndr_pull_security_descriptor
284         return secdesc
285
286
287 class Shares(object):
288     """Container for share objects."""
289     def __init__(self, lp, shareinfo):
290         self.lp = lp
291         self.shareinfo = shareinfo
292
293     def __len__(self):
294         """Number of shares."""
295         return len(self.lp) - 1
296
297     def __iter__(self):
298         """Iterate over the share names."""
299         return self.lp.__iter__()
300
301
302 def shellsplit(text):
303     """Very simple shell-like line splitting.
304
305     :param text: Text to split.
306     :return: List with parts of the line as strings.
307     """
308     ret = list()
309     inquotes = False
310     current = ""
311     for c in text:
312         if c == "\"":
313             inquotes = not inquotes
314         elif c in ("\t", "\n", " ") and not inquotes:
315             if current != "":
316                 ret.append(current)
317             current = ""
318         else:
319             current += c
320     if current != "":
321         ret.append(current)
322     return ret
323
324
325 class WinsDatabase(object):
326     """Samba 3 WINS database reader."""
327     def __init__(self, file):
328         self.entries = {}
329         f = open(file, 'r')
330         assert f.readline().rstrip("\n") == "VERSION 1 0"
331         for l in f.readlines():
332             if l[0] == "#": # skip comments
333                 continue
334             entries = shellsplit(l.rstrip("\n"))
335             name = entries[0]
336             ttl = int(entries[1])
337             i = 2
338             ips = []
339             while "." in entries[i]:
340                 ips.append(entries[i])
341                 i+=1
342             nb_flags = int(entries[i][:-1], 16)
343             assert not name in self.entries, "Name %s exists twice" % name
344             self.entries[name] = (ttl, ips, nb_flags)
345         f.close()
346
347     def __getitem__(self, name):
348         return self.entries[name]
349
350     def __len__(self):
351         return len(self.entries)
352
353     def __iter__(self):
354         return iter(self.entries)
355
356     def items(self):
357         """Return the entries in this WINS database."""
358         return self.entries.items()
359
360     def close(self): # for consistency
361         pass
362
363
364 class Samba3(object):
365     """Samba 3 configuration and state data reader."""
366
367     def __init__(self, smbconfpath, s3_lp_ctx=None):
368         """Open the configuration and data for a Samba 3 installation.
369
370         :param smbconfpath: Path to the smb.conf file.
371         :param s3_lp_ctx: Samba3 Loadparm context
372         """
373         self.smbconfpath = smbconfpath
374         if s3_lp_ctx:
375             self.lp = s3_lp_ctx
376         else:
377             self.lp = s3param.get_context()
378             self.lp.load(smbconfpath)
379
380     def statedir_path(self, path):
381         if path[0] == "/" or path[0] == ".":
382             return path
383         return os.path.join(self.lp.get("state directory"), path)
384
385     def privatedir_path(self, path):
386         if path[0] == "/" or path[0] == ".":
387             return path
388         return os.path.join(self.lp.get("private dir"), path)
389
390     def get_conf(self):
391         return self.lp
392
393     def get_sam_db(self):
394         return passdb.PDB(self.lp.get('passdb backend'))
395
396     def get_registry(self):
397         return Registry(self.statedir_path("registry"))
398
399     def get_secrets_db(self):
400         return SecretsDatabase(self.privatedir_path("secrets"))
401
402     def get_shareinfo_db(self):
403         return ShareInfoDatabase(self.statedir_path("share_info"))
404
405     def get_idmap_db(self):
406         return IdmapDatabase(self.statedir_path("winbindd_idmap"))
407
408     def get_wins_db(self):
409         return WinsDatabase(self.statedir_path("wins.dat"))
410
411     def get_shares(self):
412         return Shares(self.get_conf(), self.get_shareinfo_db())