mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
fix(backup): refuse prometheus_enabled when the backup has no token either (#2656)
The companion-credential rule has five conditions, and the second one -- "the backup itself carried a usable credential" -- was applied to all five pairs. It should not be. It is what stops the rule over-refusing an anonymous MQTT broker or an anonymous LDAP bind, both of which are working configs: there, an empty credential in the backup means the restore is not producing anything weaker than what was backed up. For prometheus_enabled it does not transfer. An empty prometheus_token removes /api/v1/metrics' only gate, so the exposure is a property of the toggle, not of a downgrade relative to the backup -- and prometheus_token is optional, so a backup taken on an instance that enabled Prometheus without ever setting one carries the toggle and no usable token. That payload skipped the refusal entirely: not a candidate, so the local-state pass never ran, and the blocklist quietly dropped the token key. On a token-less target the result was prometheus_enabled=true, no token row anywhere, and a full unauthenticated metrics dump -- the same hole the rule was written to close, reached from the likelier of the two directions. So condition 2 is now per-pair: an exposure class (prometheus) that skips it and is judged on local state alone, and an availability class (mqtt, ldap, ha, virtual_printer) that keeps it. Nothing else changes -- the local-state pass already stands down when the instance has its own credential, when HA_TOKEN is in the environment, and when the toggle is already on locally, so "the exposure pre-dates this restore" still holds and refusals still get no tally increment. One wording consequence: an exposure toggle can now be refused on a payload with no credential-like key in it at all, where the shared caveat would have read "0 credential-like key(s) will be skipped". That case gets its own preview detail code, settingsCompanionOnlyWillSkip, in all 13 locales. Tests: 6 that fail pre-fix -- the token key absent and blank at the unit level, the new preview wording, and the integration test through the real endpoint for both payloads (200 with a full metrics body before this, 404 after). Plus 3 controls, because over-refusal is still the real risk: the exposure route must still stand down for a local token and for an already-on toggle, and the availability class must still let a credential-less mqtt/ldap/virtual_printer toggle through. The anonymous-broker and anonymous-bind controls are unchanged and still pass.
This commit is contained in:
@@ -413,6 +413,40 @@ class TestRestoreDoesNotOpenTheMetricsEndpoint:
|
||||
assert "bambuddy_build_info" not in response.text
|
||||
assert any(note["code"] == "settingsCompanionSkipped" for note in tally.notes)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
{"prometheus_enabled": "true", "currency": "EUR"},
|
||||
{"prometheus_enabled": "true", "prometheus_token": "", "currency": "EUR"},
|
||||
],
|
||||
ids=["token-key-absent", "token-blank"],
|
||||
)
|
||||
async def test_a_token_less_backup_leaves_the_endpoint_shut_too(
|
||||
self, async_client: AsyncClient, db_session, payload
|
||||
):
|
||||
"""The route the test above does not cover, and the likelier one.
|
||||
|
||||
``prometheus_token`` is optional, so an instance can enable Prometheus
|
||||
without ever setting it. Such a backup carries the toggle and no usable
|
||||
token — and because the companion rule's second condition asks whether
|
||||
the *backup* had a credential, that payload used to sail straight past
|
||||
the refusal and open the endpoint the case above proves shut.
|
||||
"""
|
||||
from backend.app.services.github_restore import _CategoryTally, github_restore_service
|
||||
|
||||
assert (await async_client.get("/api/v1/metrics")).status_code == 404
|
||||
|
||||
tally = _CategoryTally()
|
||||
await github_restore_service._restore_settings(db_session, {"settings": payload}, overwrite=False, tally=tally)
|
||||
await db_session.commit()
|
||||
|
||||
response = await async_client.get("/api/v1/metrics")
|
||||
assert response.status_code == 404, "a token-less Prometheus backup opened the metrics endpoint"
|
||||
assert "bambuddy_build_info" not in response.text
|
||||
assert any(note["code"] == "settingsCompanionSkipped" for note in tally.notes)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_an_instance_with_its_own_token_still_gets_the_toggle_back(
|
||||
|
||||
Reference in New Issue
Block a user