mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-05 05:18:05 +08:00
- 新增数据编辑、结构编辑、脚本执行和数据导入四类连接级保护配置 - 升级生产连接保护弹窗为多选卡片,并修复选项对齐与勾选态显示 - 按保护类型收口 QueryEditor、DataGrid、表设计、导入与同步目标入口 - 后端统一拦截 SQL 或 Mongo 写操作、结果编辑、结构变更和导入写入 - AI 本地工具与 RPC 执行链路透传连接保护配置并复用后端守卫 - 补充多语言文案、定向测试与需求追踪记录
240 lines
6.7 KiB
Go
240 lines
6.7 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"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": {},
|
|
}
|
|
|
|
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 readOnlyConnectionQueryBlockedMessage() string {
|
|
return "当前连接已启用生产保护,仅允许执行查询操作"
|
|
}
|
|
|
|
func readOnlyConnectionActionBlockedMessage(action string) string {
|
|
label := strings.TrimSpace(action)
|
|
if label == "" {
|
|
return readOnlyConnectionQueryBlockedMessage()
|
|
}
|
|
return fmt.Sprintf("当前连接已启用生产保护,禁止执行%s", label)
|
|
}
|
|
|
|
func ensureConnectionAllowsQuery(config connection.ConnectionConfig, query string) error {
|
|
if !isConnectionScriptExecutionRestricted(config) {
|
|
return nil
|
|
}
|
|
for _, statement := range splitSQLStatements(query) {
|
|
if trimmed := strings.TrimSpace(statement); trimmed != "" && !isReadOnlySQLQuery(resolveDDLDBType(config), trimmed) {
|
|
return errors.New(readOnlyConnectionQueryBlockedMessage())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ensureConnectionAllowsAction(config connection.ConnectionConfig, key connectionProtectionKey, action string) error {
|
|
if !isConnectionProtectionEnabled(config, key) {
|
|
return nil
|
|
}
|
|
return errors.New(readOnlyConnectionActionBlockedMessage(action))
|
|
}
|
|
|
|
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
|
|
}
|