diff --git a/root/usr/local/bin/epfl_roaming.py b/root/usr/local/bin/epfl_roaming.py index 8a01492..c7a1158 100755 --- a/root/usr/local/bin/epfl_roaming.py +++ b/root/usr/local/bin/epfl_roaming.py @@ -1,929 +1,917 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ epfl_roaming : Script to make application's preferences move along with the user. + It is called by PAM as root at session_open and session_close : $ epfl_roaming.py --pam does : - mount & umount - files/folders ln -s & cp - rm -rf at session_close - DConf dump at session_close + It is called by an autostart ~/.config/autostart/epfl_roaming.desktop as username : $ epfl_roaming.py --session does : - DConf load + It is called by a systemd service /etc/systemd/system/epfl_roaming_on_shutdown.service at shutdown|reboot as root : $ epfl_roaming.py --on_halt does : - run roaming_close for every user still logged before network is turned off. It requires packages: $ sudo apt install python-lockfile It requires to work with script manage_cred.py """ import os import sys import re import argparse import pwd, grp import ldap import pickle import subprocess import lockfile import shutil import xml.dom.minidom import datetime import signal import time import traceback ### CONSTANTS LOG_PAM = "/var/log/epfl_roaming.log" LOG_SESSION = "/tmp/epfl_roaming_{username}.log" # username replaced during execution time CONFIG_FILE = "/usr/local/etc/epfl_roaming.conf" LDAP_SERVER = "ldap://ldap.epfl.ch" LDAP_BASE_DN = "c=ch" LDAP_SCOPE = ldap.SCOPE_SUBTREE LDAP_NB_RETRY = 3 RM_MAX_ATTEMPT = 3 RM_SLEEP = 1 UMOUNT_MAX_ATTEMPT = 3 UMOUNT_SLEEP = 1 MANAGE_CRED_FLAG_FILE = "/var/run/epfl_roaming/manage_cred.flag" MANAGE_CRED_PID_FILE = "/var/run/manage_cred/manage_cred_{username}.pid" MANAGE_CRED_TIMEOUT = 3 MANAGE_CRED_TERM = True VAR_RUN = "/var/run/epfl_roaming" SEMAPHORE_LOCK_FILE = "/var/run/epfl_roaming/global_lock" SESSIONS_COUNT_FILE = "/var/run/epfl_roaming/sessions_count" class PreventInterrupt(object): def __init__(self): pass def __enter__(self): PreventInterrupt.__no_interrupt__() def __exit__(self, typ, val, tb): PreventInterrupt.__can_interrupt__() @classmethod def is_interruptible(cls): try: return cls.__can_interrupt except Exception: return True @classmethod def __no_interrupt__(cls): cls.__can_interrupt = False @classmethod def __can_interrupt__(cls): cls.__can_interrupt = True class UserIdentity(): def __init__(self, user): self.user = user def __enter__(self): os.setegid(int(self.user.gid)) os.seteuid(int(self.user.uid)) IO.write("ID changed : %s" % (os.getresuid(), )) def __exit__(self, typ, val, tb): os.seteuid(0) os.setegid(0) IO.write("ID changed : %s" % (os.getresuid(), )) class IO(object): def __init__(self, filename): self.filename = filename def __enter__(self): IO.__open__(self.filename) try: for msg, eol in IO.previous_writes: IO.write(msg, eol) except AttributeError: pass def __exit__(self, typ, val, tb): IO.__close__() @classmethod def write(cls, msg, eol="\n"): pid = os.getpid() try: cls.f.write("\n".join(["(%s) %s" % (pid, s) for s in msg.split("\n")]) + eol) except AttributeError: try: cls.previous_writes.append((msg, eol)) except AttributeError: cls.previous_writes = [(msg, eol),] @classmethod def __open__(cls, filename): cls.f = open(filename, "a", 1) # line buffered @classmethod def __close__(cls): cls.f.close() class NameSpace(object): def __repr__(self): type_name = type(self).__name__ args_string = [] for arg in self._get_args(): args_string.append(repr(arg)) for name, value in self._get_kwargs(): args_string.append("%s=%r" % (name, value)) return "%s(%s)" % (type_name, ", ".join(args_string)) def _get_kwargs(self): return sorted(self.__dict__.items()) def _get_args(self): return [] class Ldap(object): def __init__(self): success = False for _ in xrange(LDAP_NB_RETRY): try: self.l = ldap.initialize(LDAP_SERVER) success = True except Exception, e: time.sleep(1) if not success: raise e def search_s(self, l_filter, l_attrs): for _ in xrange(LDAP_NB_RETRY): try: return self.l.search_s( base=LDAP_BASE_DN, scope=LDAP_SCOPE, filterstr=l_filter, attrlist=l_attrs ) except Exception, e: time.sleep(1) raise e def run_cmd(cmd, s_cmd=None, env=None, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, s_input=None, shell=False): p = subprocess.Popen( cmd, env=env, stdin=stdin, stdout=stdout, stderr=stderr, shell=shell, ) if s_cmd != None: IO.write("-> (%s) %s" % (p.pid, s_cmd)) else: if shell: IO.write("-> (%s) %s" % (p.pid, cmd)) else: IO.write("-> (%s) %s" % (p.pid, " ".join(cmd))) output = p.communicate(s_input)[0] if output != "": IO.write("| (%s) " % p.pid + re.sub(r"\n", "\n| (%s) " % p.pid, output)) if p.returncode == 0: IO.write("ok (%s)" % p.pid) else: IO.write("Error: Returned non-zero exit status %d (%s)" % (p.returncode, p.pid)) return p.returncode == 0 def read_options(): """ Parse command line args """ print " ".join(sys.argv) parser = argparse.ArgumentParser(description="EPFL Roaming.") parser.add_argument( "--pam", help="PAM related actions (!=lightdm) : filers (u)mount, folders/files link/copy, DConf save", action="store_const", dest="context", default=None, const="pam", ) parser.add_argument( "--session", help="Session (DConf load)", action="store_const", dest="context", const="session", ) parser.add_argument( "--on_halt", help="Hold it's execution until all session have been cleaned by epfl_roaming.py (for shutdown/reboot).", action="store_const", dest="context", const="on_halt", ) parser.add_argument( "--list_users", help="List users currently logged in and how many sessions they have.", action="store_const", dest="context", const="list_users", ) parser.add_argument( "--test_load", help="Load DConf (test)", action="store_const", dest="context", const="test_load", ) parser.add_argument( "--test_dump", help="Dump DConf (test)", action="store_const", dest="context", const="test_dump", ) options = parser.parse_args() if options.context == None: parser.print_help() sys.exit(1) return options def read_user(options, on_halt_username=None): """ Extract all necessary info for the user """ user = NameSpace() if options.context == "pam": user.username = os.environ.get("PAM_USER", None) # SERVICE : lightdm | sshd | login | gdm-password # other services (like slurm) are not taken in account in epfl_roaming user.conn_service = os.environ.get("PAM_SERVICE", None) # TTY : :0 | ssh user.conn_tty = os.environ.get("PAM_TTY", None) # TYPE : open_session | close_session user.conn_type = os.environ.get("PAM_TYPE", None) elif options.context == "on_halt": user.username = on_halt_username else: user.username = pwd.getpwuid(os.getuid())[0] + user.username = user.username.split('@')[0] user.home_dir = os.path.expanduser("~%s" % user.username) # shortcuts if not user.home_dir.startswith("/home/"): return user try: pw = pwd.getpwnam(user.username) except (KeyError, TypeError): return user #~ if options.context in ("pam", "on_halt"): my_ldap = Ldap() try : ldap_res = my_ldap.search_s( # l_filter="uidNumber=%s" % pw.pw_uid, l_filter="uid=%s" % user.username, l_attrs=["uniqueIdentifier"] ) unique_identifier = ldap_res[0][1]["uniqueIdentifier"][0] except (KeyError, IndexError): # no pw_uid or not in ldap! return user # EPFL Guests if ldap_res[0][0].endswith("o=epfl-guests,c=ch"): user.epfl_account_type = "guest" return user # Normal EPFL account automount_informations = ("", "", "", "") ldap_res = my_ldap.search_s( l_filter="cn=%s" % user.username, l_attrs=["automountInformation"] ) for entry in ldap_res: if entry[1].get("automountInformation") != None: automount_informations = entry[1]["automountInformation"][0] automount_informations = re.findall(r'-fstype=(\w+),(.+) ([\w\.]+):(.+)$', automount_informations)[0] gr = grp.getgrgid(pw.pw_gid) user.epfl_account_type = "normal" user.uid = str(pw.pw_uid) user.gid = str(pw.pw_gid) user.groupname = gr.gr_name user.domain = "INTRANET" user.sciper = unique_identifier user.sciper_digit = unique_identifier[-1] user.automount_fstype = automount_informations[0] user.automount_options = automount_informations[1] user.automount_host = automount_informations[2] user.automount_path = automount_informations[3] return user def check_options(options, user): """ Performs all required checks """ if options.context == "pam" and user.conn_service not in ("lightdm", "sshd", "login", "common-session", "gdm-password"): IO.write("Not doing anything for PAM_SERVICE '%s'" % user.conn_service) sys.exit(0) if options.context in ("pam", "on_halt") and os.geteuid() != 0: IO.write("Error: this should be run as root.") sys.exit(1) if options.context == "session" and os.geteuid() == 0: IO.write("Error: this should not be running as root.") sys.exit(1) if user.username == None: if options.context == "pam": IO.write("Error: Could not read PAM_USER") else: IO.write("Error: Could not read USER") sys.exit(1) if not user.home_dir.startswith("/home/"): IO.write("Nothing to do for user %s (home dir: %s)" % (user.username, user.home_dir)) sys.exit(0) if options.context == "pam": if user.conn_type == None: IO.write("Error: Could not read PAM_TYPE") sys.exit(1) if user.conn_type not in ("open_session", "close_session"): IO.write("Error: Unknown PAM_TYPE : %s" % user.conn_type) sys.exit(1) if options.context in ("pam", "on_halt"): try: user.epfl_account_type except AttributeError: IO.write("Warning: Incomplete user informations found in LDAP. Exiting.") sys.exit(0) def apply_subst(name, user): name = re.sub(r'_SCIPER_DIGIT_', user.sciper_digit, name) name = re.sub(r'_SCIPER_', user.sciper, name) name = re.sub(r'_USERNAME_', user.username, name) name = re.sub(r'_GROUPNAME_', user.groupname, name) name = re.sub(r'_DOMAIN_', user.domain, name) name = re.sub(r'_UID_', user.uid, name) name = re.sub(r'_GID_', user.gid, name) name = re.sub(r'_FSTYPE_', user.automount_fstype, name) name = re.sub(r'_HOST_', user.automount_host, name) name = re.sub(r'_PATH_', user.automount_path, name) name = re.sub(r'_OPTIONS_', user.automount_options, name) return name def read_config(options, user): """ Read and Parse config file """ class ConfigLineException(Exception): def __init__(self, line, reason="syntax"): self.line = line self.reason = reason conf = {"mounts" : {}, "links" : [], "su_links" : [], "dconf" : {},} dconf_file = "" try: with open(CONFIG_FILE, "r") as f: for line in f: try: line = re.sub(r'\s*#.*$', '', line).rstrip() if line == "": continue try: subject = re.findall(r'(\S+)', line)[0] except IndexError, e: raise ConfigLineException(line, reason="syntax") ## Mounts if subject == "mount": if not options.context in ("pam", "on_halt"): continue line = apply_subst(line, user) mount_point = get_mount_point(line) conf["mounts"][mount_point] = line ## Links elif subject == "link": try: target, link_name = re.findall(r'"([^"]+)"', line)[0:2] target = apply_subst(target, user) link_name = apply_subst(link_name, user) conf["links"].append((target, link_name)) except IndexError, e: raise ConfigLineException(line, reason="syntax") ## Links elif subject == "su_link": try: target, link_name = re.findall(r'"([^"]+)"', line)[0:2] target = apply_subst(target, user) link_name = apply_subst(link_name, user) conf["su_links"].append((target, link_name)) except IndexError, e: raise ConfigLineException(line, reason="syntax") ## dconf file elif subject == "dconf_file": try: dconf_file = re.findall(r'"(.+)"', line)[0] dconf_file = apply_subst(dconf_file, user) except IndexError, e: raise ConfigLineException(line, reason="syntax") ## dconf entry elif subject == "dconf": if dconf_file == "": raise ConfigLineException(line, reason="dconf key before dconf_file instruction") try: dconf_entry = re.findall(r'"(.+)"', line)[0] conf["dconf"].setdefault(dconf_file, []).append(dconf_entry) except IndexError, e: raise ConfigLineException(line, reason="syntax") else: raise ConfigLineException(line, reason="syntax") except ConfigLineException, e: IO.write("Error: ", eol = "") if e.reason == "syntax": IO.write("Unrecognized line :\n%s" % e.line) else: IO.write("%s :\n%s" % (e.reason, e.line)) IO.write("Continuing ignoring that one.") except IOError: IO.write("Conf file %s not readable" % CONFIG_FILE) return conf ### # Clean DConf (remove englobing elements) for dconf_file in conf["dconf"]: indexes_to_drop = set() for i in xrange(len(conf["dconf"][dconf_file])): for j in xrange(len(conf["dconf"][dconf_file])): if i == j: continue if conf["dconf"][dconf_file][i].startswith(conf["dconf"][dconf_file][j]): indexes_to_drop.add(i) for i in reversed(sorted(list(indexes_to_drop))): del(conf["dconf"][dconf_file][i]) return conf def count_sessions(user, increment=0, clear_count=False): """ Increments/decrements session count for current user """ try: with open(SESSIONS_COUNT_FILE, "rb") as f: user_sessions = pickle.load(f) except: user_sessions = {} user_sessions.setdefault(user.username, 0) old_count = user_sessions[user.username] if clear_count: new_count = 0 user_sessions.pop(user.username) else: new_count = max(user_sessions[user.username] + increment, 0) if new_count <= 0: user_sessions.pop(user.username) else: user_sessions[user.username] = new_count IO.write("%i -> %i" % (old_count, new_count)) try: with open(SESSIONS_COUNT_FILE, "wb") as f: pickle.dump(user_sessions, f) except Exception, e: IO.write("Error : %s" % e) raise return old_count, new_count def get_mount_point(mount_instruction): """ Guess mointpoint from a mount instruction """ line = mount_instruction line = re.sub(r'-o \S+\s*', '', line) line = re.sub(r'-t \S+\s*', '', line) line = re.sub(r'-[fnrsvw]\s*', '', line) m = re.search ('(\S+)\s*$', line) if m: return m.group(1) else: IO.write("Error: Mount point not found in %s" % mount_instruction) IO.write("Aborting") sys.exit(1) def ismount(path): """ Replaces os.path.ismount which doesn't work for nfsv4 run from root """ p = subprocess.Popen(["mount"], stdout=subprocess.PIPE) output = p.communicate()[0] return path in re.findall(r' on (\S+)\s+', output) def dconf_dump(config, user, test=False): if not os.path.exists(os.path.join(user.home_dir, ".config/dconf/user")): IO.write("dconf_dump : ~/.config/dconf/user not found -> Skipping.") return IO.write("dconf_dump") for dconf_file, keys_to_save in config["dconf"].items(): dconf_file = os.path.join(user.home_dir, dconf_file) IO.write("DConf to %s" % dconf_file) dir_save_to = os.path.dirname(dconf_file) if not os.path.exists(dir_save_to): IO.write("mkdir -p %s" % dir_save_to) os.makedirs(dir_save_to) dump_succeeded = True dump_dconf = "" for k in keys_to_save: IO.write("+ %s" % k) if k[-1] == "/": if test: cmd = ["dconf", "dump", k] else: cmd = ["sudo", "-u", user.username, "dconf", "dump", k] p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env={}) k_dumped = p.communicate()[0] for line in k_dumped.split("\n"): try: fold = re.findall(r'^\[(.*)\]$', line)[0] if fold == "/": dump_dconf += "[%s]\n" % k[1:-1] else: dump_dconf += "[%s]\n" % os.path.join(k[1:-1], fold) except IndexError, e: dump_dconf += line + "\n" else: if test: cmd = ["dconf", "read", k] else: cmd = ["sudo", "-u", user.username, "dconf", "read", k] p = subprocess.Popen(cmd, stdout=subprocess.PIPE) k_dumped = p.communicate()[0] if k_dumped != "": dump_dconf += """ [%s] %s=%s """ % (os.path.dirname(k)[1:], os.path.basename(k), k_dumped) if p.returncode != 0: dump_succeeded = False break if dump_succeeded and dump_dconf != "": with open(dconf_file, "w") as f: f.write(dump_dconf) else: IO.write("DConf dump did not succeeded. Aborting this.") def dconf_load(config, user, test=False): IO.write("dconf_load") for dconf_file in config["dconf"]: dconf_file = os.path.join(user.home_dir, dconf_file) if os.path.exists(dconf_file): with open(dconf_file, "r") as f: dconf_dumped = f.read() # "dbus-launch", "--exit-with-session", cmd = ["dconf", "load", "/"] run_cmd( cmd=cmd, s_cmd="cat %s | %s" % (dconf_file, " ".join(cmd)), stdin=subprocess.PIPE, s_input=dconf_dumped, ) def filers_mount(config, user): """ Triggers manage_cred's extension to mount for us """ try: with open(MANAGE_CRED_PID_FILE.format(username=user.username), "r") as f: manage_cred_pid = int(f.readline()) except: IO.write("Warning, could not find manage_cred process. Not gonna mount filers.") return open(MANAGE_CRED_FLAG_FILE, "a").close() os.kill(manage_cred_pid, signal.SIGUSR2) manage_cred_finished = False for _ in range(MANAGE_CRED_TIMEOUT*10): time.sleep(0.1) if not os.path.exists(MANAGE_CRED_FLAG_FILE): manage_cred_finished = True break if not manage_cred_finished: IO.write("Warning, manage_cred didn't complete mount filers.") if MANAGE_CRED_TERM: os.kill(manage_cred_pid, signal.SIGTERM) def filers_umount(config, user): """ Performs all umount return True if all succeed return False if one failed """ IO.write("Proceeding umount!") success = True for mount_point in config["mounts"].keys() + [os.path.join(user.home_dir, ".gvfs"), os.path.join(user.home_dir, "freerds_client"),]: if not ismount(mount_point): IO.write("%s not mounted. Skip." % mount_point) continue for i in xrange(UMOUNT_MAX_ATTEMPT): if run_cmd( cmd=["umount", "-fl", mount_point], ): break time.sleep(UMOUNT_SLEEP) if ismount(mount_point): success = False return success def make_homedir(user): if not os.path.exists(user.home_dir): IO.write("Make homedir") run_cmd( cmd=["cp", "-R", "/etc/skel", user.home_dir] ) run_cmd( cmd=["chown", "-R", "%s:" % user.username, user.home_dir] ) else: IO.write("homedir already exists.") def proceed_roaming_open(config, user): IO.write("Proceeding roaming 'open'!") - paths_to_chown = [] def prepare_link(target, link_name, user): if re.search(r'/$', target): target_is_dir = True else: target_is_dir = False if re.match(r'\+', target): force_link = True target = target[1:] else: force_link = False target = os.path.normpath(os.path.join(user.home_dir, target)) link_name = os.path.normpath(os.path.join(user.home_dir, link_name)) target_parent = os.path.normpath(target + "/..") link_name_parent = os.path.normpath(link_name + "/..") already_done = (os.path.islink(link_name) and os.readlink(link_name) == target) if already_done: return if force_link: # create target if non existent if target_is_dir: if not os.path.exists(target): os.makedirs(target) else: if not os.path.exists(target_parent): os.makedirs(target_parent) open(target, "a").close() else: no_target = not os.path.exists(target) if no_target: return # Remove link_name if already exist if os.path.isdir(link_name) and not os.path.islink(link_name): shutil.rmtree(link_name) elif os.path.lexists(link_name): os.unlink(link_name) # Make the symlink if not os.path.exists(link_name_parent): os.makedirs(link_name_parent) IO.write("ln -s %s %s" % (target, link_name)) os.symlink(target, link_name) - # Symlink link_name and it's parents - if link_name not in paths_to_chown: - paths_to_chown.append(link_name) - path_to_chown = os.path.dirname(link_name) - while path_to_chown.startswith(user.home_dir): - if path_to_chown not in paths_to_chown: - paths_to_chown.append(path_to_chown) - path_to_chown = os.path.dirname(path_to_chown) - - ## Make homedir make_homedir(user) ## Mounts (sudo) filers_mount(config, user) - ## Links - for target, link_name in config["links"] + config["su_links"]: - prepare_link(target, link_name, user) + with UserIdentity(user): + ## Links + for target, link_name in config["links"] + config["su_links"]: + prepare_link(target, link_name, user) - if len(paths_to_chown) != 0: - run_cmd( - cmd=["chown", "-h", "%s:" % user.username] + paths_to_chown - ) def proceed_roaming_close(options, config, user): IO.write("Proceeding roaming 'close'!") ## Links - for target, link_name in config["links"]: - if re.match(r'\+', target): - target = target[1:] - target = os.path.normpath(os.path.join(user.home_dir, target)) - target_parent = os.path.normpath(target + "/..") - link_name = os.path.normpath(os.path.join(user.home_dir, link_name)) - link_name_parent = os.path.normpath(link_name + "/..") - if os.path.exists(link_name): - if os.path.realpath(link_name) != os.path.realpath(target): - # link_name doesn't point to target -> new content -> rm old content. - run_cmd( - cmd=["rm", "-rf", "--one-file-system", target], - ) - if not os.path.exists(target): - if not os.path.lexists(target_parent): - IO.write("mkdir -p %s" % target_parent) - os.makedirs(target_parent) - if os.path.isdir(link_name): + with UserIdentity(user): + for target, link_name in config["links"]: + if re.match(r'\+', target): + target = target[1:] + target = os.path.normpath(os.path.join(user.home_dir, target)) + target_parent = os.path.normpath(target + "/..") + link_name = os.path.normpath(os.path.join(user.home_dir, link_name)) + link_name_parent = os.path.normpath(link_name + "/..") + if os.path.exists(link_name): + if os.path.realpath(link_name) != os.path.realpath(target): + # link_name doesn't point to target -> new content -> rm old content. run_cmd( - cmd=["cp", "-R", link_name, target], + cmd=["rm", "-rf", "--one-file-system", target], ) - else: - run_cmd( - cmd=["cp", link_name, target], - ) - dconf_dump(config, user) + if not os.path.exists(target): + if not os.path.lexists(target_parent): + IO.write("mkdir -p %s" % target_parent) + os.makedirs(target_parent) + if os.path.isdir(link_name): + run_cmd( + cmd=["cp", "-R", link_name, target], + ) + else: + run_cmd( + cmd=["cp", link_name, target], + ) + dconf_dump(config, user) # Umounts (sudo) if not filers_umount(config, user): IO.write("Skipping rm -rf.") return # RM for i in xrange(RM_MAX_ATTEMPT): success = run_cmd( cmd=["rm", "-rf", "--one-file-system", user.home_dir] ) if success: break time.sleep(RM_SLEEP) def proceed_guest_open(user): IO.write("Proceeding guest 'open'!") make_homedir(user) def proceed_guest_close(user): IO.write("Proceeding guest 'close'!") IO.write("Nothing to be done ...") def list_current_user_sessions(display=False): try: with open(SESSIONS_COUNT_FILE, "rb") as f: user_sessions = pickle.load(f) except: user_sessions = {} if display: if len(user_sessions) == 0: print "Currently, no user has an open session." else: print "Currently, these users have an open session :" for username in user_sessions: print "%s: %i" % (username, user_sessions[username]) return user_sessions def proceed_on_halt(options): with IO(LOG_PAM): IO.write("\n*** %s" % datetime.datetime.now()) IO.write("Proceeding 'On Halt'!") try: with PreventInterrupt(): with lockfile.FileLock(SEMAPHORE_LOCK_FILE): for username in list_current_user_sessions(): IO.write("on_halt %s" % username) user = read_user(options, username) count_sessions(user, clear_count=True) if user.epfl_account_type == "guest": proceed_guest_close(user) else: config = read_config(options, user) proceed_roaming_close(options, config, user) except Exception, e: exc_type, exc_value, exc_traceback = sys.exc_info() IO.write("\n".join(traceback.format_exception(exc_type, exc_value, exc_traceback))) IO.write("done.") def signal_handler(signum, frame): IO.write("received signal %s" % signum) if PreventInterrupt.is_interruptible(): IO.write("exit.") sys.exit(1) else: IO.write("not interruptible yet. Continuing...") if __name__ == '__main__': try: os.makedirs(VAR_RUN) except OSError: pass # Manage the kill -TERM ... unfortunately not kill -9 signal.signal(signal.SIGTERM, signal_handler) #~ signal.signal(signal.SIGKILL, signal_handler) options = read_options() if options.context == "on_halt": proceed_on_halt(options) sys.exit(0) if options.context == "list_users": list_current_user_sessions(display=True) sys.exit(0) user = read_user(options) if options.context in ("pam",): logfile_name = LOG_PAM else: logfile_name = LOG_SESSION.format(username=user.username) with IO(logfile_name): try: IO.write("\n*** %s" % datetime.datetime.now()) operation = options.context if options.context == "pam": operation += "_%s" % user.conn_type IO.write("%s %s (uid=%s euid=%s)" % (operation, user.username, os.getuid(), os.geteuid())) check_options(options, user) # EPFL Guests shortcut if user.epfl_account_type == "guest": if options.context == "pam": if user.conn_type == "open_session": with lockfile.FileLock(SEMAPHORE_LOCK_FILE): if count_sessions(user, increment=+1) == (0, 1): proceed_guest_open(user) elif user.conn_type == "close_session": with lockfile.FileLock(SEMAPHORE_LOCK_FILE): if count_sessions(user, increment=-1) == (1, 0): proceed_guest_close(user) sys.exit(0) config = read_config(options, user) if options.context == "pam": if user.conn_type == "open_session": with lockfile.FileLock(SEMAPHORE_LOCK_FILE): if count_sessions(user, increment=+1) == (0, 1): proceed_roaming_open(config, user) elif user.conn_type == "close_session": with lockfile.FileLock(SEMAPHORE_LOCK_FILE): if count_sessions(user, increment=-1) == (1, 0): with PreventInterrupt(): proceed_roaming_close(options, config, user) elif options.context == "session": dconf_load(config, user) elif options.context == "test_load": dconf_load(config, user, test=True) elif options.context == "test_dump": dconf_dump(config, user, test=True) IO.write("Everything complete.") sys.exit(0) except Exception, e: exc_type, exc_value, exc_traceback = sys.exc_info() IO.write("\n".join(traceback.format_exception(exc_type, exc_value, exc_traceback))) diff --git a/root/usr/local/bin/manage_cred.py b/root/usr/local/bin/manage_cred.py index 3eeedff..bb85b2c 100755 --- a/root/usr/local/bin/manage_cred.py +++ b/root/usr/local/bin/manage_cred.py @@ -1,133 +1,134 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ manage_cred : receives user credentials during pam auth. It keeps it until MANAGE_CRED_TIMEOUT. Other applications (extensions) can ask it to do operations that need credentials. The folder /usr/local/lib/manage_cred/ is the place where extensions have to be installed. It has to be root:root 0x700. Each extension file /usr/local/lib/manage_cred/*.py has to be root:root 0x600. It has to implement : -+ FLAG_FILE : the file that flags it's epfl_roaming that sent USR1 signal to manage_cred ++ FLAG_FILE : the file that flags it's epfl_roaming that sent USR2 signal to manage_cred + run(username, password) method that does the job """ import os import sys import time import stat import signal import importlib MANAGE_CRED_TIMEOUT = 20 # sec EXT_FOLDER = "/usr/local/lib/manage_cred" VAR_RUN = "/var/run/manage_cred" PID_FILE = "/var/run/manage_cred/manage_cred_{username}.pid" class PID(): def __init__(self): self.pid_file = PID_FILE.format(username=USERNAME) def __enter__(self): with open(self.pid_file, "w") as f: f.write("%s\n" % os.getpid()) def __exit__(self, typ, val, tb): os.unlink(self.pid_file) def fork_and_wait(): def signal_USR2_handler(signum, frame): an_ext_was_run = False for ext in extensions: if os.path.exists(extensions[ext].FLAG_FILE): an_ext_was_run = True print "Running extension %s." % ext extensions[ext].run(USERNAME, PASSWORD) try: os.unlink(extensions[ext].FLAG_FILE) except: pass print "done." if not an_ext_was_run: print "got USR2 signal, but no extension were run." def signal_KILL_handler(signum, frame): print "got TERM signal, gonna exit." sys.exit(0) if os.fork() != 0: return with PID(): extensions = {} # SIGUSR2 and SIGTERM handling signal.signal(signal.SIGUSR2, signal_USR2_handler) signal.signal(signal.SIGTERM, signal_KILL_handler) # Check that Extension folder has correct rights : root:root 0x700 ext_folder_stat = os.stat(EXT_FOLDER) if (ext_folder_stat.st_uid != 0 or ext_folder_stat.st_gid != 0 or ext_folder_stat.st_mode & (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) != stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR): print "Error, extensions folder %s doesn't have correct rights : root:root 0x700.\nAborting." % EXT_FOLDER sys.exit(1) sys.path.insert(0, EXT_FOLDER) for f in os.listdir(EXT_FOLDER): if f == "__init__.py" or not f.endswith(".py"): continue # check extension has correct rights : root:root 0x600 ext_path = os.path.join(EXT_FOLDER, f) ext_stat = os.stat(ext_path) if (ext_stat.st_uid != 0 or ext_stat.st_gid != 0 or ext_stat.st_mode & (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) != stat.S_IRUSR | stat.S_IWUSR): print "Error, extensions %s doesn't have correct rights : root:root 0x600.\nSkipping." % ext_path continue try: ext_name = f[:-3] mod = importlib.import_module(ext_name) if ("FLAG_FILE" in dir(mod) and "run" in dir(mod) and type(mod.FLAG_FILE) == str and callable(mod.run)): extensions[ext_name] = mod else: print "Error, %s doesn't implement required variables and functions; Skipping." % ext_name except Exception, e: print "Error, could not import %s; Skipping." % ext_name print e for i in range(MANAGE_CRED_TIMEOUT): time.sleep(1) print "Finished to wait for %i seconds; exiting." % MANAGE_CRED_TIMEOUT if __name__ == "__main__": USERNAME = os.environ["PAM_USER"] + USERNAME = USERNAME.split('@')[0] SERVICE = os.environ["PAM_SERVICE"] TYPE = os.environ["PAM_TYPE"] print "USERNAME %s" % USERNAME print "SERVICE %s" % SERVICE print "TYPE %s" % TYPE if TYPE != "auth": sys.exit(0) PASSWORD = sys.stdin.readline().rstrip(chr(0)) try: os.makedirs(VAR_RUN) except OSError: pass fork_and_wait() sys.exit(0) diff --git a/root/usr/local/lib/manage_cred/ext_epfl_roaming.py b/root/usr/local/lib/manage_cred/ext_epfl_roaming.py index b2baf3c..10637b9 100644 --- a/root/usr/local/lib/manage_cred/ext_epfl_roaming.py +++ b/root/usr/local/lib/manage_cred/ext_epfl_roaming.py @@ -1,60 +1,56 @@ #!/usr/bin/env python2 # -*- coding: utf-8 -*- """ epfl_roaming extension for manage_cred It has to implement : + FLAG_FILE : the file that flags it's epfl_roaming that sent USR2 signal to manage_cred + run(username, password) method that does the job """ import os import sys +import pwd import subprocess FLAG_FILE = "/var/run/epfl_roaming/manage_cred.flag" EPFL_ROAMING_FOLDER = "/usr/local/bin" sys.path.append(EPFL_ROAMING_FOLDER) -from epfl_roaming import IO, LOG_PAM, read_user, read_config, run_cmd, NameSpace +from epfl_roaming import IO, LOG_PAM, read_user, read_config, run_cmd, NameSpace, UserIdentity def run(username, password): """ Performs all mount for epfl_roaming """ with IO(LOG_PAM): IO.write("Running epfl_roaming extension from manage_cred for user %s." % username) options = NameSpace() options.context = "pam" user = read_user(options, username) config = read_config(options, user) for mount_point, mount_instruction in config["mounts"].items(): if not os.path.exists(mount_point): - run_cmd( - cmd=["mkdir", "-p", mount_point] - ) - run_cmd( - cmd=["chown", "%s:" % username, mount_point] - ) - # chown parents also - parent_dir = os.path.dirname(mount_point) - while parent_dir.startswith(user.home_dir): + user = NameSpace() + user.username = username + pw = pwd.getpwnam(user.username) + user.uid = str(pw.pw_uid) + user.gid = str(pw.pw_gid) + with UserIdentity(user): run_cmd( - cmd=["chown", "%s:" % username, parent_dir] + cmd=["mkdir", "-p", mount_point] ) - parent_dir = os.path.dirname(parent_dir) - # Mount os.environ['PASSWD'] = password run_cmd( cmd=mount_instruction, shell=True, ) del os.environ['PASSWD'] IO.write("Done running epfl_roaming extension from manage_cred for user %s." % username) if __name__ == "__main__": print >> sys.stderr, "This is not to be run this way!" sys.exit(1)