实现了 Teams 机器人所需的全功能组件,包括: - 基础命令解析与帮助卡片生成 - 租户验证与访问控制 - 自定义 UA 与媒体工具 - 消息分块、批注处理与会话管理 - 防抖、缓存与配置路由能力 - 投票、配对、审计与运行时状态管理 - TTS 语音合成与卡片构建工具 - 群组管理与权限控制逻辑
123 lines
4.6 KiB
Python
123 lines
4.6 KiB
Python
"""Microsoft Teams Graph API 权限审计。
|
|
|
|
检查 Bot 所需的 Graph API 权限是否已授予,返回缺失权限列表。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import aiohttp
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
REQUIRED_GRAPH_PERMISSIONS = [
|
|
"TeamsAppInstallation.ReadWriteForUser.All",
|
|
"ChannelMessage.Read.All",
|
|
"ChannelMessage.Send",
|
|
"User.Read.All",
|
|
"Team.ReadBasic.All",
|
|
"Group.Read.All",
|
|
]
|
|
|
|
GRAPH_SP_URL = (
|
|
"https://graph.microsoft.com/v1.0/servicePrincipals"
|
|
"?$filter=appId eq '{app_id}'&$select=appRoles,oauth2PermissionScopes"
|
|
)
|
|
|
|
BOT_SP_URL = (
|
|
"https://graph.microsoft.com/v1.0/servicePrincipals"
|
|
"?$filter=appId eq '{app_id}'&$select=appRoles,oauth2PermissionScopes"
|
|
)
|
|
|
|
|
|
class GraphPermissionAuditor:
|
|
def __init__(self, token: str, app_id: str):
|
|
self._token = token
|
|
self._app_id = app_id
|
|
|
|
async def audit(self) -> dict[str, bool]:
|
|
headers = {
|
|
"Authorization": f"Bearer {self._token}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
url = GRAPH_SP_URL.format(app_id=self._app_id)
|
|
async with session.get(url, headers=headers) as resp:
|
|
if resp.status != 200:
|
|
logger.warning(f"Graph SP query failed: HTTP {resp.status}")
|
|
return {perm: False for perm in REQUIRED_GRAPH_PERMISSIONS}
|
|
|
|
data = await resp.json()
|
|
granted = self._parse_oauth2_permissions(data)
|
|
except Exception as e:
|
|
logger.error(f"Graph permission audit failed: {e}")
|
|
granted: set[str] = set()
|
|
|
|
return {perm: perm in granted for perm in REQUIRED_GRAPH_PERMISSIONS}
|
|
|
|
def _parse_oauth2_permissions(self, data: dict) -> set[str]:
|
|
values = data.get("value", [])
|
|
if not values:
|
|
return set()
|
|
sp = values[0]
|
|
scopes = sp.get("oauth2PermissionScopes", [])
|
|
return {scope.get("value", "") for scope in scopes if isinstance(scope, dict)}
|
|
|
|
def get_missing_permissions(self, audit_result: dict[str, bool]) -> list[str]:
|
|
return [perm for perm, granted in audit_result.items() if not granted]
|
|
|
|
async def extract_scopes_and_roles(self) -> dict[str, Any]:
|
|
headers = {
|
|
"Authorization": f"Bearer {self._token}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
url = GRAPH_SP_URL.format(app_id=self._app_id)
|
|
async with session.get(url, headers=headers) as resp:
|
|
if resp.status != 200:
|
|
return {"error": f"HTTP {resp.status}"}
|
|
data = await resp.json()
|
|
values = data.get("value", [])
|
|
if not values:
|
|
return {"scopes": [], "roles": []}
|
|
sp = values[0]
|
|
scopes = [
|
|
{
|
|
"value": s.get("value", ""),
|
|
"type": s.get("type", ""),
|
|
"admin_consent_display_name": s.get("adminConsentDisplayName", ""),
|
|
}
|
|
for s in (sp.get("oauth2PermissionScopes", []) or [])
|
|
if isinstance(s, dict)
|
|
]
|
|
roles = [
|
|
{
|
|
"value": r.get("value", ""),
|
|
"display_name": r.get("displayName", ""),
|
|
"description": r.get("description", ""),
|
|
}
|
|
for r in (sp.get("appRoles", []) or [])
|
|
if isinstance(r, dict)
|
|
]
|
|
return {"scopes": scopes, "roles": roles}
|
|
except Exception as e:
|
|
logger.error(f"Scopes/roles extraction failed: {e}")
|
|
return {"error": str(e)}
|
|
|
|
@staticmethod
|
|
def format_audit_display(audit_data: dict[str, Any]) -> str:
|
|
lines: list[str] = []
|
|
scopes = audit_data.get("scopes", [])
|
|
roles = audit_data.get("roles", [])
|
|
if scopes:
|
|
lines.append("**OAuth2 Permission Scopes:**")
|
|
for s in scopes:
|
|
lines.append(f"- `{s['value']}` ({s.get('type', '')})")
|
|
if roles:
|
|
lines.append("\n**App Roles:**")
|
|
for r in roles:
|
|
lines.append(f"- `{r['value']}`: {r.get('display_name', '')}")
|
|
return "\n".join(lines) if lines else "No scopes or roles found"
|