r26607: Fix reading of values and subkeys in Samba 3 registry files.
[ira/wip.git] / source / scripting / python / samba / samba3.py
1 #!/usr/bin/python
2
3 # Unix SMB/CIFS implementation.
4 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007
5 #   
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #   
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #   
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19
20 """Support for reading Samba 3 data files."""
21
22 REGISTRY_VALUE_PREFIX = "SAMBA_REGVAL"
23 REGISTRY_DB_VERSION = 1
24
25 import os
26 import tdb
27
28 class Registry:
29     """Simple read-only support for reading the Samba3 registry."""
30     def __init__(self, file):
31         self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
32
33     def close(self):
34         self.tdb.close()
35
36     def __len__(self):
37         """Return the number of keys."""
38         return len(self.keys())
39
40     def keys(self):
41         """Return list with all the keys."""
42         return [k.rstrip("\x00") for k in self.tdb.keys() if not k.startswith(REGISTRY_VALUE_PREFIX)]
43
44     def subkeys(self, key):
45         data = self.tdb.get("%s\x00" % key)
46         if data is None:
47             return []
48         import struct
49         (num, ) = struct.unpack("<L", data[0:4])
50         keys = data[4:].split("\0")
51         assert keys[-1] == ""
52         keys.pop()
53         assert len(keys) == num
54         return keys
55
56     def values(self, key):
57         """Return a dictionary with the values set for a specific key."""
58         data = self.tdb.get("%s/%s\x00" % (REGISTRY_VALUE_PREFIX, key))
59         if data is None:
60             return {}
61         ret = {}
62         import struct
63         (num, ) = struct.unpack("<L", data[0:4])
64         data = data[4:]
65         for i in range(num):
66             # Value name
67             (name, data) = data.split("\0", 1)
68
69             (type, ) = struct.unpack("<L", data[0:4])
70             data = data[4:]
71             (value_len, ) = struct.unpack("<L", data[0:4])
72             data = data[4:]
73
74             ret[name] = (type, data[:value_len])
75             data = data[value_len:]
76
77         return ret
78
79
80 class PolicyDatabase:
81     def __init__(self, file):
82         self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
83         self.min_password_length = self.tdb.fetch_uint32("min password length\x00")
84         self.password_history = self.tdb.fetch_uint32("password history\x00")
85         self.user_must_logon_to_change_password = self.tdb.fetch_uint32("user must logon to change pasword\x00")
86         self.maximum_password_age = self.tdb.fetch_uint32("maximum password age\x00")
87         self.minimum_password_age = self.tdb.fetch_uint32("minimum password age\x00")
88         self.lockout_duration = self.tdb.fetch_uint32("lockout duration\x00")
89         self.reset_count_minutes = self.tdb.fetch_uint32("reset count minutes\x00")
90         self.bad_lockout_minutes = self.tdb.fetch_uint32("bad lockout minutes\x00")
91         self.disconnect_time = self.tdb.fetch_int32("disconnect time\x00")
92         self.refuse_machine_password_change = self.tdb.fetch_uint32("refuse machine password change\x00")
93
94         # FIXME: Read privileges as well
95
96     def close(self):
97         self.tdb.close()
98
99
100 GROUPDB_DATABASE_VERSION_V1 = 1 # native byte format.
101 GROUPDB_DATABASE_VERSION_V2 = 2 # le format.
102
103 GROUP_PREFIX = "UNIXGROUP/"
104
105 # Alias memberships are stored reverse, as memberships. The performance
106 # critical operation is to determine the aliases a SID is member of, not
107 # listing alias members. So we store a list of alias SIDs a SID is member of
108 # hanging of the member as key.
109 MEMBEROF_PREFIX = "MEMBEROF/"
110
111 class GroupMappingDatabase:
112     def __init__(self, file): 
113         self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
114         assert self.tdb.fetch_int32("INFO/version\x00") in (GROUPDB_DATABASE_VERSION_V1, GROUPDB_DATABASE_VERSION_V2)
115
116     def groupsids(self):
117         for k in self.tdb.keys():
118             if k.startswith(GROUP_PREFIX):
119                 yield k[len(GROUP_PREFIX):].rstrip("\0")
120
121     def aliases(self):
122         for k in self.tdb.keys():
123             if k.startswith(MEMBEROF_PREFIX):
124                 yield k[len(MEMBEROF_PREFIX):].rstrip("\0")
125
126     def close(self):
127         self.tdb.close()
128
129
130 # High water mark keys
131 IDMAP_HWM_GROUP = "GROUP HWM\0"
132 IDMAP_HWM_USER = "USER HWM\0"
133
134 IDMAP_GROUP_PREFIX = "GID "
135 IDMAP_USER_PREFIX = "UID "
136
137 # idmap version determines auto-conversion
138 IDMAP_VERSION_V2 = 2
139
140 class IdmapDatabase:
141     def __init__(self, file):
142         self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
143         assert self.tdb.fetch_int32("IDMAP_VERSION\0") == IDMAP_VERSION_V2
144
145     def uids(self):
146         for k in self.tdb.keys():
147             if k.startswith(IDMAP_USER_PREFIX):
148                 yield int(k[len(IDMAP_USER_PREFIX):].rstrip("\0"))
149
150     def gids(self):
151         for k in self.tdb.keys():
152             if k.startswith(IDMAP_GROUP_PREFIX):
153                 yield int(k[len(IDMAP_GROUP_PREFIX):].rstrip("\0"))
154
155     def get_user_sid(self, uid):
156         data = self.tdb.get("%s%d\0" % (IDMAP_USER_PREFIX, uid))
157         if data is None:
158             return data
159         return data.rstrip("\0")
160
161     def get_group_sid(self, gid):
162         data = self.tdb.get("%s%d\0" % (IDMAP_GROUP_PREFIX, gid))
163         if data is None:
164             return data
165         return data.rstrip("\0")
166
167     def get_user_hwm(self):
168         return self.tdb.fetch_uint32(IDMAP_HWM_USER)
169
170     def get_group_hwm(self):
171         return self.tdb.fetch_uint32(IDMAP_HWM_GROUP)
172
173     def close(self):
174         self.tdb.close()
175
176
177 class SecretsDatabase:
178     def __init__(self, file):
179         self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
180
181     def get_auth_password(self):
182         return self.tdb.get("SECRETS/AUTH_PASSWORD")
183
184     def get_auth_domain(self):
185         return self.tdb.get("SECRETS/AUTH_DOMAIN")
186
187     def get_auth_user(self):
188         return self.tdb.get("SECRETS/AUTH_USER")
189
190     def get_domain_guid(self, host):
191         return self.tdb.get("SECRETS/DOMGUID/%s" % host)
192
193     def ldap_dns(self):
194         for k in self.tdb.keys():
195             if k.startswith("SECRETS/LDAP_BIND_PW/"):
196                 yield k[len("SECRETS/LDAP_BIND_PW/"):].rstrip("\0")
197
198     def domains(self):
199         for k in self.tdb.keys():
200             if k.startswith("SECRETS/SID/"):
201                 yield k[len("SECRETS/SID/"):].rstrip("\0")
202
203     def get_ldap_bind_pw(self, host):
204         return self.tdb.get("SECRETS/LDAP_BIND_PW/%s" % host)
205     
206     def get_afs_keyfile(self, host):
207         return self.tdb.get("SECRETS/AFS_KEYFILE/%s" % host)
208
209     def get_machine_sec_channel_type(self, host):
210         return self.tdb.fetch_uint32("SECRETS/MACHINE_SEC_CHANNEL_TYPE/%s" % host)
211
212     def get_machine_last_change_time(self, host):
213         return self.tdb.fetch_uint32("SECRETS/MACHINE_LAST_CHANGE_TIME/%s" % host)
214             
215     def get_machine_password(self, host):
216         return self.tdb.get("SECRETS/MACHINE_PASSWORD/%s" % host)
217
218     def get_machine_acc(self, host):
219         return self.tdb.get("SECRETS/$MACHINE.ACC/%s" % host)
220
221     def get_domtrust_acc(self, host):
222         return self.tdb.get("SECRETS/$DOMTRUST.ACC/%s" % host)
223
224     def trusted_domains(self):
225         for k in self.tdb.keys():
226             if k.startswith("SECRETS/$DOMTRUST.ACC/"):
227                 yield k[len("SECRETS/$DOMTRUST.ACC/"):].rstrip("\0")
228
229     def get_random_seed(self):
230         return self.tdb.get("INFO/random_seed")
231
232     def get_sid(self, host):
233         return self.tdb.get("SECRETS/SID/%s" % host.upper())
234
235     def close(self):
236         self.tdb.close()
237
238
239 SHARE_DATABASE_VERSION_V1 = 1
240 SHARE_DATABASE_VERSION_V2 = 2
241
242 class ShareInfoDatabase:
243     def __init__(self, file):
244         self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
245         assert self.tdb.fetch_int32("INFO/version\0") in (SHARE_DATABASE_VERSION_V1, SHARE_DATABASE_VERSION_V2)
246
247     def get_secdesc(self, name):
248         secdesc = self.tdb.get("SECDESC/%s" % name)
249         # FIXME: Run ndr_pull_security_descriptor
250         return secdesc
251
252     def close(self):
253         self.tdb.close()
254
255
256 class Shares:
257     def __init__(self, lp, shareinfo):
258         self.lp = lp
259         self.shareinfo = shareinfo
260
261     def __len__(self):
262         return len(self.lp) - 1
263
264     def __iter__(self):
265         return self.lp.__iter__()
266
267
268 ACB_DISABLED = 0x00000001
269 ACB_HOMDIRREQ = 0x00000002
270 ACB_PWNOTREQ = 0x00000004
271 ACB_TEMPDUP = 0x00000008
272 ACB_NORMAL = 0x00000010
273 ACB_MNS = 0x00000020
274 ACB_DOMTRUST = 0x00000040
275 ACB_WSTRUST = 0x00000080
276 ACB_SVRTRUST = 0x00000100
277 ACB_PWNOEXP = 0x00000200
278 ACB_AUTOLOCK = 0x00000400
279 ACB_ENC_TXT_PWD_ALLOWED = 0x00000800
280 ACB_SMARTCARD_REQUIRED = 0x00001000
281 ACB_TRUSTED_FOR_DELEGATION = 0x00002000
282 ACB_NOT_DELEGATED = 0x00004000
283 ACB_USE_DES_KEY_ONLY = 0x00008000
284 ACB_DONT_REQUIRE_PREAUTH = 0x00010000
285 ACB_PW_EXPIRED = 0x00020000
286 ACB_NO_AUTH_DATA_REQD = 0x00080000
287
288 acb_info_mapping = {
289         'N': ACB_PWNOTREQ,  # 'N'o password. 
290         'D': ACB_DISABLED,  # 'D'isabled.
291                 'H': ACB_HOMDIRREQ, # 'H'omedir required.
292                 'T': ACB_TEMPDUP,   # 'T'emp account.
293                 'U': ACB_NORMAL,    # 'U'ser account (normal).
294                 'M': ACB_MNS,       # 'M'NS logon user account. What is this ?
295                 'W': ACB_WSTRUST,   # 'W'orkstation account.
296                 'S': ACB_SVRTRUST,  # 'S'erver account. 
297                 'L': ACB_AUTOLOCK,  # 'L'ocked account.
298                 'X': ACB_PWNOEXP,   # No 'X'piry on password
299                 'I': ACB_DOMTRUST,  # 'I'nterdomain trust account.
300         ' ': 0
301         }
302
303 def decode_acb(text):
304     assert not "[" in text and not "]" in text
305     ret = 0
306     for x in text:
307         ret |= acb_info_mapping[x]
308     return ret
309
310
311 class SmbpasswdFile:
312     def __init__(self, file):
313         self.users = {}
314         f = open(file, 'r')
315         for l in f.readlines():
316             if len(l) == 0 or l[0] == "#":
317                 continue # Skip comments and blank lines
318             parts = l.split(":")
319             username = parts[0]
320             uid = int(parts[1])
321             acct_ctrl = 0
322             last_change_time = None
323             if parts[2] == "NO PASSWORD":
324                 acct_ctrl |= ACB_PWNOTREQ
325                 lm_password = None
326             elif parts[2][0] in ("*", "X"):
327                 # No password set
328                 lm_password = None
329             else:
330                 lm_password = parts[2]
331
332             if parts[3][0] in ("*", "X"):
333                 # No password set
334                 nt_password = None
335             else:
336                 nt_password = parts[3]
337
338             if parts[4][0] == '[':
339                 assert "]" in parts[4]
340                 acct_ctrl |= decode_acb(parts[4][1:-1])
341                 if parts[5].startswith("LCT-"):
342                     last_change_time = int(parts[5][len("LCT-"):], 16)
343             else: # old style file
344                 if username[-1] == "$":
345                     acct_ctrl &= ~ACB_NORMAL
346                     acct_ctrl |= ACB_WSTRUST
347
348             self.users[username] = (uid, lm_password, nt_password, acct_ctrl, last_change_time)
349
350         f.close()
351
352     def __len__(self):
353         return len(self.users)
354
355     def __getitem__(self, name):
356         return self.users[name]
357
358     def __iter__(self):
359         return iter(self.users)
360
361     def close(self): # For consistency
362         pass
363
364
365 TDBSAM_FORMAT_STRING_V0 = "ddddddBBBBBBBBBBBBddBBwdwdBwwd"
366 TDBSAM_FORMAT_STRING_V1 = "dddddddBBBBBBBBBBBBddBBwdwdBwwd"
367 TDBSAM_FORMAT_STRING_V2 = "dddddddBBBBBBBBBBBBddBBBwwdBwwd"
368 TDBSAM_USER_PREFIX = "USER_"
369
370
371 class TdbSam:
372     def __init__(self, file):
373         self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
374         self.version = self.tdb.fetch_uint32("INFO/version") or 0
375         assert self.version in (0, 1, 2)
376
377     def usernames(self):
378         for k in self.tdb.keys():
379             if k.startswith(TDBSAM_USER_PREFIX):
380                 yield k[len(TDBSAM_USER_PREFIX):].rstrip("\0")
381
382     __iter__ = usernames
383
384     def close(self):
385         self.tdb.close()
386
387
388 def shellsplit(text):
389     """Very simple shell-like line splitting.
390     
391     :param text: Text to split.
392     :return: List with parts of the line as strings.
393     """
394     ret = list()
395     inquotes = False
396     current = ""
397     for c in text:
398         if c == "\"":
399             inquotes = not inquotes
400         elif c in ("\t", "\n", " ") and not inquotes:
401             ret.append(current)
402             current = ""
403         else:
404             current += c
405     if current != "":
406         ret.append(current)
407     return ret
408
409
410 class WinsDatabase:
411     def __init__(self, file):
412         self.entries = {}
413         f = open(file, 'r')
414         assert f.readline().rstrip("\n") == "VERSION 1 0"
415         for l in f.readlines():
416             if l[0] == "#": # skip comments
417                 continue
418             entries = shellsplit(l.rstrip("\n"))
419             name = entries[0]
420             ttl = int(entries[1])
421             i = 2
422             ips = []
423             while "." in entries[i]:
424                 ips.append(entries[i])
425                 i+=1
426             nb_flags = entries[i]
427             assert not name in self.entries, "Name %s exists twice" % name
428             self.entries[name] = (ttl, ips, nb_flags)
429         f.close()
430
431     def __getitem__(self, name):
432         return self.entries[name]
433
434     def __len__(self):
435         return len(self.entries)
436
437     def __iter__(self):
438         return iter(self.entries)
439
440     def close(self): # for consistency
441         pass
442
443 class Samba3:
444     def __init__(self, libdir, smbconfpath):
445         self.smbconfpath = smbconfpath
446         self.libdir = libdir
447         import param
448         self.lp = param.ParamFile()
449         self.lp.read(self.smbconfpath)
450
451     def libdir_path(self, path):
452         if path[0] == "/" or path[0] == ".":
453             return path
454         return os.path.join(self.libdir, path)
455
456     def get_conf(self):
457         return self.lp
458
459     def get_sam_db(self):
460         lp = self.get_conf()
461         backends = str(lp.get("passdb backend")).split(" ")
462         if backends[0].startswith("smbpasswd"):
463             if ":" in backends[0]:
464                 return SmbpasswdFile(self.libdir_path(backends[0][len("smbpasswd:"):]))
465             return SmbpasswdFile(self.libdir_path("smbpasswd"))
466         elif backends[0].startswith("tdbsam"):
467             if ":" in backends[0]:
468                 return TdbSam(self.libdir_path(backends[0][len("tdbsam:"):]))
469             return TdbSam(self.libdir_path("passdb.tdb"))
470         else:
471             raise NotImplementedError("unsupported passdb backend %s" % backends[0])
472
473     def get_policy_db(self):
474         return PolicyDatabase(self.libdir_path("account_policy.tdb"))
475     
476     def get_registry(self):
477         return Registry(self.libdir_path("registry.tdb"))
478
479     def get_secrets_db(self):
480         return SecretsDatabase(self.libdir_path("secrets.tdb"))
481
482     def get_shareinfo_db(self):
483         return ShareInfoDatabase(self.libdir_path("share_info.tdb"))
484
485     def get_idmap_db(self):
486         return IdmapDatabase(self.libdir_path("winbindd_idmap.tdb"))
487
488     def get_wins_db(self):
489         return WinsDatabase(self.libdir_path("wins.dat"))
490
491     def get_shares(self):
492         return Shares(self.get_conf(), self.get_shareinfo_db())
493
494     def get_groupmapping_db(self):
495         return GroupMappingDatabase(self.libdir_path("group_mapping.tdb"))