diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/federation.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/federation.py | 86 |
1 files changed, 41 insertions, 45 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/federation.py b/packages/meshbay-hub/src/meshbay_hub/api/federation.py index 88bb645..b6b4acf 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/federation.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/federation.py @@ -32,21 +32,15 @@ from sqlalchemy.ext.asyncio import AsyncSession from meshbay_common import MHP_VERSION from meshbay_hub import __version__ -from meshbay_hub.api.deps import get_current_user +from meshbay_hub.api.deps import require_admin from meshbay_hub.auth import _hub_id, _hub_sk_pem, hub_public_key_pem from meshbay_hub.db.engine import get_db -from meshbay_hub.db.models import Group, User -from sqlalchemy.ext.asyncio import AsyncSession +from meshbay_hub.db.models import FederatedGroup, Group, HubPeer, User log = logging.getLogger(__name__) router = APIRouter(prefix="/mhp", tags=["federation"]) -# ── In-memory peer registry ─────────────────────────────────────────────────── -# Production: move to DB table (peers: hub_id, hub_url, pk_hub_pem, trusted_since) - -_peers: dict[str, dict] = {} # hub_id → {url, pk_pem, trusted_since} - def _issue_mhp_token(target_hub_id: str) -> str: """Issue a short-lived JWT for authenticating to a peer hub.""" @@ -57,18 +51,19 @@ def _issue_mhp_token(target_hub_id: str) -> str: "aud": target_hub_id, "jti": str(uuid.uuid4()), "iat": now, - "exp": now + 300, # 5 minute window + "exp": now + 300, "mhp": MHP_VERSION, }, _hub_sk_pem, algorithm="EdDSA") -def _verify_mhp_token(token: str, expected_aud: str | None = None) -> dict: - """Verify a JWT from a peer hub.""" - # First decode without verification to get iss (sender hub_id) +async def _verify_mhp_token( + token: str, db: AsyncSession, expected_aud: str | None = None, +) -> dict: + """Verify a JWT from a peer hub using DB-stored public key.""" unverified = jwt.decode(token, options={"verify_signature": False}) sender_id = unverified.get("iss") - peer = _peers.get(sender_id) + peer = await db.get(HubPeer, sender_id) if not peer: raise PermissionError(f"Unknown hub: {sender_id!r}. Register as peer first.") @@ -77,7 +72,7 @@ def _verify_mhp_token(token: str, expected_aud: str | None = None) -> dict: options["audience"] = expected_aud decoded = jwt.decode( - token, peer["pk_pem"].encode(), + token, peer.pk_hub_pem.encode(), algorithms=["EdDSA"], options=options, ) @@ -104,12 +99,8 @@ async def export_directory( db: AsyncSession = Depends(get_db), authorization: str = Header(...), ): - """ - Export our public Mesh Directory to a peer hub. - Auth: Bearer JWT signed by peer hub's key. - """ try: - _verify_mhp_token(authorization.removeprefix("Bearer ")) + await _verify_mhp_token(authorization.removeprefix("Bearer "), db) except Exception as e: raise HTTPException(status_code=401, detail=str(e)) @@ -144,16 +135,11 @@ async def receive_directory( authorization: str = Header(...), db: AsyncSession = Depends(get_db), ): - """ - Receive a Mesh Directory update from a peer hub. - Persists groups to federated_groups table for cross-hub search. - """ try: - _verify_mhp_token(authorization.removeprefix("Bearer ")) + await _verify_mhp_token(authorization.removeprefix("Bearer "), db) except Exception as e: raise HTTPException(status_code=401, detail=str(e)) - from meshbay_hub.db.models import FederatedGroup from datetime import datetime, timezone now = datetime.now(timezone.utc) count = 0 @@ -179,24 +165,19 @@ async def receive_directory( # ── Revocation propagation ──────────────────────────────────────────────────── class RevocationPayload(BaseModel): - token: str # signed revocation JWT from originating hub - + token: str @router.post("/revoke", status_code=202) async def receive_revocation( body: RevocationPayload, authorization: str = Header(...), + db: AsyncSession = Depends(get_db), ): - """ - Receive a revocation from a peer hub. Verify and propagate to our nodes. - """ try: - _verify_mhp_token(authorization.removeprefix("Bearer ")) + await _verify_mhp_token(authorization.removeprefix("Bearer "), db) except Exception as e: raise HTTPException(status_code=401, detail=str(e)) - # The revocation token is signed by the ORIGINATING hub's key (not the relaying hub) - # For now: re-broadcast to our connected nodes from meshbay_hub.api.revocation import broadcast_revocation sent = await broadcast_revocation(body.token) log.info("Propagated revocation to %d local nodes", sent) @@ -208,30 +189,45 @@ async def receive_revocation( class PeerRegisterRequest(BaseModel): hub_id: str hub_url: str - pk_hub_pem: str # peer hub's Ed25519 public key PEM - + pk_hub_pem: str @router.post("/peers", status_code=201) async def register_peer( body: PeerRegisterRequest, - current_user: User = Depends(get_current_user), + current_user: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), ): - """Admin: register a trusted peer hub. Manual step — no auto-discovery.""" - _peers[body.hub_id] = { - "url": body.hub_url, - "pk_pem": body.pk_hub_pem, - "trusted_since": int(time.time()), - } + """Admin: register a trusted peer hub.""" + existing = await db.get(HubPeer, body.hub_id) + if existing: + existing.hub_url = body.hub_url + existing.pk_hub_pem = body.pk_hub_pem + else: + db.add(HubPeer( + hub_id=body.hub_id, + hub_url=body.hub_url, + pk_hub_pem=body.pk_hub_pem, + )) + await db.commit() log.info("Peer registered: %s (%s)", body.hub_id, body.hub_url) return {"status": "registered", "hub_id": body.hub_id} @router.get("/peers") -async def list_peers(current_user: User = Depends(get_current_user)): +async def list_peers( + current_user: User = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): """Admin: list registered peer hubs.""" + result = await db.execute(select(HubPeer)) + peers = result.scalars().all() return { "peers": [ - {"hub_id": hid, "url": p["url"], "trusted_since": p["trusted_since"]} - for hid, p in _peers.items() + { + "hub_id": p.hub_id, + "url": p.hub_url, + "trusted_since": p.trusted_since.isoformat(), + } + for p in peers ] } |