- config 重写:删 user-ou/inactive-ou/group-root-* / group-object-class / group-member-attr / initial-password;新增 lldap 写通道 + webhook + confirm-base-url + 两套 schedule + guard - ldaplib 收拢为只读:属性常量/DN 归入 attrs.go;删 ops.go 写路径与 internal/password - config.example.yml 重写成 LLDAP 形态 - 组命名修订(呼应 AGENTS.md 约束12):用部门稳定索引 cn=<dept_id> 做组身份, mirror_departments 增 group_id 列并跨快照保留,可读中文名只走 DB 展示
211 lines
7.2 KiB
Go
211 lines
7.2 KiB
Go
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,<base>。
|
||
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)
|
||
} |