# 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 = "";
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(":")
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:
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;
userlist = {}
# Fetch all the users
global PasswdAttrs;
- if PasswdAttrs == None:
- raise "No Users";
I = 0;
for x in PasswdAttrs:
# Fetch all the users
global PasswdAttrs;
- if PasswdAttrs == None:
- raise "No Users";
I = 0;
for x in PasswdAttrs:
Done(File,None,F);
# Generate the sudo passwd file
-def GenShadowSudo(l,File):
+def GenShadowSudo(l,File, untrusted):
F = None;
try:
OldMask = os.umask(0077);
# Fetch all the users
global PasswdAttrs;
- if PasswdAttrs == None:
- raise "No Users";
for x in PasswdAttrs:
Pass = '*'
hosts = Match.group(3)
cryptedpass = Match.group(4)
- if status != 'confirmed:'+make_sudopasswd_hmac('password-is-confirmed', uuid, hosts, cryptedpass):
+ 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
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:
Die(masterFileName,masterFile,None)
raise;
- if SingleSSHFile:
- Done(masterFileName,masterFile,None)
- singlefile = os.path.basename(masterFileName)
+ return userfiles
- return singlefile, 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
+
+ existingGroups.append(group)
+
+ if SubGroupMap.has_key(group):
+ addGroups(existingGroups, SubGroupMap[group], uid)
# Generate the group list
def GenGroup(l,File):
# 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;
# Fetch all the users
global PasswdAttrs;
- if PasswdAttrs == None:
- raise "No Users";
# Write out the email address for each user
for x in PasswdAttrs:
# Fetch all the users
global PasswdAttrs;
- if PasswdAttrs == None:
- raise "No Users";
# Write out the email address for each user
for x in PasswdAttrs:
# Fetch all the users
global PasswdAttrs;
- if PasswdAttrs == None:
- raise "No Users";
# Write out the position for each user
for x in PasswdAttrs:
# 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;
# Fetch all the users
global PasswdAttrs;
- if PasswdAttrs == None:
- raise "No Users";
+ global DisabledUsers
I = 0;
for x in PasswdAttrs:
if Line != "":
F.write(Sanitize(Line) + "\n")
+ DisabledUsers.append(x)
+
# Oops, something unspeakable happened.
except:
Die(File,F,None);
# Fetch all the users
global PasswdAttrs;
- if PasswdAttrs == None:
- raise "No Users";
for x in PasswdAttrs:
Reason = None
# Fetch all the users
global PasswdAttrs;
- if PasswdAttrs == None:
- raise "No Users";
for x in PasswdAttrs:
Reason = None
# Fetch all the users
global PasswdAttrs;
- if PasswdAttrs == None:
- raise "No Users";
for x in PasswdAttrs:
Reason = 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;
# 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:
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)));
# 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 \
# 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:
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]
global HostAttrs
if HostAttrs == None:
- raise "No Hosts";
+ raise UDEmptyList, "No Hosts"
for x in HostAttrs:
if x[1].has_key("hostname") == 0 or \
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':
# 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 = connectLDAP()
F = open(PassDir+"/pass-"+pwd.getpwuid(os.getuid())[0],"r");
# 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=*",\
"allowedHost","sshRSAAuthKey","dnsZoneEntry","cn","sn",\
"keyFingerPrint","privateSub","mailDisableMessage",\
"mailGreylisting","mailCallout","mailRBL","mailRHSBL",\
- "mailWhitelist", "sudoPassword"]);
+ "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:
# 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");
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");
# Compatibility.
GenForward(l,GlobalDir+"forward-alias");
+PasswdAttrs = filter(lambda x: not x in DisabledUsers, PasswdAttrs)
+
while(1):
Line = F.readline();
if Line == "":
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")
userlist = GenPasswd(l,OutDir+"passwd",Split[1], "x");
sys.stdout.flush();
grouprevmap = GenGroup(l,OutDir+"group");
- GenShadowSudo(l, OutDir+"sudo-passwd")
+ 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");
+ 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");