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