Mini Shell

Direktori : /usr/share/cagefs/
Upload File :
Current File : //usr/share/cagefs/generic_hook_lib.py

# -*- coding: utf-8 -*-

# CageFS generic hooks library
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT

from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

from future import standard_library
standard_library.install_aliases()
from builtins import *

import os
import subprocess
import sys
import logging

from clcommon.const import Feature
from clcommon.cpapi import getCPName, PLESK_NAME, is_admin, DIRECTADMIN_NAME, CPANEL_NAME, \
    is_panel_feature_supported
from clcommon.clpwd import ClPwd
from clcommon.utils import (
    get_file_lines,
    run_command,
    ExternalProgramFailed,
    is_user_present
)
from secureio import write_file_via_tempfile

# CageFS imports
sys.path.append('/usr/share/cagefs')
from cagefsctl import get_min_uid, MIN_UID, get_user_prefix, is_user_enabled

# for backward compatibility with old code & files
if getCPName() == 'DirectAdmin':
    _CAGEFS_EXCLUDE_FILE = '/etc/cagefs/exclude/directadmin.admins'
else:
    _CAGEFS_EXCLUDE_FILE = '/etc/cagefs/exclude/panel.admins'
_CAGEFS_BINARY = '/usr/sbin/cagefsctl'
_CAGEFS_SKELETON_BIN = '/usr/share/cagefs-skeleton/bin'

logger = logging.getLogger('clcommon.public_hooks')


def _call_with_logging(*args, **kwargs):
    """
    This file is mostly a copy-paste from bash where
    return code of process was ignored. In order not to
    break servers, we decided to ignore exit codes in python too.

    This method runs subprocess and silently logs errors
    if exit code was not zero. No errors raised.
    """
    p = subprocess.Popen(
        *args, stderr=subprocess.PIPE,
        stdout=subprocess.PIPE, text=True, **kwargs)
    stdout, stderr = p.communicate()

    logger.info('Executing %s', args)
    if p.returncode == 0:
        logger.debug('stdout: `%s`\nstderr:`%s`', stdout, stderr)
    else:
        logger.error('process %s dies with exit code %s'
                     ' and stdout: `%s`\nstderr:`%s`'
                     '', args, p.returncode, stdout, stderr)
    return p.returncode


def _is_user_uid_suitable_for_cagefs(username):
    """
    Checks if user can be places inside cage by his uid.
    :param username: unix user name
    :return: True or False
    """
    try:
        uid = ClPwd().get_uid(username)
    except ClPwd.NoSuchUserException:
        print("ERROR: No such user %s" % username)
        return False
    get_min_uid()           # Update MIN_UID global variable
    if uid < MIN_UID:
        print("SKIP: User %s uid is %d - too small. min_uid is %d" % (username, uid, MIN_UID))
        return False
    return True


def post_delete_admin_handler(admin_name):
    """
    Remove admin name from cagefs exclude file
    :param admin_name: admin name
    """
    if not os.path.exists(_CAGEFS_EXCLUDE_FILE):
        return "WARNING: exclude list %s does not exist" % _CAGEFS_EXCLUDE_FILE

    f_lines = get_file_lines(_CAGEFS_EXCLUDE_FILE)
    line_for_write = admin_name + '\n'
    if line_for_write in f_lines:
        f_lines.remove(line_for_write)
        write_file_via_tempfile(''.join(f_lines), _CAGEFS_EXCLUDE_FILE, 0o0600, suffix='tmp')
    return "OK"


def post_create_admin_handler(admin_name):
    """
    Triggered after creating new UNIX user for admin.
    :param admin_name: admin name
    """
    # get lines from file if it exists
    # otherwise empty array
    f_lines = get_file_lines(_CAGEFS_EXCLUDE_FILE)
    line_for_write = admin_name + '\n'
    if line_for_write not in f_lines:
        f_lines.append(line_for_write)
        write_file_via_tempfile(''.join(f_lines), _CAGEFS_EXCLUDE_FILE, 0o0600, suffix='tmp')

    # and also disable it in cagefs manually
    try:
        run_command([_CAGEFS_BINARY, '--disable', admin_name])
    except ExternalProgramFailed as e:
        return "cagefsctl utility failed: %s" % str(e)
    return "OK"


