188 lines
6.2 KiB
Python
188 lines
6.2 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import logging
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
DM_POLICIES = ("pairing", "allowlist", "open", "disabled")
|
||
|
|
GROUP_POLICIES = ("open", "allowlist", "disabled")
|
||
|
|
REPLY_STYLES = ("thread", "top-level")
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class MSTeamsDmPolicy:
|
||
|
|
mode: str = "pairing"
|
||
|
|
allow_from: list[str] = field(default_factory=list)
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class MSTeamsGroupPolicy:
|
||
|
|
mode: str = "allowlist"
|
||
|
|
allow_from: list[str] = field(default_factory=list)
|
||
|
|
require_mention: bool = True
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class MSTeamsChannelPolicy:
|
||
|
|
require_mention: bool = True
|
||
|
|
reply_style: str = "thread"
|
||
|
|
tools: dict | None = None
|
||
|
|
tools_by_sender: list[dict] | None = None
|
||
|
|
|
||
|
|
|
||
|
|
class MSTeamsSecurity:
|
||
|
|
def __init__(self, config: dict | None = None):
|
||
|
|
self._config = config or {}
|
||
|
|
|
||
|
|
def _channel_cfg(self) -> dict:
|
||
|
|
return self._config.get("channels", {}).get("msteams", {}) or {}
|
||
|
|
|
||
|
|
def resolve_dm_policy(self, account: dict | None = None) -> MSTeamsDmPolicy:
|
||
|
|
channel_cfg = self._channel_cfg()
|
||
|
|
mode = channel_cfg.get("dmPolicy", "pairing")
|
||
|
|
allow_from = channel_cfg.get("allowFrom", [])
|
||
|
|
if account:
|
||
|
|
mode = account.get("dm_policy", mode)
|
||
|
|
allow_from = account.get("allow_from", allow_from)
|
||
|
|
return MSTeamsDmPolicy(mode=mode, allow_from=allow_from)
|
||
|
|
|
||
|
|
def resolve_group_policy(
|
||
|
|
self,
|
||
|
|
account: dict | None = None,
|
||
|
|
team_id: str | None = None,
|
||
|
|
channel_id: str | None = None,
|
||
|
|
) -> MSTeamsGroupPolicy:
|
||
|
|
channel_cfg = self._channel_cfg()
|
||
|
|
default_mode = channel_cfg.get("groupPolicy", "allowlist")
|
||
|
|
default_mention = channel_cfg.get("requireMention", True)
|
||
|
|
allow_from = channel_cfg.get("groupAllowFrom", [])
|
||
|
|
|
||
|
|
teams = channel_cfg.get("teams", {}) or {}
|
||
|
|
|
||
|
|
if team_id:
|
||
|
|
team_cfg = teams.get(team_id, {}) or {}
|
||
|
|
if channel_id and team_cfg:
|
||
|
|
channels = team_cfg.get("channels", {}) or {}
|
||
|
|
ch_cfg = channels.get(channel_id, {}) or {}
|
||
|
|
if ch_cfg:
|
||
|
|
return MSTeamsGroupPolicy(
|
||
|
|
mode=ch_cfg.get("policy", default_mode),
|
||
|
|
require_mention=ch_cfg.get("requireMention", team_cfg.get("requireMention", default_mention)),
|
||
|
|
)
|
||
|
|
return MSTeamsGroupPolicy(
|
||
|
|
mode=team_cfg.get("policy", default_mode),
|
||
|
|
require_mention=team_cfg.get("requireMention", default_mention),
|
||
|
|
)
|
||
|
|
|
||
|
|
wildcard = teams.get("*", {}) or {}
|
||
|
|
return MSTeamsGroupPolicy(
|
||
|
|
mode=wildcard.get("policy", default_mode),
|
||
|
|
allow_from=allow_from,
|
||
|
|
require_mention=wildcard.get("requireMention", default_mention),
|
||
|
|
)
|
||
|
|
|
||
|
|
def is_allowed_dm(self, policy: MSTeamsDmPolicy, peer_id: str) -> tuple[bool, str | None]:
|
||
|
|
if policy.mode == "disabled":
|
||
|
|
return False, "dm-disabled"
|
||
|
|
if policy.mode == "open":
|
||
|
|
return True, None
|
||
|
|
if policy.mode == "allowlist":
|
||
|
|
if _check_allowlist(policy.allow_from, peer_id):
|
||
|
|
return True, None
|
||
|
|
return False, "not-in-allowlist"
|
||
|
|
if policy.mode == "pairing":
|
||
|
|
if _check_allowlist(policy.allow_from, peer_id):
|
||
|
|
return True, "paired"
|
||
|
|
return True, "pairing-required"
|
||
|
|
return False, "unknown-policy"
|
||
|
|
|
||
|
|
def is_allowed_group(
|
||
|
|
self,
|
||
|
|
policy: MSTeamsGroupPolicy,
|
||
|
|
peer_id: str,
|
||
|
|
is_mentioned: bool = False,
|
||
|
|
) -> tuple[bool, str | None]:
|
||
|
|
if policy.mode == "disabled":
|
||
|
|
return False, "group-disabled"
|
||
|
|
if policy.require_mention and not is_mentioned:
|
||
|
|
return False, "mention-required"
|
||
|
|
if policy.mode == "open":
|
||
|
|
return True, None
|
||
|
|
if policy.mode == "allowlist":
|
||
|
|
if _check_allowlist(policy.allow_from, peer_id) or not policy.allow_from:
|
||
|
|
return True, None
|
||
|
|
return False, "not-in-group-allowlist"
|
||
|
|
return False, "unknown-policy"
|
||
|
|
|
||
|
|
def resolve_reply_style(
|
||
|
|
self,
|
||
|
|
team_id: str | None = None,
|
||
|
|
channel_id: str | None = None,
|
||
|
|
) -> str:
|
||
|
|
channel_cfg = self._channel_cfg()
|
||
|
|
default_style = channel_cfg.get("replyStyle", "thread")
|
||
|
|
|
||
|
|
if not team_id:
|
||
|
|
return default_style
|
||
|
|
|
||
|
|
teams = channel_cfg.get("teams", {}) or {}
|
||
|
|
team_cfg = teams.get(team_id, {}) or {}
|
||
|
|
|
||
|
|
if channel_id:
|
||
|
|
channels = team_cfg.get("channels", {}) or {}
|
||
|
|
ch_cfg = channels.get(channel_id, {}) or {}
|
||
|
|
if "replyStyle" in ch_cfg:
|
||
|
|
return ch_cfg["replyStyle"]
|
||
|
|
|
||
|
|
return team_cfg.get("replyStyle", default_style)
|
||
|
|
|
||
|
|
def resolve_tools_policy(
|
||
|
|
self,
|
||
|
|
team_id: str | None = None,
|
||
|
|
channel_id: str | None = None,
|
||
|
|
sender_peer_id: str | None = None,
|
||
|
|
) -> dict:
|
||
|
|
channel_cfg = self._channel_cfg()
|
||
|
|
default_tools = channel_cfg.get("tools", {}) or {}
|
||
|
|
|
||
|
|
if not team_id:
|
||
|
|
return default_tools
|
||
|
|
|
||
|
|
teams = channel_cfg.get("teams", {}) or {}
|
||
|
|
team_cfg = teams.get(team_id, {}) or {}
|
||
|
|
team_tools = team_cfg.get("tools", {}) or {}
|
||
|
|
|
||
|
|
if channel_id:
|
||
|
|
channels = team_cfg.get("channels", {}) or {}
|
||
|
|
ch_cfg = channels.get(channel_id, {}) or {}
|
||
|
|
ch_tools = ch_cfg.get("tools", {}) or {}
|
||
|
|
effective = {**default_tools, **team_tools, **ch_tools}
|
||
|
|
else:
|
||
|
|
effective = {**default_tools, **team_tools}
|
||
|
|
|
||
|
|
tools_by_sender = effective.get("toolsBySender", [])
|
||
|
|
if sender_peer_id and tools_by_sender:
|
||
|
|
for rule in tools_by_sender:
|
||
|
|
if rule.get("sender") == sender_peer_id or rule.get("sender") == "*":
|
||
|
|
return rule
|
||
|
|
|
||
|
|
return effective
|
||
|
|
|
||
|
|
|
||
|
|
def _normalize_peer(peer_id: str) -> str:
|
||
|
|
for prefix in ("msteams:", "teams:"):
|
||
|
|
if peer_id.startswith(prefix):
|
||
|
|
return peer_id[len(prefix) :]
|
||
|
|
return str(peer_id)
|
||
|
|
|
||
|
|
|
||
|
|
def _check_allowlist(allow_from: list[str], peer_id: str) -> bool:
|
||
|
|
if "*" in allow_from:
|
||
|
|
return True
|
||
|
|
normalized = _normalize_peer(str(peer_id))
|
||
|
|
for entry in allow_from:
|
||
|
|
if _normalize_peer(str(entry)) == normalized:
|
||
|
|
return True
|
||
|
|
return False
|