Moving away from string exceptions
[mirror/userdir-ldap.git] / ud-generate
index 17e54b3..8001012 100755 (executable)
@@ -9,6 +9,8 @@
 #   Copyright (c) 2008 Peter Palfrader <peter@palfrader.org>
 #   Copyright (c) 2008 Andreas Barth <aba@not.so.argh.org>
 #   Copyright (c) 2008 Mark Hymers <mhy@debian.org>
+#   Copyright (c) 2008 Luk Claes <luk@debian.org>
+#   Copyright (c) 2008 Thomas Viehmann <tv@beamnet.de>
 #
 #   This program is free software; you can redistribute it and/or modify
 #   it under the terms of the GNU General Public License as published by
 
 import string, re, time, ldap, getopt, sys, os, pwd, posix, socket, base64, sha, shutil, errno, tarfile, grp
 from userdir_ldap import *;
+from userdir_exceptions import *
 
 global Allowed;
 global CurrentHost;
 
 PasswdAttrs = None;
+DisabledUsers = []
+RetiredUsers = []
 GroupIDMap = {};
+SubGroupMap = {};
 Allowed = None;
 CurrentHost = "";
 
+UUID_FORMAT = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
+
 EmailCheck = re.compile("^([^ <>@]+@[^ ,<>@]+)?$");
 BSMTPCheck = re.compile(".*mx 0 (gluck)\.debian\.org\..*",re.DOTALL);
+PurposeHostField = re.compile(r"\[\[([\*\-]?[a-z0-9.\-]*)(?:\|.*)?\]\]")
 DNSZone = ".debian.net"
 Keyrings = ConfModule.sync_keyrings.split(":")
 
@@ -66,6 +75,36 @@ def DoLink(From,To,File):
    except: pass;
    posix.link(From+File,To+File);
 
+def IsRetired(DnRecord):
+   """
+   Looks for accountStatus in the LDAP record and tries to
+   match it against one of the known retired statuses
+   """
+
+   status = GetAttr(DnRecord,"accountStatus", None)
+   if status is None:
+      return False
+
+   line = status.split()
+   status = line[0]
+   
+   if status == "inactive":
+      return True
+
+   elif status == "memorial":
+      return True
+
+   elif status == "retiring":
+      # We'll give them a few extra days over what we said
+      age = 6 * 31 * 24 * 60 * 60
+      try:
+         if (time.time() - time.mktime(time.strptime(line[1], "%Y-%m-%d"))) > age:
+            return True
+      except IndexError:
+         return False
+
+   return False
+
 # See if this user is in the group list
 def IsInGroup(DnRecord):
   if Allowed == None:
@@ -85,9 +124,10 @@ def IsInGroup(DnRecord):
   if DnRecord[1].has_key("supplementaryGid") == 0:
      return 0;
 
-  # Check the supplementary groups
-  for I in DnRecord[1]["supplementaryGid"]:
-     if Allowed.has_key(I):
+  supgroups=[]
+  addGroups(supgroups, DnRecord[1]["supplementaryGid"], GetAttr(DnRecord,"uid"))
+  for g in supgroups:
+     if Allowed.has_key(g):
         return 1;
   return 0;
 
@@ -108,7 +148,7 @@ def Done(File,F,Fdb):
   if Fdb != None:
     Fdb.close();
     os.rename(File + ".tdb.tmp",File+".tdb");
-  
+
 # Generate the password list
 def GenPasswd(l,File,HomePrefix,PwdMarker):
   F = None;
@@ -118,8 +158,6 @@ def GenPasswd(l,File,HomePrefix,PwdMarker):
    userlist = {}
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    I = 0;
    for x in PasswdAttrs:
