121 lines
4.3 KiB
Python
121 lines
4.3 KiB
Python
"""Tests for the inviter allowlist gate on pending-invite reconciliation.
|
|
|
|
``_on_invite`` only auto-joins when the inviter is allow-listed, but the
|
|
reconcile pass over sync's ``rooms.invite`` runs after event dispatch and
|
|
sees every pending invite — including ones ``_on_invite`` just rejected,
|
|
and ones that arrived while the gateway was down. An unconditional join
|
|
there re-admitted rejected live invites milliseconds later and auto-joined
|
|
arbitrary federated invites on restart. ``_schedule_pending_invite_joins``
|
|
must apply the same gate, reading the inviter from the stripped invite
|
|
state.
|
|
"""
|
|
|
|
import time
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from gateway.config import PlatformConfig
|
|
|
|
|
|
def _make_adapter():
|
|
"""Create a MatrixAdapter with mocked config and a one-user allowlist."""
|
|
from plugins.platforms.matrix.adapter import MatrixAdapter
|
|
|
|
config = PlatformConfig(
|
|
enabled=True,
|
|
token="syt_test_token",
|
|
extra={
|
|
"homeserver": "https://matrix.example.org",
|
|
"user_id": "@hermes:example.org",
|
|
},
|
|
)
|
|
adapter = MatrixAdapter(config)
|
|
adapter._allowed_user_ids = {"@alice:example.org"}
|
|
adapter._join_room_by_id = AsyncMock(return_value=True)
|
|
adapter._record_dm_room = AsyncMock()
|
|
return adapter
|
|
|
|
|
|
def _member_invite_event(
|
|
state_key="@hermes:example.org",
|
|
sender="@alice:example.org",
|
|
is_direct=True,
|
|
membership="invite",
|
|
):
|
|
"""Create a stripped m.room.member event as found in invite_state."""
|
|
return {
|
|
"type": "m.room.member",
|
|
"state_key": state_key,
|
|
"sender": sender,
|
|
"content": {"membership": membership, "is_direct": is_direct},
|
|
}
|
|
|
|
|
|
def _invite_sync_data(room_id="!pending_room:example.org", invite_state=None):
|
|
"""Create a sync payload with one pending invite room."""
|
|
room = {} if invite_state is None else {"invite_state": invite_state}
|
|
return {"rooms": {"invite": {room_id: room}}, "next_batch": "s1"}
|
|
|
|
|
|
async def _drain_invite_tasks(adapter):
|
|
"""Await any tasks _schedule_invite_join spawned."""
|
|
for task in list(adapter._invite_join_tasks.values()):
|
|
await task
|
|
|
|
|
|
class TestPendingInviteAuthorization:
|
|
"""_schedule_pending_invite_joins applies _on_invite's inviter gate.
|
|
|
|
Rejection mirrors _on_invite exactly: no join is scheduled (so no
|
|
entry lands in _invite_join_tasks), nothing is recorded in m.direct,
|
|
and the pending invite is otherwise left untouched.
|
|
"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_allowed_inviter_is_joined(self):
|
|
adapter = _make_adapter()
|
|
|
|
sync_data = _invite_sync_data(invite_state={"events": [_member_invite_event()]})
|
|
adapter._schedule_pending_invite_joins(sync_data)
|
|
await _drain_invite_tasks(adapter)
|
|
|
|
adapter._join_room_by_id.assert_awaited_once_with("!pending_room:example.org")
|
|
adapter._record_dm_room.assert_awaited_once_with(
|
|
"!pending_room:example.org", "@alice:example.org"
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
"invite_state",
|
|
[
|
|
pytest.param(
|
|
{"events": [_member_invite_event(sender="@mallory:evil.example")]},
|
|
id="non-allowed-inviter",
|
|
),
|
|
pytest.param(None, id="no-invite-state"),
|
|
pytest.param({"events": []}, id="empty-events"),
|
|
pytest.param(
|
|
{"events": [_member_invite_event(state_key="@other:example.org")]},
|
|
id="member-event-for-other-user",
|
|
),
|
|
pytest.param(
|
|
{"events": [_member_invite_event(sender="")]},
|
|
id="missing-inviter",
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_unauthorized_or_unknown_inviter_is_not_joined(self, invite_state):
|
|
"""An inviter outside the allowlist, or one that cannot be read
|
|
from the stripped invite state at all, fails closed like
|
|
_on_invite: no join is scheduled and nothing is recorded."""
|
|
adapter = _make_adapter()
|
|
|
|
sync_data = _invite_sync_data(invite_state=invite_state)
|
|
adapter._schedule_pending_invite_joins(sync_data)
|
|
await _drain_invite_tasks(adapter)
|
|
|
|
adapter._join_room_by_id.assert_not_awaited()
|
|
adapter._record_dm_room.assert_not_awaited()
|
|
assert adapter._invite_join_tasks == {}
|