Files
ImpTune/CLAUDE.md
T
kawaandClaude Opus 5 b397d3dc3d feat: memory-only sessions on HTTP, streamed exports, UI refresh
Session

- COOKIE_SECURE=false no longer persists the owner key for ten years.
  services/session.cookie_kwargs() drops max_age in that mode, so the
  browser holds the key in memory and the session ends with the window.
  Everything still persists server-side; only the browser link is
  temporary. base.html shows a warning banner (FR/EN) and an extra
  paragraph in the onboarding modal, and the README explains the
  trade-off and the backup-key escape hatch.
- Both cookie writers (middleware, POST /session/restore) go through
  cookie_kwargs() so the policy cannot drift between them.
- The CSRF guard on /session/restore compared request.url.scheme against
  the Origin header. Behind a TLS-terminating proxy uvicorn sees http
  while the browser sends https, so every legitimate restore was
  rejected with 403. It now compares hosts only, including
  X-Forwarded-Host.
- /static/*, /favicon.ico and /robots.txt skip the middleware. Each
  cookieless hit was inserting an Owner row no browser could ever use.

Reliability

- Malformed printer-form FK fields no longer escape as HTTP 500:
  a non-numeric client_id/driver_id raised ValueError and an unknown
  driver_id hit a FOREIGN KEY constraint. Both are now 400/404 HTMX
  fragments, and the duplicated field checks moved into
  _validate_fields().
- Package exports stream. build_intunewin() encrypts the inner ZIP in
  1 MB chunks against temp files with a streaming HMAC and SHA256, and
  both endpoints serve the result with FileResponse plus a background
  cleanup task. A 100 MB driver used to be held in memory three or four
  times over per concurrent download. The byte layout is unchanged.
- FileResponse also escapes the download filename, which was previously
  interpolated raw into Content-Disposition.
- python-multipart >= 0.0.18 (CVE-2024-53981, reachable from
  /drivers/upload) and Pillow >= 10.3 (CVE-2024-28219, reachable from
  icon upload).
- icons.py reads cfg.ICONS_DIR instead of re-deriving the path from
  DATA_DIR, matching the .intunewin export.

UI

- Sidebar/topbar shell, inline SVG icon macros (partials/icons.html),
  card and data-table components, grouped printer list, and the
  dedicated /printers/new page replacing partials/printer_form.html.

Tests

- 194 pass with a bare `pytest tests/`: tests/conftest.py now forces
  cfg.COOKIE_SECURE = False like the e2e conftest already did, so the
  Secure cookie is no longer dropped over http://testserver.
- New coverage for the malformed-FK guards, the chunk-boundary cases in
  the encrypt loop (every residue mod _CHUNK plus a multi-megabyte
  payload), temp-dir cleanup after both exports, and the whole
  COOKIE_SECURE matrix.
- test_printer_edit.py located the Edit button by its translated label,
  so it only passed on English-locale machines. It now targets the
  showModal() hook, which also cuts the e2e run from 84s to 15s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:58:49 +02:00

7.8 KiB
Raw Blame History

CLAUDE.md

Guide for Claude Code (claude.ai/code) work in repo.

Commands

Run tests:

pytest tests/                         # whole suite (no env vars needed)
pytest tests/test_inf_parser.py       # single test file (no tests/unit/ dir)
pytest tests/ -k "test_name"          # single test by name

Both tests/conftest.py (tmp_data_dir) and tests/e2e/conftest.py force cfg.COOKIE_SECURE = False, because TestClient talks plain HTTP to http://testserver and a Secure cookie would be dropped — every request would land on a new Owner and ~41 tests would 404. Tests asserting the Secure branch (test_secure_mode_* in tests/test_session.py) monkeypatch it back to True.

Run dev server:

export DATA_DIR=/tmp/imptune_data
export COOKIE_SECURE=false  # plain HTTP — omit if serving behind TLS
uvicorn imptune.main:app --reload --port 8000

Docker:

docker-compose up
docker build -t imptune .

Install deps:

pip install -r requirements.txt -r requirements-dev.txt

Architecture

ImpTune make printer deploy packages (.intunewin for Intune, .zip for NinjaRMM) from Windows driver ZIPs + web UI. No external services — single FastAPI + SQLite + Docker volume.

Request flow:

  1. Driver upload → api/drivers.pyservices/inf_parser.py parse INF → storage/driver_store.py store by SHA256 → Peewee Driver record (shared/global — visible to every Owner)
  2. Printer config → api/printers.pydb/models.py Printer record (links Driver FK, scoped to request.state.owner)
  3. Icon upload → api/icons.py → Pillow validate PNG 256×256 → SHA256 storage → Icon record
  4. Package export → api/packages.pygenerators/script_generator.py render Jinja2 PS1 templates → generators/intunewin_builder.py encrypt ZIP (AES-256-CBC + HMAC-SHA256)

Key modules:

  • imptune/config.pyDATA_DIR, DB_PATH, DRIVERS_DIR, ICONS_DIR, COOKIE_SECURE from env
  • imptune/db/database.py — SQLite WAL mode + foreign_keys=1; all models inherit BaseModel; init_db() also backfills owner_id on pre-per-owner-scoping DBs into a synthetic legacy Owner (key written to {DATA_DIR}/legacy_owner_key.txt)
  • imptune/services/session.pyOwnerSessionMiddleware resolves request.state.owner from the imptune_owner_key cookie, creating one on first visit (skips /health)
  • imptune/services/inf_parser.py — auto-detect encoding (UTF-16/UTF-8/cp1252), resolve %TOKEN% from [Strings], handle multi-model INFs
  • imptune/generators/intunewin_builder.py — Python-native .intunewin (ZIP-in-ZIP); IV 16 bytes (not 32); match reference tool 1.8.6.0 output
  • imptune/templates/scripts/ — Jinja2 templates for install.ps1, uninstall.ps1, detect.ps1

Per-owner storage: Printer/Client (groups) are scoped to an Owner identified by an opaque bearer key in a cookie — no accounts. Driver stays global/shared. Every route taking a printer_id/client_id must filter/check .owner == request.state.owner (404, not 403, on mismatch) — printer IDs are small sequential ints, so a list-only filter isn't enough. Onboarding modal (templates/base.html, gated on request.state.is_new_owner) offers "download backup key" (GET /session/key/download, marks Owner.is_permanent) vs. temporary; /session/restore re-attaches a browser to a previously downloaded key. In tests, use the owner fixture (tests/conftest.py) when creating Printer/Client rows directly via the ORM so the client fixture's cookie-scoped requests can see them.

UI stack: Pico CSS + HTMX 2 + Alpine.js 3 + Jinja2 server-side templates.

Design layer (static/app.css): a token + component layer over Pico. Tokens (--im-*) are declared three times — :root:not([data-theme=dark]), the prefers-color-scheme: dark block, and [data-theme=dark] — mirroring Pico's own selectors so equal specificity + later source order wins; a new color must be added to all three. Pico vars are remapped from those tokens, so use --im-* in components. Prose is set in the system UI face, machine values (IPs, ports, INF names, PS commands) in --im-mono. Components: .card, .rail (the driver → printer → package pipeline on the dashboard), .data-table, .badge, .pill, .kv, .cmd, .empty, .form-section, .toolbar. Because the edit dialog renders inside a table cell, dialog resets inherited text-align / white-space — keep that.

Shell: base.html owns the sidebar + topbar; pages fill the crumb, page_title, page_actions, and content blocks and must not render their own <h1>. Icons come from {% import "partials/icons.html" as ico %}{{ ico.i('printer') }} — inline SVG with no text nodes, because E2E tests read textContent of nav links to assert the translated label. Nav links are {{ ico.i(...) }}<span x-text="...">: never add count badges or other text inside them.

i18n: every user-facing string goes through $store.i18n.t('key') with the English text as the element's fallback body, and keys must be added to both fr and en in base.html. Server-rendered HTMX fragments (icon-upload confirmation, _error_response) are English-only.

The store's default language follows navigator.language, so E2E specs must never locate a control by its visible labelbutton:has-text('Edit') matched only on English-locale machines and timed out everywhere else. Target a structural hook instead (button[onclick*='showModal']), except in test_i18n_toggle.py, which asserts the labels on purpose and pins locale= per context.

Client-side filter: Alpine.store('filter') holds the printer search text. Rows and group cards carry data-search (lowercased) and x-show off that store, so HTMX-swapped rows keep filtering. A group's data-search must be a superset of its rows' — otherwise a matching row hides inside a hidden group. .col-defaults / .col-arch / .col-used / .col-added mark columns dropped on narrow screens or in the add-printer sidebar (.form-aside).

HTMX pattern: Forms hx-post, swap #driver-list / #printer-list / #client-list targets. Errors return inline HTML fragments (HTTP 400/409) via _error_response(). Success return partials from templates/partials/.

PowerShell install script notes:

  • WOW64 64-bit relaunch guard (Intune run 32-bit, pnputil need 64-bit)
  • UAC self-elevation for user context (SYSTEM context skip)
  • Two-step: pnputil /add-driver then Add-PrinterDriver + Add-PrinterPort + Add-Printer
  • All idempotent (-ErrorAction SilentlyContinue)

Test Setup

conftest.py monkeypatch config.DATA_DIR + config.DB_PATH to temp dir per test. client fixture yield TestClient(app) with isolated SQLite. E2E in tests/e2e/ use Playwright.

Environment Variables

Var Default Purpose
DATA_DIR /data Storage root (DB + drivers + icons)
PORT 8000 Server port
COOKIE_SECURE true Owner-session cookie Secure flag. Set false for plain-HTTP serving or the browser drops the cookie and a new Owner is created on every request. false also makes the cookie memory-only (no Max-Age) — see below.

COOKIE_SECURE=false degrades the session instead of weakening the credential: services/session.cookie_kwargs() drops max_age, so the browser holds the owner key in memory and the session ends when the window closes. Everything still persists server-side; only the browser's link to it is temporary. Both cookie-setting call sites (the middleware and POST /session/restore) must go through cookie_kwargs(). request.state.ephemeral_session mirrors the flag, and base.html renders the #ephemeral-session-warning banner plus an extra paragraph in the onboarding modal off it. Changing this touches tests/test_session.py::test_insecure_mode_* / test_secure_mode_*.