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