"""脱敏规则 API 路由 — 提供脱敏规则的 CRUD 和批量操作。""" import json from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select, func, and_ 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.models.masking_rule import MaskingRule from app.schemas.masking import ( MaskingRuleCreate, MaskingRuleUpdate, MaskingRuleResponse, MaskingRuleBatchCreate, MaskingRuleListResponse, ) router = APIRouter(prefix="/instances", tags=["数据脱敏规则"]) def _serialize_rule(rule: MaskingRule) -> dict: """Serialize a MaskingRule model, parsing mask_config JSON string.""" config = json.loads(rule.mask_config) if rule.mask_config else None return { "id": rule.id, "instance_id": rule.instance_id, "database": rule.database, "table_name": rule.table_name, "column_name": rule.column_name, "mask_type": rule.mask_type, "mask_config": config, "is_enabled": rule.is_enabled, "created_at": rule.created_at, "updated_at": rule.updated_at, } async def _verify_instance( db: AsyncSession, instance_id: int, current_user: User ) -> Instance: """验证实例存在且用户有权限访问。 Args: db: 数据库会话 instance_id: 实例ID current_user: 当前用户 Returns: 实例对象 Raises: HTTPException: 实例不存在或无权访问 """ 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="数据源不存在") return instance async def _check_duplicate_rule( db: AsyncSession, instance_id: int, database: str, table_name: str, column_name: str, exclude_id: Optional[int] = None, ) -> bool: """检查是否存在重复规则。 Args: db: 数据库会话 instance_id: 实例ID database: 数据库名 table_name: 表名 column_name: 列名 exclude_id: 排除的规则ID(用于更新时) Returns: 如果存在重复规则返回 True """ stmt = select(MaskingRule).where( MaskingRule.instance_id == instance_id, MaskingRule.database == database, MaskingRule.table_name == table_name, MaskingRule.column_name == column_name, ) if exclude_id: stmt = stmt.where(MaskingRule.id != exclude_id) result = await db.execute(stmt) return result.scalar_one_or_none() is not None @router.get("/{instance_id}/masking-rules", response_model=MaskingRuleListResponse) async def list_masking_rules( instance_id: int, database: Optional[str] = Query(None, description="数据库筛选"), table_name: Optional[str] = Query(None, description="表名筛选"), keyword: Optional[str] = Query(None, description="关键词搜索"), page: int = Query(1, ge=1, description="页码"), page_size: int = Query(20, ge=1, le=100, description="每页数量"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """列出指定实例的脱敏规则。 Args: instance_id: 数据源实例ID database: 按数据库名筛选 table_name: 按表名筛选 keyword: 关键词搜索(匹配列名) page: 页码 page_size: 每页数量 db: 数据库会话 current_user: 当前用户 Returns: 脱敏规则列表 """ await _verify_instance(db, instance_id, current_user) stmt = select(MaskingRule).where(MaskingRule.instance_id == instance_id) if database: stmt = stmt.where(MaskingRule.database == database) if table_name: stmt = stmt.where(MaskingRule.table_name == table_name) if keyword: stmt = stmt.where( MaskingRule.column_name.contains(keyword) ) # 总数量 total_stmt = select(func.count()).select_from(stmt.subquery()) total = (await db.execute(total_stmt)).scalar() or 0 # 分页 stmt = ( stmt.order_by(MaskingRule.created_at.desc()) .offset((page - 1) * page_size) .limit(page_size) ) result = await db.execute(stmt) rules = result.scalars().all() return { "total": total, "items": [_serialize_rule(r) for r in rules], } @router.post( "/{instance_id}/masking-rules", response_model=MaskingRuleResponse, status_code=201, ) async def create_masking_rule( instance_id: int, data: MaskingRuleCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin", "dba")), ): """创建脱敏规则。""" await _verify_instance(db, instance_id, current_user) is_dup = await _check_duplicate_rule( db, instance_id, data.database, data.table_name, data.column_name ) if is_dup: raise HTTPException( status_code=409, detail="该实例+数据库+表+列的脱敏规则已存在", ) rule = MaskingRule( instance_id=instance_id, database=data.database, table_name=data.table_name, column_name=data.column_name, mask_type=data.mask_type, mask_config=json.dumps(data.mask_config) if data.mask_config else None, is_enabled=data.is_enabled, ) db.add(rule) await db.commit() await db.refresh(rule) return _serialize_rule(rule) @router.put("/masking-rules/{rule_id}", response_model=MaskingRuleResponse) async def update_masking_rule( rule_id: int, data: MaskingRuleUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin", "dba")), ): """更新脱敏规则。 Args: rule_id: 规则ID data: 脱敏规则更新数据 db: 数据库会话 current_user: 当前用户 Returns: 更新后的脱敏规则 Raises: HTTPException: 规则不存在或重复 """ result = await db.execute(select(MaskingRule).where(MaskingRule.id == rule_id)) rule = result.scalar_one_or_none() if not rule: raise HTTPException(status_code=404, detail="脱敏规则不存在") # 如果更新了唯一键字段,检查重复 db_val = data.database or rule.database tb_val = data.table_name or rule.table_name col_val = data.column_name or rule.column_name is_dup = await _check_duplicate_rule( db, rule.instance_id, db_val, tb_val, col_val, exclude_id=rule_id ) if is_dup: raise HTTPException( status_code=409, detail="更新后与现有规则冲突", ) if data.database is not None: rule.database = data.database if data.table_name is not None: rule.table_name = data.table_name if data.column_name is not None: rule.column_name = data.column_name if data.mask_type is not None: rule.mask_type = data.mask_type if data.mask_config is not None: rule.mask_config = json.dumps(data.mask_config) if data.is_enabled is not None: rule.is_enabled = data.is_enabled await db.commit() await db.refresh(rule) return _serialize_rule(rule) @router.delete("/masking-rules/{rule_id}", status_code=204) async def delete_masking_rule( rule_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin", "dba")), ): """删除脱敏规则。 Args: rule_id: 规则ID db: 数据库会话 current_user: 当前用户 Raises: HTTPException: 规则不存在 """ result = await db.execute(select(MaskingRule).where(MaskingRule.id == rule_id)) rule = result.scalar_one_or_none() if not rule: raise HTTPException(status_code=404, detail="脱敏规则不存在") await db.delete(rule) await db.commit() @router.post("/masking-rules/batch", response_model=MaskingRuleListResponse, status_code=201) async def batch_create_masking_rules( data: MaskingRuleBatchCreate, instance_id: int = Query(..., description="数据源实例ID"), db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin", "dba")), ): """批量创建脱敏规则。 Args: data: 批量规则数据 instance_id: 数据源实例ID db: 数据库会话 current_user: 当前用户 Returns: 成功创建的规则列表 Raises: HTTPException: 实例不存在或无权限 """ await _verify_instance(db, instance_id, current_user) created_rules: list[MaskingRule] = [] errors: list[str] = [] for i, rule_data in enumerate(data.rules): is_dup = await _check_duplicate_rule( db, instance_id, rule_data.database, rule_data.table_name, rule_data.column_name ) if is_dup: errors.append( f"第 {i + 1} 条规则重复: {rule_data.database}.{rule_data.table_name}." f"{rule_data.column_name}" ) continue rule = MaskingRule( instance_id=instance_id, database=rule_data.database, table_name=rule_data.table_name, column_name=rule_data.column_name, mask_type=rule_data.mask_type, mask_config=json.dumps(rule_data.mask_config) if rule_data.mask_config else None, is_enabled=rule_data.is_enabled, ) db.add(rule) created_rules.append(rule) if created_rules: await db.commit() for rule in created_rules: await db.refresh(rule) if errors and not created_rules: raise HTTPException(status_code=409, detail="; ".join(errors)) return { "total": len(created_rules), "items": [_serialize_rule(r) for r in created_rules], }