633a5d9e2d59cb8f8b7379ed011c90e15634efe6
[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 DebianUsers = None
39 DisabledUsers = []
40 GroupIDMap = {}
41 SubGroupMap = {}
42 Allowed = None
43 CurrentHost = ""
44
45 UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
46
47 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$")
48 BSMTPCheck = re.compile(".*mx 0 (gluck|master)\.debian\.org\..*",re.DOTALL)
49 PurposeHostField = re.compile(r".*\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
50 DNSZone = ".debian.net"
51 Keyrings = ConfModule.sync_keyrings.split(":")
52
53 def safe_makedirs(dir):
54    try:
55       os.makedirs(dir)
56    except OSError, e:
57       if e.errno == errno.EEXIST:
58          pass
59       else:
60          raise e
61
62 def safe_rmtree(dir):
63    try:
64       shutil.rmtree(dir)
65    except OSError, e:
66       if e.errno == errno.ENOENT:
67          pass
68       else:
69          raise e
70
71 def Sanitize(Str):
72    return Str.translate(string.maketrans("\n\r\t", "$$$"))
73
74 def DoLink(From, To, File):
75    try: 
76       posix.remove(To + File)
77    except: 
78       pass
79    posix.link(From + File, To + File)
80
81 def IsRetired(DnRecord):
82    """
83    Looks for accountStatus in the LDAP record and tries to
84    match it against one of the known retired statuses
85    """
86
87    status = GetAttr(DnRecord, "accountStatus", None)
88    if status is None:
89       return False
90
91    line = status.split()
92    status = line[0]
93    
94    if status == "inactive":
95       return True
96
97    elif status == "memorial":
98       return True
99
100    elif status == "retiring":
101       # We'll give them a few extra days over what we said
102       age = 6 * 31 * 24 * 60 * 60
103       try:
104          return (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age
105       except IndexError:
106          return False
107       except ValueError:
108          return False
109
110    return False
111
112 def IsGidDebian(x):
113    try:
114       return int(GetAttr(x, "gidNumber", 0)) == 800
115    except ValueError:
116       return False
117
118 # See if this user is in the group list
119 def IsInGroup(DnRecord):
120   if Allowed is None:
121      return True
122
123   # See if the primary group is in the list
124   if Allowed.has_key(GetAttr(DnRecord, "gidNumber")) != 0:
125      return True
126
127   # Check the host based ACL
128   if DnRecord[1].has_key("allowedHost") != 0:
129      if CurrentHost in DnRecord[1]["allowedHost"]:
130         return True
131
132   # See if there are supplementary groups
133   if DnRecord[1].has_key("supplementaryGid") == 0:
134      return False
135
136   supgroups=[]
137   addGroups(supgroups, DnRecord[1]["supplementaryGid"], GetAttr(DnRecord, "uid"))
138   for g in supgroups:
139      if Allowed.has_key(g):
140         return True
141   return False
142
143 def Die(File, F, Fdb):
144    if F != None:
145       F.close()
146    if Fdb != None:
147       Fdb.close()
148    try: 
149       os.remove(File + ".tmp")
150    except:
151       pass
152    try: 
153       os.remove(File + ".tdb.tmp")
154    except: 
155       pass
156
157 def Done(File, F, Fdb):
158    if F != None:
159       F.close()
160       os.rename(File + ".tmp", File)
161    if Fdb != None:
162       Fdb.close()
163       os.rename(File + ".tdb.tmp", File + ".tdb")
164
165 # Generate the password list
166 def GenPasswd(File, HomePrefix, PwdMarker):
167    F = None
168    try:
169       F = open(File + ".tdb.tmp", "w")
170      
171       userlist = {}
172       # Fetch all the users
173       global PasswdAttrs
174      
175       I = 0
176       for x in PasswdAttrs:
177          if x[1].has_key("uidNumber") == 0 or not IsInGroup(x):
178             continue
179      
180          # Do not let people try to buffer overflow some busted passwd parser.
181          if len(GetAttr(x, "gecos")) > 100 or len(GetAttr(x, "loginShell")) > 50:
182             continue
183      
184          userlist[GetAttr(x, "uid")] = int(GetAttr(x, "gidNumber"))
185          Line = "%s:%s:%s:%s:%s:%s%s:%s" % (GetAttr(x, "uid"),\
186                  PwdMarker,\
187                  GetAttr(x, "uidNumber"), GetAttr(x, "gidNumber"),\
188                  GetAttr(x, "gecos"), HomePrefix, GetAttr(x, "uid"),\
189                  GetAttr(x, "loginShell"))
190      
191          Line = Sanitize(Line) + "\n"
192          F.write("0%u %s" % (I, Line))
193          F.write(".%s %s" % (GetAttr(x, "uid"), Line))
194          F.write("=%s %s" % (GetAttr(x, "uidNumber"), Line))
195          I = I + 1
196   
197    # Oops, something unspeakable happened.
198    except:
199       Die(File, None, F)
200       raise
201    Done(File, None, F)
202
203    # Return the list of users so we know which keys to export
204    return userlist
205
206 # Generate the shadow list
207 def GenShadow(File):
208    F = None
209    try:
210       OldMask = os.umask(0077)
211       F = open(File + ".tdb.tmp", "w", 0600)
212       os.umask(OldMask)
213      
214       # Fetch all the users
215       global PasswdAttrs
216      
217       I = 0
218       for x in PasswdAttrs:
219          if x[1].has_key("uidNumber") == 0 or not IsInGroup(x):
220             continue
221      
222          Pass = GetAttr(x, "userPassword")
223          if Pass[0:7] != "{crypt}" or len(Pass) > 50:
224             Pass = '*'
225          else:
226             Pass = Pass[7:]
227      
228          # If the account is locked, mark it as such in shadow
229          # See Debian Bug #308229 for why we set it to 1 instead of 0
230          if (GetAttr(x, "userPassword").find("*LK*") != -1) \
231              or GetAttr(x, "userPassword").startswith("!"):
232             ShadowExpire = '1'
233          else:
234             ShadowExpire = GetAttr(x, "shadowExpire")
235      
236          Line = "%s:%s:%s:%s:%s:%s:%s:%s:" % (GetAttr(x, "uid"),\
237                  Pass, GetAttr(x, "shadowLastChange"),\
238                  GetAttr(x, "shadowMin"), GetAttr(x, "shadowMax"),\
239                  GetAttr(x, "shadowWarning"), GetAttr(x, "shadowInactive"),\
240                  ShadowExpire)
241          Line = Sanitize(Line) + "\n"
242          F.write("0%u %s" % (I, Line))
243          F.write(".%s %s" % (GetAttr(x, "uid"), Line))
244          I = I + 1
245   
246    # Oops, something unspeakable happened.
247    except:
248       Die(File, None, F)
249       raise
250    Done(File, None, F)
251
252 # Generate the sudo passwd file
253 def GenShadowSudo(File, untrusted):
254    F = None
255    try:
256       OldMask = os.umask(0077)
257       F = open(File + ".tmp", "w", 0600)
258       os.umask(OldMask)
259      
260       # Fetch all the users
261       global PasswdAttrs
262      
263       for x in PasswdAttrs:
264          Pass = '*'
265          if x[1].has_key("uidNumber") == 0 or not IsInGroup(x):
266             continue
267      
268          if x[1].has_key('sudoPassword'):
269             for entry in x[1]['sudoPassword']:
270                Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
271                if Match == None:
272                   continue
273                uuid = Match.group(1)
274                status = Match.group(2)
275                hosts = Match.group(3)
276                cryptedpass = Match.group(4)
277      
278                if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', x[1]['uid'][0], uuid, hosts, cryptedpass):
279                   continue
280                for_all = hosts == "*"
281                for_this_host = CurrentHost in hosts.split(',')
282                if not (for_all or for_this_host):
283                   continue
284                # ignore * passwords for untrusted hosts, but copy host specific passwords
285                if for_all and untrusted:
286                   continue
287                Pass = cryptedpass
288                if for_this_host: # this makes sure we take a per-host entry over the for-all entry
289                   break
290             if len(Pass) > 50:
291                Pass = '*'
292      
293          Line = "%s:%s" % (GetAttr(x, "uid"), Pass)
294          Line = Sanitize(Line) + "\n"
295          F.write("%s" % (Line))
296   
297    # Oops, something unspeakable happened.
298    except:
299       Die(File, F, None)
300       raise
301    Done(File, F, None)
302
303 # Generate the shadow list
304 def GenSSHShadow():
305    # Fetch all the users
306    userfiles = []
307
308    global PasswdAttrs
309
310    safe_rmtree(os.path.join(GlobalDir, 'userkeys'))
311    safe_makedirs(os.path.join(GlobalDir, 'userkeys'))
312
313    for x in PasswdAttrs:
314
315       if x[1].has_key("uidNumber") == 0 or \
316          x[1].has_key("sshRSAAuthKey") == 0:
317          continue
318
319       User = GetAttr(x, "uid")
320       F = None
321
322       try:
323          OldMask = os.umask(0077)
324          File = os.path.join(GlobalDir, 'userkeys', User)
325          F = open(File + ".tmp", "w", 0600)
326          os.umask(OldMask)
327
328          for I in x[1]["sshRSAAuthKey"]:
329             MultipleLine = "%s" % I
330             MultipleLine = Sanitize(MultipleLine) + "\n"
331             F.write(MultipleLine)
332
333          Done(File, F, None)
334          userfiles.append(os.path.basename(File))
335
336       # Oops, something unspeakable happened.
337       except IOError:
338          Die(File, F, None)
339          Die(masterFileName, masterFile, None)
340          raise
341
342    return userfiles
343
344 def GenSSHtarballs(userlist, SSHFiles, grouprevmap, target):
345    OldMask = os.umask(0077)
346    tf = tarfile.open(name=os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
347    os.umask(OldMask)
348    for f in userlist.keys():
349       if f not in SSHFiles:
350          continue
351       # If we're not exporting their primary group, don't export
352       # the key and warn
353       grname = None
354       if userlist[f] in grouprevmap.keys():
355          grname = grouprevmap[userlist[f]]
356       else:
357          try:
358             if int(userlist[f]) <= 100:
359                # In these cases, look it up in the normal way so we
360                # deal with cases where, for instance, users are in group
361                # users as their primary group.
362                grname = grp.getgrgid(userlist[f])[0]
363          except Exception, e:
364             pass
365
366       if grname is None:
367          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])
368          continue
369
370       to = tf.gettarinfo(os.path.join(GlobalDir, 'userkeys', f), f)
371       # These will only be used where the username doesn't
372       # exist on the target system for some reason; hence,
373       # in those cases, the safest thing is for the file to
374       # be owned by root but group nobody.  This deals with
375       # the bloody obscure case where the group fails to exist
376       # whilst the user does (in which case we want to avoid
377       # ending up with a file which is owned user:root to avoid
378       # a fairly obvious attack vector)
379       to.uid = 0
380       to.gid = 65534
381       # Using the username / groupname fields avoids any need
382       # to give a shit^W^W^Wcare about the UIDoffset stuff.
383       to.uname = f
384       to.gname = grname
385       to.mode  = 0400
386       tf.addfile(to, file(os.path.join(GlobalDir, 'userkeys', f)))
387
388    tf.close()
389    os.rename(os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
390
391 # add a list of groups to existing groups,
392 # including all subgroups thereof, recursively.
393 # basically this proceduces the transitive hull of the groups in
394 # addgroups.
395 def addGroups(existingGroups, newGroups, uid):
396    for group in newGroups:
397       # if it's a <group>@host, split it and verify it's on the current host.
398       s = group.split('@', 1)
399       if len(s) == 2 and s[1] != CurrentHost:
400          continue
401       group = s[0]
402
403       # let's see if we handled this group already
404       if group in existingGroups:
405          continue
406
407       if not GroupIDMap.has_key(group):
408          print "Group", group, "does not exist but", uid, "is in it"
409          continue
410
411       existingGroups.append(group)
412
413       if SubGroupMap.has_key(group):
414          addGroups(existingGroups, SubGroupMap[group], uid)
415
416 # Generate the group list
417 def GenGroup(File):
418    grouprevmap = {}
419    F = None
420    try:
421       F = open(File + ".tdb.tmp", "w")
422      
423       # Generate the GroupMap
424       GroupMap = {}
425       for x in GroupIDMap.keys():
426          GroupMap[x] = []
427      
428       # Fetch all the users
429       global PasswdAttrs
430      
431       # Sort them into a list of groups having a set of users
432       for x in PasswdAttrs:
433          uid = GetAttr(x, "uid")
434          if x[1].has_key("uidNumber") == 0 or not IsInGroup(x):
435             continue
436          if x[1].has_key("supplementaryGid") == 0:
437             continue
438      
439          supgroups=[]
440          addGroups(supgroups, x[1]["supplementaryGid"], uid)
441          for g in supgroups:
442             GroupMap[g].append(uid)
443      
444       # Output the group file.
445       J = 0
446       for x in GroupMap.keys():
447          grouprevmap[GroupIDMap[x]] = x
448          if GroupIDMap.has_key(x) == 0:
449             continue
450          Line = "%s:x:%u:" % (x, GroupIDMap[x])
451          Comma = ''
452          for I in GroupMap[x]:
453             Line = Line + ("%s%s" % (Comma, I))
454             Comma = ','
455          Line = Sanitize(Line) + "\n"
456          F.write("0%u %s" % (J, Line))
457          F.write(".%s %s" % (x, Line))
458          F.write("=%u %s" % (GroupIDMap[x], Line))
459          J = J + 1
460   
461    # Oops, something unspeakable happened.
462    except:
463       Die(File, None, F)
464       raise
465    Done(File, None, F)
466   
467    return grouprevmap
468
469 def CheckForward():
470    global DebianUsers
471    for x in DebianUsers:
472       if x[1].has_key("emailForward") == 0:
473          continue
474    
475       if not IsInGroup(x):
476          x[1].pop("emailForward")
477          continue
478
479       # Do not allow people to try to buffer overflow busted parsers
480       if len(GetAttr(x, "emailForward")) > 200:
481          x[1].pop("emailForward")
482          continue
483
484       # Check the forwarding address
485       if EmailCheck.match(GetAttr(x, "emailForward")) == None:
486          x[1].pop("emailForward")
487
488 # Generate the email forwarding list
489 def GenForward(File):
490    F = None
491    try:
492       OldMask = os.umask(0022)
493       F = open(File + ".tmp", "w", 0644)
494       os.umask(OldMask)
495      
496       # Fetch all the users
497       global DebianUsers
498      
499       # Write out the email address for each user
500       for x in DebianUsers:
501          if x[1].has_key("emailForward") == 0:
502             continue
503      
504          Line = "%s: %s" % (GetAttr(x, "uid"), GetAttr(x, "emailForward"))
505          Line = Sanitize(Line) + "\n"
506          F.write(Line)
507   
508    # Oops, something unspeakable happened.
509    except:
510       Die(File, F, None)
511       raise
512    Done(File, F, None)
513
514 def GenAllForward(File):
515    Fdb = None
516    try:
517       OldMask = os.umask(0022)
518       Fdb = os.popen("cdbmake %s %s.tmp"%(File, File), "w")
519       os.umask(OldMask)
520      
521       # Fetch all the users
522       global DebianUsers
523      
524       # Write out the email address for each user
525       for x in DebianUsers:
526          if x[1].has_key("emailForward") == 0:
527             continue
528      
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             DisabledUsers.append(x)
622      
623          if Line != "":
624             F.write(Sanitize(Line) + "\n")
625      
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 is 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 is 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 # Connect to the ldap server
1000 l = connectLDAP()
1001 F = open(PassDir + "/pass-" + pwd.getpwuid(os.getuid())[0], "r")
1002 Pass = F.readline().strip().split(" ")
1003 F.close()
1004 l.simple_bind_s("uid=" + Pass[0] + "," + BaseDn, Pass[1])
1005
1006 # Fetch all the groups
1007 GroupIDMap = {}
1008 Attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
1009                   ["gid", "gidNumber", "subGroup"])
1010
1011 # Generate the SubGroupMap and GroupIDMap
1012 for x in Attrs:
1013    if x[1].has_key("gidNumber") == 0:
1014       continue
1015    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0])
1016    if x[1].has_key("subGroup") != 0:
1017       SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"])
1018
1019 # Fetch all the users
1020 PasswdAttrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "uid=*",\
1021                 ["uid", "uidNumber", "gidNumber", "supplementaryGid",\
1022                  "gecos", "loginShell", "userPassword", "shadowLastChange",\
1023                  "shadowMin", "shadowMax", "shadowWarning", "shadowInactive",
1024                  "shadowExpire", "emailForward", "latitude", "longitude",\
1025                  "allowedHost", "sshRSAAuthKey", "dnsZoneEntry", "cn", "sn",\
1026                  "keyFingerPrint", "privateSub", "mailDisableMessage",\
1027                  "mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
1028                  "mailWhitelist", "sudoPassword", "objectClass", "accountStatus"])
1029
1030 if PasswdAttrs is None:
1031    raise UDEmptyList, "No Users"
1032
1033 # Fetch all the hosts
1034 HostAttrs    = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "sshRSAHostKey=*",\
1035                 ["hostname", "sshRSAHostKey", "purpose"])
1036
1037 # Open the control file
1038 if len(sys.argv) == 1:
1039    F = open(GenerateConf, "r")
1040 else:
1041    F = open(sys.argv[1], "r")
1042
1043 # Generate global things
1044 GlobalDir = GenerateDir + "/"
1045 GenDisabledAccounts(GlobalDir + "disabled-accounts")
1046
1047 PasswdAttrs = filter(lambda x: not IsRetired(x), PasswdAttrs)
1048 #DebianUsers = filter(lambda x: IsGidDebian(x), PasswdAttrs)
1049 DebianUsers = 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: