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