diff --git a/companion/pairing.py b/companion/pairing.py index 66c63bc..4819730 100644 --- a/companion/pairing.py +++ b/companion/pairing.py @@ -64,11 +64,16 @@ def find_admin_user() -> str | None: auth_path = os.path.join("data", "auth.json") try: with open(auth_path, "r", encoding="utf-8") as f: - users = (json.load(f) or {}).get("users", {}) + data = json.load(f) except (OSError, json.JSONDecodeError): return None + if not isinstance(data, dict): + return None + users = data.get("users") or {} + if not isinstance(users, dict): + return None for uname, udata in users.items(): - if udata.get("is_admin") is True: + if isinstance(udata, dict) and udata.get("is_admin") is True: return uname return next(iter(users), None) diff --git a/tests/test_companion_pairing.py b/tests/test_companion_pairing.py index 3bedd15..d7ec04d 100644 --- a/tests/test_companion_pairing.py +++ b/tests/test_companion_pairing.py @@ -102,6 +102,16 @@ def test_pairing_payload_shape(): assert p == {"v": 1, "host": "192.168.1.9", "port": 7000, "token": "ody_x"} +@pytest.mark.parametrize("payload", ["[]", '{"users": []}']) +def test_find_admin_user_ignores_invalid_auth_shape(tmp_path, monkeypatch, payload): + data_dir = tmp_path / "data" + data_dir.mkdir() + (data_dir / "auth.json").write_text(payload) + monkeypatch.chdir(tmp_path) + + assert P.find_admin_user() is None + + # --- admin-only gate: a bearer/non-admin caller is rejected ---------------- def _admin_mgr(is_admin):