summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py70
1 files changed, 70 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index 64df7ac..81db0e9 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -56,6 +56,7 @@ from meshbay_common.handshake import (
)
from meshbay_common.adminop import (
ADMIN_CHALLENGE_TTL,
+ OP_DIR_DELETE,
OP_FILE_DELETE,
OP_INVITE_CREATE,
admin_transcript,
@@ -357,6 +358,8 @@ class WebRTCPeerSession:
self._do_file_upload(msg)
elif mtype == MNP.DIR_CREATE:
self._do_dir_create(msg)
+ elif mtype == MNP.DIR_DELETE:
+ self._do_dir_delete(msg)
elif mtype == MNP.FILE_DELETE:
self._do_file_delete(msg)
elif mtype == MNP.ADMIN_RESPONSE:
@@ -932,6 +935,70 @@ class WebRTCPeerSession:
"dir": str(target.relative_to(shared_root)),
})
+ def _do_dir_delete(self, msg: dict) -> None:
+ """
+ Remove an empty directory, for the node operator.
+
+ Creating one is not privileged — a member who can add a file may organise
+ where it goes — but removing one is: it acts on a name other members are
+ using, and on the operator's disk. Empty is the whole safety property
+ here. Nothing recursive: refusing a directory with anything in it means
+ this can never destroy content, whatever the caller intended, so the
+ operator deletes the files first and sees what they are losing.
+ """
+ ctx = self._group_ctx()
+ shared_root = ctx.get("shared_root")
+ if not shared_root:
+ self._send({"type": "error", "detail": "No shared directory"})
+ return
+
+ target = safe_subdir(shared_root, msg.get("dir") or "")
+ if target is None or target == shared_root:
+ self._send({"type": "error", "detail": "Invalid directory"})
+ return
+ if not target.is_dir():
+ self._send({"type": "error", "detail": "Not a directory"})
+ return
+ if any(target.iterdir()):
+ self._send({"type": "error", "detail": "Directory is not empty"})
+ return
+ if not self._has_admin_authority():
+ self._send({"type": "error", "detail": "No authorized key for deletion"})
+ return
+
+ self._issue_admin_challenge(
+ OP_DIR_DELETE, str(target.relative_to(shared_root)))
+
+ async def _admin_exec_dir_delete(
+ self, pending: dict, transcript: bytes, sig: bytes,
+ ) -> None:
+ rel = pending["subject"]
+ ctx = self._group_ctx()
+ shared_root = ctx.get("shared_root")
+ target = safe_subdir(shared_root, rel) if shared_root else None
+ if target is None or target == shared_root or not target.is_dir():
+ self._send({"type": "error", "detail": "Not a directory"})
+ return
+
+ # Operator only. A file has an uploader who may remove their own; a
+ # directory has none, so there is no second key to accept here.
+ if not await self._verify_admin_sig(transcript, sig):
+ self._send({"type": "error", "detail": "Signature verification failed"})
+ self._audit("admin_auth_failed", f"dir_delete:{rel}")
+ return
+
+ # Checked again after the signature: the emptiness test that let this
+ # through happened before a round trip to the operator's browser, and a
+ # file could have landed in the meantime.
+ if any(target.iterdir()):
+ self._send({"type": "error", "detail": "Directory is not empty"})
+ return
+
+ target.rmdir()
+ log.info("Directory removed by %s: %s", self._user_id[:8], rel)
+ self._audit("dir_delete", rel)
+ self._send({"type": MNP.DIR_DELETE_ACK, "v": MNP_VERSION, "dir": rel})
+
async def _do_keypair_bundle_delete(self) -> None:
"""
Withdraw our own key backup from this node.
@@ -1472,6 +1539,9 @@ class WebRTCPeerSession:
if pending["op"] == OP_FILE_DELETE:
asyncio.ensure_future(
self._admin_exec_file_delete(pending, transcript, sig_bytes))
+ elif pending["op"] == OP_DIR_DELETE:
+ asyncio.ensure_future(
+ self._admin_exec_dir_delete(pending, transcript, sig_bytes))
elif pending["op"] == OP_INVITE_CREATE:
asyncio.ensure_future(
self._admin_exec_invite_create(pending, transcript, sig_bytes))