Your IP : 10.1.126.163


Current Path : /opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/generic/
Upload File :
Current File : //opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/subsys/panels/generic/panel.py

import asyncio
import datetime
import functools
import json
import logging
import os
import shlex
from collections import defaultdict
from typing import Dict, List, Optional, Set

import cerberus
import yaml

from defence360agent.api.integration_conf import (
    ClIntegrationConfig,
    IntegrationConfig,
)
from defence360agent.api.jwt_issuer import JWTIssuer
from defence360agent.application.determine_hosting_panel import (
    is_generic_panel_installed,
)
from defence360agent.contracts.config import int_from_envvar
from defence360agent.rpc_tools.lookup import UserType
from defence360agent.utils import (
    CheckRunError,
    check_run,
    get_non_system_users,
    timed_cache,
)

from .. import base

logger = logging.getLogger(__name__)
_SCHEMA_PATH_TMPL = (
    os.path.dirname(__file__) + "/users_script_schemas/schema-{}.yaml"
)

ADMIN_LIST_FILE_PATH = "/etc/sysconfig/imunify360/auth.admin"
METADATA = "metadata"
EXPIRATION_TIME_FOR_INTEGRATION_SCRIPTS = datetime.timedelta(
    seconds=int_from_envvar(
        "IMUNIFY360_EXPIRATION_FOR_INTEGRATION_SCRIPTS", 90
    )
)


@functools.lru_cache(maxsize=2)
def _get_validator(script: str) -> cerberus.Validator:
    """Returns a validator for given script."""
    with open(_SCHEMA_PATH_TMPL.format(script)) as schema_file:
        schema = yaml.safe_load(schema_file)
        if script is not METADATA:
            schema[METADATA] = {"required": True}
        return cerberus.Validator(schema)


def get_users_default_impl():
    return [dict(name=pw.pw_name) for pw in get_non_system_users()]


def _get_conf_path(cl, script):
    d = cl.to_dict()
    if "integration_scripts" in d and script in d["integration_scripts"]:
        return d["integration_scripts"][script]
    return None


@timed_cache(expiration=EXPIRATION_TIME_FOR_INTEGRATION_SCRIPTS, maxsize=10)
async def get_integration_data(script: str):
    path = _get_conf_path(IntegrationConfig(), script)
    if not path:
        path = _get_conf_path(ClIntegrationConfig(), script)
    if not path:
        raise IntegrationScriptError(
            "%s not found neither in "
            "/etc/sysconfig/imunify360/integration.conf "
            "nor in /opt/cpvendor/etc/integration.ini." % script
        )

    return await _get_integration_data(script, path)


def _build_integration_argv(script: str, path: str) -> List[str]:
    """Tokenize a config-supplied script command into argv (no shell)."""
    if not path or not path.strip():
        raise IntegrationScriptError(
            "Empty integration script path for %s" % script
        )

    try:
        argv = shlex.split(path)
    except ValueError as e:
        raise IntegrationScriptError(
            "Invalid integration script path for %s: %s" % (script, e)
        )
    if not argv:
        raise IntegrationScriptError(
            "Empty integration script path for %s" % script
        )

    executable = argv[0]
    if not os.path.isabs(executable):
        raise IntegrationScriptError(
            "Integration script path for %s must be absolute: %s"
            % (script, executable)
        )
    if not os.path.isfile(executable):
        raise IntegrationScriptError(
            "Integration script for %s does not exist: %s"
            % (script, executable)
        )
    if not os.access(executable, os.X_OK):
        raise IntegrationScriptError(
            "Integration script for %s is not executable: %s"
            % (script, executable)
        )
    return argv


async def _get_integration_data(script: str, path: str):
    argv = _build_integration_argv(script, path)
    try:
        stdout = await check_run(argv)
    except CheckRunError as e:
        raise IntegrationScriptError(
            "Integrations script {script} "
            "failed with exit code {e.returncode} \n"
            "{e.stderr}".format(script=script, e=e)
        )

    try:
        data = json.loads(stdout.decode())
    except (UnicodeDecodeError, json.JSONDecodeError) as e:
        raise IntegrationScriptError(
            "Cannot decode output of %s as JSON" % path
        ) from e
    if not isinstance(data, dict):
        raise IntegrationScriptError("%s should return dict" % path)

    metadata_validator = _get_validator(METADATA)
    if not metadata_validator.validate(data):
        raise IntegrationScriptError(
            "Validation error in metadata of %s script: %s"
            % (script, metadata_validator.errors)
        )

    if data[METADATA]["result"] != "ok":
        metadata_error = data[METADATA]["result"]
        if "message" in data[METADATA]:
            metadata_error += ": %s" % data[METADATA]["message"]
        raise IntegrationScriptError(metadata_error)

    validator = _get_validator(script)
    if not validator.validate(data):
        raise IntegrationScriptError(
            "Validation error in %s script: %s" % (script, validator.errors)
        )

    return data["data"]


async def _get_client_data():
    try:
        users = await get_users_integration_data()
        domains = await get_integration_data("domains")
        for k, v in domains.items():
            if v and v.get("owner"):
                user_domains = users.setdefault(v["owner"], [])
                user_domains.append(k)
        return [{"name": k, "domains": v} for k, v in users.items()]

    except IntegrationScriptError:
        logger.warning(
            "Applying default implementation of users and domains lists"
        )
        return get_users_default_impl()


async def get_users_integration_data():
    users = await get_integration_data("users")
    users_dict = {}

    for user in users:
        if not user or not user.get("username"):
            logger.warning(f"Found user with an empty username: {user}")
        else:
            users_dict[user["username"]] = []

    return users_dict


