summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api/federation.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/federation.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/federation.py86
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
]
}