mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-16 18:11:43 +08:00
Merge branch 'dev' into feature/20260602_connection_driver_i18n
This commit is contained in:
@@ -8,6 +8,15 @@ import (
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
type connectionProtectionKey string
|
||||
|
||||
const (
|
||||
connectionProtectionDataEdit connectionProtectionKey = "restrictDataEdit"
|
||||
connectionProtectionStructureEdit connectionProtectionKey = "restrictStructureEdit"
|
||||
connectionProtectionScriptExecution connectionProtectionKey = "restrictScriptExecution"
|
||||
connectionProtectionDataImport connectionProtectionKey = "restrictDataImport"
|
||||
)
|
||||
|
||||
var connectionReadOnlySupportedTypes = map[string]struct{}{
|
||||
"clickhouse": {},
|
||||
"dameng": {},
|
||||
@@ -88,40 +97,41 @@ var mongoMetaCommandKeys = map[string]struct{}{
|
||||
}
|
||||
|
||||
var readOnlyConnectionActionTextKeys = map[string]string{
|
||||
"创建数据库": "connection.backend.action.create_database",
|
||||
"connection.backend.action.create_database": "connection.backend.action.create_database",
|
||||
"创建模式": "connection.backend.action.create_schema",
|
||||
"connection.backend.action.create_schema": "connection.backend.action.create_schema",
|
||||
"重命名模式": "connection.backend.action.rename_schema",
|
||||
"connection.backend.action.rename_schema": "connection.backend.action.rename_schema",
|
||||
"删除模式": "connection.backend.action.drop_schema",
|
||||
"connection.backend.action.drop_schema": "connection.backend.action.drop_schema",
|
||||
"重命名数据库": "connection.backend.action.rename_database",
|
||||
"connection.backend.action.rename_database": "connection.backend.action.rename_database",
|
||||
"删除数据库": "connection.backend.action.drop_database",
|
||||
"connection.backend.action.drop_database": "connection.backend.action.drop_database",
|
||||
"重命名表": "connection.backend.action.rename_table",
|
||||
"connection.backend.action.rename_table": "connection.backend.action.rename_table",
|
||||
"删除表": "connection.backend.action.drop_table",
|
||||
"connection.backend.action.drop_table": "connection.backend.action.drop_table",
|
||||
"删除视图": "connection.backend.action.drop_view",
|
||||
"connection.backend.action.drop_view": "connection.backend.action.drop_view",
|
||||
"删除函数或存储过程": "connection.backend.action.drop_function_or_procedure",
|
||||
"创建数据库": "connection.backend.action.create_database",
|
||||
"connection.backend.action.create_database": "connection.backend.action.create_database",
|
||||
"创建模式": "connection.backend.action.create_schema",
|
||||
"connection.backend.action.create_schema": "connection.backend.action.create_schema",
|
||||
"重命名模式": "connection.backend.action.rename_schema",
|
||||
"connection.backend.action.rename_schema": "connection.backend.action.rename_schema",
|
||||
"删除模式": "connection.backend.action.drop_schema",
|
||||
"connection.backend.action.drop_schema": "connection.backend.action.drop_schema",
|
||||
"重命名数据库": "connection.backend.action.rename_database",
|
||||
"connection.backend.action.rename_database": "connection.backend.action.rename_database",
|
||||
"删除数据库": "connection.backend.action.drop_database",
|
||||
"connection.backend.action.drop_database": "connection.backend.action.drop_database",
|
||||
"重命名表": "connection.backend.action.rename_table",
|
||||
"connection.backend.action.rename_table": "connection.backend.action.rename_table",
|
||||
"删除表": "connection.backend.action.drop_table",
|
||||
"connection.backend.action.drop_table": "connection.backend.action.drop_table",
|
||||
"删除视图": "connection.backend.action.drop_view",
|
||||
"connection.backend.action.drop_view": "connection.backend.action.drop_view",
|
||||
"删除函数或存储过程": "connection.backend.action.drop_function_or_procedure",
|
||||
"connection.backend.action.drop_function_or_procedure": "connection.backend.action.drop_function_or_procedure",
|
||||
"重命名视图": "connection.backend.action.rename_view",
|
||||
"connection.backend.action.rename_view": "connection.backend.action.rename_view",
|
||||
"导入数据": "connection.backend.action.import_data",
|
||||
"connection.backend.action.import_data": "connection.backend.action.import_data",
|
||||
"提交结果修改": "connection.backend.action.apply_result_changes",
|
||||
"connection.backend.action.apply_result_changes": "connection.backend.action.apply_result_changes",
|
||||
"预览结果修改": "connection.backend.action.preview_result_changes",
|
||||
"connection.backend.action.preview_result_changes": "connection.backend.action.preview_result_changes",
|
||||
"clear_table": "connection.backend.action.clear_table",
|
||||
"connection.backend.action.clear_table": "connection.backend.action.clear_table",
|
||||
"truncate_table": "connection.backend.action.truncate_table",
|
||||
"connection.backend.action.truncate_table": "connection.backend.action.truncate_table",
|
||||
"数据同步写入": "connection.backend.action.data_sync_write",
|
||||
"connection.backend.action.data_sync_write": "connection.backend.action.data_sync_write",
|
||||
"重命名视图": "connection.backend.action.rename_view",
|
||||
"connection.backend.action.rename_view": "connection.backend.action.rename_view",
|
||||
"导入数据": "connection.backend.action.import_data",
|
||||
"connection.backend.action.import_data": "connection.backend.action.import_data",
|
||||
"提交结果修改": "connection.backend.action.apply_result_changes",
|
||||
"connection.backend.action.apply_result_changes": "connection.backend.action.apply_result_changes",
|
||||
"预览结果修改": "connection.backend.action.preview_result_changes",
|
||||
"connection.backend.action.preview_result_changes": "connection.backend.action.preview_result_changes",
|
||||
"clear_table": "connection.backend.action.clear_table",
|
||||
"connection.backend.action.clear_table": "connection.backend.action.clear_table",
|
||||
"truncate_table": "connection.backend.action.truncate_table",
|
||||
"connection.backend.action.truncate_table": "connection.backend.action.truncate_table",
|
||||
"connection.backend.action.data_sync_structure": "connection.backend.action.data_sync_structure",
|
||||
"数据同步写入": "connection.backend.action.data_sync_write",
|
||||
"connection.backend.action.data_sync_write": "connection.backend.action.data_sync_write",
|
||||
}
|
||||
|
||||
func supportsConnectionReadOnlyMode(config connection.ConnectionConfig) bool {
|
||||
@@ -129,8 +139,57 @@ func supportsConnectionReadOnlyMode(config connection.ConnectionConfig) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func hasAnyConnectionProtection(config connection.ConnectionProtectionConfig) bool {
|
||||
return config.RestrictDataEdit ||
|
||||
config.RestrictStructureEdit ||
|
||||
config.RestrictScriptExecution ||
|
||||
config.RestrictDataImport
|
||||
}
|
||||
|
||||
func resolveConnectionProtectionConfig(config connection.ConnectionConfig) connection.ConnectionProtectionConfig {
|
||||
if !supportsConnectionReadOnlyMode(config) {
|
||||
return connection.ConnectionProtectionConfig{}
|
||||
}
|
||||
if hasAnyConnectionProtection(config.Protection) {
|
||||
return config.Protection
|
||||
}
|
||||
if config.ReadOnly {
|
||||
return connection.ConnectionProtectionConfig{
|
||||
RestrictDataEdit: true,
|
||||
RestrictStructureEdit: true,
|
||||
RestrictScriptExecution: true,
|
||||
RestrictDataImport: true,
|
||||
}
|
||||
}
|
||||
return connection.ConnectionProtectionConfig{}
|
||||
}
|
||||
|
||||
func isConnectionProtectionEnabled(config connection.ConnectionConfig, key connectionProtectionKey) bool {
|
||||
protection := resolveConnectionProtectionConfig(config)
|
||||
switch key {
|
||||
case connectionProtectionDataEdit:
|
||||
return protection.RestrictDataEdit
|
||||
case connectionProtectionStructureEdit:
|
||||
return protection.RestrictStructureEdit
|
||||
case connectionProtectionScriptExecution:
|
||||
return protection.RestrictScriptExecution
|
||||
case connectionProtectionDataImport:
|
||||
return protection.RestrictDataImport
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isConnectionForcedReadOnly(config connection.ConnectionConfig) bool {
|
||||
return config.ReadOnly && supportsConnectionReadOnlyMode(config)
|
||||
protection := resolveConnectionProtectionConfig(config)
|
||||
return protection.RestrictDataEdit &&
|
||||
protection.RestrictStructureEdit &&
|
||||
protection.RestrictScriptExecution &&
|
||||
protection.RestrictDataImport
|
||||
}
|
||||
|
||||
func isConnectionScriptExecutionRestricted(config connection.ConnectionConfig) bool {
|
||||
return isConnectionProtectionEnabled(config, connectionProtectionScriptExecution)
|
||||
}
|
||||
|
||||
func normalizeReadOnlyConnectionText(text func(string, map[string]any) string) func(string, map[string]any) string {
|
||||
@@ -174,9 +233,9 @@ func readOnlyConnectionActionBlockedMessage(action string) string {
|
||||
return readOnlyConnectionActionBlockedMessageWithText(action, defaultAppText)
|
||||
}
|
||||
|
||||
func ensureReadOnlyConnectionAllowsQueryWithText(config connection.ConnectionConfig, query string, text func(string, map[string]any) string) error {
|
||||
func ensureConnectionAllowsQueryWithText(config connection.ConnectionConfig, query string, text func(string, map[string]any) string) error {
|
||||
text = normalizeReadOnlyConnectionText(text)
|
||||
if !isConnectionForcedReadOnly(config) {
|
||||
if !isConnectionScriptExecutionRestricted(config) {
|
||||
return nil
|
||||
}
|
||||
for _, statement := range splitSQLStatements(query) {
|
||||
@@ -187,20 +246,32 @@ func ensureReadOnlyConnectionAllowsQueryWithText(config connection.ConnectionCon
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureReadOnlyConnectionAllowsQuery(config connection.ConnectionConfig, query string) error {
|
||||
return ensureReadOnlyConnectionAllowsQueryWithText(config, query, defaultAppText)
|
||||
func ensureConnectionAllowsQuery(config connection.ConnectionConfig, query string) error {
|
||||
return ensureConnectionAllowsQueryWithText(config, query, defaultAppText)
|
||||
}
|
||||
|
||||
func ensureReadOnlyConnectionAllowsActionWithText(config connection.ConnectionConfig, action string, text func(string, map[string]any) string) error {
|
||||
func ensureConnectionAllowsActionWithText(config connection.ConnectionConfig, key connectionProtectionKey, action string, text func(string, map[string]any) string) error {
|
||||
text = normalizeReadOnlyConnectionText(text)
|
||||
if !isConnectionForcedReadOnly(config) {
|
||||
if !isConnectionProtectionEnabled(config, key) {
|
||||
return nil
|
||||
}
|
||||
return errors.New(readOnlyConnectionActionBlockedMessageWithText(action, text))
|
||||
}
|
||||
|
||||
func ensureReadOnlyConnectionAllowsAction(config connection.ConnectionConfig, action string) error {
|
||||
return ensureReadOnlyConnectionAllowsActionWithText(config, action, defaultAppText)
|
||||
func ensureConnectionAllowsAction(config connection.ConnectionConfig, key connectionProtectionKey, action string) error {
|
||||
return ensureConnectionAllowsActionWithText(config, key, action, defaultAppText)
|
||||
}
|
||||
|
||||
func ensureConnectionAllowsDataEdit(config connection.ConnectionConfig, action string) error {
|
||||
return ensureConnectionAllowsAction(config, connectionProtectionDataEdit, action)
|
||||
}
|
||||
|
||||
func ensureConnectionAllowsStructureEdit(config connection.ConnectionConfig, action string) error {
|
||||
return ensureConnectionAllowsAction(config, connectionProtectionStructureEdit, action)
|
||||
}
|
||||
|
||||
func ensureConnectionAllowsDataImport(config connection.ConnectionConfig, action string) error {
|
||||
return ensureConnectionAllowsAction(config, connectionProtectionDataImport, action)
|
||||
}
|
||||
|
||||
func isReadOnlyMongoCommand(query string) bool {
|
||||
|
||||
@@ -32,6 +32,7 @@ func TestConnectionReadOnlyMessagesUseLocalizedText(t *testing.T) {
|
||||
"connection.backend.error.readonly_action_blocked",
|
||||
"connection.backend.action.create_database",
|
||||
"connection.backend.action.import_data",
|
||||
"connection.backend.action.data_sync_structure",
|
||||
"connection.backend.action.data_sync_write",
|
||||
"connection.backend.action.clear_table",
|
||||
"connection.backend.action.truncate_table",
|
||||
@@ -67,6 +68,7 @@ func TestConnectionReadOnlyCatalogKeysExist(t *testing.T) {
|
||||
"connection.backend.action.preview_result_changes",
|
||||
"connection.backend.action.clear_table",
|
||||
"connection.backend.action.truncate_table",
|
||||
"connection.backend.action.data_sync_structure",
|
||||
"connection.backend.action.data_sync_write",
|
||||
}
|
||||
|
||||
|
||||
@@ -22,18 +22,18 @@ func TestSupportsConnectionReadOnlyMode(t *testing.T) {
|
||||
|
||||
func TestEnsureReadOnlyConnectionAllowsQuery(t *testing.T) {
|
||||
sqlConfig := connection.ConnectionConfig{Type: "postgres", ReadOnly: true}
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(sqlConfig, "SELECT * FROM users"); err != nil {
|
||||
if err := ensureConnectionAllowsQuery(sqlConfig, "SELECT * FROM users"); err != nil {
|
||||
t.Fatalf("read-only postgres connection should allow select: %v", err)
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(sqlConfig, "UPDATE users SET name = 'next'"); err == nil {
|
||||
if err := ensureConnectionAllowsQuery(sqlConfig, "UPDATE users SET name = 'next'"); err == nil {
|
||||
t.Fatal("read-only postgres connection should block update")
|
||||
}
|
||||
|
||||
mongoConfig := connection.ConnectionConfig{Type: "mongodb", ReadOnly: true}
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(mongoConfig, `{"find":"users","filter":{"active":true}}`); err != nil {
|
||||
if err := ensureConnectionAllowsQuery(mongoConfig, `{"find":"users","filter":{"active":true}}`); err != nil {
|
||||
t.Fatalf("read-only mongodb connection should allow find: %v", err)
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(mongoConfig, `{"delete":"users","deletes":[{"q":{"active":false},"limit":0}]}`); err == nil {
|
||||
if err := ensureConnectionAllowsQuery(mongoConfig, `{"delete":"users","deletes":[{"q":{"active":false},"limit":0}]}`); err == nil {
|
||||
t.Fatal("read-only mongodb connection should block delete")
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func TestEnsureReadOnlyConnectionAllowsAction(t *testing.T) {
|
||||
})
|
||||
|
||||
config := connection.ConnectionConfig{Type: "postgres", ReadOnly: true}
|
||||
err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.drop_database")
|
||||
err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.drop_database")
|
||||
if err == nil {
|
||||
t.Fatal("read-only connection should block mutating actions")
|
||||
}
|
||||
@@ -53,3 +53,27 @@ func TestEnsureReadOnlyConnectionAllowsAction(t *testing.T) {
|
||||
t.Fatalf("blocked action message should include action label, got %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureConnectionProtectionSeparatesActionCategories(t *testing.T) {
|
||||
config := connection.ConnectionConfig{
|
||||
Type: "postgres",
|
||||
Protection: connection.ConnectionProtectionConfig{
|
||||
RestrictDataEdit: true,
|
||||
RestrictDataImport: true,
|
||||
RestrictStructureEdit: false,
|
||||
},
|
||||
}
|
||||
|
||||
if err := ensureConnectionAllowsQuery(config, "UPDATE users SET name = 'next'"); err != nil {
|
||||
t.Fatalf("script execution should remain allowed when only data-edit/import restrictions are enabled: %v", err)
|
||||
}
|
||||
if err := ensureConnectionAllowsDataEdit(config, "connection.backend.action.apply_result_changes"); err == nil {
|
||||
t.Fatal("data edit restriction should block result changes")
|
||||
}
|
||||
if err := ensureConnectionAllowsDataImport(config, "connection.backend.action.import_data"); err == nil {
|
||||
t.Fatal("data import restriction should block imports")
|
||||
}
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.drop_database"); err != nil {
|
||||
t.Fatalf("structure edits should remain allowed when structure restriction is disabled: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ func (a *App) CreateDatabase(config connection.ConnectionConfig, dbName string)
|
||||
if dbName == "" {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("db.backend.error.database_name_required", nil)}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.create_database"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.create_database"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
@@ -368,7 +368,7 @@ func resolveSchemaDDLTargetDatabaseWithText(config connection.ConnectionConfig,
|
||||
}
|
||||
|
||||
func (a *App) CreateSchema(config connection.ConnectionConfig, dbName string, schemaName string) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.create_schema"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.create_schema"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
dbType := resolveDDLDBType(config)
|
||||
@@ -396,7 +396,7 @@ func (a *App) CreateSchema(config connection.ConnectionConfig, dbName string, sc
|
||||
}
|
||||
|
||||
func (a *App) RenameSchema(config connection.ConnectionConfig, dbName string, oldSchemaName string, newSchemaName string) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.rename_schema"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.rename_schema"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
dbType := resolveDDLDBType(config)
|
||||
@@ -422,7 +422,7 @@ func (a *App) RenameSchema(config connection.ConnectionConfig, dbName string, ol
|
||||
}
|
||||
|
||||
func (a *App) DropSchema(config connection.ConnectionConfig, dbName string, schemaName string) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.drop_schema"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.drop_schema"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
dbType := resolveDDLDBType(config)
|
||||
@@ -680,7 +680,7 @@ func (a *App) RenameDatabase(config connection.ConnectionConfig, oldName string,
|
||||
if oldName == "" || newName == "" {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("db.backend.error.database_name_required", nil)}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.rename_database"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.rename_database"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
if strings.EqualFold(oldName, newName) {
|
||||
@@ -726,7 +726,7 @@ func (a *App) DropDatabase(config connection.ConnectionConfig, dbName string) co
|
||||
if dbName == "" {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("db.backend.error.database_name_required", nil)}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.drop_database"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.drop_database"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
@@ -763,7 +763,7 @@ func (a *App) RenameTable(config connection.ConnectionConfig, dbName string, old
|
||||
if oldTableName == "" || newTableName == "" {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("db.backend.error.table_name_required", nil)}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.rename_table"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.rename_table"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
if strings.EqualFold(oldTableName, newTableName) {
|
||||
@@ -818,7 +818,7 @@ func (a *App) DropTable(config connection.ConnectionConfig, dbName string, table
|
||||
if tableName == "" {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("db.backend.error.table_name_required", nil)}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.drop_table"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.drop_table"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
@@ -885,7 +885,7 @@ func (a *App) DBQueryWithCancel(config connection.ConnectionConfig, dbName strin
|
||||
}
|
||||
|
||||
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(config, query); err != nil {
|
||||
if err := ensureConnectionAllowsQuery(config, query); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
|
||||
}
|
||||
|
||||
@@ -1021,7 +1021,7 @@ func (a *App) DBQueryMulti(config connection.ConnectionConfig, dbName string, qu
|
||||
}
|
||||
|
||||
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(config, query); err != nil {
|
||||
if err := ensureConnectionAllowsQuery(config, query); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
|
||||
}
|
||||
|
||||
@@ -1429,7 +1429,7 @@ func (a *App) DBQueryIsolated(config connection.ConnectionConfig, dbName string,
|
||||
runConfig := normalizeRunConfig(config, dbName)
|
||||
|
||||
query = sanitizeSQLForPgLike(resolveDDLDBType(config), query)
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(config, query); err != nil {
|
||||
if err := ensureConnectionAllowsQuery(config, query); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
@@ -2318,7 +2318,7 @@ func (a *App) DropView(config connection.ConnectionConfig, dbName string, viewNa
|
||||
if viewName == "" {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("db.backend.error.view_name_required", nil)}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.drop_view"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.drop_view"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
@@ -2353,7 +2353,7 @@ func (a *App) DropFunction(config connection.ConnectionConfig, dbName string, ro
|
||||
if routineName == "" {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("db.backend.error.routine_name_required", nil)}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.drop_function_or_procedure"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.drop_function_or_procedure"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
if routineType != "FUNCTION" && routineType != "PROCEDURE" {
|
||||
@@ -2398,7 +2398,7 @@ func (a *App) RenameView(config connection.ConnectionConfig, dbName string, oldN
|
||||
if oldName == "" || newName == "" {
|
||||
return connection.QueryResult{Success: false, Message: a.appText("db.backend.error.view_name_required", nil)}
|
||||
}
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.rename_view"); err != nil {
|
||||
if err := ensureConnectionAllowsStructureEdit(config, "connection.backend.action.rename_view"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
if strings.EqualFold(oldName, newName) {
|
||||
|
||||
@@ -45,7 +45,7 @@ func (a *App) DBQueryMultiTransactional(config connection.ConnectionConfig, dbNa
|
||||
}
|
||||
|
||||
query = sanitizeSQLForPgLike(transactionDBType, query)
|
||||
if err := ensureReadOnlyConnectionAllowsQuery(config, query); err != nil {
|
||||
if err := ensureConnectionAllowsQuery(config, query); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error(), QueryID: queryID}
|
||||
}
|
||||
if !shouldUseManagedSQLTransaction(transactionDBType, query) {
|
||||
|
||||
@@ -1938,7 +1938,7 @@ func (a *App) PreviewImportFile(filePath string) connection.QueryResult {
|
||||
}
|
||||
|
||||
func (a *App) ImportData(config connection.ConnectionConfig, dbName, tableName string) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.import_data"); err != nil {
|
||||
if err := ensureConnectionAllowsDataImport(config, "connection.backend.action.import_data"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
@@ -2284,7 +2284,7 @@ func formatImportSQLValue(dbType, columnType string, value interface{}) string {
|
||||
|
||||
// ImportDataWithProgress 执行导入并发送进度事件
|
||||
func (a *App) ImportDataWithProgress(config connection.ConnectionConfig, dbName, tableName, filePath string) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.import_data"); err != nil {
|
||||
if err := ensureConnectionAllowsDataImport(config, "connection.backend.action.import_data"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
runConfig := normalizeRunConfig(config, dbName)
|
||||
@@ -2338,7 +2338,7 @@ func (a *App) ImportDataWithProgress(config connection.ConnectionConfig, dbName,
|
||||
}
|
||||
|
||||
func (a *App) ApplyChanges(config connection.ConnectionConfig, dbName, tableName string, changes connection.ChangeSet) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.apply_result_changes"); err != nil {
|
||||
if err := ensureConnectionAllowsDataEdit(config, "connection.backend.action.apply_result_changes"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
runConfig := normalizeRunConfig(config, dbName)
|
||||
@@ -2367,7 +2367,7 @@ type ChangePreview struct {
|
||||
}
|
||||
|
||||
func (a *App) PreviewChanges(config connection.ConnectionConfig, dbName, tableName string, changes connection.ChangeSet) connection.QueryResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, "connection.backend.action.preview_result_changes"); err != nil {
|
||||
if err := ensureConnectionAllowsDataEdit(config, "connection.backend.action.preview_result_changes"); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
runConfig := normalizeRunConfig(config, dbName)
|
||||
@@ -2903,7 +2903,7 @@ func tableDataClearMessageKeys(mode tableDataClearMode, partial bool) (failureKe
|
||||
|
||||
func (a *App) runTableDataClear(config connection.ConnectionConfig, dbName string, tableNames []string, mode tableDataClearMode) connection.QueryResult {
|
||||
actionLabel, progressLabel := tableDataClearActionLabels(mode)
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config, actionLabel); err != nil {
|
||||
if err := ensureConnectionAllowsDataEdit(config, actionLabel); err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
runConfig := normalizeRunConfig(config, dbName)
|
||||
|
||||
@@ -11,6 +11,29 @@ import (
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
func ensureDataSyncTargetProtection(config sync.SyncConfig) error {
|
||||
content := strings.ToLower(strings.TrimSpace(config.Content))
|
||||
strategy := strings.ToLower(strings.TrimSpace(config.TargetTableStrategy))
|
||||
touchesStructure := content == "schema" ||
|
||||
content == "both" ||
|
||||
config.AutoAddColumns ||
|
||||
config.CreateIndexes ||
|
||||
(strategy != "" && strategy != "existing_only")
|
||||
touchesData := content == "" || content == "data" || content == "both"
|
||||
|
||||
if touchesStructure {
|
||||
if err := ensureConnectionAllowsStructureEdit(config.TargetConfig, "connection.backend.action.data_sync_structure"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if touchesData {
|
||||
if err := ensureConnectionAllowsDataImport(config.TargetConfig, "connection.backend.action.data_sync_write"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) resolveDataSyncConfigSecrets(config sync.SyncConfig) (sync.SyncConfig, error) {
|
||||
resolved := config
|
||||
sourceConfig, sourceDatabase, err := a.resolveDataSyncEndpointConfig(config.SourceConfig, config.SourceDatabase)
|
||||
@@ -60,7 +83,7 @@ func (a *App) resolveDataSyncEndpointConfig(raw connection.ConnectionConfig, sel
|
||||
|
||||
// DataSync executes a data synchronization task
|
||||
func (a *App) DataSync(config sync.SyncConfig) sync.SyncResult {
|
||||
if err := ensureReadOnlyConnectionAllowsAction(config.TargetConfig, "connection.backend.action.data_sync_write"); err != nil {
|
||||
if err := ensureDataSyncTargetProtection(config); err != nil {
|
||||
return sync.SyncResult{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
|
||||
@@ -77,53 +77,62 @@ type JVMConfig struct {
|
||||
Diagnostic JVMDiagnosticConfig `json:"diagnostic,omitempty"`
|
||||
}
|
||||
|
||||
// ConnectionProtectionConfig 存储生产连接保护的细粒度限制项。
|
||||
type ConnectionProtectionConfig struct {
|
||||
RestrictDataEdit bool `json:"restrictDataEdit,omitempty"`
|
||||
RestrictStructureEdit bool `json:"restrictStructureEdit,omitempty"`
|
||||
RestrictScriptExecution bool `json:"restrictScriptExecution,omitempty"`
|
||||
RestrictDataImport bool `json:"restrictDataImport,omitempty"`
|
||||
}
|
||||
|
||||
// ConnectionConfig 存储数据库连接的完整配置,包括 SSH、代理、SSL 等网络层设置。
|
||||
type ConnectionConfig struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
SavePassword bool `json:"savePassword,omitempty"` // Persist password in saved connection
|
||||
Database string `json:"database"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"` // Production guard: allow query-only operations on supported databases
|
||||
UseSSL bool `json:"useSSL,omitempty"` // MySQL-like SSL/TLS switch
|
||||
SSLMode string `json:"sslMode,omitempty"` // preferred | required | skip-verify | disable
|
||||
SSLCAPath string `json:"sslCAPath,omitempty"` // TLS root CA / server certificate path
|
||||
SSLCertPath string `json:"sslCertPath,omitempty"` // TLS client certificate path (e.g., Dameng)
|
||||
SSLKeyPath string `json:"sslKeyPath,omitempty"` // TLS client private key path (e.g., Dameng)
|
||||
UseSSH bool `json:"useSSH"`
|
||||
SSH SSHConfig `json:"ssh"`
|
||||
UseProxy bool `json:"useProxy,omitempty"`
|
||||
Proxy ProxyConfig `json:"proxy,omitempty"`
|
||||
UseHTTPTunnel bool `json:"useHttpTunnel,omitempty"`
|
||||
HTTPTunnel HTTPTunnelConfig `json:"httpTunnel,omitempty"`
|
||||
Driver string `json:"driver,omitempty"` // For custom connection
|
||||
DSN string `json:"dsn,omitempty"` // For custom connection
|
||||
ConnectionParams string `json:"connectionParams,omitempty"` // Extra URI query parameters for built-in drivers
|
||||
Timeout int `json:"timeout,omitempty"` // Connection timeout in seconds (default: 30)
|
||||
KeepAliveEnabled bool `json:"keepAliveEnabled,omitempty"` // Enable background keep-alive ping for long-lived cached connections
|
||||
KeepAliveIntervalMinutes int `json:"keepAliveIntervalMinutes,omitempty"` // Keep-alive ping interval in minutes (default: 240)
|
||||
RedisDB int `json:"redisDB,omitempty"` // Redis database index (0-15)
|
||||
RedisSentinelMaster string `json:"redisSentinelMaster,omitempty"` // Redis Sentinel master name
|
||||
RedisSentinelUser string `json:"redisSentinelUser,omitempty"` // Redis Sentinel auth user
|
||||
RedisSentinelPassword string `json:"redisSentinelPassword,omitempty"` // Redis Sentinel auth password
|
||||
URI string `json:"uri,omitempty"` // Connection URI for copy/paste
|
||||
ClickHouseProtocol string `json:"clickHouseProtocol,omitempty"` // auto | http | native
|
||||
OceanBaseProtocol string `json:"oceanBaseProtocol,omitempty"` // OceanBase tenant compatibility protocol: mysql | oracle
|
||||
Hosts []string `json:"hosts,omitempty"` // Multi-host addresses: host:port
|
||||
Topology string `json:"topology,omitempty"` // single | replica | cluster | sentinel
|
||||
MySQLReplicaUser string `json:"mysqlReplicaUser,omitempty"` // MySQL replica auth user
|
||||
MySQLReplicaPassword string `json:"mysqlReplicaPassword,omitempty"` // MySQL replica auth password
|
||||
ReplicaSet string `json:"replicaSet,omitempty"` // MongoDB replica set name
|
||||
AuthSource string `json:"authSource,omitempty"` // MongoDB authSource
|
||||
ReadPreference string `json:"readPreference,omitempty"` // MongoDB readPreference
|
||||
MongoSRV bool `json:"mongoSrv,omitempty"` // MongoDB use mongodb+srv URI scheme
|
||||
MongoAuthMechanism string `json:"mongoAuthMechanism,omitempty"` // MongoDB authMechanism
|
||||
MongoReplicaUser string `json:"mongoReplicaUser,omitempty"` // MongoDB replica auth user
|
||||
MongoReplicaPassword string `json:"mongoReplicaPassword,omitempty"` // MongoDB replica auth password
|
||||
JVM JVMConfig `json:"jvm,omitempty"` // JVM connector config
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
SavePassword bool `json:"savePassword,omitempty"` // Persist password in saved connection
|
||||
Database string `json:"database"`
|
||||
ReadOnly bool `json:"readOnly,omitempty"` // Legacy production guard compatibility flag. Prefer Protection for new logic.
|
||||
Protection ConnectionProtectionConfig `json:"protection,omitempty"`
|
||||
UseSSL bool `json:"useSSL,omitempty"` // MySQL-like SSL/TLS switch
|
||||
SSLMode string `json:"sslMode,omitempty"` // preferred | required | skip-verify | disable
|
||||
SSLCAPath string `json:"sslCAPath,omitempty"` // TLS root CA / server certificate path
|
||||
SSLCertPath string `json:"sslCertPath,omitempty"` // TLS client certificate path (e.g., Dameng)
|
||||
SSLKeyPath string `json:"sslKeyPath,omitempty"` // TLS client private key path (e.g., Dameng)
|
||||
UseSSH bool `json:"useSSH"`
|
||||
SSH SSHConfig `json:"ssh"`
|
||||
UseProxy bool `json:"useProxy,omitempty"`
|
||||
Proxy ProxyConfig `json:"proxy,omitempty"`
|
||||
UseHTTPTunnel bool `json:"useHttpTunnel,omitempty"`
|
||||
HTTPTunnel HTTPTunnelConfig `json:"httpTunnel,omitempty"`
|
||||
Driver string `json:"driver,omitempty"` // For custom connection
|
||||
DSN string `json:"dsn,omitempty"` // For custom connection
|
||||
ConnectionParams string `json:"connectionParams,omitempty"` // Extra URI query parameters for built-in drivers
|
||||
Timeout int `json:"timeout,omitempty"` // Connection timeout in seconds (default: 30)
|
||||
KeepAliveEnabled bool `json:"keepAliveEnabled,omitempty"` // Enable background keep-alive ping for long-lived cached connections
|
||||
KeepAliveIntervalMinutes int `json:"keepAliveIntervalMinutes,omitempty"` // Keep-alive ping interval in minutes (default: 240)
|
||||
RedisDB int `json:"redisDB,omitempty"` // Redis database index (0-15)
|
||||
RedisSentinelMaster string `json:"redisSentinelMaster,omitempty"` // Redis Sentinel master name
|
||||
RedisSentinelUser string `json:"redisSentinelUser,omitempty"` // Redis Sentinel auth user
|
||||
RedisSentinelPassword string `json:"redisSentinelPassword,omitempty"` // Redis Sentinel auth password
|
||||
URI string `json:"uri,omitempty"` // Connection URI for copy/paste
|
||||
ClickHouseProtocol string `json:"clickHouseProtocol,omitempty"` // auto | http | native
|
||||
OceanBaseProtocol string `json:"oceanBaseProtocol,omitempty"` // OceanBase tenant compatibility protocol: mysql | oracle
|
||||
Hosts []string `json:"hosts,omitempty"` // Multi-host addresses: host:port
|
||||
Topology string `json:"topology,omitempty"` // single | replica | cluster | sentinel
|
||||
MySQLReplicaUser string `json:"mysqlReplicaUser,omitempty"` // MySQL replica auth user
|
||||
MySQLReplicaPassword string `json:"mysqlReplicaPassword,omitempty"` // MySQL replica auth password
|
||||
ReplicaSet string `json:"replicaSet,omitempty"` // MongoDB replica set name
|
||||
AuthSource string `json:"authSource,omitempty"` // MongoDB authSource
|
||||
ReadPreference string `json:"readPreference,omitempty"` // MongoDB readPreference
|
||||
MongoSRV bool `json:"mongoSrv,omitempty"` // MongoDB use mongodb+srv URI scheme
|
||||
MongoAuthMechanism string `json:"mongoAuthMechanism,omitempty"` // MongoDB authMechanism
|
||||
MongoReplicaUser string `json:"mongoReplicaUser,omitempty"` // MongoDB replica auth user
|
||||
MongoReplicaPassword string `json:"mongoReplicaPassword,omitempty"` // MongoDB replica auth password
|
||||
JVM JVMConfig `json:"jvm,omitempty"` // JVM connector config
|
||||
}
|
||||
|
||||
// ResultSetData 表示一个查询结果集(行 + 列名),用于多结果集场景。
|
||||
|
||||
Reference in New Issue
Block a user