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:
Jens Langhammer 2021-04-26 11:53:06 +02:00
parent f89479caf3
commit 5b150657f5
8 changed files with 292 additions and 235 deletions

View File

@ -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

View File

@ -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=

View File

@ -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
} }

View File

@ -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)
}

View 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)
}

View 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
}

View File

@ -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

View File

@ -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
} }