ldap-upstream/AGENTS.md
2026-11-01 17:18:23 -06:00

6 KiB

AGENTS.md

What this is

A standalone LDAPv3 read-only bridge in front of a NetAuth server (github.com/netauth/netauth). It translates LDAP bind/search operations into NetAuth RPC calls via the netauth/protocol protobuf types. It is not a full LDAP server and never stores data; it only reshapes and serves live NetAuth data. Single static binary (ldapd), intended to run bound to localhost.

There are no tests anywhere in this repo (no *_test.go files). Do not assume go test ./... validates anything.

Commands

go build ./...            # build
go vet ./...
go run .                  # run locally (reads config like the real binary, see below)
go mod vendor             # repo vendors deps; vendor/ and dist/ are gitignored

# Production-style build (matches Dockerfile)
CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o ldapd .

Release is via goreleaser triggered by v* tags in .github/workflows/release.yml; the before-hooks run go mod vendor and scripts/prepare.sh (regenerates NOTICE from vendored license files via scripts/vendor-licenses, which needs the vendor tree present). Note dist/config.yaml is a generated/stale goreleaser artifact, not the config actually used.

Layout

main.go                    # wiring only: config, logging, client, serve
internal/ldap/             # the LDAP server (package ldap)
  ldap.go                  # New(), routing, SetDomain(), Serve/ServeTLS
  type.go                  # server struct + naClient interface (the seam for fakes)
  option.go                # functional options: WithLogger, WithNetAuth, WithAnonBind
  bind.go                  # bind handler (auth against NetAuth)
  search.go                # RootDSE, entity & group search handlers
  bleve_mapper.go          # LDAP filter -> Bleve query string translation
  util.go                  # DN parsing (entityIDFromDN)
internal/buildinfo/        # Version var ("dev"), advertised in RootDSE

Architecture: main builds a viper config + netauth client, constructs the server with functional options, and calls Serve/ServeTLS. The underlying LDAP engine is a RouteMux (net/http-style routing on LDAP ops) with handlers keyed by base DN and scope.

Non-obvious gotchas

  • Forked LDAP libraries: the server stack is github.com/ps78674/ldapserver + github.com/ps78674/goldap, not the more common nwaples/ldapserver / go-ldap packages. Import as ldap "github.com/ps78674/ldapserver". Check the vendored API before assuming upstream signatures.
  • Routes depend on the domain: New() only registers NotFound/Abandon/Bind and the RootDSE route. The ou=entities / ou=groups search routes are registered as a side effect of SetDomain(), which must be called after New() or searches won't route at all. Calling it twice re-registers routes.
  • Base DN construction: SetDomain takes a plain domain (example.com), splits on ., prepends dc=netauth, yielding dc=netauth,dc=example,dc=com. Tree layout under it is fixed: uid=<id>,ou=entities,<base> and cn=<name>,ou=groups,<base>.
  • Supported LDAP filters are extremely limited (bleve_mapper.go): only equality match, OR, and attribute-present filters; only the attributes uid (→ ID:) and cn (→ Name:); values are passed through as raw Bleve query strings to NetAuth's search. Everything else returns unwillingToPerform/"Filter type not supported".
  • Anonymous bind fall-through (bind.go): when allowAnon is enabled and the bind DN is empty, a success response is written but the handler does not return — execution continues into entityIDFromDN(""), which errors and writes a second response. This appears to be a bug; be careful not to "fix" it blindly if behavior parity matters, and definitely add a response write guard if you touch it.
  • Only simple bind — no SASL; anything else is rejected outright.
  • DN parsing known defect: entityIDFromDN splits on , without handling escaped commas (documented as acceptable since NetAuth IDs can't contain commas). Error strings there have typos ("is underneath", "must be rooted"); leave them alone unless asked.
  • Search fan-out: each entity result triggers an extra EntityGroups RPC for memberOf, and each group result triggers GroupMembers for member. A broad search is N+1 RPCs against NetAuth. Group expansion is done server-side by NetAuth (flattened, fully expanded groups).
  • Logging is dual-library: app logging uses hashicorp/go-hclog; the vendored ldapserver uses logrus, and main.go hijacks the global logrus logger to funnel protocol traces into hclog at forced Trace level via NETAUTH_LOGLEVEL. Use s.l (hclog, keyed with "key", value pairs) inside the server package.
  • NetAuth dependency seam: server code talks to NetAuth only through the naClient interface in type.go:12. If you add a NetAuth call, add it to that interface, not directly to the concrete client.
  • Version injection mismatch: goreleaser ldflags set main.version etc., but the RootDSE reports internal/buildinfo.Version, which stays "dev" in built binaries.

Configuration

viper-driven; config file config.{yaml,...} searched in /etc/netauth/, $HOME/.netauth/, . — a missing config file is fatal (exit 5), even with all values provided by env. Env vars use the NETAUTH_ prefix with _ replacing . (e.g. NETAUTH_LDAP_BIND, NETAUTH_LDAP_DOMAIN, NETAUTH_LOGLEVEL). Known keys (defaults in main.go:16-22): ldap.bind (localhost:389), ldap.tls (false), ldap.key, ldap.cert, ldap.allow_anon (false), ldap.domain (no default — required).

Style notes

  • Commit message convention: component: Capitalized description (e.g. internal/ldap: Add option to bind anonmyously).
  • Handlers are methods on *server named handle<X>; response pattern is construct response → w.Write(res) on every exit path.
  • Loop style favors for i := range x throughout.