From e726ef9926fcb2e07ddcc2a7632b5a13bb6354db Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Wed, 1 Apr 2026 12:19:24 -0700 Subject: [PATCH] feat: redact federation/remote keys from GET /api/settings --- muxplex/main.py | 11 +++++- muxplex/tests/test_api.py | 81 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/muxplex/main.py b/muxplex/main.py index b7628f0..5d8cefa 100644 --- a/muxplex/main.py +++ b/muxplex/main.py @@ -9,6 +9,7 @@ Background poll loop reconciles tmux session state every POLL_INTERVAL seconds. import asyncio import contextlib +import copy import hmac import json import logging @@ -540,8 +541,14 @@ async def setup_hooks() -> dict: @app.get("/api/settings") async def get_settings() -> dict: - """Return the current settings.""" - return load_settings() + """Return the current settings with sensitive keys redacted.""" + settings = load_settings() + result = copy.deepcopy(settings) + result["federation_key"] = "" + for inst in result.get("remote_instances", []): + if "key" in inst: + inst["key"] = "" + return result @app.patch("/api/settings") diff --git a/muxplex/tests/test_api.py b/muxplex/tests/test_api.py index a8186b1..06ef64f 100644 --- a/muxplex/tests/test_api.py +++ b/muxplex/tests/test_api.py @@ -938,6 +938,87 @@ def test_get_settings_returns_saved_values(client, tmp_path, monkeypatch): assert data["sort_order"] == "alphabetical" +def test_get_settings_redacts_federation_key(client, tmp_path, monkeypatch): + """GET /api/settings must NOT return the federation_key value — it must be empty string.""" + import json + + import muxplex.settings as settings_mod + + settings_path = tmp_path / "settings.json" + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_path) + + # Save a settings file that includes a non-empty federation_key + settings_path.write_text(json.dumps({"federation_key": "secret-should-not-appear"})) + + response = client.get("/api/settings") + assert response.status_code == 200 + data = response.json() + + # The federation_key key may be present but its value must be empty + assert data.get("federation_key") == "", ( + f"federation_key must be redacted (empty string), got: {data.get('federation_key')!r}" + ) + # The secret must not appear anywhere in the response + assert "secret-should-not-appear" not in str(data), ( + "federation_key secret value must not appear anywhere in the response" + ) + + +def test_get_settings_redacts_remote_instance_keys(client, tmp_path, monkeypatch): + """GET /api/settings must redact the 'key' field from each item in remote_instances.""" + import json + + import muxplex.settings as settings_mod + + settings_path = tmp_path / "settings.json" + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_path) + + # Save settings with remote_instances containing secret key fields + settings_path.write_text( + json.dumps( + { + "remote_instances": [ + { + "url": "http://remote-a:8088", + "key": "remote-secret-key-a", + "name": "Remote A", + "id": "remote-a", + }, + { + "url": "http://remote-b:8088", + "key": "remote-secret-key-b", + "name": "Remote B", + "id": "remote-b", + }, + ] + } + ) + ) + + response = client.get("/api/settings") + assert response.status_code == 200 + data = response.json() + + remote_instances = data.get("remote_instances", []) + assert len(remote_instances) == 2, ( + f"Expected 2 remote_instances, got {len(remote_instances)}" + ) + + for i, inst in enumerate(remote_instances): + assert inst.get("key") == "", ( + f"remote_instances[{i}]['key'] must be redacted (empty string), " + f"got: {inst.get('key')!r}" + ) + + # The secrets must not appear anywhere in the response + assert "remote-secret-key-a" not in str(data), ( + "remote-secret-key-a must not appear anywhere in the response" + ) + assert "remote-secret-key-b" not in str(data), ( + "remote-secret-key-b must not appear anywhere in the response" + ) + + # --------------------------------------------------------------------------- # PATCH /api/settings # ---------------------------------------------------------------------------