- P0: 实例访问控制全线接入权限组 (829测试) - P1: permissions字段激活 + 操作级权限检查 (843测试) - 前端: 删除数据权限页面, 权限组合并为唯一入口 - 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析 - 新增: MongoDB管理页面 + Dashboard增强 - 修复: Pydantic V2迁移 + 前端TS错误修复
338 lines
10 KiB
Python
338 lines
10 KiB
Python
"""脱敏规则 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],
|
|
}
|