atlus/backend/config.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.9 KiB
Python

"""Atlus configuration — paths, JWT settings, defaults."""
import os
import json
import secrets
from pathlib import Path
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
BASE_DIR = Path(__file__).resolve().parent.parent # repo root
FRONTEND_DIR = BASE_DIR / "frontend"
_default_config = "/etc/atlus" if os.path.exists("/etc") and os.access("/etc", os.W_OK) else str(BASE_DIR / ".atlus_data")
_default_data = "/var/lib/atlus" if os.path.exists("/var/lib") and os.access("/var/lib", os.W_OK) else str(BASE_DIR / ".atlus_data")
CONFIG_DIR = Path(os.environ.get("ATLUS_CONFIG_DIR", _default_config))
DATA_DIR = Path(os.environ.get("ATLUS_DATA_DIR", _default_data))
USER_CONFIG_FILE = CONFIG_DIR / "atlus.json"
# ---------------------------------------------------------------------------
# Server
# ---------------------------------------------------------------------------
HOST = os.environ.get("ATLUS_HOST", "0.0.0.0")
PORT = int(os.environ.get("ATLUS_PORT", "7779"))
# ---------------------------------------------------------------------------
# JWT
# ---------------------------------------------------------------------------
JWT_SECRET_FILE = DATA_DIR / "jwt_secret"
JWT_ALGORITHM = "HS256"
JWT_EXPIRY_HOURS = int(os.environ.get("ATLUS_JWT_EXPIRY_HOURS", "24"))
# Revoked tokens (in-memory set, cleared on restart)
_revoked_tokens: set[str] = set()
def get_jwt_secret() -> str:
"""Return persistent JWT secret, generating one on first run."""
if JWT_SECRET_FILE.exists():
return JWT_SECRET_FILE.read_text().strip()
JWT_SECRET_FILE.parent.mkdir(parents=True, exist_ok=True)
secret = secrets.token_hex(32)
JWT_SECRET_FILE.write_text(secret)
JWT_SECRET_FILE.chmod(0o600)
return secret
def revoke_token(jti: str) -> None:
_revoked_tokens.add(jti)
def is_token_revoked(jti: str) -> bool:
return jti in _revoked_tokens
# ---------------------------------------------------------------------------
# User config helpers (persisted JSON)
# ---------------------------------------------------------------------------
_DEFAULT_CONFIG: dict = {
"hostname_display": None, # override for panel display
"timezone": None, # e.g. "America/New_York", None=system
"ntp_enabled": True,
"dock_apps": [
"terminal", "files", "services", "tasks", "network", "settings"
],
"panel_services": [], # systemd unit names to show in panel
"gui_apps": [], # GUI apps: [{id, name, command, icon, args, target_fps}]
"default_user": None, # OS user for autostart apps (None = defer until first login)
"asi_bridge": {
"cifs_share": "//192.168.10.120/share",
"mount_point": "/mnt/asiair",
"cifs_user": "anonymous",
"cifs_pass": "",
},
"session_timeout_minutes": 1440, # 24 h
"stats_interval_seconds": 2,
"update_check_enabled": True,
"update_check_interval": 60, # seconds between update checks
}
def load_config() -> dict:
"""Load user config from disk, merged over defaults."""
cfg = dict(_DEFAULT_CONFIG)
if USER_CONFIG_FILE.exists():
try:
with open(USER_CONFIG_FILE) as f:
user = json.load(f)
cfg.update(user)
except (json.JSONDecodeError, OSError):
pass
return cfg
def save_config(cfg: dict) -> None:
"""Persist user config to disk."""
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
with open(USER_CONFIG_FILE, "w") as f:
json.dump(cfg, f, indent=2)
# ---------------------------------------------------------------------------
# Stats push interval
# ---------------------------------------------------------------------------
STATS_INTERVAL = float(os.environ.get(
"ATLUS_STATS_INTERVAL",
str(load_config().get("stats_interval_seconds", 2)),
))