This commit is contained in:
hermes-$(hostname) committed 2026-07-08 08:53:48 +00:00
1 parent f5cf3f4440
commit 0fcdbc5f6c
29 files changed
+773 -264

No files matched your search

+21 -7
View File
@@ -13,20 +13,34 @@ import (
func ListAuditLogs(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 { page = 1 }
if pageSize < 1 || pageSize > 100 { pageSize = 20 }
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
var total int64
model.DB.Model(&model.AuditLog{}).Count(&total)
query := model.DB.Model(&model.AuditLog{})
if action := c.Query("action"); action != "" {
query = query.Where("action = ?", action)
}
if targetType := c.Query("target_type"); targetType != "" {
query = query.Where("target_type = ?", targetType)
}
if resultStatus := c.Query("result_status"); resultStatus != "" {
query = query.Where("result_status = ?", resultStatus)
}
query.Count(&total)
var logs []model.AuditLog
offset := (page - 1) * pageSize
model.DB.Order("id DESC").Offset(offset).Limit(pageSize).Find(&logs)
query.Order("id DESC").Offset(offset).Limit(pageSize).Find(&logs)
c.JSON(http.StatusOK, gin.H{
"items": logs,
"total": total,
"page": page,
"items": logs,
"total": total,
"page": page,
"page_size": pageSize,
})
}
+7 -4
View File
@@ -159,17 +159,20 @@ func TestConnection(c *gin.Context) {
}
result := testDBConnectionFull(&instance)
// Update version & cluster_mode in DB if detected
// Keep the persisted list status in sync with an explicit connection test.
updates := map[string]interface{}{}
if result.Success {
updates["status"] = "connected"
} else {
updates["status"] = "error"
}
if result.Success && result.Version != "" {
updates["version"] = result.Version
}
if instance.DBType == "redis" {
updates["cluster_mode"] = result.ClusterMode
}
if len(updates) > 0 {
model.DB.Model(&model.Instance{}).Where("id = ?", instance.ID).Updates(updates)
}
model.DB.Model(&model.Instance{}).Where("id = ?", instance.ID).Updates(updates)
if result.Success {
c.JSON(http.StatusOK, gin.H{"success": true, "message": result.Message, "version": result.Version, "cluster_mode": result.ClusterMode})
} else {
+59 -12
View File
@@ -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.
+13 -9
View File
@@ -484,9 +484,20 @@ func RedisDbsize(c *gin.Context) {
func RedisScanKeys(c *gin.Context) {
id := c.Param("id")
dbParam := c.Query("db")
rawPattern := strings.TrimSpace(c.DefaultQuery("pattern", "*"))
type KeyInfo struct {
Key string `json:"key"`
Type string `json:"type"`
TTL int64 `json:"ttl"`
}
rawPattern := strings.TrimSpace(c.Query("pattern"))
if rawPattern == "" {
rawPattern = "*"
c.JSON(http.StatusOK, gin.H{
"keys": []KeyInfo{},
"cursor": 0,
"total": 0,
})
return
}
pattern := rawPattern
hasWildcard := strings.ContainsAny(pattern, "*?[")
@@ -512,13 +523,6 @@ func RedisScanKeys(c *gin.Context) {
ctx := context.Background()
// Get types for each key
type KeyInfo struct {
Key string `json:"key"`
Type string `json:"type"`
TTL int64 `json:"ttl"`
}
// Redis Cluster 精确 key 查询应优先走槽位路由直查。全量 SCAN 需要访问各 master,
// 在 NAT/代理暴露单入口时容易受节点通告地址影响。
if !hasWildcard && pattern != "*" {