def _is_cpanel_restore_process():
    """
    cPanel calls post_create_user_handler during restore process
    (transferring or restoring from backup)
    But we should not do some actions in that case.

    Detect restoration process by control panel name and
    env variable that we set in cllib.
    :return: boolean
    """
    return getCPName() == CPANEL_NAME and os.environ.get('CPANEL_RESTORE', '0') == '1'


def post_create_user_handler(username):
    """
    Triggered after creating new user.
    :param username: account name

    It is important to have in mind that current handler
    is called twice during account restoring process on cPanel:
    first time - after account creation ("post_create_user" hook with envvar CPANEL_RESTORE = 1),
    second time - after the actual restore process ("post_restore_user" hook).
    """
    # Do nothing if admin. There is separate hook for admins
    if is_admin(username):
        return "SKIP: User %s is admin" % username

    # this logic was only for cpanel
    # but I do not see any reasons why
    # we cannot run it for other control panels
    if not _is_user_uid_suitable_for_cagefs(username):
        return "SKIP"

    # we use call below because previous implementation
    # of hooks did not care about cagefsctl failures
    _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--set-default-user-status', username])

    if not _is_cpanel_restore_process():
        # we don't run "create-namespace" after an account creation during restore process,
        # because it will cause in creating /var/cagefs/${prefix}/${username}/etc
        # when some user's config files (e.g. /home/${username}/.cl.selector/defaults.cfg)
        # are not exist yet.
        # This will still be called in "post_restore_user" hook (see this function description)
        if not is_panel_feature_supported(Feature.LVE):
            _call_with_logging([_CAGEFS_BINARY, '--create-namespace', username])

        # despite the fact that plesk calls this method for each
        # created domain or subdomain, we run cpetc for historical reasons
        if os.path.isdir(_CAGEFS_SKELETON_BIN):
            _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--cpetc', username])

    # we need this for plesk because of home dirs in /var/www/DOMAIN_NAME
    if getCPName() == PLESK_NAME:
        _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--remount-virtmp', username])
    return "OK"


def post_create_domain_handler(username, domain):
    """
    Triggered after creating additional domains in control panel.
    :param username: owner of the domain
    :param domain: name of the domain
    """
    if getCPName() != PLESK_NAME:
        print("WARNING: post create domain cagefs hook is not " \
              "implemented for %s control panel and used only for Plesk" % getCPName())
        return

    # despite the fact that plesk calls this method for each
    # created domain or subdomain, we run cpetc for historical reasons
    # TODO: re-check if we really need this
    if os.path.isdir(_CAGEFS_SKELETON_BIN):
        _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--cpetc', username])

    # we need this for plesk because of home dirs in /var/www/DOMAIN_NAME
    # TODO: think about this part:
    #  - do we need it for other control panels?
    #  - is it safe to run it on other control panels?
    # remount defined user to cagefs and recreate his virtual mount points
    _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--remount-virtmp', username])

    return "OK"


def _post_restore_user_directadmin(username: str):
    """
    Post restore action for directadmin. Cleans
    and rebuilds cl.selector files.
    :param username: account name
    """
    if not os.path.isdir(_CAGEFS_SKELETON_BIN):
        # no cagefs -> no problems
        return "SKIP: no working cagefs"
    user_prefix = get_user_prefix(username)
    cagefs_user_dir = '/var/cagefs/{prefix}/{username}'.format(
        prefix=user_prefix, username=username)
    cl_selector_path = os.path.join(cagefs_user_dir, 'etc/cl.selector')
    cl_php_d_path = os.path.join(cagefs_user_dir, 'etc/cl.php.d')
    _call_with_logging(['rm', '-rf', cl_selector_path])
    _call_with_logging(['rm', '-rf', cl_php_d_path])

    if is_user_enabled(username):
        _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--cpetc', username])
    return "OK"


