- P0: 实例访问控制全线接入权限组 (829测试) - P1: permissions字段激活 + 操作级权限检查 (843测试) - 前端: 删除数据权限页面, 权限组合并为唯一入口 - 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析 - 新增: MongoDB管理页面 + Dashboard增强 - 修复: Pydantic V2迁移 + 前端TS错误修复
276 lines
9.4 KiB
Python
276 lines
9.4 KiB
Python
"""Redis management API endpoints.
|
||
|
||
集成 RedisCommandValidator 进行只读命令安全验证:
|
||
- developer 角色仅允许执行只读命令
|
||
- admin/dba 角色可执行所有命令
|
||
- FLUSHDB 等高危操作仅限 admin
|
||
"""
|
||
from typing import Optional
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from pydantic import BaseModel
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.core.database import get_db
|
||
from app.core.security import get_current_user, require_role
|
||
from app.models.user import User
|
||
from app.models.instance import Instance
|
||
from app.services import redis_connector
|
||
from app.services.redis_validator import RedisCommandValidator
|
||
|
||
router = APIRouter(tags=["Redis管理"])
|
||
|
||
|
||
async def _get_redis_instance(instance_id: int, db: AsyncSession) -> Instance:
|
||
"""Fetch and validate a Redis instance."""
|
||
result = await db.execute(select(Instance).where(Instance.id == instance_id))
|
||
instance = result.scalar_one_or_none()
|
||
if not instance:
|
||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||
if instance.db_type != "redis":
|
||
raise HTTPException(status_code=400, detail="该实例不是 Redis 类型")
|
||
return instance
|
||
|
||
|
||
# ── Request schemas ──────────────────────────────────────────────
|
||
|
||
class RedisKeyCreate(BaseModel):
|
||
db: int = 0
|
||
key: str
|
||
value: object
|
||
type: str = "string"
|
||
ttl: Optional[int] = None
|
||
|
||
|
||
class RedisCommandRequest(BaseModel):
|
||
db: int = 0
|
||
command: str
|
||
|
||
|
||
class RedisExpireRequest(BaseModel):
|
||
db: int = 0
|
||
seconds: int
|
||
|
||
|
||
# ── Endpoints ────────────────────────────────────────────────────
|
||
|
||
@router.get("/redis/{instance_id}/info")
|
||
async def get_redis_info(
|
||
instance_id: int,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""Redis 信息概览."""
|
||
instance = await _get_redis_instance(instance_id, db)
|
||
try:
|
||
info = await redis_connector.RedisConnector.get_info(instance)
|
||
return info
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail="获取 Redis 信息失败: " + str(e))
|
||
|
||
|
||
@router.get("/redis/{instance_id}/dbsize")
|
||
async def get_redis_dbsize(
|
||
instance_id: int,
|
||
db_num: int = Query(0, alias="db"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""DBSIZE."""
|
||
instance = await _get_redis_instance(instance_id, db)
|
||
try:
|
||
size = await redis_connector.RedisConnector.get_dbsize(instance, db_num)
|
||
return {"db": db_num, "size": size}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail="获取 DBSIZE 失败: " + str(e))
|
||
|
||
|
||
@router.get("/redis/{instance_id}/keys")
|
||
async def get_redis_keys(
|
||
instance_id: int,
|
||
db_num: int = Query(0, alias="db"),
|
||
pattern: str = Query("*"),
|
||
type: Optional[str] = Query(None, alias="type"),
|
||
cursor: int = Query(0),
|
||
count: int = Query(50),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""键列表."""
|
||
instance = await _get_redis_instance(instance_id, db)
|
||
try:
|
||
next_cursor, keys = await redis_connector.RedisConnector.scan_keys(
|
||
instance, db=db_num, pattern=pattern,
|
||
type_filter=type, cursor=cursor, count=count,
|
||
)
|
||
return {"cursor": next_cursor, "keys": keys}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail="获取键列表失败: " + str(e))
|
||
|
||
|
||
@router.get("/redis/{instance_id}/keys/{key:path}")
|
||
async def get_redis_key_detail(
|
||
instance_id: int,
|
||
key: str,
|
||
db_num: int = Query(0, alias="db"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""键详情."""
|
||
instance = await _get_redis_instance(instance_id, db)
|
||
try:
|
||
detail = await redis_connector.RedisConnector.get_key_value(
|
||
instance, db_num, key
|
||
)
|
||
return detail
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail="获取键详情失败: " + str(e))
|
||
|
||
|
||
@router.post("/redis/{instance_id}/keys")
|
||
async def create_redis_key(
|
||
instance_id: int,
|
||
data: RedisKeyCreate,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(require_role("admin", "dba")),
|
||
):
|
||
"""创建/更新键."""
|
||
instance = await _get_redis_instance(instance_id, db)
|
||
try:
|
||
result = await redis_connector.RedisConnector.set_key_value(
|
||
instance, data.db, data.key, data.value, data.type, data.ttl
|
||
)
|
||
return {"message": "操作成功", "result": result}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail="创建键失败: " + str(e))
|
||
|
||
|
||
@router.delete("/redis/{instance_id}/keys/{key:path}")
|
||
async def delete_redis_key(
|
||
instance_id: int,
|
||
key: str,
|
||
db_num: int = Query(0, alias="db"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(require_role("admin", "dba")),
|
||
):
|
||
"""删除键."""
|
||
instance = await _get_redis_instance(instance_id, db)
|
||
try:
|
||
deleted = await redis_connector.RedisConnector.del_key(instance, db_num, key)
|
||
return {"message": "删除成功", "deleted": deleted}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail="删除键失败: " + str(e))
|
||
|
||
|
||
@router.put("/redis/{instance_id}/keys/{key:path}/expire")
|
||
async def set_redis_key_expire(
|
||
instance_id: int,
|
||
key: str,
|
||
data: RedisExpireRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(require_role("admin", "dba")),
|
||
):
|
||
"""设置 TTL."""
|
||
instance = await _get_redis_instance(instance_id, db)
|
||
try:
|
||
ok = await redis_connector.RedisConnector.set_ttl(
|
||
instance, data.db, key, data.seconds
|
||
)
|
||
return {"message": "设置成功" if ok else "设置失败"}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail="设置 TTL 失败: " + str(e))
|
||
|
||
|
||
@router.delete("/redis/{instance_id}/flushdb")
|
||
async def flush_redis_db(
|
||
instance_id: int,
|
||
db_num: int = Query(0, alias="db"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(require_role("admin")),
|
||
):
|
||
"""清空DB (admin only!)."""
|
||
instance = await _get_redis_instance(instance_id, db)
|
||
try:
|
||
result = await redis_connector.RedisConnector.flush_db(instance, db_num)
|
||
return {"message": "清空成功", "result": result}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail="清空 DB 失败: " + str(e))
|
||
|
||
|
||
@router.post("/redis/{instance_id}/command")
|
||
async def execute_redis_command(
|
||
instance_id: int,
|
||
data: RedisCommandRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""执行 Redis 命令(带安全验证)。
|
||
|
||
权限控制:
|
||
- developer 角色只能执行只读命令(白名单内的 GET, HGETALL 等)
|
||
- admin/dba 角色可以执行所有命令(但 FLUSHDB/FLUSHALL 仍仅限 admin)
|
||
"""
|
||
instance = await _get_redis_instance(instance_id, db)
|
||
|
||
# 提取命令关键字用于权限判断
|
||
parts = data.command.strip().split()
|
||
if not parts:
|
||
raise HTTPException(status_code=400, detail="命令不能为空")
|
||
cmd = parts[0].upper()
|
||
|
||
# ── 权限校验 ────────────────────────────────────────────────
|
||
|
||
# FLUSHDB / FLUSHALL 仅 admin 可用
|
||
if cmd in ("FLUSHDB", "FLUSHALL"):
|
||
if current_user.role != "admin":
|
||
raise HTTPException(
|
||
status_code=403, detail=f"Redis 命令 '{cmd}' 仅限 admin 执行"
|
||
)
|
||
else:
|
||
# developer 只允许只读命令
|
||
if current_user.role == "developer":
|
||
is_safe, error_msg = RedisCommandValidator.validate(data.command)
|
||
if not is_safe:
|
||
raise HTTPException(status_code=403, detail=error_msg)
|
||
# admin/dba 允许所有命令(上面已拦截 FLUSHDB/FLUSHALL)
|
||
|
||
try:
|
||
output = await redis_connector.RedisConnector.execute_command(
|
||
instance, data.db, data.command
|
||
)
|
||
return {"output": output}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail="命令执行失败: " + str(e))
|
||
|
||
|
||
@router.get("/redis/{instance_id}/slowlog")
|
||
async def get_redis_slowlog(
|
||
instance_id: int,
|
||
count: int = Query(10),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""慢日志."""
|
||
instance = await _get_redis_instance(instance_id, db)
|
||
try:
|
||
logs = await redis_connector.RedisConnector.get_slowlog(instance, count)
|
||
return logs
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail="获取慢日志失败: " + str(e))
|
||
|
||
|
||
@router.get("/redis/{instance_id}/clients")
|
||
async def get_redis_clients(
|
||
instance_id: int,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""客户端列表."""
|
||
instance = await _get_redis_instance(instance_id, db)
|
||
try:
|
||
clients = await redis_connector.RedisConnector.get_clients(instance)
|
||
return clients
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail="获取客户端列表失败: " + str(e))
|