@@ -162,14 +200,12 @@ def GenShadow(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    I = 0;
    for x in PasswdAttrs:
       if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
          continue;
-        
+
       Pass = GetAttr(x,"userPassword");
       if Pass[0:7] != "{crypt}" or len(Pass) > 50:
          Pass = '*';
@@ -200,66 +236,93 @@ def GenShadow(l,File):
    raise;
   Done(File,None,F);
 
+# Generate the sudo passwd file
+def GenShadowSudo(l,File, untrusted):
+  F = None;
+  try:
+   OldMask = os.umask(0077);
+   F = open(File + ".tmp","w",0600);
+   os.umask(OldMask);
+
+   # Fetch all the users
+   global PasswdAttrs;
+
+   for x in PasswdAttrs:
+      Pass = '*'
+      if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
+         continue;
+
+      if x[1].has_key('sudoPassword'):
+         for entry in x[1]['sudoPassword']:
+            Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
+            if Match == None:
+               continue
+            uuid = Match.group(1)
+            status = Match.group(2)
+            hosts = Match.group(3)
+            cryptedpass = Match.group(4)
+
+            if status != 'confirmed:'+make_passwd_hmac('password-is-confirmed', 'sudo', x[1]['uid'][0], uuid, hosts, cryptedpass):
+               continue
+            for_all = hosts == "*"
+            for_this_host = CurrentHost in hosts.split(',')
+            if not (for_all or for_this_host):
+               continue
+            # ignore * passwords for untrusted hosts, but copy host specific passwords
+            if for_all and untrusted:
+               continue
+            Pass = cryptedpass
+            if for_this_host: # this makes sure we take a per-host entry over the for-all entry
+              break
+         if len(Pass) > 50:
+            Pass = '*'
+
+      Line = "%s:%s" % (GetAttr(x,"uid"), Pass)
+      Line = Sanitize(Line) + "\n";
+      F.write("%s" % (Line));
+
+  # Oops, something unspeakable happened.
+  except:
+   Die(File,F,None);
+   raise;
+  Done(File,F,None);
+
 # Generate the shadow list
-def GenSSHShadow(l,masterFileName):
+def GenSSHShadow(l):
    # Fetch all the users
    singlefile = None
    userfiles = []
-   # Depending on config, we write out either a single file,
-   # multiple files, or both
-   if SingleSSHFile:
-       try:
-           OldMask = os.umask(0077);
-           masterFile = open(masterFileName + ".tmp","w",0600);
-           os.umask(OldMask);
-       except IOError:
-           Die(masterFileName,masterFile,None)
-           raise
 
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
-
-   # If we're going to be dealing with multiple keys, empty the
-   # directory before we start to avoid old keys hanging around
-   if MultipleSSHFiles:
-      safe_rmtree(os.path.join(GlobalDir, 'userkeys'))
-      safe_makedirs(os.path.join(GlobalDir, 'userkeys'))
-      
+
+   safe_rmtree(os.path.join(GlobalDir, 'userkeys'))
+   safe_makedirs(os.path.join(GlobalDir, 'userkeys'))
+
    for x in PasswdAttrs:
-      # If the account is locked, do not write it.
-      # This is a partial stop-gap. The ssh also needs to change this
-      # to ignore ~/.ssh/authorized* files.
-      if (GetAttr(x,"userPassword").find("*LK*") != -1) \
-             or GetAttr(x,"userPassword").startswith("!"):
-         continue;
+
+      if x in DisabledUsers:
+         continue
 
       if x[1].has_key("uidNumber") == 0 or \
          x[1].has_key("sshRSAAuthKey") == 0:
          continue;
+
       User = GetAttr(x,"uid");
       F = None;
 
       try:
-         if MultipleSSHFiles:
-             OldMask = os.umask(0077);
-             File = os.path.join(GlobalDir, 'userkeys', User)
-             F = open(File + ".tmp","w",0600);
-             os.umask(OldMask);
+         OldMask = os.umask(0077);
+         File = os.path.join(GlobalDir, 'userkeys', User)
+         F = open(File + ".tmp","w",0600);
+         os.umask(OldMask);
 
          for I in x[1]["sshRSAAuthKey"]:
-             if MultipleSSHFiles:
-                 MultipleLine = "%s" % I
-                 MultipleLine = Sanitize(MultipleLine) + "\n"
-                 F.write(MultipleLine)
-             if SingleSSHFile:
-                 SingleLine = "%s: %s" % (User, I)
-                 SingleLine = Sanitize(SingleLine) + "\n"
-                 masterFile.write(SingleLine)
-
-         if MultipleSSHFiles:
-             Done(File,F,None);
-             userfiles.append(os.path.basename(File))
+            MultipleLine = "%s" % I
+            MultipleLine = Sanitize(MultipleLine) + "\n"
+            F.write(MultipleLine)
+
+         Done(File,F,None);
+         userfiles.append(os.path.basename(File))
 
       # Oops, something unspeakable happened.
       except IOError:
@@ -267,11 +330,79 @@ def GenSSHShadow(l,masterFileName):
           Die(masterFileName,masterFile,None)
           raise;
 
-   if SingleSSHFile:
-       Done(masterFileName,masterFile,None)
-       singlefile = os.path.basename(masterFileName)
+   return userfiles
+
+def GenSSHtarballs(userlist, SSHFiles, grouprevmap, target):
+   OldMask = os.umask(0077);
+   tf = tarfile.open(name=os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
+   os.umask(OldMask);
+   for f in userlist.keys():
+      if f not in SSHFiles:
+         continue
+      # If we're not exporting their primary group, don't export
+      # the key and warn
+      grname = None
+      if userlist[f] in grouprevmap.keys():
+         grname = grouprevmap[userlist[f]]
+      else:
+         try:
+            if int(userlist[f]) <= 100:
+               # In these cases, look it up in the normal way so we
+               # deal with cases where, for instance, users are in group
+               # users as their primary group.
+               grname = grp.getgrgid(userlist[f])[0]
+         except Exception, e:
+            pass
+
+      if grname is None:
+         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])
+         continue
+
+      to = tf.gettarinfo(os.path.join(GlobalDir, 'userkeys', f), f)
+      # These will only be used where the username doesn't
+      # exist on the target system for some reason; hence,
+      # in those cases, the safest thing is for the file to
+      # be owned by root but group nobody.  This deals with
+      # the bloody obscure case where the group fails to exist
+      # whilst the user does (in which case we want to avoid
+      # ending up with a file which is owned user:root to avoid
+      # a fairly obvious attack vector)
+      to.uid = 0
+      to.gid = 65534
+      # Using the username / groupname fields avoids any need
+      # to give a shit^W^W^Wcare about the UIDoffset stuff.
+      to.uname = f
+      to.gname = grname
+      to.mode  = 0400
+      tf.addfile(to, file(os.path.join(GlobalDir, 'userkeys', f)))
+
+   tf.close()
+   os.rename(os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), target)
+
+# add a list of groups to existing groups,
+# including all subgroups thereof, recursively.
+# basically this proceduces the transitive hull of the groups in
+# addgroups.
+def addGroups(existingGroups, newGroups, uid):
+   for group in newGroups:
+      # if it's a <group>@host, split it and verify it's on the current host.
+      s = group.split('@', 1)
+      if len(s) == 2 and s[1] != CurrentHost:
+         continue;
+      group = s[0]
+
+      # let's see if we handled this group already
+      if group in existingGroups:
+        continue
+
+      if not GroupIDMap.has_key(group):
+         print "Group", group, "does not exist but", uid, "is in it"
+         continue
 
-   return singlefile, userfiles
+      existingGroups.append(group)
+
+      if SubGroupMap.has_key(group):
+         addGroups(existingGroups, SubGroupMap[group], uid)
 
 # Generate the group list
 def GenGroup(l,File):
@@ -284,25 +415,23 @@ def GenGroup(l,File):
    GroupMap = {};
    for x in GroupIDMap.keys():
       GroupMap[x] = [];
-      
+
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    # Sort them into a list of groups having a set of users
    for x in PasswdAttrs:
+      uid = GetAttr(x,"uid")
       if x[1].has_key("uidNumber") == 0 or IsInGroup(x) == 0:
          continue;
       if x[1].has_key("supplementaryGid") == 0:
          continue;
-        
-      for I in x[1]["supplementaryGid"]:
-         if GroupMap.has_key(I):
-           GroupMap[I].append(GetAttr(x,"uid"));
-        else:
-            print "Group does not exist ",I,"but",GetAttr(x,"uid"),"is in it";
-           
+
+      supgroups=[]
+      addGroups(supgroups, x[1]["supplementaryGid"], uid)
+      for g in supgroups:
+         GroupMap[g].append(uid);
+
    # Output the group file.
    J = 0;
    for x in GroupMap.keys():
@@ -319,7 +448,7 @@ def GenGroup(l,File):
       F.write(".%s %s" % (x,Line));
       F.write("=%u %s" % (GroupIDMap[x],Line));
       J = J + 1;
-      
+
   # Oops, something unspeakable happened.
   except:
    Die(File,None,F);
