update
This commit is contained in:
1 parent
f5cf3f4440
commit
0fcdbc5f6c
29 files changed
+773
-264
No files matched your search
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 != "*" {
|
||||
|
||||
Reference in new issue
Block a user