atlus/backend/routers/session.py
roberts 6a0c8757f8 Run terminals and GUI apps as the authenticated user, not root
Atlus runs as root (systemd) but user-facing processes must run under the
authenticated user's identity. Added privilege-dropping via preexec_fn
(os.setgid + os.initgroups + os.setuid) to both terminal PTY spawning
and GUI app launching. System admin operations (services, packages,
network, updates) intentionally remain root.

Autostart apps now support a configurable default_user; without one set,
autostart defers until the first user logs in and runs as that user.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 00:35:52 -05:00

107 lines
3.2 KiB
Python

"""Desktop session — persistent state + terminal management."""
import logging
from fastapi import APIRouter, Depends, HTTPException, Request, Query
from pydantic import BaseModel
from backend.auth import get_current_user, decode_token
from backend.display import display_manager
from backend.sessions import manager
router = APIRouter(prefix="/api/session", tags=["session"])
log = logging.getLogger("atlus.session")
# ---------------------------------------------------------------------------
# Models
# ---------------------------------------------------------------------------
class DesktopState(BaseModel):
open_apps: list[str] = []
active_app: str | None = None
terminal_tabs: list[dict] = [] # [{terminal_id, title}]
class TerminalCreate(BaseModel):
cols: int = 120
rows: int = 30
# ---------------------------------------------------------------------------
# Session endpoints
# ---------------------------------------------------------------------------
@router.get("")
async def get_session(user: str = Depends(get_current_user)):
"""Get or create the user's desktop session."""
session = manager.get_or_create(user)
# Trigger deferred autostart apps on first login
await display_manager.trigger_deferred_autostart(user)
return session.to_dict()
@router.put("/state")
async def save_state(
state: DesktopState,
request: Request,
token: str = Query(default=None),
user: str = Depends(get_current_user),
):
"""Save desktop state (open apps, active app, terminal tab info)."""
manager.save_state(user, state.model_dump())
return {"ok": True}
@router.post("/state")
async def save_state_beacon(
request: Request,
token: str = Query(default=None),
):
"""Save desktop state via sendBeacon (POST with token in query param)."""
if not token:
raise HTTPException(401, "Missing token")
try:
payload = decode_token(token)
except Exception:
raise HTTPException(401, "Invalid token")
user = payload["sub"]
body = await request.json()
state = DesktopState(**body)
manager.save_state(user, state.model_dump())
return {"ok": True}
# ---------------------------------------------------------------------------
# Terminal endpoints
# ---------------------------------------------------------------------------
@router.get("/terminals")
async def list_terminals(user: str = Depends(get_current_user)):
"""List all alive terminals for the user."""
return manager.list_terminals(user)
@router.post("/terminals")
async def create_terminal(
req: TerminalCreate,
user: str = Depends(get_current_user),
):
"""Create a new persistent terminal."""
try:
terminal = manager.create_terminal(user, cols=req.cols, rows=req.rows)
except Exception as e:
log.exception("Failed to create terminal for %s", user)
raise HTTPException(500, f"Failed to spawn terminal: {e}")
return terminal.to_dict()
@router.delete("/terminals/{terminal_id}")
async def close_terminal(
terminal_id: str,
user: str = Depends(get_current_user),
):
"""Close and kill a terminal."""
if not manager.close_terminal(user, terminal_id):
raise HTTPException(404, "Terminal not found")
return {"ok": True}