import os import subprocess import pwd import lib.UserExceptions class System: """Class to interact with the system specifically to support our needs 0w0 :Example: >>> from lib.System import System as System >>> Sys_ctl = System("Test", dryrun=True) >>> Sys_ctl.register() >>> Sys_ctl.lock_user_pw() >>> Sys_ctl.add_to_usergroup() >>> Sys_ctl.make_ssh_usable("sshkey") """ dry: bool = False create_command = [] home: str = "" user: str def setUser(self, username: str): self.user = username def __init__(self, username: str, dryrun: bool = False, home: str = "/home/"): """Creates an objects. Can set dry run. :param username: Username to manipulate :type username: str :param dryrun: Run all command in a dry-run? When enabled, doesn't make any changes to the system (defaults to false) :type dryrun: bool :param home: Standard directory to search for the home directories of your users(default is /home/) :type home: str :raises: ValueError: if homedir can not be found """ self.dry = dryrun if not home.endswith("/"): home += "/" if not os.path.isdir(home): raise ValueError("home should be an existent directory...") self.home = home self.user = username def aio_approve(self, pubkey, group="tilde"): """ Executes all neccessary steps to create a user from itself. Raises ALOT of possible exceptions :Note: CAREFULL! You MUST except the exceptions! :param pubkey: Users public ssh key :type pubkey: str :param group: User-group. Defaults to tilde :type group: str :return: None :raises: lib.UserExceptions.UserExistsAlready: User Exists already on system lib.UserExceptions.UnknownReturnCode: Unknown Return Code from useradd lib.UserExceptions.SSHDirUncreatable: Users SSH Dir couldnt be created lib.UserExceptions.ModifyFilesystem: Something with CHMOD failed """ self.register() self.lock_user_pw() self.add_to_usergroup(group) self.make_ssh_usable(pubkey) def register(self, cc: tuple = tuple(["useradd", "-m"])) -> bool: """Creates an local account for the given username :param cc: Tuple with commands separated to execute on the machine. (defaults to useradd -m) :type cc: tuple :return: True, if worked, raises lib.UserExceptions.UserExistsAlready when not :rtype: bool :raises: lib.UserExceptions.UserExistsAlready: when username specified already exists on the system """ create_command = cc cc = create_command + tuple([self.user]) if self.dry: self.printTuple(cc) return True elif not self.dry: rt = subprocess.call(cc) if rt != 0: raise lib.UserExceptions.UserExistsAlready(f"User {self.user} exists already") return True def unregister(self) -> bool: """ Just an alias function for removeUser :return: True, when success, False(or exception) when not :rtype: bool """ return self.remove_user() def make_ssh_usable(self, pubkey: str, sshdir: str = ".ssh/") -> bool: """ Make SSH usable for our newly registered user :param pubkey: Public SSH Key for the User you want accessible by SSH :type pubkey: str :param sshdir: Directory to write the authorized_keys File to. PWD is $HOME of said user. (defaults to ".ssh/") :type sshdir: str :return: True, if worked, raises lib.UserExceptions.UnknownReturnCode, lib.UserExceptions.HomeDirExistsAlready or lib.UserExceptions.ModifyFilesystem when not :rtype: bool :raises: lib.UserExceptions.SSHDirUncreatable: if the ssh-dir couldnt be created nor exist lib.UserExceptions.ModifyFilesystem: When chmod to .ssh and authorized_keys failed lib.UserExceptions.General: if PWD cant find the specified user """ if self.dry: print("Nah, @TODO, but actually kinda too lazy for this lul. Just a lot happening here") return True if not sshdir.endswith("/"): sshdir += "/" ssh_dir = self.home + self.user + "/" + sshdir try: os.mkdir(ssh_dir) except FileExistsError: pass # thats actually a good one for us :D except OSError as e: raise lib.UserExceptions.SSHDirUncreatable(f"Could not create {ssh_dir}: Exception: {e}") try: self.write_ssh(pubkey, ssh_dir) except OSError as e: raise lib.UserExceptions.ModifyFilesystem( f"Could not write and/or chmod 0700 {ssh_dir} or {ssh_dir}/authorized_keys, Exception: {e}") try: pwdnam = pwd.getpwnam(self.user) os.chown(ssh_dir, pwdnam[2], pwdnam[3]) # 2=>uid, 3=>gid os.chown(ssh_dir + "authorized_keys", pwd.getpwnam(self.user)[2], pwd.getpwnam(self.user)[3]) except OSError as e: # by os.chown raise lib.UserExceptions.ModifyFilesystem( f"Could not chown {ssh_dir} and/or authorized_keys to {self.user} and their group, Exception: {e}",) except KeyError as e: # by PWD raise lib.UserExceptions.General(f"PWD can't find {self.user}: {e}") return True @staticmethod def write_ssh(key: str, ssh_dir: str) -> None: """ Write SSH key to a specified directory(appends authorized_keys itself!) :param key: Key to write :type key: str :param ssh_dir: SSH Directory to write to :type ssh_dir: str :return: None """ with open(ssh_dir + "authorized_keys", "w") as f: print(key, file=f) f.close() os.chmod(ssh_dir + "authorized_keys", 0o700) # we dont care about the directory here def lock_user_pw(self, cc: tuple = tuple(["usermod", "--lock"])) -> bool: """Lock a users password so it stays empty :param cc: Commands to run in the subprocess to lock it down(defaults to usermod --lock) :type cc: tuple :rtype: bool :return: True, if worked, raises lib.UserExceptions.UnknownReturnCode when not :raises: lib.UserExceptions.UnknownReturnCode: When cc returns something else then 0 """ lock_command = cc cc = lock_command + tuple([self.user]) if self.dry: self.printTuple(cc) return True elif not self.dry: rt = subprocess.call(cc) if rt != 0: raise lib.UserExceptions.UnknownReturnCode(f"Could not lock user '{self.user}'; '{cc}' returned '{rt}'") return True def add_to_usergroup(self, group: str = "tilde", cc: tuple = tuple(["usermod", "-a", "-G"])) -> bool: """ Adds a given user to a given group :param group: Groupname where you want to add your user to :type group: str :param cc: Commands to execute that adds your user to said specific group(defaults to usermod -a -G") :type cc: tuple :return: True, if worked, raises lib.UserExceptions.UnknownReturnCode when not :rtype bool :raises: lib.UserExceptions.UnknownReturnCode: if cc returned something else then 0 """ add_command = cc cc = add_command + tuple([group, self.user]) if self.dry: self.printTuple(cc) return True elif not self.dry: rt = subprocess.call(cc) if rt != 0: raise lib.UserExceptions.UnknownReturnCode( f"Could not add user '{self.user}' to group '{group}' with command '{cc}', returned '{rt}'",) return True @staticmethod def printTuple(tup: tuple) -> None: """Prints a tuple with spaces as separators :param tup: Tuple you want to print :type tup: tuple :rtype: None :returns: Nothing """ pp = "" for i in tup: pp += i + " " print(pp) def remove_user(self, cc: tuple = tuple(["userdel", "-r"])) -> bool: """Removes the specified user from the system :param cc: Commands to execute to delete the user from the System(defaults to userdel -r) :type cc: tuple :return: True, if worked, raises lib.UserExceptions.UnknownReturnCode when not :rtype: bool :raises: lib.UserExceptions.UnknownReturnCode: When cc returns something else then 0 or 6 """ remove_command = cc cc = remove_command + tuple([self.user]) if self.dry: self.printTuple(cc) return True else: ret = subprocess.Popen(cc, stdout=subprocess.PIPE, stderr=subprocess.PIPE) ret.wait() # wait for cc stdio, err_io = ret.communicate() # get stdout as well as stderr if ret.returncode != 0 and ret.returncode != 6: # userdel returns 6 when no mail dir was found but success raise lib.UserExceptions.UnknownReturnCode( f"Could not delete user with command {cc}. Return code: {ret.returncode}," f" stdout/stderr: {stdio+err_io}") return True if __name__ == "__main__": try: S = System("dar", dryrun=True) S.register() S.lock_user_pw() S.add_to_usergroup() # if not S.make_ssh_usable("dar", "SSHpub"): # print("Huh, error :shrug:") exit(0) except KeyboardInterrupt: pass