fix(signal): normalize composite sender_ids in is_allowed too

The base BaseChannel.is_allowed() does a literal ``sender_id in allow_from``
check, but Signal's sender_id is a pipe-joined composite of phone/UUID
parts. After splitting an allowlist entry like ``+phone|uuid`` into two
separate entries, the per-DM gate accepted it but the base gate still
denied because the composite sender string wasn't literally in the list.

Override is_allowed on SignalChannel to delegate to
_sender_matches_allowlist, which already splits both sides on ``|`` and
normalizes each part. _sender_matches_allowlist itself now also splits
allowlist entries on ``|`` so legacy composite entries keep working too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kaloyan Tenchov
2026-05-21 01:00:36 +08:00
committed by Xubin Ren
co-authored by Claude Opus 4.7
parent 632f41e418
commit b300ea495f
2 changed files with 86 additions and 6 deletions
+61
View File
@@ -493,6 +493,51 @@ class TestGroupBuffer:
# ---------------------------------------------------------------------------
class TestIsAllowed:
"""The base-channel allowlist gate is overridden to understand Signal's
pipe-joined composite sender_ids and the +/no-+ phone variants.
"""
def test_denies_when_allowlist_empty(self):
ch = _make_channel(dm_enabled=True, dm_policy="open") # open -> no entries
assert ch.is_allowed("+19995550001") is False
def test_allows_wildcard(self):
ch = _make_channel(dm_policy="allowlist", dm_allow_from=["*"])
assert ch.is_allowed("+19995550001|some-uuid") is True
def test_allows_composite_sender_against_split_allowlist(self):
"""Composite sender_id, single-id allow_from — must match either part."""
ch = _make_channel(
dm_policy="allowlist",
dm_allow_from=["+19995550001"],
)
assert ch.is_allowed("+19995550001|1872ba20-uuid") is True
def test_allows_composite_sender_against_composite_allowlist_entry(self):
"""Backward compat: pipe-joined composite allowlist entries still match."""
composite = "+19995550001|1872ba20-uuid"
ch = _make_channel(dm_policy="allowlist", dm_allow_from=[composite])
assert ch.is_allowed(composite) is True
def test_allows_when_only_uuid_part_is_listed(self):
ch = _make_channel(dm_policy="allowlist", dm_allow_from=["1872ba20-uuid"])
assert ch.is_allowed("+19995550001|1872ba20-uuid") is True
def test_denies_when_no_part_matches(self):
ch = _make_channel(dm_policy="allowlist", dm_allow_from=["+12223334444"])
assert ch.is_allowed("+19995550001|1872ba20-uuid") is False
def test_allowlist_union_includes_group_ids(self):
"""allow_from is the union of dm.allow_from and group.allow_from."""
ch = _make_channel(
group_enabled=True,
group_policy="allowlist",
group_allow_from=["group-id-base64=="],
)
assert "group-id-base64==" in ch.config.allow_from
class TestCheckInboundPolicy:
"""Direct tests for the policy gate that _handle_data_message now delegates to."""
@@ -665,6 +710,22 @@ class TestHandleDataMessageDM:
await ch._handle_receive_notification(params)
assert len(handled) == 1
@pytest.mark.asyncio
async def test_dm_allowlist_matches_pipe_joined_composite_entry(self):
"""Allowlist entries written as ``phone|uuid`` composites still work.
Some configs pre-date the per-part splitting and store the full
sender_id composite as a single allow_from entry. Keep matching it.
"""
composite = "+19995550001|1872ba20-f52a-4bad-b434-bf7f808c8b22"
ch, handled = self._make_dm_channel(policy="allowlist", allow_from=[composite])
params = _dm_envelope(
source_number="+19995550001",
source_uuid="1872ba20-f52a-4bad-b434-bf7f808c8b22",
)
await ch._handle_receive_notification(params)
assert len(handled) == 1
@pytest.mark.asyncio
async def test_dm_disabled_rejected(self):
ch = _make_channel(dm_enabled=False)