sync: 同步 sql-platform-pro 最新代码 — 权限体系安全修复 v0.8.1
- P0: 实例访问控制全线接入权限组 (829测试) - P1: permissions字段激活 + 操作级权限检查 (843测试) - 前端: 删除数据权限页面, 权限组合并为唯一入口 - 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析 - 新增: MongoDB管理页面 + Dashboard增强 - 修复: Pydantic V2迁移 + 前端TS错误修复
This commit is contained in:
1 parent
6fcd9465cf
commit
4c9427e546
105 files changed
+23680
-471
No files matched your search
@@ -0,0 +1,337 @@
|
||||
"""脱敏规则 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],
|
||||
}
|
||||
Reference in new issue
Block a user