feat: improve datasource query audit workflows
This commit is contained in:
1 parent
74748e4914
commit
644981eef4
32 files changed
+780
-3061
No files matched your search
@@ -7,20 +7,20 @@ import (
|
||||
|
||||
"sql-platform/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"time"
|
||||
"context"
|
||||
"sql-platform/internal/middleware"
|
||||
"strings"
|
||||
"strconv"
|
||||
"sort"
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"regexp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/readpref"
|
||||
"regexp"
|
||||
"sort"
|
||||
"sql-platform/internal/middleware"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GET /api/v1/instances/:id/detail/databases
|
||||
@@ -523,7 +523,6 @@ func ExecuteQuery(c *gin.Context) {
|
||||
}
|
||||
// Read query limits from platform settings
|
||||
maxLimit := getSettingInt("query_limit_max", 1000)
|
||||
autoLimit := getSetting("query_auto_limit", "true") == "true"
|
||||
if input.Limit <= 0 {
|
||||
input.Limit = maxLimit
|
||||
if input.Limit > 1000 {
|
||||
@@ -547,7 +546,6 @@ func ExecuteQuery(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = autoLimit
|
||||
|
||||
var instance model.Instance
|
||||
if err := model.DB.First(&instance, input.InstanceID).Error; err != nil {
|
||||
@@ -619,6 +617,9 @@ func executeSQLQuery(instance *model.Instance, dbName string, sqlStr string, lim
|
||||
_ = ctx
|
||||
|
||||
upperSQL := strings.ToUpper(strings.TrimSpace(sqlStr))
|
||||
if ok, reason := isReadOnlySQL(sqlStr); !ok {
|
||||
return nil, fmt.Errorf("查询入口仅允许只读语句:%s。数据变更请提交工单", reason)
|
||||
}
|
||||
|
||||
// Count total (only when no user LIMIT and offset=0)
|
||||
total := 0
|
||||
@@ -643,7 +644,8 @@ func executeSQLQuery(instance *model.Instance, dbName string, sqlStr string, lim
|
||||
|
||||
// Inject LIMIT/OFFSET if not already present — only for SELECT statements
|
||||
// SHOW/DESC/EXPLAIN/CREATE/ALTER/DROP/etc do not support LIMIT syntax
|
||||
needsLimit := !hasLimit && strings.HasPrefix(upperSQL, "SELECT")
|
||||
autoLimit := getSetting("query_auto_limit", "true") == "true"
|
||||
needsLimit := autoLimit && !hasLimit && isLimitableReadSQL(upperSQL)
|
||||
if needsLimit {
|
||||
sqlStr = fmt.Sprintf("%s LIMIT %d OFFSET %d", sqlStr, limit, offset)
|
||||
}
|
||||
@@ -689,6 +691,82 @@ func executeSQLQuery(instance *model.Instance, dbName string, sqlStr string, lim
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isReadOnlySQL(sqlStr string) (bool, string) {
|
||||
cleaned := strings.TrimSpace(stripLeadingSQLComments(sqlStr))
|
||||
if cleaned == "" {
|
||||
return false, "空语句"
|
||||
}
|
||||
if hasMultipleSQLStatements(cleaned) {
|
||||
return false, "不允许一次执行多条语句"
|
||||
}
|
||||
|
||||
upper := strings.ToUpper(cleaned)
|
||||
readPrefixes := []string{"SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN", "WITH"}
|
||||
allowed := false
|
||||
for _, prefix := range readPrefixes {
|
||||
if strings.HasPrefix(upper, prefix+" ") || upper == prefix {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return false, "只允许 SELECT / SHOW / DESCRIBE / EXPLAIN"
|
||||
}
|
||||
|
||||
blockedPatterns := []string{
|
||||
" FOR UPDATE", " LOCK IN SHARE MODE", " INTO OUTFILE", " INTO DUMPFILE",
|
||||
"EXPLAIN ANALYZE", "DELETE ", "UPDATE ", "INSERT ", "REPLACE ",
|
||||
"CREATE ", "ALTER ", "DROP ", "TRUNCATE ", "RENAME ", "GRANT ", "REVOKE ",
|
||||
"CALL ", "DO ", "LOAD DATA", "COPY ",
|
||||
}
|
||||
for _, pattern := range blockedPatterns {
|
||||
if strings.Contains(upper, pattern) {
|
||||
return false, strings.TrimSpace(pattern)
|
||||
}
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func isLimitableReadSQL(upperSQL string) bool {
|
||||
return strings.HasPrefix(upperSQL, "SELECT") || strings.HasPrefix(upperSQL, "WITH")
|
||||
}
|
||||
|
||||
func hasMultipleSQLStatements(sqlStr string) bool {
|
||||
trimmed := strings.TrimSpace(sqlStr)
|
||||
if strings.HasSuffix(trimmed, ";") {
|
||||
trimmed = strings.TrimSpace(strings.TrimSuffix(trimmed, ";"))
|
||||
}
|
||||
return strings.Contains(trimmed, ";")
|
||||
}
|
||||
|
||||
func stripLeadingSQLComments(sqlStr string) string {
|
||||
s := strings.TrimSpace(sqlStr)
|
||||
for {
|
||||
switch {
|
||||
case strings.HasPrefix(s, "--"):
|
||||
if idx := strings.Index(s, "\n"); idx >= 0 {
|
||||
s = strings.TrimSpace(s[idx+1:])
|
||||
continue
|
||||
}
|
||||
return ""
|
||||
case strings.HasPrefix(s, "#"):
|
||||
if idx := strings.Index(s, "\n"); idx >= 0 {
|
||||
s = strings.TrimSpace(s[idx+1:])
|
||||
continue
|
||||
}
|
||||
return ""
|
||||
case strings.HasPrefix(s, "/*"):
|
||||
if idx := strings.Index(s, "*/"); idx >= 0 {
|
||||
s = strings.TrimSpace(s[idx+2:])
|
||||
continue
|
||||
}
|
||||
return s
|
||||
default:
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseCommandParts splits a command string into parts, respecting quoted strings.
|
||||
// e.g. HGET "my key" "my field" => ["HGET", "my key", "my field"]
|
||||
func parseCommandParts(input string) []string {
|
||||
|
||||
Reference in new issue
Block a user