package main import ( "crypto/rand" "fmt" "log" "os" "path/filepath" "sql-platform/internal/model" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) // seedAll initializes the database with default data (idempotent). // Safe to run on every startup — skips existing records. func seedAll(db *gorm.DB) { seedDefaultUsers(db) seedDefaultWorkflow(db) seedDefaultSettings(db) seedLDAPConfig(db) seedAuditRules(db) log.Println("[seed] Default data initialized") } // InitPasswordFile 首次启动生成的默认账号初始密码文件名。 // 默认账号(admin/dba/auditor/developer)共用这一个密码,登录后请尽快修改。 const InitPasswordFile = "initial-admin-password.txt" // randomPassword 生成 n 位随机密码(大小写字母 + 数字,排除 0/O/1/l/I 等易混淆字符) func randomPassword(n int) (string, error) { const charset = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789" buf := make([]byte, n) if _, err := rand.Read(buf); err != nil { return "", err } for i := range buf { buf[i] = charset[int(buf[i])%len(charset)] } return string(buf), nil } // dataDir 返回数据目录(由 DB_PATH 决定),用于存放初始密码文件 func dataDir() string { dbPath := os.Getenv("DB_PATH") if dbPath == "" { dbPath = "/app/data/sqlite3.db" } return filepath.Dir(dbPath) } // seedDefaultUsers creates default users if they don't exist. func seedDefaultUsers(db *gorm.DB) { type seedUser struct { username string displayName string email string role string } users := []seedUser{ {"admin", "超级管理员", "admin@opsx.local", "admin"}, {"dba", "DBA管理员", "dba@opsx.local", "dba"}, {"auditor", "审计员", "auditor@opsx.local", "auditor"}, {"developer", "开发者", "developer@opsx.local", "developer"}, } // 先找出缺失的默认账号;一个都不缺就直接返回,不生成任何密码 var missing []seedUser for _, u := range users { var existing model.User if db.Where("username = ?", u.username).First(&existing).Error != nil { missing = append(missing, u) } } if len(missing) == 0 { return } // 决定初始密码:ADMIN_INIT_PASSWORD 优先,否则随机生成 pwd := os.Getenv("ADMIN_INIT_PASSWORD") generated := false if pwd == "" { var err error if pwd, err = randomPassword(16); err != nil { log.Fatalf("[seed] 生成随机初始密码失败: %v;请通过 ADMIN_INIT_PASSWORD 环境变量指定初始密码", err) } generated = true } var created []string for _, u := range missing { hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) email := u.email active := true user := model.User{ Username: u.username, DisplayName: u.displayName, Email: &email, HashedPassword: string(hash), Role: u.role, IsActive: &active, } if err := db.Create(&user).Error; err != nil { log.Printf("[seed] Failed to create user %s: %v", u.username, err) } else { created = append(created, u.username) log.Printf("[seed] Created user: %s (%s)", u.username, u.role) } } if len(created) == 0 { return } if generated { path := filepath.Join(dataDir(), InitPasswordFile) content := fmt.Sprintf("# opsxsql 默认账号初始密码(首次启动生成)\n"+ "# 登录后请立即修改;确认无需保留时可删除本文件\n"+ "# 适用账号: %v\n"+ "密码: %s\n", created, pwd) if err := os.WriteFile(path, []byte(content), 0600); err != nil { log.Printf("[seed] 写入初始密码文件失败: %v", err) } log.Printf("============================================================") log.Printf(" 首次启动:已创建默认账号 %v", created) log.Printf(" 初始密码 : %s", pwd) log.Printf(" 已保存到 : %s", path) log.Printf(" 获取方式 :") log.Printf(" docker compose exec <服务名> cat %s", path) log.Printf(" kubectl -n exec deploy/opsxsql -- cat %s", path) log.Printf(" ⚠️ 所有默认账号共用此密码,登录后请立即修改") log.Printf("============================================================") } else { log.Printf("[seed] 默认账号初始密码来自环境变量 ADMIN_INIT_PASSWORD") } } // seedDefaultWorkflow creates a default approval workflow if none exists. func seedDefaultWorkflow(db *gorm.DB) { var count int64 db.Model(&model.WorkflowDefinition{}).Count(&count) if count > 0 { return // Already has workflows } // Create default workflow t := true wf := model.WorkflowDefinition{ Name: "默认审批流程", Description: strPtr("系统默认审批流程:开发者提交 → DBA审核 → 执行"), IsDefault: &t, IsActive: &t, } if err := db.Create(&wf).Error; err != nil { log.Printf("[seed] Failed to create default workflow: %v", err) return } // Create steps steps := []model.WorkflowStep{ { WorkflowID: wf.ID, StepNumber: 1, Name: "提交审核", StepType: "submit", IsRequired: &t, TimeoutHours: intPtr(0), }, { WorkflowID: wf.ID, StepNumber: 2, Name: "DBA审核", StepType: "audit", ApproverRole: strPtr("dba"), IsRequired: &t, TimeoutHours: intPtr(24), }, { WorkflowID: wf.ID, StepNumber: 3, Name: "执行变更", StepType: "execute", IsRequired: &t, TimeoutHours: intPtr(0), }, } for _, s := range steps { db.Create(&s) } log.Printf("[seed] Created default workflow: %s (id=%d)", wf.Name, wf.ID) } func intPtr(v int) *int { return &v } // seedLDAPConfig inserts default LDAP settings if not present. func seedLDAPConfig(db *gorm.DB) { ldapDefaults := []model.SystemSetting{ {Key: "ldap_host", Value: "8.217.205.92", ValueType: "string", Group: "ldap", Label: "LDAP 服务器地址", Desc: strPtr("LDAP 服务器 IP 或域名"), DefaultValue: strPtr("8.217.205.92"), SortOrder: 1}, {Key: "ldap_port", Value: "389", ValueType: "number", Group: "ldap", Label: "LDAP 端口", Desc: strPtr("默认 389,LDAPS 用 636"), DefaultValue: strPtr("389"), SortOrder: 2}, {Key: "ldap_use_ssl", Value: "false", ValueType: "boolean", Group: "ldap", Label: "启用 LDAPS (SSL/TLS)", Desc: strPtr("使用 SSL/TLS 加密连接"), DefaultValue: strPtr("false"), SortOrder: 3}, {Key: "ldap_bind_dn", Value: "cn=admin,dc=opsx,dc=vip", ValueType: "string", Group: "ldap", Label: "管理员 DN", Desc: strPtr("LDAP 管理员绑定 DN"), DefaultValue: strPtr("cn=admin,dc=opsx,dc=vip"), SortOrder: 4}, {Key: "ldap_base_dn", Value: "dc=opsx,dc=vip", ValueType: "string", Group: "ldap", Label: "Base DN", Desc: strPtr("用户搜索的基础 DN"), DefaultValue: strPtr("dc=opsx,dc=vip"), SortOrder: 5}, {Key: "ldap_user_filter", Value: "(objectClass=inetOrgPerson)", ValueType: "string", Group: "ldap", Label: "用户过滤器", Desc: strPtr("LDAP 用户搜索过滤器"), DefaultValue: strPtr("(objectClass=inetOrgPerson)"), SortOrder: 6}, {Key: "ldap_uid_attr", Value: "uid", ValueType: "string", Group: "ldap", Label: "UID 属性", Desc: strPtr("用户唯一标识属性"), DefaultValue: strPtr("uid"), SortOrder: 7}, {Key: "ldap_mail_attr", Value: "mail", ValueType: "string", Group: "ldap", Label: "邮箱属性", Desc: strPtr("用户邮箱属性名"), DefaultValue: strPtr("mail"), SortOrder: 8}, {Key: "ldap_cn_attr", Value: "cn", ValueType: "string", Group: "ldap", Label: "显示名属性", Desc: strPtr("用户显示名属性名"), DefaultValue: strPtr("cn"), SortOrder: 9}, } for _, s := range ldapDefaults { var existing model.SystemSetting if db.Where("`key` = ?", s.Key).First(&existing).Error != nil { db.Create(&s) } } log.Println("[seed] LDAP default config seeded") } // seedAuditRules creates default audit rules for all DB types. func seedAuditRules(db *gorm.DB) { var count int64 db.Model(&model.AuditRule{}).Count(&count) if count > 0 { return // Already has rules } enabled := true type seedRule struct { Name, Desc, RuleType, Severity, Pattern, Message, Value string } allRules := map[string][]seedRule{ "mysql": { {"禁止 DROP TABLE", "生产环境不允许删除表", "DDL", "error", `(?i)^\s*DROP\s+TABLE`, "禁止执行 DROP TABLE 操作,可能导致数据永久丢失", "reject"}, {"禁止 DROP DATABASE", "不允许删除数据库", "DDL", "error", `(?i)^\s*DROP\s+DATABASE`, "禁止执行 DROP DATABASE 操作", "reject"}, {"禁止 TRUNCATE TABLE", "清空表数据需走审批", "DDL", "error", `(?i)^\s*TRUNCATE\s+TABLE?`, "禁止执行 TRUNCATE 操作,请使用 DELETE 逐批删除", "reject"}, {"ALTER TABLE 需审核", "表结构变更需审批", "DDL", "warning", `(?i)^\s*ALTER\s+TABLE`, "ALTER TABLE 操作需要审批后执行", "review"}, {"禁止 DELETE 无 WHERE", "删除操作必须带条件", "DML", "error", `(?i)^\s*DELETE\s+FROM?\s+\S+\s*$`, "DELETE 语句必须包含 WHERE 条件", "reject"}, {"禁止 UPDATE 无 WHERE", "更新操作必须带条件", "DML", "error", `(?i)^\s*UPDATE\s+\S+\s+SET\s+.*\s*$`, "UPDATE 语句必须包含 WHERE 条件", "reject"}, {"SELECT * 建议", "避免使用 SELECT *", "DQL", "info", `(?i)^\s*SELECT\s+\*\s+FROM`, "建议明确指定查询字段,避免使用 SELECT *", "warn"}, {"INSERT 需指定列", "INSERT 需指定列名", "DML", "warning", `(?i)^\s*INSERT\s+INTO\s+\S+\s+VALUES`, "INSERT 语句应明确指定列名", "review"}, {"禁止 GRANT ALL", "不允许授予全部权限", "DCL", "error", `(?i)^\s*GRANT\s+ALL`, "禁止使用 GRANT ALL PRIVILEGES", "reject"}, {"大表变更警告", "影响行数超过阈值需注意", "performance", "warning", "", "操作影响行数可能较大,建议在低峰期执行", "warn"}, {"禁止不带 LIMIT 的 DELETE", "批量删除建议带 LIMIT", "DML", "warning", `(?i)^\s*DELETE\s+FROM\s+\S+\s+WHERE`, "建议 DELETE 语句添加 LIMIT 子句分批删除", "warn"}, }, "postgresql": { {"禁止 DROP TABLE", "生产环境不允许删除表", "DDL", "error", `(?i)^\s*DROP\s+TABLE`, "禁止执行 DROP TABLE 操作", "reject"}, {"禁止 DROP DATABASE", "不允许删除数据库", "DDL", "error", `(?i)^\s*DROP\s+DATABASE`, "禁止执行 DROP DATABASE 操作", "reject"}, {"禁止 DELETE 无 WHERE", "删除操作必须带条件", "DML", "error", `(?i)^\s*DELETE\s+FROM?\s+\S+\s*$`, "DELETE 语句必须包含 WHERE 条件", "reject"}, {"禁止 UPDATE 无 WHERE", "更新操作必须带条件", "DML", "error", `(?i)^\s*UPDATE\s+\S+\s+SET\s+.*\s*$`, "UPDATE 语句必须包含 WHERE 条件", "reject"}, {"ALTER TABLE 需审核", "表结构变更需审批", "DDL", "warning", `(?i)^\s*ALTER\s+TABLE`, "ALTER TABLE 操作需要审批后执行", "review"}, {"SELECT * 建议", "避免使用 SELECT *", "DQL", "info", `(?i)^\s*SELECT\s+\*\s+FROM`, "建议明确指定查询字段", "warn"}, {"禁止 GRANT ALL", "不允许授予全部权限", "DCL", "error", `(?i)^\s*GRANT\s+ALL`, "禁止使用 GRANT ALL", "reject"}, }, "redis": { {"禁止 FLUSHALL", "不允许清空所有数据", "DDL", "error", `(?i)^FLUSHALL`, "禁止执行 FLUSHALL 操作", "reject"}, {"禁止 FLUSHDB", "清空数据库需审批", "DDL", "error", `(?i)^FLUSHDB`, "禁止执行 FLUSHDB 操作", "reject"}, {"禁止 CONFIG", "配置修改需走审批", "security", "error", `(?i)^CONFIG\s`, "CONFIG 命令需要审批后执行", "reject"}, {"KEYS 命令警告", "生产环境禁止 KEYS", "performance", "error", `(?i)^KEYS\s`, "生产环境禁止使用 KEYS 命令,请使用 SCAN", "reject"}, }, "mongodb": { {"禁止 dropDatabase", "不允许删除数据库", "DDL", "error", `dropDatabase`, "禁止执行 dropDatabase 操作", "reject"}, {"禁止 drop 集合", "不允许删除集合", "DDL", "error", `(?i)\.drop\(\)`, "禁止执行集合 drop 操作", "reject"}, {"remove 需带条件", "删除操作必须带条件", "DML", "error", `(?i)remove\(\s*\)`, "remove 操作必须带查询条件", "reject"}, {"updateMany 需审核", "批量更新需审批", "DML", "warning", `(?i)updateMany`, "updateMany 操作需要审批后执行", "review"}, }, } total := 0 for dbType, rules := range allRules { for _, r := range rules { desc := r.Desc pattern := r.Pattern severity := r.Severity value := r.Value rule := model.AuditRule{ Name: r.Name, Desc: &desc, RuleType: r.RuleType, Severity: &severity, DBType: &dbType, Pattern: &pattern, IsEnabled: &enabled, Message: r.Message, Value: &value, } if err := db.Create(&rule).Error; err != nil { log.Printf("[seed] Failed to create audit rule %s/%s: %v", dbType, r.Name, err) } else { total++ } } } log.Printf("[seed] Created %d default audit rules across 4 DB types", total) }