Files
opsxsql/backend/app/api/v1/endpoints/masking_rule.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

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],
}