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