- P0: 实例访问控制全线接入权限组 (829测试) - P1: permissions字段激活 + 操作级权限检查 (843测试) - 前端: 删除数据权限页面, 权限组合并为唯一入口 - 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析 - 新增: MongoDB管理页面 + Dashboard增强 - 修复: Pydantic V2迁移 + 前端TS错误修复
402 lines
12 KiB
Python
402 lines
12 KiB
Python
"""数据脱敏引擎 — 对查询结果进行动态脱敏处理。
|
|
|
|
支持脱敏类型:
|
|
- full: 完全遮挡 (****)
|
|
- partial: 部分遮挡 (138****8888, 李**)
|
|
- hash: SHA256 哈希前16位
|
|
- truncate: 截断显示 (abc...)
|
|
- regex: 正则表达式替换
|
|
|
|
使用方式:
|
|
rules = await load_masking_rules(instance_id, database)
|
|
masker = DataMasker(rules)
|
|
masked_rows = [masker.mask_row(table_name, row) for row in rows]
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Any, Dict, List, Optional, Callable
|
|
|
|
|
|
class MaskType(str, Enum):
|
|
"""脱敏类型枚举。"""
|
|
FULL = "full"
|
|
PARTIAL = "partial"
|
|
HASH = "hash"
|
|
TRUNCATE = "truncate"
|
|
REGEX = "regex"
|
|
|
|
|
|
@dataclass
|
|
class MaskingRule:
|
|
"""脱敏规则数据类,用于运行时脱敏引擎。
|
|
|
|
Attributes:
|
|
instance_id: 数据源实例ID
|
|
database: 数据库名称
|
|
table_name: 表名(支持通配符 * 表示所有表)
|
|
column_name: 列名(支持通配符 * 表示所有列)
|
|
mask_type: 脱敏类型
|
|
mask_config: 类型特定的配置字典
|
|
is_enabled: 是否启用
|
|
"""
|
|
instance_id: int
|
|
database: str
|
|
table_name: str
|
|
column_name: str
|
|
mask_type: MaskType
|
|
mask_config: Dict[str, Any] = field(default_factory=dict)
|
|
is_enabled: bool = True
|
|
|
|
@classmethod
|
|
def from_db_model(cls, db_rule: Any) -> "MaskingRule":
|
|
"""从数据库模型创建 MaskingRule 实例。
|
|
|
|
Args:
|
|
db_rule: SQLAlchemy MaskingRule 模型实例
|
|
|
|
Returns:
|
|
MaskingRule 数据类实例
|
|
"""
|
|
config: Dict[str, Any] = {}
|
|
if db_rule.mask_config:
|
|
try:
|
|
config = json.loads(db_rule.mask_config)
|
|
except json.JSONDecodeError:
|
|
config = {}
|
|
|
|
mask_type = MaskType(db_rule.mask_type) if db_rule.mask_type in {
|
|
m.value for m in MaskType
|
|
} else MaskType.FULL
|
|
|
|
return cls(
|
|
instance_id=db_rule.instance_id,
|
|
database=db_rule.database,
|
|
table_name=db_rule.table_name,
|
|
column_name=db_rule.column_name,
|
|
mask_type=mask_type,
|
|
mask_config=config,
|
|
is_enabled=db_rule.is_enabled,
|
|
)
|
|
|
|
|
|
class DataMasker:
|
|
"""数据脱敏引擎,对查询结果进行动态脱敏。
|
|
|
|
根据配置的脱敏规则,对行数据进行后处理脱敏。
|
|
脱敏不改变原始查询语句,仅对返回结果进行处理。
|
|
|
|
Attributes:
|
|
_rules: 脱敏规则列表
|
|
_rule_map: 规则查找映射 {(table, column): MaskingRule}
|
|
_table_wildcards: 表级通配规则 {table: [rules]}
|
|
_global_columns: 全局列规则 {column: rule}
|
|
"""
|
|
|
|
def __init__(self, rules: List[MaskingRule]) -> None:
|
|
"""初始化脱敏引擎。
|
|
|
|
Args:
|
|
rules: 脱敏规则列表
|
|
"""
|
|
self._rules: List[MaskingRule] = [r for r in rules if r.is_enabled]
|
|
self._rule_map: Dict[tuple, MaskingRule] = {}
|
|
self._table_wildcards: Dict[str, List[MaskingRule]] = {}
|
|
self._global_columns: Dict[str, MaskingRule] = {}
|
|
|
|
for rule in self._rules:
|
|
if rule.table_name == "*" and rule.column_name == "*":
|
|
# 全局通配规则 — 暂不支持
|
|
continue
|
|
elif rule.table_name == "*":
|
|
# 全局列规则:适用于所有表的指定列
|
|
self._global_columns[rule.column_name.lower()] = rule
|
|
elif rule.column_name == "*":
|
|
# 表级通配规则:适用于指定表的所有列
|
|
self._table_wildcards.setdefault(rule.table_name.lower(), []).append(rule)
|
|
else:
|
|
# 精确匹配规则
|
|
key = (rule.table_name.lower(), rule.column_name.lower())
|
|
self._rule_map[key] = rule
|
|
|
|
# ===== 公共接口 =====
|
|
|
|
def mask(self, table: str, column: str, value: Any) -> Any:
|
|
"""对单个值进行脱敏。
|
|
|
|
Args:
|
|
table: 表名
|
|
column: 列名
|
|
value: 原始值
|
|
|
|
Returns:
|
|
脱敏后的值。如果值为 None 或规则不存在,返回原值。
|
|
"""
|
|
if value is None:
|
|
return None
|
|
|
|
rule = self._find_rule(table, column)
|
|
if not rule:
|
|
return value
|
|
|
|
return self._apply_mask(rule, value)
|
|
|
|
def mask_row(self, table: str, row: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""对整行数据进行脱敏。
|
|
|
|
Args:
|
|
table: 表名
|
|
row: 行数据字典 {column: value}
|
|
|
|
Returns:
|
|
脱敏后的行数据字典
|
|
"""
|
|
if not row:
|
|
return row
|
|
|
|
result: Dict[str, Any] = {}
|
|
for col, val in row.items():
|
|
result[col] = self.mask(table, col, val)
|
|
return result
|
|
|
|
def mask_rows(self, table: str, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
"""对多行数据进行脱敏。
|
|
|
|
Args:
|
|
table: 表名
|
|
rows: 行数据列表
|
|
|
|
Returns:
|
|
脱敏后的行数据列表
|
|
"""
|
|
if not rows:
|
|
return rows
|
|
return [self.mask_row(table, row) for row in rows]
|
|
|
|
def has_rules_for_table(self, table: str) -> bool:
|
|
"""检查是否有规则适用于指定表。
|
|
|
|
Args:
|
|
table: 表名
|
|
|
|
Returns:
|
|
如果存在适用的脱敏规则返回 True
|
|
"""
|
|
table_lower = table.lower()
|
|
# 检查精确匹配
|
|
if any(k[0] == table_lower for k in self._rule_map):
|
|
return True
|
|
# 检查表级通配
|
|
if table_lower in self._table_wildcards:
|
|
return True
|
|
# 检查全局列规则
|
|
if self._global_columns:
|
|
return True
|
|
return False
|
|
|
|
# ===== 规则查找 =====
|
|
|
|
def _find_rule(self, table: str, column: str) -> Optional[MaskingRule]:
|
|
"""查找适用于指定表和列的脱敏规则。
|
|
|
|
优先级: 精确匹配 > 表级通配 > 全局列规则
|
|
|
|
Args:
|
|
table: 表名
|
|
column: 列名
|
|
|
|
Returns:
|
|
匹配的脱敏规则,未找到返回 None
|
|
"""
|
|
table_lower = table.lower()
|
|
col_lower = column.lower()
|
|
|
|
# 1. 精确匹配
|
|
key = (table_lower, col_lower)
|
|
if key in self._rule_map:
|
|
return self._rule_map[key]
|
|
|
|
# 2. 表级通配 (table.*)
|
|
if table_lower in self._table_wildcards:
|
|
for rule in self._table_wildcards[table_lower]:
|
|
if rule.column_name == "*":
|
|
return rule
|
|
|
|
# 3. 全局列规则 (*.column)
|
|
if col_lower in self._global_columns:
|
|
return self._global_columns[col_lower]
|
|
|
|
return None
|
|
|
|
# ===== 脱敏实现 =====
|
|
|
|
def _apply_mask(self, rule: MaskingRule, value: Any) -> Any:
|
|
"""根据规则类型应用对应的脱敏算法。
|
|
|
|
Args:
|
|
rule: 脱敏规则
|
|
value: 原始值
|
|
|
|
Returns:
|
|
脱敏后的值
|
|
|
|
Raises:
|
|
ValueError: 不支持的脱敏类型
|
|
"""
|
|
# 非字符串值转换为字符串处理
|
|
str_value = str(value) if value is not None else ""
|
|
|
|
mask_methods: Dict[MaskType, Callable[[str, Dict[str, Any]], str]] = {
|
|
MaskType.FULL: self._mask_full,
|
|
MaskType.PARTIAL: self._mask_partial,
|
|
MaskType.HASH: self._mask_hash,
|
|
MaskType.TRUNCATE: self._mask_truncate,
|
|
MaskType.REGEX: self._mask_regex,
|
|
}
|
|
|
|
method = mask_methods.get(rule.mask_type)
|
|
if not method:
|
|
raise ValueError(f"不支持的脱敏类型: {rule.mask_type}")
|
|
|
|
return method(str_value, rule.mask_config)
|
|
|
|
@staticmethod
|
|
def _mask_full(value: str, config: Dict[str, Any]) -> str:
|
|
"""完全遮挡 — 将值替换为固定数量的星号。
|
|
|
|
Args:
|
|
value: 原始值
|
|
config: 配置项 {length: 遮挡长度(默认4), char: 遮挡字符(默认*)}
|
|
|
|
Returns:
|
|
完全遮挡后的字符串
|
|
"""
|
|
length = config.get("length", 4)
|
|
char = config.get("char", "*")
|
|
return char * length
|
|
|
|
@staticmethod
|
|
def _mask_partial(value: str, config: Dict[str, Any]) -> str:
|
|
"""部分遮挡 — 保留前后若干字符,中间用星号替换。
|
|
|
|
Args:
|
|
value: 原始值
|
|
config: 配置项 {
|
|
keep_prefix: 保留前缀字符数(默认2),
|
|
keep_suffix: 保留后缀字符数(默认2),
|
|
mask_char: 遮挡字符(默认*)
|
|
}
|
|
|
|
Returns:
|
|
部分遮挡后的字符串
|
|
|
|
Examples:
|
|
>>> DataMasker._mask_partial("13812345678", {"keep_prefix": 3, "keep_suffix": 4})
|
|
'138****5678'
|
|
>>> DataMasker._mask_partial("张三丰", {"keep_prefix": 1, "keep_suffix": 0})
|
|
'张**'
|
|
"""
|
|
keep_prefix = config.get("keep_prefix", 2)
|
|
keep_suffix = config.get("keep_suffix", 2)
|
|
mask_char = config.get("mask_char", "*")
|
|
|
|
if len(value) <= keep_prefix + keep_suffix:
|
|
# 值太短,全部遮挡
|
|
return mask_char * len(value)
|
|
|
|
prefix = value[:keep_prefix]
|
|
suffix = value[-keep_suffix:] if keep_suffix > 0 else ""
|
|
mask_len = len(value) - keep_prefix - keep_suffix
|
|
return prefix + mask_char * mask_len + suffix
|
|
|
|
@staticmethod
|
|
def _mask_hash(value: str, config: Dict[str, Any]) -> str:
|
|
"""SHA256 哈希 — 取哈希值的前N位。
|
|
|
|
Args:
|
|
value: 原始值
|
|
config: 配置项 {length: 截取长度(默认16)}
|
|
|
|
Returns:
|
|
哈希前缀字符串
|
|
"""
|
|
length = config.get("length", 16)
|
|
hash_val = hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
return hash_val[:length]
|
|
|
|
@staticmethod
|
|
def _mask_truncate(value: str, config: Dict[str, Any]) -> str:
|
|
"""截断显示 — 只保留前N个字符,后面加省略号。
|
|
|
|
Args:
|
|
value: 原始值
|
|
config: 配置项 {length: 保留长度(默认3), ellipsis: 省略号(默认...)
|
|
|
|
Returns:
|
|
截断后的字符串
|
|
"""
|
|
length = config.get("length", 3)
|
|
ellipsis = config.get("ellipsis", "...")
|
|
|
|
if len(value) <= length:
|
|
return value
|
|
return value[:length] + ellipsis
|
|
|
|
@staticmethod
|
|
def _mask_regex(value: str, config: Dict[str, Any]) -> str:
|
|
"""正则表达式替换 — 使用正则匹配并替换。
|
|
|
|
Args:
|
|
value: 原始值
|
|
config: 配置项 {
|
|
pattern: 正则表达式,
|
|
replacement: 替换字符串(默认 ***)
|
|
}
|
|
|
|
Returns:
|
|
替换后的字符串
|
|
|
|
Raises:
|
|
ValueError: 正则表达式无效
|
|
"""
|
|
pattern = config.get("pattern", ".*")
|
|
replacement = config.get("replacement", "***")
|
|
|
|
try:
|
|
return re.sub(pattern, replacement, value)
|
|
except re.error as e:
|
|
raise ValueError(f"无效的正则表达式: {pattern}, 错误: {e}")
|
|
|
|
|
|
# ===== 便捷函数 =====
|
|
|
|
async def load_masking_rules(
|
|
db: Any, instance_id: int, database: str
|
|
) -> List[MaskingRule]:
|
|
"""从数据库加载指定实例和数据库的脱敏规则。
|
|
|
|
Args:
|
|
db: 异步数据库会话 (AsyncSession)
|
|
instance_id: 数据源实例ID
|
|
database: 数据库名称
|
|
|
|
Returns:
|
|
MaskingRule 列表
|
|
"""
|
|
from sqlalchemy import select
|
|
from app.models.masking_rule import MaskingRule as MaskingRuleModel
|
|
|
|
stmt = (
|
|
select(MaskingRuleModel)
|
|
.where(
|
|
MaskingRuleModel.instance_id == instance_id,
|
|
MaskingRuleModel.database == database,
|
|
MaskingRuleModel.is_enabled == True,
|
|
)
|
|
)
|
|
result = await db.execute(stmt)
|
|
db_rules = result.scalars().all()
|
|
return [MaskingRule.from_db_model(r) for r in db_rules]
|