djust 1.0.4

StableSecurityReleased
Install
pip install djust==1.0.4

Added

  • auto_navigate — opt-in automatic SPA link interception (#1734, ADR-021 Stage 2). With dj-navigate you annotate each link; auto_navigate goes one step further — a single delegated click listener SPA-navigates plain <a href> links (no djust attribute needed) whenever the link's path resolves in the route map. Enable via LIVEVIEW_CONFIG['auto_navigate'] = True (default OFF); {% djust_client_config %} then emits a <meta name="djust-auto-navigate"> flag (CSP-clean, no inline script) and the client installs one delegated document listener. It is deliberately conservative — a link falls through to a normal browser navigation on a modifier/middle click, a target other than _self, a download attr, rel="external", a data-no-navigate ancestor, an external origin or non-http(s) scheme, a same-page hash-only jump, or any path not in the (auth-filtered, #1758) route map — so admin pages, plain Django views, and routes the user can't access reload normally and the server enforces access. Same-view query-only changes use live_patch (state-preserving); cross-view uses live_redirect. Native dj-navigate/auto_navigate is positioned as djust's canonical SPA model; turbonav-integration.md is reframed as interop (#1735). New TestRouteMapAuthFilter-adjacent JS suite (tests/js/auto_navigate_1734.test.js, 15 cases incl. the full opt-out matrix) + TestClientConfig emit tests.
  • djust deploy preflight "deploy doctor" warns on settings that violate the platform env contract (#1760). A foreign (non-scaffold) app — one written for on-device/loopback use — could deploy "successfully" and then 500 silently in production because its Django settings ignore the env values the platform injects. djust deploy / djust deploy-dir now run a non-blocking static preflight (_run_deploy_doctor) over the resolved settings module (located via manage.py's DJANGO_SETTINGS_MODULE, falling back to the shallowest settings.py) and print warnings to stderr — never errors, the deploy still proceeds — when it finds: a hardcoded literal SECRET_KEY (dev key on a public host), an ALLOWED_HOSTS literal not read from env (→ DisallowedHost 400), a DATABASES block that never consults DATABASE_URL (→ OperationalError: readonly database on the read-only app rootfs), or a sqlite ENGINE (sqlite under the read-only project dir 500s on first write). Each warning ends with the env-injection pointer. The checks are grep-level static inspection of the settings source text — the module is never imported, so an unimportable foreign settings file can't break the doctor, and the whole pass is wrapped fail-soft so a doctor error can never block a deploy. Prevents the silent "successful deploy, 500s at runtime" chain that cost a multi-hour production debug. 14 new cases in TestDeployDoctor.
  • djust deploy shows "rolling out" instead of "active" while a blue/green rollout is still serving stale code (#1761). djustlive's deployment_status endpoint now returns an additive serving_current boolean (djustlive #517) — False while a new rootfs is built and marked current but the old placement is still serving the env URL during cutover. The deploy-dir poll loop now consumes it: while the deployment row reads active/deploying but serving_current is False, the CLI prints Status: rolling out (new version built; old version still serving — waiting for cutover) and keeps polling, instead of reporting active and printing the URL on stale code (which led users to re-test against the old rootfs). A missing field is treated as True (fail-safe), so the behavior is byte-identical against older servers that don't send it. The poll decision is extracted into a pure _poll_display(data) -> (message, done, url) helper. 8 new cases in TestPollDisplay.

Security

  • Documented LIVEVIEW_ALLOWED_MODULES as recommended production hardening (#1778, threat model T4). The WebSocket mount allowlist is enforced only when non-empty; an unset list lets a client request mounting any LiveView by path (enumeration — not an auth bypass, since per-view auth still gates). Added guidance to docs/guides/security.md + docs/SECURITY_GUIDELINES.md to set it to your app's module prefixes in prod (djust.V005 flags views outside a non-empty list but cannot warn about an unset one).

  • Opt-in per-event auth re-check on the WebSocket path (#1777, threat model T3, defense-in-depth). Auth runs at mount; the connect-time scope user is cached, so an authenticated user who logs out or loses a permission mid-session keeps dispatching events on the open socket until they reconnect. New LIVEVIEW_CONFIG['reauth_on_event'] (default OFF): when enabled and the mounted view declares login_required/permission_required, handle_event re-resolves the user from the session (channels.auth.get_user) and re-runs the view's auth check, sending a navigate redirect + close(4403) + clearing view_instance on failure. Default OFF because it costs one session read per event — opt in for high-security apps that want mid-session deauthorization enforced on the live path. Fail-safe (skips the check, never breaks the event) when there is no session in scope. Behavior note: when the flag is on, the re-resolved (current) user is also written to request.user, so event handlers observe live auth state rather than the connect-time snapshot. Complements the T1/T2 mount-redirect fix.

  • WebSocket auth bypass fixed: a login_required mount redirect now closes the socket instead of leaving it open (threat model T1/T2). handle_mount sent a {"type":"navigate"} redirect frame on the login_required (and on_mount-hook) failure branches but did not close the socket — only the PermissionDenied branch closed it (4403). The view never mounted yet view_instance stayed set, and handle_event never re-checks auth, so a raw WebSocket client that ignored the navigate frame could send {"type":"event"} messages and reach @event_handler methods with no authenticated session — a full auth bypass on the live mutation path (a browser obeys the redirect and hides it). Reachable via both the initial mount and handle_live_redirect_mount (which delegates to handle_mount). Both redirect branches now send the navigate frame, then close(code=4403) and clear view_instance, mirroring the PermissionDenied branch; public/authorized mounts are unchanged. A WebsocketCommunicator reproducer (anonymous scope) proves the bypass pre-fix and the close post-fix. The complete WS auth/transport threat model — 9 threats, including T3 (the event path does not re-check auth mid-session) and T4 (LIVEVIEW_ALLOWED_MODULES is default-open), tracked as follow-ups — is documented in docs/audits/websocket-auth-2026-06.md.

  • The auto-emitted client route map is now auth-filtered — gated routes no longer leak to clients that can't access them (#1758, ADR-021 Stage 2). window.djust._routeMap was built by build_route_map_from_urlconf with no auth filtering and emitted to every client (including anonymous visitors), enumerating all LiveView routes — login_required / permission_required / admin ones included — each with its dotted module.QualName view-class path. An anonymous visitor to a public page therefore learned the full route table plus the internal view-class names of routes they cannot reach (recon-grade information disclosure; not an auth bypass — the WS mount path allowlists modules and views still enforce auth at mount). get_route_map_script(request) — the single funnel both template engines use — now omits any route whose LiveView declares login_required/permission_required (or whose callback is login_required(as_view())-wrapped, or which uses Django's LoginRequiredMixin/PermissionRequiredMixin) unless request.user satisfies it, and fails closed for anonymous / no-request callers. Public routes are unaffected, so existing public apps emit an identical map. A gating sidecar is built by the same single URLconf walk (no extra cost). New TestRouteMapAuthFilter cases + a dedicated fixture URLconf.

  • Bump PyO3 0.25 → 0.29 to fix two advisories — GHSA-36hh-v3qg-5jq4 (High) and GHSA-chgr-c6px-7xpp (Moderate) (#103, #104). Both advisories cover all PyO3 versions < 0.29.0 with no backport: an out-of-bounds read in nth/nth_back for PyList/PyTuple iterators (High), and a missing Sync bound on PyCFunction::new_closure closures (Moderate). No Dependabot PR existed for either. Bumps pyo3 and pyo3-async-runtimes 0.25 → 0.29 and migrates the FFI layer across the 0.26–0.29 breaking changes: Python::with_gilPython::attach and Python::allow_threadsPython::detach (GIL attach/detach rename), the removed PyObject alias → Py<PyAny>, Bound::downcastBound::cast, the reshaped two-lifetime FromPyObject trait (extract(Borrowed<…>) replacing extract_bound(&Bound<…>)), and the now-opt-in Clone auto-FromPyObject on #[pyclass] types (PyVNode opts in, SupervisorStatsPy opts out). Behavior-preserving: clippy clean under -D warnings, the full Rust suite (two-phase) and the full Python suite both pass against the rebuilt extension; no public API change.

Fixed

  • Deploy doctor no longer false-positives on env-derived DATABASES built from individual os.environ vars (#1768, follow-up to #1760). The #1760 DATABASES check warned whenever the settings text mentioned neither DATABASE_URL nor dj_database_url, so a perfectly environment-driven config assembled from individual os.environ['DB_NAME']/['DB_HOST']/… vars got a spurious "doesn't consult DATABASE_URL" warning. The env-read detection is now widened — the canonical DATABASE_URL/dj_database_url tokens still count anywhere, and other env reads (os.environ, os.getenv, python-decouple config()/env()) count within the DATABASES assignment region (a new brace-balancing _databases_region() scopes it, so an unrelated SECRET_KEY = os.environ[...] elsewhere can't mask a genuinely hardcoded DB block). 2 new cases in TestDeployDoctor.

  • Log the swallowed theme-context cache-write skip instead of silently passing (#2380). CodeQL flagged except (AttributeError, TypeError): pass in theme_context (python/djust/theming/context_processors.py) as an empty except. The branch intentionally skips the per-request cache write for request objects that can't hold arbitrary attributes (a __slots__ object in tests / exotic callers) — correctness over the micro-optimization — but the swallowed write was dropped with no trace. It now emits a logger.debug naming the request type and the exception, satisfying both CodeQL and the project's no-bare-except rule. Behavior is otherwise unchanged. New case in TestThemeContextCache.

  • djust deploy now respects .gitignore when building the deploy tarball, and warns before oversized uploads (#1759). _create_tarball previously walked the source tree using a hardcoded EXCLUDE_* list, ignoring the project's .gitignore. Non-standard names — a .venv-dev/ virtualenv carrying a 115 MB Playwright node binary, scratch/ screenshots, mobile-app/wheels/ — slipped straight in, producing a 152 MB tarball that the server's ingress rejected with a raw 413. Fix: when the source is a git work tree, _create_tarball takes its file list from git ls-files --cached --others --exclude-standard (tracked ∪ untracked, minus ignored), so the project's .gitignore is the source of truth. Non-git directories fall back to the existing os.walk path unchanged. The EXCLUDE_* security net (EXCLUDE_DIR_NAMES, EXCLUDE_FILENAMES, EXCLUDE_FILE_SUFFIXES, EXCLUDE_FILENAME_STEMS) still applies on the git path so credentials and live databases are dropped even if the user forgot to gitignore them (#1505 intent preserved). A new _tarball_size_warning prints an actionable warning to stderr before upload when the packed tarball exceeds 50 MB, listing the largest included files so the user can identify what to add to .gitignore instead of hitting a raw nginx 413 page. 6 new regression cases in python/tests/test_deploy_cli.py.

All releases · Atom feed