Viewing File: /opt/imunify360/venv/versions/imunify-core-8.14.0-1/defence360agent/utils/tenant_path.py

"""Tenant identity carried by the path, for cluster mode.

A resource names its tenant in its own path — ``/app1/var/www/html/x`` —
so nothing else has to carry an application id. This module is the codec
for that form; both products read and write it.
"""

import os
from typing import Union

from defence360agent.utils import is_cluster


def to_prefixed(path: Union[str, os.PathLike], user: "str | None") -> str:
    """Serialize a (user, path) pair into the canonical tenant-prefixed
    string used in the DB and on the MRS/server wire on k8s:
    ``/var/www/x`` + ``app1`` -> ``/app1/var/www/x``.

    No-op (returns ``str(path)``) when the user is falsy, outside k8s,
    or the path already carries this user's prefix — so it is safe to
    call at every serialization boundary without double-prefixing.
    """
    s = os.fspath(path)
    if not user or not is_cluster():
        return s
    prefix = f"/{user}"
    if s == prefix or s.startswith(prefix + "/"):
        return s
    return prefix + s


def split_prefixed(path: Union[str, os.PathLike]) -> "tuple[str | None, str]":
    """Purely syntactic split of a tenant-prefixed path:
    ``/app1/var/www/x`` -> ``("app1", "/var/www/x")``.

    Returns ``(None, path)`` for relative paths or paths with fewer than
    two components (``/app1`` alone is not a prefixed path). No registry
    validation happens here — callers at input boundaries must validate
    the candidate user against registered applications.
    """
    s = os.fspath(path)
    if not s.startswith("/"):
        return None, s
    first, sep, rest = s[1:].partition("/")
    if not sep or not first or not rest:
        return None, s
    return first, "/" + rest


def strip_prefix(path: Union[str, os.PathLike], user: "str | None") -> str:
    """Strip ``/<user>`` iff the path starts with it; else return unchanged.

    Used on the read/display side when the tenant is already known
    (e.g. from the ``MalwareHit.user`` column). Gated on k8s like
    ``to_prefixed`` — on a standalone host a user named e.g. ``home``
    must never get ``/home/home/x`` mangled to ``/home/x``.
    """
    s = os.fspath(path)
    if not user or not is_cluster():
        return s
    prefix = f"/{user}"
    if s.startswith(prefix + "/"):
        return s[len(prefix) :]
    return s


#: Tenant name of an ignore-list entry that applies to every application.
#: Not a legal application id, so it cannot collide with a real tenant.
GLOBAL_TENANT = "*"


def under_prefix(field, user: str):
    """peewee predicate: ``field`` is ``/<user>`` or lives beneath it.

    A half-open range instead of ``LIKE '/user/%'``: exact under BINARY
    collation (LIKE is case-insensitive in SQLite and would let ``/App-1/``
    read ``/app-1/``'s rows), free of ``%``/``_`` escaping, and index-usable.
    """
    prefix = f"/{user}/"
    upper = prefix[:-1] + chr(ord(prefix[-1]) + 1)
    return (field == f"/{user}") | ((field >= prefix) & (field < upper))


def strip_tenant(path, user: "str | None" = None) -> str:
    """A stored path as a consumer outside the agent's tables wants it.

    The wildcard marker always comes off, and ``user``'s own prefix when a
    tenant is given. Anything else is returned untouched: the split is
    syntactic, so a bare host path (/usr/share/..., which no cluster write
    ever prefixes) must not lose its first directory, and another tenant's
    entry is left as it is — as a scanner exclusion pattern it simply never
    matches inside this container.
    """
    s = os.fspath(path)
    if not is_cluster():
        return s
    tenant, bare = split_prefixed(s)
    if tenant == GLOBAL_TENANT or (user is not None and tenant == user):
        return bare
    return s


def split_marked(path, applications) -> "tuple[str | None, str]":
    """Split a stored path into (tenant, in-container path), but only when
    the tenant is one we can prove: the wildcard, or a registered
    application. A host path such as /usr/share/... must not lose its first
    directory to a syntactic guess.
    """
    s = os.fspath(path)
    tenant, bare = split_prefixed(s)
    if tenant == GLOBAL_TENANT or (
        tenant is not None and tenant in applications
    ):
        return tenant, bare
    return None, s


def is_under_prefix(path, user: str) -> bool:
    """Python counterpart of :func:`under_prefix`."""
    s = os.fspath(path)
    return s == f"/{user}" or s.startswith(f"/{user}/")
Back to Directory