from __future__ import annotations import re import logging logger = logging.getLogger(__name__) _GROUP_KEY_PATTERN = re.compile(r"^spaces/.+$") def apply_inbound_access_policy( space_name: str, space_type: str, sender_name: str, is_dm: bool, account: dict, ) -> dict: """ Returns {"allowed": bool, "reason": str, "requires_pairing": bool} """ if is_dm: return _check_dm_policy(sender_name, account) else: return _check_group_policy(space_name, space_type, account) def _check_dm_policy(sender_name: str, account: dict) -> dict: dm_policy = account.get("dm_policy", "pairing") dm_enabled = account.get("dm_enabled", True) if not dm_enabled: return {"allowed": False, "reason": "dm_disabled"} if dm_policy == "open": return {"allowed": True, "reason": "open_dm"} if dm_policy == "allowlist": allow_from = account.get("dm_allow_from", []) if "*" in allow_from: return {"allowed": True, "reason": "wildcard_allow"} if sender_name in allow_from: return {"allowed": True, "reason": "in_allowlist"} normalized = _normalize_sender_for_allowlist_check(sender_name, allow_from) if normalized: return {"allowed": True, "reason": "in_allowlist"} return {"allowed": False, "reason": "not_in_allowlist"} if dm_policy == "pairing": allow_from = account.get("dm_allow_from", []) resolved = account.get("resolved") if resolved and hasattr(resolved, "dm_allow_from"): allow_from = resolved.dm_allow_from if sender_name in allow_from: return {"allowed": True, "reason": "already_paired"} normalized = _normalize_sender_for_allowlist_check(sender_name, allow_from) if normalized: return {"allowed": True, "reason": "already_paired"} return {"allowed": False, "reason": "pairing_required", "requires_pairing": True} return {"allowed": False, "reason": f"unknown_policy: {dm_policy}"} def _check_group_policy(space_name: str, space_type: str, account: dict) -> dict: group_policy = account.get("group_policy", "allowlist") groups = account.get("groups", {}) if group_policy == "disabled": return {"allowed": False, "reason": "groups_disabled"} if group_policy == "open": return {"allowed": True, "reason": "open_group"} if group_policy == "allowlist": if "*" in groups: return {"allowed": True, "reason": "wildcard_group"} if space_name in groups: entry = groups[space_name] if entry.get("enabled") is False: return {"allowed": False, "reason": "group_disabled"} return {"allowed": True, "reason": "in_group_list"} if not _GROUP_KEY_PATTERN.match(space_name): return {"allowed": False, "reason": "deprecated_group_key"} return {"allowed": False, "reason": "not_in_group_list"} return {"allowed": False, "reason": f"unknown_policy: {group_policy}"} def _normalize_sender_for_allowlist_check(sender_name: str, allow_from: list[str]) -> str | None: for entry in allow_from: if entry.startswith("googlechat:"): if entry[len("googlechat:"):] == sender_name: return sender_name if entry.startswith("users/"): if entry == sender_name: return sender_name return None def is_control_command_message(text: str) -> bool: text = text.strip() return text.startswith("/config") or text.startswith("/status") or text.startswith("/admin") def should_require_command_authorization( is_group: bool, text: str, command_authorized: bool ) -> bool: if not is_group: return False if not is_control_command_message(text): return False return not command_authorized def detect_mention_for_access(annotations: list[dict]) -> bool: if not annotations: return False for ann in annotations: if ann.get("type") == "USER_MENTION": um = ann.get("userMention", {}) if um.get("user", {}).get("name") == "users/app": return True return False def resolve_group_require_mention(space_name: str, account: dict) -> bool: groups = account.get("groups", {}) default_require = account.get("require_mention", True) if "*" in groups: entry = groups["*"] if "requireMention" in entry: return entry["requireMention"] if space_name in groups: entry = groups[space_name] if "requireMention" in entry: return entry["requireMention"] return default_require def should_bypass_mention_for_text_commands(text: str, account: dict) -> bool: if not account.get("allow_text_commands", False): return False return is_control_command_message(text)