"""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))