@@ -338,14 +467,12 @@ def GenForward(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    # Write out the email address for each user
    for x in PasswdAttrs:
       if x[1].has_key("emailForward") == 0 or IsInGroup(x) == 0:
          continue;
-      
+
       # Do not allow people to try to buffer overflow busted parsers
       if len(GetAttr(x,"emailForward")) > 200:
          continue;
@@ -356,7 +483,7 @@ def GenForward(l,File):
       Line = "%s: %s" % (GetAttr(x,"uid"),GetAttr(x,"emailForward"));
       Line = Sanitize(Line) + "\n";
       F.write(Line);
-      
+
   # Oops, something unspeakable happened.
   except:
    Die(File,F,None);
@@ -372,14 +499,12 @@ def GenAllForward(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    # Write out the email address for each user
    for x in PasswdAttrs:
       if x[1].has_key("emailForward") == 0:
          continue;
-      
+
       # Do not allow people to try to buffer overflow busted parsers
       Forward = GetAttr(x,"emailForward");
       if len(Forward) > 200:
@@ -388,7 +513,7 @@ def GenAllForward(l,File):
       # Check the forwarding address
       if EmailCheck.match(Forward) == None:
          continue;
-        
+
       User = GetAttr(x,"uid");
       Fdb.write("+%d,%d:%s->%s\n"%(len(User),len(Forward),User,Forward));
    Fdb.write("\n");
@@ -399,7 +524,7 @@ def GenAllForward(l,File):
   if Fdb.close() != None:
     raise "cdbmake gave an error";
 
-# Generate the anon XEarth marker file 
+# Generate the anon XEarth marker file
 def GenMarkers(l,File):
   F = None;
   try:
@@ -407,20 +532,18 @@ def GenMarkers(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    # Write out the position for each user
    for x in PasswdAttrs:
       if x[1].has_key("latitude") == 0 or x[1].has_key("longitude") == 0:
-         continue;      
+         continue;
       try:
          Line = "%8s %8s \"\""%(DecDegree(GetAttr(x,"latitude"),1),DecDegree(GetAttr(x,"longitude"),1));
          Line = Sanitize(Line) + "\n";
          F.write(Line);
       except:
          pass;
-      
+
   # Oops, something unspeakable happened.
   except:
    Die(File,F,None);
@@ -435,26 +558,19 @@ def GenPrivate(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    # Write out the position for each user
    for x in PasswdAttrs:
       if x[1].has_key("privateSub") == 0:
          continue;
 
-      # If the account is locked, do not write it
-      if (GetAttr(x,"userPassword").find("*LK*") != -1) \
-             or GetAttr(x,"userPassword").startswith("!"):
-         continue;
-
       # If the account has no PGP key, do not write it
       if x[1].has_key("keyFingerPrint") == 0:
          continue;
 
       # Must be in the Debian group (yuk, hard coded for now)
       if GetAttr(x,"gidNumber") != "800":
-        continue;
+         continue;
 
       try:
          Line = "%s"%(GetAttr(x,"privateSub"));
@@ -462,7 +578,7 @@ def GenPrivate(l,File):
          F.write(Line);
       except:
          pass;
-      
+
   # Oops, something unspeakable happened.
   except:
    Die(File,F,None);
@@ -477,24 +593,25 @@ def GenDisabledAccounts(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
+   global DisabledUsers
 
    I = 0;
    for x in PasswdAttrs:
       if x[1].has_key("uidNumber") == 0:
          continue;
-        
+
       Pass = GetAttr(x,"userPassword");
       Line = ""
       # *LK* is the reference value for a locked account
       # password starting with ! is also a locked account
       if Pass.find("*LK*") != -1 or Pass.startswith("!"):
-        # Format is <login>:<reason>
-        Line = "%s:%s" % (GetAttr(x,"uid"), "Account is locked")
+         # Format is <login>:<reason>
+         Line = "%s:%s" % (GetAttr(x,"uid"), "Account is locked")
 
       if Line != "":
-        F.write(Sanitize(Line) + "\n")
+         F.write(Sanitize(Line) + "\n")
+
+      DisabledUsers.append(x)
 
   # Oops, something unspeakable happened.
   except:
@@ -510,8 +627,6 @@ def GenMailDisable(l,File):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    for x in PasswdAttrs:
       Reason = None
@@ -523,7 +638,7 @@ def GenMailDisable(l,File):
 
       # Must be in the Debian group (yuk, hard coded for now)
       if GetAttr(x,"gidNumber") != "800":
-        continue;
+         continue;
 
       try:
          Line = "%s: %s"%(GetAttr(x,"uid"),Reason);
@@ -531,7 +646,7 @@ def GenMailDisable(l,File):
          F.write(Line);
       except:
          pass;
-      
+
   # Oops, something unspeakable happened.
   except:
    Die(File,F,None);
@@ -546,18 +661,16 @@ def GenMailBool(l,File,Key):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    for x in PasswdAttrs:
       Reason = None
-      
+
       if x[1].has_key(Key) == 0:
          continue
 
       # Must be in the Debian group (yuk, hard coded for now)
       if GetAttr(x,"gidNumber") != "800":
-        continue
+         continue
 
       if GetAttr(x,Key) != "TRUE":
          continue
@@ -568,7 +681,7 @@ def GenMailBool(l,File,Key):
          F.write(Line);
       except:
          pass;
-      
+
   # Oops, something unspeakable happened.
   except:
    Die(File,F,None);
@@ -583,29 +696,27 @@ def GenMailList(l,File,Key):
 
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    for x in PasswdAttrs:
       Reason = None
-      
+
       if x[1].has_key(Key) == 0:
          continue
 
       # Must be in the Debian group (yuk, hard coded for now)
       if GetAttr(x,"gidNumber") != "800":
-        continue
+         continue
 
       try:
          found = 0
          Line = None
          for z in x[1][Key]:
              if Key == "mailWhitelist":
-                if re.match('^[-\w.]+(/[\d]+)?$',z) == None:
-                    continue
-            else:
-                if re.match('^[-\w.]+$',z) == None:
-                    continue
+                 if re.match('^[-\w.]+(/[\d]+)?$',z) == None:
+                     continue
+             else:
+                 if re.match('^[-\w.]+$',z) == None:
+                     continue
              if found == 0:
                  found = 1
                  Line = GetAttr(x,"uid")
@@ -620,23 +731,31 @@ def GenMailList(l,File,Key):
              F.write(Line);
       except:
          pass;
-      
+
   # Oops, something unspeakable happened.
   except:
    Die(File,F,None);
    raise;
   Done(File,F,None);
 
+def isRoleAccount(pwEntry):
+   if not pwEntry.has_key("objectClass"):
+      raise "pwEntry has no objectClass"
+   oc =  pwEntry['objectClass']
+   try:
+      i = oc.index('debianRoleAccount')
+      return True
+   except ValueError:
+      return False
+
 # Generate the DNS Zone file
 def GenDNS(l,File,HomePrefix):
   F = None;
   try:
    F = open(File + ".tmp","w");
-   
+
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    # Write out the zone file entry for each user
    for x in PasswdAttrs:
@@ -644,38 +763,38 @@ def GenDNS(l,File,HomePrefix):
          continue;
 
       # If the account has no PGP key, do not write it
-      if x[1].has_key("keyFingerPrint") == 0:
+      if x[1].has_key("keyFingerPrint") == 0 and not isRoleAccount(x[1]):
          continue;
       try:
          F.write("; %s\n"%(EmailAddress(x)));
          for z in x[1]["dnsZoneEntry"]:
             Split = z.lower().split()
-           if Split[1].lower() == 'in':
+            if Split[1].lower() == 'in':
                for y in range(0,len(Split)):
                   if Split[y] == "$":
                      Split[y] = "\n\t";
                Line = " ".join(Split) + "\n";
                F.write(Line);
-              
-              Host = Split[0] + DNSZone;
-              if BSMTPCheck.match(Line) != None:
-                  F.write("; Has BSMTP\n");
-                              
-              # Write some identification information
+
+               Host = Split[0] + DNSZone;
+               if BSMTPCheck.match(Line) != None:
+                   F.write("; Has BSMTP\n");
+
+               # Write some identification information
                if Split[2].lower() == "a":
-                 Line = "%s IN TXT \"%s\"\n"%(Split[0],EmailAddress(x));
+                  Line = "%s IN TXT \"%s\"\n"%(Split[0],EmailAddress(x));
                   for y in x[1]["keyFingerPrint"]:
-                    Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0],FormatPGPKey(y));
+                     Line = Line + "%s IN TXT \"PGP %s\"\n"%(Split[0],FormatPGPKey(y));
                   F.write(Line);
-           else:
+            else:
                Line = "; Err %s"%(str(Split));
                F.write(Line);
 
          F.write("\n");
       except:
-        F.write("; Errors\n");
+         F.write("; Errors\n");
          pass;
-      
+
   # Oops, something unspeakable happened.
   except:
    Die(File,F,None);
@@ -687,11 +806,11 @@ def GenSSHFP(l,File,HomePrefix):
   F = None
   try:
    F = open(File + ".tmp","w")
-   
+
    # Fetch all the hosts
    global HostAttrs
    if HostAttrs == None:
-      raise "No Hosts"
+      raise UDEmptyList, "No Hosts"
 
    for x in HostAttrs:
       if x[1].has_key("hostname") == 0 or \
@@ -722,11 +841,9 @@ def GenBSMTP(l,File,HomePrefix):
   F = None;
   try:
    F = open(File + ".tmp","w");
-   
+
    # Fetch all the users
    global PasswdAttrs;
-   if PasswdAttrs == None:
-      raise "No Users";
 
    # Write out the zone file entry for each user
    for x in PasswdAttrs:
@@ -739,21 +856,21 @@ def GenBSMTP(l,File,HomePrefix):
       try:
          for z in x[1]["dnsZoneEntry"]:
             Split = z.lower().split()
-           if Split[1].lower() == 'in':
+            if Split[1].lower() == 'in':
                for y in range(0,len(Split)):
                   if Split[y] == "$":
                      Split[y] = "\n\t";
                Line = " ".join(Split) + "\n";
-              
-              Host = Split[0] + DNSZone;
-              if BSMTPCheck.match(Line) != None:
-                  F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
-                              GetAttr(x,"uid"),HomePrefix,GetAttr(x,"uid"),Host));
-                              
+
+               Host = Split[0] + DNSZone;
+               if BSMTPCheck.match(Line) != None:
+                   F.write("%s: user=%s group=Debian file=%s%s/bsmtp/%s\n"%(Host,
+                               GetAttr(x,"uid"),HomePrefix,GetAttr(x,"uid"),Host));
+
       except:
-        F.write("; Errors\n");
+         F.write("; Errors\n");
          pass;
-      
+
   # Oops, something unspeakable happened.
   except:
    Die(File,F,None);
@@ -771,9 +888,10 @@ def HostToIP(Host):
         except socket.gaierror, (code):
             if code[0] != -2: raise
         IPAdresses = []
-        for addr in IPAdressesT:
-            if addr[0] == socket.AF_INET: IPAdresses += [addr[1], "::ffff:"+addr[1]]
-            else: IPAdresses += [addr[1]]
+        if not IPAdressesT is None:
+            for addr in IPAdressesT:
+               if addr[0] == socket.AF_INET: IPAdresses += [addr[1], "::ffff:"+addr[1]]
+               else: IPAdresses += [addr[1]]
         HostToIPCache[Host] = IPAdresses
     return HostToIPCache[Host]
 
@@ -788,16 +906,35 @@ def GenSSHKnown(l,File,mode=None):
 
    global HostAttrs
    if HostAttrs == None:
-      raise "No Hosts";
-   
+      raise UDEmptyList, "No Hosts"
+
    for x in HostAttrs:
       if x[1].has_key("hostname") == 0 or \
          x[1].has_key("sshRSAHostKey") == 0:
          continue;
       Host = GetAttr(x,"hostname");
       HostNames = [ Host ]
-      SHost = Host.find(".")
-      if SHost != None: HostNames += [Host[0:SHost]]
+      if Host.endswith(HostDomain):
+         HostNames.append(Host[:-(len(HostDomain)+1)])
+
+      # in the purpose field [[host|some other text]] (where some other text is optional)
+      # makes a hyperlink on the web thing. we now also add these hosts to the ssh known_hosts
+      # file.  But so that we don't have to add everything we link we can add an asterisk
+      # and say [[*... to ignore it.  In order to be able to add stuff to ssh without
+      # http linking it we also support [[-hostname]] entries.
+      for i in x[1].get("purpose",[]):
+         m = PurposeHostField.match(i)
+         if m:
+            m = m.group(1)
+            # we ignore [[*..]] entries
+            if m.startswith('*'):
+               continue;
+            if m.startswith('-'):
+               m = m[1:]
+            if m:
+               HostNames.append(m)
+               if m.endswith(HostDomain):
+                  HostNames.append(m[:-(len(HostDomain)+1)])
 
       for I in x[1]["sshRSAHostKey"]:
          if mode and mode == 'authorized_keys':
@@ -815,40 +952,52 @@ def GenSSHKnown(l,File,mode=None):
 
 # Generate the debianhosts file (list of all IP addresses)
 def GenHosts(l,File):
-  F = None;
+  F = None
   try:
-   OldMask = os.umask(0022);
-   F = open(File + ".tmp","w",0644);
-   os.umask(OldMask);
-
-   # Fetch all the hosts
-   HostNames = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"hostname=*",\
-                ["hostname"]);
-   
-   if HostNames == None:
-      raise "No Hosts";
-
-   for x in HostNames:
-      if x[1].has_key("hostname") == 0:
-         continue;
-      Host = GetAttr(x,"hostname");
-      try:
-        Addr = socket.gethostbyname(Host);
-        F.write(Addr + "\n");
-      except:
-        pass
+    OldMask = os.umask(0022)
+    F = open(File + ".tmp","w",0644)
+    os.umask(OldMask)
+
+    # Fetch all the hosts
+    hostnames = l.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "hostname=*",
+                           ["hostname"])
+
+    if hostnames == None:
+       raise UDEmptyList, "No Hosts"
+
+    seen = set()
+    for x in hostnames:
+      host = GetAttr(x,"hostname", None)
+      if host:
+        addrs = []
+        try:
+          addrs += socket.getaddrinfo(host, None, socket.AF_INET)
+        except socket.error:
+          pass
+        try:
+          addrs += socket.getaddrinfo(host, None, socket.AF_INET6)
+        except socket.error:
+          pass
+
+        for addrinfo in addrs:
+          if addrinfo[0] in (socket.AF_INET, socket.AF_INET6):
+            addr = addrinfo[4][0]
+            if addr not in seen:
+              print >> F, addrinfo[4][0]
+              seen.add(addr)
   # Oops, something unspeakable happened.
   except:
