Browse Source

代码提交

master
si@aidatagov.com 6 days ago
parent
commit
a1038a7f3c
  1. 2
      vue-fastapi-backend/module_admin/dao/metadata_config_dao.py
  2. 1
      vue-fastapi-backend/module_admin/entity/do/metadata_config_do.py
  3. 1
      vue-fastapi-backend/module_admin/entity/vo/metadata_config_vo.py
  4. 164
      vue-fastapi-backend/module_admin/service/metasecurity_service.py
  5. 48
      vue-fastapi-frontend/src/views/metadataConfig/bizPermiConfig/index.vue

2
vue-fastapi-backend/module_admin/dao/metadata_config_dao.py

@ -282,7 +282,7 @@ class MetadataConfigDao:
SecuBizPermiConfig.update_by, SecuBizPermiConfig.update_by,
SecuBizPermiConfig.update_time, SecuBizPermiConfig.update_time,
SecuBizConfig.biz_name, SecuBizConfig.biz_name,
MetadataSec.sec_level_summary SecuBizPermiConfig.sec_level_summary
) )
.join(SecuBizConfig, SecuBizPermiConfig.biz_onum == SecuBizConfig.onum, isouter=True) .join(SecuBizConfig, SecuBizPermiConfig.biz_onum == SecuBizConfig.onum, isouter=True)
.join(MetadataSec, SecuBizConfig.risk_lvl == MetadataSec.onum, isouter=True) .join(MetadataSec, SecuBizConfig.risk_lvl == MetadataSec.onum, isouter=True)

1
vue-fastapi-backend/module_admin/entity/do/metadata_config_do.py

@ -73,6 +73,7 @@ class SecuBizPermiConfig(Base):
create_time = Column(DateTime, nullable=True, comment="创建时间") create_time = Column(DateTime, nullable=True, comment="创建时间")
update_by = Column(String(20), nullable=True, comment="更新者") update_by = Column(String(20), nullable=True, comment="更新者")
update_time = Column(DateTime, nullable=True, comment="更新时间") update_time = Column(DateTime, nullable=True, comment="更新时间")
sec_level_summary = Column(String(200), comment='等级简介')
class SecuBizConfig(Base): class SecuBizConfig(Base):

1
vue-fastapi-backend/module_admin/entity/vo/metadata_config_vo.py

@ -219,6 +219,7 @@ class SecuBizPermiConfigModel(BaseModel):
create_time: Optional[datetime] = Field(default=None, description='创建时间') create_time: Optional[datetime] = Field(default=None, description='创建时间')
update_by: Optional[str] = Field(default=None, description='更新者') update_by: Optional[str] = Field(default=None, description='更新者')
update_time: Optional[datetime] = Field(default=None, description='更新时间') update_time: Optional[datetime] = Field(default=None, description='更新时间')
sec_level_summary: Optional[str] = Field(default=None, description='等级简介')
class SecuBizPermiConfigBatchModel(BaseModel): class SecuBizPermiConfigBatchModel(BaseModel):

164
vue-fastapi-backend/module_admin/service/metasecurity_service.py

