All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 31s
The exporter discovered groups by walking each user's memberships, so a group nobody belongs to never reached the snapshot, and a failed lookup was skipped by a `continue` under a comment claiming it was recorded in the incompatibility report. The run then emitted `result: "success"`. Add an optional `domain.GroupLister` capability and implement `ListGroups` on the LLDAP adapter as a direct group-subtree search, kept off `UserRepository` because the OIDC layer never enumerates the directory. Record `groupEnumeration` on every result and a `Complete()` predicate over it; abort rather than write a smaller snapshot when the enumeration fails; report a failed per-user lookup on the fallback path; emit `partial` telemetry and name the mode from the CLI. Reading the adapter to write this surfaced a defect the assessment had not listed: `LookupGroups` never populated `Group.Members`, and the exporter built every membership from that field, so against a real directory the `memberships` block was always empty while the fixture-backed tests passed. Memberships on the fallback path now come from the user/group pair actually observed. Sort users, groups and memberships so an unchanged directory exports identically. Closes G05 of the scope/intent assessment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WAsfsfQmDu4vcBhiMcmQp Assistant: claude-code Assistant-Model: opus Assistant-Process: 867844@bnt-lap001 Assistant-Session: 3d45905e-0016-4b49-b828-231406881f7b
70 lines
2.1 KiB
Go
70 lines
2.1 KiB
Go
// 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
|
|
}
|
|
}
|