-   Die(File,F,None);
-   raise;
-  Done(File,F,None);
+    Die(File,F,None)
+    raise
+  Done(File,F,None)
 
 def GenKeyrings(l,OutDir):
   for k in Keyrings:
     shutil.copy(k, OutDir)
 
+
 # Connect to the ldap server
-l = ldap.open(LDAPServer);
+l = connectLDAP()
 F = open(PassDir+"/pass-"+pwd.getpwuid(os.getuid())[0],"r");
 Pass = F.readline().strip().split(" ")
 F.close();
@@ -857,27 +1006,33 @@ l.simple_bind_s("uid="+Pass[0]+","+BaseDn,Pass[1]);
 # Fetch all the groups
 GroupIDMap = {};
 Attrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"gid=*",\
-                  ["gid","gidNumber"]);
+                  ["gid","gidNumber","subGroup"]);
 
-# Generate the GroupMap and GroupIDMap
+# Generate the SubGroupMap and GroupIDMap
 for x in Attrs:
    if x[1].has_key("gidNumber") == 0:
       continue;
    GroupIDMap[x[1]["gid"][0]] = int(x[1]["gidNumber"][0]);
+   if x[1].has_key("subGroup") != 0:
+      SubGroupMap.setdefault(x[1]["gid"][0], []).extend(x[1]["subGroup"]);
 
 # Fetch all the users
 PasswdAttrs = l.search_s(BaseDn,ldap.SCOPE_ONELEVEL,"uid=*",\
                 ["uid","uidNumber","gidNumber","supplementaryGid",\
                  "gecos","loginShell","userPassword","shadowLastChange",\
                  "shadowMin","shadowMax","shadowWarning","shadowInactive",
-                "shadowExpire","emailForward","latitude","longitude",\
+                 "shadowExpire","emailForward","latitude","longitude",\
                  "allowedHost","sshRSAAuthKey","dnsZoneEntry","cn","sn",\
-                "keyFingerPrint","privateSub","mailDisableMessage",\
+                 "keyFingerPrint","privateSub","mailDisableMessage",\
                  "mailGreylisting","mailCallout","mailRBL","mailRHSBL",\
-                 "mailWhitelist"]);
+                 "mailWhitelist", "sudoPassword", "objectClass", "accountStatus"])
+
+if PasswdAttrs is None:
+   raise UDEmptyList, "No Users"
+
 # Fetch all the hosts
 HostAttrs    = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"sshRSAHostKey=*",\
