samba3-python: Add methods to get any entry (user/group) and its sid from idmap
[mdw/samba.git] / source4 / scripting / 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
29
30 def fetch_uint32(tdb, key):
31     try:
32         data = tdb[key]
33     except KeyError:
34         return None
35     assert len(data) == 4
36     return struct.unpack("<L", data)[0]
37
38
39 def fetch_int32(tdb, key):
40     try:
41         data = tdb[key]
42     except KeyError:
43         return None
44     assert len(data) == 4
45     return struct.unpack("<l", data)[0]
46
47
48 class TdbDatabase(object):
49     """Simple Samba 3 TDB database reader."""
50     def __init__(self, file):
51         """Open a file.
52
53         :param file: Path of the file to open (appending "2" if TDB2 enabled).
54         """
55         if tdb.__version__.startswith("2"):
56             self.tdb = tdb.Tdb(file + "2", flags=os.O_RDONLY)
57         else:
58             self.tdb = tdb.Tdb(file, flags=os.O_RDONLY)
59         self._check_version()
60
61     def _check_version(self):
62         pass
63
64     def close(self):
65         """Close resources associated with this object."""
66         self.tdb.close()
67
68
69 class Registry(TdbDatabase):
70     """Simple read-only support for reading the Samba3 registry.
71
72     :note: This object uses the same syntax for registry key paths as
73         Samba 3. This particular format uses forward slashes for key path
74         separators and abbreviations for the predefined key names.
75         e.g.: HKLM/Software/Bar.
76     """
77     def __len__(self):
78         """Return the number of keys."""
79         return len(self.keys())
80
81     def keys(self):
82         """Return list with all the keys."""
83         return [k.rstrip("\x00") for k in self.tdb.iterkeys() if not k.startswith(REGISTRY_VALUE_PREFIX)]
84
85     def subkeys(self, key):
86         """Retrieve the subkeys for the specified key.
87
88         :param key: Key path.
89         :return: list with key names
90         """
91         data = self.tdb.get("%s\x00" % key)
92         if data is None:
93             return []
94         (num, ) = struct.unpack("<L", data[0:4])
95         keys = data[4:].split("\0")
96         assert keys[-1] == ""
97         keys.pop()
98         assert len(keys) == num
99         return keys
100
101     def values(self, key):
102         """Return a dictionary with the values set for a specific key.
103
104         :param key: Key to retrieve values for.
105         :return: Dictionary with value names as key, tuple with type and
106             data as value."""
107         data = self.tdb.get("%s/%s\x00" % (REGISTRY_VALUE_PREFIX, key))
108         if data is None:
109             return {}
110         ret = {}
111         (num, ) = struct.unpack("<L", data[0:4])
112         data = data[4:]
113         for i in range(num):
114             # Value name
115             (name, data) = data.split("\0", 1)
116
117             (type, ) = struct.unpack("<L", data[0:4])
118             data = data[4:]
119             (value_len, ) = struct.unpack("<L", data[0:4])
120             data = data[4:]
121
122             ret[name] = (type, data[:value_len])
123             data = data[value_len:]
124
125         return ret
126
127
128 class PolicyDatabase(TdbDatabase):
129     """Samba 3 Account Policy database reader."""
130     def __init__(self, file):
131         """Open a policy database
132
133         :param file: Path to the file to open.
134         """
135         super(PolicyDatabase, self).__init__(file)
136         self.min_password_length = fetch_uint32(self.tdb, "min password length\x00")
137         self.password_history = fetch_uint32(self.tdb, "password history\x00")
138         self.user_must_logon_to_change_password = fetch_uint32(self.tdb, "user must logon to change pasword\x00")
139         self.maximum_password_age = fetch_uint32(self.tdb, "maximum password age\x00")
140         self.minimum_password_age = fetch_uint32(self.tdb, "minimum password age\x00")
141         self.lockout_duration = fetch_uint32(self.tdb, "lockout duration\x00")
142         self.reset_count_minutes = fetch_uint32(self.tdb, "reset count minutes\x00")
143         self.bad_lockout_minutes = fetch_uint32(self.tdb, "bad lockout minutes\x00")
144         self.disconnect_time = fetch_int32(self.tdb, "disconnect time\x00")
145         self.refuse_machine_password_change = fetch_uint32(self.tdb, "refuse machine password change\x00")
146
147         # FIXME: Read privileges as well
148
149
150 GROUPDB_DATABASE_VERSION_V1 = 1 # native byte format.
151 GROUPDB_DATABASE_VERSION_V2 = 2 # le format.
152
153 GROUP_PREFIX = "UNIXGROUP/"
154
155 # Alias memberships are stored reverse, as memberships. The performance
156 # critical operation is to determine the aliases a SID is member of, not
157 # listing alias members. So we store a list of alias SIDs a SID is member of
158 # hanging of the member as key.
159 MEMBEROF_PREFIX = "MEMBEROF/"
160
161 class GroupMappingDatabase(TdbDatabase):
162     """Samba 3 group mapping database reader."""
163     def _check_version(self):
164         assert fetch_int32(self.tdb, "INFO/version\x00") in (GROUPDB_DATABASE_VERSION_V1, GROUPDB_DATABASE_VERSION_V2)
165
166     def groupsids(self):
167         """Retrieve the SIDs for the groups in this database.
168
169         :return: List with sids as strings.
170         """
171         for k in self.tdb.iterkeys():
172             if k.startswith(GROUP_PREFIX):
173                 yield k[len(GROUP_PREFIX):].rstrip("\0")
174
175     def get_group(self, sid):
176         """Retrieve the group mapping information for a particular group.
177
178         :param sid: SID of the group
179         :return: None if the group can not be found, otherwise
180             a tuple with gid, sid_name_use, the NT name and comment.
181         """
182         data = self.tdb.get("%s%s\0" % (GROUP_PREFIX, sid))
183         if data is None:
184             return data
185         (gid, sid_name_use) = struct.unpack("<lL", data[0:8])
186         (nt_name, comment, _) = data[8:].split("\0")
187         return (gid, sid_name_use, nt_name, comment)
188
189     def aliases(self):
190         """Retrieve the aliases in this database."""
191         for k in self.tdb.iterkeys():
192             if k.startswith(MEMBEROF_PREFIX):
193                 yield k[len(MEMBEROF_PREFIX):].rstrip("\0")
194
195
196 # High water mark keys
197 IDMAP_HWM_GROUP = "GROUP HWM\0"
198 IDMAP_HWM_USER = "USER HWM\0"
199
200 IDMAP_GROUP_PREFIX = "GID "
201 IDMAP_USER_PREFIX = "UID "
202
203 # idmap version determines auto-conversion
204 IDMAP_VERSION_V2 = 2
205
206 class IdmapDatabase(TdbDatabase):
207     """Samba 3 ID map database reader."""
208     def _check_version(self):
209         assert fetch_int32(self.tdb, "IDMAP_VERSION\0") == IDMAP_VERSION_V2
210
211     def ids(self):
212         """Retrieve a list of all ids in this database."""
213         for k in self.tdb.iterkeys():
214             if k.startswith(IDMAP_USER_PREFIX):
215                 yield k.rstrip("\0").split(" ")
216             if k.startswith(IDMAP_GROUP_PREFIX):
217                 yield k.rstrip("\0").split(" ")
218
219     def uids(self):
220         """Retrieve a list of all uids in this database."""
221         for k in self.tdb.iterkeys():
222             if k.startswith(IDMAP_USER_PREFIX):
223                 yield int(k[len(IDMAP_USER_PREFIX):].rstrip("\0"))
224
225     def gids(self):
226         """Retrieve a list of all gids in this database."""
227         for k in self.tdb.iterkeys():
228             if k.startswith(IDMAP_GROUP_PREFIX):
229                 yield int(k[len(IDMAP_GROUP_PREFIX):].rstrip("\0"))
230
231     def get_sid(self, xid, id_type):
232         data = self.tdb.get("%s %s\0" % (id_type, str(xid)))
233         if data is None:
234             return data
235         return data.rstrip("\0")
236
237     def get_user_sid(self, uid):
238         """Retrieve the SID associated with a particular uid.
239
240         :param uid: UID to retrieve SID for.
241         :return: A SID or None if no mapping was found.
242         """
243         data = self.tdb.get("%s%d\0" % (IDMAP_USER_PREFIX, uid))
244         if data is None:
245             return data
246         return data.rstrip("\0")
247
248     def get_group_sid(self, gid):
249         data = self.tdb.get("%s%d\0" % (IDMAP_GROUP_PREFIX, gid))
250         if data is None:
251             return data
252         return data.rstrip("\0")
253
254     def get_user_hwm(self):
255         """Obtain the user high-water mark."""
256         return fetch_uint32(self.tdb, IDMAP_HWM_USER)
257
258     def get_group_hwm(self):
259         """Obtain the group high-water mark."""
260         return fetch_uint32(self.tdb, IDMAP_HWM_GROUP)
261
262
263 class SecretsDatabase(TdbDatabase):
264     """Samba 3 Secrets database reader."""
265     def get_auth_password(self):
266         return self.tdb.get("SECRETS/AUTH_PASSWORD")
267
268     def get_auth_domain(self):
269         return self.tdb.get("SECRETS/AUTH_DOMAIN")
270
271     def get_auth_user(self):
272         return self.tdb.get("SECRETS/AUTH_USER")
273
274     def get_domain_guid(self, host):
275         return self.tdb.get("SECRETS/DOMGUID/%s" % host)
276
277     def ldap_dns(self):
278         for k in self.tdb.iterkeys():
279             if k.startswith("SECRETS/LDAP_BIND_PW/"):
280                 yield k[len("SECRETS/LDAP_BIND_PW/"):].rstrip("\0")
281
282     def domains(self):
283         """Iterate over domains in this database.
284
285         :return: Iterator over the names of domains in this database.
286         """
287         for k in self.tdb.iterkeys():
288             if k.startswith("SECRETS/SID/"):
289                 yield k[len("SECRETS/SID/"):].rstrip("\0")
290
291     def get_ldap_bind_pw(self, host):
292         return self.tdb.get("SECRETS/LDAP_BIND_PW/%s" % host)
293
294     def get_afs_keyfile(self, host):
295         return self.tdb.get("SECRETS/AFS_KEYFILE/%s" % host)
296
297     def get_machine_sec_channel_type(self, host):
298         return fetch_uint32(self.tdb, "SECRETS/MACHINE_SEC_CHANNEL_TYPE/%s" % host)
299
300     def get_machine_last_change_time(self, host):
301         return fetch_uint32(self.tdb, "SECRETS/MACHINE_LAST_CHANGE_TIME/%s" % host)
302
303     def get_machine_password(self, host):
304         return self.tdb.get("SECRETS/MACHINE_PASSWORD/%s" % host)
305
306     def get_machine_acc(self, host):
307         return self.tdb.get("SECRETS/$MACHINE.ACC/%s" % host)
308
309     def get_domtrust_acc(self, host):
310         return self.tdb.get("SECRETS/$DOMTRUST.ACC/%s" % host)
311
312     def trusted_domains(self):
313         for k in self.tdb.iterkeys():
314             if k.startswith("SECRETS/$DOMTRUST.ACC/"):
315                 yield k[len("SECRETS/$DOMTRUST.ACC/"):].rstrip("\0")
316
317     def get_random_seed(self):
318         return self.tdb.get("INFO/random_seed")
319
320     def get_sid(self, host):
321         return self.tdb.get("SECRETS/SID/%s" % host.upper())
322
323
324 SHARE_DATABASE_VERSION_V1 = 1
325 SHARE_DATABASE_VERSION_V2 = 2
326
327 class ShareInfoDatabase(TdbDatabase):
328     """Samba 3 Share Info database reader."""
329     def _check_version(self):
330         assert fetch_int32(self.tdb, "INFO/version\0") in (SHARE_DATABASE_VERSION_V1, SHARE_DATABASE_VERSION_V2)
331
332     def get_secdesc(self, name):
333         """Obtain the security descriptor on a particular share.
334
335         :param name: Name of the share
336         """
337         secdesc = self.tdb.get("SECDESC/%s" % name)
338         # FIXME: Run ndr_pull_security_descriptor
339         return secdesc
340
341
342 class Shares(object):
343     """Container for share objects."""
344     def __init__(self, lp, shareinfo):
345         self.lp = lp
346         self.shareinfo = shareinfo
347
348     def __len__(self):
349         """Number of shares."""
350         return len(self.lp) - 1
351
352     def __iter__(self):
353         """Iterate over the share names."""
354         return self.lp.__iter__()
355
356
357 ACB_DISABLED = 0x00000001
358 ACB_HOMDIRREQ = 0x00000002
359 ACB_PWNOTREQ = 0x00000004
360 ACB_TEMPDUP = 0x00000008
361 ACB_NORMAL = 0x00000010
362 ACB_MNS = 0x00000020
363 ACB_DOMTRUST = 0x00000040
364 ACB_WSTRUST = 0x00000080
365 ACB_SVRTRUST = 0x00000100
366 ACB_PWNOEXP = 0x00000200
367 ACB_AUTOLOCK = 0x00000400
368 ACB_ENC_TXT_PWD_ALLOWED = 0x00000800
369 ACB_SMARTCARD_REQUIRED = 0x00001000
370 ACB_TRUSTED_FOR_DELEGATION = 0x00002000
371 ACB_NOT_DELEGATED = 0x00004000
372 ACB_USE_DES_KEY_ONLY = 0x00008000
373 ACB_DONT_REQUIRE_PREAUTH = 0x00010000
374 ACB_PW_EXPIRED = 0x00020000
375 ACB_NO_AUTH_DATA_REQD = 0x00080000
376
377 acb_info_mapping = {
378         'N': ACB_PWNOTREQ,  # 'N'o password.
379         'D': ACB_DISABLED,  # 'D'isabled.
380         'H': ACB_HOMDIRREQ, # 'H'omedir required.
381         'T': ACB_TEMPDUP,   # 'T'emp account.
382         'U': ACB_NORMAL,    # 'U'ser account (normal).
383         'M': ACB_MNS,       # 'M'NS logon user account. What is this ?
384         'W': ACB_WSTRUST,   # 'W'orkstation account.
385         'S': ACB_SVRTRUST,  # 'S'erver account.
386         'L': ACB_AUTOLOCK,  # 'L'ocked account.
387         'X': ACB_PWNOEXP,   # No 'X'piry on password
388         'I': ACB_DOMTRUST,  # 'I'nterdomain trust account.
389         ' ': 0
390         }
391
392 def decode_acb(text):
393     """Decode a ACB field.
394
395     :param text: ACB text
396     :return: integer with flags set.
397     """
398     assert not "[" in text and not "]" in text
399     ret = 0
400     for x in text:
401         ret |= acb_info_mapping[x]
402     return ret
403
404
405 class SAMUser(object):
406     """Samba 3 SAM User.
407
408     :note: Unknown or unset fields are set to None.
409     """
410     def __init__(self, name, uid=None, lm_password=None, nt_password=None, acct_ctrl=None,
411                  last_change_time=None, nt_username=None, fullname=None, logon_time=None, logoff_time=None,
412                  acct_desc=None, group_rid=None, bad_password_count=None, logon_count=None,
413                  domain=None, dir_drive=None, munged_dial=None, homedir=None, logon_script=None,
414                  profile_path=None, workstations=None, kickoff_time=None, bad_password_time=None,
415                  pass_last_set_time=None, pass_can_change_time=None, pass_must_change_time=None,
416                  user_rid=None, unknown_6=None, nt_password_history=None,
417                  unknown_str=None, hours=None, logon_divs=None):
418         self.username = name
419         self.uid = uid
420         self.lm_password = lm_password
421         self.nt_password = nt_password
422         self.acct_ctrl = acct_ctrl
423         self.pass_last_set_time = last_change_time
424         self.nt_username = nt_username
425         self.fullname = fullname
426         self.logon_time = logon_time
427         self.logoff_time = logoff_time
428         self.acct_desc = acct_desc
429         self.group_rid = group_rid
430         self.bad_password_count = bad_password_count
431         self.logon_count = logon_count
432         self.domain = domain
433         self.dir_drive = dir_drive
434         self.munged_dial = munged_dial
435         self.homedir = homedir
436         self.logon_script = logon_script
437         self.profile_path = profile_path
438         self.workstations = workstations
439         self.kickoff_time = kickoff_time
440         self.bad_password_time = bad_password_time
441         self.pass_can_change_time = pass_can_change_time
442         self.pass_must_change_time = pass_must_change_time
443         self.user_rid = user_rid
444         self.unknown_6 = unknown_6
445         self.nt_password_history = nt_password_history
446         self.unknown_str = unknown_str
447         self.hours = hours
448         self.logon_divs = logon_divs
449
450     def __eq__(self, other):
451         if not isinstance(other, SAMUser):
452             return False
453         return self.__dict__ == other.__dict__
454
455
456 class SmbpasswdFile(object):
457     """Samba 3 smbpasswd file reader."""
458     def __init__(self, file):
459         self.users = {}
460         f = open(file, 'r')
461         for l in f.readlines():
462             if len(l) == 0 or l[0] == "#":
463                 continue # Skip comments and blank lines
464             parts = l.split(":")
465             username = parts[0]
466             uid = int(parts[1])
467             acct_ctrl = 0
468             last_change_time = None
469             if parts[2] == "NO PASSWORD":
470                 acct_ctrl |= ACB_PWNOTREQ
471                 lm_password = None
472             elif parts[2][0] in ("*", "X"):
473                 # No password set
474                 lm_password = None
475             else:
476                 lm_password = parts[2]
477
478             if parts[3][0] in ("*", "X"):
479                 # No password set
480                 nt_password = None
481             else:
482                 nt_password = parts[3]
483
484             if parts[4][0] == '[':
485                 assert "]" in parts[4]
486                 acct_ctrl |= decode_acb(parts[4][1:-1])
487                 if parts[5].startswith("LCT-"):
488                     last_change_time = int(parts[5][len("LCT-"):], 16)
489             else: # old style file
490                 if username[-1] == "$":
491                     acct_ctrl &= ~ACB_NORMAL
492                     acct_ctrl |= ACB_WSTRUST
493
494             self.users[username] = SAMUser(username, uid, lm_password, nt_password, acct_ctrl, last_change_time)
495
496         f.close()
497
498     def __len__(self):
499         return len(self.users)
500
501     def __getitem__(self, name):
502         return self.users[name]
503
504     def __iter__(self):
505         return iter(self.users)
506
507     def close(self): # For consistency
508         pass
509
510
511 TDBSAM_FORMAT_STRING_V0 = "ddddddBBBBBBBBBBBBddBBwdwdBwwd"
512 TDBSAM_FORMAT_STRING_V1 = "dddddddBBBBBBBBBBBBddBBwdwdBwwd"
513 TDBSAM_FORMAT_STRING_V2 = "dddddddBBBBBBBBBBBBddBBBwwdBwwd"
514 TDBSAM_USER_PREFIX = "USER_"
515
516
517 class LdapSam(object):
518     """Samba 3 LDAP passdb backend reader."""
519     def __init__(self, url):
520         self.ldap_url = url
521
522
523 class TdbSam(TdbDatabase):
524     """Samba 3 TDB passdb backend reader."""
525     def _check_version(self):
526         self.version = fetch_uint32(self.tdb, "INFO/version\0") or 0
527         assert self.version in (0, 1, 2)
528
529     def usernames(self):
530         """Iterate over the usernames in this Tdb database."""
531         for k in self.tdb.iterkeys():
532             if k.startswith(TDBSAM_USER_PREFIX):
533                 yield k[len(TDBSAM_USER_PREFIX):].rstrip("\0")
534
535     __iter__ = usernames
536
537     def __getitem__(self, name):
538         data = self.tdb["%s%s\0" % (TDBSAM_USER_PREFIX, name)]
539         user = SAMUser(name)
540
541         def unpack_string(data):
542             (length, ) = struct.unpack("<L", data[:4])
543             data = data[4:]
544             if length == 0:
545                 return (None, data)
546             return (data[:length].rstrip("\0"), data[length:])
547
548         def unpack_int32(data):
549             (value, ) = struct.unpack("<l", data[:4])
550             return (value, data[4:])
551
552         def unpack_uint32(data):
553             (value, ) = struct.unpack("<L", data[:4])
554             return (value, data[4:])
555
556         def unpack_uint16(data):
557             (value, ) = struct.unpack("<H", data[:2])
558             return (value, data[2:])
559
560         (logon_time, data) = unpack_int32(data)
561         (logoff_time, data) = unpack_int32(data)
562         (kickoff_time, data) = unpack_int32(data)
563
564         if self.version > 0:
565             (bad_password_time, data) = unpack_int32(data)
566             if bad_password_time != 0:
567                 user.bad_password_time = bad_password_time
568         (pass_last_set_time, data) = unpack_int32(data)
569         (pass_can_change_time, data) = unpack_int32(data)
570         (pass_must_change_time, data) = unpack_int32(data)
571
572         if logon_time != 0:
573             user.logon_time = logon_time
574         user.logoff_time = logoff_time
575         user.kickoff_time = kickoff_time
576         if pass_last_set_time != 0:
577             user.pass_last_set_time = pass_last_set_time
578         user.pass_can_change_time = pass_can_change_time
579
580         (user.username, data) = unpack_string(data)
581         (user.domain, data) = unpack_string(data)
582         (user.nt_username, data) = unpack_string(data)
583         (user.fullname, data) = unpack_string(data)
584         (user.homedir, data) = unpack_string(data)
585         (user.dir_drive, data) = unpack_string(data)
586         (user.logon_script, data) = unpack_string(data)
587         (user.profile_path, data) = unpack_string(data)
588         (user.acct_desc, data) = unpack_string(data)
589         (user.workstations, data) = unpack_string(data)
590         (user.unknown_str, data) = unpack_string(data)
591         (user.munged_dial, data) = unpack_string(data)
592
593         (user.user_rid, data) = unpack_int32(data)
594         (user.group_rid, data) = unpack_int32(data)
595
596         (user.lm_password, data) = unpack_string(data)
597         (user.nt_password, data) = unpack_string(data)
598
599         if self.version > 1:
600             (user.nt_password_history, data) = unpack_string(data)
601
602         (user.acct_ctrl, data) = unpack_uint16(data)
603         (_, data) = unpack_uint32(data) # remove_me field
604         (user.logon_divs, data) = unpack_uint16(data)
605         (hours, data) = unpack_string(data)
606         user.hours = []
607         for entry in hours:
608             for i in range(8):
609                 user.hours.append(ord(entry) & (2 ** i) == (2 ** i))
610         (user.bad_password_count, data) = unpack_uint16(data)
611         (user.logon_count, data) = unpack_uint16(data)
612         (user.unknown_6, data) = unpack_uint32(data)
613         assert len(data) == 0
614         return user
615
616
617 def shellsplit(text):
618     """Very simple shell-like line splitting.
619
620     :param text: Text to split.
621     :return: List with parts of the line as strings.
622     """
623     ret = list()
624     inquotes = False
625     current = ""
626     for c in text:
627         if c == "\"":
628             inquotes = not inquotes
629         elif c in ("\t", "\n", " ") and not inquotes:
630             ret.append(current)
631             current = ""
632         else:
633             current += c
634     if current != "":
635         ret.append(current)
636     return ret
637
638
639 class WinsDatabase(object):
640     """Samba 3 WINS database reader."""
641     def __init__(self, file):
642         self.entries = {}
643         f = open(file, 'r')
644         assert f.readline().rstrip("\n") == "VERSION 1 0"
645         for l in f.readlines():
646             if l[0] == "#": # skip comments
647                 continue
648             entries = shellsplit(l.rstrip("\n"))
649             name = entries[0]
650             ttl = int(entries[1])
651             i = 2
652             ips = []
653             while "." in entries[i]:
654                 ips.append(entries[i])
655                 i+=1
656             nb_flags = int(entries[i][:-1], 16)
657             assert not name in self.entries, "Name %s exists twice" % name
658             self.entries[name] = (ttl, ips, nb_flags)
659         f.close()
660
661     def __getitem__(self, name):
662         return self.entries[name]
663
664     def __len__(self):
665         return len(self.entries)
666
667     def __iter__(self):
668         return iter(self.entries)
669
670     def items(self):
671         """Return the entries in this WINS database."""
672         return self.entries.items()
673
674     def close(self): # for consistency
675         pass
676
677
678 class ParamFile(object):
679     """Simple smb.conf-compatible file parser
680
681     Does not use a parameter table, unlike the "normal".
682     """
683
684     def __init__(self, sections=None):
685         self._sections = sections or {}
686
687     def _sanitize_name(self, name):
688         return name.strip().lower().replace(" ","")
689
690     def __repr__(self):
691         return "ParamFile(%r)" % self._sections
692
693     def read(self, filename):
694         """Read a file.
695
696         :param filename: Path to the file
697         """
698         section = None
699         for i, l in enumerate(open(filename, 'r').xreadlines()):
700             l = l.strip()
701             if not l or l[0] == '#' or l[0] == ';':
702                 continue
703             if l[0] == "[" and l[-1] == "]":
704                 section = self._sanitize_name(l[1:-1])
705                 self._sections.setdefault(section, {})
706             elif "=" in l:
707                (k, v) = l.split("=", 1)
708                self._sections[section][self._sanitize_name(k)] = v
709             else:
710                 raise Exception("Unable to parser line %d: %r" % (i+1,l))
711
712     def get(self, param, section=None):
713         """Return the value of a parameter.
714
715         :param param: Parameter name
716         :param section: Section name, defaults to "global"
717         :return: parameter value as string if found, None otherwise.
718         """
719         if section is None:
720             section = "global"
721         section = self._sanitize_name(section)
722         if not section in self._sections:
723             return None
724         param = self._sanitize_name(param)
725         if not param in self._sections[section]:
726             return None
727         return self._sections[section][param].strip()
728
729     def __getitem__(self, section):
730         return self._sections[section]
731
732     def get_section(self, section):
733         return self._sections.get(section)
734
735     def add_section(self, section):
736         self._sections[self._sanitize_name(section)] = {}
737
738     def set_string(self, name, value):
739         self._sections["global"][name] = value
740
741     def get_string(self, name):
742         return self._sections["global"].get(name)
743
744
745 class Samba3(object):
746     """Samba 3 configuration and state data reader."""
747     def __init__(self, libdir, smbconfpath):
748         """Open the configuration and data for a Samba 3 installation.
749
750         :param libdir: Library directory
751         :param smbconfpath: Path to the smb.conf file.
752         """
753         self.smbconfpath = smbconfpath
754         self.libdir = libdir
755         self.lp = ParamFile()
756         self.lp.read(self.smbconfpath)
757
758     def libdir_path(self, path):
759         if path[0] == "/" or path[0] == ".":
760             return path
761         return os.path.join(self.libdir, path)
762
763     def get_conf(self):
764         return self.lp
765
766     def get_sam_db(self):
767         lp = self.get_conf()
768         backends = (lp.get("passdb backend") or "").split(" ")
769         if ":" in backends[0]:
770             (name, location) = backends[0].split(":", 2)
771         else:
772             name = backends[0]
773             location = None
774         if name == "smbpasswd":
775             return SmbpasswdFile(self.libdir_path(location or "smbpasswd"))
776         elif name == "tdbsam":
777             return TdbSam(self.libdir_path(location or "passdb.tdb"))
778         elif name == "ldapsam":
779             if location is not None:
780                 return LdapSam("ldap:%s" % location)
781             return LdapSam(lp.get("ldap server"))
782         else:
783             raise NotImplementedError("unsupported passdb backend %s" % backends[0])
784
785     def get_policy_db(self):
786         return PolicyDatabase(self.libdir_path("account_policy.tdb"))
787
788     def get_registry(self):
789         return Registry(self.libdir_path("registry.tdb"))
790
791     def get_secrets_db(self):
792         return SecretsDatabase(self.libdir_path("secrets.tdb"))
793
794     def get_shareinfo_db(self):
795         return ShareInfoDatabase(self.libdir_path("share_info.tdb"))
796
797     def get_idmap_db(self):
798         return IdmapDatabase(self.libdir_path("winbindd_idmap.tdb"))
799
800     def get_wins_db(self):
801         return WinsDatabase(self.libdir_path("wins.dat"))
802
803     def get_shares(self):
804         return Shares(self.get_conf(), self.get_shareinfo_db())
805
806     def get_groupmapping_db(self):
807         return GroupMappingDatabase(self.libdir_path("group_mapping.tdb"))