?
Current File : //opt/cloudlinux/venv/lib/python3.11/site-packages/lvemanager/sudoers.py
# coding=utf-8
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
"""
clsudoers unix group management for lvemanager.

Operator upgrade note (CLOS-4576, F-37): ``add_unix_user_to_sudoers()`` now
fails closed when the ``clsudoers`` unix group has pre-existing members
outside the legitimate set (``admins()`` + the user being added +
package-owned members ``root`` / ``lvemanager``). Previously,
``groupadd -f`` was silently idempotent and any stowaway primary-GID
owners or ``gr_mem`` entries (left by manual admin steps or another
RPM's post-install) were quietly elevated to passwordless sudo for
selectorctl / cloudlinux-selector.

The allowlist explicitly includes ``root`` (implicit group membership in
many tools) and ``lvemanager`` (the package's runtime user, placed in
``clsudoers`` by the lvemanager RPM's ``%post`` for SPA's clsudo flow on
DirectAdmin/Plesk) — so the gate fires only on the F-37 attack signature
(a stowaway secondary member or a primary-GID-reuse owner), not on
legitimate package-installed state.

After upgrading to a build that ships this change, the first admin-add
operation on an AFFECTED host (one with non-package stowaways in the
group) raises an exception from ``_assert_sudoers_group_clean()``. To
recover, operators must either:

  * remove the stowaway entries from the ``clsudoers`` group (audit with
    ``getent group clsudoers`` and ``awk -F: '$4==<gid> {print $1}' /etc/passwd``
    for primary-GID owners), OR
  * add those users via the panel's ``admins()`` interface (panel admin user
    list) so they are recognised as legitimate members before retrying.

This is intentional — silent elevation of unaudited accounts is the
vulnerability being closed; reverting to ``groupadd -f`` idempotence is not
an acceptable workaround.
"""

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

import grp
import pwd
import subprocess

from cl_proc_hidepid import remount_proc
from clcommon.cpapi import admins, getCPName
from clcommon.sysctl import SysCtlConf, SYSCTL_CL_CONF_FILE
from clcommon.const import Feature
from clcommon.cpapi import is_panel_feature_supported
from clsudo import Clsudo

# Default admins group
DEFAULT_GROUP_NAME = "admin"

# Group name for fs.proc_super_gid
SUPER_GROUP_NAME = "clsupergid"

# Groupname for sudoers
SUDOERS_GROUP_NAME = "clsudoers"

# System-UID ceiling: an upper bound on the UID an account may have to be
# considered "package-installed" (`useradd --system`). The allowlist below
# additionally requires the account name to belong to the lvemanager
# package family — UID range alone is NOT the gate; it is the SECOND
# precondition. Hard-coding 999 is safe across CL7/8/9/10 and ubuntu22.
_SYSTEM_UID_MAX = 999

# Always-allowed package-owned member names. The lvemanager package's
# canonical SPA runtime user is `lvemanager`. `root` is implicit / always
# benign. DirectAdmin installs sometimes append a UUID suffix
# (`lvemanager_<uuid>`); the helper below matches that family by name
# prefix AND requires the matched account to live in the system-UID
# range. Anything else (a `daemon`/`bin`/`mail` system account, a
# regular user, a stowaway with a fabricated name) fails closed.
_PACKAGE_OWNED_CLSUDOERS_MEMBERS = frozenset({"root", "lvemanager"})
_PACKAGE_OWNED_CLSUDOERS_NAME_PREFIXES = ("lvemanager_",)


