Merge changes back in
[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)\.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          # Do not allow people to try to buffer overflow busted parsers
530          Forward = GetAttr(x, "emailForward")
531      
532          User = GetAttr(x, "uid")
533          Fdb.write("+%d,%d:%s->%s\n" % (len(User), len(Forward), User, Forward))
534   
535       Fdb.write("\n")
536    # Oops, something unspeakable happened.
537    except:
538       Fdb.close()
539       raise
540    if Fdb.close() != None:
541       raise "cdbmake gave an error"
542
543 # Generate the anon XEarth marker file
544 def GenMarkers(File):
545    F = None
546    try:
547       F = open(File + ".tmp", "w")
548      
549       # Fetch all the users
550       global DebianUsers
551      
552       # Write out the position for each user
553       for x in DebianUsers:
554          if x[1].has_key("latitude") == 0 or x[1].has_key("longitude") == 0:
555             continue
556          try:
557             Line = "%8s %8s \"\""%(DecDegree(GetAttr(x, "latitude"), 1), DecDegree(GetAttr(x, "longitude"), 1))
558             Line = Sanitize(Line) + "\n"
559             F.write(Line)
560          except:
561             pass
562   
563    # Oops, something unspeakable happened.
564    except:
565       Die(File, F, None)
566       raise
567    Done(File, F, None)
568
569 # Generate the debian-private subscription list
570 def GenPrivate(File):
571    F = None
572    try:
573       F = open(File + ".tmp", "w")
574      
575       # Fetch all the users
576       global DebianUsers
577      
578       # Write out the position for each user
579       for x in DebianUsers:
580          if x[1].has_key("privateSub") == 0:
581             continue
582      
583          # If the account has no PGP key, do not write it
584          if x[1].has_key("keyFingerPrint") == 0:
585             continue
586      
587          try:
588             Line = "%s"%(GetAttr(x, "privateSub"))
589             Line = Sanitize(Line) + "\n"
590             F.write(Line)
591          except:
592             pass
593   
594    # Oops, something unspeakable happened.
595    except:
596       Die(File, F, None)
597       raise
598    Done(File, F, None)
599
600 # Generate a list of locked accounts
601 def GenDisabledAccounts(File):
602    F = None
603    try:
604       F = open(File + ".tmp", "w")
605      
606       # Fetch all the users
607       global PasswdAttrs
608       global DisabledUsers
609      
610       I = 0
611       for x in PasswdAttrs:
612          if x[1].has_key("uidNumber") == 0:
613             continue
614      
615          Pass = GetAttr(x, "userPassword")
616          Line = ""
617          # *LK* is the reference value for a locked account
618          # password starting with ! is also a locked account
619          if Pass.find("*LK*") != -1 or Pass.startswith("!"):
620             # Format is <login>:<reason>
621             Line = "%s:%s" % (GetAttr(x, "uid"), "Account is locked")
622      
623          if Line != "":
624             F.write(Sanitize(Line) + "\n")
625      
626          DisabledUsers.append(x)
627    
628    # Oops, something unspeakable happened.
629    except:
630       Die(File, F, None)
631       raise
632    Done(File, F, None)
633
634 # Generate the list of local addresses that refuse all mail
635 def GenMailDisable(File):
636    F = None
637    try:
638       F = open(File + ".tmp", "w")
639      
640       # Fetch all the users
641       global DebianUsers
642      
643       for x in DebianUsers:
644          Reason = None
645      
646          if x[1].has_key("mailDisableMessage"):
647             Reason = GetAttr(x, "mailDisableMessage")
648          else:
649             continue
650      
651          try:
652             Line = "%s: %s"%(GetAttr(x, "uid"), Reason)
653             Line = Sanitize(Line) + "\n"
654             F.write(Line)
655          except:
656             pass
657   
658    # Oops, something unspeakable happened.
659    except:
660       Die(File, F, None)
661       raise
662    Done(File, F, None)
663
664 # Generate a list of uids that should have boolean affects applied
665 def GenMailBool(File, Key):
666    F = None
667    try:
668       F = open(File + ".tmp", "w")
669      
670       # Fetch all the users
671       global DebianUsers
672      
673       for x in DebianUsers:
674          Reason = None
675      
676          if x[1].has_key(Key) == 0:
677             continue
678      
679          if GetAttr(x, Key) != "TRUE":
680             continue
681      
682          try:
683             Line = "%s"%(GetAttr(x, "uid"))
684             Line = Sanitize(Line) + "\n"
685             F.write(Line)
686          except:
687             pass
688   
689    # Oops, something unspeakable happened.
690    except:
691       Die(File, F, None)
692       raise
693    Done(File, F, None)
694
695 # Generate a list of hosts for RBL or whitelist purposes.
696 def GenMailList(File, Key):
697    F = None
698    try:
699       F = open(File + ".tmp", "w")
700      
701       # Fetch all the users
702       global DebianUsers
703      
704       for x in DebianUsers:
705          Reason = None
706      
707          if x[1].has_key(Key) == 0:
708             continue
709      
710          try:
711             found = 0
712             Line = None
713             for z in x[1][Key]:
714                 if Key == "mailWhitelist":
715                    if re.match('^[-\w.]+(/[\d]+)?$', z) == None:
716                       continue
717                 else:
718                    if re.match('^[-\w.]+$', z) == None:
719                       continue
720                 if found == 0:
721                    found = 1
722                    Line = GetAttr(x, "uid")
723                 else:
724                     Line += " "
725                 Line += ": " + z
726                 if Key == "mailRHSBL":
727                    Line += "/$sender_address_domain"
728      
729             if Line != None:
730                Line = Sanitize(Line) + "\n"
731                F.write(Line)
732          except:
733             pass
734   
735    # Oops, something unspeakable happened.
736    except:
737       Die(File, F, None)
738       raise
739    Done(File, F, None)
740
741 def isRoleAccount(pwEntry):
742    if not pwEntry.has_key("objectClass"):
743       raise "pwEntry has no objectClass"
744    oc =  pwEntry['objectClass']
745    try:
746       i = oc.index('debianRoleAccount')
747       return True
748    except ValueError:
749       return False
750
751 # Generate the DNS Zone file
752 def GenDNS(File, HomePrefix):
753    F = None
754    try:
755       F = open(File + ".tmp", "w")
756      
757       # Fetch all the users
758       global PasswdAttrs
759      
760       # Write out the zone file entry for each user
761       for x in PasswdAttrs:
762          if x[1].has_key("dnsZoneEntry") == 0:
763             continue
764      
765          # If the account has no PGP key, do not write it
766          if x[1].has_key("keyFingerPrint") == 0 and not isRoleAccount(x[1]):
767             continue
768          try:
769             F.write("; %s\n"%(EmailAddress(x)))
770             for z in x[1]["dnsZoneEntry"]:
771                Split = z.lower().split()
772                if Split[1].lower() == 'in':
773                   for y in range(0, len(Split)):
774                      if Split[y] == "$":
775                         Split[y] = "\n\t"
776                   Line = " ".join(Split) + "\n"
777                   F.write(Line)
778      
779                   Host = Split[0] + DNSZone
780                   if BSMTPCheck.match(Line) != None:
781                      F.write("; Has BSMTP\n")
782      
783                   # Write some identification information
784                   if Split[2].lower() == "a":
785                      Line = "%s IN TXT \"%s\"\n"%(Split[0], EmailAddress(x))
786                      for y in x[1]["keyFingerPrint"]:
787                         Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0], FormatPGPKey(y))
788                      F.write(Line)
789                else:
790                   Line = "; Err %s"%(str(Split))
791                   F.write(Line)
792      
793             F.write("\n")
794          except:
795             F.write("; Errors\n")
796             pass
797   
798    # Oops, something unspeakable happened.
799    except:
800       Die(File, F, None)
801       raise
802    Done(File, F, None)
803
804 # Generate the DNS SSHFP records
805 def GenSSHFP(File, HomePrefix):
806    F = None
807    try:
808       F = open(File + ".tmp", "w")
809      
810       # Fetch all the hosts
811       global HostAttrs
812       if HostAttrs == None:
813          raise UDEmptyList, "No Hosts"
814      
815       for x in HostAttrs:
816          if x[1].has_key("hostname") == 0 or \
817             x[1].has_key("sshRSAHostKey") == 0:
818             continue
819          Host = GetAttr(x, "hostname")
820          Algorithm = None
821          for I in x[1]["sshRSAHostKey"]:
822             Split = I.split()
823             if Split[0] == 'ssh-rsa':
824                Algorithm = 1
825             if Split[0] == 'ssh-dss':
826                Algorithm = 2
827             if Algorithm == None:
828                continue
829             Fingerprint = sha.new(base64.decodestring(Split[1])).hexdigest()
830             Line = "%s. IN SSHFP %u 1 %s" % (Host, Algorithm, Fingerprint)
831             Line = Sanitize(Line) + "\n"
832             F.write(Line)
833    # Oops, something unspeakable happened.
834    except:
835       Die(File, F, None)
836       raise
837    Done(File, F, None)
838
839 # Generate the BSMTP file
840 def GenBSMTP(File, HomePrefix):
841    F = None
842    try:
843       F = open(File + ".tmp", "w")
844      
845       # Fetch all the users
846       global DebianUsers
847      
848       # Write out the zone file entry for each user
849       for x in DebianUsers:
850          if x[1].has_key("dnsZoneEntry") == 0:
851             continue
852      
853          # If the account has no PGP key, do not write it
854          if x[1].has_key("keyFingerPrint") == 0:
855             continue
856          try:
857             for z in x[1]["dnsZoneEntry"]:
858                Split = z.lower().split()
859                if Split[1].lower() == 'in':
860                   for y in range(0, len(Split)):
861                      if Split[y] == "$":
862                         Split[y] = "\n\t"
863                   Line = " ".join(Split) + "\n"
864      
865                   Host = Split[0] + DNSZone
866                   if BSMTPCheck.match(Line) != None:
867                       F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
868                                   GetAttr(x, "uid"), HomePrefix, GetAttr(x, "uid"), Host))
869      
870          except:
871             F.write("; Errors\n")
872             pass
873   
874    # Oops, something unspeakable happened.
875    except:
876       Die(File, F, None)
877       raise
878    Done(File, F, None)
879   
880 #  cache IP adresses
881 HostToIPCache = {}
882 def HostToIP(Host):
883    global HostToIPCache
884    if not Host in HostToIPCache:
885       IPAdressesT = None
886       try:
887          IPAdressesT = list(set([ (a[0], a[4][0]) for a in socket.getaddrinfo(Host, None)]))
888       except socket.gaierror, (code):
889          if code[0] != -2:
890             raise
891       IPAdresses = []
892       if not IPAdressesT is None:
893          for addr in IPAdressesT:
894             if addr[0] == socket.AF_INET:
895                IPAdresses += [addr[1], "::ffff:"+addr[1]]
896             else:
897                IPAdresses += [addr[1]]
898       HostToIPCache[Host] = IPAdresses
899    return HostToIPCache[Host]
900
901 # Generate the ssh known hosts file
902 def GenSSHKnown(File, mode=None):
903    F = None
904    try:
905       OldMask = os.umask(0022)
906       F = open(File + ".tmp", "w", 0644)
907       os.umask(OldMask)
908      
909       global HostAttrs
910       if HostAttrs is None:
911          raise UDEmptyList, "No Hosts"
912      
913       for x in HostAttrs:
914          if x[1].has_key("hostname") == 0 or \
915             x[1].has_key("sshRSAHostKey") == 0:
916             continue
917          Host = GetAttr(x, "hostname")
918          HostNames = [ Host ]
919          if Host.endswith(HostDomain):
920             HostNames.append(Host[:-(len(HostDomain) + 1)])
921      
922          # in the purpose field [[host|some other text]] (where some other text is optional)
923          # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
924          # file.  But so that we don't have to add everything we link we can add an asterisk
925          # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
926          # http linking it we also support [[-hostname]] entries.
927          for i in x[1].get("purpose", []):
928             m = PurposeHostField.match(i)
929             if m:
930                m = m.group(1)
931                # we ignore [[*..]] entries
932                if m.startswith('*'):
933                   continue
934                if m.startswith('-'):
935                   m = m[1:]
936                if m:
937                   HostNames.append(m)
938                   if m.endswith(HostDomain):
939                      HostNames.append(m[:-(len(HostDomain) + 1)])
940      
941          for I in x[1]["sshRSAHostKey"]:
942             if mode and mode == 'authorized_keys':
943                #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)
944                Line = 'command="rsync --server --sender -pr . /var/cache/userdir-ldap/hosts/%s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding %s' % (Host,I)
945             else:
946                Line = "%s %s" %(",".join(HostNames + HostToIP(Host)), I)
947             Line = Sanitize(Line) + "\n"
948             F.write(Line)
949    # Oops, something unspeakable happened.
950    except:
951       Die(File, F, None)
952       raise
953    Done(File, F, None)
954
955 # Generate the debianhosts file (list of all IP addresses)
956 def GenHosts(l, File):
957    F = None
958    try:
959       OldMask = os.umask(0022)
960       F = open(File + ".tmp", "w", 0644)
961       os.umask(OldMask)
962      
963       # Fetch all the hosts
964       hostnames = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "hostname=*",
965                              ["hostname"])
966      
967       if hostnames is None:
968          raise UDEmptyList, "No Hosts"
969      
970       seen = set()
971       for x in hostnames:
972          host = GetAttr(x, "hostname", None)
973          if host:
974             addrs = []
975             try:
976                addrs += socket.getaddrinfo(host, None, socket.AF_INET)
977             except socket.error:
978                pass
979             try:
980                addrs += socket.getaddrinfo(host, None, socket.AF_INET6)
981             except socket.error:
982                pass
983            
984             for addrinfo in addrs:
985                if addrinfo[0] in (socket.AF_INET, socket.AF_INET6):
986                   addr = addrinfo[4][0]
987                   if addr not in seen:
988                      print >> F, addrinfo[4][0]
989                      seen.add(addr)
990    # Oops, something unspeakable happened.
991    except:
992       Die(File, F, None)
993       raise
994    Done(File, F, None)
995
996 def GenKeyrings(OutDir):
997    for k in Keyrings:
998       shutil.copy(k, OutDir)
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: