🐛 fix(mongodb/query-editor): 修复受限账号连接与不限行数

- MongoDB 指定数据库时保留连接校验并跳过全库枚举
- 允许查询最大行数为 0 并持久化不限设置
- 补充数据库枚举与不限行数持久化回归测试

Refs #741
Refs #752
This commit is contained in:
Syngnat
2026-07-29 17:19:15 +08:00
parent 61358b1a62
commit bd259e4e51
4 changed files with 145 additions and 7 deletions

View File

@@ -3,6 +3,7 @@ package app
import (
"context"
"fmt"
"net/url"
"sort"
"strconv"
"strings"
@@ -1865,8 +1866,33 @@ func ensureNonNilSlice[T any](items []T) []T {
return items
}
func resolveConfiguredMongoDatabase(config connection.ConnectionConfig) string {
if !strings.EqualFold(strings.TrimSpace(config.Type), "mongodb") {
return ""
}
if database := strings.TrimSpace(config.Database); database != "" {
return database
}
rawURI := strings.TrimSpace(config.URI)
lowerURI := strings.ToLower(rawURI)
if !strings.HasPrefix(lowerURI, "mongodb://") && !strings.HasPrefix(lowerURI, "mongodb+srv://") {
return ""
}
parsed, err := url.Parse(rawURI)
if err != nil {
return ""
}
database := strings.Trim(strings.TrimSpace(parsed.Path), "/")
if database == "" || strings.Contains(database, "/") {
return ""
}
return database
}
func (a *App) DBGetDatabases(config connection.ConnectionConfig) connection.QueryResult {
runConfig := normalizeRunConfig(config, "")
configuredMongoDatabase := resolveConfiguredMongoDatabase(runConfig)
if strings.EqualFold(strings.TrimSpace(runConfig.Type), "redis") {
runConfig.Type = "redis"
client, err := a.getRedisClient(runConfig)
@@ -1890,6 +1916,12 @@ func (a *App) DBGetDatabases(config connection.ConnectionConfig) connection.Quer
logger.Error(err, "DBGetDatabases 获取连接失败:%s", formatConnSummary(runConfig))
return connection.QueryResult{Success: false, Message: err.Error()}
}
if configuredMongoDatabase != "" {
return connection.QueryResult{
Success: true,
Data: []map[string]string{{"Database": configuredMongoDatabase}},
}
}
dbs, err := dbInst.GetDatabases()
if err != nil && shouldRefreshCachedConnection(err) {

View File

@@ -10,9 +10,12 @@ import (
)
type releaseRecordingDB struct {
closed int
connect func(config connection.ConnectionConfig) error
closeErr error
closed int
connect func(config connection.ConnectionConfig) error
closeErr error
pingCalls int
getDatabasesCalls int
databases []string
}
func (f *releaseRecordingDB) Connect(config connection.ConnectionConfig) error {
@@ -25,12 +28,18 @@ func (f *releaseRecordingDB) Close() error {
f.closed++
return f.closeErr
}
func (f *releaseRecordingDB) Ping() error { return nil }
func (f *releaseRecordingDB) Ping() error {
f.pingCalls++
return nil
}
func (f *releaseRecordingDB) Query(query string) ([]map[string]interface{}, []string, error) {
return nil, nil, nil
}
func (f *releaseRecordingDB) Exec(query string) (int64, error) { return 0, nil }
func (f *releaseRecordingDB) GetDatabases() ([]string, error) { return nil, nil }
func (f *releaseRecordingDB) Exec(query string) (int64, error) { return 0, nil }
func (f *releaseRecordingDB) GetDatabases() ([]string, error) {
f.getDatabasesCalls++
return f.databases, nil
}
func (f *releaseRecordingDB) GetTables(dbName string) ([]string, error) { return nil, nil }
func (f *releaseRecordingDB) GetCreateStatement(dbName, tableName string) (string, error) {
return "", nil
@@ -75,6 +84,86 @@ func TestNormalizeTestConnectionConfig_ZeroTimeout(t *testing.T) {
}
}
func TestDBGetDatabases_MongoConfiguredDatabaseSkipsEnumeration(t *testing.T) {
installFakeOptionalDriverRuntime(t)
tests := []struct {
name string
config connection.ConnectionConfig
want string
}{
{
name: "database field",
config: connection.ConnectionConfig{
Type: "mongodb",
Database: " application ",
},
want: "application",
},
{
name: "database in URI",
config: connection.ConnectionConfig{
Type: "mongodb",
URI: "mongodb://user:password@localhost:27017/reporting?authSource=admin",
},
want: "reporting",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
app := NewApp()
database := &releaseRecordingDB{databases: []string{"admin", "other"}}
app.dbCache[getCacheKey(test.config)] = cachedDatabase{
inst: database,
config: normalizeCacheKeyConfig(test.config),
}
result := app.DBGetDatabases(test.config)
if !result.Success {
t.Fatalf("expected scoped MongoDB database lookup to succeed, got %q", result.Message)
}
if database.pingCalls != 1 {
t.Fatalf("expected scoped MongoDB lookup to validate the connection, got %d pings", database.pingCalls)
}
if database.getDatabasesCalls != 0 {
t.Fatalf("expected scoped MongoDB lookup to skip database enumeration, got %d calls", database.getDatabasesCalls)
}
rows, ok := result.Data.([]map[string]string)
if !ok {
t.Fatalf("expected database rows, got %#v", result.Data)
}
if len(rows) != 1 || rows[0]["Database"] != test.want {
t.Fatalf("expected only configured database %q, got %#v", test.want, rows)
}
})
}
}
func TestDBGetDatabases_MongoWithoutConfiguredDatabaseStillEnumerates(t *testing.T) {
installFakeOptionalDriverRuntime(t)
config := connection.ConnectionConfig{Type: "mongodb"}
app := NewApp()
database := &releaseRecordingDB{databases: []string{"admin", "application"}}
app.dbCache[getCacheKey(config)] = cachedDatabase{
inst: database,
config: normalizeCacheKeyConfig(config),
}
result := app.DBGetDatabases(config)
if !result.Success {
t.Fatalf("expected unscoped MongoDB database lookup to succeed, got %q", result.Message)
}
if database.getDatabasesCalls != 1 {
t.Fatalf("expected unscoped MongoDB lookup to enumerate databases once, got %d calls", database.getDatabasesCalls)
}
rows, ok := result.Data.([]map[string]string)
if !ok || len(rows) != 2 || rows[0]["Database"] != "admin" || rows[1]["Database"] != "application" {
t.Fatalf("expected enumerated MongoDB databases, got %#v", result.Data)
}
}
func TestValidateTestConnectionInput_ClickHouseRequiresTarget(t *testing.T) {
err := validateTestConnectionInput(connection.ConnectionConfig{Type: "clickhouse"})
if err == nil {