-                ["hostname","sshRSAHostKey"]);
+                ["hostname","sshRSAHostKey","purpose"]);
 
 # Open the control file
 if len(sys.argv) == 1:
@@ -887,7 +1042,15 @@ else:
 
 # Generate global things
 GlobalDir = GenerateDir+"/";
-SSHGlobal, SSHFiles = GenSSHShadow(l,GlobalDir+"ssh-rsa-shadow");
+GenMailDisable(l,GlobalDir+"mail-disable")
+
+for x in PasswdAttrs:
+   if IsRetired(x):
+      RetiredUsers.append(x)
+
+PasswdAttrs = filter(lambda x: not x in RetiredUsers, PasswdAttrs)
+
+SSHFiles = GenSSHShadow(l);
 GenAllForward(l,GlobalDir+"mail-forward.cdb");
 GenMarkers(l,GlobalDir+"markers");
 GenPrivate(l,GlobalDir+"debian-private");
@@ -895,7 +1058,6 @@ GenDisabledAccounts(l,GlobalDir+"disabled-accounts");
 GenSSHKnown(l,GlobalDir+"ssh_known_hosts");
 #GenSSHKnown(l,GlobalDir+"authorized_keys", 'authorized_keys');
 GenHosts(l,GlobalDir+"debianhosts");