async def get_user_homes_integration_data() -> Dict[str, Optional[str]]:
    """
    Get user home directories from integration script.
    Returns dict mapping username to home directory (or None if not provided).
    """
    try:
        users = await get_integration_data("users")
    except IntegrationScriptError:
        return {}

    return {
        user["username"]: user.get("home")
        for user in users
        if user.get("username")
    }


async def get_domain_data():
    try:
        return await get_integration_data("domains")
    except IntegrationScriptError:
        logger.warning("Could not parse domains lists")
        return {}


async def get_admin_list() -> List[str]:
    script_name = "admins"
    admins_set = {"root"}
    admins_from_integration_scripts = await asyncio.gather(
        _get_integration_data(
            script_name,
            _get_conf_path(
                IntegrationConfig(),
                script_name,
            ),
        ),
        _get_integration_data(
            script_name,
            _get_conf_path(
                ClIntegrationConfig(),
                script_name,
            ),
        ),
        return_exceptions=True,
    )
    custom_admins = {
        admin["name"]
        for admins in admins_from_integration_scripts
        if isinstance(admins, list)  # skip exceptions
        for admin in admins
    }

    if not custom_admins:
        logger.warning(
            "Error occurred during extracting admins "
            "from integration configs: %s",
            admins_from_integration_scripts,
        )

    admins_set |= custom_admins
    try:
        with open(ADMIN_LIST_FILE_PATH) as admin_list_file:
            admins_set.update(admin_list_file.read().splitlines())
    except OSError:
        logger.warning(
            "Failed to retrieve admins list from %s", ADMIN_LIST_FILE_PATH
        )
    return list(admins_set)


class IntegrationScriptError(base.PanelException):
    def __init__(self, *args, **kwargs):
        super().__init__(*args)
        logger.warning(self)


class GenericPanel(base.AbstractPanel):
    """
    Panel, UI to which is provided by
    imunify{-antivirus,360-firewall}-generic.{rpm,deb}
    """

    NAME = base.GENERIC_PANEL_NAME
    exception = IntegrationScriptError

    @classmethod
    def is_installed(cls):
        return is_generic_panel_installed()  # pragma: no cover

    async def enable_imunify_plugin(self, name=None):
        pass

    async def disable_imunify_plugin(self, plugin_name=None):
        pass

    @classmethod
    async def version(cls):
        try:
            info = await get_integration_data("panel_info")
            return "{name} {version}".format(**info)
        except IntegrationScriptError:
            return "0"

    @classmethod
    async def name(cls):
        try:
            info = await get_integration_data("panel_info")
            return "{name}".format(**info)
        except IntegrationScriptError:
            return cls.NAME

    async def get_user_domains(self):
        users = await _get_client_data()
        result = []
        for user in users:
            result.extend(user.get("domains", tuple()))
        return result

    async def get_users(self) -> List[str]:
        users = await _get_client_data()
        return [user["name"] for user in users]

    async def get_user_homes(self) -> Dict[str, Optional[str]]:
        """
        Returns dict mapping username to home directory from integration script.
        Users without home in integration script will have None value.
        """
        return await get_user_homes_integration_data()

    async def get_domain_to_owner(self) -> Dict[str, List[str]]:
        users = await _get_client_data()
        result = defaultdict(list)
        for user in users:
            for domain in user.get("domains", []):
                result[domain].append(user["name"])
        return result

    async def get_user_details(self) -> Dict[str, Dict[str, str]]:
        try:
            user_data = await get_integration_data("users")
        except IntegrationScriptError:
            return await super().get_user_details()
        usernames = await self.get_users()
        admins = await get_admin_list()

        def user_info_to_level(user_info: Dict):
            if user_info.get("username") == "root":
                return base.UserLevel.ADMIN

            if user_info.get("username") in admins:
                return base.UserLevel.RESSELER

            return base.UserLevel.REGULAR_USER

        return {
            info.get("username"): {
                "email": info.get("email", ""),
                "locale": info.get("locale_code", ""),
                "level": int(user_info_to_level(info)),
            }
            for info in user_data
            if info and info.get("username") in usernames
        }

    async def get_domains_per_user(self) -> Dict[str, List[str]]:
        users = await _get_client_data()

        return {user["name"]: user.get("domains", []) for user in users}

    def authenticate(self, protocol, data: dict):
        if protocol._uid != 0 and data["command"] != ["login", "pam"]:
            token = data["params"].pop("jwt", None)
            parsed_token = JWTIssuer.parse_token(token)
            return parsed_token["user_type"], (
                parsed_token["user_name"]
                if parsed_token["user_type"] == UserType.NON_ROOT
                else None
            )
        else:
            return protocol.user, None

    def basedirs(self) -> Set[str]:
        conf = IntegrationConfig().to_dict()
        if "malware" in conf and "basedir" in conf["malware"]:
            return set(conf["malware"]["basedir"].split())
        return set()

    async def list_docroots(self) -> Dict[str, str]:
        domains = await get_domain_data()
        return {
            v["document_root"]: domain
            for domain, v in domains.items()
            if v and v.get("document_root")
        }

    async def get_domain_paths(self) -> Dict[str, List[str]]:
        domains = await get_domain_data()
        return {
            domain: [v["document_root"]]
            for domain, v in domains.items()
            if v and v.get("document_root")
        }

    async def panel_user_link(self, username) -> str:
        """
        Returns panel url
        :return: str
        """
        return ""

    @classmethod
    async def get_user_domains_details(
        cls, username: str
    ) -> list[base.DomainData]:
        domains = await get_domain_data()
        return [
            base.DomainData(
                docroot=v.get("document_root") or "",
                domain=domain,
                type="main" if v.get("is_main") else "addon",
                username=username,
            )
            for domain, v in domains.items()
            if v and v.get("owner") == username
        ]