r26616: Support parsing of user data in SAmba 3 tdbsam.
[ab/samba.git/.git] / source4 / 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 get_group(self, sid):
122         data = self.tdb.get("%s%s\0" % (GROUP_PREFIX, sid))
123         if data is None:
124             return data
125         import struct
126         (gid, sid_name_use) = struct.unpack("<lL", data[0:8])
127         (nt_name, comment, _) = data[8:].split("\0")
128         return (gid, sid_name_use, nt_name, comment)
129
130     def aliases(self):
131         for k in self.tdb.keys():
132             if k.startswith(MEMBEROF_PREFIX):
133                 yield k[len(MEMBEROF_PREFIX):].rstrip("\0")
134
135     def close(self):
136         self.tdb.close()
137
138
139 # High water mark keys
140 IDMAP_HWM_GROUP = "GROUP HWM\0"
141 IDMAP_HWM_USER = "USER HWM\0"
142
143 IDMAP_GROUP_PREFIX = "GID "
144 IDMAP_USER_PREFIX = "UID "
145
146 # idmap version determines auto-conversion
147 IDMAP_VERSION_V2 = 2
148
149 class IdmapDatabase:
150     def __init__(self, file):
151         self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
152         assert self.tdb.fetch_int32("IDMAP_VERSION\0") == IDMAP_VERSION_V2
153
154     def uids(self):
155         for k in self.tdb.keys():
156             if k.startswith(IDMAP_USER_PREFIX):
157                 yield int(k[len(IDMAP_USER_PREFIX):].rstrip("\0"))
158
159     def gids(self):
160         for k in self.tdb.keys():
161             if k.startswith(IDMAP_GROUP_PREFIX):
162                 yield int(k[len(IDMAP_GROUP_PREFIX):].rstrip("\0"))
163
164     def get_user_sid(self, uid):
165         data = self.tdb.get("%s%d\0" % (IDMAP_USER_PREFIX, uid))
166         if data is None:
167             return data
168         return data.rstrip("\0")
169
170     def get_group_sid(self, gid):
171         data = self.tdb.get("%s%d\0" % (IDMAP_GROUP_PREFIX, gid))
172         if data is None:
173             return data
174         return data.rstrip("\0")
175
176     def get_user_hwm(self):
177         return self.tdb.fetch_uint32(IDMAP_HWM_USER)
178
179     def get_group_hwm(self):
180         return self.tdb.fetch_uint32(IDMAP_HWM_GROUP)
181
182     def close(self):
183         self.tdb.close()
184
185
186 class SecretsDatabase:
187     def __init__(self, file):
188         self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
189
190     def get_auth_password(self):
191         return self.tdb.get("SECRETS/AUTH_PASSWORD")
192
193     def get_auth_domain(self):
194         return self.tdb.get("SECRETS/AUTH_DOMAIN")
195
196     def get_auth_user(self):
197         return self.tdb.get("SECRETS/AUTH_USER")
198
199     def get_domain_guid(self, host):
200         return self.tdb.get("SECRETS/DOMGUID/%s" % host)
201
202     def ldap_dns(self):
203         for k in self.tdb.keys():
204             if k.startswith("SECRETS/LDAP_BIND_PW/"):
205                 yield k[len("SECRETS/LDAP_BIND_PW/"):].rstrip("\0")
206
207     def domains(self):
208         for k in self.tdb.keys():
209             if k.startswith("SECRETS/SID/"):
210                 yield k[len("SECRETS/SID/"):].rstrip("\0")
211
212     def get_ldap_bind_pw(self, host):
213         return self.tdb.get("SECRETS/LDAP_BIND_PW/%s" % host)
214     
215     def get_afs_keyfile(self, host):
216         return self.tdb.get("SECRETS/AFS_KEYFILE/%s" % host)
217
218     def get_machine_sec_channel_type(self, host):
219         return self.tdb.fetch_uint32("SECRETS/MACHINE_SEC_CHANNEL_TYPE/%s" % host)
220
221     def get_machine_last_change_time(self, host):
222         return self.tdb.fetch_uint32("SECRETS/MACHINE_LAST_CHANGE_TIME/%s" % host)
223             
224     def get_machine_password(self, host):
225         return self.tdb.get("SECRETS/MACHINE_PASSWORD/%s" % host)
226
227     def get_machine_acc(self, host):
228         return self.tdb.get("SECRETS/$MACHINE.ACC/%s" % host)
229
230     def get_domtrust_acc(self, host):
231         return self.tdb.get("SECRETS/$DOMTRUST.ACC/%s" % host)
232
233     def trusted_domains(self):
234         for k in self.tdb.keys():
235             if k.startswith("SECRETS/$DOMTRUST.ACC/"):
236                 yield k[len("SECRETS/$DOMTRUST.ACC/"):].rstrip("\0")
237
238     def get_random_seed(self):
239         return self.tdb.get("INFO/random_seed")
240
241     def get_sid(self, host):
242         return self.tdb.get("SECRETS/SID/%s" % host.upper())
243
244     def close(self):
245         self.tdb.close()
246
247
248 SHARE_DATABASE_VERSION_V1 = 1
249 SHARE_DATABASE_VERSION_V2 = 2
250
251 class ShareInfoDatabase:
252     def __init__(self, file):
253         self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
254         assert self.tdb.fetch_int32("INFO/version\0") in (SHARE_DATABASE_VERSION_V1, SHARE_DATABASE_VERSION_V2)
255
256     def get_secdesc(self, name):
257         secdesc = self.tdb.get("SECDESC/%s" % name)
258         # FIXME: Run ndr_pull_security_descriptor
259         return secdesc
260
261     def close(self):
262         self.tdb.close()
263
264
265 class Shares:
266     def __init__(self, lp, shareinfo):
267         self.lp = lp
268         self.shareinfo = shareinfo
269
270     def __len__(self):
271         return len(self.lp) - 1
272
273     def __iter__(self):
274         return self.lp.__iter__()
275
276
277 ACB_DISABLED = 0x00000001
278 ACB_HOMDIRREQ = 0x00000002
279 ACB_PWNOTREQ = 0x00000004
280 ACB_TEMPDUP = 0x00000008
281 ACB_NORMAL = 0x00000010
282 ACB_MNS = 0x00000020
283 ACB_DOMTRUST = 0x00000040
284 ACB_WSTRUST = 0x00000080
285 ACB_SVRTRUST = 0x00000100
286 ACB_PWNOEXP = 0x00000200
287 ACB_AUTOLOCK = 0x00000400
288 ACB_ENC_TXT_PWD_ALLOWED = 0x00000800
289 ACB_SMARTCARD_REQUIRED = 0x00001000
290 ACB_TRUSTED_FOR_DELEGATION = 0x00002000
291 ACB_NOT_DELEGATED = 0x00004000
292 ACB_USE_DES_KEY_ONLY = 0x00008000
293 ACB_DONT_REQUIRE_PREAUTH = 0x00010000
294 ACB_PW_EXPIRED = 0x00020000
295 ACB_NO_AUTH_DATA_REQD = 0x00080000
296
297 acb_info_mapping = {
298         'N': ACB_PWNOTREQ,  # 'N'o password. 
299         'D': ACB_DISABLED,  # 'D'isabled.
300         'H': ACB_HOMDIRREQ, # 'H'omedir required.
301         'T': ACB_TEMPDUP,   # 'T'emp account.
302         'U': ACB_NORMAL,    # 'U'ser account (normal).
303         'M': ACB_MNS,       # 'M'NS logon user account. What is this ?
304         'W': ACB_WSTRUST,   # 'W'orkstation account.
305         'S': ACB_SVRTRUST,  # 'S'erver account. 
306         'L': ACB_AUTOLOCK,  # 'L'ocked account.
307         'X': ACB_PWNOEXP,   # No 'X'piry on password
308         'I': ACB_DOMTRUST,  # 'I'nterdomain trust account.
309         ' ': 0
310         }
311
312 def decode_acb(text):
313     assert not "[" in text and not "]" in text
314     ret = 0
315     for x in text:
316         ret |= acb_info_mapping[x]
317     return ret
318
319
320 class SAMUser:
321     def __init__(self, name, uid=None, lm_password=None, nt_password=None, acct_ctrl=None, 
322                  last_change_time=None, nt_username=None, fullname=None, logon_time=None, logoff_time=None,
323                  acct_desc=None, group_rid=None, bad_password_count=None, logon_count=None,
324                  domain=None, dir_drive=None, munged_dial=None, homedir=None, logon_script=None,
325                  profile_path=None, workstations=None, kickoff_time=None, bad_password_time=None,
326                  pass_last_set_time=None, pass_can_change_time=None, pass_must_change_time=None,
327                  user_rid=None, unknown_6=None, nt_password_history=None,
328                  unknown_str=None, hours=None, logon_divs=None):
329         self.username = name
330         self.uid = uid
331         self.lm_password = lm_password
332         self.nt_password = nt_password
333         self.acct_ctrl = acct_ctrl
334         self.pass_last_set_time = last_change_time
335         self.nt_username = nt_username
336         self.fullname = fullname
337         self.logon_time = logon_time
338         self.logoff_time = logoff_time
339         self.acct_desc = acct_desc
340         self.group_rid = group_rid
341         self.bad_password_count = bad_password_count
342         self.logon_count = logon_count
343         self.domain = domain
344         self.dir_drive = dir_drive
345         self.munged_dial = munged_dial
346         self.homedir = homedir
347         self.logon_script = logon_script
348         self.profile_path = profile_path
349         self.workstations = workstations
350         self.kickoff_time = kickoff_time
351         self.bad_password_time = bad_password_time
352         self.pass_can_change_time = pass_can_change_time
353         self.pass_must_change_time = pass_must_change_time
354         self.user_rid = user_rid
355         self.unknown_6 = unknown_6
356         self.nt_password_history = nt_password_history
357         self.unknown_str = unknown_str
358         self.hours = hours
359         self.logon_divs = logon_divs
360
361     def __eq__(self, other): 
362         if not isinstance(other, SAMUser):
363             return False
364         return self.__dict__ == other.__dict__
365
366 class SmbpasswdFile:
367     def __init__(self, file):
368         self.users = {}
369         f = open(file, 'r')
370         for l in f.readlines():
371             if len(l) == 0 or l[0] == "#":
372                 continue # Skip comments and blank lines
373             parts = l.split(":")
374             username = parts[0]
375             uid = int(parts[1])
376             acct_ctrl = 0
377             last_change_time = None
378             if parts[2] == "NO PASSWORD":
379                 acct_ctrl |= ACB_PWNOTREQ
380                 lm_password = None
381             elif parts[2][0] in ("*", "X"):
382                 # No password set
383                 lm_password = None
384             else:
385                 lm_password = parts[2]
386
387             if parts[3][0] in ("*", "X"):
388                 # No password set
389                 nt_password = None
390             else:
391                 nt_password = parts[3]
392
393             if parts[4][0] == '[':
394                 assert "]" in parts[4]
395                 acct_ctrl |= decode_acb(parts[4][1:-1])
396                 if parts[5].startswith("LCT-"):
397                     last_change_time = int(parts[5][len("LCT-"):], 16)
398             else: # old style file
399                 if username[-1] == "$":
400                     acct_ctrl &= ~ACB_NORMAL
401                     acct_ctrl |= ACB_WSTRUST
402
403             self.users[username] = SAMUser(username, uid, lm_password, nt_password, acct_ctrl, last_change_time)
404
405         f.close()
406
407     def __len__(self):
408         return len(self.users)
409
410     def __getitem__(self, name):
411         return self.users[name]
412
413     def __iter__(self):
414         return iter(self.users)
415
416     def close(self): # For consistency
417         pass
418
419
420 TDBSAM_FORMAT_STRING_V0 = "ddddddBBBBBBBBBBBBddBBwdwdBwwd"
421 TDBSAM_FORMAT_STRING_V1 = "dddddddBBBBBBBBBBBBddBBwdwdBwwd"
422 TDBSAM_FORMAT_STRING_V2 = "dddddddBBBBBBBBBBBBddBBBwwdBwwd"
423 TDBSAM_USER_PREFIX = "USER_"
424
425
426 class LdapSam:
427     def __init__(self, url):
428         self.ldap_url = ldap_url
429
430
431 class TdbSam:
432     def __init__(self, file):
433         self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
434         self.version = self.tdb.fetch_uint32("INFO/version\0") or 0
435         assert self.version in (0, 1, 2)
436
437     def usernames(self):
438         for k in self.tdb.keys():
439             if k.startswith(TDBSAM_USER_PREFIX):
440                 yield k[len(TDBSAM_USER_PREFIX):].rstrip("\0")
441
442     __iter__ = usernames
443     
444     def __getitem__(self, name):
445         data = self.tdb["%s%s\0" % (TDBSAM_USER_PREFIX, name)]
446         user = SAMUser(name)
447         import struct
448     
449         def unpack_string(data):
450             (length, ) = struct.unpack("<L", data[:4])
451             data = data[4:]
452             if length == 0:
453                 return (None, data)
454             return (data[:length].rstrip("\0"), data[length:])
455
456         def unpack_int32(data):
457             (value, ) = struct.unpack("<l", data[:4])
458             return (value, data[4:])
459
460         def unpack_uint32(data):
461             (value, ) = struct.unpack("<L", data[:4])
462             return (value, data[4:])
463
464         def unpack_uint16(data):
465             (value, ) = struct.unpack("<H", data[:2])
466             return (value, data[2:])
467
468         (logon_time, data) = unpack_int32(data)
469         (logoff_time, data) = unpack_int32(data)
470         (kickoff_time, data) = unpack_int32(data)
471
472         if self.version > 0:
473             (bad_password_time, data) = unpack_int32(data)
474             if bad_password_time != 0:
475                 user.bad_password_time = bad_password_time
476         (pass_last_set_time, data) = unpack_int32(data)
477         (pass_can_change_time, data) = unpack_int32(data)
478         (pass_must_change_time, data) = unpack_int32(data)
479
480         if logon_time != 0:
481             user.logon_time = logon_time
482         user.logoff_time = logoff_time
483         user.kickoff_time = kickoff_time
484         if pass_last_set_time != 0:
485             user.pass_last_set_time = pass_last_set_time
486         user.pass_can_change_time = pass_can_change_time
487
488         (user.username, data) = unpack_string(data)
489         (user.domain, data) = unpack_string(data)
490         (user.nt_username, data) = unpack_string(data)
491         (user.fullname, data) = unpack_string(data)
492         (user.homedir, data) = unpack_string(data)
493         (user.dir_drive, data) = unpack_string(data)
494         (user.logon_script, data) = unpack_string(data)
495         (user.profile_path, data) = unpack_string(data)
496         (user.acct_desc, data) = unpack_string(data)
497         (user.workstations, data) = unpack_string(data)
498         (user.unknown_str, data) = unpack_string(data)
499         (user.munged_dial, data) = unpack_string(data)
500
501         (user.user_rid, data) = unpack_int32(data)
502         (user.group_rid, data) = unpack_int32(data)
503
504         (user.lm_password, data) = unpack_string(data)
505         (user.nt_password, data) = unpack_string(data)
506
507         if self.version > 1:
508             (user.nt_password_history, data) = unpack_string(data)
509
510         (user.acct_ctrl, data) = unpack_uint16(data)
511         (_, data) = unpack_uint32(data) # remove_me field
512         (user.logon_divs, data) = unpack_uint16(data)
513         (hours, data) = unpack_string(data)
514         user.hours = []
515         for entry in hours:
516             for i in range(8):
517                 user.hours.append(ord(entry) & (2 ** i) == (2 ** i))
518         (user.bad_password_count, data) = unpack_uint16(data)
519         (user.logon_count, data) = unpack_uint16(data)
520         (user.unknown_6, data) = unpack_uint32(data)
521         assert len(data) == 0
522         return user
523
524     def close(self):
525         self.tdb.close()
526
527
528 def shellsplit(text):
529     """Very simple shell-like line splitting.
530     
531     :param text: Text to split.
532     :return: List with parts of the line as strings.
533     """
534     ret = list()
535     inquotes = False
536     current = ""
537     for c in text:
538         if c == "\"":
539             inquotes = not inquotes
540         elif c in ("\t", "\n", " ") and not inquotes:
541             ret.append(current)
542             current = ""
543         else:
544             current += c
545     if current != "":
546         ret.append(current)
547     return ret
548
549
550 class WinsDatabase:
551     def __init__(self, file):
552         self.entries = {}
553         f = open(file, 'r')
554         assert f.readline().rstrip("\n") == "VERSION 1 0"
555         for l in f.readlines():
556             if l[0] == "#": # skip comments
557                 continue
558             entries = shellsplit(l.rstrip("\n"))
559             name = entries[0]
560             ttl = int(entries[1])
561             i = 2
562             ips = []
563             while "." in entries[i]:
564                 ips.append(entries[i])
565                 i+=1
566             nb_flags = int(entries[i][:-1], 16)
567             assert not name in self.entries, "Name %s exists twice" % name
568             self.entries[name] = (ttl, ips, nb_flags)
569         f.close()
570
571     def __getitem__(self, name):
572         return self.entries[name]
573
574     def __len__(self):
575         return len(self.entries)
576
577     def __iter__(self):
578         return iter(self.entries)
579
580     def items(self):
581         return self.entries.items()
582
583     def close(self): # for consistency
584         pass
585
586 class Samba3:
587     def __init__(self, libdir, smbconfpath):
588         self.smbconfpath = smbconfpath
589         self.libdir = libdir
590         import param
591         self.lp = param.ParamFile()
592         self.lp.read(self.smbconfpath)
593
594     def libdir_path(self, path):
595         if path[0] == "/" or path[0] == ".":
596             return path
597         return os.path.join(self.libdir, path)
598
599     def get_conf(self):
600         return self.lp
601
602     def get_sam_db(self):
603         lp = self.get_conf()
604         backends = str(lp.get("passdb backend")).split(" ")
605         if ":" in backends[0]:
606             (name, location) = backends[0].split(":", 2)
607         else:
608             name = backends[0]
609             location = None
610         if name == "smbpasswd":
611             return SmbpasswdFile(self.libdir_path(location or "smbpasswd"))
612         elif name == "tdbsam":
613             return TdbSam(self.libdir_path(location or "passdb.tdb"))
614         elif name == "ldapsam":
615             if location is not None:
616                 return LdapSam("ldap:%s" % location)
617             return LdapSam(lp.get("ldap server"))
618         else:
619             raise NotImplementedError("unsupported passdb backend %s" % backends[0])
620
621     def get_policy_db(self):
622         return PolicyDatabase(self.libdir_path("account_policy.tdb"))
623     
624     def get_registry(self):
625         return Registry(self.libdir_path("registry.tdb"))
626
627     def get_secrets_db(self):
628         return SecretsDatabase(self.libdir_path("secrets.tdb"))
629
630     def get_shareinfo_db(self):
631         return ShareInfoDatabase(self.libdir_path("share_info.tdb"))
632
633     def get_idmap_db(self):
634         return IdmapDatabase(self.libdir_path("winbindd_idmap.tdb"))
635
636     def get_wins_db(self):
637         return WinsDatabase(self.libdir_path("wins.dat"))
638
639     def get_shares(self):
640         return Shares(self.get_conf(), self.get_shareinfo_db())
641
642     def get_groupmapping_db(self):
643         return GroupMappingDatabase(self.libdir_path("group_mapping.tdb"))