Files
wecom_it_smart_desk/backend/app/services/external/base.py
T

313 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 企微IT智能服务台 — 外部系统适配器抽象基类 + 统一数据模型
# =============================================================================
# 说明:
# 1. 定义所有外部系统共用的抽象接口(ABC)
# 2. 定义统一的DTO模型(TerminalInfo/SecurityStatus/VpnSession
# 3. 每个外部系统实现此接口,上层业务只依赖抽象接口
#
# 设计原则:
# - 默认返回None/空 — 子类按需覆写自己支持的方法
# - 不支持的能力不报错,返回None让调用方走降级逻辑
# - raw_data字段保留原始响应,调试用,生产环境可关闭
# =============================================================================
import logging
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Dict, List, Optional
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
# =============================================================================
# 统一数据模型(DTO
# =============================================================================
class TerminalInfo(BaseModel):
"""统一终端信息模型 — 所有Adapter返回同一结构
做什么:把联软/火绒/aTrust不同格式的终端数据映射到统一结构
为什么:上层业务代码不需要关心数据来自哪个系统
"""
# ── 来源标识 ──
source_system: str = Field(..., description="数据来源系统标识: lianruan/huorong/atrust/ehr")
# ── 基础标识 ──
terminal_id: Optional[str] = Field(None, description="终端在来源系统中的唯一ID")
computer_name: str = Field(..., description="计算机名")
# ── 网络信息 ──
ip_addresses: List[str] = Field(default_factory=list, description="IP地址列表(含VPN虚拟IP")
mac_addresses: List[str] = Field(default_factory=list, description="MAC地址列表")
# ── 系统信息 ──
os_version: Optional[str] = Field(None, description="操作系统版本")
is_online: bool = Field(False, description="是否在线")
# ── 用户映射(核心字段)──
logged_in_user: Optional[str] = Field(None, description="当前登录用户账号 — 映射核心字段")
logged_in_user_name: Optional[str] = Field(None, description="用户姓名")
department: Optional[str] = Field(None, description="所属部门")
# ── 硬件摘要 ──
hardware_summary: Optional[Dict] = Field(None, description="硬件摘要(CPU/内存/磁盘使用率等)")
# ── 时间信息 ──
last_seen: Optional[datetime] = Field(None, description="最后在线时间")
# ── 调试用 ──
raw_data: Optional[Dict] = Field(None, description="原始响应数据(调试用,生产可关闭)")
class VulnerabilityItem(BaseModel):
"""漏洞条目"""
name: str = Field(..., description="漏洞名称")
level: str = Field("info", description="严重程度: critical/high/medium/low/info")
description: Optional[str] = Field(None, description="漏洞描述")
publish_time: Optional[str] = Field(None, description="发布时间")
class SecurityStatus(BaseModel):
"""统一安全状态模型
做什么:聚合火绒的病毒/漏洞/隔离数据
为什么:坐席需要一目了然看到终端安全全貌
"""
source_system: str = Field(..., description="数据来源系统标识")
terminal_id: str = Field(..., description="终端ID")
computer_name: Optional[str] = Field(None, description="计算机名")
# ── 安全指标 ──
virus_total: int = Field(0, description="病毒事件总数")
virus_uncleaned: int = Field(0, description="未处理病毒数")
vulnerabilities: List[VulnerabilityItem] = Field(default_factory=list, description="高危漏洞列表")
high_vuln_count: int = Field(0, description="高危漏洞数量")
# ── 隔离状态 ──
is_isolated: bool = Field(False, description="是否被隔离")
isolation_source: Optional[str] = Field(None, description="隔离来源系统")
# ── 检查时间 ──
checked_at: datetime = Field(default_factory=datetime.now, description="检查时间")
class VpnSession(BaseModel):
"""VPN会话模型(仅aTrust
做什么:描述一个aTrust VPN在线会话
为什么:坐席需要知道远程员工是否通过VPN在线、VPN IP是什么
"""
source_system: str = "atrust"
session_id: Optional[str] = Field(None, description="会话ID(用于踢出操作)")
username: str = Field(..., description="用户名(登录名)")
display_name: Optional[str] = Field(None, description="显示名")
remote_ip: str = Field(..., description="接入IP(公网IP或'内网IP'")
vpn_ip: Optional[str] = Field(None, description="VPN虚拟内网IP — 火绒交叉匹配关键字段")
is_trusted: bool = Field(False, description="终端是否已授信")
os: Optional[str] = Field(None, description="接入终端操作系统")
last_login: Optional[datetime] = Field(None, description="最后登录时间")
domain: Optional[str] = Field(None, description="登录域")
# =============================================================================
# 适配器抽象基类
# =============================================================================
class ExternalSystemAdapter(ABC):
"""外部系统适配器抽象基类
做什么:定义所有外部系统共用的接口规范
为什么:让上层业务代码只依赖抽象接口,不感知底层系统差异
设计原则:
- 默认方法返回None/空列表/False,子类按需覆写自己支持的能力
- 不支持的能力不报错,让调用方走降级逻辑
- 每个Adapter只负责一个外部系统的对接
"""
@property
@abstractmethod
def system_name(self) -> str:
"""系统标识名称
返回值: 'lianruan' / 'huorong' / 'atrust' / 'ehr' / 'mock'
"""
...
@property
@abstractmethod
def is_available(self) -> bool:
"""当前系统是否可用(凭证已配置+网络可达)
做什么:检查配置是否完整,不实际发起网络请求
为什么:调用方可据此决定是否跳过本系统
"""
...
@abstractmethod
async def health_check(self) -> bool:
"""健康检查 — 验证凭证和网络连通性
做什么:实际发起一次轻量级API调用,确认系统可达
为什么:定期健康检查可提前发现连接问题
"""
...
# =========================================================================
# 终端查询能力
# =========================================================================
async def get_terminal_by_user(self, username: str) -> Optional[TerminalInfo]:
"""通过员工账号查询终端信息(映射核心方法)
做什么:输入员工账号,返回该员工使用的终端信息
为什么:这是员工→终端映射的核心入口
各系统实现方式:
- 联软:queryDevByParams(strusername=xxx) — 精确匹配
- 火绒:_list(ip=xxx) — 需配合联软IP交叉匹配
- aTrustqueryAll(bindUserList) — 终端绑定用户
- eHR:不提供终端数据,返回None
Args:
username: 员工账号(如 'songxian'
Returns:
TerminalInfo 或 None(系统不支持或未找到)
"""
return None
async def get_terminal_by_computer(self, computer_name: str) -> Optional[TerminalInfo]:
"""通过计算机名查询终端信息
Args:
computer_name: 计算机名(如 'IT-SONGXIAN'
"""
return None
async def get_terminal_detail(self, terminal_id: str) -> Optional[TerminalInfo]:
"""查询终端详细信息(硬件/软件/网络配置)
做什么:返回比 get_terminal_by_user 更详细的信息
为什么:排查时需要硬件配置、磁盘使用率、已安装软件等
各系统实现方式:
- 联软:getDevAllInfo — 极详细(主板/CPU/内存/硬盘/网卡/显示器)
- 火绒:_info2 — 中等详细(硬件/软件/网络配置)
- aTrust/eHR:不支持
Args:
terminal_id: 终端在来源系统中的唯一ID
"""
return None
# =========================================================================
# 安全能力
# =========================================================================
async def get_security_status(self, terminal_id: str) -> Optional[SecurityStatus]:
"""获取终端安全状态(病毒/漏洞/隔离状态)
做什么:聚合安全指标,坐席一目了然
为什么:安全问题通常需要紧急处理
仅火绒支持此接口。
Args:
terminal_id: 终端ID(火绒的client_id
"""
return None
async def isolate_terminal(self, terminal_id: str, reason: str) -> bool:
"""隔离终端(断网)
做什么:调用火绒 _create(type=netctrl) 隔离终端
为什么:安全事件紧急处理,阻断威胁扩散
仅火绒支持。调用前必须二次确认+审计日志记录。
Args:
terminal_id: 终端ID
reason: 隔离原因(记入审计日志)
Returns:
True=成功, False=失败
Raises:
NotImplementedError: 本系统不支持隔离操作
"""
raise NotImplementedError(f"{self.system_name} 不支持终端隔离")
async def unisolate_terminal(self, terminal_id: str) -> bool:
"""解除终端隔离(恢复网络)
仅火绒支持。
Args:
terminal_id: 终端ID
Returns:
True=成功, False=失败
"""
raise NotImplementedError(f"{self.system_name} 不支持解除隔离")
# =========================================================================
# VPN/在线状态能力
# =========================================================================
async def get_vpn_sessions(self, username: Optional[str] = None) -> List[VpnSession]:
"""查询VPN在线会话
做什么:获取当前通过aTrust在线的VPN会话
为什么:坐席需要知道远程员工VPN状态和IP
仅aTrust支持。
Args:
username: 可选,过滤指定用户
Returns:
VPN会话列表
"""
return []
async def get_online_status(self, username: str) -> bool:
"""查询用户是否在线
做什么:检查用户终端是否当前在线
为什么:坐席需要知道用户是否可达
各系统实现方式:
- 联软:existOnlineUser
- 火绒:_list(is_online=True) + IP交叉匹配
- aTrustgetUserStatus
Args:
username: 员工账号
Returns:
True=在线, False=离线或未知
"""
return False
# =========================================================================
# 辅助方法
# =========================================================================
def _log_not_implemented(self, method_name: str) -> None:
"""记录未实现方法的调试日志
做什么:当子类未覆写某个方法时记录DEBUG级日志
为什么:开发期帮助发现调用链路问题,生产环境可关闭DEBUG
"""
logger.debug(
f"[{self.system_name}] {method_name} 未实现,"
f"将走降级逻辑"
)