From 2cf2849cea03d4e12f02d01a5d4d2895d8850f59 Mon Sep 17 00:00:00 2001 From: Michael Aldridge Date: Sun, 23 Aug 2020 00:14:41 -0700 Subject: [PATCH] internal/ldap: Implement search for entities --- go.mod | 1 + internal/ldap/ldap.go | 22 +++++--- internal/ldap/search.go | 120 +++++++++++++++++++++++++++++++--------- internal/ldap/type.go | 6 ++ 4 files changed, 115 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index 2bf8f4c..e820f73 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/hashicorp/go-hclog v0.9.2 github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3 // indirect github.com/netauth/netauth v0.3.4 + github.com/netauth/protocol v0.0.0-20191124005711-167b58b61c72 github.com/ps78674/goldap v0.0.0-20200721080011-cd2e7ee23841 github.com/ps78674/ldapserver v0.0.0-20200521101606-2395f680392c github.com/spf13/viper v1.3.2 diff --git a/internal/ldap/ldap.go b/internal/ldap/ldap.go index deed242..404dcdb 100644 --- a/internal/ldap/ldap.go +++ b/internal/ldap/ldap.go @@ -17,20 +17,18 @@ func New(l hclog.Logger, nacl naClient) *server { x.c = nacl x.Server = ldap.NewServer() - routes := ldap.NewRouteMux() - routes.NotFound(x.handleNotFound) - routes.Abandon(x.handleAbandon) - routes.Bind(x.handleBind) + x.routes = ldap.NewRouteMux() + x.routes.NotFound(x.handleNotFound) + x.routes.Abandon(x.handleAbandon) + x.routes.Bind(x.handleBind) - routes.Search(x.handleSearchDSE). + x.routes.Search(x.handleSearchDSE). BaseDn(""). Scope(ldap.SearchRequestScopeBaseObject). Filter("(objectclass=*)"). Label("Search - ROOT DSE") - routes.Search(x.handleSearch).Label("Search - Generic") - - x.Handle(routes) + x.Handle(x.routes) return x } @@ -88,4 +86,12 @@ func (s *server) SetDomain(domain string) { for i := range s.nc { s.nc[i] = strings.TrimSpace(s.nc[i]) } + + // Register routes that are dependent on the namingConvention + entitySearchDN := "ou=entities," + strings.Join(s.nc, ",") + s.routes.Search(s.handleSearchEntities). + BaseDn(entitySearchDN). + Scope(ldap.SearchRequestHomeSubtree). + Label("Search - Entities") + } diff --git a/internal/ldap/search.go b/internal/ldap/search.go index e64d32f..78889a9 100644 --- a/internal/ldap/search.go +++ b/internal/ldap/search.go @@ -1,13 +1,17 @@ package ldap import ( - "log" + "context" + "errors" + "strconv" "strings" "github.com/ps78674/goldap/message" ldap "github.com/ps78674/ldapserver" "github.com/netauth/ldap/internal/buildinfo" + + pb "github.com/netauth/protocol" ) func (s *server) handleSearchDSE(w ldap.ResponseWriter, m *ldap.Message) { @@ -25,37 +29,101 @@ func (s *server) handleSearchDSE(w ldap.ResponseWriter, m *ldap.Message) { w.Write(res) } -func (s *server) handleSearch(w ldap.ResponseWriter, m *ldap.Message) { - r := m.GetSearchRequest() - log.Printf("Request BaseDn=%s", r.BaseObject()) - log.Printf("Request Filter=%s", r.Filter()) - log.Printf("Request FilterString=%s", r.FilterString()) - log.Printf("Request Attributes=%s", r.Attributes()) - log.Printf("Request TimeLimit=%d", r.TimeLimit().Int()) +func (s *server) handleSearchEntities(w ldap.ResponseWriter, m *ldap.Message) { + ctx := context.Background() + s.l.Debug("Search Entities") - // Handle Stop Signal (server stop / client disconnected / Abandoned request....) - select { - case <-m.Done: - log.Print("Leaving handleSearch...") - return + r := m.GetSearchRequest() + + // This switch performs stage one of mapping from an ldap + // search expression to a NetAuth search expression. The + // second phase of the mapping happens in another function. + var expr string + var err error + switch r.Filter().(type) { + case message.FilterEqualityMatch: + f := r.Filter().(message.FilterEqualityMatch) + expr, err = entitySearchExprHelper(string(f.AttributeDesc()), "=", string(f.AssertionValue())) default: + err = errors.New("unsupported filter type") + } + if err != nil { + // If err is non-nil at this point it must mean that + // the above match didn't find a supported filter. + res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultUnwillingToPerform) + res.SetDiagnosticMessage("Filter type not supported") + w.Write(res) + return } - e := ldap.NewSearchResultEntry("cn=Valere JEANTET, " + string(r.BaseObject())) - e.AddAttribute("mail", "valere.jeantet@gmail.com", "mail@vjeantet.fr") - e.AddAttribute("company", "SODADI") - e.AddAttribute("department", "DSI/SEC") - e.AddAttribute("l", "Ferrieres en brie") - e.AddAttribute("mobile", "0612324567") - e.AddAttribute("telephoneNumber", "0612324567") - e.AddAttribute("cn", "Valère JEANTET") - w.Write(e) + s.l.Debug("Searching entities", "query", expr) - e = ldap.NewSearchResultEntry("cn=Claire Thomas, " + string(r.BaseObject())) - e.AddAttribute("mail", "claire.thomas@gmail.com") - e.AddAttribute("cn", "Claire THOMAS") - w.Write(e) + ents, err := s.c.EntitySearch(ctx, expr) + if err != nil { + res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultOperationsError) + res.SetDiagnosticMessage(err.Error()) + w.Write(res) + return + } + + for i := range ents { + e, err := s.entitySearchResult(ctx, ents[i], r.BaseObject(), r.Attributes()) + if err != nil { + res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultOperationsError) + res.SetDiagnosticMessage(err.Error()) + w.Write(res) + return + } + w.Write(e) + } + + s.l.Debug("Entities", "res", ents) res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultSuccess) w.Write(res) } + +// entitySearchResult maps an entity onto a SearchResultEntry, +// performing the additional lookup for groups to populate the +// memberOf attribute. Though not implemented, the attrs list is +// plumbed down to this level to permit attribute filtering in the +// future. +func (s *server) entitySearchResult(ctx context.Context, e *pb.Entity, dn message.LDAPDN, attrs message.AttributeSelection) (message.SearchResultEntry, error) { + res := ldap.NewSearchResultEntry("uid=" + e.GetID() + "," + string(dn)) + res.AddAttribute("uid", message.AttributeValue(e.GetID())) + res.AddAttribute("uidNumber", message.AttributeValue(strconv.Itoa(int(e.GetNumber())))) + + grps, err := s.c.EntityGroups(ctx, e.GetID()) + if err != nil { + return res, err + } + + for i := range grps { + g := "cn=" + grps[i].GetName() + ",ou=groups," + strings.Join(s.nc, ",") + res.AddAttribute("memberOf", message.AttributeValue(g)) + } + + return res, nil +} + +// entitySearchExprHelper helps in mapping ldap search expressions to +// search expressions that NetAuth understands. +func entitySearchExprHelper(attr, op, val string) (string, error) { + var predicate, operator string + + switch attr { + case "uid": + predicate = "ID" + default: + return "", errors.New("search attribute is unsupported") + } + + switch op { + case "=": + operator = "=" + default: + return "", errors.New("search comparison is unsupported") + } + + return predicate + operator + val, nil +} diff --git a/internal/ldap/type.go b/internal/ldap/type.go index 78f71d6..49cbbe3 100644 --- a/internal/ldap/type.go +++ b/internal/ldap/type.go @@ -5,15 +5,21 @@ import ( "github.com/hashicorp/go-hclog" ldap "github.com/ps78674/ldapserver" + + pb "github.com/netauth/protocol" ) type naClient interface { AuthEntity(context.Context, string, string) error + EntitySearch(context.Context, string) ([]*pb.Entity, error) + EntityGroups(context.Context, string) ([]*pb.Group, error) } type server struct { *ldap.Server + routes *ldap.RouteMux + c naClient l hclog.Logger