mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
The Clear Plate button (and 4 other features on the Printers page) read their state from /settings, which requires SETTINGS_READ. Granting that permission also adds the Settings nav item and leaks SMTP/LDAP/MQTT credentials — exactly what users were trying to avoid by giving an operator only printers:clear_plate. New /settings/ui-preferences endpoint returns a curated, opt-in subset of non-sensitive fields. Matches the existing /default-sidebar-order precedent. PrintersPage switched to the new endpoint; admin pages still use /settings for full access.
120 lines
4.7 KiB
Python
120 lines
4.7 KiB
Python
"""Tests for the public /settings/ui-preferences endpoint (#1293).
|
|
|
|
Reporter @Tivonfeng: granting `printers:clear_plate` alone wasn't enough to
|
|
make the Clear Plate button work — the frontend also needs `require_plate_clear`
|
|
from /settings, which requires SETTINGS_READ (and also surfaces SMTP/LDAP/MQTT
|
|
secrets). The fix is a public subset endpoint that returns only UI rendering
|
|
fields, so the frontend doesn't have to demand SETTINGS_READ for non-admin UX.
|
|
|
|
The two guarantees pinned here:
|
|
1. The endpoint is accessible without SETTINGS_READ.
|
|
2. The endpoint NEVER returns sensitive fields (SMTP/LDAP/MQTT credentials,
|
|
API tokens, HA bearer token, etc.) — even if a future commit accidentally
|
|
adds one of those keys to _UI_PREFERENCE_FIELDS, this test fails loudly.
|
|
"""
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
# Anything in this list MUST NOT appear in the /ui-preferences response.
|
|
# Mirror of _SENSITIVE_FIELDS_FOR_API_KEY in backend/app/api/routes/settings.py
|
|
# plus a wider net for any *_password / *_token / *_key suffix.
|
|
_SENSITIVE_KEYS = {
|
|
"smtp_password",
|
|
"smtp_username",
|
|
"smtp_from_email",
|
|
"smtp_host",
|
|
"smtp_port",
|
|
"mqtt_password",
|
|
"mqtt_username",
|
|
"mqtt_broker",
|
|
"ha_token",
|
|
"ha_url",
|
|
"prometheus_token",
|
|
"virtual_printer_access_code",
|
|
"ldap_bind_password",
|
|
"ldap_bind_dn",
|
|
"ldap_server_url",
|
|
"external_url",
|
|
"bambu_studio_api_url",
|
|
"orcaslicer_api_url",
|
|
"local_backup_path",
|
|
"github_token",
|
|
"gitea_token",
|
|
"obico_api_key",
|
|
"obico_endpoint_url",
|
|
}
|
|
|
|
|
|
class TestUiPreferencesEndpoint:
|
|
"""The new public endpoint must work without SETTINGS_READ and must
|
|
never return sensitive fields."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_endpoint_returns_200_without_auth(self, async_client: AsyncClient):
|
|
"""No SETTINGS_READ required — that's the whole point of the endpoint."""
|
|
response = await async_client.get("/api/v1/settings/ui-preferences")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert isinstance(data, dict)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_require_plate_clear(self, async_client: AsyncClient):
|
|
"""The field that drove #1293: PrintersPage gates the Clear Plate button
|
|
on this. Must be present in the response."""
|
|
response = await async_client.get("/api/v1/settings/ui-preferences")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "require_plate_clear" in data
|
|
# Type must be bool (frontend does === true checks)
|
|
assert isinstance(data["require_plate_clear"], bool)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_expected_field_set(self, async_client: AsyncClient):
|
|
"""Pin the exact set of fields the endpoint exposes — adding a sensitive
|
|
field to _UI_PREFERENCE_FIELDS by accident should fail this assert and
|
|
force the author to reconsider."""
|
|
response = await async_client.get("/api/v1/settings/ui-preferences")
|
|
data = response.json()
|
|
expected = {
|
|
"require_plate_clear",
|
|
"check_printer_firmware",
|
|
"camera_view_mode",
|
|
"time_format",
|
|
"date_format",
|
|
"drying_presets",
|
|
"ams_humidity_good",
|
|
"ams_humidity_fair",
|
|
"ams_temp_good",
|
|
"ams_temp_fair",
|
|
"bed_cooled_threshold",
|
|
}
|
|
assert set(data.keys()) == expected
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_response_excludes_sensitive_fields(self, async_client: AsyncClient, db_session):
|
|
"""Even with sensitive fields seeded in the DB, none of them must
|
|
appear in the response — the endpoint is opt-in, not opt-out."""
|
|
from backend.app.models.settings import Settings
|
|
|
|
# Seed every sensitive field with a unique recognizable value so a leak
|
|
# would be obvious in failure output.
|
|
for i, key in enumerate(_SENSITIVE_KEYS):
|
|
db_session.add(Settings(key=key, value=f"SECRET_VALUE_{i}_DO_NOT_LEAK"))
|
|
await db_session.commit()
|
|
|
|
response = await async_client.get("/api/v1/settings/ui-preferences")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
# No sensitive key should appear in the response keys
|
|
leaked_keys = _SENSITIVE_KEYS & set(data.keys())
|
|
assert leaked_keys == set(), f"Leaked sensitive fields: {leaked_keys}"
|
|
|
|
# And the recognizable values shouldn't appear in any value either
|
|
response_text = response.text
|
|
for i in range(len(_SENSITIVE_KEYS)):
|
|
assert f"SECRET_VALUE_{i}_DO_NOT_LEAK" not in response_text, (
|
|
f"Sensitive value index {i} leaked into response body"
|
|
)
|