package seed import ( "crypto/rand" "log" "os" "path/filepath" "time" "opsxloki/models" "opsxloki/pkg/crypto" "opsxloki/repository" ) // InitPasswordFile 首次启动生成的随机管理员密码,存放于数据卷根目录。 // 用户名 admin 首次登录并修改密码后,该文件会被自动删除(见 CleanupInitPassword)。 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 } func dataDir() string { if d := os.Getenv("DATA_DIR"); d != "" { return d } return "/app/data" } func Seed() { db := repository.GetDB() db.RLock() hasUsers := len(db.Users) > 0 db.RUnlock() if !hasUsers { // 优先级:环境变量 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 } hash, _ := crypto.HashPassword(pwd) repository.GetDB().CreateUser(&models.User{ Username: "admin", Password: hash, Nickname: "Administrator", Role: "admin", }) if generated { path := filepath.Join(dataDir(), InitPasswordFile) if err := os.WriteFile(path, []byte(pwd+"\n"), 0600); err != nil { log.Printf("[SEED] 写入初始密码文件失败: %v", err) } log.Printf("==================================================") log.Printf(" 首次启动:已生成管理员账号") log.Printf(" 用户名 : admin") log.Printf(" 密 码 : %s", pwd) log.Printf(" 已保存 : %s", path) log.Printf(" 获取方式:") log.Printf(" docker compose exec <服务名> cat %s", path) log.Printf(" kubectl -n exec deploy/opsx-loki -- cat %s", path) log.Printf(" ⚠️ 首次登录后请立即修改密码(改密后本文件自动删除)") log.Printf("==================================================") } else { log.Printf("[SEED] 管理员初始密码来自环境变量 ADMIN_INIT_PASSWORD") } } // 内置默认查询策略:所有人可查询 default 命名空间。 // 目的:新环境/新账号开箱即有可查范围,快捷 LogQL 的 namespace 下拉不会空。 seedDefaultPolicy() // 内置告警规则:有 managed 数据源(部署过 Loki)时补齐通用模板(默认禁用) SeedAlertRules() } // seedDefaultPolicy 创建「默认策略」:所有用户可查询 default 命名空间。 // // 设计要点: // - UserIDs 里的 0 是哨兵值,表示「所有人」(anyone / *),见 policyGroupAccess // - 只在当前还没有任何策略组时创建,避免覆盖管理员的自定义配置 // - 命名空间取 default 作为占位;管理员可在「查询授权」里改成实际业务命名空间 // (如 astralx / prod),改动对该策略组所有用户立即生效 func seedDefaultPolicy() { db := repository.GetDB() if db == nil { return } db.RLock() hasGroup := len(db.LogPolicyGroups) > 0 var sourceID uint if len(db.LogSources) > 0 { sourceID = db.LogSources[0].ID } db.RUnlock() if hasGroup || sourceID == 0 { return } id := db.NextID("logPolicyGroups") now := time.Now() group := models.LogPolicyGroup{ ID: id, Name: "默认策略(所有人 · default 命名空间)", SourceID: sourceID, Labels: map[string]string{"namespace": "default"}, UserIDs: []uint{0}, // 0 = 所有人 CreatedAt: now, UpdatedAt: now, } db.Lock() db.LogPolicyGroups = append(db.LogPolicyGroups, group) db.Unlock() db.RequestSave() log.Printf("[SEED] 已创建默认查询策略:所有人可查 default 命名空间(可在「查询授权」改为实际业务命名空间)") } // CleanupInitPassword 在 admin 首次修改密码后调用,删除明文密码文件。 // 幂等:文件不存在时静默返回。 func CleanupInitPassword() { path := filepath.Join(dataDir(), InitPasswordFile) if _, err := os.Stat(path); err != nil { return } if err := os.Remove(path); err != nil { log.Printf("[SEED] 删除初始密码文件失败: %v", err) return } log.Printf("[SEED] 初始密码文件已删除: %s", path) }