PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /opt/sharedrads/ |
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 : //opt/sharedrads/suspend_user |
#!/opt/imh-python/bin/python3 import grp import sys import re import pwd import os import time import locale import subprocess import syslog import platform from pathlib import Path from argparse import ArgumentParser from typing import Literal, Union from pp_api import PowerPanel from rads.color import red import rads pp = PowerPanel() # Set locale so that the time zone will be logged properly locale.setlocale(locale.LC_TIME, '') LOCK_FILE = Path('/var/lock/suspend_user') LOCK_WAIT = 30 # Seconds to wait to acquire a lock LOG_FILE = '/var/log/suspension.log' # Suspension log PROCNAME = Path(sys.argv[0]).name # Are we suspending or unsuspending? "unsuspend_user" is a symlink # to this script SUSPEND = 'unsuspend' not in PROCNAME # Is the script being run from a terminal? TTY = sys.stdout.isatty() REASON_LIST = ( 'security', 'ra', 'moved', 'billing', 'canceled', 'tos', 'legal', 'orphan', 'donotterm', 'other', ) def get_groups(user: str) -> list[str]: groups = [g.gr_name for g in grp.getgrall() if user in g.gr_mem] gid = pwd.getpwnam(user).pw_gid groups.append(grp.getgrgid(gid).gr_name) return groups def non_root_safe(user): if SUSPEND: return user = rads.get_login() if user == 'root': return groups = get_groups(user) if ( 'tier2s@ipa.imhtech.net' in groups or 'managedhosting@ipa.imhtech.net' in groups ): return legit_unsuspend = ['billing', 'moved', 'orphan', 'canceled'] path = Path('/var/cpanel/suspended', user.lstrip('/')) if not path.is_file(): return try: data = path.read_text(encoding='utf-8') except FileNotFoundError: return try: suspension_reason = data.splitlines()[0].split(':', maxsplit=1)[0] except (IndexError, ValueError): sys.exit( 'Unable to determine the reason for suspension. ' 'Please consult tier2s.' ) issues = [ match for match in legit_unsuspend if match in suspension_reason.lower() ] if not issues: sys.exit( "Only root allowed to unsuspend people that are suspended " f"for {suspension_reason}" ) def print_red(msg: str): if TTY: print(red(msg)) else: print(msg) def logdate(): """Returns the current date and time in a format suitable for inclusion in a log file""" return time.strftime("%Y-%m-%d:%H:%M:%S %Z", time.localtime(time.time())) def log_suspension( user: str, susp_type: str, caller: str, reason: str, duration: str, comment: Union[str, None], ): """Logs account suspension and unsuspension events""" if not comment: comment = '-' blame = get_calling_username() susp_type = susp_type.upper() entry = f'{user} [{susp_type}] {reason} {duration} {blame} "{comment}"' syslog.openlog(PROCNAME) syslog.syslog(entry) acquire_lock() with open(LOG_FILE, 'a', encoding='utf-8') as file: file.write(f'{logdate()} {os.path.basename(caller)}: {entry}\n') LOCK_FILE.unlink(missing_ok=True) def get_calling_username(): try: blame = f'{os.getlogin()}:{pwd.getpwuid(os.geteuid()).pw_name}' except OSError: blame = pwd.getpwuid(os.geteuid()).pw_name return blame def send_suspension_email(user: str, comment: str, is_temp=False, duration=0): """Send suspension email""" # IMH - Normal suspension id 7 # IMH - Temp suspension id 135 variable1 == duration # WHH - Normal suspension id 322 # WHH - Temp suspension id 392 variable1 == duration # Reseller - Child acct suspension id 515 variable1 == user # Reseller - Child account temp suspension id 516 variable1 == duration, # variable2 == user if 'hub' in platform.node(): template_id = 392 if is_temp else 322 send_to = user else: # If the customer is the child of a reseller, use the # reseller template instead owner = rads.get_owner(user) if owner not in rads.OUR_RESELLERS: template_id = 516 if is_temp else 515 send_to = owner else: template_id = 135 if is_temp else 7 send_to = user duration = duration / 60 template_info = pp.call("notification.fetch-template", template=template_id) if template_info.status == 0: variables = {} for variable in template_info.data['variables']: if variable['description'] == "Child User": variables[variable['name']] = user else: variables[variable['name']] = "%s minutes" % duration response = pp.call( "notification.send", template=template_id, cpanelUser=send_to, **variables, ) if response.status == 0: print("Sent email, review at %s" % response.data['reviewUrl']) logged_in_user = get_calling_username().split(':')[0] if logged_in_user == "root": reporter = "auto" else: reporter = logged_in_user pp( 'hosting.insert-note', user=user, admin_user=reporter, flagged=True, type='Suspension', # Prepend user to the note because the hosting.insert-note # endpoint doesn't seem to honor the 'admin_user' parameter # This issue is tracked in Devel #4775 # https://trac.imhtech.net/Development/ticket/4775 note=f'{reporter}: {comment}', ) return print_red( "Could not send suspension email or note acct, please do this manually!" ) def suspend_unsuspend(args): """Suspends or unsuspends based on the value of the global SUSPEND""" if SUSPEND: action = 'suspend' cmd = [f"/scripts/{action}acct", args.user] cmd.append(f'{args.reason}:{args.comment}') if args.lock or not args.log_only: cmd.append("1") # lock else: action = 'unsuspend' cmd = [f"/scripts/{action}acct", args.user] try: subprocess.check_call(cmd, env={'RADSSUSPEND': 'True'}) except (OSError, subprocess.CalledProcessError): print(f'WARNING: Account may not have been properly {action}ed!') def suspend_special(args, type_str: Literal['suspended', 'autosuspend']): """ Takes the option autosuspend or suspended (the path where sharedrads expects to find files for scheduled and temp suspensions """ # Sched and temp suspensions expect slightly different data if type_str == 'suspended': data = args.duration_str else: data = args.reason.lower() if not os.path.exists('/opt/sharedrads/%s' % type_str): os.mkdir('/opt/sharedrads/%s' % type_str) path = Path('/opt/sharedrads', type_str, args.user) with open(path, 'w', encoding='utf-8') as file: # write future timestamp file.write(f'{args.duration_secs + int(time.time())} {data}\n') def archive_logs(user: str): user_pwd = pwd.getpwnam(user) dotlogs_file = os.path.join('/home', user, '.cpanel-logs') with open(dotlogs_file, 'w', encoding='utf-8') as logpref_file: logpref_file.write('archive-logs=1\nremove-old-archived-logs=1\n') os.chown(dotlogs_file, user_pwd.pw_uid, user_pwd.pw_gid) def acquire_lock(): if not LOCK_FILE.exists(): LOCK_FILE.write_text(str(os.getpid()), encoding='ascii') return pid = int(LOCK_FILE.read_text(encoding='ascii')) try: cmdline = Path('/proc', str(pid), 'cmdline').read_text(encoding='ascii') except OSError: print(f"Stale lock file found, but {pid} isn't running. Moving on...") LOCK_FILE.write_text(str(os.getpid()), encoding='ascii') return # Hard code suspend_user because sys.argv[0] could be unsuspend_user if 'suspend_user' not in cmdline: print( f"Stale lock file found, but {pid}", "isn't an instance of this script. Moving on...", ) LOCK_FILE.write_text(str(os.getpid()), encoding='ascii') return expired = False print("Waiting for account suspension lock", end=' ') for _ in range(LOCK_WAIT): print('.', end=' ', flush=True) time.sleep(1) if not LOCK_FILE.exists(): expired = True print("Process", pid, "released its lock") break if not expired: print( f"Process {pid} hasn't released its lock.", "Stealing the lock file forcibly", ) LOCK_FILE.write_text(str(os.getpid()), encoding='ascii') def parse_args(): if SUSPEND: parser = ArgumentParser( description='Suspend user', usage="%(prog)s username [duration|delay] [options] " "[--sched|-s] [--lock|-l] -r reason", ) else: parser = ArgumentParser( description='Unsuspend user', usage='%(prog)s username [options]', ) # fmt: off parser.add_argument( "-c", "--comment", dest="comment", help="additional comment to place in the suspension log", ) if not TTY: parser.add_argument("--info", action="store_true", dest="info") parser.add_argument("--invoked-by", dest="caller", default="unknown") if SUSPEND: parser.add_argument( "-s", "--sched", action="store_true", dest="sched", help="do not suspend immediately; schedule a suspension " "to occur later", ) parser.add_argument( "-l", "--lock", action="store_const", const="lock", dest="lock", help="lock the suspension: non-root reseller cannot unsuspend", ) reason_group = parser.add_mutually_exclusive_group(required=True) reason_group.add_argument( "-r", "--reason", dest="reason", choices=REASON_LIST, help="reason for the suspension", ) reason_group.add_argument( "--ra", action="store_const", const="ra", dest="reason", help="suspended for RA", ) reason_group.add_argument( "--moved", "--move", action="store_const", const="moved", dest="reason", help="account was moved", ) reason_group.add_argument( "--billing", action="store_const", const="billing", dest="reason", help="suspended for billing purposes", ) reason_group.add_argument( "--canceled", action="store_const", const="canceled", dest="reason", help="suspended because of cancellation", ) reason_group.add_argument( "--tos", action="store_const", const="tos", dest="reason", help="suspended for ToS violation", ) reason_group.add_argument( "--legal", action="store_const", const="legal", dest="reason", help="suspended for legal reasons", ) reason_group.add_argument( "--security", action="store_const", const="security", dest="reason", help="suspended for security violation", ) reason_group.add_argument( "--orphan", action="store_const", const="orphan", dest="reason", help="account is orphaned", ) reason_group.add_argument( "--donotterm", action="store_const", const="donotterm", dest="reason", help="do not terminate account", ) # fmt: on args, extras = parser.parse_known_args() if TTY: args.info = None args.caller = None args.user = None args.log_only = False args.duration_secs = None args.duration_str = '' # Regular expression that must be valid for temp or scheduled suspensions time_re = re.compile(r'^(\d+)([dhm])$') # Number of seconds in a minute, hour, day secs = {'m': 60, 'h': 3600, 'd': 86400} # loop through positional args for pos_arg in extras: try: uid = pwd.getpwnam(pos_arg).pw_uid except Exception: uid = None # If string exists in the password database and is a cPanel user # Assign as the user name to act upon if uid is not None and os.path.exists(f'/var/cpanel/users/{pos_arg}'): args.user = pos_arg # If string matches the time regexp set it as args.duration_* elif match := time_re.match(pos_arg): dur, unit = match.groups() args.duration_secs = int(dur) * secs[unit] args.duration_str = pos_arg elif 'log_only' in pos_arg: args.log_only = True # If string doesn't match assume it's the comment else: args.comment = pos_arg # Require a valid user if args.user is None: print("ERROR: No valid user specified") sys.exit(1) return args def main(): args = parse_args() if args.caller: invoking_process = args.caller else: invoking_process = PROCNAME if args.log_only and 'RADSSUSPEND' in os.environ: # Don't actually suspend, just update the suspension log, this # condition is meant to be triggered by cPanel suspension hooks if SUSPEND: log_suspension( user=args.user, susp_type='cp_suspension', caller=invoking_process, reason='-', duration='perm', comment=args.comment, ) archive_logs(args.user) else: log_suspension( user=args.user, susp_type='cp_unsuspension', caller=invoking_process, reason='-', duration='-', comment=args.comment, ) archive_logs(args.user) elif SUSPEND: # If invoked with the --info option just add a note # Only maint scripts should call suspend_user in this fashion if args.reason.lower() == "donotterm" and not args.comment: args.comment = "Marked as Do Not Terminate" if not TTY and args.info: log_suspension( user=args.user, susp_type='info', caller=invoking_process, reason=args.reason, duration='-', comment=args.comment, ) # Is the user already suspended? elif not rads.cpuser_suspended(args.user): # Display an error and exit if a scheduled suspension is chosen # but d is None if args.duration_secs is None and args.sched: print("ERROR: Scheduled suspensions require a valid delay!") sys.exit(1) # Temp suspension elif args.duration_secs is not None and not args.sched: print_red( f'Suspending {args.user} (reason: {args.reason}) ' f'for {args.duration_secs} seconds ({args.duration_str}) ' f'with comment "{args.comment}"' ) if args.reason.lower() == "ra": if not args.comment: print_red( "Account note is required for RA suspensions! " "Not suspending user!" ) sys.exit(1) send_suspension_email( args.user, args.comment, is_temp=True, duration=args.duration_secs, ) log_suspension( user=args.user, susp_type='suspension', caller=invoking_process, reason=args.reason, duration='temp:%s' % args.duration_str, comment=args.comment, ) suspend_special(args, 'suspended') suspend_unsuspend(args) archive_logs(args.user) # Sched suspension elif args.duration_secs is not None and args.sched: print_red( f'Suspending {args.user} (reason: {args.reason}) ' f'{args.duration_secs} seconds ({args.duration_str}) ' f'from now with comment "{args.comment}"' ) log_suspension( user=args.user, susp_type='sched_suspension', caller=invoking_process, reason=args.reason, duration='sched:%s' % args.duration_str, comment=args.comment, ) suspend_special(args, 'autosuspend') # Auto suspension elif not TTY and 'autosuspend' in args.caller: log_suspension( user=args.user, susp_type='auto_suspension', caller=invoking_process, reason=args.reason, duration='perm', comment=args.comment, ) suspend_unsuspend(args) archive_logs(args.user) # Perm suspension elif args.duration_secs is None and not args.sched: print_red( f'Suspending {args.user} (reason: {args.reason}) ' f'permanently with comment "{args.comment}"' ) if args.reason.lower() == "ra": if not args.comment: print_red( "Account note is required for RA suspensions! " "Not suspending user!" ) sys.exit(1) send_suspension_email( args.user, args.comment, is_temp=False ) log_suspension( user=args.user, susp_type='suspension', caller=invoking_process, reason=args.reason, duration='perm', comment=args.comment, ) suspend_unsuspend(args) archive_logs(args.user) else: print("ERROR: %s already appears to be suspended!" % args.user) sys.exit(1) else: if rads.cpuser_suspended(args.user): non_root_safe(args.user) print_red( 'Unsuspending {} with comment "{}"'.format( args.user, args.comment ) ) log_suspension( user=args.user, susp_type='unsuspension', caller=invoking_process, reason='-', duration='-', comment=args.comment, ) suspend_unsuspend(args) try: os.unlink('/opt/sharedrads/suspended/%s' % args.user) print_red('Removed RADS temp suspension file') except Exception: pass else: print("ERROR: %s does not appear to be suspended!" % args.user) sys.exit(1) if __name__ == '__main__': main()