mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* fix(qq): parse allowed_users with the shared fail-closed allowlist
QQChannel built its allowlist with set(config.get("allowed_users") or []).
A single OpenID, which is what an `allowed_users: $QQ_ALLOWED_USERS`
reference resolves to, became a set of its characters, so the configured
user was silently dropped. A mapping authorized every key, including one
mapped to false, and a numeric scalar raised TypeError while the channel
was being constructed.
Parse it with the shared parse_allowed_users helper that Telegram and
WeChat already use: unset, null, empty collections and a blank string
allow everyone; a scalar OpenID is one entry; entries that are not
non-blank strings (such as numeric QQ account numbers) are dropped with a
warning, and a mapping is rejected as a whole; a configured value with no
valid OpenID denies every sender and logs an error. A scalar containing commas or whitespace warns
that it stays one literal ID. Binding codes still precede the allowlist.
* test(qq): gate group messages by member OpenID in allowlist tests
Every allowlist test sent a C2C frame, and the shared event() helper puts
the sender in both user_openid and member_openid, so a group-path
regression in sender resolution would pass unnoticed. admits() now takes
a kind; group frames carry only member_openid and a group_openid that
differs from the sender. The listed-OpenID test runs for both kinds, and a
new test shows that listing the group's OpenID does not admit its members.
Document that a blank or whitespace-only allowed_users string allows every
sender, like an unset value or an empty list.
* refactor(channels): share scalar allowlist separator warnings
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
51 lines
2.1 KiB
Python
51 lines
2.1 KiB
Python
"""Shared fail-closed parsing for channel user allowlists."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Callable, Hashable
|
|
from typing import Any
|
|
|
|
|
|
def parse_allowed_users[UserId: Hashable](
|
|
allowed_users: Any,
|
|
*,
|
|
parse_user_id: Callable[[Any], UserId | None],
|
|
logger: logging.Logger,
|
|
channel_name: str,
|
|
expected_id: str,
|
|
valid_id_name: str = "user ID",
|
|
warn_on_scalar_separators: bool = False,
|
|
) -> frozenset[UserId] | None:
|
|
"""Return ``None`` for an unset/empty allowlist, or its valid user IDs.
|
|
|
|
Scalars are single entries; mappings must not authorize their keys. The
|
|
channel's coercer validates each entry. Invalid entries warn and are dropped,
|
|
but a configured value with no valid entry returns an empty set and logs an
|
|
error, so callers deny everyone instead of silently removing the restriction.
|
|
String-ID channels can opt into a scalar-separator warning without splitting
|
|
the ID; surrounding whitespace alone does not trigger it.
|
|
"""
|
|
if allowed_users is None or (isinstance(allowed_users, str) and not allowed_users.strip()):
|
|
return None
|
|
if warn_on_scalar_separators and isinstance(allowed_users, str) and ("," in allowed_users or any(char.isspace() for char in allowed_users.strip())):
|
|
logger.warning(
|
|
"[%s] allowed_users is a scalar string containing separators; treating it as one literal %s. Use a YAML list for multiple IDs",
|
|
channel_name,
|
|
valid_id_name,
|
|
)
|
|
entries = list(allowed_users) if isinstance(allowed_users, (list, tuple, set, frozenset)) else [allowed_users]
|
|
if not entries:
|
|
return None
|
|
|
|
user_ids: set[UserId] = set()
|
|
for entry in entries:
|
|
user_id = parse_user_id(entry)
|
|
if user_id is None:
|
|
logger.warning("[%s] Ignoring allowed_users entry %r: expected %s; list several IDs as a YAML list", channel_name, entry, expected_id)
|
|
else:
|
|
user_ids.add(user_id)
|
|
if not user_ids:
|
|
logger.error("[%s] allowed_users has no valid %s; denying every user until it is fixed", channel_name, valid_id_name)
|
|
return frozenset(user_ids)
|