def post_restore_user_handler(username):
    """
    Triggered after restoring user.
    :param username: account name
    """
    # directadmin has different procedure of restoring users
    if getCPName() == DIRECTADMIN_NAME:
        return _post_restore_user_directadmin(username)
    # we do not do same things for plesk because
    # of no hook for restore account there
    # TODO: problably we must implement post_restore action for plesk
    elif getCPName() != PLESK_NAME:
        # nothing different from default account creation
        return post_create_user_handler(username)


def post_modify_user_handler(username, new_name=None, new_owner=None):
    """
    Triggered after any modifications made to user
    """
    if not _is_user_uid_suitable_for_cagefs(new_name or username):
        return "SKIP"

    if new_name is not None and getCPName() == PLESK_NAME:
        # for plesk we had another actions on account rename
        # TODO: problably this is wrong, because
        #  running --set-default-user-status after rename is not ok
        return post_create_user_handler(new_name)
    elif new_name is not None:
        # any control panel that reports user rename, including vendors
        _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--update-etc', new_name or username])
        _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--remount', new_name or username])

    if new_name is None and getCPName() == PLESK_NAME:
        _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--update-etc', new_name or username])

        # NOTE: the following command was executed on every user update
        #  (like password change, php settings update, etc) and it caused a lot of unneeded remounts
        #  I left this code here in case this change causes some errors on customers environments
        #  we still handle rename cases above
        # _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--remount-virtmp', new_name or username])

    return "OK"


# TODO: this hook is not used on directadmin
#  we should add it there too
def pre_delete_user_handler(username):
    """
    Triggered before control panel actually removes account.
    :param username: unix user name
    """
    if not _is_user_uid_suitable_for_cagefs(username):
        return

    # unmount user before trying to remove his home dir
    # otherwise this can lead to "resource busy" problem
    _call_with_logging([_CAGEFS_BINARY, '--unmount', username])

    if not is_user_present(username):
        logger.error('User %s does no longer exist after unmount', username)


def post_delete_domain_handler(username, domain):
    """
    Triggered after deleting domain owned by system account.
    Not it is used only for plesk because of virt.mp file used
    to add domain-related data into cagefs and we should remount
    user on any change.
    :param username: account name
    :param domain: domain name
    """
    if getCPName() != PLESK_NAME:
        print("WARNING: post delete domain hook is not "
              "implemented for %s control panel and used only for Plesk" % getCPName())
        return

    if not _is_user_uid_suitable_for_cagefs(username):
        return
    _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--remount-virtmp', username])


def post_modify_domain_handler(username, old_domain, new_domain=None):
    """
    Triggered after deleting domain owned by system account.
    Not it is used only for plesk because of virt.mp file used
    to add domain-related data into cagefs and we should remount
    user on any change.
    :param username: account name
    :param old_domain: old domain name
    :param new_domain: new domain name
    """
    if getCPName() != PLESK_NAME:
        print("WARNING: post modify domain cagefs hook is not " \
              "implemented for %s control panel and used only for Plesk" % getCPName())
        return

    if not _is_user_uid_suitable_for_cagefs(username):
        return

    # TODO: we must check if domain really changed first
    #  looks like in all other cases these update-etc and remounts
    #  are just waisting server time and not doing any real stuff
    _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--update-etc', username])

    # Previously the following command was executed on every domain update
    # (like php settings change) and it caused a lot of unneeded remounts
    if old_domain != new_domain:
        _call_with_logging([_CAGEFS_BINARY, '--wait-lock', '--remount-virtmp', username])
    else:
        print('INFO: Omitting remount because old_domain=%s equals to new_domain=%s'
              '' % (old_domain, new_domain))

Zerion Mini Shell 1.0