@ -17,7 +17,9 @@ from sqlalchemy.exc import OperationalError
import json import json
import re import re
from decimal import Decimal from decimal import Decimal
import sqlparse
from sqlparse.sql import Identifier, IdentifierList, Function, Token
from sqlparse.tokens import Keyword, DML
class MetaSecurityService: class MetaSecurityService:
""" """
数据源安全管理模块服务层 数据源安全管理模块服务层
@ -304,7 +306,7 @@ class MetaSecurityService:
dbConnent= cls.get_db_engine(dsDataResource["type"],dsDataResource["connectionParams"]) dbConnent= cls.get_db_engine(dsDataResource["type"],dsDataResource["connectionParams"])
# await test_connection(dbConnent) # await test_connection(dbConnent)
#3获取sql中涉及的表名 #3获取sql中涉及的表名
sqlScheamAndTable =await cls.get_tables_from_sql(page_object.sqlStr) sqlScheamAndTable =await get_tables_from_sql(page_object.sqlStr)
oldStrSql=generate_pagination_sql(page_object,dsDataResource["type"]) oldStrSql=generate_pagination_sql(page_object,dsDataResource["type"])
#4.执行原始sql #4.执行原始sql
result = await cls.execute_sql(dbConnent, oldStrSql,"原始") result = await cls.execute_sql(dbConnent, oldStrSql,"原始")
@ -405,74 +407,6 @@ class MetaSecurityService:
except SQLAlchemyError as e: except SQLAlchemyError as e:
raise RuntimeError(f"{sql_type}执行 SQL 查询时发生错误: {e}") raise RuntimeError(f"{sql_type}执行 SQL 查询时发生错误: {e}")
# async def get_tables_from_sql(sql_query: str):
# """
# 解析 SQL 查询,提取所有 Schema 和 Table 名称,并确保表名包含模式名(schema.table)。
# :param sql_query: SQL 查询字符串
# :return: {'schemas': [...], 'table_names': [...]}
# :raises ServiceException: 如果 SQL 未使用 schema.table 结构,则抛出异常
# """
# # ✅ 改进正则:支持 `FROM a.o, b.x JOIN c.y`
# table_section_pattern = r"(?i)(?:FROM|JOIN|INTO|UPDATE)\s+([\w\.\s,]+)"
# table_sections = re.findall(table_section_pattern, sql_query, re.DOTALL)
# if not table_sections:
# raise ServiceException(data='', message='SQL 解析失败,未找到表名')
# # 解析多个表(用 `,` 和 `JOIN` 拆分)
# for section in table_sections:
# tables = re.split(r"\s*,\s*|\s+JOIN\s+", section, flags=re.IGNORECASE)
# for table in tables:
# table = table.strip().split()[0] # 取 `schema.table`,忽略别名
# if "." not in table:
# raise ServiceException(
# data='',
# message=f"SQL 中的表名必须携带模式名(schema.table),但发现了无模式的表:{table}"
# )
# return table_sections
async def get_tables_from_sql(sql_query: str):
"""
解析 SQL 查询提取所有 schema.table 名称支持嵌套子查询别名JOININTOUPDATE
自动排除字段引用与无模式表
"""
# 1️⃣ 清理注释与多余空白
sql_query = re.sub(r"--.*?$", "", sql_query, flags=re.MULTILINE)
sql_query = re.sub(r"/\*.*?\*/", "", sql_query, flags=re.DOTALL)
sql_query = " ".join(sql_query.split())
# 2️⃣ 匹配 FROM/JOIN/INTO/UPDATE 后面的 schema.table
pattern = re.compile(
r"""(?ix)
(?:FROM|JOIN|INTO|UPDATE)\s+ # SQL 关键字
(?!\() # 排除子查询
(?P<schema>["'`]?[A-Za-z_][\w\$]*["'`]?) # schema
\. # .
(?P<table>["'`]?[A-Za-z_][\w\$]*["'`]?) # table
\b
""",
re.VERBOSE
)
# 3️⃣ 使用 finditer,逐个安全提取匹配项
table_names = set()
for m in pattern.finditer(sql_query):
schema_raw = m.group("schema")
table_raw = m.group("table")
if not schema_raw or not table_raw:
continue
schema =unquote_ident(schema_raw)
table = unquote_ident(table_raw)
table_names.add(f"{schema}.{table}")
# 4️⃣ 检查结果
if not table_names:
raise ServiceException(data='', message="SQL 解析失败,未找到任何 schema.table 结构")
return list(table_names)
@classmethod @classmethod
async def get_columns_from_tables(cls, dbConnent, table_names, db_type: str): async def get_columns_from_tables(cls, dbConnent, table_names, db_type: str):
@ -835,3 +769,93 @@ def generate_pagination_sql(page_object: MetaSecurityApiModel, db_type: str) ->
raise ValueError(f"不支持的数据库类型: {db_type}") raise ValueError(f"不支持的数据库类型: {db_type}")
return newStrSql return newStrSql
def _extract_identifiers(token):
"""
Identifier IdentifierList 中抽取 (schema, table)
返回格式为 'schema.table'如果有 schema否则 None
"""
if isinstance(token, Identifier):
real_name = token.get_real_name() # table
parent_name = token.get_parent_name() # schema if exists
if real_name and parent_name:
return f"{parent_name}.{real_name}"
# 处理像 schema.table AS alias 这种形式
# token.get_name() 返回 alias 或 table,根据需要可扩展
return None
async def get_tables_from_sql(sql_query: str):
"""
使用 sqlparse 解析 SQL 并返回 schema.table 列表去重
支持嵌套子查询函数别名JOININTOUPDATE
只返回包含 schema 的标识符即有点号的
"""
parsed = sqlparse.parse(sql_query)
tables = set()
for stmt in parsed:
# 遍历语句的 token 树,寻找顶层的 FROM/JOIN/INTO/UPDATE 子句
for token in stmt.tokens:
# 忽略函数、子查询整体(它们会在自己的 stmt 中被处理)
# 但我们需要遍历整个树以捕获顶层的 Identifier/IdentifierList
if token.is_group:
# 递归遍历 group 内的 token
for t in token.flatten():
# 跳过在函数内部的 FROM(例如 EXTRACT(... FROM ...))
# 方法:判断最近的父级 group 是否为 Function(我们用了 flatten,故这里检查 parent types is hard)
# 简化办法:如果 token 的上层类型是 Function 的一部分,skip(handled by checking surrounding tokens)
pass
# 更好的做法:直接按 sqlparse 提供的机制遍历并查找 Identifier/IdentifierList
for token in stmt.tokens:
if token.ttype is DML and token.normalized.upper() in ("UPDATE",):
# UPDATE table_name ...
# 下一个非空白 token 往往是 Identifier
nxt = stmt.token_next(stmt.token_index(token), skip_ws=True, skip_cm=True)
if nxt:
name = _extract_identifiers(nxt[1]) if isinstance(nxt[1], (Identifier, IdentifierList)) else None
if name:
tables.add(name)
# 使用遍历获取所有 Identifier / IdentifierList 出现在 FROM 或 JOIN 后面的情况
# 这里遍历 token 序列并在遇到 FROM/JOIN/INTO/UPDATE 时提取后续 identifier
idx = 0
tokens = list(stmt.tokens)
while idx < len(tokens):
t = tokens[idx]
if t.is_whitespace:
idx += 1
continue
if t.ttype is Keyword and t.normalized.upper() in ("FROM", "JOIN", "INTO"):
# 找下一个有意义的 token(可能是 Identifier 或 IdentifierList 或 Parenthesis 表示子查询)
nxt = stmt.token_next(idx, skip_ws=True, skip_cm=True)
if nxt:
tok = nxt[1]
# 如果是 parenthesis -> 子查询,跳过
if tok.is_group and isinstance(tok, Function):
# 函数内的 FROM(如 EXTRACT(...))会被解析为 Function 的一部分 —— 跳过
pass
else:
# 处理 Identifier 或 IdentifierList
if isinstance(tok, Identifier):
name = _extract_identifiers(tok)
if name:
tables.add(name)
elif isinstance(tok, IdentifierList):
for ident in tok.get_identifiers():
name = _extract_identifiers(ident)
if name:
tables.add(name)
else:
# 可能是直接的 Name token 'schema.table'(未被识别为 Identifier)
txt = tok.value
if "." in txt:
parts = txt.strip().strip('`"\'').split(".")
if len(parts) == 2:
tables.add(f"{parts[0]}.{parts[1]}")
idx += 1
continue
idx += 1
if not tables:
raise ValueError("SQL 解析失败,未找到任何 schema.table 结构")
return sorted(tables)

