update
This commit is contained in:
1 parent
f5cf3f4440
commit
0fcdbc5f6c
29 files changed
+773
-264
No files matched your search
@@ -505,7 +505,7 @@ func DetailListUsers(c *gin.Context) {
|
||||
|
||||
// POST /api/v1/query/execute
|
||||
func ExecuteQuery(c *gin.Context) {
|
||||
userID, _, _ := middleware.GetCurrentUser(c)
|
||||
userID, username, _ := middleware.GetCurrentUser(c)
|
||||
var input struct {
|
||||
InstanceID int `json:"instance_id"`
|
||||
Database string `json:"database"`
|
||||
@@ -535,17 +535,6 @@ func ExecuteQuery(c *gin.Context) {
|
||||
if input.Offset < 0 {
|
||||
input.Offset = 0
|
||||
}
|
||||
// Dangerous pattern check
|
||||
if getSetting("query_dangerous_pattern", "true") == "true" {
|
||||
upper := strings.ToUpper(input.SQL)
|
||||
for _, pat := range []string{"DELETE FROM", "UPDATE ", "DROP ", "TRUNCATE", "ALTER ", "RENAME "} {
|
||||
if strings.Contains(upper, pat) && !strings.Contains(upper, " WHERE ") && !strings.Contains(upper, " LIMIT ") {
|
||||
c.JSON(http.StatusOK, gin.H{"columns": []string{}, "rows": []interface{}{}, "total": 0, "cost_ms": 0,
|
||||
"error": "危险操作检测: " + pat + " 缺少 WHERE/LIMIT 条件,请在工单中提交"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var instance model.Instance
|
||||
if err := model.DB.First(&instance, input.InstanceID).Error; err != nil {
|
||||
@@ -553,6 +542,19 @@ func ExecuteQuery(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Dangerous pattern check
|
||||
if getSetting("query_dangerous_pattern", "true") == "true" {
|
||||
upper := strings.ToUpper(input.SQL)
|
||||
for _, pat := range []string{"DELETE FROM", "UPDATE ", "DROP ", "TRUNCATE", "ALTER ", "RENAME "} {
|
||||
if strings.Contains(upper, pat) && !strings.Contains(upper, " WHERE ") && !strings.Contains(upper, " LIMIT ") {
|
||||
saveQueryAuditLog(c, userID, username, &instance, input.Database, input.SQL, 0, 0, "failed", "危险操作检测: "+pat+" 缺少 WHERE/LIMIT 条件,请在工单中提交", true)
|
||||
c.JSON(http.StatusOK, gin.H{"columns": []string{}, "rows": []interface{}{}, "total": 0, "cost_ms": 0,
|
||||
"error": "危险操作检测: " + pat + " 缺少 WHERE/LIMIT 条件,请在工单中提交"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
switch instance.DBType {
|
||||
@@ -560,6 +562,7 @@ func ExecuteQuery(c *gin.Context) {
|
||||
result, err := executeSQLQuery(&instance, input.Database, input.SQL, input.Limit, input.Offset)
|
||||
costMs := time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
saveQueryAuditLog(c, userID, username, &instance, input.Database, input.SQL, costMs, 0, "failed", err.Error(), false)
|
||||
c.JSON(http.StatusOK, gin.H{"columns": []string{}, "rows": []interface{}{}, "total": 0, "cost_ms": costMs, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -567,12 +570,14 @@ func ExecuteQuery(c *gin.Context) {
|
||||
rowsInterface, _ := result["rows"].([]interface{})
|
||||
applyDataMasking(result, &instance)
|
||||
saveQueryHistory(userID, input.InstanceID, input.Database, input.SQL, costMs, len(rowsInterface))
|
||||
saveQueryAuditLog(c, userID, username, &instance, input.Database, input.SQL, costMs, len(rowsInterface), "success", "", false)
|
||||
c.JSON(http.StatusOK, result)
|
||||
|
||||
case "redis":
|
||||
result, err := executeRedisQuery(&instance, input.Database, input.SQL, input.Limit, input.Offset)
|
||||
costMs := time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
saveQueryAuditLog(c, userID, username, &instance, input.Database, input.SQL, costMs, 0, "failed", err.Error(), false)
|
||||
c.JSON(http.StatusOK, gin.H{"columns": []string{}, "rows": []interface{}{}, "total": 0, "cost_ms": costMs, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -583,12 +588,14 @@ func ExecuteQuery(c *gin.Context) {
|
||||
}
|
||||
applyDataMasking(result, &instance)
|
||||
saveQueryHistory(userID, input.InstanceID, input.Database, input.SQL, costMs, rowCount)
|
||||
saveQueryAuditLog(c, userID, username, &instance, input.Database, input.SQL, costMs, rowCount, "success", "", false)
|
||||
c.JSON(http.StatusOK, result)
|
||||
|
||||
case "mongodb", "mongo":
|
||||
result, err := executeMongoQuery(&instance, input.Database, input.SQL, input.Limit, input.Offset)
|
||||
costMs := time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
saveQueryAuditLog(c, userID, username, &instance, input.Database, input.SQL, costMs, 0, "failed", err.Error(), false)
|
||||
c.JSON(http.StatusOK, gin.H{"columns": []string{}, "rows": []interface{}{}, "total": 0, "cost_ms": costMs, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -596,6 +603,7 @@ func ExecuteQuery(c *gin.Context) {
|
||||
rowsInterface, _ := result["rows"].([]interface{})
|
||||
applyDataMasking(result, &instance)
|
||||
saveQueryHistory(userID, input.InstanceID, input.Database, input.SQL, costMs, len(rowsInterface))
|
||||
saveQueryAuditLog(c, userID, username, &instance, input.Database, input.SQL, costMs, len(rowsInterface), "success", "", false)
|
||||
c.JSON(http.StatusOK, result)
|
||||
|
||||
default:
|
||||
@@ -1594,6 +1602,45 @@ func saveQueryHistory(userID int, instanceID int, database string, sqlStr string
|
||||
model.DB.Create(&history)
|
||||
}
|
||||
|
||||
func saveQueryAuditLog(c *gin.Context, userID int, username string, instance *model.Instance, database string, sqlStr string, costMs int64, rowCount int, status string, errMsg string, isRisky bool) {
|
||||
targetID := instance.ID
|
||||
cost := int(costMs)
|
||||
rows := rowCount
|
||||
action := "query"
|
||||
if status != "success" {
|
||||
action = "query_failed"
|
||||
}
|
||||
|
||||
detail := fmt.Sprintf("数据查询: %s/%s, 返回 %d 行, 耗时 %dms", instance.Name, database, rowCount, costMs)
|
||||
if errMsg != "" {
|
||||
detail = fmt.Sprintf("数据查询失败: %s/%s, %s", instance.Name, database, errMsg)
|
||||
}
|
||||
|
||||
auditLog := model.AuditLog{
|
||||
UserID: userID,
|
||||
Username: &username,
|
||||
Action: action,
|
||||
TargetType: "query",
|
||||
TargetID: &targetID,
|
||||
Detail: &detail,
|
||||
IPAddress: auditStringPtr(c.ClientIP()),
|
||||
DBType: &instance.DBType,
|
||||
DatabaseName: &database,
|
||||
InstanceName: &instance.Name,
|
||||
ExecutionTimeMs: &cost,
|
||||
RowsAffected: &rows,
|
||||
ResultStatus: &status,
|
||||
QueryContent: &sqlStr,
|
||||
UserAgent: auditStringPtr(c.GetHeader("User-Agent")),
|
||||
IsRisky: &isRisky,
|
||||
}
|
||||
model.DB.Create(&auditLog)
|
||||
}
|
||||
|
||||
func auditStringPtr(value string) *string {
|
||||
return &value
|
||||
}
|
||||
|
||||
// applyDataMasking masks sensitive fields in query results with "*".
|
||||
// It reads global settings (data_masking_enabled + sensitive fields) and
|
||||
// merges with instance-level mask_fields.
|
||||
|
||||
Reference in new issue
Block a user