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