ldap-upstream/internal/ldap/ldap.go
2026-11-01 22:17:59 -06:00

169 lines
5 KiB
Go

package ldap
import (
"crypto/tls"
"errors"
"fmt"
"strings"
ldap "github.com/vjeantet/ldapserver"
)
// New returns a new ldap server instance
func New(opts ...Option) *server {
x := new(server)
for _, o := range opts {
o(x)
}
x.Server = ldap.NewServer()
// The underlying server only permits a single handler and aborts
// the process on a second registration, so a stable delegating
// handler is registered once and SetDomain swaps the routed mux
// underneath it.
x.Handle(x)
x.buildRoutes()
return x
}
// buildRoutes (re)creates the route mux. It is called by New and again
// by SetDomain so that the naming-convention dependent routes always
// reflect the current domain without ever leaving duplicates behind.
func (s *server) buildRoutes() {
routes := ldap.NewRouteMux()
routes.NotFound(s.guard(s.handleNotFound, failOperationError))
routes.Abandon(s.guard(s.handleAbandon, nil))
routes.Bind(s.guard(s.handleBind, failBindError))
routes.Search(s.guard(s.handleSearchDSE, failSearchError)).
BaseDn("").
Scope(ldap.SearchRequestScopeBaseObject).
Filter("(objectclass=*)").
Label("Search - ROOT DSE")
if len(s.nc) > 0 {
base := strings.Join(s.nc, ",")
routes.Search(s.guard(s.handleSearchEntities, failSearchError)).
BaseDn("ou=entities," + base).
Scope(ldap.SearchRequestHomeSubtree).
Label("Search - Entities")
routes.Search(s.guard(s.handleSearchGroups, failSearchError)).
BaseDn("ou=groups," + base).
Scope(ldap.SearchRequestHomeSubtree).
Label("Search - Groups")
}
s.routes = routes
}
// Serve serves a plaintext DSA on the provided bind string. It
// returns once the listener is bound; the server keeps running in the
// background until Stop is called.
func (s *server) Serve(bind string) error {
return s.listenAndServe(bind)
}
// ServeTLS serves a TLS encrypted DSA on the provided bindstring
// using a key/cert pair located at the paths provided. Key and
// certificate should be PEM encoded.
func (s *server) ServeTLS(bind, keypath, certpath string) error {
cert, err := tls.LoadX509KeyPair(certpath, keypath)
if err != nil {
return fmt.Errorf("error creating certificate chain: %s", err)
}
cfg := &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}
return s.listenAndServe(bind, func(srv *ldap.Server) {
srv.Listener = tls.NewListener(srv.Listener, cfg)
})
}
// listenAndServe binds the listening socket synchronously so that
// bind errors are reported to the caller, then serves in the
// background. The underlying server assigns its Listener field before
// invoking the option functions it is handed, so closing the started
// channel from an option guarantees that bookkeeping (and any TLS
// wrapping) is complete, and in a reliably ordered way, before Serve
// returns. Stop depends on reading that field.
func (s *server) listenAndServe(bind string, opts ...func(*ldap.Server)) error {
started := make(chan struct{})
failed := make(chan error, 1)
go func() {
opts := append(opts, func(*ldap.Server) { close(started) })
if err := s.Server.ListenAndServe(bind, opts...); err != nil {
select {
case <-started:
s.l.Error("Error from main server thread", "error", err)
default:
failed <- err
}
}
}()
select {
case <-started:
return nil
case err := <-failed:
return fmt.Errorf("error creating listener: %s", err)
}
}
func (s *server) handleNotFound(w ldap.ResponseWriter, r *ldap.Message) {
res := ldap.NewResponse(ldap.LDAPResultUnwillingToPerform)
res.SetDiagnosticMessage("Operation not implemented by server")
w.Write(res)
}
func (s *server) handleAbandon(w ldap.ResponseWriter, m *ldap.Message) {
var req = m.GetAbandonRequest()
// retreive the request to abandon, and send a abort signal to it
if requestToAbandon, ok := m.Client.GetMessageByID(int(req)); ok {
requestToAbandon.Abandon()
}
}
// SetDomain configures the naming context from a plain domain name
// such as "example.com". NetAuth has no notion of a base DN, so it is
// supplied here: the domain is split on dots, each label becomes a
// dc component, and dc=netauth is prepended. It must be called after
// New and is safe to call more than once.
func (s *server) SetDomain(domain string) error {
if domain == "" {
return errors.New("a domain must be configured")
}
labels := strings.Split(domain, ".")
nc := []string{"dc=netauth"}
for _, l := range labels {
l = strings.TrimSpace(l)
if l == "" {
return errors.New("domain contains an empty label")
}
nc = append(nc, "dc="+l)
}
s.nc = nc
s.buildRoutes()
return nil
}
// ServeLDAP implements the underlying server's Handler interface. It
// is registered once with the server and forwards to whatever route
// mux is current, which lets SetDomain rebuild routing without
// re-registering a handler (the server aborts on a second
// registration).
func (s *server) ServeLDAP(w ldap.ResponseWriter, r *ldap.Message) {
if s.routes == nil {
res := ldap.NewResponse(ldap.LDAPResultUnavailable)
res.SetDiagnosticMessage("server is not configured with a domain")
w.Write(res)
return
}
s.routes.ServeLDAP(w, r)
}