// lldap-export exports the LLDAP directory as a canonical YAML snapshot // for use with the validator and migration tools. package main import ( "context" "flag" "fmt" "os" "keycape/internal/adapters/lldap" "keycape/internal/migration/lldapexport" "keycape/internal/server/telemetry" "keycape/internal/validator" "github.com/rs/zerolog" ) func main() { // Flags. url := flag.String("url", "ldap://localhost:389", "LLDAP server URL (ldap:// or ldaps://)") bindDN := flag.String("bind-dn", "", "Service account bind DN (required)") bindPW := flag.String("bind-pw", "", "Service account password (required)") baseDN := flag.String("base-dn", "", "LDAP search base DN (required)") output := flag.String("output", "canonical-export.yaml", "Output file path") tlsSkip := flag.Bool("tls-skip-verify", false, "Skip TLS certificate verification (dev only)") flag.Parse() if *bindDN == "" || *baseDN == "" { fmt.Fprintln(os.Stderr, "lldap-export: --bind-dn and --base-dn are required") flag.Usage() os.Exit(1) } log := zerolog.New(os.Stderr).With().Timestamp().Logger() emitter := telemetry.NewLogEmitter(log) cfg := lldap.Config{ URL: *url, BindDN: *bindDN, BindPW: *bindPW, BaseDN: *baseDN, TLSSkipVerify: *tlsSkip, } repo := lldap.New(cfg) exp := lldapexport.New(repo, validator.ModeProvisioning, emitter) result, err := exp.Export(context.Background(), *output) if err != nil { fmt.Fprintf(os.Stderr, "lldap-export: export failed: %v\n", err) os.Exit(1) } fmt.Fprintf(os.Stdout, "Exported %d users, %d groups to %s (group enumeration: %s)\n", len(result.Users), len(result.Groups), *output, result.GroupEnumeration) if result.GroupEnumeration != lldapexport.EnumerationDirectory { fmt.Fprintln(os.Stderr, "lldap-export: groups were derived from user memberships; groups with no members are absent") } if len(result.IncompatibilityReport) > 0 { fmt.Fprintln(os.Stderr, "Incompatibility report:") for _, item := range result.IncompatibilityReport { fmt.Fprintln(os.Stderr, " -", item) } os.Exit(2) // partial success: exported with warnings } }