Files
caowangrenbo 979a98d502 refactor: config + lldaplib 收敛到 LLDAP 形态(读只读、删 OpenLDAP 写路径与密码包)
- 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 展示
2026-09-29 23:48:49 +08:00

211 lines
7.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}