feat: 审核规则驱动SQL检查 + db_type过滤 + Redis只读拦截 + 配置文档

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审核规则匹配正确
This commit is contained in:
logan committed 2026-06-03 03:17:22 +08:00
1 parent 8fbcb8647d
commit 3a6dd99633
3 files changed
+563 -55

No files matched your search

+5 -1
View File
@@ -427,7 +427,11 @@ func GhOstListTasks(c *gin.Context) {
// GET /api/v1/audit-rules // GET /api/v1/audit-rules
func ListAuditRules(c *gin.Context) { func ListAuditRules(c *gin.Context) {
var rules []model.AuditRule 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) c.JSON(http.StatusOK, rules)
} }
+73 -54
View File
@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -244,74 +245,92 @@ func CreateTicket(c *gin.Context) {
return return
} }
// Reject read-only SQL statements (SELECT/SHOW/DESC/EXPLAIN) // Reject read-only SQL statements
upperSQL := strings.ToUpper(strings.TrimSpace(input.SQLContent)) upperSQL := strings.ToUpper(strings.TrimSpace(input.SQLContent))
readOnlyPrefixes := []string{"SELECT", "SHOW", "DESC", "DESCRIBE", "EXPLAIN"} if input.DBType == "mysql" || input.DBType == "postgresql" {
for _, prefix := range readOnlyPrefixes { readOnlyPrefixes := []string{"SELECT", "SHOW", "DESC", "DESCRIBE", "EXPLAIN"}
if strings.HasPrefix(upperSQL, prefix) { for _, prefix := range readOnlyPrefixes {
c.JSON(http.StatusBadRequest, gin.H{"detail": "数据变更工单不支持 SELECT/SHOW/DESC 等只读语句,请使用查询数据功能"}) if strings.HasPrefix(upperSQL, prefix) {
return 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
}
} }
// Warn: UPDATE/DELETE without WHERE }
if (strings.HasPrefix(upper, "UPDATE ") || strings.HasPrefix(upper, "DELETE FROM ")) && } else if input.DBType == "redis" {
!strings.Contains(upper, " WHERE ") { // Redis: reject read-only commands (GET/HGET/KEYS/SCAN/TTL/TYPE/LLEN/LRANGE/INFO etc.)
c.JSON(http.StatusBadRequest, gin.H{"detail": "禁止无 WHERE 条件的 UPDATE/DELETE — 全表更新/删除属于高危操作,请添加 WHERE 子句限定范围"}) 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 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" { 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) cmdParts := parseCommandParts(input.SQLContent)
if len(cmdParts) > 0 { if len(cmdParts) > 0 {
upperCmd := strings.ToUpper(cmdParts[0]) sqlToCheck = strings.ToUpper(cmdParts[0]) + " " + strings.Join(cmdParts[1:], " ")
for _, bl := range redisBlacklist { }
if upperCmd == strings.TrimSuffix(bl.cmd, " ") || strings.HasPrefix(upperCmd+" ", bl.cmd) { }
c.JSON(http.StatusBadRequest, gin.H{"detail": bl.reason})
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 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 // Auto-fill workflow_id from instance if not provided
+485
View File
@@ -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()` 函数及本文档。