-GenMailDisable(l,GlobalDir+"mail-disable");
 GenMailBool(l,GlobalDir+"mail-greylist","mailGreylisting");
 GenMailBool(l,GlobalDir+"mail-callout","mailCallout");
 GenMailList(l,GlobalDir+"mail-rbl","mailRBL");
@@ -906,6 +1068,8 @@ GenKeyrings(l,GlobalDir);
 # Compatibility.
 GenForward(l,GlobalDir+"forward-alias");
 
+PasswdAttrs = filter(lambda x: not x in DisabledUsers, PasswdAttrs)
+
 while(1):
    Line = F.readline();
    if Line == "":
@@ -937,10 +1101,6 @@ while(1):
      Allowed = None
    CurrentHost = Split[0];
 
-   # If we're using a single SSH file, deal with it
-   if SSHGlobal is not None:
-      DoLink(GlobalDir, OutDir, SSHGlobal)
-
    DoLink(GlobalDir,OutDir,"debianhosts");
    DoLink(GlobalDir,OutDir,"ssh_known_hosts");
    DoLink(GlobalDir,OutDir,"disabled-accounts")
@@ -952,64 +1112,21 @@ while(1):
       userlist = GenPasswd(l,OutDir+"passwd",Split[1], "x");
    sys.stdout.flush();
    grouprevmap = GenGroup(l,OutDir+"group");
