mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-25 16:04:02 +08:00
316 lines
12 KiB
Go
316 lines
12 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
|
|
"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": {},
|
|
"diros": {},
|
|
"duckdb": {},
|
|
"gaussdb": {},
|
|
"highgo": {},
|
|
"iris": {},
|
|
"kingbase": {},
|
|
"mariadb": {},
|
|
"mongodb": {},
|
|
"mysql": {},
|
|
"oceanbase": {},
|
|
"opengauss": {},
|
|
"oracle": {},
|
|
"postgres": {},
|
|
"sphinx": {},
|
|
"sqlite": {},
|
|
"sqlserver": {},
|
|
"starrocks": {},
|
|
"tdengine": {},
|
|
"trino": {},
|
|
"vastbase": {},
|
|
}
|
|
|
|
var mongoReadOnlyCommands = map[string]struct{}{
|
|
"aggregate": {},
|
|
"buildinfo": {},
|
|
"collstats": {},
|
|
"connectionstatus": {},
|
|
"count": {},
|
|
"countdocuments": {},
|
|
"dbstats": {},
|
|
"distinct": {},
|
|
"explain": {},
|
|
"find": {},
|
|
"findone": {},
|
|
"getparameter": {},
|
|
"hello": {},
|
|
"hostinfo": {},
|
|
"ismaster": {},
|
|
"listcollections": {},
|
|
"listdatabases": {},
|
|
"listindexes": {},
|
|
"ping": {},
|
|
"serverstatus": {},
|
|
}
|
|
|
|
var mongoWriteCommands = map[string]struct{}{
|
|
"bulkwrite": {},
|
|
"collmod": {},
|
|
"create": {},
|
|
"createindexes": {},
|
|
"delete": {},
|
|
"drop": {},
|
|
"dropdatabase": {},
|
|
"dropindexes": {},
|
|
"findandmodify": {},
|
|
"insert": {},
|
|
"mapreduce": {},
|
|
"renamecollection": {},
|
|
"update": {},
|
|
}
|
|
|
|
var mongoMetaCommandKeys = map[string]struct{}{
|
|
"$db": {},
|
|
"$readpreference": {},
|
|
"api": {},
|
|
"apideprecationerrors": {},
|
|
"apistrict": {},
|
|
"comment": {},
|
|
"let": {},
|
|
"lsid": {},
|
|
"maxtimems": {},
|
|
"ordered": {},
|
|
"readconcern": {},
|
|
"writeconcern": {},
|
|
}
|
|
|
|
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.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_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 {
|
|
_, ok := connectionReadOnlySupportedTypes[resolveDDLDBType(config)]
|
|
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 {
|
|
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 {
|
|
if text != nil {
|
|
return text
|
|
}
|
|
return defaultAppText
|
|
}
|
|
|
|
func readOnlyConnectionQueryBlockedMessageWithText(text func(string, map[string]any) string) string {
|
|
text = normalizeReadOnlyConnectionText(text)
|
|
return text("query_editor.message.connection_readonly_blocked", nil)
|
|
}
|
|
|
|
func resolveReadOnlyConnectionActionLabel(action string, text func(string, map[string]any) string) string {
|
|
label := strings.TrimSpace(action)
|
|
if label == "" {
|
|
return ""
|
|
}
|
|
text = normalizeReadOnlyConnectionText(text)
|
|
if key, ok := readOnlyConnectionActionTextKeys[label]; ok {
|
|
return text(key, nil)
|
|
}
|
|
return label
|
|
}
|
|
|
|
func readOnlyConnectionQueryBlockedMessage() string {
|
|
return readOnlyConnectionQueryBlockedMessageWithText(defaultAppText)
|
|
}
|
|
|
|
func readOnlyConnectionActionBlockedMessageWithText(action string, text func(string, map[string]any) string) string {
|
|
label := resolveReadOnlyConnectionActionLabel(action, text)
|
|
if label == "" {
|
|
return readOnlyConnectionQueryBlockedMessageWithText(text)
|
|
}
|
|
text = normalizeReadOnlyConnectionText(text)
|
|
return text("connection.backend.error.readonly_action_blocked", map[string]any{"action": label})
|
|
}
|
|
|
|
func readOnlyConnectionActionBlockedMessage(action string) string {
|
|
return readOnlyConnectionActionBlockedMessageWithText(action, defaultAppText)
|
|
}
|
|
|
|
func ensureConnectionAllowsQueryWithText(config connection.ConnectionConfig, query string, text func(string, map[string]any) string) error {
|
|
text = normalizeReadOnlyConnectionText(text)
|
|
if !isConnectionScriptExecutionRestricted(config) {
|
|
return nil
|
|
}
|
|
for _, statement := range splitSQLStatements(query) {
|
|
if trimmed := strings.TrimSpace(statement); trimmed != "" && !isReadOnlySQLQuery(resolveDDLDBType(config), trimmed) {
|
|
return errors.New(readOnlyConnectionQueryBlockedMessageWithText(text))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ensureConnectionAllowsQuery(config connection.ConnectionConfig, query string) error {
|
|
return ensureConnectionAllowsQueryWithText(config, query, defaultAppText)
|
|
}
|
|
|
|
func ensureConnectionAllowsActionWithText(config connection.ConnectionConfig, key connectionProtectionKey, action string, text func(string, map[string]any) string) error {
|
|
text = normalizeReadOnlyConnectionText(text)
|
|
if !isConnectionProtectionEnabled(config, key) {
|
|
return nil
|
|
}
|
|
return errors.New(readOnlyConnectionActionBlockedMessageWithText(action, text))
|
|
}
|
|
|
|
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 {
|
|
trimmed := strings.TrimSpace(query)
|
|
if !strings.HasPrefix(trimmed, "{") {
|
|
return false
|
|
}
|
|
var doc map[string]interface{}
|
|
if err := json.Unmarshal([]byte(trimmed), &doc); err != nil {
|
|
return false
|
|
}
|
|
commandKey := resolveMongoCommandKey(doc)
|
|
if commandKey == "" {
|
|
return false
|
|
}
|
|
if _, blocked := mongoWriteCommands[commandKey]; blocked {
|
|
return false
|
|
}
|
|
_, allowed := mongoReadOnlyCommands[commandKey]
|
|
return allowed
|
|
}
|
|
|
|
func resolveMongoCommandKey(doc map[string]interface{}) string {
|
|
commandKey := ""
|
|
for key := range doc {
|
|
normalized := strings.ToLower(strings.TrimSpace(key))
|
|
if normalized == "" {
|
|
continue
|
|
}
|
|
if _, isMeta := mongoMetaCommandKeys[normalized]; isMeta {
|
|
continue
|
|
}
|
|
if _, isWrite := mongoWriteCommands[normalized]; isWrite {
|
|
return normalized
|
|
}
|
|
if _, isRead := mongoReadOnlyCommands[normalized]; isRead {
|
|
commandKey = normalized
|
|
}
|
|
}
|
|
return commandKey
|
|
}
|