PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /lib/fixperms/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //lib/fixperms/fixperms_base.py |
"""Common fixperms classes""" import os from shlex import quote from pathlib import Path from typing import Union from stat import S_IMODE, S_ISREG, S_ISDIR, S_ISLNK import re from fixperms_cli import Args from fixperms_ids import IDCache class PermMap: """Base class for fixperms""" def __init__( self, ids: IDCache, args: Args, user: str, all_docroots: list[str], docroot_chmod: int, docroot_chown: tuple[str, str], ): self.args = args self.skip = self.args.skip.copy() self.all_docroots = all_docroots self.log = args.logger self.hard_links = HardLinkTracker(self) self.ids = ids self.user = user pwuser = self.ids.getpwnam(user) self.uid = pwuser.pw_uid self.gid = pwuser.pw_gid doc_uid = ids.getpwnam(docroot_chown[0]).pw_uid doc_gid = ids.getgrnam(docroot_chown[1]).gr_gid self.docroot_perms = Rule('', (None, docroot_chmod), (doc_uid, doc_gid)) self.homedir = os.path.realpath(pwuser.pw_dir) if not re.match(r'\/home\d*\/', self.homedir): raise ValueError(f"{user}: unexpected homedir: {self.homedir!r}") self.home_re = re.escape(pwuser.pw_dir) self.perm_map: list['Rule'] = [] def add_rule( self, regex: str, modes: tuple[Union[int, None], Union[int, None]], chown: tuple[int, int], ) -> None: """Add a fixperms path rule. ^HOMEDIR is automatically added""" # no actual ^ becasue we use .match, not .search self.perm_map.append(Rule(f"{self.home_re}{regex}", modes, chown)) def lchown(self, path: str, stat: os.stat_result, uid: int, gid: int): """Runs os.lchown""" if uid == gid == -1: return tgt_uid = stat.st_uid if uid == -1 else uid tgt_gid = stat.st_gid if gid == -1 else gid if (stat.st_uid, stat.st_gid) == (tgt_uid, tgt_gid): return if not self.args.noop: try: os.lchown(path, uid, gid) except OSError as exc: self.log.error(exc) return old_user = self.ids.uid_label(stat.st_uid) old_group = self.ids.gid_label(stat.st_gid) new_user = self.ids.uid_label(tgt_uid) new_group = self.ids.gid_label(tgt_gid) self.log.debug( 'Changed ownership of %s from %s:%s to %s:%s', quote(path), old_user, old_group, new_user, new_group, ) def lchmod( self, path: str, stat: os.stat_result, mode: Union[int, None], ): """Runs os.chmod if the path is not a symlink""" if mode is None: return orig = S_IMODE(stat.st_mode) if orig == mode: return if S_ISLNK(stat.st_mode): return # Linux does not support follow_symlinks=False if not self.args.noop: try: os.chmod(path, mode) except OSError as exc: self.log.error(exc) return self.log.debug( 'Changed mode of %s from %s to %s', quote(path), oct(orig)[2:], oct(mode)[2:], ) def walk(self, path: str, ignore_skips: bool = False): """os.walk/os.lstat to yield a path and all of its contents""" for entry in self._walk(path, ignore_skips): try: stat = os.lstat(entry) except OSError as exc: self.log.error(exc) continue yield stat, entry def _walk(self, top_dir: str, ignore_skips: bool = False): if not ignore_skips and self.should_skip(top_dir): return yield top_dir if not os.path.isdir(top_dir): return for dirpath, dirnames, filenames in os.walk(top_dir): for filename in filenames: path = os.path.join(dirpath, filename) if ignore_skips or not self.should_skip(path): yield path skip_dirs = [] for dirname in dirnames: path = os.path.join(dirpath, dirname) if not ignore_skips and self.should_skip(path): skip_dirs.append(path) else: yield path if skip_dirs: # editing dirnames[:] in-place causes os.walk to not traverse it dirnames[:] = [x for x in dirnames if x not in skip_dirs] def run(self) -> None: """To be called from fixperms_main.py - processes this user""" self.fixperms() self.hard_links.handle() def fixperms(self) -> None: """Iterate over a user's files and chown/chmod as needed""" for stat, path in self.walk(self.homedir): try: self.check_path(stat, path) except OSError as exc: self.log.error(exc) def with_exec_bits(self, stat: os.stat_result, new_mode: Union[None, int]): """Get a new file mode including old mode's exec bits""" if new_mode is None: return None if self.args.preserve_exec: exec_bits = stat.st_mode & 0o111 return new_mode | exec_bits return new_mode def check_path(self, stat: os.stat_result, path: str): """Chown and chmod files as necessary""" rule = self.find_rule(str(path)) file_mode, dir_mode = rule.modes if S_ISREG(stat.st_mode): # path is a regular file new_mode = self.with_exec_bits(stat, file_mode) if stat.st_nlink > 1: self.hard_links.add(path, stat, rule.chown, new_mode) return elif S_ISDIR(stat.st_mode): # path is a directory new_mode = dir_mode elif S_ISLNK(stat.st_mode): # path is a symlink new_mode = None else: # path is socket/device/fifo/etc self.log.warning("Skipping unexpected path type at %s", path) return if new_mode is not None: self.lchmod(path, stat, new_mode) self.lchown(path, stat, *rule.chown) def find_rule(self, path: str) -> 'Rule': """Find the matching ``Rule`` for a given path""" assert isinstance(path, str) if path in self.all_docroots: return self.docroot_perms for rule in self.perm_map: if rule.regex.match(path): return rule raise ValueError(f"No matching rule for {path}") def should_skip(self, path: str): """Determine if a path should be skipped based on --skip args""" for skip in self.skip: if path == skip: return True if Path(path).is_relative_to(skip): return True return False class HardLinkTracker: """Tracks and handles hard links discovered while walking through a user's files""" def __init__(self, perm_map: PermMap): self.perm_map = perm_map self.chowns: dict[int, tuple[int, int]] = {} self.stats: dict[int, os.stat_result] = {} self.modes: dict[int, int] = {} self.paths: dict[int, list[str]] = {} def add( self, path: str, stat: os.stat_result, chown: tuple[int, int], mode: Union[int, None], ): """Used to add a hard link found during the fixperms run which might be unsafe to operate on""" inum = stat.st_ino self.stats[inum] = stat # will be the same for all ends of the link if inum in self.paths: self.paths[inum].append(path) else: self.paths[inum] = [path] if inum in self.chowns: uid, gid = chown prev_uid, prev_gid = self.chowns[inum] if uid == -1: uid = prev_uid if gid == -1: gid = prev_gid self.chowns[inum] = [uid, gid] else: self.chowns[inum] = chown if mode is not None: self.modes[inum] = mode def handle(self): """If self.hard_links was populated with any items, handle any that are safe, or log any that are not""" for inum, stat in self.stats.items(): # for each distinct inode found with hard links... if stat.st_nlink == len(self.paths[inum]): # If we came across every end of the link in this run, then it's # safe to operate on. Chmod the first instance of it; the rest # will change with it. path = self.paths[inum][0] self.perm_map.lchown(path, stat, *self.chowns[inum]) self.perm_map.lchmod(path, stat, self.modes.get(inum, None)) continue # Otherwise these hard links can't be trusted. for path in self.paths[inum]: self.perm_map.log.error( '%s is hardlinked and not owned by the user', quote(path), ) class Rule: """Fixperms path rule""" def __init__( self, regex: str, modes: tuple[Union[int, None], Union[int, None]], chown: tuple[int, int], ): """Fixperms path rule Args: regex (str): regular expression file tuple[(int | None), (int | None)]: (file, dir) modes if matched chown tuple[int, int]: if a matching file/dir is found, chown to this UID/GID. Use -1 to make no change. """ self.regex = re.compile(regex) assert isinstance(modes, tuple) assert isinstance(chown, tuple) self.modes = modes self.chown = chown