Files
ImpTune/imptune/api/packages.py
T
kawaandClaude Haiku 4.5 ed41f7f520 feat(session): per-owner printer/config storage via cookie-scoped bearer key
Printers and groups (Client) are now scoped to an Owner identified by an opaque
bearer key (secrets.token_urlsafe(32)) stored in an httponly cookie, defaulting
to temporary. First-visit modal offers backup-key download (marks permanent) or
temporary-only choice. /session/restore re-attaches a fresh browser to a saved
key. Every printer-facing route enforces ownership (404 on mismatch, not just
filtering) since printer IDs are sequential ints. Drivers stay global/shared.

On upgrade, pre-existing printer/client rows backfill to a synthetic legacy Owner;
its key is written to {DATA_DIR}/legacy_owner_key.txt for manual restore.

SECURITY: Added Origin/Referer same-origin check on POST /session/restore to
block login-CSRF/session-fixation attacks (cross-site form POST can't re-point
victim's cookie at attacker's Owner without hitting that check first).

Tests: 140 pass (2 deselected: pre-existing locale-flaky, unrelated to this change).
Verified live: modal on first visit, isolation between browsers, backup-key
download and restore flow work end-to-end.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-04 11:29:43 +02:00

167 lines
6.1 KiB
Python

"""Package export endpoints — serves deployment packages for NinjaRMM and Microsoft Intune."""
import io
import json
import os
import shutil
import tempfile
import zipfile
from fastapi import APIRouter, Request
from fastapi.responses import PlainTextResponse, Response
import imptune.config as cfg
from imptune.db.models import Icon, Owner, Printer
from imptune.generators.intunewin_builder import build_intunewin
from imptune.generators.script_generator import render_detect, render_install, render_uninstall
from imptune.storage.driver_store import DriverStore
router = APIRouter(prefix="/printers")
def _get_printer_and_driver(printer_id: int, owner: Owner):
"""Fetch printer (scoped to owner) and validate driver — returns (printer, driver, driver_name) or PlainTextResponse error."""
printer = Printer.get_or_none((Printer.id == printer_id) & (Printer.owner == owner))
if printer is None:
return None, PlainTextResponse("Printer not found", status_code=404)
driver = printer.driver
if driver is None:
return None, PlainTextResponse("No driver assigned", status_code=422)
if not driver.inf_filename:
return None, PlainTextResponse("Driver has no INF file", status_code=422)
if not driver.driver_desc:
return None, PlainTextResponse("Driver has no description", status_code=422)
try:
desc_list = json.loads(driver.driver_desc)
driver_name = desc_list[0]
except (json.JSONDecodeError, IndexError, TypeError):
return None, PlainTextResponse("Driver description is invalid", status_code=422)
return (printer, driver, driver_name), None
def _get_driver_zip_path(driver) -> str:
return str(DriverStore(cfg.DRIVERS_DIR).get_path(driver.sha256))
@router.get("/{printer_id}/packages/ninja")
def get_ninja_package(request: Request, printer_id: int):
"""Download a NinjaRMM-ready ZIP containing install.ps1 and the driver files."""
result, error = _get_printer_and_driver(printer_id, request.state.owner)
if error is not None:
return error
printer, driver, driver_name = result
# Validate driver file exists on disk
driver_zip_path = _get_driver_zip_path(driver)
if not os.path.isfile(driver_zip_path):
return PlainTextResponse("Driver file not found on disk", status_code=422)
safe_name = printer.name.replace(" ", "_")
# Render install script
install_script = render_install(
printer_name=printer.name,
ip_address=printer.ip_address,
port_name=printer.port_name,
driver_name=driver_name,
inf_filename=driver.inf_filename,
duplex_mode=printer.duplex_mode,
color_mode=printer.color_mode,
paper_size=printer.paper_size,
collate=printer.collate,
)
# Build ZIP in-memory
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as zf:
# Add install script
zf.writestr(f"{safe_name}/install.ps1", install_script)
# Extract and re-add driver files from driver ZIP
with zipfile.ZipFile(driver_zip_path, "r") as driver_zf:
for member in driver_zf.namelist():
member_data = driver_zf.read(member)
zf.writestr(f"{safe_name}/drivers/{member}", member_data)
return Response(
content=buf.getvalue(),
media_type="application/zip",
headers={"Content-Disposition": f'attachment; filename="{safe_name}_ninja.zip"'},
)
@router.get("/{printer_id}/packages/intunewin")
def get_intunewin_package(request: Request, printer_id: int):
"""Download a Microsoft Intune .intunewin deployment package."""
result, error = _get_printer_and_driver(printer_id, request.state.owner)
if error is not None:
return error
printer, driver, driver_name = result
# Validate driver file exists on disk
driver_zip_path = _get_driver_zip_path(driver)
if not os.path.isfile(driver_zip_path):
return PlainTextResponse("Driver file not found on disk", status_code=422)
safe_name = printer.name.replace(" ", "_")
# Render all three scripts
install_script = render_install(
printer_name=printer.name,
ip_address=printer.ip_address,
port_name=printer.port_name,
driver_name=driver_name,
inf_filename=driver.inf_filename,
duplex_mode=printer.duplex_mode,
color_mode=printer.color_mode,
paper_size=printer.paper_size,
collate=printer.collate,
)
uninstall_script = render_uninstall(
printer_name=printer.name,
driver_name=driver_name,
port_name=printer.port_name,
)
detect_script = render_detect(printer_name=printer.name)
with tempfile.TemporaryDirectory(prefix="imptune_") as tmpdir:
# Write scripts
with open(os.path.join(tmpdir, "install.ps1"), "w", encoding="utf-8") as f:
f.write(install_script)
with open(os.path.join(tmpdir, "uninstall.ps1"), "w", encoding="utf-8") as f:
f.write(uninstall_script)
with open(os.path.join(tmpdir, "detect.ps1"), "w", encoding="utf-8") as f:
f.write(detect_script)
# Extract driver ZIP contents into tmpdir/drivers/
drivers_subdir = os.path.join(tmpdir, "drivers")
os.makedirs(drivers_subdir, exist_ok=True)
with zipfile.ZipFile(driver_zip_path, "r") as driver_zf:
driver_zf.extractall(drivers_subdir)
# Copy icon into staging if one exists for this printer
icon_record = Icon.get_or_none(Icon.printer == printer.id)
if icon_record is not None:
icon_src = os.path.join(cfg.ICONS_DIR, icon_record.sha256)
if os.path.isfile(icon_src):
shutil.copy2(icon_src, os.path.join(tmpdir, "icon.png"))
# Build .intunewin
output_path = os.path.join(tmpdir, "out.intunewin")
build_intunewin(tmpdir, "install.ps1", output_path)
with open(output_path, "rb") as f:
content = f.read()
return Response(
content=content,
media_type="application/octet-stream",
headers={"Content-Disposition": f'attachment; filename="{safe_name}.intunewin"'},
)