def _is_system_owned_user(name):
    """Return True if `name` is a known package-owned `clsudoers` member
    (always-allowed name OR an lvemanager-package-family name) AND lives
    in the system-UID range. Both conditions are required — a regular
    user named `lvemanager_evil` (UID 5000) is NOT package-owned; an
    unrelated system account named `daemon` (UID 2) is NOT package-owned
    either. The gate exists to fire on the F-37 priv_group_reuse
    signature; we only want to exempt the RPM's own post-install state.
    """
    if name == "root":
        # Implicit group membership in many tools; never a stowaway.
        return True
    is_known_name = (
        name in _PACKAGE_OWNED_CLSUDOERS_MEMBERS or
        any(name.startswith(p) for p in _PACKAGE_OWNED_CLSUDOERS_NAME_PREFIXES)
    )
    if not is_known_name:
        return False
    try:
        return pwd.getpwnam(name).pw_uid <= _SYSTEM_UID_MAX
    except KeyError:
        # gr_mem references a non-existent user — that itself is suspicious
        # (stowaway entry); let the gate flag it.
        return False


def _add_user_to_group(user_name, group_name):
    """Add user to given unix group"""
    retcode = subprocess.call(["/usr/bin/gpasswd", "-a", user_name, group_name])
    if retcode != 0:
        return False
    return True


# Remove user from group
def _remove_user_from_group(user_name, group_name):
    retcode = subprocess.call(["/usr/bin/gpasswd", "-d", user_name, group_name])
    if retcode != 0:
        return False
    return True


def _add_admins_into_group(group_name, new_admin_name):
    """
    Add all present DA admins (plus new_admin_name admin) to supplied group
    :param new_admin_name: new admin name to add
    :return:
    """
    # Get admin list from DA and append new admin name to it
    admin_list = list(admins())
    if new_admin_name not in admin_list:
        admin_list.append(new_admin_name)
    for admin in admin_list:
        _add_user_to_group(admin, group_name)


def _create_group(group_name):
    """Create group with given name"""
    retcode = subprocess.call(["/usr/sbin/groupadd", "-f", group_name])
    if retcode != 0:
        return False
    return True


def _add_admins_into_supergid_grp(new_admin_name):
    """
    Add all present DA admins (plus new_admin_name admin) to current supergid group
    :param new_admin_name: new admin name to add
    :return:
    """
    # Determine SUPER_GROUP_NAME gid
    super_gid = str(grp.getgrnam(SUPER_GROUP_NAME).gr_gid)

    sysctl_cfg = SysCtlConf(config_file=SYSCTL_CL_CONF_FILE)

    # returns set gid from sysctl.conf or kernel default
    proc_super_gid = sysctl_cfg.get('fs.proc_super_gid')
    # set fs.proc_super_gid and add admins to group with this gid if:
    #  1. it was not found in sysctl.conf;
    if not sysctl_cfg.has_parameter('fs.proc_super_gid'):
        sysctl_cfg.set('fs.proc_super_gid', super_gid)
        _add_admins_into_group(SUPER_GROUP_NAME, new_admin_name)
        return
    elif getCPName() == 'DirectAdmin':
        # Only for DA
        try:
            admin_gid = str(grp.getgrnam(DEFAULT_GROUP_NAME).gr_gid)
        except KeyError:
            admin_gid = None
        if proc_super_gid == admin_gid:
            sysctl_cfg.set('fs.proc_super_gid', super_gid)
            _add_admins_into_group(SUPER_GROUP_NAME, new_admin_name)
            return
    # otherwise read fs.proc_super_gid and add admins to group with this gid
    try:
        proc_super_gid = int(proc_super_gid)
    except ValueError:
        raise RuntimeError("Bad fs.proc_super_gid option value in /etc/sysctl.conf")
    # add all panel admins into custom proc_super_gid group
    proc_super_name = grp.getgrgid(proc_super_gid).gr_name
    _add_admins_into_group(proc_super_name, new_admin_name)


