Skip to content

Commit cd054ed

Browse files
authored
Fix: check sql component required fields (#19418)
1 parent 792a17c commit cd054ed

3 files changed

Lines changed: 259 additions & 0 deletions

File tree

internal/agent/component/dynamic_params.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ func validateDynamicParams(component string, params map[string]any) error {
9292
return validateVariableAggregatorGroups(component, params)
9393
case "userfillup":
9494
return validateInputOptions(component, params)
95+
case "exesql":
96+
if err := validateExeSQLParams(params); err != nil {
97+
return fmt.Errorf("[%s] %w", component, err)
98+
}
9599
}
96100
return nil
97101
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package component
16+
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
"reflect"
21+
"strings"
22+
)
23+
24+
// exeSQLDBTypes is the whitelist enforced at save time for an ExeSQL
25+
// component's db_type. It mirrors Python ExeSQLParam.check
26+
// (agent/tools/exesql.py), where ExeSQLParam defaults db_type to "mysql"
27+
// when the DSL omits it.
28+
var exeSQLDBTypes = []string{"mysql", "postgres", "mariadb", "mssql", "IBM DB2", "trino", "oceanbase"}
29+
30+
// validateExeSQLParams enforces the required connection settings of an
31+
// ExeSQL component at save time. The Go agent PUT path validates DSL
32+
// component parameters through validateDynamicParams; Python performs the
33+
// same checks via Canvas.validate_component_parameters ->
34+
// ExeSQLParam.check. Without them a canvas can be persisted with a broken
35+
// DB tool that only fails at runtime.
36+
func validateExeSQLParams(params map[string]any) error {
37+
dbType, explicit := params["db_type"]
38+
if !explicit {
39+
// Python default when the DSL omits db_type.
40+
dbType = "mysql"
41+
}
42+
dbTypeString, ok := dbType.(string)
43+
if !ok || !exeSQLDBTypeAllowed(dbTypeString) {
44+
return fmt.Errorf("Choose DB type %v is not supported, it should be in %v", dbType, exeSQLDBTypes)
45+
}
46+
47+
// Connection identity fields have no Python defaults and must be present.
48+
for _, field := range []struct {
49+
key string
50+
desc string
51+
}{
52+
{"database", "Database name"},
53+
{"username", "database username"},
54+
{"host", "IP Address"},
55+
} {
56+
if !isNonBlankString(params[field.key]) {
57+
return fmt.Errorf("%s does not support empty value", field.desc)
58+
}
59+
}
60+
61+
if port, present := params["port"]; present {
62+
if !isPositiveInteger(port) {
63+
return fmt.Errorf("IP Port %v not supported, should be positive integer", port)
64+
}
65+
}
66+
67+
// Trino connects without credentials; every other supported DB type
68+
// requires a password.
69+
if dbTypeString != "trino" && !isNonBlankString(params["password"]) {
70+
return fmt.Errorf("Database password does not support empty value")
71+
}
72+
73+
if maxRecords, present := params["max_records"]; present {
74+
if !isPositiveInteger(maxRecords) {
75+
return fmt.Errorf("Maximum number of records %v not supported, should be positive integer", maxRecords)
76+
}
77+
}
78+
79+
// Refuse configs aimed at RAGFlow's own metadata database, mirroring
80+
// Python ExeSQLParam.check's security guard.
81+
if trimmedDBString(params["database"]) == "rag_flow" &&
82+
(trimmedDBString(params["host"]) == "ragflow-mysql" || trimmedDBString(params["password"]) == "infini_rag_flow") {
83+
return fmt.Errorf("For the security reason, it does not support database named rag_flow.")
84+
}
85+
return nil
86+
}
87+
88+
// exeSQLDBTypeAllowed reports whether dbType is one of exeSQLDBTypes.
89+
func exeSQLDBTypeAllowed(dbType string) bool {
90+
for _, candidate := range exeSQLDBTypes {
91+
if candidate == dbType {
92+
return true
93+
}
94+
}
95+
return false
96+
}
97+
98+
// trimmedDBString returns the whitespace-trimmed value of a string-ish
99+
// parameter, or "" when it is absent or not a string.
100+
func trimmedDBString(value any) string {
101+
s, _ := value.(string)
102+
return strings.TrimSpace(s)
103+
}
104+
105+
// isPositiveInteger reports whether value is a whole number greater than
106+
// zero. json.Number lexemes survive the JSON decode and whole-valued
107+
// float64/float32 are accepted for consistency with isIntegerNumber.
108+
func isPositiveInteger(value any) bool {
109+
if !isIntegerNumber(value) {
110+
return false
111+
}
112+
switch n := value.(type) {
113+
case json.Number:
114+
i, err := n.Int64()
115+
return err == nil && i > 0
116+
case float64:
117+
return n > 0
118+
case float32:
119+
return n > 0
120+
default:
121+
v := reflect.ValueOf(value)
122+
switch v.Kind() {
123+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
124+
return v.Int() > 0
125+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
126+
return v.Uint() > 0
127+
default:
128+
return false
129+
}
130+
}
131+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package component
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestValidateDynamicEntriesExeSQL(t *testing.T) {
9+
valid := dslWithComponents(
10+
componentDSL("ExeSQL", map[string]any{
11+
"db_type": "mysql",
12+
"database": "orders",
13+
"username": "report",
14+
"host": "10.0.0.5",
15+
"port": 3306,
16+
"password": "secret",
17+
"max_records": 100,
18+
}),
19+
)
20+
if err := ValidateDynamicEntries(valid); err != nil {
21+
t.Fatalf("valid ExeSQL DSL rejected: %v", err)
22+
}
23+
24+
// db_type is optional: ExeSQLParam defaults it to "mysql".
25+
noDBType := dslWithComponents(componentDSL("ExeSQL", map[string]any{
26+
"database": "orders",
27+
"username": "report",
28+
"host": "10.0.0.5",
29+
"password": "secret",
30+
}))
31+
if err := ValidateDynamicEntries(noDBType); err != nil {
32+
t.Fatalf("ExeSQL DSL without db_type rejected: %v", err)
33+
}
34+
35+
// Trino connects without credentials.
36+
trino := dslWithComponents(componentDSL("ExeSQL", map[string]any{
37+
"db_type": "trino",
38+
"database": "catalog",
39+
"username": "report",
40+
"host": "10.0.0.5",
41+
"password": "",
42+
}))
43+
if err := ValidateDynamicEntries(trino); err != nil {
44+
t.Fatalf("valid trino ExeSQL DSL rejected: %v", err)
45+
}
46+
47+
tests := []struct {
48+
name string
49+
component map[string]any
50+
want string
51+
}{
52+
{
53+
"unsupported db_type",
54+
componentDSL("ExeSQL", map[string]any{"db_type": "oracle", "database": "d", "username": "u", "host": "h", "password": "p"}),
55+
"Choose DB type oracle is not supported, it should be in",
56+
},
57+
{
58+
"non-string db_type",
59+
componentDSL("ExeSQL", map[string]any{"db_type": 1, "database": "d", "username": "u", "host": "h", "password": "p"}),
60+
"Choose DB type 1 is not supported, it should be in",
61+
},
62+
{
63+
"empty database",
64+
componentDSL("ExeSQL", map[string]any{"db_type": "mysql", "database": "", "username": "u", "host": "h", "password": "p"}),
65+
"[ExeSQL] Database name does not support empty value",
66+
},
67+
{
68+
"missing database",
69+
componentDSL("ExeSQL", map[string]any{"db_type": "mysql", "username": "u", "host": "h", "password": "p"}),
70+
"[ExeSQL] Database name does not support empty value",
71+
},
72+
{
73+
"empty username",
74+
componentDSL("ExeSQL", map[string]any{"db_type": "mysql", "database": "d", "username": "", "host": "h", "password": "p"}),
75+
"[ExeSQL] database username does not support empty value",
76+
},
77+
{
78+
"empty host",
79+
componentDSL("ExeSQL", map[string]any{"db_type": "mysql", "database": "d", "username": "u", "host": " ", "password": "p"}),
80+
"[ExeSQL] IP Address does not support empty value",
81+
},
82+
{
83+
"missing password",
84+
componentDSL("ExeSQL", map[string]any{"db_type": "mysql", "database": "d", "username": "u", "host": "h"}),
85+
"[ExeSQL] Database password does not support empty value",
86+
},
87+
{
88+
"zero port",
89+
componentDSL("ExeSQL", map[string]any{"db_type": "mysql", "database": "d", "username": "u", "host": "h", "password": "p", "port": 0}),
90+
"[ExeSQL] IP Port 0 not supported, should be positive integer",
91+
},
92+
{
93+
"fractional port",
94+
componentDSL("ExeSQL", map[string]any{"db_type": "mysql", "database": "d", "username": "u", "host": "h", "password": "p", "port": 3306.5}),
95+
"[ExeSQL] IP Port 3306.5 not supported, should be positive integer",
96+
},
97+
{
98+
"negative max_records",
99+
componentDSL("ExeSQL", map[string]any{"db_type": "mysql", "database": "d", "username": "u", "host": "h", "password": "p", "max_records": -1}),
100+
"[ExeSQL] Maximum number of records -1 not supported, should be positive integer",
101+
},
102+
{
103+
"rag_flow database with default mysql host",
104+
componentDSL("ExeSQL", map[string]any{"db_type": "mysql", "database": "rag_flow", "username": "u", "host": "ragflow-mysql", "password": "secret"}),
105+
"[ExeSQL] For the security reason, it does not support database named rag_flow.",
106+
},
107+
{
108+
"rag_flow database with default mysql password",
109+
componentDSL("ExeSQL", map[string]any{"db_type": "mysql", "database": "rag_flow", "username": "u", "host": "10.0.0.5", "password": "infini_rag_flow"}),
110+
"[ExeSQL] For the security reason, it does not support database named rag_flow.",
111+
},
112+
}
113+
for _, tt := range tests {
114+
t.Run(tt.name, func(t *testing.T) {
115+
err := ValidateDynamicEntries(dslWithComponents(tt.component))
116+
if err == nil {
117+
t.Fatalf("expected error containing %q, got nil", tt.want)
118+
}
119+
if !strings.Contains(err.Error(), tt.want) {
120+
t.Fatalf("error %q does not contain %q", err.Error(), tt.want)
121+
}
122+
})
123+
}
124+
}

0 commit comments

Comments
 (0)