"""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"