48
vue-fastapi-frontend/src/views/metadataConfig/bizPermiConfig/index.vue

@ -125,6 +125,22 @@
maxlength="30" maxlength="30"
@change="handleObjValueChange" @change="handleObjValueChange"
>
<el-option
v-for="dict in userOrRoleList"
:key="dict.id"
:label="dict.name"
:value="dict.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="对象名称" prop="objValue">
<el-select
v-model="form.objValue"
placeholder="请选择"
maxlength="30"
@change="handleObjValueChange"
> >
<el-option <el-option
v-for="dict in userOrRoleList" v-for="dict in userOrRoleList"
@ -135,6 +151,22 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="安全等级" prop="secLevelSummary">
<el-select
v-model="form.secLevelSummary"
placeholder="请选择安全等级"
maxlength="30"
>
<el-option
v-for="dict in secList"
:key="dict.onum"
:label="dict.secLevelSummary"
:value="dict.secLevelSummary"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="是否停用" prop="isStop"> <el-form-item label="是否停用" prop="isStop">
<el-select v-model="form.isStop" placeholder="请选择状态"> <el-select v-model="form.isStop" placeholder="请选择状态">
<el-option label="运行" :value=true /> <el-option label="运行" :value=true />
@ -159,6 +191,7 @@ import {
addBizPermiConfig, addBizPermiConfig,
updateBizPermiConfig, updateBizPermiConfig,
listBizConfigAll, listBizConfigAll,
listMetadataSec,
delBizPermiConfig delBizPermiConfig
} from '@/api/metadataConfig/metadataConfig' } from '@/api/metadataConfig/metadataConfig'
import { listUser} from "@/api/system/user"; import { listUser} from "@/api/system/user";
@ -169,6 +202,7 @@ const queryForm = reactive({
pageNum: 1, pageNum: 1,
pageSize: 10 pageSize: 10
}) })
const secList = ref([])
const permList = ref([]) const permList = ref([])
const total = ref(0) const total = ref(0)
@ -205,6 +239,7 @@ const form = reactive({
objType: '', objType: '',
objName: '', objName: '',
objValue: '', objValue: '',
secLevelSummary:'',
isStop: null isStop: null
}) })
function changeMetaSecurityObj(data){ function changeMetaSecurityObj(data){
@ -313,6 +348,7 @@ if (title.value.includes('新增')) {
bizOnumList: form.bizOnum, bizOnumList: form.bizOnum,
objType: form.objType, objType: form.objType,
objName: form.objName, objName: form.objName,
secLevelSummary: form.secLevelSummary,
objValue: form.objValue, // objValue: form.objValue, //
isStop: form.isStop, isStop: form.isStop,
} }
@ -323,6 +359,7 @@ if (title.value.includes('新增')) {
onum: form.onum, onum: form.onum,
bizOnum: form.bizOnum, bizOnum: form.bizOnum,
objType: form.objType, objType: form.objType,
secLevelSummary: form.secLevelSummary,
objName: form.objName, objName: form.objName,
objValue: form.objValue, // objValue: form.objValue, //
isStop: form.isStop, isStop: form.isStop,
@ -370,7 +407,14 @@ function deleteSelected() {
}) })
.catch(() => {}) .catch(() => {})
} }
async function getSecList() {
try {
const res = await listMetadataSec({pageSize:100,pageNum:1})
secList.value = res.rows
} catch (error) {
ElMessage.error('获取安全等级列表失败,请重试')
}
}
function handleClose(done) { function handleClose(done) {
permFormRef.value.resetFields() permFormRef.value.resetFields()
done() done()
@ -380,6 +424,8 @@ onMounted(() => {
getList() getList()
getRoleOrUserList(); getRoleOrUserList();
getBizList(); getBizList();
getSecList()
}) })
</script> </script>

Loading…
Cancel
Save