diff --git a/outpost/go.mod b/outpost/go.mod index d275e9c1e..9664acdc5 100644 --- a/outpost/go.mod +++ b/outpost/go.mod @@ -39,7 +39,7 @@ require ( golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 // indirect golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect - golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect + golang.org/x/sys v0.0.0-20210426080607-c94f62235c83 // indirect google.golang.org/appengine v1.6.7 // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect diff --git a/outpost/go.sum b/outpost/go.sum index aca1116cb..d08d4491c 100644 --- a/outpost/go.sum +++ b/outpost/go.sum @@ -778,8 +778,6 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210331060903-cb1fcc7394e5/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d h1:BgJvlyh+UqCUaPlscHJ+PN8GcpfrFdr7NHjd1JL0+Gs= -golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 h1:0PC75Fz/kyMGhL0e1QnypqK2kQMqKt9csD1GnMJR+Zk= golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -852,11 +850,9 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4= -golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426080607-c94f62235c83 h1:kHSDPqCtsHZOg0nVylfTo20DDhE9gG4Y0jn7hKQ0QAM= +golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/outpost/pkg/ldap/api.go b/outpost/pkg/ldap/api.go index 142826483..be3e2cdc1 100644 --- a/outpost/pkg/ldap/api.go +++ b/outpost/pkg/ldap/api.go @@ -1,10 +1,38 @@ package ldap import ( + "errors" + "fmt" + "strings" + log "github.com/sirupsen/logrus" + "goauthentik.io/outpost/pkg/client/outposts" ) func (ls *LDAPServer) Refresh() error { + outposts, err := ls.ac.Client.Outposts.OutpostsLdapList(outposts.NewOutpostsLdapListParams(), ls.ac.Auth) + if err != nil { + return err + } + if len(outposts.Payload.Results) < 1 { + return errors.New("no ldap provider defined") + } + providers := make([]*ProviderInstance, len(outposts.Payload.Results)) + for idx, provider := range outposts.Payload.Results { + userDN := strings.ToLower(fmt.Sprintf("cn=users,%s", provider.BaseDn)) + groupDN := strings.ToLower(fmt.Sprintf("cn=groups,%s", provider.BaseDn)) + providers[idx] = &ProviderInstance{ + BaseDN: provider.BaseDn, + GroupDN: groupDN, + UserDN: userDN, + appSlug: *provider.ApplicationSlug, + flowSlug: *provider.BindFlowSlug, + s: ls, + log: log.WithField("provider", provider.Name), + } + } + ls.providers = providers + ls.log.Info("Update providers") return nil } diff --git a/outpost/pkg/ldap/bind.go b/outpost/pkg/ldap/bind.go index e3671bfad..f1f6bf5f1 100644 --- a/outpost/pkg/ldap/bind.go +++ b/outpost/pkg/ldap/bind.go @@ -1,20 +1,9 @@ package ldap import ( - "context" - "errors" - "fmt" "net" - "net/http" - "net/http/cookiejar" - "strings" - - goldap "github.com/go-ldap/ldap/v3" - httptransport "github.com/go-openapi/runtime/client" "github.com/nmcclain/ldap" - "goauthentik.io/outpost/pkg/client/core" - "goauthentik.io/outpost/pkg/client/flows" ) type UIDResponse struct { @@ -25,106 +14,13 @@ type PasswordResponse struct { Password string `json:"password"` } -func (ls *LDAPServer) getUsername(dn string) (string, error) { - if !strings.HasSuffix(dn, ls.BaseDN) { - return "", errors.New("invalid base DN") - } - dns, err := goldap.ParseDN(dn) - if err != nil { - return "", err - } - for _, part := range dns.RDNs { - for _, attribute := range part.Attributes { - if attribute.Type == "DN" { - return attribute.Value, nil - } - } - } - return "", errors.New("failed to find dn") -} - func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { - username, err := ls.getUsername(bindDN) - if err != nil { - ls.log.WithError(err).Warning("failed to parse user dn") - return ldap.LDAPResultInvalidCredentials, nil - } - ls.log.WithField("dn", username).Debug("bind") - jar, err := cookiejar.New(nil) - if err != nil { - ls.log.WithError(err).Warning("Failed to create cookiejar") - return ldap.LDAPResultOperationsError, nil - } - client := &http.Client{ - Jar: jar, - } - passed, err := ls.solveFlowChallenge(username, bindPW, client) - if err != nil { - ls.log.WithField("dn", username).WithError(err).Warning("failed to solve challenge") - return ldap.LDAPResultOperationsError, nil - } - if !passed { - return ldap.LDAPResultInvalidCredentials, nil - } - _, err = ls.ac.Client.Core.CoreApplicationsCheckAccess(&core.CoreApplicationsCheckAccessParams{ - Slug: ls.appSlug, - Context: context.Background(), - HTTPClient: client, - }, httptransport.PassThroughAuth) - if err != nil { - if _, denied := err.(*core.CoreApplicationsCheckAccessForbidden); denied { - ls.log.WithField("dn", username).Info("Access denied for user") - return ldap.LDAPResultInvalidCredentials, nil - } - ls.log.WithField("dn", username).WithError(err).Warning("failed to check access") - return ldap.LDAPResultOperationsError, nil - } - ls.log.WithField("dn", username).Info("User has access") - return ldap.LDAPResultSuccess, nil -} - -func (ls *LDAPServer) solveFlowChallenge(bindDN string, password string, client *http.Client) (bool, error) { - challenge, err := ls.ac.Client.Flows.FlowsExecutorGet(&flows.FlowsExecutorGetParams{ - FlowSlug: ls.flowSlug, - Query: "ldap=true", - Context: context.Background(), - HTTPClient: client, - }, httptransport.PassThroughAuth) - if err != nil { - ls.log.WithError(err).Warning("Failed to get challenge") - return false, err - } - ls.log.WithField("component", challenge.Payload.Component).WithField("type", *challenge.Payload.Type).Debug("Got challenge") - responseParams := &flows.FlowsExecutorSolveParams{ - FlowSlug: ls.flowSlug, - Query: "ldap=true", - Context: context.Background(), - HTTPClient: client, - } - switch challenge.Payload.Component { - case "ak-stage-identification": - responseParams.Data = &UIDResponse{UIDFIeld: bindDN} - case "ak-stage-password": - responseParams.Data = &PasswordResponse{Password: password} - default: - return false, fmt.Errorf("unsupported challenge type: %s", challenge.Payload.Component) - } - response, err := ls.ac.Client.Flows.FlowsExecutorSolve(responseParams, httptransport.PassThroughAuth) - ls.log.WithField("component", response.Payload.Component).WithField("type", *response.Payload.Type).Debug("Got response") - if *response.Payload.Type == "redirect" { - return true, nil - } - if err != nil { - ls.log.WithError(err).Warning("Failed to submit challenge") - return false, err - } - if len(response.Payload.ResponseErrors) > 0 { - for key, errs := range response.Payload.ResponseErrors { - for _, err := range errs { - ls.log.WithField("key", key).WithField("code", *err.Code).Debug(*err.String) - return false, nil - } + for _, instance := range ls.providers { + username, err := instance.getUsername(bindDN) + if err == nil { + return instance.Bind(username, bindPW, conn) } } - return ls.solveFlowChallenge(bindDN, password, client) + ls.log.WithField("dn", bindDN).WithField("request", "bind").Warning("No provider found for request") + return ldap.LDAPResultOperationsError, nil } diff --git a/outpost/pkg/ldap/instance_bind.go b/outpost/pkg/ldap/instance_bind.go new file mode 100644 index 000000000..9b91e235c --- /dev/null +++ b/outpost/pkg/ldap/instance_bind.go @@ -0,0 +1,115 @@ +package ldap + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "net/http/cookiejar" + "strings" + + goldap "github.com/go-ldap/ldap/v3" + httptransport "github.com/go-openapi/runtime/client" + "github.com/nmcclain/ldap" + "goauthentik.io/outpost/pkg/client/core" + "goauthentik.io/outpost/pkg/client/flows" +) + +func (pi *ProviderInstance) getUsername(dn string) (string, error) { + if !strings.HasSuffix(dn, pi.BaseDN) { + return "", errors.New("invalid base DN") + } + dns, err := goldap.ParseDN(dn) + if err != nil { + return "", err + } + for _, part := range dns.RDNs { + for _, attribute := range part.Attributes { + if attribute.Type == "DN" { + return attribute.Value, nil + } + } + } + return "", errors.New("failed to find dn") +} + +func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { + jar, err := cookiejar.New(nil) + if err != nil { + pi.log.WithError(err).Warning("Failed to create cookiejar") + return ldap.LDAPResultOperationsError, nil + } + client := &http.Client{ + Jar: jar, + } + passed, err := pi.solveFlowChallenge(username, bindPW, client) + if err != nil { + pi.log.WithField("dn", username).WithError(err).Warning("failed to solve challenge") + return ldap.LDAPResultOperationsError, nil + } + if !passed { + return ldap.LDAPResultInvalidCredentials, nil + } + _, err = pi.s.ac.Client.Core.CoreApplicationsCheckAccess(&core.CoreApplicationsCheckAccessParams{ + Slug: pi.appSlug, + Context: context.Background(), + HTTPClient: client, + }, httptransport.PassThroughAuth) + if err != nil { + if _, denied := err.(*core.CoreApplicationsCheckAccessForbidden); denied { + pi.log.WithField("dn", username).Info("Access denied for user") + return ldap.LDAPResultInvalidCredentials, nil + } + pi.log.WithField("dn", username).WithError(err).Warning("failed to check access") + return ldap.LDAPResultOperationsError, nil + } + pi.log.WithField("dn", username).Info("User has access") + return ldap.LDAPResultSuccess, nil +} + +func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, client *http.Client) (bool, error) { + challenge, err := pi.s.ac.Client.Flows.FlowsExecutorGet(&flows.FlowsExecutorGetParams{ + FlowSlug: pi.flowSlug, + Query: "ldap=true", + Context: context.Background(), + HTTPClient: client, + }, httptransport.PassThroughAuth) + if err != nil { + pi.log.WithError(err).Warning("Failed to get challenge") + return false, err + } + pi.log.WithField("component", challenge.Payload.Component).WithField("type", *challenge.Payload.Type).Debug("Got challenge") + responseParams := &flows.FlowsExecutorSolveParams{ + FlowSlug: pi.flowSlug, + Query: "ldap=true", + Context: context.Background(), + HTTPClient: client, + } + switch challenge.Payload.Component { + case "ak-stage-identification": + responseParams.Data = &UIDResponse{UIDFIeld: bindDN} + case "ak-stage-password": + responseParams.Data = &PasswordResponse{Password: password} + default: + return false, fmt.Errorf("unsupported challenge type: %s", challenge.Payload.Component) + } + response, err := pi.s.ac.Client.Flows.FlowsExecutorSolve(responseParams, httptransport.PassThroughAuth) + pi.log.WithField("component", response.Payload.Component).WithField("type", *response.Payload.Type).Debug("Got response") + if *response.Payload.Type == "redirect" { + return true, nil + } + if err != nil { + pi.log.WithError(err).Warning("Failed to submit challenge") + return false, err + } + if len(response.Payload.ResponseErrors) > 0 { + for key, errs := range response.Payload.ResponseErrors { + for _, err := range errs { + pi.log.WithField("key", key).WithField("code", *err.Code).Debug(*err.String) + return false, nil + } + } + } + return pi.solveFlowChallenge(bindDN, password, client) +} diff --git a/outpost/pkg/ldap/instance_search.go b/outpost/pkg/ldap/instance_search.go new file mode 100644 index 000000000..6d07d9456 --- /dev/null +++ b/outpost/pkg/ldap/instance_search.go @@ -0,0 +1,115 @@ +package ldap + +import ( + "fmt" + "net" + "strconv" + "strings" + + "github.com/nmcclain/ldap" + "goauthentik.io/outpost/pkg/client/core" +) + +func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { + bindDN = strings.ToLower(bindDN) + baseDN := strings.ToLower("," + pi.BaseDN) + + entries := []*ldap.Entry{} + filterEntity, err := ldap.GetFilterObjectClass(searchReq.Filter) + if err != nil { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", searchReq.Filter) + } + if len(bindDN) < 1 { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", bindDN) + } + if !strings.HasSuffix(bindDN, baseDN) { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", bindDN, pi.BaseDN) + } + + switch filterEntity { + default: + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, searchReq.Filter) + case GroupObjectClass: + groups, err := pi.s.ac.Client.Core.CoreGroupsList(core.NewCoreGroupsListParams(), pi.s.ac.Auth) + if err != nil { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err) + } + for _, g := range groups.Payload.Results { + attrs := []*ldap.EntryAttribute{ + { + Name: "cn", + Values: []string{*g.Name}, + }, + { + Name: "uid", + Values: []string{string(g.Pk)}, + }, + { + Name: "objectClass", + Values: []string{GroupObjectClass, "goauthentik.io/ldap/group"}, + }, + } + attrs = append(attrs, AKAttrsToLDAP(g.Attributes)...) + // attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s", g.Name)}}) + // attrs = append(attrs, &ldap.EntryAttribute{Name: "gidNumber", Values: []string{fmt.Sprintf("%d", g.UnixID)}}) + // attrs = append(attrs, &ldap.EntryAttribute{Name: "uniqueMember", Values: h.getGroupMembers(g.UnixID)}) + // attrs = append(attrs, &ldap.EntryAttribute{Name: "memberUid", Values: h.getGroupMemberIDs(g.UnixID)}) + dn := fmt.Sprintf("cn=%s,%s", *g.Name, pi.GroupDN) + entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) + } + case UserObjectClass, "": + users, err := pi.s.ac.Client.Core.CoreUsersList(core.NewCoreUsersListParams(), pi.s.ac.Auth) + if err != nil { + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err) + } + for _, u := range users.Payload.Results { + attrs := []*ldap.EntryAttribute{ + { + Name: "cn", + Values: []string{*u.Username}, + }, + { + Name: "uid", + Values: []string{strconv.Itoa(int(u.Pk))}, + }, + { + Name: "name", + Values: []string{*u.Name}, + }, + { + Name: "displayName", + Values: []string{*u.Name}, + }, + { + Name: "mail", + Values: []string{u.Email.String()}, + }, + { + Name: "objectClass", + Values: []string{UserObjectClass, "organizationalPerson", "goauthentik.io/ldap/user"}, + }, + } + + if u.IsActive { + attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"inactive"}}) + } else { + attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"active"}}) + } + + if *u.IsSuperuser { + attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"inactive"}}) + } else { + attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"active"}}) + } + + // attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: h.getGroupDNs(append(u.OtherGroups, u.PrimaryGroup))}) + + attrs = append(attrs, AKAttrsToLDAP(u.Attributes)...) + + dn := fmt.Sprintf("cn=%s,%s", *u.Name, pi.UserDN) + entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) + } + } + pi.log.WithField("filter", searchReq.Filter).Debug("Search OK") + return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil +} diff --git a/outpost/pkg/ldap/ldap.go b/outpost/pkg/ldap/ldap.go index 2d4bc132b..68c6add96 100644 --- a/outpost/pkg/ldap/ldap.go +++ b/outpost/pkg/ldap/ldap.go @@ -1,9 +1,6 @@ package ldap import ( - "fmt" - "strings" - log "github.com/sirupsen/logrus" "goauthentik.io/outpost/pkg/ak" @@ -13,33 +10,35 @@ import ( const GroupObjectClass = "group" const UserObjectClass = "user" -type LDAPServer struct { +type ProviderInstance struct { BaseDN string - userDN string - groupDN string + UserDN string + GroupDN string + appSlug string + flowSlug string + s *LDAPServer + log *log.Entry +} + +type LDAPServer struct { s *ldap.Server log *log.Entry ac *ak.APIController - // TODO: Make configurable - flowSlug string - appSlug string + providers []*ProviderInstance } func NewServer(ac *ak.APIController) *LDAPServer { s := ldap.NewServer() s.EnforceLDAP = true ls := &LDAPServer{ - s: s, - log: log.WithField("logger", "ldap-server"), - ac: ac, - - BaseDN: "DC=ldap,DC=goauthentik,DC=io", + s: s, + log: log.WithField("logger", "ldap-server"), + ac: ac, + providers: []*ProviderInstance{}, } - ls.userDN = strings.ToLower(fmt.Sprintf("cn=users,%s", ls.BaseDN)) - ls.groupDN = strings.ToLower(fmt.Sprintf("cn=groups,%s", ls.BaseDN)) s.BindFunc("", ls) s.SearchFunc("", ls) return ls diff --git a/outpost/pkg/ldap/search.go b/outpost/pkg/ldap/search.go index 3eb0049b8..79f1626c6 100644 --- a/outpost/pkg/ldap/search.go +++ b/outpost/pkg/ldap/search.go @@ -1,115 +1,23 @@ package ldap import ( - "fmt" + "errors" "net" - "strconv" - "strings" + goldap "github.com/go-ldap/ldap/v3" "github.com/nmcclain/ldap" - "goauthentik.io/outpost/pkg/client/core" ) -func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { - bindDN = strings.ToLower(bindDN) - baseDN := strings.ToLower("," + ls.BaseDN) - - entries := []*ldap.Entry{} - filterEntity, err := ldap.GetFilterObjectClass(searchReq.Filter) +func (ls *LDAPServer) Search(boundDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { + bd, err := goldap.ParseDN(boundDN) if err != nil { - return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", searchReq.Filter) + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("invalid DN") } - if len(bindDN) < 1 { - return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", bindDN) - } - if !strings.HasSuffix(bindDN, baseDN) { - return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", bindDN, ls.BaseDN) - } - - switch filterEntity { - default: - return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, searchReq.Filter) - case GroupObjectClass: - groups, err := ls.ac.Client.Core.CoreGroupsList(core.NewCoreGroupsListParams(), ls.ac.Auth) - if err != nil { - return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err) - } - for _, g := range groups.Payload.Results { - attrs := []*ldap.EntryAttribute{ - { - Name: "cn", - Values: []string{*g.Name}, - }, - { - Name: "uid", - Values: []string{string(g.Pk)}, - }, - { - Name: "objectClass", - Values: []string{GroupObjectClass, "goauthentik.io/ldap/group"}, - }, - } - attrs = append(attrs, AKAttrsToLDAP(g.Attributes)...) - // attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s", g.Name)}}) - // attrs = append(attrs, &ldap.EntryAttribute{Name: "gidNumber", Values: []string{fmt.Sprintf("%d", g.UnixID)}}) - // attrs = append(attrs, &ldap.EntryAttribute{Name: "uniqueMember", Values: h.getGroupMembers(g.UnixID)}) - // attrs = append(attrs, &ldap.EntryAttribute{Name: "memberUid", Values: h.getGroupMemberIDs(g.UnixID)}) - dn := fmt.Sprintf("cn=%s,%s", *g.Name, ls.groupDN) - entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) - } - case UserObjectClass, "": - users, err := ls.ac.Client.Core.CoreUsersList(core.NewCoreUsersListParams(), ls.ac.Auth) - if err != nil { - return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err) - } - for _, u := range users.Payload.Results { - attrs := []*ldap.EntryAttribute{ - { - Name: "cn", - Values: []string{*u.Username}, - }, - { - Name: "uid", - Values: []string{strconv.Itoa(int(u.Pk))}, - }, - { - Name: "name", - Values: []string{*u.Name}, - }, - { - Name: "displayName", - Values: []string{*u.Name}, - }, - { - Name: "mail", - Values: []string{u.Email.String()}, - }, - { - Name: "objectClass", - Values: []string{UserObjectClass, "organizationalPerson", "goauthentik.io/ldap/user"}, - }, - } - - if u.IsActive { - attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"inactive"}}) - } else { - attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"active"}}) - } - - if *u.IsSuperuser { - attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"inactive"}}) - } else { - attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"active"}}) - } - - // attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: h.getGroupDNs(append(u.OtherGroups, u.PrimaryGroup))}) - - attrs = append(attrs, AKAttrsToLDAP(u.Attributes)...) - - dn := fmt.Sprintf("cn=%s,%s", *u.Name, ls.userDN) - entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs}) + for _, provider := range ls.providers { + providerBase, _ := goldap.ParseDN(provider.BaseDN) + if providerBase.AncestorOf(bd) { + return provider.Search(boundDN, searchReq, conn) } } - ls.log.WithField("filter", searchReq.Filter).Debug("Search OK") - return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil + return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("invalid DN") }