summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-15 12:32:24 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-15 12:32:24 +0200
commite0a1036e0fee15b3c6c83f3e935d50f472a2e776 (patch)
tree75d28932b3010ebe3781b4f36dffed19dae74297 /packages/meshbay-node/src
parenta0be6bfcac07a256949082e935e1be06753ab872 (diff)
downloadmeshbay-e0a1036e0fee15b3c6c83f3e935d50f472a2e776.tar.gz
fix(node): an operator who was a member first holds the authority at once
A member pinned on the node who then redeems the operator pairing code was written down as the operator but the node kept its "nobody can authorize" flag until it restarted: every admin operation — removing a folder, inviting, the apps' settings — was refused meanwhile. Only a key paired fresh set it. The join result now names `operator` for an account holding the node-wide operator row, whatever its row in the group says. The page and the CLI read that role to know what to offer, and refused before asking deletions the node would have accepted. Nothing is granted by it: every admin operation is still checked against the operators' keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py28
1 files changed, 27 insertions, 1 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py
index baa8001..3a5ec0b 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/admission.py
@@ -339,6 +339,11 @@ class AdmissionMixin:
role=invite["role"], status="active",
approved_by=invite["created_by"],
)
+ # As `_pin_and_admit` does for a key paired fresh: without it, a
+ # member who pairs as the operator is refused every admin
+ # operation until the node restarts.
+ if invite["role"] == ROLE_OPERATOR:
+ self._ctx["has_admin_authority"] = True
self._audit_join(
"join_pinned",
f"group={invite['group_id'][:8]} role={invite['role']} "
@@ -352,7 +357,7 @@ class AdmissionMixin:
return
await self._join_ok(
user_id, pk_x_raw, session_group,
- role=member["role"] if member else "",
+ role=await self._announced_role(roster, user_id, member["role"]),
recognised=True,
)
return
@@ -800,6 +805,27 @@ class AdmissionMixin:
log.info("Identity pinned (%s): user=%s role=%s", via, user_id[:8], role)
self._audit_join("join_pinned", f"role={role} via={via}")
+ @staticmethod
+ async def _announced_role(roster, user_id: str, role: str) -> str:
+ """
+ The role the join result names: `operator` for an account holding the
+ node-wide operator row, whatever its row in this group says.
+
+ Operator authority is node-wide and checked on every admin operation's
+ signature (`_verify_admin_sig`), not taken from this answer — which only
+ tells the client what to offer. An operator who was a member of the group
+ before pairing has both rows, and naming the group's one hid the
+ authority: the page and the CLI refused, before asking, deletions the node
+ would have accepted.
+ """
+ if role == ROLE_OPERATOR:
+ return role
+ node_wide = await roster.get_member("", user_id)
+ if node_wide and node_wide.get("role") == ROLE_OPERATOR \
+ and node_wide.get("status") == "active":
+ return ROLE_OPERATOR
+ return role
+
async def _join_ok(
self, user_id: str, pk_x_raw: bytes, group_id: str,
*, role: str, recognised: bool,