aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/federation.py49
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/hub.py7
-rw-r--r--packages/meshbay-hub/tests/test_federation.py41
-rw-r--r--packages/meshbay-hub/tests/test_public_groups_toggle.py13
4 files changed, 108 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/federation.py b/packages/meshbay-hub/src/meshbay_hub/api/federation.py
index 327102e..9e252c6 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/federation.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/federation.py
@@ -40,7 +40,54 @@ from meshbay_hub.db.models import FederatedGroup, Group, HubPeer, User
log = logging.getLogger(__name__)
-router = APIRouter(prefix="/mhp", tags=["federation"])
+# ── Federation is closed ──────────────────────────────────────────────────────
+#
+# **Every MHP endpoint refuses, and this flag is the only thing that decides it.**
+#
+# Not because the protocol is wrong, but because nothing has ever run it. Two
+# hubs have never completed a single authenticated request between them: until
+# 2026-09-12 `_issue_mhp_token` bound the hub's signing key at import — before
+# `load_hub_keypair` runs — so this hub signed with `None` and called itself
+# `meshbay.org` whatever it was named, while the verifier named no audience for
+# the `aud` the issuer sets, which PyJWT refuses outright (**AV14**). Both were
+# found by reading, and they were found because a test had written down, in its
+# own words, that the real issuer "cannot be used from a test" and had signed
+# its own tokens instead. What else is in here of that shape is not known, and
+# the way to know is to stand up a second hub — not to leave the door open
+# meanwhile.
+#
+# The surface being closed is worth naming: four of the six routes carry no
+# authentication of their own (the MHP token *is* the authentication), two of
+# those write — a directory push and a revocation — and every one of them is
+# reachable by anyone who can reach the hub.
+#
+# **A constant and not a setting, deliberately.** A row in `hub_settings` and a
+# switch in the admin panel would invite an operator to turn on a feature that
+# has never worked between two machines. This takes an edit and a deploy, by
+# somebody who has read this. The refusal is a stated 503 rather than a 404
+# because a peer hub deserves a reason it can act on, which is §5.6's rule for
+# the wire one level up.
+#
+# **To re-open it:** set this True, stand up a second hub, and run the exchange
+# both ways. `test_federation.py` covers the protocol and proves nothing about
+# two machines; §15.2 carries federation as not built until that has happened.
+FEDERATION_ENABLED = False
+
+
+def _federation_open() -> None:
+ """Refuse every route on this router while federation is closed.
+
+ A router dependency rather than a line in each handler: it covers the six
+ routes that exist and every one anybody adds later. A gate you have to
+ remember to write is the shape **C6** is the standing lesson about.
+ """
+ if not FEDERATION_ENABLED:
+ raise HTTPException(status_code=503,
+ detail="Federation is not enabled on this hub")
+
+
+router = APIRouter(prefix="/mhp", tags=["federation"],
+ dependencies=[Depends(_federation_open)])
# One push may not dump the world, and one peer may not fill the table.
MAX_FEDERATED_GROUPS_PER_PUSH = 500
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/hub.py b/packages/meshbay-hub/src/meshbay_hub/api/hub.py
index a48313d..5a3eb4b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/hub.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/hub.py
@@ -5,6 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from meshbay_common import MNP_VERSION, MHP_VERSION
from meshbay_hub import __version__, hub_settings
+from meshbay_hub.api import federation
from meshbay_hub.auth import hub_public_key_pem
from meshbay_hub.config import HubConfig
from meshbay_hub.db.engine import get_db, get_engine
@@ -32,6 +33,12 @@ async def hub_info(db: AsyncSession = Depends(get_db)):
# reachable before the group list loads. The hub enforces it regardless
# of what any client does with this flag.
"allow_public_groups": await hub_settings.public_groups_allowed(db),
+ # Stated, because `mhp_version` above is otherwise a claim this hub does
+ # not honour: every MHP route refuses while federation is closed (see
+ # `api/federation.py`). A peer finds out at `/mhp/info` either way,
+ # which answers 503 — this is the same answer somewhere an operator can
+ # read it without opening the source.
+ "federation": federation.FEDERATION_ENABLED,
"captcha_site_key": _cfg.captcha.site_key if _cfg and _cfg.captcha.enabled else "",
}
diff --git a/packages/meshbay-hub/tests/test_federation.py b/packages/meshbay-hub/tests/test_federation.py
index 54f86a7..17d4161 100644
--- a/packages/meshbay-hub/tests/test_federation.py
+++ b/packages/meshbay-hub/tests/test_federation.py
@@ -4,6 +4,17 @@ MHP federation — what a registered peer hub may and may not do.
A peer is trusted enough to advertise its own public groups into our directory
and to withdraw them. It is not trusted to speak for a third hub, to shadow a
local group, to revoke our users, or to replay a state-changing request.
+
+**Federation is switched off in the code** (`federation.FEDERATION_ENABLED`),
+so every route answers 503 as the hub ships. These tests open it for their own
+duration and exercise the protocol underneath, which is what will be wanted the
+day it is re-opened — and they are also the reason it is closed. They pass, and
+they passed while two hubs could not complete one authenticated request between
+them: a second implementation of a peer proves the protocol and nothing about
+two machines, which is the same sentence this repo already writes about a
+second implementation of the client.
+
+The one test that runs with the gate as it ships is the first one below.
"""
import base64
@@ -18,6 +29,36 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from meshbay_hub.api.deps import set_admin_usernames
+@pytest.fixture(autouse=True)
+def _federation_open(monkeypatch):
+ """Open the gate for the protocol tests, and only for them."""
+ from meshbay_hub.api import federation
+ monkeypatch.setattr(federation, "FEDERATION_ENABLED", True)
+
+
+async def test_every_mhp_route_is_closed_as_the_hub_ships(client, monkeypatch):
+ """The gate itself, with the flag as it ships.
+
+ Four of these six take no authentication of their own — the MHP token is
+ the authentication — and two of them write. A refusal that has to be
+ written into each handler is one somebody adds a route without; this is a
+ dependency on the router, so a route added later is closed before it is
+ written.
+ """
+ from meshbay_hub.api import federation
+ monkeypatch.setattr(federation, "FEDERATION_ENABLED", False)
+
+ for method, path in (("get", "/mhp/info"),
+ ("get", "/mhp/directory"),
+ ("post", "/mhp/directory"),
+ ("post", "/mhp/revoke"),
+ ("get", "/mhp/peers"),
+ ("post", "/mhp/peers")):
+ r = await getattr(client, method)(path, **({} if method == "get" else {"json": {}}))
+ assert r.status_code == 503, f"{method.upper()} {path}: {r.status_code}"
+ assert "not enabled" in r.text
+
+
def _auth_key(password: str, username: str) -> str:
salt = hashlib.sha256(f"meshbay:auth:v1:{username}".encode()).digest()
return base64.b64encode(
diff --git a/packages/meshbay-hub/tests/test_public_groups_toggle.py b/packages/meshbay-hub/tests/test_public_groups_toggle.py
index e52fa85..2a57429 100644
--- a/packages/meshbay-hub/tests/test_public_groups_toggle.py
+++ b/packages/meshbay-hub/tests/test_public_groups_toggle.py
@@ -228,7 +228,18 @@ def _mhp_token(hub_id):
@pytest.mark.asyncio
-async def test_disabled_empties_the_federation_export(client):
+async def test_disabled_empties_the_federation_export(client, monkeypatch):
+ """What the export advertises, for the day federation re-opens.
+
+ MHP is switched off in the code and every route answers 503
+ (`federation.FEDERATION_ENABLED`, §7.6), so this opens the gate for its own
+ duration. The subject is the public-groups switch, not federation: with
+ public groups off, the export must advertise nothing, and that has to stay
+ true whether the door is open today or not.
+ """
+ from meshbay_hub.api import federation
+ monkeypatch.setattr(federation, "FEDERATION_ENABLED", True)
+
admin = await _admin(client)
owner = await _user(client, "ivan")
await _create_public(client, owner, "exported-square")