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)) routes.Abandon(s.guard(s.handleAbandon)) routes.Bind(s.guard(s.handleBind)) routes.Search(s.guard(s.handleSearchDSE)). 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)). BaseDn("ou=entities," + base). Scope(ldap.SearchRequestHomeSubtree). Label("Search - Entities") routes.Search(s.guard(s.handleSearchGroups)). BaseDn("ou=groups," + base). Scope(ldap.SearchRequestHomeSubtree). Label("Search - Groups") } // Catch-all route for single-entry lookups, registered last so // the specific routes above win. Clients such as HashiCorp Vault // address one entry directly with a base-object search whose base // DN is the entry itself; that DN has no fixed value to match on, // so only the scope can be matched here and the DN is dispatched // inside the handler. Subtree searches with an unknown base stay // unrouted and are still refused outright. routes.Search(s.guard(s.handleSearchEntry)). Scope(ldap.SearchRequestScopeBaseObject). Label("Search - Single Entry") 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) { s.l.Warn("Refusing unrouted operation", "op", r.ProtocolOpName(), "message", r.String()) writeResult(w, r, ldap.LDAPResultUnwillingToPerform, "Operation not implemented by server, searches must use a base DN of ou=entities or ou=groups underneath the naming context") } 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 { writeResult(w, r, ldap.LDAPResultUnavailable, "server is not configured with a domain") return } s.routes.ServeLDAP(w, r) }