summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/src/meshbay_cli/client/remove.py
blob: ed9c65a077de3ef78451d8a90024f1a926ae94af (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
"""
Deleting a file, or an empty folder, from a group (docs/MESHBAY_DESIGN.md §5.4).

The node decides, and on two grounds only: a key paired with it as operator, or
a live device of the account that uploaded the file — any of them, so a file
sent from a phone is deleted from the laptop. Being the group's owner on the hub
is neither. A folder has no uploader: only the operator removes one, and only
an empty one — the node never deletes recursively. The request is answered with
a challenge the client signs with its key on the node; the node rebuilds what
was signed itself.
"""

from meshbay_common import MNP_VERSION
from meshbay_common.adminop import OP_DIR_DELETE, OP_FILE_DELETE, admin_transcript

from ..errors import NODE, CliError
from .keys import b64, unb64
from .session import GroupSession


def may_delete(session: GroupSession, entry: dict, user_id: str) -> bool:
    """What the node will accept, known before asking: the operator, or the
    file's uploader. The node checks again; this only avoids asking in vain."""
    return session.role == "operator" or entry.get("uploader_id") == user_id


async def delete_file(session: GroupSession, file_id: str) -> None:
    """Delete one file. Raises `NodeRefusal` with the node's reason."""
    await _signed(session, {"type": "file_delete", "v": MNP_VERSION, "file_id": file_id},
                  OP_FILE_DELETE, file_id, "file_delete_ack")


async def delete_dir(session: GroupSession, path: str) -> None:
    """Remove one empty folder (root/folders), as the node's operator."""
    await _signed(session, {"type": "dir_delete", "v": MNP_VERSION, "dir": path},
                  OP_DIR_DELETE, path, "dir_delete_ack")


async def _signed(session: GroupSession, msg: dict, op: str, subject: str,
                  ack: str) -> None:
    reply = await session.ch.request(msg, "admin_challenge", ack)
    if reply["type"] == ack:
        return
    # What is signed is checked against what was asked, never taken from the
    # node: a challenge for another operation, subject, group or node is refused.
    if (reply.get("op") != op or reply.get("subject") != subject
            or reply.get("group_id") != session.group_id
            or reply.get("node_pk") != session.node_pk):
        raise CliError(NODE, "The node asked to sign something other than this deletion; "
                             "nothing was signed.")
    transcript = admin_transcript(op=op, node_pk_b64=session.node_pk,
                                  group_id=session.group_id, subject=subject,
                                  nonce=unb64(reply["nonce"]), ts=int(reply["ts"]))
    await session.ch.request({"type": "admin_response", "v": MNP_VERSION,
                              "op_id": reply["op_id"], "op": op,
                              "signature": b64(session.identity.sk_ed.sign(transcript))},
                             ack)