mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-08-11 00:30:12 -04:00
fix(auth): expose /api/v1/system/appliance through the auth middleware allowlist
The /system/appliance endpoint is fetched by the SPA's i18n bootstrap on mount to seed locale, hostname, timezone, and the chrony NTP-gate state BEFORE any login state exists. The route handler itself has no auth dependency and the test_route_auth_coverage allowlist correctly marks it public, but the global auth_middleware in main.py — which short-circuits every /api/ path not in PUBLIC_API_ROUTES — was never told about it. Result: every browser session on an auth-enabled install logged a 401 on the appliance endpoint before login. Added /api/v1/system/appliance to PUBLIC_API_ROUTES with a comment pointing at the dual-list pattern so this doesn't drift again, and a regression test in TestAuthMiddlewarePublicRoutes that posts /auth/setup to turn auth on, then asserts the endpoint returns 200 with the documented shape (hostname / timezone / locale / time_synced fields all present).
This commit is contained in:
parent
70857af393
commit
c236fdc650
3 changed files with 39 additions and 6 deletions
|
|
@ -6351,6 +6351,14 @@ PUBLIC_API_ROUTES = {
|
|||
"/api/v1/updates/version",
|
||||
# Metrics endpoint handles its own prometheus_token authentication
|
||||
"/api/v1/metrics",
|
||||
# Appliance bootstrap (#1589 follow-up): the SPA's i18n setup polls
|
||||
# this BEFORE a JWT is available to pick up the firstboot wizard's
|
||||
# hostname / timezone / locale and the chrony NTP-gate state. The
|
||||
# response contains user-set defaults and a public sync flag — no
|
||||
# secrets. Without this entry the global auth middleware returns 401
|
||||
# before the route handler runs, regardless of the route's own
|
||||
# "no auth required" intent.
|
||||
"/api/v1/system/appliance",
|
||||
}
|
||||
|
||||
# Route prefixes that are public (for routes with dynamic segments)
|
||||
|
|
|
|||
|
|
@ -854,6 +854,25 @@ class TestAuthMiddlewarePublicRoutes:
|
|||
assert response.status_code == 200
|
||||
assert "auth_enabled" in response.json()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_system_appliance_is_public(self, async_client: AsyncClient, enabled_auth):
|
||||
"""Verify /api/v1/system/appliance is reachable without a JWT.
|
||||
|
||||
The SPA's i18n bootstrap fetches this BEFORE login to seed locale,
|
||||
hostname, timezone, and NTP-gate state. The route handler has no
|
||||
auth dependency, but the global auth_middleware blocks every
|
||||
/api/ path not in PUBLIC_API_ROUTES — so without an explicit
|
||||
allowlist entry the user sees a 401 in the browser console on
|
||||
every page load.
|
||||
"""
|
||||
response = await async_client.get("/api/v1/system/appliance")
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
# Shape contract (no-auth surface):
|
||||
for key in ("hostname", "timezone", "locale", "time_synced"):
|
||||
assert key in body
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_auth_login_is_public(self, async_client: AsyncClient, enabled_auth):
|
||||
|
|
|
|||
|
|
@ -2964,7 +2964,7 @@ class TestSlicerProxyManager:
|
|||
slicer and printer for all protocols except MQTT, which must be
|
||||
TLS-terminated to rewrite the printer's IP in MQTT payloads.
|
||||
"""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
from backend.app.services.virtual_printer.tcp_proxy import (
|
||||
SlicerProxyManager,
|
||||
|
|
@ -2984,16 +2984,22 @@ class TestSlicerProxyManager:
|
|||
bind_address="10.0.0.1",
|
||||
)
|
||||
|
||||
# Mock asyncio.create_task and asyncio.gather to prevent actual server start
|
||||
# Mock asyncio.create_task and asyncio.gather to prevent actual
|
||||
# server start. Close every coroutine handed to gather — otherwise
|
||||
# the ~110 run_with_logging() coros built inside start() are
|
||||
# garbage-collected unfinalized and surface later as
|
||||
# PytestUnraisableExceptionWarning at random in other tests.
|
||||
async def _close_pending(*coros, **_):
|
||||
for c in coros:
|
||||
if asyncio.iscoroutine(c):
|
||||
c.close()
|
||||
|
||||
with (
|
||||
patch("asyncio.create_task") as mock_create_task,
|
||||
patch("asyncio.gather", new_callable=AsyncMock),
|
||||
patch("asyncio.gather", side_effect=_close_pending),
|
||||
patch.object(SlicerProxyManager, "_log_activity"),
|
||||
):
|
||||
mock_create_task.return_value = MagicMock()
|
||||
# start() will create proxies then try to gather tasks — we just
|
||||
# need to verify the proxy types after creation.
|
||||
# Trigger start but let gather return immediately.
|
||||
await mgr.start()
|
||||
|
||||
# FTP, FileTransfer, RTSP should be TCPProxy (transparent)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue