PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/thread-self/root/proc/self/root/proc/self/root/opt/maint/bin/ |
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 : //proc/thread-self/root/proc/self/root/proc/self/root/opt/maint/bin/audit_resellers.py |
#!/opt/imh-python/bin/python3 """ Reseller audit script. Does the following: 1) Makes sure that all resellers are owned by 'inmotion' or 'hubhost' 2) Resets reseller ACL limits and IP pools 3) Checks for orphaned accounts (accounts that have a non-existent owner) """ from collections import defaultdict import configparser import argparse import logging import platform import sys import time import pwd from pathlib import Path from typing import Union import yaml import rads from cpapis import whmapi1, CpAPIError APIPA = '169.254.100.100' # the old moveuser used this for reseller moves HOST = platform.node().split('.')[0] RESELLER = 'hubhost' if rads.IMH_CLASS == 'hub' else 'inmotion' def parse_args() -> tuple[int, bool]: """Parse sys.argv""" parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( '--loglevel', '-l', default='INFO', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], ) parser.add_argument( '--noop', '--dry-run', '-n', dest='noop', action='store_true', help="Make no changes", ) args = parser.parse_args() loglevel = getattr(logging, args.loglevel) return loglevel, args.noop def get_dips() -> dict[str, set[str]]: """Get a mapping of ipaddr -> resellers from /var/cpanel/dips""" dips = defaultdict(set) try: for res_path in Path('/var/cpanel/dips').iterdir(): try: res_ips = set(res_path.read_text('ascii').split()) except OSError: continue try: res_ips.remove(APIPA) except KeyError: pass for ipaddr in res_ips: dips[ipaddr].add(res_path.name) except FileNotFoundError: pass return dict(dips) def check_double_ip_delegations(resellers: set[str], noop: bool): """Check for IPs which are assigned to more than one reseller""" double_delegations = { ipaddr: resellers for ipaddr, resellers in get_dips().items() if len(resellers) > 1 } if double_delegations: auto_fix_double_dips(resellers, double_delegations, noop) if not double_delegations: return logging.warning("Double-delegated IP addresses detected - sending ticket") logging.debug('double delegations: %r', double_delegations) if noop: return body = ( "The following IP addresses were detected as being delegated to " "more than one reseller and must be corrected:\n" ) for ip_addr, res in double_delegations.items(): body = f"{body}\n{ip_addr}: {', '.join(res)}" rads.send_email( to_addr="str@imhadmin.net", subject="Reseller IP delegation conflict", body=body, ) def auto_fix_double_dips( resellers: set[str], double_delegations: dict[str, set[str]], noop: bool ): """Attempt to automatically fix IP double-delegations by checking if the IP is actually in use, and removing it from resellers which aren't using it""" user_ips: dict[str, str] = yaml.load( Path('/etc/userips').read_text('ascii'), rads.DumbYamlLoader ) user_resellers: dict[str, str] = yaml.load( Path('/etc/trueuserowners').read_text('ascii'), rads.DumbYamlLoader ) user_resellers = { k: k if k in resellers else v for k, v in user_resellers.items() } for ipaddr, res in double_delegations.copy().items(): if res.intersection(rads.OUR_RESELLERS): # if there's a conflict involving one of our resellers, don't try # to auto-fix it continue # collect resellers actually using the IP using = list( {user_resellers[k] for k, v in user_ips.items() if v == ipaddr} ) if len(using) > 1: continue # legit conflict. don't auto-fix if len(using) == 0: # No one is using this IP. Take it away from all but one reseller. # If this takes away any reseller's last IP, the next run of this # cron should fix it. for remove in list(res)[1:]: remove_dip(ipaddr, remove, double_delegations, noop) elif using[0] in res: # else one reseller is using it but it's delegated to multiple for remove in list(res): if remove != using[0]: remove_dip(ipaddr, remove, double_delegations, noop) def remove_dip( ipaddr: str, reseller: str, double_delegations: dict[str, set[str]], noop: bool, ) -> None: """Remove an IP from a reseller's pool to fix a double delegation""" # make sure it wasn't their main. the calling function already checked that # the reseller didn't have it assigned main_ip = Path('/var/cpanel/mainips', reseller).read_text('ascii').strip() if main_ip == ipaddr: return logging.warning("removing %s from %s's IP pool", ipaddr, reseller) pool = whmapi1.getresellerips(reseller)['ip'] try: pool.remove(ipaddr) except ValueError: # but the previous lookup had it? logging.error("Could not remove %s from %s's IP pool", ipaddr, reseller) return if not noop: try: whmapi1.setresellerips(reseller, pool, delegate=True) except CpAPIError as exc: logging.error( "Could not remove %s from %s's IP pool: %s", ipaddr, reseller, exc, ) return double_delegations[ipaddr].remove(reseller) if len(double_delegations[ipaddr]) < 2: double_delegations.pop(ipaddr) class CpanelConf(configparser.ConfigParser): """Handles reading /var/cpanel/users and /var/cpanel/packages files""" def __init__(self, path: Path): super().__init__(allow_no_value=True, interpolation=None, strict=False) try: self.read_string(f"[config]\n{path.read_text('utf-8')}") except Exception as exc: logging.error('%s - %s: %s', path, type(exc).__name__, exc) raise @classmethod def user_conf(cls, user: str): """Read /var/cpanel/users/{user}""" return cls(Path('/var/cpanel/users', user)) @classmethod def pkg_conf(cls, pkg: str): """Read /var/cpanel/packages/{pkg}""" return cls(Path('/var/cpanel/packages', pkg)) @property def res_limits(self) -> dict[str, str]: """Read imh custom reseller limits from a cPanel package (use only with pkg_conf)""" imh_keys = ( 'account_limit', 'bandwidth_limit', 'diskspace_limit', 'enable_account_limit', 'enable_resource_limits', 'enable_overselling', 'enable_overselling_bandwidth', 'enable_overselling_diskspace', ) return { x: self.get('config', f'imh_{x}', fallback='') for x in imh_keys } def get_main_ips() -> set[str]: """Collect IPs from /var/cpanel/mainip and /var/cpanel/mainips/root""" with open('/var/cpanel/mainip', encoding='ascii') as ip_file: ips = set(ip_file.read().split()) try: with open('/var/cpanel/mainips/root', encoding='ascii') as ip_file: ips.update(ip_file.read().split()) except FileNotFoundError: pass return ips def get_new_ip() -> str: """Get an IP which is not already in use""" with open('/etc/ipaddrpool', encoding='ascii') as pool: # not assigned as dedicated, but may be in a reseller pool unassigned = pool.read().split() for ip_addr in unassigned: if not assigned_to_res(ip_addr): return ip_addr return '' def assigned_to_res(ip_addr): """Determine if an IP is already delegated to a reseller""" for entry in Path('/var/cpanel/dips').iterdir(): with entry.open('r', encoding='ascii') as dips: if ip_addr in dips.read().split(): return True return False def non_res_checks(noop: bool): """Reseller-owner checks on non-reseller servers""" for path in Path('/var/cpanel/users').iterdir(): user = path.name if user == 'root': logging.warning('%s exists. Skipping.', path) continue if user in rads.OUR_RESELLERS: try: whmapi1.set_owner(user, 'root') except CpAPIError as exc: logging.error( "Error changing owner of %s to root: %s", user, exc ) continue try: user_conf = CpanelConf.user_conf(user) except Exception: continue try: owner = user_conf.get('config', 'owner') except configparser.NoOptionError: logging.warning( '%s is missing OWNER and may not be a valid CPanel user file', path, ) continue if owner != RESELLER: set_owner(user, owner, RESELLER, noop) def get_resellers() -> set[str]: """Read resellers from /var/cpanel/resellers""" resellers = set() with open('/var/cpanel/resellers', encoding='utf-8') as res_file: for line in res_file: if res := line.split(':', maxsplit=1)[0]: resellers.add(res) return resellers def main(): """Cron main""" loglevel, noop = parse_args() if noop: logfmt = '%(asctime)s %(levelname)s NOOP %(message)s' else: logfmt = '%(asctime)s %(levelname)s %(message)s' rads.setup_logging( path=None, loglevel=loglevel, fmt=logfmt, print_out=sys.stdout ) if rads.IMH_ROLE != 'shared': logging.critical("rads.IMH_CLASS=%r", rads.IMH_ROLE) sys.exit(1) if 'res' in HOST and rads.IMH_CLASS != 'reseller': logging.critical( "hostname=%r but rads.IMH_CLASS=%r", HOST, rads.IMH_CLASS ) sys.exit(1) resellers = get_resellers() all_res = resellers | set(rads.OUR_RESELLERS) | {"system", rads.SECURE_USER} if rads.IMH_CLASS == 'reseller': main_ips = get_main_ips() for reseller in resellers: res_checks(reseller, main_ips, noop) orphan_storage = defaultdict(list) term_fails = defaultdict(list) for entry in Path("/var/cpanel/users").iterdir(): user = entry.name if user in all_res: continue try: pwd.getpwnam(user) except KeyError: logging.warning("Removing erroneous file at %s", entry) if not noop: entry.unlink() continue check_orphans(user, main_ips, orphan_storage, term_fails, noop) for reseller, orphans in orphan_storage.items(): orphans_notify(reseller, orphans, noop) for reseller, orphans in term_fails.items(): term_fail_notice(reseller, orphans, noop) else: non_res_checks(noop) cleanup_delegations(all_res, noop) check_double_ip_delegations(resellers, noop) def cleanup_delegations(all_res: set[str], noop: bool): """Remove /var/cpanel/dips (ip delegation) files for deleted resellers""" for entry in Path('/var/cpanel/dips').iterdir(): if entry.name not in all_res: logging.debug('deleting %s', entry) if not noop: entry.unlink() def check_orphans( user: str, main_ips: set[str], orphan_storage: defaultdict[list], term_fails: defaultdict[list], noop: bool, ): """Find orphaned accounts (accounts that have no existing owner)""" try: user_conf = CpanelConf.user_conf(user) except Exception: return owner = user_conf.get('config', 'owner', fallback=None) if not owner: return ip_address = user_conf.get('config', 'ip', fallback=None) if ( not Path('/var/cpanel/users', owner).exists() or owner in rads.OUR_RESELLERS ): # this is an orphaned account try: susp_time = Path('/var/cpanel/suspended', user).stat().st_mtime except FileNotFoundError: # the orphaned account is not suspended orphan_storage[owner].append(user) return # If the orphan is suspended for more than 14 days, terminate it if time.time() - susp_time > 14 * 86400: logging.info("Terminating suspended orphan user %s", user) if noop: return try: whmapi1.removeacct(user, keepdns=False) except CpAPIError as exc: logging.warning("Failed to terminate user %s: %s", user, exc) term_fails[owner].append(user) else: logging.debug( "Orphaned user %s has not been suspended long " "enough for auto-terminate", user, ) return # This is a non-orphaned, child account. # While we're here, make sure the user's IP is correct. if not ip_address or ip_address in main_ips: # Assign the user their owner's IP set_child_owner_ip(user, owner, noop) def orphans_notify(reseller: str, orphans: list[str], noop: bool) -> None: """Notify for unsuspended orphan accounts""" logging.warning( '%s orphaned accounts exist under the reseller %s. Sending STR.', len(orphans), reseller, ) logging.debug('Orphans under %s: %r', reseller, orphans) if noop: return str_body = f""" The following orphan accounts have been located under owner {reseller}: {' '.join(orphans)} They appear to have an owner that does not exist, or is a reseller missing reseller privileges. If the orphan's owner exists in PowerPanel, please set their owner to 'inmotion' or 'hubhost' as appropriate. If the orphan's owner is a reseller, add reseller privileges. If the orphan account does not exist, please suspend them on the server with the command "for orphan in {' '.join(orphans)}; do suspend_user $orphan -r orphan; done" Thank you, {HOST}""" rads.send_email( to_addr="str@imhadmin.net", subject=f"Orphan accounts on {HOST} with owner {reseller}", body=str_body, ) def term_fail_notice(reseller: str, orphans: list[str], noop: bool) -> None: """Separate notification for orphans that failed to auto-term, because suspending them again won't fix the problem""" logging.warning( "%s orphaned accounts failed to auto-terminate under the reseller %s. " "Sending STR.", len(orphans), reseller, ) logging.debug("terms failed for %r", orphans) if noop: return str_body = f""" The following orphan accounts were found under owner {reseller} and were suspended long enough to auto-terminate, but auto-termination failed: {' '.join(orphans)} Please investigate and if appropriate, run removeacct on the orphan accounts. Thank you, {HOST}""" rads.send_email( to_addr="str@imhadmin.net", subject=f"Failed to auto-term orphans on {HOST} with owner {reseller}", body=str_body, ) def set_child_owner_ip(user: str, owner: str, noop: bool) -> None: """Assign the user their owner's IP""" try: owner_conf = CpanelConf.user_conf(owner) except Exception: owner_ipaddr = None else: owner_ipaddr = owner_conf.get('config', 'ip') if not owner_ipaddr: logging.error( "User %s has shared IP, but couldn't determine the IP of " "the owner %s to assign it to the child account", user, owner, ) return logging.warning( "User %s has shared IP. Changing to owner %s's IP of %s", user, owner, owner_ipaddr, ) if noop: return try: whmapi1.setsiteip(user, owner_ipaddr) except CpAPIError as exc: logging.error( "Error changing IP of %s to %s: %s", user, owner_ipaddr, exc ) def set_owner(user: str, old: str, new: str, noop: bool): """Change user owner and log""" logging.info("Changing ownership of %s from %s to %s", user, old, new) if noop: return try: whmapi1.set_owner(user, new) except CpAPIError as exc: logging.error( "Error changing ownership of %s to %s: %s", user, new, exc ) def res_checks(user: str, main_ips: set[str], noop: bool): """All reseller and IP checks for res servers""" try: user_conf = CpanelConf.user_conf(user) except Exception: return if Path('/var/cpanel/suspended', user).exists(): return if user not in rads.OUR_RESELLERS: owner_needed = RESELLER # 1) Reset the reseller ACL to match the package name if pkg := set_reseller_acl(user, user_conf, noop): try: package_conf = CpanelConf.pkg_conf(pkg) except Exception: return # 2) Reset the reseller's resource limits set_reseller_resource_limits(user, package_conf, noop) else: owner_needed = 'root' # 3) Make sure the reseller itself is owned by the correct user owner = user_conf.get('config', 'owner', fallback=None) if owner != owner_needed: set_owner(user, owner, owner_needed, noop) if user not in rads.OUR_RESELLERS: setup_dips(user, user_conf, main_ips, noop) def setup_dips( user: str, user_conf: CpanelConf, main_ips: set[str], noop: bool ): """Create a dedicated IP pool for resellers that don't have one""" # This is necessary to prevent resellers from having access to assign all # IPs on a server ipaddr = user_conf.get('config', 'ip', fallback='') if not ipaddr or ipaddr in main_ips: # Assign the user a new IP ### if ipaddr := get_new_ip(): logging.info("Assigning reseller %s its own IP %s", user, ipaddr) if not noop: try: whmapi1.setsiteip(user, ipaddr) except CpAPIError as exc: logging.error( "Error changing IP of %s to %s: %s", user, ipaddr, exc ) else: logging.error("Could not find an unused IP to assign to %s", user) return set_reseller_mainip(user, ipaddr, noop) # check if user has a dedicated ip pool if Path(f'/var/cpanel/dips/{user}').exists(): current = set(whmapi1.getresellerips(user)['ip']) pool = {x for x in current if x not in main_ips} pool.add(ipaddr) # whmapi1 getresellerips returns all free ips if no delegation exists else: current = None pool = {ipaddr} if current != pool: logging.info( "Changing IP delegation for %s from %r to %r", user, current, pool ) if noop: return try: whmapi1.setresellerips(user, pool, delegate=True) except CpAPIError as exc: logging.error( "Error changing IP delegation for %s to %r: %s", user, pool, exc ) def set_reseller_acl( user: str, user_conf: CpanelConf, noop: bool ) -> Union[str, None]: """Reset the reseller ACL to match the package name""" pkg = user_conf.get('config', 'plan', fallback=None) if not pkg or not Path('/var/cpanel/acllists', pkg).exists(): # This means the reseller is set to a plan that likely isn't configured # on the server. If this is the case, strip their ACL (just to be safe) pkg = None logging.debug("Setting reseller %s to ACL %r", user, pkg) if noop: return pkg try: whmapi1.set_acllist(user, pkg) except CpAPIError as exc: logging.error("Error setting %s to ACL %s: %s", user, pkg, exc) return pkg def set_reseller_mainip(user: str, ipaddr: str, noop: bool): """Call setresellermainip if needed""" try: current = Path('/var/cpanel/mainips', user).read_text('ascii').strip() except OSError: current = None if current == ipaddr: return logging.info('Setting main IP for %s to %s', user, ipaddr) if noop: return try: whmapi1( 'setresellermainip', args={'user': user, 'ip': ipaddr}, check=True ) except CpAPIError as exc: logging.error( "Could not set main IP for %s to %s: %s", user, ipaddr, exc ) def set_reseller_resource_limits( user: str, package_conf: CpanelConf, noop: bool ) -> None: """Reset the reseller's resource limits""" limit_kwargs = package_conf.res_limits.copy() if limit_kwargs['enable_resource_limits'] == '1': logging.debug( "Setting reseller limits for %s to %r", user, limit_kwargs ) limit_kwargs['user'] = user if noop: return try: whmapi1('setresellerlimits', args=limit_kwargs, check=True) except CpAPIError as exc: logging.error("Error setting reseller limits for %s: %s", user, exc) if __name__ == '__main__': main()