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/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect | ||||||
| 	golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 // indirect | 	golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 // indirect | ||||||
| 	golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // 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 | 	google.golang.org/appengine v1.6.7 // indirect | ||||||
| 	gopkg.in/ini.v1 v1.62.0 // indirect | 	gopkg.in/ini.v1 v1.62.0 // indirect | ||||||
| 	gopkg.in/square/go-jose.v2 v2.5.1 // 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-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-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-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 h1:0PC75Fz/kyMGhL0e1QnypqK2kQMqKt9csD1GnMJR+Zk= | ||||||
| golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= | 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= | 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-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-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-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw= | golang.org/x/sys v0.0.0-20210426080607-c94f62235c83 h1:kHSDPqCtsHZOg0nVylfTo20DDhE9gG4Y0jn7hKQ0QAM= | ||||||
| golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | 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/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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.0/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 | package ldap | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
|  | 	"goauthentik.io/outpost/pkg/client/outposts" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func (ls *LDAPServer) Refresh() error { | 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 | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,20 +1,9 @@ | |||||||
| package ldap | package ldap | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"net" | 	"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" | 	"github.com/nmcclain/ldap" | ||||||
| 	"goauthentik.io/outpost/pkg/client/core" |  | ||||||
| 	"goauthentik.io/outpost/pkg/client/flows" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type UIDResponse struct { | type UIDResponse struct { | ||||||
| @ -25,106 +14,13 @@ type PasswordResponse struct { | |||||||
| 	Password string `json:"password"` | 	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) { | func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { | ||||||
| 	username, err := ls.getUsername(bindDN) | 	for _, instance := range ls.providers { | ||||||
| 	if err != nil { | 		username, err := instance.getUsername(bindDN) | ||||||
| 		ls.log.WithError(err).Warning("failed to parse user dn") | 		if err == nil { | ||||||
| 		return ldap.LDAPResultInvalidCredentials, 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") |  | ||||||
| 		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) | 	ls.log.WithField("dn", bindDN).WithField("request", "bind").Warning("No provider found for request") | ||||||
|  | 	return ldap.LDAPResultOperationsError, nil | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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 | package ldap | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
| 	"goauthentik.io/outpost/pkg/ak" | 	"goauthentik.io/outpost/pkg/ak" | ||||||
|  |  | ||||||
| @ -13,33 +10,35 @@ import ( | |||||||
| const GroupObjectClass = "group" | const GroupObjectClass = "group" | ||||||
| const UserObjectClass = "user" | const UserObjectClass = "user" | ||||||
|  |  | ||||||
| type LDAPServer struct { | type ProviderInstance struct { | ||||||
| 	BaseDN string | 	BaseDN string | ||||||
|  |  | ||||||
| 	userDN  string | 	UserDN  string | ||||||
| 	groupDN string | 	GroupDN string | ||||||
|  |  | ||||||
|  | 	appSlug  string | ||||||
|  | 	flowSlug string | ||||||
|  | 	s        *LDAPServer | ||||||
|  | 	log      *log.Entry | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type LDAPServer struct { | ||||||
| 	s   *ldap.Server | 	s   *ldap.Server | ||||||
| 	log *log.Entry | 	log *log.Entry | ||||||
| 	ac  *ak.APIController | 	ac  *ak.APIController | ||||||
|  |  | ||||||
| 	// TODO: Make configurable | 	providers []*ProviderInstance | ||||||
| 	flowSlug string |  | ||||||
| 	appSlug  string |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewServer(ac *ak.APIController) *LDAPServer { | func NewServer(ac *ak.APIController) *LDAPServer { | ||||||
| 	s := ldap.NewServer() | 	s := ldap.NewServer() | ||||||
| 	s.EnforceLDAP = true | 	s.EnforceLDAP = true | ||||||
| 	ls := &LDAPServer{ | 	ls := &LDAPServer{ | ||||||
| 		s:   s, | 		s:         s, | ||||||
| 		log: log.WithField("logger", "ldap-server"), | 		log:       log.WithField("logger", "ldap-server"), | ||||||
| 		ac:  ac, | 		ac:        ac, | ||||||
|  | 		providers: []*ProviderInstance{}, | ||||||
| 		BaseDN: "DC=ldap,DC=goauthentik,DC=io", |  | ||||||
| 	} | 	} | ||||||
| 	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.BindFunc("", ls) | ||||||
| 	s.SearchFunc("", ls) | 	s.SearchFunc("", ls) | ||||||
| 	return ls | 	return ls | ||||||
|  | |||||||
| @ -1,115 +1,23 @@ | |||||||
| package ldap | package ldap | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"errors" | ||||||
| 	"net" | 	"net" | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
|  | 	goldap "github.com/go-ldap/ldap/v3" | ||||||
| 	"github.com/nmcclain/ldap" | 	"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) { | func (ls *LDAPServer) Search(boundDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { | ||||||
| 	bindDN = strings.ToLower(bindDN) | 	bd, err := goldap.ParseDN(boundDN) | ||||||
| 	baseDN := strings.ToLower("," + ls.BaseDN) |  | ||||||
|  |  | ||||||
| 	entries := []*ldap.Entry{} |  | ||||||
| 	filterEntity, err := ldap.GetFilterObjectClass(searchReq.Filter) |  | ||||||
| 	if err != nil { | 	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 { | 	for _, provider := range ls.providers { | ||||||
| 		return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", bindDN) | 		providerBase, _ := goldap.ParseDN(provider.BaseDN) | ||||||
| 	} | 		if providerBase.AncestorOf(bd) { | ||||||
| 	if !strings.HasSuffix(bindDN, baseDN) { | 			return provider.Search(boundDN, searchReq, conn) | ||||||
| 		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}) |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	ls.log.WithField("filter", searchReq.Filter).Debug("Search OK") | 	return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("invalid DN") | ||||||
| 	return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil |  | ||||||
| } | } | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer