mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
421 lines
13 KiB
Python
421 lines
13 KiB
Python
"""Management Token 服务"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import ipaddress
|
||
from datetime import datetime, timezone
|
||
from typing import Any
|
||
|
||
from sqlalchemy import func
|
||
from sqlalchemy.exc import IntegrityError
|
||
from sqlalchemy.orm import Session
|
||
|
||
from src.config.settings import config
|
||
from src.core.logger import logger
|
||
from src.models.database import ManagementToken
|
||
|
||
|
||
def validate_ip_list(ips: list[str] | None) -> list[str] | None:
|
||
"""验证 IP 白名单格式
|
||
|
||
- None: 不限制 IP
|
||
- 非空列表: 只允许列表中的 IP
|
||
- 空列表: 不允许(会抛出错误)
|
||
"""
|
||
if ips is None:
|
||
return None
|
||
if len(ips) == 0:
|
||
raise ValueError("IP 白名单不能为空列表,如需取消限制请不提供此字段")
|
||
|
||
validated = []
|
||
for i, ip_str in enumerate(ips):
|
||
original = ip_str
|
||
ip_str = ip_str.strip()
|
||
if not ip_str:
|
||
raise ValueError(f"IP 白名单第 {i + 1} 项为空")
|
||
try:
|
||
if "/" in ip_str:
|
||
ipaddress.ip_network(ip_str, strict=False)
|
||
else:
|
||
ipaddress.ip_address(ip_str)
|
||
validated.append(ip_str)
|
||
except ValueError:
|
||
raise ValueError(f"无效的 IP 地址或 CIDR: {original}")
|
||
|
||
if not validated:
|
||
raise ValueError("IP 白名单不能为空,如需取消限制请不提供此字段")
|
||
|
||
return validated
|
||
|
||
|
||
def parse_expires_at(v: Any, allow_past: bool = False) -> datetime | None:
|
||
"""解析过期时间,确保时区安全
|
||
|
||
前端 datetime-local 输入返回本地时间字符串(无时区信息)。
|
||
后端要求:
|
||
- 如果是字符串且不含时区,视为 UTC
|
||
- 如果是 datetime 且无时区,视为 UTC
|
||
- 带时区的输入直接使用
|
||
- 默认要求过期时间必须在未来(allow_past=False)
|
||
|
||
Args:
|
||
v: 时间值(字符串或 datetime)
|
||
allow_past: 是否允许过去的时间(用于清除过期时间等场景)
|
||
|
||
Returns:
|
||
解析后的 datetime 或 None
|
||
"""
|
||
if v is None:
|
||
return None
|
||
if isinstance(v, str):
|
||
if not v.strip():
|
||
return None
|
||
try:
|
||
dt = datetime.fromisoformat(v.replace("Z", "+00:00"))
|
||
if dt.tzinfo is None:
|
||
dt = dt.replace(tzinfo=timezone.utc)
|
||
except ValueError as e:
|
||
raise ValueError(f"无效的时间格式: {v}") from e
|
||
elif isinstance(v, datetime):
|
||
dt = v
|
||
if dt.tzinfo is None:
|
||
dt = dt.replace(tzinfo=timezone.utc)
|
||
else:
|
||
raise ValueError(f"不支持的时间类型: {type(v)}")
|
||
|
||
if not allow_past and dt <= datetime.now(timezone.utc):
|
||
raise ValueError("过期时间必须在未来")
|
||
|
||
return dt
|
||
|
||
|
||
def token_to_dict(
|
||
token: ManagementToken,
|
||
raw_token: str | None = None,
|
||
include_user: bool = False,
|
||
) -> dict:
|
||
"""将 ManagementToken 转换为字典
|
||
|
||
Args:
|
||
token: ManagementToken 实例
|
||
raw_token: 明文 Token(仅在创建/重新生成时提供)
|
||
include_user: 是否包含用户信息(管理员视图使用)
|
||
|
||
Returns:
|
||
Token 字典表示
|
||
"""
|
||
result = {
|
||
"id": token.id,
|
||
"user_id": token.user_id,
|
||
"name": token.name,
|
||
"description": token.description,
|
||
"token_display": token.get_display_token(),
|
||
"allowed_ips": token.allowed_ips,
|
||
"expires_at": token.expires_at.isoformat() if token.expires_at else None,
|
||
"last_used_at": token.last_used_at.isoformat() if token.last_used_at else None,
|
||
"last_used_ip": token.last_used_ip,
|
||
"usage_count": token.usage_count,
|
||
"is_active": token.is_active,
|
||
"created_at": token.created_at.isoformat() if token.created_at else None,
|
||
"updated_at": token.updated_at.isoformat() if token.updated_at else None,
|
||
}
|
||
if raw_token:
|
||
result["token"] = raw_token
|
||
if include_user and token.user:
|
||
result["user"] = {
|
||
"id": token.user.id,
|
||
"email": token.user.email,
|
||
"username": token.user.username,
|
||
"role": token.user.role.value if token.user.role else None,
|
||
}
|
||
return result
|
||
|
||
|
||
class ManagementTokenService:
|
||
"""Management Token 服务类"""
|
||
|
||
@staticmethod
|
||
def create_token(
|
||
db: Session,
|
||
user_id: str,
|
||
name: str,
|
||
description: str | None = None,
|
||
allowed_ips: list[str] | None = None,
|
||
expires_at: datetime | None = None,
|
||
) -> tuple[ManagementToken, str]:
|
||
"""创建 Management Token
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
user_id: 用户 ID
|
||
name: Token 名称
|
||
description: 描述
|
||
allowed_ips: IP 白名单
|
||
expires_at: 过期时间
|
||
|
||
Returns:
|
||
(ManagementToken, 明文 Token) 元组
|
||
|
||
Raises:
|
||
ValueError: 如果名称已存在或超过数量限制
|
||
"""
|
||
# 检查用户 Token 数量限制
|
||
token_count = int(
|
||
db.query(func.count(ManagementToken.id))
|
||
.filter(ManagementToken.user_id == user_id)
|
||
.scalar()
|
||
or 0
|
||
)
|
||
max_tokens = config.management_token_max_per_user
|
||
if token_count >= max_tokens:
|
||
raise ValueError(f"已达到 Token 数量上限({max_tokens})")
|
||
|
||
# 检查名称是否已存在
|
||
existing = (
|
||
db.query(ManagementToken)
|
||
.filter(ManagementToken.user_id == user_id, ManagementToken.name == name)
|
||
.first()
|
||
)
|
||
if existing:
|
||
raise ValueError(f"已存在名为 '{name}' 的 Token")
|
||
|
||
# 生成 Token
|
||
raw_token = ManagementToken.generate_token()
|
||
|
||
# 创建记录
|
||
token = ManagementToken(
|
||
user_id=user_id,
|
||
name=name,
|
||
description=description,
|
||
allowed_ips=allowed_ips,
|
||
expires_at=expires_at,
|
||
)
|
||
token.set_token(raw_token)
|
||
|
||
db.add(token)
|
||
try:
|
||
db.commit()
|
||
except IntegrityError:
|
||
db.rollback()
|
||
# 并发创建导致唯一约束冲突
|
||
raise ValueError(f"已存在名为 '{name}' 的 Token")
|
||
db.refresh(token)
|
||
|
||
logger.info(f"创建 Management Token: {token.id} for user {user_id}")
|
||
|
||
return token, raw_token
|
||
|
||
@staticmethod
|
||
def get_token_by_id(
|
||
db: Session, token_id: str, user_id: str | None = None
|
||
) -> ManagementToken | None:
|
||
"""根据 ID 获取 Token
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
token_id: Token ID
|
||
user_id: 用户 ID(如果提供,则只查询该用户的 Token)
|
||
|
||
Returns:
|
||
ManagementToken 或 None
|
||
"""
|
||
query = db.query(ManagementToken).filter(ManagementToken.id == token_id)
|
||
if user_id:
|
||
query = query.filter(ManagementToken.user_id == user_id)
|
||
return query.first()
|
||
|
||
@staticmethod
|
||
def list_tokens(
|
||
db: Session,
|
||
user_id: str | None = None,
|
||
is_active: bool | None = None,
|
||
skip: int = 0,
|
||
limit: int = 100,
|
||
) -> tuple[list[ManagementToken], int]:
|
||
"""列出 Tokens
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
user_id: 用户 ID(如果提供,则只查询该用户的 Token)
|
||
is_active: 筛选激活状态
|
||
skip: 跳过记录数
|
||
limit: 返回记录数
|
||
|
||
Returns:
|
||
(Token 列表, 总数) 元组
|
||
"""
|
||
query = db.query(ManagementToken)
|
||
|
||
if user_id:
|
||
query = query.filter(ManagementToken.user_id == user_id)
|
||
if is_active is not None:
|
||
query = query.filter(ManagementToken.is_active == is_active)
|
||
|
||
total = int(query.with_entities(func.count(ManagementToken.id)).scalar() or 0)
|
||
tokens = query.order_by(ManagementToken.created_at.desc()).offset(skip).limit(limit).all()
|
||
|
||
return tokens, total
|
||
|
||
@staticmethod
|
||
def update_token(
|
||
db: Session,
|
||
token_id: str,
|
||
user_id: str | None = None,
|
||
name: str | None = None,
|
||
description: str | None = None,
|
||
allowed_ips: list[str] | None = None,
|
||
expires_at: datetime | None = None,
|
||
is_active: bool | None = None,
|
||
clear_description: bool = False,
|
||
clear_allowed_ips: bool = False,
|
||
clear_expires_at: bool = False,
|
||
) -> ManagementToken | None:
|
||
"""更新 Token
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
token_id: Token ID
|
||
user_id: 用户 ID(如果提供,则只更新该用户的 Token)
|
||
name: 新名称
|
||
description: 新描述
|
||
allowed_ips: 新 IP 白名单
|
||
expires_at: 新过期时间
|
||
is_active: 新激活状态
|
||
clear_description: 是否清空描述(True 时 description 被忽略)
|
||
clear_allowed_ips: 是否清空 IP 白名单(True 时 allowed_ips 被忽略)
|
||
clear_expires_at: 是否清空过期时间(True 时 expires_at 被忽略)
|
||
|
||
Returns:
|
||
更新后的 ManagementToken 或 None
|
||
|
||
Raises:
|
||
ValueError: 如果新名称已被其他 Token 使用
|
||
"""
|
||
token = ManagementTokenService.get_token_by_id(db, token_id, user_id)
|
||
if not token:
|
||
return None
|
||
|
||
# 如果更新名称,检查是否与其他 Token 冲突
|
||
if name is not None and name != token.name:
|
||
existing = (
|
||
db.query(ManagementToken)
|
||
.filter(
|
||
ManagementToken.user_id == token.user_id,
|
||
ManagementToken.name == name,
|
||
ManagementToken.id != token_id,
|
||
)
|
||
.first()
|
||
)
|
||
if existing:
|
||
raise ValueError(f"已存在名为 '{name}' 的 Token")
|
||
token.name = name
|
||
|
||
# 处理 description:支持清空
|
||
if clear_description:
|
||
token.description = None
|
||
elif description is not None:
|
||
token.description = description
|
||
|
||
# 处理 allowed_ips:支持清空
|
||
if clear_allowed_ips:
|
||
token.allowed_ips = None
|
||
elif allowed_ips is not None:
|
||
token.allowed_ips = allowed_ips if allowed_ips else None
|
||
|
||
# 处理 expires_at:支持清空
|
||
if clear_expires_at:
|
||
token.expires_at = None
|
||
elif expires_at is not None:
|
||
token.expires_at = expires_at
|
||
|
||
if is_active is not None:
|
||
token.is_active = is_active
|
||
|
||
db.commit()
|
||
db.refresh(token)
|
||
|
||
logger.info(f"更新 Management Token: {token.id}")
|
||
|
||
return token
|
||
|
||
@staticmethod
|
||
def delete_token(db: Session, token_id: str, user_id: str | None = None) -> bool:
|
||
"""删除 Token
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
token_id: Token ID
|
||
user_id: 用户 ID(如果提供,则只删除该用户的 Token)
|
||
|
||
Returns:
|
||
是否删除成功
|
||
"""
|
||
token = ManagementTokenService.get_token_by_id(db, token_id, user_id)
|
||
if not token:
|
||
return False
|
||
|
||
db.delete(token)
|
||
db.commit()
|
||
|
||
logger.info(f"删除 Management Token: {token_id}")
|
||
|
||
return True
|
||
|
||
@staticmethod
|
||
def toggle_status(
|
||
db: Session, token_id: str, user_id: str | None = None
|
||
) -> ManagementToken | None:
|
||
"""切换 Token 状态
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
token_id: Token ID
|
||
user_id: 用户 ID(如果提供,则只操作该用户的 Token)
|
||
|
||
Returns:
|
||
更新后的 ManagementToken 或 None
|
||
"""
|
||
token = ManagementTokenService.get_token_by_id(db, token_id, user_id)
|
||
if not token:
|
||
return None
|
||
|
||
token.is_active = not token.is_active
|
||
db.commit()
|
||
db.refresh(token)
|
||
|
||
logger.info(f"切换 Management Token 状态: {token.id} -> {token.is_active}")
|
||
|
||
return token
|
||
|
||
@staticmethod
|
||
def regenerate_token(
|
||
db: Session, token_id: str, user_id: str | None = None
|
||
) -> tuple[ManagementToken | None, str | None, str | None]:
|
||
"""重新生成 Token
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
token_id: Token ID
|
||
user_id: 用户 ID(如果提供,则只操作该用户的 Token)
|
||
|
||
Returns:
|
||
(ManagementToken, 新的明文 Token, 旧的 token_hash) 元组,失败返回 (None, None, None)
|
||
"""
|
||
token = ManagementTokenService.get_token_by_id(db, token_id, user_id)
|
||
if not token:
|
||
return None, None, None
|
||
|
||
# 保存旧的 token_hash 用于审计
|
||
old_token_hash = token.token_hash
|
||
|
||
# 生成新 Token
|
||
raw_token = ManagementToken.generate_token()
|
||
token.set_token(raw_token)
|
||
|
||
db.commit()
|
||
db.refresh(token)
|
||
|
||
logger.info(f"重新生成 Management Token: {token.id}")
|
||
|
||
return token, raw_token, old_token_hash
|