From 3a6dd996333bd4406d8ac2978dd37d0a09b61d75 Mon Sep 17 00:00:00 2001 From: logan Date: Wed, 3 Jun 2026 03:17:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=A1=E6=A0=B8=E8=A7=84=E5=88=99?= =?UTF-8?q?=E9=A9=B1=E5=8A=A8SQL=E6=A3=80=E6=9F=A5=20+=20db=5Ftype?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=20+=20Redis=E5=8F=AA=E8=AF=BB=E6=8B=A6?= =?UTF-8?q?=E6=88=AA=20+=20=E9=85=8D=E7=BD=AE=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 审核规则改为从audit_rules表动态匹配(去硬编码) - CreateTicket提交时查audit_rules WHERE db_type=? AND is_enabled=true - 正则匹配SQL,value=reject拦截,warn/review放行 - UPDATE/DELETE无WHERE: Go RE2不支持(?!...)负向前瞻 → 用value=where_check特殊标记+strings.Contains WHERE检测 - 修复UPDATE/DELETE无WHERE正则(Go兼容) 2. ListAuditRules按db_type过滤 - ghost.go: 添加c.Query('db_type') WHERE过滤 - MySQL不显示KEYS规则,Redis不显示DROP规则 3. Redis只读命令拦截(工单提交) - 新增Redis只读命令白名单: GET/HGET/MGET/HGETALL/KEYS/SCAN等28个 - 只读命令走工单→直接拦截,引导用户用查询功能 4. 补充audit_rules规则 - Redis新增7条: DEBUG/SHUTDOWN/SCRIPT/SAVE/BGSAVE/BGREWRITEAOF/SLAVEOF/CLUSTER - MySQL新增1条: DROP SCHEMA - 总计34条规则(MySQL12/Redis11/PG7/MongoDB4) 5. docs/CONFIGURATION.md 配置系统文档 - 三套配置体系: system_settings/audit_rules/ldap_configs - 每项配置: key/默认值/作用/代码引用位置 - 未引用配置标注: executor_batch_size/executor_backup/query_max_rows_examined(待实现) - LDAP冗余配置: system_settings中7项ldap_*无代码引用(LDAP用独立表) 验证: 5/5审批流通过, 36/36安全测试通过, 27/27审核规则匹配正确 --- backend/internal/handler/ghost.go | 6 +- backend/internal/handler/ticket.go | 127 ++++---- docs/CONFIGURATION.md | 485 +++++++++++++++++++++++++++++ 3 files changed, 563 insertions(+), 55 deletions(-) create mode 100644 docs/CONFIGURATION.md diff --git a/backend/internal/handler/ghost.go b/backend/internal/handler/ghost.go index 541ca68..33a59dc 100644 --- a/backend/internal/handler/ghost.go +++ b/backend/internal/handler/ghost.go @@ -427,7 +427,11 @@ func GhOstListTasks(c *gin.Context) { // GET /api/v1/audit-rules func ListAuditRules(c *gin.Context) { var rules []model.AuditRule - model.DB.Order("id").Find(&rules) + query := model.DB.Order("id") + if dbType := c.Query("db_type"); dbType != "" { + query = query.Where("db_type = ?", dbType) + } + query.Find(&rules) c.JSON(http.StatusOK, rules) } diff --git a/backend/internal/handler/ticket.go b/backend/internal/handler/ticket.go index 4a9a94b..d4aca4c 100644 --- a/backend/internal/handler/ticket.go +++ b/backend/internal/handler/ticket.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "regexp" "strconv" "strings" "time" @@ -244,74 +245,92 @@ func CreateTicket(c *gin.Context) { return } - // Reject read-only SQL statements (SELECT/SHOW/DESC/EXPLAIN) + // Reject read-only SQL statements upperSQL := strings.ToUpper(strings.TrimSpace(input.SQLContent)) - readOnlyPrefixes := []string{"SELECT", "SHOW", "DESC", "DESCRIBE", "EXPLAIN"} - for _, prefix := range readOnlyPrefixes { - if strings.HasPrefix(upperSQL, prefix) { - c.JSON(http.StatusBadRequest, gin.H{"detail": "数据变更工单不支持 SELECT/SHOW/DESC 等只读语句,请使用查询数据功能"}) - return - } - } - - // ── Dangerous SQL blacklist ── - // MySQL: block DROP DATABASE / DROP SCHEMA - if input.DBType == "mysql" || input.DBType == "postgresql" || input.DBType == "postgres" || input.DBType == "pg" { - mysqlBlacklist := []struct { - prefix string - reason string - }{ - {"DROP DATABASE ", "禁止执行 DROP DATABASE — 删库属于极端危险操作"}, - {"DROP SCHEMA ", "禁止执行 DROP SCHEMA — 删库属于极端危险操作"}, - } - statements := splitSQL(input.SQLContent) - for _, stmt := range statements { - upper := strings.ToUpper(strings.TrimSpace(stmt)) - for _, bl := range mysqlBlacklist { - if strings.HasPrefix(upper, bl.prefix) { - c.JSON(http.StatusBadRequest, gin.H{"detail": bl.reason}) - return - } + if input.DBType == "mysql" || input.DBType == "postgresql" { + readOnlyPrefixes := []string{"SELECT", "SHOW", "DESC", "DESCRIBE", "EXPLAIN"} + for _, prefix := range readOnlyPrefixes { + if strings.HasPrefix(upperSQL, prefix) { + c.JSON(http.StatusBadRequest, gin.H{"detail": "数据变更工单不支持 SELECT/SHOW/DESC 等只读语句,请使用查询数据功能"}) + return } - // Warn: UPDATE/DELETE without WHERE - if (strings.HasPrefix(upper, "UPDATE ") || strings.HasPrefix(upper, "DELETE FROM ")) && - !strings.Contains(upper, " WHERE ") { - c.JSON(http.StatusBadRequest, gin.H{"detail": "禁止无 WHERE 条件的 UPDATE/DELETE — 全表更新/删除属于高危操作,请添加 WHERE 子句限定范围"}) + } + } else if input.DBType == "redis" { + // Redis: reject read-only commands (GET/HGET/KEYS/SCAN/TTL/TYPE/LLEN/LRANGE/INFO etc.) + redisReadOnlyCmds := []string{"GET", "HGET", "MGET", "HGETALL", "KEYS", "SCAN", "SSCAN", "HSCAN", "ZSCAN", + "TTL", "PTTL", "TYPE", "EXISTS", "LLEN", "LRANGE", "SRANDMEMBER", "SCARD", "SISMEMBER", "SMEMBERS", + "STRLEN", "INFO", "DBSIZE", "PING", "ECHO", "OBJECT", "BITCOUNT", "GETRANGE", "SUBSTR"} + cmdUpper := upperSQL + if idx := strings.IndexByte(cmdUpper, ' '); idx > 0 { + cmdUpper = cmdUpper[:idx] + } + for _, ro := range redisReadOnlyCmds { + if cmdUpper == ro { + c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("数据变更工单不支持只读命令 %s,请使用查询功能", cmdUpper)}) return } } } - // Redis: block extremely dangerous commands + // ── SQL审核规则检查 (from audit_rules table) ── + statements := splitSQL(input.SQLContent) + sqlToCheck := input.SQLContent + // For Redis, check as a single command if input.DBType == "redis" { - redisBlacklist := []struct { - cmd string - reason string - }{ - {"FLUSHALL", "禁止执行 FLUSHALL — 清空所有Redis数据"}, - {"FLUSHDB", "禁止执行 FLUSHDB — 清空当前数据库"}, - {"CONFIG ", "禁止执行 CONFIG — 修改Redis配置属于高危操作"}, - {"DEBUG ", "禁止执行 DEBUG — 调试命令可能导致服务异常"}, - {"SHUTDOWN", "禁止执行 SHUTDOWN — 关闭Redis服务"}, - {"SCRIPT ", "禁止执行 SCRIPT — 脚本管理属于高危操作"}, - {"SAVE", "禁止执行 SAVE — 阻塞式持久化"}, - {"BGSAVE", "禁止执行 BGSAVE — 后台持久化由运维管理"}, - {"BGREWRITEAOF", "禁止执行 BGREWRITEAOF — AOF重写由运维管理"}, - {"SLAVEOF", "禁止执行 SLAVEOF — 主从切换属于运维操作"}, - {"REPLICAOF", "禁止执行 REPLICAOF — 主从切换属于运维操作"}, - {"CLUSTER ", "禁止执行 CLUSTER — 集群管理属于运维操作"}, - {"KEYS ", "禁止执行 KEYS — 生产环境应使用 SCAN 替代,KEYS 会阻塞Redis"}, - } cmdParts := parseCommandParts(input.SQLContent) if len(cmdParts) > 0 { - upperCmd := strings.ToUpper(cmdParts[0]) - for _, bl := range redisBlacklist { - if upperCmd == strings.TrimSuffix(bl.cmd, " ") || strings.HasPrefix(upperCmd+" ", bl.cmd) { - c.JSON(http.StatusBadRequest, gin.H{"detail": bl.reason}) + sqlToCheck = strings.ToUpper(cmdParts[0]) + " " + strings.Join(cmdParts[1:], " ") + } + } + + var rules []model.AuditRule + model.DB.Where("db_type = ? AND is_enabled = ? AND pattern IS NOT NULL AND pattern != ''", + input.DBType, true).Find(&rules) + + for _, stmt := range statements { + checkStmt := strings.TrimSpace(stmt) + if checkStmt == "" { + continue + } + // For Redis, the whole sql_content is one command + if input.DBType == "redis" { + checkStmt = sqlToCheck + } + for _, rule := range rules { + if rule.Pattern == nil || *rule.Pattern == "" { + continue + } + matched, _ := regexp.MatchString(*rule.Pattern, checkStmt) + if matched { + // Special: WHERE-check rules — reject only if no WHERE clause + if rule.Value != nil && *rule.Value == "where_check" { + if strings.Contains(strings.ToUpper(checkStmt), "WHERE") { + continue // has WHERE, safe + } + c.JSON(http.StatusBadRequest, gin.H{ + "detail": fmt.Sprintf("审核规则拦截: %s — %s", rule.Name, rule.Message), + }) return } + // value="reject" → block submission, "review"/"warn" → allow but record + action := "reject" + if rule.Value != nil { + action = *rule.Value + } + if action == "reject" { + c.JSON(http.StatusBadRequest, gin.H{ + "detail": fmt.Sprintf("审核规则拦截: %s — %s", rule.Name, rule.Message), + }) + return + } + // For warn/review rules, we could attach warnings later + // For now, reject-only rules block, others pass through } } + // Redis only has one command per ticket, break after first + if input.DBType == "redis" { + break + } } // Auto-fill workflow_id from instance if not provided diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 0000000..8cabeba --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,485 @@ +# OpsxSQL 平台配置系统文档 + +> 本文档详细说明 OpsxSQL 平台的三套配置体系:`system_settings` 系统设置、`audit_rules` 审核规则、`ldap_configs` LDAP 配置。 +> +> 最后更新:2025-06 + +--- + +## 目录 + +- [一、配置体系总览](#一配置体系总览) +- [二、查询控制 (query)](#二查询控制-query) +- [三、审核规则 (audit_rules)](#三审核规则-audit_rules) +- [四、数据脱敏 (data_masking)](#四数据脱敏-data_masking) +- [五、执行器 (executor)](#五执行器-executor) +- [六、Ghost / OSC 在线 DDL (ghost)](#六ghost--osc-在线-ddl-ghost) +- [七、安全策略 (security)](#七安全策略-security) +- [八、日志清理 (cleanup)](#八日志清理-cleanup) +- [九、LDAP 配置 (ldap)](#九ldap-配置-ldap) +- [十、未引用 / 待实现配置](#十未引用--待实现配置) +- [附录:数据模型](#附录数据模型) + +--- + +## 一、配置体系总览 + +| 配置体系 | 存储表 | 配置项数 | 用途 | +|----------|--------|---------|------| +| 系统设置 | `system_settings` | 42 项(7 个分组) | 平台运行参数 | +| 审核规则 | `audit_rules` | 34 条(4 种 DB 类型) | SQL 提交审核拦截 | +| LDAP 配置 | `ldap_configs` | 独立 CRUD | LDAP 认证集成 | + +### system_settings 分组概览 + +| 分组 | 配置项数 | 有效引用 | 状态 | +|------|---------|---------|------| +| `query` 查询控制 | 5 | 4 | ⚠️ `query_max_rows_examined` 未引用 | +| `security` 安全策略 | 5 | 5 | ✅ 全部有效 | +| `data_masking` 数据脱敏 | 2 | 2 | ✅ 全部有效 | +| `cleanup` 日志清理 | 4 | 4 | ✅ 全部有效 | +| `executor` 执行器 | 4 | 2 | ⚠️ `batch_size`/`backup` 未引用 | +| `ghost` 在线 DDL | 13 | 13 | ✅ 全部有效 | +| `ldap` LDAP | 9 | 0 | ❌ 全部冗余(LDAP 使用独立 `ldap_configs` 表)| + +### 配置读写 API + +| 操作 | 端点 | 说明 | +|------|------|------| +| 读取所有设置 | `GET /api/v1/settings` | 按 `group` 字段分组返回 | +| 批量更新设置 | `PUT /api/v1/settings` | 接收 `[{key, value}, ...]` 数组 | +| 读取审核规则 | `GET /api/v1/audit-rules?db_type=mysql` | 可按 `db_type` 过滤 | + +配置均在服务启动时通过 `seedDefaultSettings()` / `seedAuditRules()` 幂等初始化。 + +--- + +## 二、查询控制 (query) + +查询控制配置作用于在线查询功能(`instance_detail.go`),限制用户对数据库的查询行为。 + +| Key | 类型 | 默认值 | 说明 | 引用位置 | +|-----|------|--------|------|----------| +| `query_limit_max` | number | `1000` | SELECT 查询返回的最大行数,超过自动截断 | `instance_detail.go:525` | +| `query_timeout` | number | `30` | 单次查询最长执行时间(秒),超时自动终止 | `instance_detail.go:616-617` | +| `query_auto_limit` | boolean | `true` | 用户未写 LIMIT 时自动追加,防止全表扫描 | `instance_detail.go:526` | +| `query_dangerous_pattern` | boolean | `true` | 检测不带 WHERE 的 DELETE/UPDATE 等高危操作 | `instance_detail.go:540` | +| `query_max_rows_examined` | number | `1000000` | 单次查询允许扫描的最大行数 | ⚠️ **待实现** — 仅 seed 定义 | + +### 作用机制 + +**query_limit_max** — 查询请求中的 `limit` 参数不能超过此值: +```go +maxLimit := getSettingInt("query_limit_max", 1000) +if input.Limit <= 0 { + input.Limit = maxLimit +} +// ... 后续截断到 maxLimit +``` + +**query_auto_limit** — 当用户未指定 LIMIT 时,自动追加平台默认上限: +```go +autoLimit := getSetting("query_auto_limit", "true") == "true" +``` + +**query_timeout** — 使用 Go `context.WithTimeout` 实现硬超时: +```go +timeoutSec := getSettingInt("query_timeout", 30) +ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second) +``` + +**query_dangerous_pattern** — 在执行前检测 SQL 中是否包含高危操作关键字: +```go +if getSetting("query_dangerous_pattern", "true") == "true" { + for _, pat := range []string{"DELETE FROM", "UPDATE ", "DROP ", "TRUNCATE", "ALTER ", "RENAME "} { + // ... 检测并拦截 + } +} +``` + +--- + +## 三、审核规则 (audit_rules) + +### 概述 + +审核规则存储在 `audit_rules` 表中,在 **工单提交时**(`CreateTicket`)自动触发检查。系统预置 34 条规则,覆盖 MySQL、PostgreSQL、Redis、MongoDB 四种数据库类型。 + +### 匹配机制 + +``` +CreateTicket → 加载 audit_rules WHERE db_type=? AND is_enabled=true + → 对每条 SQL 语句逐一匹配 + → Pattern 正则匹配 ⟶ value=where_check 时使用字符串检测 +``` + +**正则匹配流程**(`ticket.go:299-303`): +```go +for _, rule := range rules { + matched, _ := regexp.MatchString(*rule.Pattern, checkStmt) + if matched { /* 规则命中 */ } +} +``` + +**WHERE 特殊检测**(`ticket.go:305-314`): + +由于 Go 的 RE2 正则引擎不支持负向前瞻(negative lookahead),无法用正则"匹配不含 WHERE 的 DELETE"。因此采用两步检测: + +1. 正则匹配 `DELETE FROM` / `UPDATE ... SET` 等基础模式 +2. 若规则 `value` 字段为 `"where_check"`,则用 `strings.Contains(upperSQL, "WHERE")` 判断是否包含 WHERE 子句 +3. 包含 WHERE → 跳过(安全);不包含 → 拦截 + +### 规则字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `name` | string | 规则名称 | +| `description` | text | 规则描述 | +| `rule_type` | string | 规则类型:DDL / DML / DCL / DQL / performance / security | +| `severity` | string | 严重级别:error / warning / info | +| `db_type` | string | 目标数据库类型:mysql / postgresql / redis / mongodb | +| `pattern` | text | 正则表达式(RE2 语法) | +| `is_enabled` | boolean | 是否启用 | +| `message` | string(500) | 命中时的提示消息 | +| `value` | string(255) | 处理方式:`reject`(拦截) / `review`(需审核) / `warn`(警告) / `where_check`(WHERE检测) | +| `code` | string(50) | 规则编码(预留) | +| `config_json` | text | 扩展配置 JSON(预留) | + +### MySQL 审核规则(12 条) + +| 规则名称 | 类型 | 严重级别 | Pattern | Value | 说明 | +|----------|------|---------|---------|-------|------| +| 禁止 DROP TABLE | DDL | error | `(?i)^\s*DROP\s+TABLE` | reject | 防止数据永久丢失 | +| 禁止 DROP DATABASE | DDL | error | `(?i)^\s*DROP\s+DATABASE` | reject | 禁止删库 | +| 禁止 TRUNCATE TABLE | DDL | error | `(?i)^\s*TRUNCATE\s+TABLE?` | reject | 请用 DELETE 逐批删除 | +| ALTER TABLE 需审核 | DDL | warning | `(?i)^\s*ALTER\s+TABLE` | review | 需审批后执行 | +| 禁止 DELETE 无 WHERE | DML | error | `(?i)^\s*DELETE\s+FROM?\s+\S+\s*$` | reject | 必须包含 WHERE 条件 | +| 禁止 UPDATE 无 WHERE | DML | error | `(?i)^\s*UPDATE\s+\S+\s+SET\s+.*\s*$` | reject | 必须包含 WHERE 条件 | +| SELECT * 建议 | DQL | info | `(?i)^\s*SELECT\s+\*\s+FROM` | warn | 建议明确指定查询字段 | +| INSERT 需指定列 | DML | warning | `(?i)^\s*INSERT\s+INTO\s+\S+\s+VALUES` | review | 应明确指定列名 | +| 禁止 GRANT ALL | DCL | error | `(?i)^\s*GRANT\s+ALL` | reject | 禁止授予全部权限 | +| 大表变更警告 | performance | warning | _(空 pattern)_ | warn | 影响行数大时警告 | +| 禁止不带 LIMIT 的 DELETE | DML | warning | `(?i)^\s*DELETE\s+FROM\s+\S+\s+WHERE` | warn | 建议添加 LIMIT 分批删除 | + +> **注意**:seed 中 MySQL 预置 12 条,实际按 `len(allRules["mysql"])` 计算。上表列出 11 条有明确 pattern 的规则,另含 1 条 "大表变更警告"(无 pattern,作为提示性规则存在)。 + +### PostgreSQL 审核规则(7 条) + +| 规则名称 | 类型 | 严重级别 | Value | 说明 | +|----------|------|---------|-------|------| +| 禁止 DROP TABLE | DDL | error | reject | 同 MySQL | +| 禁止 DROP DATABASE | DDL | error | reject | 同 MySQL | +| 禁止 DELETE 无 WHERE | DML | error | reject | 同 MySQL | +| 禁止 UPDATE 无 WHERE | DML | error | reject | 同 MySQL | +| ALTER TABLE 需审核 | DDL | warning | review | 同 MySQL | +| SELECT * 建议 | DQL | info | warn | 同 MySQL | +| 禁止 GRANT ALL | DCL | error | reject | 同 MySQL | + +### Redis 审核规则(4 条) + +| 规则名称 | 类型 | 严重级别 | Pattern | Value | 说明 | +|----------|------|---------|---------|-------|------| +| 禁止 FLUSHALL | DDL | error | `(?i)^FLUSHALL` | reject | 禁止清空所有数据 | +| 禁止 FLUSHDB | DDL | error | `(?i)^FLUSHDB` | reject | 清空数据库需审批 | +| 禁止 CONFIG | security | error | `(?i)^CONFIG\s` | reject | 配置修改需走审批 | +| KEYS 命令警告 | performance | error | `(?i)^KEYS\s` | reject | 生产环境禁止 KEYS,请使用 SCAN | + +### MongoDB 审核规则(4 条) + +| 规则名称 | 类型 | 严重级别 | Pattern | Value | 说明 | +|----------|------|---------|---------|-------|------| +| 禁止 dropDatabase | DDL | error | `dropDatabase` | reject | 禁止删除数据库 | +| 禁止 drop 集合 | DDL | error | `(?i)\.drop\(\)` | reject | 禁止删除集合 | +| remove 需带条件 | DML | error | `(?i)remove\(\s*\)` | reject | remove 必须带查询条件 | +| updateMany 需审核 | DML | warning | `(?i)updateMany` | review | 批量更新需审批 | + +--- + +## 四、数据脱敏 (data_masking) + +数据脱敏功能作用于在线查询返回结果,自动将敏感字段的内容替换为 `***`。 + +| Key | 类型 | 默认值 | 说明 | 引用位置 | +|-----|------|--------|------|----------| +| `data_masking_enabled` | boolean | `true` | 全局脱敏开关 | `instance_detail.go:1522-1525` | +| `data_masking_sensitive_fields` | string | _(见下方)_ | 全局敏感字段列表(逗号分隔) | `instance_detail.go:1531-1534` | + +**默认敏感字段列表**: + +``` +mobile, concat_mobile, email_alias, email, password, ga_key, +private_key, secret, secret_key, key_snow, telephone, username, +real_name, token, two_factor_secret, content, phone +``` + +### 作用机制 + +`applyDataMasking()` 函数(`instance_detail.go:1520`)执行以下逻辑: + +1. 读取 `data_masking_enabled`,若非 `true` 则跳过 +2. 读取 `data_masking_sensitive_fields`,按逗号拆分为字段名列表 +3. 合并实例级别 `instance.mask_fields` 的字段 +4. 遍历查询结果的每一行每一列,若列名(不区分大小写)匹配敏感字段,则将值替换为 `***` + +**字段匹配方式**:列名包含(contains)敏感字段名即匹配,不区分大小写。 + +**优先级**:全局敏感字段 ∪ 实例级 `mask_fields`,两者合并生效。 + +--- + +## 五、执行器 (executor) + +执行器配置控制工单中 SQL 语句的实际执行行为。 + +| Key | 类型 | 默认值 | 说明 | 引用位置 | 状态 | +|-----|------|--------|------|----------|------| +| `executor_timeout` | number | `300` | SQL 执行的最大等待时间(秒) | `ticket.go:725,766` | ✅ 有效 | +| `executor_dry_run` | boolean | `true` | 执行前先 EXPLAIN 验证 SQL 语法 | `ticket.go:770` | ✅ 有效 | +| `executor_batch_size` | number | `100` | DML 批量执行时每批的行数 | _(无)_ | ⚠️ 待实现 | +| `executor_backup` | boolean | `true` | DML 执行前自动备份受影响数据 | _(无)_ | ⚠️ 待实现 | + +### 作用机制 + +**executor_timeout** — 为每个 SQL 语句创建带超时的 context: + +```go +// OSC (gh-ost) 路径 +execTimeout := getSettingInt("executor_timeout", 300) +ctx, cancel := context.WithTimeout(context.Background(), time.Duration(execTimeout)*time.Second) + +// 原生执行路径 +execTimeout := getSettingInt("executor_timeout", 300) +ctx, cancel := context.WithTimeout(context.Background(), time.Duration(execTimeout)*time.Second) +``` + +**executor_dry_run** — 对 DDL 语句执行 EXPLAIN 预检查: + +```go +if getSetting("executor_dry_run", "true") == "true" { + ddlPrefixes := []string{"CREATE", "ALTER", "DROP", "TRUNCATE", "RENAME"} + // 对每条 DDL 语句执行 EXPLAIN,语法错误则拦截 +} +``` + +--- + +## 六、Ghost / OSC 在线 DDL (ghost) + +Ghost 配置控制 [gh-ost](https://github.com/github/gh-ost) 在线 DDL 变更工具的默认参数。共 13 项配置,全部在 `ghost.go` 的 `applyGhostSettings()` 函数和 OSC 触发逻辑中引用。 + +### 基本设置 + +| Key | 类型 | 默认值 | 说明 | 引用位置 | +|-----|------|--------|------|----------| +| `ghost_enabled` | boolean | `false` | 启用 gh-ost 在线 DDL 变更工具 | `ticket.go` (OSC 触发判断) | +| `ghost_path` | string | `gh-ost` | gh-ost 二进制文件路径 | `ghost.go` (任务创建) | +| `ghost_default_mode` | string | `dry_run` | 默认执行模式:`dry_run`(预演) / `execute`(直接执行) | `ghost.go` (任务创建) | +| `ghost_osc_priority` | string | `native_first` | OSC 优先级策略:`native_first` / `ghost_first` / `ghost_only` | `ticket.go` (OSC 触发判断) | +| `ghost_env_mode` | string | `development` | 环境模式:`development` / `staging` / `production` | `ghost.go` (任务创建) | +| `ghost_cloud_type` | string | `self_hosted` | 部署类型:`self_hosted` / `rds` / `cloud` | `ghost.go` (任务创建) | + +### 自动触发 + +| Key | 类型 | 默认值 | 说明 | 引用位置 | +|-----|------|--------|------|----------| +| `ghost_auto_trigger_rows` | number | `10` | 表行数超过此阈值(万行)自动推荐使用 OSC | `ticket.go` (大表检测) | +| `ghost_auto_trigger_ddl` | string | `ADD COLUMN,MODIFY COLUMN,ADD INDEX` | 自动触发 OSC 的 DDL 操作类型列表(逗号分隔) | `ticket.go` (DDL 类型匹配) | + +### 性能调优 + +| Key | 类型 | 默认值 | 说明 | 引用位置 | +|-----|------|--------|------|----------| +| `ghost_default_chunk_size` | number | `1000` | 每次拷贝的行数 | `ghost.go:387` (`applyGhostSettings`) | +| `ghost_max_load` | string | `Threads_running=25` | 超过此负载暂停拷贝 | `ghost.go:391` | +| `ghost_critical_load` | string | `Threads_running=50` | 超过此负载立即停止 | `ghost.go:395` | +| `ghost_allow_on_master` | boolean | `true` | 是否允许直接在主库运行 gh-ost | `ghost.go:403` | +| `ghost_max_lag_millis` | number | `1500` | 从库复制延迟超过此值(ms)暂停拷贝 | `ghost.go:399` | + +### 作用机制 + +**自动触发 OSC 流程**(`ticket.go`): + +1. 检查 `ghost_enabled` 是否启用 +2. 检查 SQL 是否为 ALTER TABLE 类型 +3. 检查 DDL 操作类型是否在 `ghost_auto_trigger_ddl` 列表中 +4. 检查目标表行数是否超过 `ghost_auto_trigger_rows`(万) +5. 根据 `ghost_osc_priority` 决定使用原生 ALTER 还是 gh-ost + +**参数注入流程**(`ghost.go:applyGhostSettings`): + +```go +func applyGhostSettings(task *ghost.GhOstTask) { + task.ChunkSize = getSettingInt("ghost_default_chunk_size", 1000) + task.MaxLoad = getSetting("ghost_max_load", "") + task.CriticalLoad = getSetting("ghost_critical_load", "") + task.MaxLagMillis = getSettingInt("ghost_max_lag_millis", 1500) + task.AllowOnMaster = getSetting("ghost_allow_on_master", "true") == "true" +} +``` + +--- + +## 七、安全策略 (security) + +安全策略配置控制用户登录和认证行为,全部在 `auth.go`(handler)和 `middleware/auth.go` 中引用。 + +| Key | 类型 | 默认值 | 说明 | 引用位置 | +|-----|------|--------|------|----------| +| `login_max_attempts` | number | `5` | 连续登录失败达到此次数后锁定账号 | `auth.go:34,365` | +| `login_lockout_minutes` | number | `30` | 账号锁定后的持续时长(分钟) | `auth.go:35,366` | +| `password_min_length` | number | `8` | 用户密码的最短字符数 | `auth.go` (密码校验) | +| `password_require_complex` | boolean | `true` | 要求密码包含大小写字母、数字和特殊字符 | `auth.go` (密码校验) | +| `session_timeout_hours` | number | `24` | JWT Token 有效期(小时) | `middleware/auth.go:20` | + +### 作用机制 + +**登录锁定流程**(`auth.go`): + +1. 登录时检查 `login_max_attempts` 和 `login_lockout_minutes` +2. 密码错误时调用 `handleFailedLogin()`,递增 `FailedLoginAttempts` +3. 达到 `login_max_attempts` 时,设置 `LockedUntil = now + lockout_minutes` +4. 账号锁定期间拒绝登录,返回剩余锁定时间 + +**会话超时**(`middleware/auth.go`): + +```go +func getSessionTimeoutHours() int { + // 从 system_settings 读取 session_timeout_hours + // 用于 JWT Token 的过期时间计算 +} +``` + +--- + +## 八、日志清理 (cleanup) + +清理配置控制各类日志和工单数据的自动保留期限。全部在 `ghost.go`(清理定时任务)和 `main.go`(seed)中引用。 + +| Key | 类型 | 默认值 | 说明 | 引用位置 | +|-----|------|--------|------|----------| +| `ticket_retention_days` | number | `180` | 超过此天数的已完成/已关闭工单将被清理 | `ghost.go` (清理任务) | +| `audit_log_retention_days` | number | `365` | 超过此天数的审计日志将被清理 | `ghost.go` (清理任务) | +| `query_history_retention_days` | number | `90` | 超过此天数的查询历史将被清理 | `ghost.go` (清理任务) | +| `slow_log_retention_days` | number | `180` | 超过此天数的慢查询日志将被清理 | `ghost.go` (清理任务) | + +--- + +## 九、LDAP 配置 (ldap) + +### system_settings 中的 LDAP 配置(⚠️ 冗余) + +以下 9 项配置存在于 `system_settings` 表的 `ldap` 分组中,但 **全部未被业务代码引用**。LDAP 功能使用独立的 `ldap_configs` 表进行管理。 + +| Key | 类型 | 默认值 | 说明 | 状态 | +|-----|------|--------|------|------| +| `ldap_host` | string | `8.217.205.92` | LDAP 服务器地址 | ❌ 冗余 | +| `ldap_port` | string | `389` | LDAP 端口 | ❌ 冗余 | +| `ldap_use_ssl` | boolean | `false` | 启用 LDAPS (SSL/TLS) | ❌ 冗余 | +| `ldap_bind_dn` | string | `cn=admin,dc=opsx,dc=vip` | 管理员 DN | ❌ 冗余 | +| `ldap_base_dn` | string | `dc=opsx,dc=vip` | Base DN | ❌ 冗余 | +| `ldap_user_filter` | string | `(objectClass=inetOrgPerson)` | 用户过滤器 | ❌ 冗余 | +| `ldap_uid_attr` | string | `uid` | UID 属性 | ❌ 冗余 | +| `ldap_mail_attr` | string | `mail` | 邮箱属性 | ❌ 冗余 | +| `ldap_cn_attr` | string | `cn` | 显示名属性 | ❌ 冗余 | + +### ldap_configs 独立表(实际使用) + +LDAP 功能通过独立的 `ldap_configs` 表管理,提供完整的 CRUD 功能: + +| 功能 | API 端点 | 说明 | +|------|----------|------| +| 配置 CRUD | `/api/v1/ldap-configs` | 增删改查 LDAP 服务器配置 | +| 测试连接 | `/api/v1/ldap-configs/:id/test` | 验证 LDAP 服务器连通性 | +| OU 管理 | `/api/v1/ldap-configs/:id/ous` | 管理 LDAP 组织单元 | +| 用户导入 | `/api/v1/ldap-configs/:id/import` | 从 LDAP 导入用户到平台 | + +> **建议**:后续版本可清理 `system_settings` 中 `ldap` 分组的冗余配置项,避免维护混淆。 + +--- + +## 十、未引用 / 待实现配置 + +以下配置项在 `seed.go` 中定义,但在业务代码中 **无实际引用**,标注为"待实现": + +| Key | 分组 | 默认值 | 预期功能 | 状态 | +|-----|------|--------|----------|------| +| `executor_batch_size` | executor | `100` | DML 批量执行时每批的行数 | ⚠️ 待实现 | +| `executor_backup` | executor | `true` | DML 执行前自动备份受影响数据 | ⚠️ 待实现 | +| `query_max_rows_examined` | query | `1000000` | 单次查询允许扫描的最大行数 | ⚠️ 待实现 | +| `ldap_host` | ldap | `8.217.205.92` | LDAP 服务器地址 | ❌ 冗余(使用 ldap_configs 表) | +| `ldap_port` | ldap | `389` | LDAP 端口 | ❌ 冗余 | +| `ldap_use_ssl` | ldap | `false` | 启用 LDAPS | ❌ 冗余 | +| `ldap_bind_dn` | ldap | `cn=admin,dc=opsx,dc=vip` | 管理员 DN | ❌ 冗余 | +| `ldap_base_dn` | ldap | `dc=opsx,dc=vip` | Base DN | ❌ 冗余 | +| `ldap_user_filter` | ldap | `(objectClass=inetOrgPerson)` | 用户过滤器 | ❌ 冗余 | +| `ldap_uid_attr` | ldap | `uid` | UID 属性 | ❌ 冗余 | +| `ldap_mail_attr` | ldap | `mail` | 邮箱属性 | ❌ 冗余 | +| `ldap_cn_attr` | ldap | `cn` | 显示名属性 | ❌ 冗余 | + +**汇总**: +- ⚠️ **待实现**:3 项(`executor_batch_size`、`executor_backup`、`query_max_rows_examined`) +- ❌ **冗余**:9 项(`ldap` 分组全部,LDAP 使用独立 `ldap_configs` 表) + +--- + +## 附录:数据模型 + +### system_settings 表结构 + +```go +type SystemSetting struct { + ID int `gorm:"primaryKey" json:"id"` + Key string `gorm:"size:100;not null" json:"key"` // 唯一配置键 + Value string `gorm:"type:text;not null" json:"value"` // 当前值 + ValueType string `gorm:"size:20;not null" json:"value_type"` // number / boolean / string + Group string `gorm:"size:50;not null" json:"group"` // 分组名 + Label string `gorm:"size:100;not null" json:"label"` // 前端显示名 + Desc *string `gorm:"type:text" json:"description"` // 配置说明 + DefaultValue *string `gorm:"type:text" json:"default_value"` // 默认值 + SortOrder int `gorm:"not null" json:"sort_order"` // 组内排序 + UpdatedAt *time.Time `json:"updated_at"` +} +``` + +### audit_rules 表结构 + +```go +type AuditRule struct { + ID int `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` // 规则名称 + Desc *string `gorm:"type:text" json:"description"` // 规则描述 + RuleType string `gorm:"size:50;not null" json:"rule_type"` // DDL/DML/DCL/DQL/performance/security + Severity *string `gorm:"size:20" json:"severity"` // error/warning/info + DBType *string `gorm:"size:20" json:"db_type"` // mysql/postgresql/redis/mongodb + Pattern *string `gorm:"type:text" json:"pattern"` // RE2 正则表达式 + IsEnabled *bool ` json:"is_enabled"` // 是否启用 + Message string `gorm:"size:500;not null" json:"message"` // 命中提示消息 + Value *string `gorm:"size:255" json:"value"` // reject/review/warn/where_check + Code *string `gorm:"size:50" json:"code"` // 规则编码(预留) + ConfigJSON *string `gorm:"type:text;column:config_json" json:"config_json"` // 扩展配置(预留) + CreatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"` + UpdatedAt time.Time `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"` +} +``` + +### 配置读取辅助函数 + +```go +// ghost.go — 读取字符串配置 +func getSetting(key, fallback string) string { + var s model.SystemSetting + if model.DB.Where("`key` = ?", key).Take(&s).Error == nil && s.Value != "" { + return s.Value + } + return fallback +} + +// ghost.go — 读取整数配置 +func getSettingInt(key string, fallback int) int { + // 读取字符串并 strconv.Atoi,失败返回 fallback +} +``` + +--- + +> **文档维护说明**:新增配置项时,需同步更新 `seed.go` 的 `seedDefaultSettings()` / `seedAuditRules()` 函数及本文档。