def _assert_sudoers_group_clean(group_name, expected_members):
    """
    Refuse to grant %group_name passwordless sudo if the unix group already
    exists with members or primary-GID owners that the lvemanager flow did
    not place there. `groupadd -f` is silently idempotent and would otherwise
    elevate any pre-existing member on the first call.

    :param group_name: target group (clsudoers)
    :param expected_members: iterable of usernames the lvemanager flow is
        about to authorize (panel admins + the user being added).
    :raises Exception: if any unexpected secondary member or primary-GID
        owner is found.
    """
    try:
        gr = grp.getgrnam(group_name)
    except KeyError:
        # Group does not exist yet — _create_group will create it empty.
        return
    expected = set(expected_members)
    # Secondary `gr_mem` entries that are neither expected (panel admins +
    # the user being added) nor package-installed system accounts are the
    # F-37 stowaway signature. The system-account allowlist covers
    # legitimate runtime users the lvemanager RPM (and DA-side installers)
    # place in the group at install time, including DA variants like
    # `lvemanager_<uuid>` which still land in the UID system range.
    unexpected_members = [
        m for m in gr.gr_mem
        if m not in expected and not _is_system_owned_user(m)
    ]
    # Primary-GID owners are the canonical F-37 attack: a user account
    # whose primary group landed on clsudoers.gr_gid via GID-reuse or a
    # buggy installer. Only the lvemanager-package family of system
    # accounts is legitimately allowed to have clsudoers as a primary
    # group; an unrelated system user with the same primary GID is the
    # priv_group_reuse signature and must trip the gate. Reuse the
    # `_is_system_owned_user` predicate so the name + UID criteria stay
    # in lockstep with the gr_mem path above.
    unexpected_primary = [
        p.pw_name for p in pwd.getpwall()
        if p.pw_gid == gr.gr_gid
        and p.pw_name not in expected
        and not _is_system_owned_user(p.pw_name)
    ]
    if unexpected_members or unexpected_primary:
        raise Exception(
            "ERROR: refusing to grant %%%s passwordless sudo: group already "
            "has unexpected members %r and primary-GID owners %r" % (
                group_name, unexpected_members, unexpected_primary))


def add_unix_user_to_sudoers(name):
    # create all supergid stuff only if regular CL edition
    if is_panel_feature_supported(Feature.LVE):
        if not _create_group(SUPER_GROUP_NAME):
            raise Exception("ERROR: Can't create %s group\n" % SUPER_GROUP_NAME)

        _add_admins_into_supergid_grp(name)

        if not _add_user_to_group(name, SUPER_GROUP_NAME):
            raise Exception("ERROR: Can't add user %s to %s group\n" % (
                name, SUPER_GROUP_NAME))

    # Gate: if clsudoers already exists with non-lvemanager members, fail
    # closed rather than silently elevate them via Clsudo.add_lvemanager_group.
    # Allowlist members the lvemanager RPM itself places in the group at
    # package install time (see _PACKAGE_OWNED_CLSUDOERS_MEMBERS) so the
    # gate fires only on the F-37 attack signature, not on legitimate
    # package-installed state.
    expected_clsudoers = set(admins())
    expected_clsudoers.add(name)
    expected_clsudoers.update(_PACKAGE_OWNED_CLSUDOERS_MEMBERS)
    _assert_sudoers_group_clean(SUDOERS_GROUP_NAME, expected_clsudoers)

    if not _create_group(SUDOERS_GROUP_NAME):
        raise Exception("ERROR: Can't create %s group\n" % SUDOERS_GROUP_NAME)

    if not _add_user_to_group(name, SUDOERS_GROUP_NAME):
        raise Exception("ERROR: Can't add user %s to %s group\n" % (
            name, SUDOERS_GROUP_NAME))

    # Add SUDOERS_GROUP_NAME group to /etc/sudoers
    sudo = Clsudo()
    sudo.add_lvemanager_group(SUDOERS_GROUP_NAME)

    # CAG-796: use hidepid=2 when mounting /proc
    remount_proc()


def remove_unix_user_from_sudoers(name):
    # Remove user from all groups
    _remove_user_from_group(name, SUPER_GROUP_NAME)
    _remove_user_from_group(name, SUDOERS_GROUP_NAME)