swordmasters-optimizer / tests /test_mod_access.py
oettaM123's picture
Claude Opus 5.5
Secure mod access: one-time invite links, per-device sessions, device list
370fc18
Raw History Blame Contribute Delete
9.78 kB
import os
import re
import sys
import tempfile
import time
import unittest
from pathlib import Path
from types import SimpleNamespace
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
import app as A # noqa: E402
from fastapi.testclient import TestClient # noqa: E402
OWNER_KEY = "test-owner-key-0123456789"
LEGACY_KEY = "legacy-shared-key-xyz"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0 Safari/537.36"
class ModAccessTests(unittest.TestCase):
"""One-time invite links -> per-device sessions; the old shared key grants nothing."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self._saved = (A._MOD_STORE_PATH, A.SM_MOD_KEY, os.environ.get("SM_OWNER_KEY"))
A._MOD_STORE_PATH = Path(self.tmp.name) / "mod_access.json"
A._MOD = None
A._MOD_LAST_SAVE = 0.0
A._MOD_LOGIN_FAILS.clear()
A.SM_MOD_KEY = LEGACY_KEY
os.environ["SM_OWNER_KEY"] = OWNER_KEY
def tearDown(self):
A._MOD_STORE_PATH, A.SM_MOD_KEY, owner = self._saved
if owner is None:
os.environ.pop("SM_OWNER_KEY", None)
else:
os.environ["SM_OWNER_KEY"] = owner
A._MOD = None
A._MOD_LOGIN_FAILS.clear()
self.tmp.cleanup()
# ── helpers ──
def device(self, ip="10.0.0.1"):
return TestClient(A.app, headers={"X-Forwarded-For": ip, "User-Agent": UA})
def owner(self, ip="10.0.0.1"):
c = self.device(ip)
r = c.post("/mod/admin/login", data={"key": OWNER_KEY, "name": "matteo-pc"}, follow_redirects=False)
self.assertEqual(r.status_code, 303)
self.assertIn(A.SM_MOD_SESSION_COOKIE, c.cookies)
return c
def invite(self, owner, name="Nice Face", role="mod"):
r = owner.post("/mod/admin/action", data={"do": "invite", "name": name, "role": role, "hours": "24"})
self.assertEqual(r.status_code, 200)
m = re.search(r"/mod/join\?t=([A-Za-z0-9_\-]+)", r.text)
self.assertIsNotNone(m, "invite link not shown")
return m.group(1)
@staticmethod
def sees_mod_ui(c):
return 'data-tab="room"' in c.get("/").text
# ── tests ──
def test_old_shared_key_grants_nothing(self):
c = self.device()
r = c.get(f"/?mod={LEGACY_KEY}", follow_redirects=False)
self.assertEqual(r.status_code, 303)
self.assertNotIn(A.SM_MOD_SESSION_COOKIE, c.cookies)
self.assertFalse(self.sees_mod_ui(c))
legacy = TestClient(A.app, cookies={"sm_mod": LEGACY_KEY})
self.assertFalse(self.sees_mod_ui(legacy))
for path in ("/room-scan", f"/room-scan?mod_key={LEGACY_KEY}", "/usage-admin",
f"/room-lookup-log?mod_key={LEGACY_KEY}", "/power-series-admin"):
self.assertEqual(legacy.get(path).status_code, 404, path)
self.assertEqual(legacy.get("/room-lookup?code=ABCDEF").status_code, 502)
def test_admin_is_hidden_and_owner_login_works(self):
anon = self.device()
self.assertEqual(anon.get("/mod/admin").status_code, 404)
self.assertIn("Owner login", anon.get("/mod/admin?login").text)
bad = anon.post("/mod/admin/login", data={"key": "wrong-key-wrong-key"}, follow_redirects=False)
self.assertEqual(bad.status_code, 403)
self.assertNotIn(A.SM_MOD_SESSION_COOKIE, anon.cookies)
owner = self.owner()
page = owner.get("/mod/admin")
self.assertEqual(page.status_code, 200)
self.assertIn("Devices with access (1)", page.text)
self.assertIn("matteo-pc", page.text)
self.assertTrue(self.sees_mod_ui(owner))
def test_owner_login_is_rate_limited_per_ip(self):
attacker = self.device("6.6.6.6")
for _ in range(5):
attacker.post("/mod/admin/login", data={"key": "guess-guess-guess"})
blocked = attacker.post("/mod/admin/login", data={"key": OWNER_KEY}, follow_redirects=False)
self.assertEqual(blocked.status_code, 403)
self.owner("10.0.0.9") # a different network still gets in
def test_owner_login_disabled_without_strong_key(self):
os.environ["SM_OWNER_KEY"] = "short"
c = self.device()
self.assertEqual(c.post("/mod/admin/login", data={"key": "short"}, follow_redirects=False).status_code, 403)
def test_invite_link_works_exactly_once_and_previews_dont_burn_it(self):
token = self.invite(self.owner())
preview = self.device("162.159.0.1") # e.g. Discord's link preview fetcher
self.assertEqual(preview.get(f"/mod/join?t={token}").status_code, 200)
self.assertEqual(preview.get(f"/mod/join?t={token}").status_code, 200)
self.assertNotIn(A.SM_MOD_SESSION_COOKIE, preview.cookies)
mod = self.device("20.0.0.2")
r = mod.post("/mod/join", data={"t": token}, follow_redirects=False)
self.assertEqual(r.status_code, 303)
self.assertTrue(self.sees_mod_ui(mod))
self.assertEqual(mod.get("/room-scan").status_code, 200)
self.assertEqual(mod.get("/mod/admin").status_code, 404) # a mod is not the owner
thief = self.device("30.0.0.3")
self.assertEqual(thief.post("/mod/join", data={"t": token}).status_code, 404)
self.assertEqual(thief.get(f"/mod/join?t={token}").status_code, 404)
self.assertFalse(self.sees_mod_ui(thief))
def test_removed_device_loses_access_immediately(self):
owner = self.owner()
mod = self.device("20.0.0.2")
mod.post("/mod/join", data={"t": self.invite(owner)})
self.assertTrue(self.sees_mod_ui(mod))
self.assertIn("Nice Face", owner.get("/mod/admin").text)
sid = next(s["sid"] for s in A._mod_state()["sessions"].values() if s["name"] == "Nice Face")
owner.post("/mod/admin/action", data={"do": "revoke_session", "sid": sid})
self.assertFalse(self.sees_mod_ui(mod))
self.assertEqual(mod.get("/room-scan").status_code, 404)
self.assertTrue(self.sees_mod_ui(owner))
def test_cross_site_post_is_rejected(self):
owner = self.owner()
r = owner.post("/mod/admin/action", data={"do": "invite", "name": "evil"},
headers={"Origin": "https://evil.example"})
self.assertEqual(r.status_code, 404)
self.assertFalse(A._mod_state()["invites"])
def test_expired_invite_and_idle_session_stop_working(self):
owner = self.owner()
token = self.invite(owner)
A._mod_state()["invites"][A._mod_hash(token)]["expires_at"] = time.time() - 1
self.assertEqual(self.device().post("/mod/join", data={"t": token}).status_code, 404)
mod = self.device("20.0.0.2")
mod.post("/mod/join", data={"t": self.invite(owner, name="Idle Mod")})
rec = next(s for s in A._mod_state()["sessions"].values() if s["name"] == "Idle Mod")
rec["last_seen"] = time.time() - (A.SM_MOD_SESSION_IDLE_DAYS + 1) * 86400
self.assertFalse(self.sees_mod_ui(mod))
self.assertTrue(rec.get("revoked_at"))
def test_store_never_contains_raw_tokens(self):
owner = self.owner()
token = self.invite(owner)
mod = self.device("20.0.0.2")
mod.post("/mod/join", data={"t": token})
A._mod_save(force=True)
raw = A._MOD_STORE_PATH.read_text(encoding="utf-8")
self.assertNotIn(token, raw)
self.assertNotIn(mod.cookies.get(A.SM_MOD_SESSION_COOKIE), raw)
self.assertNotIn(owner.cookies.get(A.SM_MOD_SESSION_COOKIE), raw)
self.assertNotIn(OWNER_KEY, raw)
def test_new_network_for_same_device_is_logged(self):
owner = self.owner()
mod = self.device("20.0.0.2")
mod.post("/mod/join", data={"t": self.invite(owner)})
stolen = TestClient(A.app, headers={"X-Forwarded-For": "99.1.2.3", "User-Agent": UA},
cookies={A.SM_MOD_SESSION_COOKIE: mod.cookies.get(A.SM_MOD_SESSION_COOKIE)})
rec = next(s for s in A._mod_state()["sessions"].values() if s["name"] == "Nice Face")
rec["last_seen"] = time.time() - 600 # outside the 5-min touch throttle
stolen.get("/")
self.assertTrue(any(e["kind"] == "new_network" for e in A._mod_state()["events"]))
self.assertIn("2 networks", owner.get("/mod/admin").text)
def test_leaderboard_proxy_forwards_shared_key_only_for_mods(self):
sent = []
class FakeSession:
def get(self, url, cookies=None, timeout=None):
sent.append(dict(cookies or {}))
return SimpleNamespace(raise_for_status=lambda: None, json=lambda: {"rows": []})
saved = (A.SM_LEADERBOARD_PROXY_URL, A._sm_lb_proxy_healthy, A._SM_LB_PROXY_SESSION)
try:
A.SM_LEADERBOARD_PROXY_URL = "http://prices.example"
A._sm_lb_proxy_healthy = lambda: True
A._SM_LB_PROXY_SESSION = FakeSession()
def req(cookies):
return SimpleNamespace(cookies=cookies, url=SimpleNamespace(query="limit=1"),
headers={"user-agent": UA}, client=None)
A._sm_lb_proxy_fetch(req({"sm_mod": LEGACY_KEY}), "/leaderboard")
owner = self.owner()
A._sm_lb_proxy_fetch(req({A.SM_MOD_SESSION_COOKIE: owner.cookies.get(A.SM_MOD_SESSION_COOKIE)}), "/leaderboard")
finally:
A.SM_LEADERBOARD_PROXY_URL, A._sm_lb_proxy_healthy, A._SM_LB_PROXY_SESSION = saved
self.assertEqual(sent[0], {}) # old key cookie from a browser: not forwarded
self.assertEqual(sent[1], {"sm_mod": LEGACY_KEY}) # real mod: server adds the key itself
if __name__ == "__main__":
unittest.main()