Ctrl K
ComunidadGuías…

rex/permissions.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Permissions system for Rex.

Provides a per-user permission model backed by data/users.db.

Permissions
-----------
- ``computer_control`` — read/write/execute files and apps on the desktop
- ``email_send``       — send email via configured email backend
- ``sms_send``         — send SMS via configured messaging backend
- ``ha_control``       — control Home Assistant entities
- ``openclaw_execute`` — execute approved OpenClaw capabilities
- ``admin``            — manage users and permissions

The first registered user is automatically granted ``admin``.
"""

from __future__ import annotations

import logging
import sqlite3
from enum import StrEnum
from pathlib import Path

from rex.runtime_paths import household_data_path

logger = logging.getLogger(__name__)


class Permission(StrEnum):
    """Enumeration of all supported permissions."""

    computer_control = "computer_control"
    email_send = "email_send"
    sms_send = "sms_send"
    ha_control = "ha_control"
    openclaw_execute = "openclaw_execute"
    admin = "admin"


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _get_db_path() -> Path:
    """Return the canonical household users database path."""
    return household_data_path("users.db")


def _open_db() -> sqlite3.Connection:
    db_path = _get_db_path()
    db_path.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(str(db_path))
    conn.row_factory = sqlite3.Row
    _init_schema(conn)
    return conn


def _init_schema(conn: sqlite3.Connection) -> None:
    """Create the user_permissions table if it does not already exist."""
    conn.execute("""
        CREATE TABLE IF NOT EXISTS user_permissions (
            user_id    TEXT NOT NULL,
            permission TEXT NOT NULL,
            PRIMARY KEY (user_id, permission)
        )
    """)
    conn.commit()


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def grant_permission(user_id: str, permission: Permission | str) -> None:
    """Grant *permission* to *user_id*.

    Silently succeeds if the permission is already granted.

    Args:
        user_id:    UUID of the target user.
        permission: A :class:`Permission` value or its string name.

    Raises:
        ValueError: If *permission* is not a recognised :class:`Permission`.
    """
    perm = Permission(permission) if isinstance(permission, str) else permission
    with _open_db() as conn:
        conn.execute(
            "INSERT OR IGNORE INTO user_permissions (user_id, permission) VALUES (?, ?)",
            (user_id, perm.value),
        )
        conn.commit()
    logger.info("Granted %s to user %s", perm.value, user_id)


def revoke_permission(user_id: str, permission: Permission | str) -> None:
    """Revoke *permission* from *user_id*.

    Silently succeeds if the permission was not held.

    Args:
        user_id:    UUID of the target user.
        permission: A :class:`Permission` value or its string name.

    Raises:
        ValueError: If *permission* is not a recognised :class:`Permission`.
    """
    perm = Permission(permission) if isinstance(permission, str) else permission
    with _open_db() as conn:
        conn.execute(
            "DELETE FROM user_permissions WHERE user_id = ? AND permission = ?",
            (user_id, perm.value),
        )
        conn.commit()
    logger.info("Revoked %s from user %s", perm.value, user_id)


def check_permission(user_id: str, permission: Permission | str) -> bool:
    """Return ``True`` if *user_id* holds *permission*, ``False`` otherwise.

    Args:
        user_id:    UUID of the user to check.
        permission: A :class:`Permission` value or its string name.

    Raises:
        ValueError: If *permission* is not a recognised :class:`Permission`.
    """
    perm = Permission(permission) if isinstance(permission, str) else permission
    with _open_db() as conn:
        row = conn.execute(
            "SELECT 1 FROM user_permissions WHERE user_id = ? AND permission = ?",
            (user_id, perm.value),
        ).fetchone()
    return row is not None


def get_permissions(user_id: str) -> list[str]:
    """Return a list of permission names held by *user_id*.

    Args:
        user_id: UUID of the user.

    Returns:
        List of permission value strings (e.g. ``["admin", "email_send"]``).
    """
    with _open_db() as conn:
        rows = conn.execute(
            "SELECT permission FROM user_permissions WHERE user_id = ?",
            (user_id,),
        ).fetchall()
    return [r["permission"] for r in rows]


def bootstrap_admin_if_first_user(user_id: str) -> None:
    """Grant ``admin`` to *user_id* if no admins exist yet.

    Should be called immediately after a new user is registered so that the
    first account in a fresh deployment automatically becomes the administrator.

    Args:
        user_id: UUID of the newly registered user.
    """
    with _open_db() as conn:
        row = conn.execute(
            "SELECT 1 FROM user_permissions WHERE permission = ?",
            (Permission.admin.value,),
        ).fetchone()
        if row is None:
            conn.execute(
                "INSERT OR IGNORE INTO user_permissions (user_id, permission) VALUES (?, ?)",
                (user_id, Permission.admin.value),
            )
            conn.commit()
            logger.info("Bootstrapped admin permission for first user %s", user_id)


__all__ = [
    "Permission",
    "bootstrap_admin_if_first_user",
    "check_permission",
    "get_permissions",
    "grant_permission",
    "revoke_permission",
]