+   GenShadowSudo(l, OutDir+"sudo-passwd", ExtraList.has_key("[UNTRUSTED]") or ExtraList.has_key("[NOPASSWD]"))
 
    # Now we know who we're allowing on the machine, export
    # the relevant ssh keys
-   if MultipleSSHFiles:
-      OldMask = os.umask(0077);
-      tf = tarfile.open(name=os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
-      os.umask(OldMask);
-      for f in userlist.keys():
-        if f not in SSHFiles:
-            continue
-        # If we're not exporting their primary group, don't export 
-        # the key and warn
-        grname = None
-        if userlist[f] in grouprevmap.keys():
-            grname = grouprevmap[userlist[f]]
-        else:
-            try:
-                if int(userlist[f]) <= 100:
-                    # In these cases, look it up in the normal way so we
-                    # deal with cases where, for instance, users are in group
-                    # users as their primary group.
-                    grname = grp.getgrgid(userlist[f])[0]
-            except Exception, e:
-                pass
-
-        if grname is None:
-            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])
-            continue
-
-        to = tf.gettarinfo(os.path.join(GlobalDir, 'userkeys', f), f)
-        # These will only be used where the username doesn't
-        # exist on the target system for some reason; hence,
-        # in those cases, the safest thing is for the file to
-        # be owned by root but group nobody.  This deals with
-        # the bloody obscure case where the group fails to exist
-        # whilst the user does (in which case we want to avoid
-        # ending up with a file which is owned user:root to avoid
-        # a fairly obvious attack vector)
-        to.uid = 0
-        to.gid = 65534
-        # Using the username / groupname fields avoids any need
-        # to give a shit^W^W^Wcare about the UIDoffset stuff.
-        to.uname = f
-        to.gname = grname
-        to.mode  = 0400
-        tf.addfile(to, file(os.path.join(GlobalDir, 'userkeys', f)))
-
-      tf.close()
-      os.rename(os.path.join(GlobalDir, 'ssh-keys-%s.tar.gz' % CurrentHost),
-                os.path.join(OutDir, 'ssh-keys.tar.gz'))
+   GenSSHtarballs(userlist, SSHFiles, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
 
    if ExtraList.has_key("[UNTRUSTED]"):
+     print "[UNTRUSTED] tag is obsolete and may be removed in the future."
      continue;
    if not ExtraList.has_key("[NOPASSWD]"):
      GenShadow(l,OutDir+"shadow");
 
-   # Link in global things   
-   DoLink(GlobalDir,OutDir,"markers");
+   # Link in global things
+   if not ExtraList.has_key("[NOMARKERS]"):
+     DoLink(GlobalDir,OutDir,"markers");
    DoLink(GlobalDir,OutDir,"mail-forward.cdb");
    DoLink(GlobalDir,OutDir,"mail-disable");
    DoLink(GlobalDir,OutDir,"mail-greylist");
@@ -1024,7 +1141,7 @@ while(1):
    if ExtraList.has_key("[DNS]"):
       GenDNS(l,OutDir+"dns-zone",Split[1]);
       GenSSHFP(l,OutDir+"dns-sshfp",Split[1])
-      
+
    if ExtraList.has_key("[BSMTP]"):
       GenBSMTP(l,OutDir+"bsmtp",Split[1])
 
@@ -1038,3 +1155,7 @@ while(1):
      for k in Keyrings:
        try: posix.remove(OutDir+os.path.basename(k));
        except: pass;
+
+# vim:set et:
+# vim:set ts=3:
+# vim:set shiftwidth=3: