ldap-upstream/AGENTS.md

7.9 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.

Commands

go build ./...
go vet ./...
go test ./...             # or: go test -race ./internal/ldap/
go run .                  # needs a config file or NETAUTH_LDAP_DOMAIN
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 .

CI (.github/workflows/test.yml) runs build, vet and go test -race on pushes to master and on PRs. Release is via goreleaser triggered by v* tags in .github/workflows/release.yml; its 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 stale generated artifact, not the config actually used — the real one is .goreleaser.yml at the repo root.

Layout

main.go                    # wiring only: config, logging, client, serve
internal/ldap/             # the LDAP server (package ldap)
  ldap.go                  # New(), route (re)building, Serve/ServeTLS, SetDomain
  handler.go               # panic guard wrapper + per-op failure responders
  conn.go                  # per-connection bind state (bound/markBound/markUnbound)
  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
  attributes.go            # RFC 4511 attribute selection matching
  bleve_mapper.go          # LDAP filter -> Bleve query string translation
  util.go                  # DN parsing (entityIDFromDN)
  *_test.go                # tests; see "Testing" below
internal/buildinfo/        # Version var, advertised in RootDSE; set by goreleaser

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. Every handler is wrapped by server.guard so a panic cannot take the process down (the vendored server dispatches requests in goroutines with no recovery of its own).

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. The fork has real limits: TLS floor is SSL 3.0, Server.Handle calls log.Fatalln on double registration, and there is no panic recovery in the request path.
  • The server struct is its own Handler: New() registers *server once (the fork forbids re-registration), and ServeLDAP delegates to whatever s.routes mux is current. SetDomain rebuilds the mux in place, which is why it is safe to call repeatedly. Never call x.Handle() again.
  • SetDomain is required and validated: returns an error for an empty domain or empty labels; searches do not route until it succeeds. Naming context is always dc=netauth,dc=<label>,.... Tree layout under it is fixed: uid=<id>,ou=entities,<base> and cn=<name>,ou=groups,<base>.
  • Bind state is per connection: searches (except RootDSE) are refused with insufficientAccessRights until a successful bind on the same connection (conn.go). A failed bind clears the state per RFC 4511. Anonymous binds succeed only when WithAnonBind(true) and also mark the connection bound.
  • Filter support (bleve_mapper.go): equality, present, AND, OR, NOT (nested only) and simple substrings (js*, *smith, a*th) are supported; only uid (→ ID) and cn (→ Name). Everything else errors rather than returning wrong results. Rules worth knowing before touching this file:
    • Plain values are quoted; wildcard values must match ^[\w.\-*]+$ to prevent Bleve query injection.
    • A top-level (or all-negative) filter errors instead of silently returning zero results, because Bleve cannot anchor a query made only of - clauses.
    • AND branches become +term; negated branches stay -term un-prefixed.
  • DN parsing: entityIDFromDN splits on , without handling escaped commas (accepted defect: NetAuth IDs cannot contain commas). It validates the DN has enough components and that the naming context matches exactly.
  • Search fan-out: entity results include memberOf and group results include member, each costing one extra NetAuth RPC per entry. These are only fetched when the client's attribute selection asks for them (attributes.go); an empty selection or * means all.
  • 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. If you add a NetAuth call, add it to that interface, not directly to the concrete client.

Testing

Tests live in internal/ldap/*_test.go. Two constraints shape them:

  • goldap types are unconstructible from outside: the message/filter types have unexported fields and no exported constructors. Tests hand-encode BER and run it through the real decoder; see the ber* helpers in testutil_test.go (berBindRequest, berSearchRequestWithAttrs, berEqualityFilter, berSubstringFilter, ...). Follow that pattern rather than trying to build request structs directly.
  • Result codes have no getters: the result types expose setters only, so resultCode() in result_test.go re-encodes a response and parses the code back out of the DER.
  • Fakes: implement naClient (fakeClient in bind_test.go records calls and returns canned protobuf data). Per-connection state is faked by setting s.conn to return a fakeConn (boundTestServer in search_test.go). Search tests must bind before searching.
  • Handlers take the ldap.ResponseWriter interface, so assertions use recordingWriter (handler_test.go), which captures the exact response sequence.

Configuration

viper-driven; config file config.{yaml,...} searched in /etc/netauth/, $HOME/.netauth/, .. A missing config file is tolerated (env-only deployments); a corrupt one is fatal (exit 5). Env vars use the NETAUTH_ prefix with _ replacing . (e.g. NETAUTH_LDAP_BIND, NETAUTH_LDAP_DOMAIN, NETAUTH_LOGLEVEL). Known keys (defaults in main.go): ldap.bind (localhost:389), ldap.tls (false), ldap.key, ldap.cert, ldap.allow_anon (false), ldap.domain (no default — required; startup exits with 3 if invalid).

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.
  • Errors returned to LDAP clients go in diagnostic messages; keep them non-technical, they are shown verbatim by LDAP browsers.