Ver código fonte

chore(deps): floor-pin pydantic-settings >=2.14.2 + msgpack >=1.2.1 for clean pip-audit

      pip-audit flagged two advisories at the resolved versions in the venv.
      Neither is reachable in shipped Bambuddy, but the pins are taken so
      the audit stays clean and a future reachable advisory in either
      package isn't masked by existing noise.

      pydantic-settings 2.14.2 patches GHSA-4xgf-cpjx-pc3j —
      NestedSecretsSettingsSource with secrets_nested_subdir=True followed
      symlinks pointing outside the configured secrets_dir, reading
      out-of-tree files into settings values and bypassing the documented
      secrets_dir_max_size cap. Affected: >=2.12.0, <2.14.2. Bambuddy uses
      pydantic-settings only for env-var-backed config; the secrets-dir
      loader is not used (grep clean on NestedSecretsSettingsSource /
      secrets_nested_subdir / secrets_dir under backend/).

      msgpack 1.2.1 patches GHSA-6v7p-g79w-8964 — reusing an Unpacker
      instance after it caught an error can crash with SEGV, which is a
      DoS vector on untrusted input. msgpack is not a runtime dep of
      Bambuddy; it enters the tree only as a transitive of CacheControl,
      itself pulled by pip-audit (the very tool that surfaced the
      advisory). Pin placed in requirements-dev.txt next to pip-audit so
      it travels with the security-scan tooling rather than implying a
      runtime use.
maziggy 2 meses atrás
pai
commit
458bfa157b

Diferenças do arquivo suprimidas por serem muito extensas
+ 2 - 0
CHANGELOG.md


+ 8 - 0
backend/app/main.py

@@ -6351,6 +6351,14 @@ PUBLIC_API_ROUTES = {
     "/api/v1/updates/version",
     "/api/v1/updates/version",
     # Metrics endpoint handles its own prometheus_token authentication
     # Metrics endpoint handles its own prometheus_token authentication
     "/api/v1/metrics",
     "/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)
 # Route prefixes that are public (for routes with dynamic segments)

+ 19 - 0
backend/tests/integration/test_auth_api.py

@@ -854,6 +854,25 @@ class TestAuthMiddlewarePublicRoutes:
         assert response.status_code == 200
         assert response.status_code == 200
         assert "auth_enabled" in response.json()
         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.asyncio
     @pytest.mark.integration
     @pytest.mark.integration
     async def test_auth_login_is_public(self, async_client: AsyncClient, enabled_auth):
     async def test_auth_login_is_public(self, async_client: AsyncClient, enabled_auth):

+ 12 - 6
backend/tests/unit/services/test_virtual_printer.py

@@ -2964,7 +2964,7 @@ class TestSlicerProxyManager:
         slicer and printer for all protocols except MQTT, which must be
         slicer and printer for all protocols except MQTT, which must be
         TLS-terminated to rewrite the printer's IP in MQTT payloads.
         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 (
         from backend.app.services.virtual_printer.tcp_proxy import (
             SlicerProxyManager,
             SlicerProxyManager,
@@ -2984,16 +2984,22 @@ class TestSlicerProxyManager:
             bind_address="10.0.0.1",
             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 (
         with (
             patch("asyncio.create_task") as mock_create_task,
             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"),
             patch.object(SlicerProxyManager, "_log_activity"),
         ):
         ):
             mock_create_task.return_value = MagicMock()
             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()
             await mgr.start()
 
 
         # FTP, FileTransfer, RTSP should be TCPProxy (transparent)
         # FTP, FileTransfer, RTSP should be TCPProxy (transparent)

+ 4 - 0
requirements-dev.txt

@@ -18,5 +18,9 @@ pyOpenSSL>=26.0.0
 # Security scanning
 # Security scanning
 bandit[sarif]>=1.7.0
 bandit[sarif]>=1.7.0
 pip-audit>=2.7.0
 pip-audit>=2.7.0
+# Transitive of pip-audit→CacheControl. 1.2.1 patches GHSA-6v7p-g79w-8964
+# (Unpacker SEGV/DoS on reuse after caught error). Not a runtime dep of
+# Bambuddy — pinned here so the audit stays clean.
+msgpack>=1.2.1
 # Secrets scan: gitleaks (Go binary, not a Python package).
 # Secrets scan: gitleaks (Go binary, not a Python package).
 # Install: go install github.com/zricethezav/gitleaks/v8@latest
 # Install: go install github.com/zricethezav/gitleaks/v8@latest

+ 4 - 1
requirements.txt

@@ -21,7 +21,10 @@ greenlet>=3.0.0
 
 
 # Pydantic
 # Pydantic
 pydantic>=2.0.0
 pydantic>=2.0.0
-pydantic-settings>=2.0.0
+# 2.14.2 patches GHSA-4xgf-cpjx-pc3j (NestedSecretsSettingsSource follows
+# symlinks out of secrets_dir). Bambuddy does not use that source — pin
+# is precautionary so the audit stays clean.
+pydantic-settings>=2.14.2
 # Transitive of pydantic-settings, floor-pinned to patch CVE-2026-28684 (dotenv 1.2.1)
 # Transitive of pydantic-settings, floor-pinned to patch CVE-2026-28684 (dotenv 1.2.1)
 python-dotenv>=1.2.2
 python-dotenv>=1.2.2
 
 

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff