package config import ( "fmt" "strings" "github.com/spf13/viper" ) // Config 是 idsync 的完整配置。 type Config struct { System System `mapstructure:"system"` Server Server `mapstructure:"server"` Feishu Feishu `mapstructure:"feishu"` Ldap Ldap `mapstructure:"ldap"` Lldap Lldap `mapstructure:"lldap"` Database Database `mapstructure:"database"` Schedule Schedule `mapstructure:"schedule"` Guard Guard `mapstructure:"guard"` } // System 是运行环境配置。 type System struct { // Mode: debug 或 release,影响日志详细度。 Mode string `mapstructure:"mode"` // DataDir 用于存放 sqlite 镜像与运行数据。 DataDir string `mapstructure:"data-dir"` } // Server 是管理界面与一次性离职确认端点的配置。 type Server struct { // Listen 形如 0.0.0.0:8899。 Listen string `mapstructure:"listen"` // AdminUser / AdminPassword 是 WebUI 的单一管理员凭据。 AdminUser string `mapstructure:"admin-user"` AdminPassword string `mapstructure:"admin-password"` AdminPasswordHash string `mapstructure:"admin-password-hash"` SessionSecret string `mapstructure:"session-secret"` SecureCookie bool `mapstructure:"secure-cookie"` // ConfirmBaseURL 是离职确认端点的对外 base-url(可被公网/反代访问), // 飞书离职卡片的「确认离职」按钮 url 用它拼接。 ConfirmBaseURL string `mapstructure:"confirm-base-url"` } // Feishu 是飞书接口配置。 type Feishu struct { AppID string `mapstructure:"app-id"` AppSecret string `mapstructure:"app-secret"` // BaseURL 留空使用飞书(open.feishu.cn);Lark 国际版填 https://open.larksuite.com。 BaseURL string `mapstructure:"base-url"` // DeptList 留空表示同步全部部门;填部门 ID 则只同步这些部门,前缀 ^ 表示排除。 DeptList []string `mapstructure:"dept-list"` QPS int `mapstructure:"qps"` TimeoutSec int `mapstructure:"timeout-sec"` // WebhookURL 是离职确认卡片走自定义机器人 webhook 发送的地址。 WebhookURL string `mapstructure:"webhook-url"` } // Ldap 是 LLDAP 的 LDAP 读通道(连接/bind/搜索)。它只读不写。 type Ldap struct { URL string `mapstructure:"url"` BaseDN string `mapstructure:"base-dn"` // AdminDN:LLDAP 管理员用户,形如 uid=admin,ou=people,。 AdminDN string `mapstructure:"admin-dn"` AdminPass string `mapstructure:"admin-pass"` TimeoutSec int `mapstructure:"timeout-sec"` StartTLS bool `mapstructure:"start-tls"` InsecureSkipVerify bool `mapstructure:"insecure-skip-verify"` } // Lldap 是 LLDAP 的 GraphQL 写通道(唯一负责写入)。 type Lldap struct { // BaseURL 是 LLDAP WebUI/GraphQL 地址,形如 http://lldap:17170。 BaseURL string `mapstructure:"base-url"` // AdminUser / AdminPass 用于 GraphQL 登录,默认 admin。 AdminUser string `mapstructure:"admin-user"` AdminPass string `mapstructure:"admin-pass"` } type Database struct { // Source 是 sqlite 文件路径。 Source string `mapstructure:"source"` } // Schedule 是两套定时任务的配置。 // 快照低频(默认每天一次),物化略高频(默认每 2–4 小时一次)。 type Schedule struct { // Enable 是否启用定时任务。 Enable bool `mapstructure:"enable"` // Snapshot 快照 cron(秒级)。 Snapshot string `mapstructure:"snapshot"` // Sync 物化 cron(秒级)。 Sync string `mapstructure:"sync"` // SnapshotDryRun 物化是否默认 dry-run(快照永远直接落库)。 SyncDryRun bool `mapstructure:"sync-dry-run"` } // Guard 是物化前的安全阈值,防止飞书权限被误改时一次性误删大量账号。 type Guard struct { // MaxDeactivate 单次允许删除(离职)的最大账号数,0 表示不限制。 MaxDeactivate int `mapstructure:"max-deactivate"` // MaxDeactivatePercent 单次允许删除的账号比例上限(0-1),0 表示不限制。 MaxDeactivatePercent float64 `mapstructure:"max-deactivate-percent"` } // Load 读取配置文件并应用默认值与校验。 func Load(path string) (*Config, error) { v := viper.New() setDefaults(v) if path != "" { v.SetConfigFile(path) } else { v.SetConfigName("config") v.SetConfigType("yaml") v.AddConfigPath(".") v.AddConfigPath("/etc/idsync") } if err := v.ReadInConfig(); err != nil { return nil, fmt.Errorf("读取配置失败: %w", err) } cfg := &Config{} if err := v.Unmarshal(cfg); err != nil { return nil, fmt.Errorf("解析配置失败: %w", err) } if err := cfg.normalize(); err != nil { return nil, err } return cfg, nil } func setDefaults(v *viper.Viper) { v.SetDefault("system.mode", "release") v.SetDefault("system.data-dir", "data") v.SetDefault("server.listen", "0.0.0.0:8899") v.SetDefault("server.admin-user", "admin") v.SetDefault("server.secure-cookie", false) v.SetDefault("feishu.qps", 5) v.SetDefault("feishu.timeout-sec", 30) v.SetDefault("ldap.url", "ldap://localhost:3890") v.SetDefault("ldap.timeout-sec", 15) v.SetDefault("ldap.start-tls", false) v.SetDefault("ldap.insecure-skip-verify", false) v.SetDefault("lldap.base-url", "http://localhost:17170") v.SetDefault("lldap.admin-user", "admin") v.SetDefault("database.source", "data/idsync.db") v.SetDefault("schedule.enable", false) v.SetDefault("schedule.snapshot", "0 20 0 * * *") v.SetDefault("schedule.sync", "0 30 */2 * * *") v.SetDefault("schedule.sync-dry-run", true) v.SetDefault("guard.max-deactivate", 0) v.SetDefault("guard.max-deactivate-percent", 0.2) } // NormalizeForTest 暴露配置规范化,供测试从结构体直接构造配置时使用。 func (c *Config) NormalizeForTest() error { return c.normalize() } // normalize 补全并校验配置。 func (c *Config) normalize() error { if c.Feishu.AppID == "" || c.Feishu.AppSecret == "" { return fmt.Errorf("feishu.app-id 与 feishu.app-secret 不能为空") } if c.Ldap.BaseDN == "" { return fmt.Errorf("ldap.base-dn 不能为空") } if c.Ldap.AdminPass == "" { return fmt.Errorf("ldap.admin-pass 不能为空") } if c.Lldap.BaseURL == "" { return fmt.Errorf("lldap.base-url 不能为空") } if c.Lldap.AdminUser == "" { c.Lldap.AdminUser = "admin" } if c.Lldap.AdminPass == "" { return fmt.Errorf("lldap.admin-pass 不能为空") } if c.Server.AdminPassword == "" && c.Server.AdminPasswordHash == "" { return fmt.Errorf("server.admin-password 或 server.admin-password-hash 必须提供其中之一") } if c.Schedule.Enable && (c.Schedule.Snapshot == "" || c.Schedule.Sync == "") { return fmt.Errorf("schedule.enable=true 时 snapshot 与 sync 的 cron 都不能为空") } // LLDAP 管理员 LDAP DN 固定挂在 ou=people 下(LLDAP 无其他 OU)。 // 未显式给定时按约定补全。 if strings.TrimSpace(c.Ldap.AdminDN) == "" { c.Ldap.AdminDN = "uid=" + c.Lldap.AdminUser + "," + c.UserOUDN() } return nil } // UserOUDN 返回用户 OU 的完整 DN。LLDAP 用户固定挂在 ou=people 下。 func (c *Config) UserOUDN() string { return "ou=people," + strings.TrimSpace(c.Ldap.BaseDN) } // GroupRootDN 返回部门组根 OU 的完整 DN。LLDAP 分组固定挂在 ou=groups 下。 func (c *Config) GroupRootDN() string { return "ou=groups," + strings.TrimSpace(c.Ldap.BaseDN) }