refactor trust management

This commit is contained in:
kc1awv
2026-01-07 13:32:13 -05:00
parent 33e6b72bf7
commit e619bc00c2
5 changed files with 205 additions and 112 deletions
+13 -12
View File
@@ -45,7 +45,7 @@ class CommandHandler:
cmd = parts[0].lower()
if cmd == "reload":
if not self.hub._is_server_op(peer_hash):
if not self.hub.trust_manager.is_server_op(peer_hash):
if self.hub.identity is not None:
self._emit_error(
outgoing,
@@ -61,7 +61,7 @@ class CommandHandler:
# Global/server-operator commands
if cmd == "stats":
if not self.hub._is_server_op(peer_hash):
if not self.hub.trust_manager.is_server_op(peer_hash):
if self.hub.identity is not None:
self._emit_error(
outgoing,
@@ -125,7 +125,7 @@ class CommandHandler:
# Check if room is private - only server operators can see private rooms
st = self.hub.room_manager._room_state_get(r)
if st and st.get("private"):
if not self.hub._is_server_op(peer_hash):
if not self.hub.trust_manager.is_server_op(peer_hash):
self._emit_notice(outgoing, link, None, f"room {r} is private")
return True
@@ -207,7 +207,7 @@ class CommandHandler:
return True
if cmd == "kline":
if not self.hub._is_server_op(peer_hash):
if not self.hub.trust_manager.is_server_op(peer_hash):
if self.hub.identity is not None:
self._emit_error(
outgoing,
@@ -230,7 +230,8 @@ class CommandHandler:
op = parts[1].strip().lower()
if op == "list":
items = sorted(h.hex() for h in self.hub._banned)
with self.hub._state_lock:
items = sorted(h.hex() for h in self.hub.trust_manager._banned)
self._emit_notice(
outgoing,
link,
@@ -261,8 +262,8 @@ class CommandHandler:
tsess = self.hub.session_manager.sessions.get(target_link)
ph = tsess.get("peer") if tsess else None
if isinstance(ph, (bytes, bytearray)):
self.hub._banned.add(bytes(ph))
self.hub._persist_banned_identities_to_config(link, None, outgoing)
self.hub.trust_manager.add_ban(bytes(ph))
self.hub.trust_manager.persist_banned_identities_to_config(link, None, outgoing)
try:
target_link.teardown()
except Exception:
@@ -285,8 +286,8 @@ class CommandHandler:
except Exception as e:
self._emit_notice(outgoing, link, None, f"bad identity hash: {e}")
return True
self.hub._banned.add(h)
self.hub._persist_banned_identities_to_config(link, None, outgoing)
self.hub.trust_manager.add_ban(h)
self.hub.trust_manager.persist_banned_identities_to_config(link, None, outgoing)
self._emit_notice(outgoing, link, None, f"kline added for {h.hex()}")
return True
@@ -297,9 +298,9 @@ class CommandHandler:
self._emit_notice(outgoing, link, None, f"bad identity hash: {e}")
return True
if h in self.hub._banned:
self.hub._banned.discard(h)
self.hub._persist_banned_identities_to_config(link, None, outgoing)
if self.hub.trust_manager.is_banned(h):
self.hub.trust_manager.remove_ban(h)
self.hub.trust_manager.persist_banned_identities_to_config(link, None, outgoing)
self._emit_notice(outgoing, link, None, f"kline removed for {h.hex()}")
else:
self._emit_notice(outgoing, link, None, f"not klined: {h.hex()}")
+1 -1
View File
@@ -238,7 +238,7 @@ class RoomManager:
"""Check if peer is a room operator."""
if peer_hash is None:
return False
if self.hub._is_server_op(peer_hash):
if self.hub.trust_manager.is_server_op(peer_hash):
return True
st = self._room_state_ensure(room)
founder = st.get("founder")
+12 -97
View File
@@ -31,6 +31,7 @@ from .rooms import RoomManager
from .router import MessageRouter, OutgoingList
from .session import SessionManager
from .stats import StatsManager
from .trust import TrustManager
from .util import expand_path
@@ -63,14 +64,13 @@ class HubService:
# Stats manager for metrics and reporting
self.stats_manager = StatsManager(self)
# Trust manager for trusted/banned identities
self.trust_manager = TrustManager(self)
self.identity: RNS.Identity | None = None
self.destination: RNS.Destination | None = None
self._trusted: set[bytes] = set()
self._banned: set[bytes] = set()
self._prune_thread: threading.Thread | None = None
self._ping_thread: threading.Thread | None = None
@@ -331,16 +331,10 @@ class HubService:
raise RuntimeError("identity_path is not set")
self.identity = self._load_identity(self.config.identity_path)
self._trusted = {
self._parse_identity_hash(h)
for h in (self.config.trusted_identities or ())
if str(h).strip()
}
self._banned = {
self._parse_identity_hash(h)
for h in (self.config.banned_identities or ())
if str(h).strip()
}
self.trust_manager.load_from_config(
self.config.trusted_identities,
self.config.banned_identities,
)
self._load_registered_rooms_from_registry()
@@ -612,8 +606,8 @@ class HubService:
with self._state_lock:
old_cfg = self.config
old_trusted = set(self._trusted)
old_banned = set(self._banned)
old_trusted = set(self.trust_manager._trusted)
old_banned = set(self.trust_manager._banned)
old_registry = dict(self.room_manager._room_registry)
# Stage config parse
@@ -661,8 +655,8 @@ class HubService:
with self._state_lock:
# Apply (all-or-nothing)
self.config = new_cfg
self._trusted = new_trusted
self._banned = new_banned
self.trust_manager._trusted = new_trusted
self.trust_manager._banned = new_banned
self.room_manager._room_registry = new_registry
# Merge registry into live per-room state (for active rooms).
@@ -713,9 +707,6 @@ class HubService:
return
self.room_manager._room_registry = registry
def _is_server_op(self, peer_hash: bytes | None) -> bool:
return self._is_trusted(peer_hash)
def _resolve_identity_hash(
self, token: str, *, room: str | None = None
) -> bytes | None:
@@ -806,82 +797,6 @@ class HubService:
return None
return expand_path(str(p))
def _persist_banned_identities_to_config(
self,
link: RNS.Link,
room: str | None,
outgoing: list[tuple[RNS.Link, bytes]] | None = None,
) -> None:
cfg_path = self._config_path_for_writes()
if not cfg_path:
self._emit_notice(
outgoing, link, room, "ban updated (not persisted; no config_path)"
)
return
try:
from tomlkit import dumps, parse, table # type: ignore
except Exception:
self._emit_notice(
outgoing,
link,
room,
"ban updated (not persisted; missing dependency tomlkit)",
)
return
try:
with self._config_write_lock:
st = None
try:
st = os.stat(cfg_path)
except Exception:
st = None
with open(cfg_path, encoding="utf-8") as f:
doc = parse(f.read())
hub = doc.get("hub")
if hub is None:
hub = table()
doc["hub"] = hub
existing = hub.get("banned_identities")
existing_list: list[str] = []
if isinstance(existing, list):
for x in existing:
if x is None:
continue
sx = str(x).strip().lower()
if sx.startswith("0x"):
sx = sx[2:]
if sx:
existing_list.append(sx)
merged = set(existing_list)
merged.update(h.hex() for h in sorted(self._banned))
hub["banned_identities"] = sorted(merged)
new_text = dumps(doc)
with open(cfg_path, "w", encoding="utf-8") as f:
f.write(new_text)
if st is not None:
try:
os.chmod(cfg_path, st.st_mode)
except Exception:
pass
except Exception as e:
self._emit_notice(
outgoing, link, room, f"ban updated (persist failed: {e})"
)
def _is_trusted(self, peer_hash: bytes | None) -> bool:
if not peer_hash:
return False
with self._state_lock:
return peer_hash in self._trusted
def _notice_to(self, link: RNS.Link, room: str | None, text: str) -> None:
if self.identity is None:
return
+3 -2
View File
@@ -90,8 +90,9 @@ class StatsManager:
memberships = room_stats["memberships"]
top_rooms = room_stats["top_rooms"]
trusted_count = len(self.hub._trusted)
banned_count = len(self.hub._banned)
trust_stats = self.hub.trust_manager.get_stats()
trusted_count = trust_stats["trusted_count"]
banned_count = trust_stats["banned_count"]
c = dict(self._counters)
lines: list[str] = []
+176
View File
@@ -0,0 +1,176 @@
"""Trust and ban management for the RRC hub."""
from __future__ import annotations
import os
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import RNS
from .service import HubService
class TrustManager:
"""
Manages trusted and banned identities for the hub.
Handles:
- Trusted identity lists (server operators)
- Banned identity lists
- Persistence of ban list to config
- Trust/ban checks
"""
def __init__(self, hub: HubService) -> None:
self.hub = hub
self.log = hub.log
self._trusted: set[bytes] = set()
self._banned: set[bytes] = set()
def load_from_config(self, trusted_list: list[str] | None, banned_list: list[str] | None) -> None:
"""Load trusted and banned identities from config lists."""
self._trusted = {
self.hub._parse_identity_hash(h)
for h in (trusted_list or ())
if str(h).strip()
}
self._banned = {
self.hub._parse_identity_hash(h)
for h in (banned_list or ())
if str(h).strip()
}
def is_trusted(self, peer_hash: bytes | None) -> bool:
"""Check if a peer identity is in the trusted list."""
if not peer_hash:
return False
with self.hub._state_lock:
return peer_hash in self._trusted
def is_server_op(self, peer_hash: bytes | None) -> bool:
"""Check if a peer is a server operator (currently same as trusted)."""
return self.is_trusted(peer_hash)
def is_banned(self, peer_hash: bytes | None) -> bool:
"""Check if a peer identity is in the banned list."""
if not peer_hash:
return False
with self.hub._state_lock:
return peer_hash in self._banned
def add_ban(self, peer_hash: bytes) -> None:
"""Add a peer identity to the banned list."""
with self.hub._state_lock:
self._banned.add(peer_hash)
def remove_ban(self, peer_hash: bytes) -> None:
"""Remove a peer identity from the banned list."""
with self.hub._state_lock:
self._banned.discard(peer_hash)
def get_stats(self) -> dict[str, int]:
"""Get statistics about trusted and banned identities."""
with self.hub._state_lock:
return {
"trusted_count": len(self._trusted),
"banned_count": len(self._banned),
}
def update_from_config(self, trusted_list: list[str] | None, banned_list: list[str] | None) -> tuple[set[bytes], set[bytes]]:
"""
Update trusted and banned lists from config.
Returns the old (trusted, banned) sets for comparison.
"""
with self.hub._state_lock:
old_trusted = set(self._trusted)
old_banned = set(self._banned)
new_trusted = {
self.hub._parse_identity_hash(h)
for h in (trusted_list or ())
if str(h).strip()
}
new_banned = {
self.hub._parse_identity_hash(h)
for h in (banned_list or ())
if str(h).strip()
}
with self.hub._state_lock:
self._trusted = new_trusted
self._banned = new_banned
return old_trusted, old_banned
def persist_banned_identities_to_config(
self,
link: RNS.Link,
room: str | None,
outgoing: list[tuple[RNS.Link, bytes]] | None = None,
) -> None:
"""Persist the current banned identities list to the config file."""
cfg_path = self.hub._config_path_for_writes()
if not cfg_path:
self.hub._emit_notice(
outgoing, link, room, "ban updated (not persisted; no config_path)"
)
return
try:
from tomlkit import dumps, parse, table # type: ignore
except Exception:
self.hub._emit_notice(
outgoing,
link,
room,
"ban updated (not persisted; missing dependency tomlkit)",
)
return
try:
with self.hub._config_write_lock:
st = None
try:
st = os.stat(cfg_path)
except Exception:
st = None
with open(cfg_path, encoding="utf-8") as f:
doc = parse(f.read())
hub = doc.get("hub")
if hub is None:
hub = table()
doc["hub"] = hub
existing = hub.get("banned_identities")
existing_list: list[str] = []
if isinstance(existing, list):
for x in existing:
if x is None:
continue
sx = str(x).strip().lower()
if sx.startswith("0x"):
sx = sx[2:]
if sx:
existing_list.append(sx)
with self.hub._state_lock:
merged = set(existing_list)
merged.update(h.hex() for h in sorted(self._banned))
hub["banned_identities"] = sorted(merged)
new_text = dumps(doc)
with open(cfg_path, "w", encoding="utf-8") as f:
f.write(new_text)
if st is not None:
try:
os.chmod(cfg_path, st.st_mode)
except Exception:
pass
except Exception as e:
self.hub._emit_notice(
outgoing, link, room, f"ban updated (persist failed: {e})"
)