# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-import string, re, time, ldap, getopt, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp
+import string, re, time, ldap, optparse, sys, os, pwd, posix, socket, base64, hashlib, shutil, errno, tarfile, grp
import lockfile
from userdir_ldap import *
from userdir_exceptions import *
#
GroupIDMap = {}
SubGroupMap = {}
-Allowed = None
CurrentHost = ""
isSSHFP = re.compile("^\s*IN\s+SSHFP")
DNSZone = ".debian.net"
Keyrings = ConfModule.sync_keyrings.split(":")
+GitoliteSSHRestrictions = getattr(ConfModule, "gitolitesshrestrictions", None)
+
def safe_makedirs(dir):
try:
def get_lock(fn, wait=5*60, max_age=3600*6):
try:
- stat = os.stat(fn)
- if stat[ST_MTIME] < time.time() - max_age:
- sys.stderr.write("Removing stale lock %s"%(fn))
- os.unlink(fn)
+ stat = os.stat(fn + '.lock')
+ if stat.st_mtime < time.time() - max_age:
+ sys.stderr.write("Removing stale lock %s"%(fn + '.lock'))
+ os.unlink(fn + '.lock')
except OSError, error:
if error.errno == errno.ENOENT:
pass
# return account['gidNumber'] == 800
# See if this user is in the group list
-def IsInGroup(account):
- if Allowed is None:
- return True
-
+def IsInGroup(account, allowed):
# See if the primary group is in the list
- if str(account['gidNumber']) in Allowed: return True
+ if str(account['gidNumber']) in allowed: return True
# Check the host based ACL
if account.is_allowed_by_hostacl(CurrentHost): return True
supgroups=[]
addGroups(supgroups, account['supplementaryGid'], account['uid'])
for g in supgroups:
- if Allowed.has_key(g):
+ if allowed.has_key(g):
return True
return False
userlist = {}
i = 0
for a in accounts:
- if not IsInGroup(a): continue
-
# Do not let people try to buffer overflow some busted passwd parser.
if len(a['gecos']) > 100 or len(a['loginShell']) > 50: continue
i = 0
for a in accounts:
- Pass = '*'
- if not IsInGroup(a): continue
-
# If the account is locked, mark it as such in shadow
# See Debian Bug #308229 for why we set it to 1 instead of 0
if not a.pw_active(): ShadowExpire = '1'
for a in accounts:
Pass = '*'
- if not IsInGroup(a): continue
-
if 'sudoPassword' in a:
for entry in a['sudoPassword']:
Match = re.compile('^('+UUID_FORMAT+') (confirmed:[0-9a-f]{40}|unconfirmed) ([a-z0-9.,*]+) ([^ ]+)$').match(entry)
raise
Done(File, F, None)
+# Generate the sudo passwd file
+def GenSSHGitolite(accounts, File):
+ F = None
+ try:
+ OldMask = os.umask(0022)
+ F = open(File + ".tmp", "w", 0600)
+ os.umask(OldMask)
+
+ if not GitoliteSSHRestrictions is None and GitoliteSSHRestrictions != "":
+ for a in accounts:
+ if not 'sshRSAAuthKey' in a: continue
+
+ User = a['uid']
+ prefix = GitoliteSSHRestrictions.replace('@@USER@@', User)
+ for I in a["sshRSAAuthKey"]:
+ if I.startswith('ssh-'):
+ line = "%s %s"%(prefix, I)
+ else:
+ line = "%s,%s"%(prefix, I)
+ line = Sanitize(line) + "\n"
+ F.write(line)
+
+ # Oops, something unspeakable happened.
+ except:
+ Die(File, F, None)
+ raise
+ Done(File, F, None)
+
# Generate the shadow list
def GenSSHShadow(global_dir, accounts):
# Fetch all the users
- userfiles = []
+ userkeys = {}
safe_rmtree(os.path.join(global_dir, 'userkeys'))
safe_makedirs(os.path.join(global_dir, 'userkeys'))
for a in accounts:
if not 'sshRSAAuthKey' in a: continue
- F = None
- try:
- OldMask = os.umask(0077)
- File = os.path.join(global_dir, 'userkeys', a['uid'])
- F = open(File + ".tmp", "w", 0600)
- os.umask(OldMask)
-
- for I in a['sshRSAAuthKey']:
- 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(File, F, None)
- # As neither masterFileName nor masterFile are defined at any point
- # this will raise a NameError.
- Die(masterFileName, masterFile, None)
- raise
+ contents = []
+ for I in a['sshRSAAuthKey']:
+ MultipleLine = "%s" % I
+ MultipleLine = Sanitize(MultipleLine)
+ contents.append(MultipleLine)
+ userkeys[a['uid']] = contents
+ return userkeys
- return userfiles
+# Generate the webPassword list
+def GenWebPassword(accounts, File):
+ F = None
+ try:
+ OldMask = os.umask(0077)
+ F = open(File, "w", 0600)
+ os.umask(OldMask)
+
+ for a in accounts:
+ if not 'webPassword' in a: continue
+ if not a.pw_active(): continue
-def GenSSHtarballs(global_dir, userlist, SSHFiles, grouprevmap, target):
+ Pass = str(a['webPassword'])
+ Line = "%s:%s" % (a['uid'], Pass)
+ Line = Sanitize(Line) + "\n"
+ F.write("%s" % (Line))
+
+ except:
+ Die(File, None, F)
+ raise
+
+def GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, target):
OldMask = os.umask(0077)
tf = tarfile.open(name=os.path.join(global_dir, 'ssh-keys-%s.tar.gz' % CurrentHost), mode='w:gz')
os.umask(OldMask)
- for f in userlist.keys():
- if f not in SSHFiles:
+ for f in userlist:
+ if f not in ssh_userkeys:
continue
# If we're not exporting their primary group, don't export
# the key and warn
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(global_dir, 'userkeys', f), f)
+ contents = ssh_userkeys[f]
+ lines = []
+ for line in contents:
+ if line.startswith("allowed_hosts=") and ' ' in line:
+ machines, line = line.split('=', 1)[1].split(' ', 1)
+ if CurrentHost not in machines.split(','):
+ continue # skip this key
+ lines.append(line)
+ if not lines:
+ continue # no keys for this host
+ contents = "\n".join(lines) + "\n"
+
+ to = tarfile.TarInfo(name=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
to.uname = f
to.gname = grname
to.mode = 0400
-
- contents = file(os.path.join(global_dir, 'userkeys', f)).read()
- lines = []
- for line in contents.splitlines():
- if line.startswith("allowed_hosts=") and ' ' in line:
- machines, line = line.split('=', 1)[1].split(' ', 1)
- if CurrentHost not in machines.split(','):
- continue # skip this key
- lines.append(line)
- if not lines:
- continue # no keys for this host
- contents = "\n".join(lines) + "\n"
+ to.mtime = int(time.time())
to.size = len(contents)
+
tf.addfile(to, StringIO(contents))
tf.close()
# Sort them into a list of groups having a set of users
for a in accounts:
GroupHasPrimaryMembers[ a['gidNumber'] ] = True
- if not IsInGroup(a): continue
if not 'supplementaryGid' in a: continue
supgroups=[]
for a in accounts:
if not 'emailForward' in a: continue
-
delete = False
- if not IsInGroup(a): delete = True
# Do not allow people to try to buffer overflow busted parsers
- elif len(a['emailForward']) > 200: delete = True
+ if len(a['emailForward']) > 200: delete = True
# Check the forwarding address
elif EmailCheck.match(a['emailForward']) is None: delete = True
for z in a["dnsZoneEntry"]:
Split = z.lower().split()
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
if not RRs.has_key(Host):
if Split[2].lower() in ["a", "aaaa"]:
else:
Line = "; Err %s"%(str(Split))
F.write(Line)
-
+
F.write("\n")
except Exception, e:
F.write("; Errors:\n")
"keyFingerPrint", "privateSub", "mailDisableMessage",\
"mailGreylisting", "mailCallout", "mailRBL", "mailRHSBL",\
"mailWhitelist", "sudoPassword", "objectClass", "accountStatus",\
- "mailContentInspectionAction"])
+ "mailContentInspectionAction", "webPassword"])
if passwd_attrs is None:
raise UDEmptyList, "No Users"
GenMailList(accounts, global_dir + "mail-rbl", "mailRBL")
GenMailList(accounts, global_dir + "mail-rhsbl", "mailRHSBL")
GenMailList(accounts, global_dir + "mail-whitelist", "mailWhitelist")
+ GenWebPassword(accounts, global_dir + "web-passwords")
GenKeyrings(global_dir)
# Compatibility.
GenAllUsers(accounts, global_dir + 'all-accounts.json')
accounts = filter(lambda a: not a in accounts_disabled, accounts)
- ssh_files = GenSSHShadow(global_dir, accounts)
+ ssh_userkeys = GenSSHShadow(global_dir, accounts)
GenMarkers(accounts, global_dir + "markers")
GenSSHKnown(host_attrs, global_dir + "ssh_known_hosts")
GenHosts(host_attrs, global_dir + "debianhosts")
+ GenSSHGitolite(accounts, global_dir + "ssh-gitolite")
GenDNS(accounts, global_dir + "dns-zone")
GenZoneRecords(host_attrs, global_dir + "dns-sshfp")
for host in host_attrs:
if not "hostname" in host[1]:
continue
- generate_host(host, global_dir, accounts, ssh_files)
+ generate_host(host, global_dir, accounts, ssh_userkeys)
-def generate_host(host, global_dir, accounts, ssh_files):
+def generate_host(host, global_dir, accounts, ssh_userkeys):
global CurrentHost
CurrentHost = host[1]['hostname'][0]
for extra in host[1]['exportOptions']:
ExtraList[extra.upper()] = True
- global Allowed
- Allowed = GroupList
- if Allowed == {}:
- Allowed = None
+ if GroupList != {}:
+ accounts = filter(lambda x: IsInGroup(x, GroupList), accounts)
DoLink(global_dir, OutDir, "debianhosts")
DoLink(global_dir, OutDir, "ssh_known_hosts")
# Now we know who we're allowing on the machine, export
# the relevant ssh keys
- GenSSHtarballs(global_dir, userlist, ssh_files, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
+ GenSSHtarballs(global_dir, userlist, ssh_userkeys, grouprevmap, os.path.join(OutDir, 'ssh-keys.tar.gz'))
if not 'NOPASSWD' in ExtraList:
GenShadow(accounts, OutDir + "shadow")
DoLink(global_dir, OutDir, "mail-rhsbl")
DoLink(global_dir, OutDir, "mail-whitelist")
DoLink(global_dir, OutDir, "all-accounts.json")
- GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "user-forward.cdb", 'emailForward')
- GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "batv-tokens.cdb", 'bATVToken')
- GenCDB(filter(lambda x: IsInGroup(x), accounts), OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
+ GenCDB(accounts, OutDir + "user-forward.cdb", 'emailForward')
+ GenCDB(accounts, OutDir + "batv-tokens.cdb", 'bATVToken')
+ GenCDB(accounts, OutDir + "default-mail-options.cdb", 'mailDefaultOptions')
# Compatibility.
DoLink(global_dir, OutDir, "forward-alias")
if 'PRIVATE' in ExtraList:
DoLink(global_dir, OutDir, "debian-private")
+ if 'GITOLITE' in ExtraList:
+ DoLink(global_dir, OutDir, "ssh-gitolite")
+
+ if 'WEB-PASSWORDS' in ExtraList:
+ DoLink(global_dir, OutDir, "web-passwords")
+
if 'KEYRING' in ExtraList:
for k in Keyrings:
bn = os.path.basename(k)
posix.remove(target)
except:
pass
+ DoLink(global_dir, OutDir, "last_update.trace")
-l = make_ldap_conn()
-mods = l.search_s('cn=log',
- ldap.SCOPE_ONELEVEL,
- '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
- ['reqEnd'])
+def getLastLDAPChangeTime(l):
+ mods = l.search_s('cn=log',
+ ldap.SCOPE_ONELEVEL,
+ '(&(&(!(reqMod=activity-from*))(!(reqMod=activity-pgp*)))(|(reqType=add)(reqType=delete)(reqType=modify)(reqType=modrdn)))',
+ ['reqEnd'])
-last = 0
+ last = 0
-# Sort the list by reqEnd
-sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
-# Take the last element in the array
-last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
+ # Sort the list by reqEnd
+ sorted_mods = sorted(mods, key=lambda mod: mod[1]['reqEnd'][0].split('.')[0])
+ # Take the last element in the array
+ last = sorted_mods[-1][1]['reqEnd'][0].split('.')[0]
-# override globaldir for testing
-if 'UD_GENERATEDIR' in os.environ:
- GenerateDir = os.environ['UD_GENERATEDIR']
+ return last
-cache_last_mod = 0
+def getLastBuildTime():
+ cache_last_mod = 0
-try:
- fd = open(os.path.join(GenerateDir, "last_update.trace"), "r")
- cache_last_mod=fd.read().strip()
- fd.close()
-except IOError, e:
- if e.errno == errno.ENOENT:
- pass
- else:
- raise e
-if cache_last_mod >= last:
- sys.exit(0)
+ try:
+ fd = open(os.path.join(GenerateDir, "last_update.trace"), "r")
+ cache_last_mod=fd.read().split()
+ try:
+ cache_last_mod = cache_last_mod[0]
+ except IndexError:
+ pass
+ fd.close()
+ except IOError, e:
+ if e.errno == errno.ENOENT:
+ pass
+ else:
+ raise e
-fd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
-fd.write(last)
-fd.close()
+ return cache_last_mod
-# Fetch all the groups
-GroupIDMap = {}
-attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
- ["gid", "gidNumber", "subGroup"])
-
-# Generate the SubGroupMap and GroupIDMap
-for x in attrs:
- if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
- continue
- 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"])
-
-lock = None
-try:
- lockf = os.path.join(GenerateDir, 'ud-generate.lock')
- lock = get_lock( lockf )
- if lock is None:
- sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
+
+
+
+def ud_generate():
+ global GenerateDir
+ global GroupIDMap
+ parser = optparse.OptionParser()
+ parser.add_option("-g", "--generatedir", dest="generatedir", metavar="DIR",
+ help="Output directory.")
+ parser.add_option("-f", "--force", dest="force", action="store_true",
+ help="Force generation, even if not update to LDAP has happened.")
+
+ (options, args) = parser.parse_args()
+ if len(args) > 0:
+ parser.print_help()
sys.exit(1)
- generate_all(GenerateDir, l)
-finally:
- if lock is not None:
- lock.release()
+ l = make_ldap_conn()
+
+ if options.generatedir is not None:
+ GenerateDir = os.environ['UD_GENERATEDIR']
+ elif 'UD_GENERATEDIR' in os.environ:
+ GenerateDir = os.environ['UD_GENERATEDIR']
+
+ ldap_last_mod = getLastLDAPChangeTime(l)
+ cache_last_mod = getLastBuildTime()
+ need_update = ldap_last_mod > cache_last_mod
+
+ if not options.force and not need_update:
+ fd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
+ fd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
+ fd.close()
+ sys.exit(0)
+
+ # Fetch all the groups
+ GroupIDMap = {}
+ attrs = l.search_s(BaseDn, ldap.SCOPE_ONELEVEL, "gid=*",\
+ ["gid", "gidNumber", "subGroup"])
+
+ # Generate the SubGroupMap and GroupIDMap
+ for x in attrs:
+ if x[1].has_key("accountStatus") and x[1]['accountStatus'] == "disabled":
+ continue
+ 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"])
+
+ lock = None
+ try:
+ lockf = os.path.join(GenerateDir, 'ud-generate.lock')
+ lock = get_lock( lockf )
+ if lock is None:
+ sys.stderr.write("Could not acquire lock %s.\n"%(lockf))
+ sys.exit(1)
+
+ tracefd = open(os.path.join(GenerateDir, "last_update.trace"), "w")
+ generate_all(GenerateDir, l)
+ tracefd.write("%s\n%s\n" % (ldap_last_mod, int(time.time())))
+ tracefd.close()
+
+ finally:
+ if lock is not None:
+ lock.release()
+
+if __name__ == "__main__":
+ ud_generate()
+
# vim:set et:
# vim:set ts=3: