outposts/ldap: add ability to use multiple providers on the same outpost
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -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 | ||||
|  | ||||
| @ -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= | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -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 | ||||
| 	for _, instance := range ls.providers { | ||||
| 		username, err := instance.getUsername(bindDN) | ||||
| 		if err == nil { | ||||
| 			return instance.Bind(username, bindPW, conn) | ||||
| 		} | ||||
| 	ls.log.WithField("dn", username).Debug("bind") | ||||
| 	jar, err := cookiejar.New(nil) | ||||
| 	if err != nil { | ||||
| 		ls.log.WithError(err).Warning("Failed to create cookiejar") | ||||
| 	} | ||||
| 	ls.log.WithField("dn", bindDN).WithField("request", "bind").Warning("No provider found for request") | ||||
| 	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 | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return ls.solveFlowChallenge(bindDN, password, client) | ||||
| } | ||||
|  | ||||
							
								
								
									
										115
									
								
								outpost/pkg/ldap/instance_bind.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								outpost/pkg/ldap/instance_bind.go
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||
| } | ||||
							
								
								
									
										115
									
								
								outpost/pkg/ldap/instance_search.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								outpost/pkg/ldap/instance_search.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| } | ||||
| @ -1,9 +1,6 @@ | ||||
| package ldap | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| 	"goauthentik.io/outpost/pkg/ak" | ||||
|  | ||||
| @ -13,19 +10,24 @@ 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 { | ||||
| @ -35,11 +37,8 @@ func NewServer(ac *ak.APIController) *LDAPServer { | ||||
| 		s:         s, | ||||
| 		log:       log.WithField("logger", "ldap-server"), | ||||
| 		ac:        ac, | ||||
|  | ||||
| 		BaseDN: "DC=ldap,DC=goauthentik,DC=io", | ||||
| 		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 | ||||
|  | ||||
| @ -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") | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer