ForcePilot/backend/package/yuxi/channel/security/identity_link.py
Kris 849d21120f feat(channel/security): 新增完整安全工具模块
新增一系列安全相关功能:
1.  新增二维码生成工具,支持自定义参数导出图片/Base64/字节流
2.  新增HTML内容安全处理工具,包括标签剥离、转义、URL校验
3.  新增日志敏感信息脱敏工具,支持配置、字典、日志记录脱敏
4.  新增密钥管理运行时,支持从环境变量/文件加载密钥
5.  新增身份链接管理,支持多渠道身份绑定与解析
6.  新增SSRF防护工具,支持域名/IP校验与固定主机
7.  新增安全权限修复工具,修复文件目录权限与配置项
8.  新增外部内容安全处理,支持LLM特殊令牌剥离与注入检测
9.  新增认证限流工具,支持多维度限流与本地回环豁免
10. 新增配对管理工具,支持安全配对码生成与校验
11. 新增白名单管理工具,支持DM/群组/来源白名单校验
2026-05-21 10:32:57 +08:00

167 lines
5.9 KiB
Python

import threading
from abc import ABC, abstractmethod
from yuxi.repositories.channel_identity_link_repo import ChannelIdentityLinkRepository
class IdentityLinkStore(ABC):
@abstractmethod
async def load_all(self) -> dict[str, list[str]]: ...
@abstractmethod
async def add(self, identity: str, channel_type: str, peer_id: str) -> None: ...
@abstractmethod
async def remove(self, channel_type: str, peer_id: str) -> None: ...
@abstractmethod
async def remove_identity(self, identity: str) -> None: ...
class PostgresIdentityLinkStore(IdentityLinkStore):
def __init__(self, repo: ChannelIdentityLinkRepository | None = None):
self._repo = repo or ChannelIdentityLinkRepository()
async def load_all(self) -> dict[str, list[str]]:
return await self._repo.find_all()
async def add(self, identity: str, channel_type: str, peer_id: str) -> None:
await self._repo.add_link(identity, channel_type, peer_id)
async def remove(self, channel_type: str, peer_id: str) -> None:
await self._repo.remove_link(channel_type, peer_id)
async def remove_identity(self, identity: str) -> None:
await self._repo.remove_identity(identity)
class InMemoryIdentityLinkStore(IdentityLinkStore):
def __init__(self):
self._data: dict[str, list[str]] = {}
async def load_all(self) -> dict[str, list[str]]:
return dict(self._data)
async def add(self, identity: str, channel_type: str, peer_id: str) -> None:
entry = f"{channel_type}:{peer_id}"
existing = self._data.setdefault(identity, [])
if entry not in existing:
existing.append(entry)
async def remove(self, channel_type: str, peer_id: str) -> None:
entry = f"{channel_type}:{peer_id}"
for identity, entries in list(self._data.items()):
if entry in entries:
entries.remove(entry)
if not entries:
del self._data[identity]
async def remove_identity(self, identity: str) -> None:
self._data.pop(identity, None)
class IdentityLinkResolver:
def __init__(self, store: IdentityLinkStore | None = None):
self._store = store or PostgresIdentityLinkStore()
self._lock = threading.Lock()
self._links: dict[str, list[str]] = {}
self._linked_id_to_identity: dict[str, str] = {}
self._resolved_cache: dict[str, str] = {}
self._loaded = False
def _rebuild_reverse_index(self) -> None:
self._linked_id_to_identity = {}
for identity, linked_ids in self._links.items():
for linked_id in linked_ids:
lid_lower = linked_id.lower()
if lid_lower not in self._linked_id_to_identity:
self._linked_id_to_identity[lid_lower] = identity
async def ensure_loaded(self) -> None:
if self._loaded:
return
with self._lock:
if self._loaded:
return
self._loaded = True
self._links = await self._store.load_all()
with self._lock:
self._rebuild_reverse_index()
self._resolved_cache.clear()
def set_links(self, links: dict[str, list[str]]) -> None:
with self._lock:
self._links = links
self._resolved_cache.clear()
self._rebuild_reverse_index()
self._loaded = True
async def add_link(self, identity: str, channel_type: str, peer_id: str) -> None:
await self._store.add(identity, channel_type, peer_id)
entry = f"{channel_type}:{peer_id}"
with self._lock:
existing = self._links.get(identity, [])
if entry not in existing:
existing.append(entry)
self._links[identity] = existing
self._resolved_cache.clear()
entry_lower = entry.lower()
if entry_lower not in self._linked_id_to_identity:
self._linked_id_to_identity[entry_lower] = identity
async def remove_link(self, channel_type: str, peer_id: str) -> None:
await self._store.remove(channel_type, peer_id)
entry = f"{channel_type}:{peer_id}"
with self._lock:
for identity, entries in list(self._links.items()):
if entry in entries:
entries.remove(entry)
if not entries:
del self._links[identity]
entry_lower = entry.lower()
self._linked_id_to_identity.pop(entry_lower, None)
self._resolved_cache.clear()
async def remove_identity(self, identity: str) -> None:
await self._store.remove_identity(identity)
with self._lock:
self._links.pop(identity, None)
self._resolved_cache.clear()
self._rebuild_reverse_index()
def resolve(self, channel: str, peer_id: str, use_cache: bool = True) -> str:
key = f"{channel}:{peer_id}"
with self._lock:
if use_cache and key in self._resolved_cache:
return self._resolved_cache[key]
if not self._linked_id_to_identity:
self._resolved_cache[key] = peer_id
return peer_id
result = self._linked_id_to_identity.get(peer_id.lower())
if result is not None:
self._resolved_cache[key] = result
return result
result = self._linked_id_to_identity.get(key.lower())
if result is not None:
self._resolved_cache[key] = result
return result
self._resolved_cache[key] = peer_id
return peer_id
def list_links(self) -> dict[str, list[str]]:
with self._lock:
return dict(self._links)
def get_resolved_peer_ids(self) -> set[str]:
with self._lock:
return set(self._resolved_cache.values())
def clear_cache(self) -> None:
with self._lock:
self._resolved_cache.clear()