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:
parent
f89479caf3
commit
5b150657f5
|
@ -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)
|
ls.log.WithField("dn", bindDN).WithField("request", "bind").Warning("No provider found for request")
|
||||||
if err != nil {
|
|
||||||
ls.log.WithError(err).Warning("Failed to create cookiejar")
|
|
||||||
return ldap.LDAPResultOperationsError, nil
|
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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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,19 +10,24 @@ 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 {
|
||||||
|
@ -35,11 +37,8 @@ func NewServer(ac *ak.APIController) *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