mirror of
https://github.com/netauth/ldap.git
synced 2026-11-03 11:43:44 -06:00
internal/ldap: Validate the domain and make SetDomain repeatable
This commit is contained in:
parent
bdc0d85b53
commit
9a270abbf2
68
internal/ldap/domain_test.go
Normal file
68
internal/ldap/domain_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package ldap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
ldap "github.com/ps78674/ldapserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetDomainValidation(t *testing.T) {
|
||||||
|
s := New(WithLogger(testLogger()))
|
||||||
|
|
||||||
|
for _, bad := range []string{"", "example..com", ".example.com", "example.com."} {
|
||||||
|
if err := s.SetDomain(bad); err == nil {
|
||||||
|
t.Errorf("SetDomain(%q) accepted an invalid domain", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.SetDomain("example.com"); err != nil {
|
||||||
|
t.Fatalf("SetDomain(example.com): %s", err)
|
||||||
|
}
|
||||||
|
want := []string{"dc=netauth", "dc=example", "dc=com"}
|
||||||
|
if strings.Join(s.nc, ",") != strings.Join(want, ",") {
|
||||||
|
t.Errorf("naming context is %v, want %v", s.nc, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search routes depend on the naming context, so a search must not be
|
||||||
|
// routed at all until a domain is configured.
|
||||||
|
func TestSearchBeforeSetDomain(t *testing.T) {
|
||||||
|
s := New(WithLogger(testLogger()), WithNetAuth(&fakeClient{}))
|
||||||
|
|
||||||
|
w := &recordingWriter{}
|
||||||
|
s.ServeLDAP(w, mustDecode(t, berSearchRequest("ou=entities,dc=netauth")))
|
||||||
|
|
||||||
|
code := resultCode(t, w.responses[len(w.responses)-1])
|
||||||
|
if code == ldap.LDAPResultSuccess {
|
||||||
|
t.Error("a search was served without a configured domain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconfiguring the domain must replace the old routes rather than
|
||||||
|
// adding to them, otherwise a renamed server keeps serving its
|
||||||
|
// previous base DN forever.
|
||||||
|
func TestSetDomainReplacesRoutes(t *testing.T) {
|
||||||
|
s := New(WithLogger(testLogger()), WithNetAuth(&fakeClient{}))
|
||||||
|
|
||||||
|
if err := s.SetDomain("example.com"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := s.SetDomain("other.org"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The old base DN must no longer route to the entity handler.
|
||||||
|
old := &recordingWriter{}
|
||||||
|
s.ServeLDAP(old, mustDecode(t, berSearchRequest("ou=entities,dc=netauth,dc=example,dc=com")))
|
||||||
|
if code := resultCode(t, old.responses[len(old.responses)-1]); code != ldap.LDAPResultUnwillingToPerform {
|
||||||
|
t.Errorf("stale base DN returned %d, want %d (unwillingToPerform)", code, ldap.LDAPResultUnwillingToPerform)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The new base DN must route.
|
||||||
|
fresh := &recordingWriter{}
|
||||||
|
s.ServeLDAP(fresh, mustDecode(t, berSearchRequest("ou=entities,dc=netauth,dc=other,dc=org")))
|
||||||
|
if code := resultCode(t, fresh.responses[len(fresh.responses)-1]); code == ldap.LDAPResultUnwillingToPerform {
|
||||||
|
t.Error("the current base DN does not route to the entity handler")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package ldap
|
package ldap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
ldap "github.com/ps78674/ldapserver"
|
ldap "github.com/ps78674/ldapserver"
|
||||||
|
|
@ -14,20 +15,47 @@ func New(opts ...Option) *server {
|
||||||
}
|
}
|
||||||
x.Server = ldap.NewServer()
|
x.Server = ldap.NewServer()
|
||||||
|
|
||||||
x.routes = ldap.NewRouteMux()
|
// The underlying server only permits a single handler and aborts
|
||||||
x.routes.NotFound(x.guard(x.handleNotFound, failOperationError))
|
// the process on a second registration, so a stable delegating
|
||||||
x.routes.Abandon(x.guard(x.handleAbandon, nil))
|
// handler is registered once and SetDomain swaps the routed mux
|
||||||
x.routes.Bind(x.guard(x.handleBind, failBindError))
|
// underneath it.
|
||||||
|
x.Handle(x)
|
||||||
|
|
||||||
x.routes.Search(x.guard(x.handleSearchDSE, failSearchError)).
|
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("").
|
BaseDn("").
|
||||||
Scope(ldap.SearchRequestScopeBaseObject).
|
Scope(ldap.SearchRequestScopeBaseObject).
|
||||||
Filter("(objectclass=*)").
|
Filter("(objectclass=*)").
|
||||||
Label("Search - ROOT DSE")
|
Label("Search - ROOT DSE")
|
||||||
|
|
||||||
x.Handle(x.routes)
|
if len(s.nc) > 0 {
|
||||||
|
base := strings.Join(s.nc, ",")
|
||||||
|
|
||||||
return x
|
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.
|
// Serve serves a plaintext DSA on the provided bind string.
|
||||||
|
|
@ -68,30 +96,42 @@ func (s *server) handleAbandon(w ldap.ResponseWriter, m *ldap.Message) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) SetDomain(domain string) {
|
// SetDomain configures the naming context from a plain domain name
|
||||||
nc := "dc=netauth,"
|
// such as "example.com". NetAuth has no notion of a base DN, so it is
|
||||||
parts := strings.Split(domain, ".")
|
// supplied here: the domain is split on dots, each label becomes a
|
||||||
for i := range parts {
|
// dc component, and dc=netauth is prepended. It must be called after
|
||||||
nc += "dc=" + parts[i] + ","
|
// New and is safe to call more than once.
|
||||||
}
|
func (s *server) SetDomain(domain string) error {
|
||||||
nc = strings.TrimSuffix(nc, ",")
|
if domain == "" {
|
||||||
|
return errors.New("a domain must be configured")
|
||||||
s.nc = strings.Split(nc, ",")
|
|
||||||
for i := range s.nc {
|
|
||||||
s.nc[i] = strings.TrimSpace(s.nc[i])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register routes that are dependent on the namingConvention
|
labels := strings.Split(domain, ".")
|
||||||
entitySearchDN := "ou=entities," + strings.Join(s.nc, ",")
|
nc := []string{"dc=netauth"}
|
||||||
s.routes.Search(s.guard(s.handleSearchEntities, failSearchError)).
|
for _, l := range labels {
|
||||||
BaseDn(entitySearchDN).
|
l = strings.TrimSpace(l)
|
||||||
Scope(ldap.SearchRequestHomeSubtree).
|
if l == "" {
|
||||||
Label("Search - Entities")
|
return errors.New("domain contains an empty label")
|
||||||
|
}
|
||||||
groupSearchDN := "ou=groups," + strings.Join(s.nc, ",")
|
nc = append(nc, "dc="+l)
|
||||||
s.routes.Search(s.guard(s.handleSearchGroups, failSearchError)).
|
}
|
||||||
BaseDn(groupSearchDN).
|
|
||||||
Scope(ldap.SearchRequestHomeSubtree).
|
|
||||||
Label("Search - Groups")
|
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
main.go
5
main.go
|
|
@ -72,7 +72,10 @@ func main() {
|
||||||
ldap.WithAnonBind(viper.GetBool("ldap.allow_anon")),
|
ldap.WithAnonBind(viper.GetBool("ldap.allow_anon")),
|
||||||
)
|
)
|
||||||
|
|
||||||
ls.SetDomain(viper.GetString("ldap.domain"))
|
if err := ls.SetDomain(viper.GetString("ldap.domain")); err != nil {
|
||||||
|
appLogger.Error("Error configuring the base DN", "error", err)
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
|
||||||
if !viper.GetBool("ldap.tls") {
|
if !viper.GetBool("ldap.tls") {
|
||||||
if !strings.HasPrefix(viper.GetString("ldap.bind"), "localhost") {
|
if !strings.HasPrefix(viper.GetString("ldap.bind"), "localhost") {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue