Files
opsxsql/backend/app/api/v1/endpoints/redis.py
T
logan 4c9427e546 sync: 同步 sql-platform-pro 最新代码 — 权限体系安全修复 v0.8.1
- P0: 实例访问控制全线接入权限组 (829测试)
- P1: permissions字段激活 + 操作级权限检查 (843测试)
- 前端: 删除数据权限页面, 权限组合并为唯一入口
- 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析
- 新增: MongoDB管理页面 + Dashboard增强
- 修复: Pydantic V2迁移 + 前端TS错误修复
2026-05-23 14:41:06 +08:00

276 lines
9.4 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.
"""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))