More loop logic cleanup - we only handle mail for gid Debian, so we
[mirror/userdir-ldap.git] / ud-generate
1 #!/usr/bin/env python
2 # -*- mode: python -*-
3 # Generates passwd, shadow and group files from the ldap directory.
4
5 #   Copyright (c) 2000-2001  Jason Gunthorpe <jgg@debian.org>
6 #   Copyright (c) 2003-2004  James Troup <troup@debian.org>
7 #   Copyright (c) 2004-2005,7  Joey Schulze <joey@infodrom.org>
8 #   Copyright (c) 2001-2007  Ryan Murray <rmurray@debian.org>
9 #   Copyright (c) 2008 Peter Palfrader <peter@palfrader.org>
10 #   Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
11 #   Copyright (c) 2008 Mark Hymers <mhy@debian.org>
12 #   Copyright (c) 2008 Luk Claes <luk@debian.org>
13 #   Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
14 #   Copyright (c) 2009 Stephen Gran <steve@lobefin.net>
15 #
16 #   This program is free software; you can redistribute it and/or modify
17 #   it under the terms of the GNU General Public License as published by
18 #   the Free Software Foundation; either version 2 of the License, or
19 #   (at your option) any later version.
20 #
21 #   This program is distributed in the hope that it will be useful,
22 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
23 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24 #   GNU General Public License for more details.
25 #
26 #   You should have received a copy of the GNU General Public License
27 #   along with this program; if not, write to the Free Software
28 #   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
29
30 import string, re, time, ldap, getopt, sys, os, pwd, posix, socket, base64, sha, shutil, errno, tarfile, grp
31 from userdir_ldap import *
32 from userdir_exceptions import *
33
34 global Allowed
35 global CurrentHost
36
37 PasswdAttrs = None
38 DisabledUsers = []
39 GroupIDMap = {}
40 SubGroupMap = {}
41 Allowed = None
42 CurrentHost = ""
43
44 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
45
46 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
47 BSMTPCheck = re.compile(".*mx 0 (gluck)\.debian\.org\..*",re.DOTALL)
48 PurposeHostField = re.compile(r"\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
49 DNSZone = ".debian.net"
50 Keyrings = ConfModule.sync_keyrings.split(":")
51
52 def safe_makedirs(dir):
53    try:
54       os.makedirs(dir)
55    except OSError, e:
56       if e.errno == errno.EEXIST:
57          pass
58       else:
59          raise e
60
61 def safe_rmtree(dir):
62    try:
63       shutil.rmtree(dir)
64    except OSError, e:
65       if e.errno == errno.ENOENT:
66          pass
67       else:
68          raise e
69
70 def Sanitize(Str):
71    return Str.translate(string.maketrans("\n\r\t", "$$$"))
72
73 def DoLink(From, To, File):
74    try: 
75       posix.remove(To + File)
76    except: 
77       pass
78    posix.link(From + File, To + File)
79
80 def IsRetired(DnRecord):
81    """
82    Looks for accountStatus in the LDAP record and tries to
83    match it against one of the known retired statuses
84    """
85
86    status = GetAttr(DnRecord, "accountStatus", None)
87    if status is None:
88       return False
89
90    line = status.split()
91    status = line[0]
92    
93    if status == "inactive":
94       return True
95
96    elif status == "memorial":
97       return True
98
99    elif status == "retiring":
100       # We'll give them a few extra days over what we said
101       age = 6 * 31 * 24 * 60 * 60
102       try:
103          if (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age:
104             return True
105       except IndexError:
106          return False
107
108    return False
109
110 def IsGidDebian(x):
111    try:
112       return int(GetAttr(x, "gidNumber", 0)) == 800
113    except ValueError:
114       return False
115
116 # See if this user is in the group list
117 def IsInGroup(DnRecord):
118   if Allowed == None:
119      return 1
120
121   # See if the primary group is in the list
122   if Allowed.has_key(GetAttr(DnRecord, "gidNumber")) != 0:
123      return 1
124
125   # Check the host based ACL
126   if DnRecord[1].has_key("allowedHost") != 0:
127      for I in DnRecord[1]["allowedHost"]:
128         if CurrentHost == I:
129            return 1
130
131   # See if there are supplementary groups
132   if DnRecord[1].has_key("supplementaryGid") == 0:
133      return 0
134
135   supgroups=[]
136   addGroups(supgroups, DnRecord[1]["supplementaryGid"], GetAttr(DnRecord, "uid"))
137   for g in supgroups:
138      if Allowed.has_key(g):
139         return 1
140   return 0
141
142 def Die(File, F, Fdb):
143    if F != None:
144       F.close()
145    if Fdb != None:
146       Fdb.close()
147    try: 
148       os.remove(File + ".tmp")
149    except:
150       pass
151    try: 
152       os.remove(File + ".tdb.tmp")
153    except: 
154       pass
155
156 def Done(File, F, Fdb):
157    if F != None:
158       F.close()
159       os.rename(File + ".tmp", File)
160    if Fdb != None:
161       Fdb.close()
162       os.rename(File + ".tdb.tmp", File + ".tdb")
163
164 # Generate the password list
165 def GenPasswd(File, HomePrefix, PwdMarker):
166    F = None
167    try:
168       F = open(File + ".tdb.tmp", "w")
169      
170       userlist = {}
171       # Fetch all the users
172       global PasswdAttrs
173      
174       I = 0
175       for x in PasswdAttrs:
176          if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
177             continue
178      
179          # Do not let people try to buffer overflow some busted passwd parser.
180          if len(GetAttr(x, "gecos")) > 100 or len(GetAttr(x, "loginShell")) > 50:
181             continue
182      
183          userlist[GetAttr(x, "uid")] = int(GetAttr(x, "gidNumber"))
184          Line = "%s:%s:%s:%s:%s:%s%s:%s" % (GetAttr(x, "uid"),\
185                  PwdMarker,\
186                  GetAttr(x, "uidNumber"), GetAttr(x, "gidNumber"),\
187                  GetAttr(x, "gecos"), HomePrefix, GetAttr(x, "uid"),\
188                  GetAttr(x, "loginShell"))
189      
190          Line = Sanitize(Line) + "\n"
191          F.write("0%u %s" % (I, Line))
192          F.write(".%s %s" % (GetAttr(x, "uid"), Line))
193          F.write("=%s %s" % (GetAttr(x, "uidNumber"), Line))
194          I = I + 1
195   
196    # Oops, something unspeakable happened.
197    except:
198       Die(File, None, F)
199       raise
200    Done(File, None, F)
201
202    # Return the list of users so we know which keys to export
203    return userlist
204
205 # Generate the shadow list
206 def GenShadow(File):
207    F = None
208    try:
209       OldMask = os.umask(0077)
210       F = open(File + ".tdb.tmp", "w", 0600)
211       os.umask(OldMask)
212      
213       # Fetch all the users
214       global PasswdAttrs
215      
216       I = 0
217       for x in PasswdAttrs:
218          if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
219             continue
220      
221          Pass = GetAttr(x, "userPassword")
222          if Pass[0:7] != "{crypt}" or len(Pass) > 50:
223             Pass = '*'
224          else:
225             Pass = Pass[7:]
226      
227          # If the account is locked, mark it as such in shadow
228          # See Debian Bug #308229 for why we set it to 1 instead of 0
229          if (GetAttr(x, "userPassword").find("*LK*") != -1) \
230              or GetAttr(x, "userPassword").startswith("!"):
231             ShadowExpire = '1'
232          else:
233             ShadowExpire = GetAttr(x, "shadowExpire")
234      
235          Line = "%s:%s:%s:%s:%s:%s:%s:%s:" % (GetAttr(x, "uid"),\
236                  Pass, GetAttr(x, "shadowLastChange"),\
237                  GetAttr(x, "shadowMin"), GetAttr(x, "shadowMax"),\
238                  GetAttr(x, "shadowWarning"), GetAttr(x, "shadowInactive"),\
239                  ShadowExpire)
240          Line = Sanitize(Line) + "\n"
241          F.write("0%u %s" % (I, Line))
242          F.write(".%s %s" % (GetAttr(x, "uid"), Line))
243          I = I + 1
244   
245    # Oops, something unspeakable happened.
246    except:
247       Die(File, None, F)
248       raise
249    Done(File, None, F)
250
251 # Generate the sudo passwd file
252 def GenShadowSudo(File, untrusted):
253    F = None
254    try:
255       OldMask = os.umask(0077)
256       F = open(File + ".tmp", "w", 0600)
257       os.umask(OldMask)
258      
259       # Fetch all the users
260       global PasswdAttrs
261      
262       for x in PasswdAttrs:
263          Pass = '*'
264          if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
265             continue
266      
267          if x[1].has_key('sudoPassword'):
268             for entry in x[1]['sudoPassword']:
269                Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
270                if Match == None:
271                   continue
272                uuid = Match.group(1)
273                status = Match.group(2)
274                hosts = Match.group(3)
275                cryptedpass = Match.group(4)
276      
277                if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', x[1]['uid'][0], uuid, hosts, cryptedpass):
278                   continue
279                for_all = hosts == "*"
280                for_this_host = CurrentHost in hosts.split(',')
281                if not (for_all or for_this_host):
282                   continue
283                # ignore * passwords for untrusted hosts, but copy host specific passwords
284                if for_all and untrusted:
285                   continue
286                Pass = cryptedpass
287                if for_this_host: # this makes sure we take a per-host entry over the for-all entry
288                  break
289             if len(Pass) > 50:
290                Pass = '*'
291      
292          Line = "%s:%s" % (GetAttr(x, "uid"), Pass)
293          Line = Sanitize(Line) + "\n"
294          F.write("%s" % (Line))
295   
296    # Oops, something unspeakable happened.
297    except:
298       Die(File, F, None)
299       raise
300    Done(File, F, None)
301
302 # Generate the shadow list
303 def GenSSHShadow():
304    # Fetch all the users
305    userfiles = []
306
307    global PasswdAttrs
308
309    safe_rmtree(os.path.join(GlobalDir, 'userkeys'))
310    safe_makedirs(os.path.join(GlobalDir, 'userkeys'))
311
312    for x in PasswdAttrs:
313
314       if x[1].has_key("uidNumber") == 0 or \
315          x[1].has_key("sshRSAAuthKey") == 0:
316          continue
317
318       User = GetAttr(x, "uid")
319       F = None
320
321       try:
322          OldMask = os.umask(0077)
323          File = os.path.join(GlobalDir, 'userkeys', User)
324          F = open(File + ".tmp", "w", 0600)
325          os.umask(OldMask)
326
327          for I in x[1]["sshRSAAuthKey"]:
328             MultipleLine = "%s" % I
329             MultipleLine = Sanitize(MultipleLine) + "\n"
330             F.write(MultipleLine)
331
332          Done(File, F, None)
333          userfiles.append(os.path.basename(File))
334
335       # Oops, something unspeakable happened.
336       except IOError:
337           Die(File, F, None)
338           Die(masterFileName, masterFile, None)
339           raise
340
341    return userfiles
342
343 def GenSSHtarballs(userlist, SSHFiles, grouprevmap, target):
344    OldMask = os.umask(0077)
345    tf = tarfile.open(name=os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
346    os.umask(OldMask)
347    for f in userlist.keys():
348       if f not in SSHFiles:
349          continue
350       # If we're not exporting their primary group, don't export
351       # the key and warn
352       grname = None
353       if userlist[f] in grouprevmap.keys():
354          grname = grouprevmap[userlist[f]]
355       else:
356          try:
357             if int(userlist[f]) <= 100:
358                # In these cases, look it up in the normal way so we
359                # deal with cases where, for instance, users are in group
360                # users as their primary group.
361                grname = grp.getgrgid(userlist[f])[0]
362          except Exception, e:
363             pass
364
365       if grname is None:
366          print "User %s is supposed to have their key exported to host %s but their primary group (gid: %d) isn't in LDAP" % (f, CurrentHost, userlist[f])
367          continue
368
369       to = tf.gettarinfo(os.path.join(GlobalDir, 'userkeys', f), f)
370       # These will only be used where the username doesn't
371       # exist on the target system for some reason; hence,
372       # in those cases, the safest thing is for the file to
373       # be owned by root but group nobody.  This deals with
374       # the bloody obscure case where the group fails to exist
375       # whilst the user does (in which case we want to avoid
376       # ending up with a file which is owned user:root to avoid
377       # a fairly obvious attack vector)
378       to.uid = 0
379       to.gid = 65534
380       # Using the username / groupname fields avoids any need
381       # to give a shit^W^W^Wcare about the UIDoffset stuff.
382       to.uname = f
383       to.gname = grname
384       to.mode  = 0400
385       tf.addfile(to, file(os.path.join(GlobalDir, 'userkeys', f)))
386
387    tf.close()
388    os.rename(os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
389
390 # add a list of groups to existing groups,
391 # including all subgroups thereof, recursively.
392 # basically this proceduces the transitive hull of the groups in
393 # addgroups.
394 def addGroups(existingGroups, newGroups, uid):
395    for group in newGroups:
396       # if it's a <group>@host, split it and verify it's on the current host.
397       s = group.split('@', 1)
398       if len(s) == 2 and s[1] != CurrentHost:
399          continue
400       group = s[0]
401
402       # let's see if we handled this group already
403       if group in existingGroups:
404         continue
405
406       if not GroupIDMap.has_key(group):
407          print "Group", group, "does not exist but", uid, "is in it"
408          continue
409
410       existingGroups.append(group)
411
412       if SubGroupMap.has_key(group):
413          addGroups(existingGroups, SubGroupMap[group], uid)
414
415 # Generate the group list
416 def GenGroup(File):
417    grouprevmap = {}
418    F = None
419    try:
420       F = open(File + ".tdb.tmp", "w")
421      
422       # Generate the GroupMap
423       GroupMap = {}
424       for x in GroupIDMap.keys():
425          GroupMap[x] = []
426      
427       # Fetch all the users
428       global PasswdAttrs
429      
430       # Sort them into a list of groups having a set of users
431       for x in PasswdAttrs:
432          uid = GetAttr(x, "uid")
433          if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
434             continue
435          if x[1].has_key("supplementaryGid") == 0:
436             continue
437      
438          supgroups=[]
439          addGroups(supgroups, x[1]["supplementaryGid"], uid)
440          for g in supgroups:
441             GroupMap[g].append(uid)
442      
443       # Output the group file.
444       J = 0
445       for x in GroupMap.keys():
446          grouprevmap[GroupIDMap[x]] = x
447          if GroupIDMap.has_key(x) == 0:
448             continue
449          Line = "%s:x:%u:" % (x, GroupIDMap[x])
450          Comma = ''
451          for I in GroupMap[x]:
452            Line = Line + ("%s%s" % (Comma, I))
453            Comma = ','
454          Line = Sanitize(Line) + "\n"
455          F.write("0%u %s" % (J, Line))
456          F.write(".%s %s" % (x, Line))
457          F.write("=%u %s" % (GroupIDMap[x], Line))
458          J = J + 1
459   
460    # Oops, something unspeakable happened.
461    except:
462       Die(File, None, F)
463       raise
464    Done(File, None, F)
465   
466    return grouprevmap
467
468 def CheckForward():
469    global DebianUsers
470    for x in DebianUsers:
471       if x[1].has_key("emailForward") == 0:
472          continue
473    
474       if IsInGroup(x) == 0:
475          x[1].pop("emailForward")
476          continue
477
478       # Do not allow people to try to buffer overflow busted parsers
479       if len(GetAttr(x, "emailForward")) > 200:
480          x[1].pop("emailForward")
481          continue
482
483       # Check the forwarding address
484       if EmailCheck.match(GetAttr(x, "emailForward")) == None:
485          x[1].pop("emailForward")
486
487 # Generate the email forwarding list
488 def GenForward(File):
489    F = None
490    try:
491       OldMask = os.umask(0022)
492       F = open(File + ".tmp", "w", 0644)
493       os.umask(OldMask)
494      
495       # Fetch all the users
496       global DebianUsers
497      
498       # Write out the email address for each user
499       for x in DebianUsers:
500          if x[1].has_key("emailForward") == 0:
501             continue
502      
503          Line = "%s: %s" % (GetAttr(x, "uid"), GetAttr(x, "emailForward"))
504          Line = Sanitize(Line) + "\n"
505          F.write(Line)
506   
507    # Oops, something unspeakable happened.
508    except:
509       Die(File, F, None)
510       raise
511    Done(File, F, None)
512
513 def GenAllForward(File):
514    Fdb = None
515    try:
516       OldMask = os.umask(0022)
517       Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
518       os.umask(OldMask)
519      
520       # Fetch all the users
521       global DebianUsers
522      
523       # Write out the email address for each user
524       for x in DebianUsers:
525          if x[1].has_key("emailForward") == 0:
526             continue
527      
528          # Do not allow people to try to buffer overflow busted parsers
529          Forward = GetAttr(x, "emailForward")
530      
531          User = GetAttr(x, "uid")
532          Fdb.write("+%d,%d:%s->%s\n" % (len(User), len(Forward), User, Forward))
533   
534       Fdb.write("\n")
535    # Oops, something unspeakable happened.
536    except:
537       Fdb.close()
538       raise
539    if Fdb.close() != None:
540       raise "cdbmake gave an error"
541
542 # Generate the anon XEarth marker file
543 def GenMarkers(File):
544    F = None
545    try:
546       F = open(File + ".tmp", "w")
547      
548       # Fetch all the users
549       global DebianUsers
550      
551       # Write out the position for each user
552       for x in DebianUsers:
553          if x[1].has_key("latitude") == 0 or x[1].has_key("longitude") == 0:
554             continue
555          try:
556             Line = "%8s %8s \"\""%(DecDegree(GetAttr(x, "latitude"), 1), DecDegree(GetAttr(x, "longitude"), 1))
557             Line = Sanitize(Line) + "\n"
558             F.write(Line)
559          except:
560             pass
561   
562    # Oops, something unspeakable happened.
563    except:
564       Die(File, F, None)
565       raise
566    Done(File, F, None)
567
568 # Generate the debian-private subscription list
569 def GenPrivate(File):
570    F = None
571    try:
572       F = open(File + ".tmp", "w")
573      
574       # Fetch all the users
575       global DebianUsers
576      
577       # Write out the position for each user
578       for x in DebianUsers:
579          if x[1].has_key("privateSub") == 0:
580             continue
581      
582          # If the account has no PGP key, do not write it
583          if x[1].has_key("keyFingerPrint") == 0:
584             continue
585      
586          try:
587             Line = "%s"%(GetAttr(x, "privateSub"))
588             Line = Sanitize(Line) + "\n"
589             F.write(Line)
590          except:
591             pass
592   
593    # Oops, something unspeakable happened.
594    except:
595       Die(File, F, None)
596       raise
597    Done(File, F, None)
598
599 # Generate a list of locked accounts
600 def GenDisabledAccounts(File):
601    F = None
602    try:
603       F = open(File + ".tmp", "w")
604      
605       # Fetch all the users
606       global PasswdAttrs
607       global DisabledUsers
608      
609       I = 0
610       for x in PasswdAttrs:
611          if x[1].has_key("uidNumber") == 0:
612             continue
613      
614          Pass = GetAttr(x, "userPassword")
615          Line = ""
616          # *LK* is the reference value for a locked account
617          # password starting with ! is also a locked account
618          if Pass.find("*LK*") != -1 or Pass.startswith("!"):
619             # Format is <login>:<reason>
620             Line = "%s:%s" % (GetAttr(x, "uid"), "Account is locked")
621      
622          if Line != "":
623             F.write(Sanitize(Line) + "\n")
624      
625          DisabledUsers.append(x)
626    
627    # Oops, something unspeakable happened.
628    except:
629       Die(File, F, None)
630       raise
631    Done(File, F, None)
632
633 # Generate the list of local addresses that refuse all mail
634 def GenMailDisable(File):
635    F = None
636    try:
637       F = open(File + ".tmp", "w")
638      
639       # Fetch all the users
640       global DebianUsers
641      
642       for x in DebianUsers:
643          Reason = None
644      
645          if x[1].has_key("mailDisableMessage"):
646             Reason = GetAttr(x, "mailDisableMessage")
647          else:
648             continue
649      
650          try:
651             Line = "%s: %s"%(GetAttr(x, "uid"), Reason)
652             Line = Sanitize(Line) + "\n"
653             F.write(Line)
654          except:
655             pass
656   
657    # Oops, something unspeakable happened.
658    except:
659       Die(File, F, None)
660       raise
661    Done(File, F, None)
662
663 # Generate a list of uids that should have boolean affects applied
664 def GenMailBool(File, Key):
665    F = None
666    try:
667       F = open(File + ".tmp", "w")
668      
669       # Fetch all the users
670       global DebianUsers
671      
672       for x in DebianUsers:
673          Reason = None
674      
675          if x[1].has_key(Key) == 0:
676             continue
677      
678          if GetAttr(x, Key) != "TRUE":
679             continue
680      
681          try:
682             Line = "%s"%(GetAttr(x, "uid"))
683             Line = Sanitize(Line) + "\n"
684             F.write(Line)
685          except:
686             pass
687   
688    # Oops, something unspeakable happened.
689    except:
690       Die(File, F, None)
691       raise
692    Done(File, F, None)
693
694 # Generate a list of hosts for RBL or whitelist purposes.
695 def GenMailList(File, Key):
696    F = None
697    try:
698       F = open(File + ".tmp", "w")
699      
700       # Fetch all the users
701       global DebianUsers
702      
703       for x in DebianUsers:
704          Reason = None
705      
706          if x[1].has_key(Key) == 0:
707             continue
708      
709          try:
710             found = 0
711             Line = None
712             for z in x[1][Key]:
713                 if Key == "mailWhitelist":
714                     if re.match('^[-\w.]+(/[\d]+)?$', z) == None:
715                         continue
716                 else:
717                     if re.match('^[-\w.]+$', z) == None:
718                         continue
719                 if found == 0:
720                     found = 1
721                     Line = GetAttr(x, "uid")
722                 else:
723                     Line += " "
724                 Line += ": " + z
725                 if Key == "mailRHSBL":
726                     Line += "/$sender_address_domain"
727      
728             if Line != None:
729                 Line = Sanitize(Line) + "\n"
730                 F.write(Line)
731          except:
732             pass
733   
734    # Oops, something unspeakable happened.
735    except:
736       Die(File, F, None)
737       raise
738    Done(File, F, None)
739
740 def isRoleAccount(pwEntry):
741    if not pwEntry.has_key("objectClass"):
742       raise "pwEntry has no objectClass"
743    oc =  pwEntry['objectClass']
744    try:
745       i = oc.index('debianRoleAccount')
746       return True
747    except ValueError:
748       return False
749
750 # Generate the DNS Zone file
751 def GenDNS(File, HomePrefix):
752    F = None
753    try:
754       F = open(File + ".tmp", "w")
755      
756       # Fetch all the users
757       global PasswdAttrs
758      
759       # Write out the zone file entry for each user
760       for x in PasswdAttrs:
761          if x[1].has_key("dnsZoneEntry") == 0:
762             continue
763      
764          # If the account has no PGP key, do not write it
765          if x[1].has_key("keyFingerPrint") == 0 and not isRoleAccount(x[1]):
766             continue
767          try:
768             F.write("; %s\n"%(EmailAddress(x)))
769             for z in x[1]["dnsZoneEntry"]:
770                Split = z.lower().split()
771                if Split[1].lower() == 'in':
772                   for y in range(0, len(Split)):
773                      if Split[y] == "$":
774                         Split[y] = "\n\t"
775                   Line = " ".join(Split) + "\n"
776                   F.write(Line)
777      
778                   Host = Split[0] + DNSZone
779                   if BSMTPCheck.match(Line) != None:
780                       F.write("; Has BSMTP\n")
781      
782                   # Write some identification information
783                   if Split[2].lower() == "a":
784                      Line = "%s IN TXT \"%s\"\n"%(Split[0], EmailAddress(x))
785                      for y in x[1]["keyFingerPrint"]:
786                         Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
787                      F.write(Line)
788                else:
789                   Line = "; Err %s"%(str(Split))
790                   F.write(Line)
791      
792             F.write("\n")
793          except:
794             F.write("; Errors\n")
795             pass
796   
797    # Oops, something unspeakable happened.
798    except:
799       Die(File, F, None)
800       raise
801    Done(File, F, None)
802
803 # Generate the DNS SSHFP records
804 def GenSSHFP(File, HomePrefix):
805    F = None
806    try:
807       F = open(File + ".tmp", "w")
808      
809       # Fetch all the hosts
810       global HostAttrs
811       if HostAttrs == None:
812          raise UDEmptyList, "No Hosts"
813      
814       for x in HostAttrs:
815          if x[1].has_key("hostname") == 0 or \
816             x[1].has_key("sshRSAHostKey") == 0:
817             continue
818          Host = GetAttr(x, "hostname")
819          Algorithm = None
820          for I in x[1]["sshRSAHostKey"]:
821             Split = I.split()
822             if Split[0] == 'ssh-rsa':
823                Algorithm = 1
824             if Split[0] == 'ssh-dss':
825                Algorithm = 2
826             if Algorithm == None:
827                continue
828             Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
829             Line = "%s. IN SSHFP %u 1 %s" % (Host, Algorithm, Fingerprint)
830             Line = Sanitize(Line) + "\n"
831             F.write(Line)
832    # Oops, something unspeakable happened.
833    except:
834       Die(File, F, None)
835       raise
836    Done(File, F, None)
837
838 # Generate the BSMTP file
839 def GenBSMTP(File, HomePrefix):
840    F = None
841    try:
842       F = open(File + ".tmp", "w")
843      
844       # Fetch all the users
845       global DebianUsers
846      
847       # Write out the zone file entry for each user
848       for x in DebianUsers:
849          if x[1].has_key("dnsZoneEntry") == 0:
850             continue
851      
852          # If the account has no PGP key, do not write it
853          if x[1].has_key("keyFingerPrint") == 0:
854             continue
855          try:
856             for z in x[1]["dnsZoneEntry"]:
857                Split = z.lower().split()
858                if Split[1].lower() == 'in':
859                   for y in range(0, len(Split)):
860                      if Split[y] == "$":
861                         Split[y] = "\n\t"
862                   Line = " ".join(Split) + "\n"
863      
864                   Host = Split[0] + DNSZone
865                   if BSMTPCheck.match(Line) != None:
866                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
867                                   GetAttr(x, "uid"), HomePrefix, GetAttr(x, "uid"), Host))
868      
869          except:
870             F.write("; Errors\n")
871             pass
872   
873    # Oops, something unspeakable happened.
874    except:
875       Die(File, F, None)
876       raise
877    Done(File, F, None)
878   
879 #  cache IP adresses
880 HostToIPCache = {}
881 def HostToIP(Host):
882    global HostToIPCache
883    if not Host in HostToIPCache:
884       IPAdressesT = None
885       try:
886          IPAdressesT = list(set([ (a[0], a[4][0]) for a in socket.getaddrinfo(Host, None)]))
887       except socket.gaierror, (code):
888          if code[0] != -2:
889             raise
890       IPAdresses = []
891       if not IPAdressesT is None:
892          for addr in IPAdressesT:
893             if addr[0] == socket.AF_INET:
894                IPAdresses += [addr[1], "::ffff:"+addr[1]]
895             else:
896                IPAdresses += [addr[1]]
897       HostToIPCache[Host] = IPAdresses
898    return HostToIPCache[Host]
899
900 # Generate the ssh known hosts file
901 def GenSSHKnown(File, mode=None):
902    F = None
903    try:
904       OldMask = os.umask(0022)
905       F = open(File + ".tmp", "w", 0644)
906       os.umask(OldMask)
907      
908       global HostAttrs
909       if HostAttrs == None:
910          raise UDEmptyList, "No Hosts"
911      
912       for x in HostAttrs:
913          if x[1].has_key("hostname") == 0 or \
914             x[1].has_key("sshRSAHostKey") == 0:
915             continue
916          Host = GetAttr(x, "hostname")
917          HostNames = [ Host ]
918          if Host.endswith(HostDomain):
919             HostNames.append(Host[:-(len(HostDomain) + 1)])
920      
921          # in the purpose field [[host|some other text]] (where some other text is optional)
922          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
923          # file.  But so that we don't have to add everything we link we can add an asterisk
924          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
925          # http linking it we also support [[-hostname]] entries.
926          for i in x[1].get("purpose", []):
927             m = PurposeHostField.match(i)
928             if m:
929                m = m.group(1)
930                # we ignore [[*..]] entries
931                if m.startswith('*'):
932                   continue
933                if m.startswith('-'):
934                   m = m[1:]
935                if m:
936                   HostNames.append(m)
937                   if m.endswith(HostDomain):
938                      HostNames.append(m[:-(len(HostDomain) + 1)])
939      
940          for I in x[1]["sshRSAHostKey"]:
941             if mode and mode == 'authorized_keys':
942                #Line = 'command="rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,from="%s" %s' % (Host, ",".join(HNames + HostToIP(Host)), I)
943                Line = 'command="rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding %s' % (Host,I)
944             else:
945                Line = "%s %s" %(",".join(HostNames + HostToIP(Host)), I)
946             Line = Sanitize(Line) + "\n"
947             F.write(Line)
948    # Oops, something unspeakable happened.
949    except:
950       Die(File, F, None)
951       raise
952    Done(File, F, None)
953
954 # Generate the debianhosts file (list of all IP addresses)
955 def GenHosts(l, File):
956    F = None
957    try:
958       OldMask = os.umask(0022)
959       F = open(File + ".tmp", "w", 0644)
960       os.umask(OldMask)
961      
962       # Fetch all the hosts
963       hostnames = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "hostname=*",
964                              ["hostname"])
965      
966       if hostnames == None:
967          raise UDEmptyList, "No Hosts"
968      
969       seen = set()
970       for x in hostnames:
971          host = GetAttr(x, "hostname", None)
972          if host:
973             addrs = []
974             try:
975                addrs += socket.getaddrinfo(host, None, socket.AF_INET)
976             except socket.error:
977                pass
978             try:
979                addrs += socket.getaddrinfo(host, None, socket.AF_INET6)
980             except socket.error:
981                pass
982            
983             for addrinfo in addrs:
984                if addrinfo[0] in (socket.AF_INET, socket.AF_INET6):
985                   addr = addrinfo[4][0]
986                   if addr not in seen:
987                      print >> F, addrinfo[4][0]
988                      seen.add(addr)
989    # Oops, something unspeakable happened.
990    except:
991      Die(File, F, None)
992      raise
993    Done(File, F, None)
994
995 def GenKeyrings(OutDir):
996    for k in Keyrings:
997       shutil.copy(k, OutDir)
998
999
1000 # Connect to the ldap server
1001 l = connectLDAP()
1002 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1003 Pass = F.readline().strip().split(" ")
1004 F.close()
1005 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1006
1007 # Fetch all the groups
1008 GroupIDMap = {}
1009 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1010                   ["gid", "gidNumber", "subGroup"])
1011
1012 # Generate the SubGroupMap and GroupIDMap
1013 for x in Attrs:
1014    if x[1].has_key("gidNumber") == 0:
1015       continue
1016    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1017    if x[1].has_key("subGroup") != 0:
1018       SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1019
1020 # Fetch all the users
1021 PasswdAttrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "uid=*",\
1022                 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1023                  "gecos", "loginShell", "userPassword", "shadowLastChange",\
1024                  "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1025                  "shadowExpire", "emailForward", "latitude", "longitude",\
1026                  "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1027                  "keyFingerPrint", "privateSub", "mailDisableMessage",\
1028                  "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1029                  "mailWhitelist", "sudoPassword", "objectClass", "accountStatus"])
1030
1031 if PasswdAttrs is None:
1032    raise UDEmptyList, "No Users"
1033
1034 # Fetch all the hosts
1035 HostAttrs    = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "sshRSAHostKey=*",\
1036                 ["hostname", "sshRSAHostKey", "purpose"])
1037
1038 # Open the control file
1039 if len(sys.argv) == 1:
1040    F = open(GenerateConf, "r")
1041 else:
1042    F = open(sys.argv[1], "r")
1043
1044 # Generate global things
1045 GlobalDir = GenerateDir + "/"
1046 GenDisabledAccounts(GlobalDir + "disabled-accounts")
1047
1048 PasswdAttrs = filter(not IsRetired, PasswdAttrs)
1049 DebianUsers = filter(IsGidDebian, PasswdAttrs)
1050
1051 CheckForward()
1052
1053 GenMailDisable(GlobalDir + "mail-disable")
1054 GenAllForward(GlobalDir + "mail-forward.cdb")
1055 GenPrivate(GlobalDir + "debian-private")
1056 #GenSSHKnown(l,GlobalDir+"authorized_keys", 'authorized_keys')
1057 GenMailBool(GlobalDir + "mail-greylist", "mailGreylisting")
1058 GenMailBool(GlobalDir + "mail-callout", "mailCallout")
1059 GenMailList(GlobalDir + "mail-rbl", "mailRBL")
1060 GenMailList(GlobalDir + "mail-rhsbl", "mailRHSBL")
1061 GenMailList(GlobalDir + "mail-whitelist", "mailWhitelist")
1062 GenKeyrings(GlobalDir)
1063
1064 # Compatibility.
1065 GenForward(GlobalDir + "forward-alias")
1066
1067 PasswdAttrs = filter(lambda x: not x in DisabledUsers, PasswdAttrs)
1068
1069 SSHFiles = GenSSHShadow()
1070 GenMarkers(GlobalDir + "markers")
1071 GenSSHKnown(GlobalDir + "ssh_known_hosts")
1072 GenHosts(l, GlobalDir + "debianhosts")
1073
1074 while(1):
1075    Line = F.readline()
1076    if Line == "":
1077       break
1078    Line = Line.strip()
1079    if Line == "":
1080       continue
1081    if Line[0] == '#':
1082       continue
1083
1084    Split = Line.split(" ")
1085    OutDir = GenerateDir + '/' + Split[0] + '/'
1086    try:
1087       os.mkdir(OutDir)
1088    except: 
1089       pass
1090
1091    # Get the group list and convert any named groups to numerics
1092    GroupList = {}
1093    ExtraList = {}
1094    for I in Split[2:]:
1095       if I[0] == '[':
1096          ExtraList[I] = None
1097          continue
1098       GroupList[I] = None
1099       if GroupIDMap.has_key(I):
1100          GroupList[str(GroupIDMap[I])] = None
1101
1102    Allowed = GroupList
1103    if Allowed == {}:
1104      Allowed = None
1105    CurrentHost = Split[0]
1106
1107    DoLink(GlobalDir, OutDir, "debianhosts")
1108    DoLink(GlobalDir, OutDir, "ssh_known_hosts")
1109    DoLink(GlobalDir, OutDir, "disabled-accounts")
1110
1111    sys.stdout.flush()
1112    if ExtraList.has_key("[NOPASSWD]"):
1113       userlist = GenPasswd(OutDir + "passwd", Split[1], "*")
1114    else:
1115       userlist = GenPasswd(OutDir + "passwd", Split[1], "x")
1116    sys.stdout.flush()
1117    grouprevmap = GenGroup(OutDir + "group")
1118    GenShadowSudo(OutDir + "sudo-passwd", ExtraList.has_key("[UNTRUSTED]") or ExtraList.has_key("[NOPASSWD]"))
1119
1120    # Now we know who we're allowing on the machine, export
1121    # the relevant ssh keys
1122    GenSSHtarballs(userlist, SSHFiles, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
1123
1124    if ExtraList.has_key("[UNTRUSTED]"):
1125       print "[UNTRUSTED] tag is obsolete and may be removed in the future."
1126       continue
1127    if not ExtraList.has_key("[NOPASSWD]"):
1128       GenShadow(OutDir + "shadow")
1129
1130    # Link in global things
1131    if not ExtraList.has_key("[NOMARKERS]"):
1132       DoLink(GlobalDir, OutDir, "markers")
1133    DoLink(GlobalDir, OutDir, "mail-forward.cdb")
1134    DoLink(GlobalDir, OutDir, "mail-disable")
1135    DoLink(GlobalDir, OutDir, "mail-greylist")
1136    DoLink(GlobalDir, OutDir, "mail-callout")
1137    DoLink(GlobalDir, OutDir, "mail-rbl")
1138    DoLink(GlobalDir, OutDir, "mail-rhsbl")
1139    DoLink(GlobalDir, OutDir, "mail-whitelist")
1140
1141    # Compatibility.
1142    DoLink(GlobalDir, OutDir, "forward-alias")
1143
1144    if ExtraList.has_key("[DNS]"):
1145       GenDNS(OutDir + "dns-zone", Split[1])
1146       GenSSHFP(OutDir + "dns-sshfp", Split[1])
1147
1148    if ExtraList.has_key("[BSMTP]"):
1149       GenBSMTP(OutDir + "bsmtp", Split[1])
1150
1151    if ExtraList.has_key("[PRIVATE]"):
1152       DoLink(GlobalDir, OutDir, "debian-private")
1153
1154    if ExtraList.has_key("[KEYRING]"):
1155       for k in Keyrings:
1156         DoLink(GlobalDir, OutDir, os.path.basename(k))
1157    else:
1158       for k in Keyrings:
1159          try: 
1160             posix.remove(OutDir + os.path.basename(k))
1161          except:
1162             pass
1163
1164 # vim:set et:
1165 # vim:set ts=3:
1166 # vim:set shiftwidth=3: