aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/groups.py14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/signaling.py69
2 files changed, 56 insertions, 27 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/groups.py b/packages/meshbay-hub/src/meshbay_hub/api/groups.py
index b903fcf..72ba194 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/groups.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/groups.py
@@ -124,7 +124,19 @@ async def group_online_nodes(
# But when the hub has public groups switched off, an existing one keeps
# working only for the people already in it: no non-member gets handed a
# node to connect to. Members always have a row here, so they are unaffected.
- if group.visibility == "public" and not await hub_settings.public_groups_allowed(db):
+ #
+ # A **private** group hands its node list to members and to nobody else. It
+ # used to answer any authenticated account that knew the id — which an
+ # ex-member knows for ever — with the ids and public keys of the machines
+ # hosting it. That is the "registered hub user with no membership" of §2.1
+ # reaching an endpoint that did not check membership, and §7.4 already
+ # states the property for the public case: a non-member is handed no node.
+ #
+ # Nothing legitimate needs this before joining. An open join writes the
+ # membership row first (`POST /{id}/join`), and an invitation registers the
+ # invitee's membership when the code is created — so by the time either asks
+ # for a node, the row exists.
+ if group.visibility != "public" or not await hub_settings.public_groups_allowed(db):
if not await db.get(GroupMember, (group_id, current_user.id)):
raise HTTPException(status_code=403, detail="Not a member of this group")
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/signaling.py b/packages/meshbay-hub/src/meshbay_hub/api/signaling.py
index b4be3f2..bc03b45 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/signaling.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/signaling.py
@@ -103,32 +103,49 @@ async def webrtc_offer(
# connection to a node just because it happens to host an open group. Members
# of that group are unaffected — they match `shared` below.
node_group_ids = set(_node_groups.get(node_id, []))
- if node_group_ids:
- result = await db.execute(
- select(GroupMember.group_id).where(
- GroupMember.user_id == current_user.id,
- GroupMember.group_id.in_(node_group_ids),
- ))
- shared = [gid for (gid,) in result.all()]
- if not shared:
- has_open = None
- if await hub_settings.public_groups_allowed(db):
- has_open = (await db.execute(
- select(Group.id).where(
- Group.id.in_(node_group_ids),
- Group.join_policy == "open",
- Group.status == "active",
- ))).first()
- if not has_open:
- raise HTTPException(status_code=403, detail="Not a member of any group on this node")
- else:
- statuses = set((await db.execute(
- select(Group.status).where(Group.id.in_(shared)))).scalars().all())
- if "active" not in statuses:
- # Report the strongest state present — "revoked" is the signed,
- # node-enforced one; "suspended" is the reversible hub flag.
- state = "revoked" if "revoked" in statuses else next(iter(statuses), "suspended")
- raise HTTPException(status_code=403, detail=f"Group is {state}")
+ # A node registered for no group shares no group with anybody, which is this
+ # check's own answer — and `if node_group_ids:` used to skip the whole thing,
+ # membership, group status and the public-group gate together. Since AV1 made
+ # an empty claim mean "no groups" rather than "all of my owner's", that is
+ # the *normal* registration of a node hosting nothing: exactly the
+ # unconfigured node left running that took a group down on 2026-09-11. So the
+ # machine least able to defend itself was the one any authenticated account
+ # could make allocate a peer connection and gather ICE, which is H6 restored
+ # in the one case AV1 made common.
+ #
+ # Nothing legitimate is lost by refusing here: a browser cannot complete a
+ # handshake with such a node anyway — `group_id` is mandatory (M1) and a node
+ # holding no group key refuses outright (NS8) — so this only declines work
+ # the node would decline one step later, at its own expense.
+ if not node_group_ids:
+ raise HTTPException(status_code=403,
+ detail="Not a member of any group on this node")
+
+ result = await db.execute(
+ select(GroupMember.group_id).where(
+ GroupMember.user_id == current_user.id,
+ GroupMember.group_id.in_(node_group_ids),
+ ))
+ shared = [gid for (gid,) in result.all()]
+ if not shared:
+ has_open = None
+ if await hub_settings.public_groups_allowed(db):
+ has_open = (await db.execute(
+ select(Group.id).where(
+ Group.id.in_(node_group_ids),
+ Group.join_policy == "open",
+ Group.status == "active",
+ ))).first()
+ if not has_open:
+ raise HTTPException(status_code=403, detail="Not a member of any group on this node")
+ else:
+ statuses = set((await db.execute(
+ select(Group.status).where(Group.id.in_(shared)))).scalars().all())
+ if "active" not in statuses:
+ # Report the strongest state present — "revoked" is the signed,
+ # node-enforced one; "suspended" is the reversible hub flag.
+ state = "revoked" if "revoked" in statuses else next(iter(statuses), "suspended")
+ raise HTTPException(status_code=403, detail=f"Group is {state}")
if _pending_per_user.get(current_user.id, 0) >= MAX_PENDING_PER_USER:
raise HTTPException(status_code=429, detail="Too many pending connections")