From dbd0e3d6897aabac7576f7d812f2909cc7560fb6 Mon Sep 17 00:00:00 2001 From: Michael Aldridge Date: Sun, 1 Nov 2026 17:18:23 -0600 Subject: [PATCH] AGENTS.md: Add documentation --- AGENTS.md | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0de438d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,116 @@ +# 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 + +```sh +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](.goreleaser.yml) triggered by `v*` tags in +[.github/workflows/release.yml](.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=,ou=entities,` and + `cn=,ou=groups,`. +- **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`; response pattern is + construct response → `w.Write(res)` on every exit path. +- Loop style favors `for i := range x` throughout.