outposts/ldap: improve logging, add request ID

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-07-19 13:41:29 +02:00
parent 148194e12b
commit d87871f806
5 changed files with 84 additions and 39 deletions

View File

@ -41,8 +41,8 @@ type FlowExecutor struct {
token string token string
} }
func NewFlowExecutor(flowSlug string, refConfig *api.Configuration) *FlowExecutor { func NewFlowExecutor(flowSlug string, refConfig *api.Configuration, logFields log.Fields) *FlowExecutor {
l := log.WithField("flow", flowSlug) l := log.WithField("flow", flowSlug).WithFields(logFields)
jar, err := cookiejar.New(nil) jar, err := cookiejar.New(nil)
if err != nil { if err != nil {
l.WithError(err).Warning("Failed to create cookiejar") l.WithError(err).Warning("Failed to create cookiejar")
@ -97,7 +97,7 @@ func (fe *FlowExecutor) CheckApplicationAccess(appSlug string) (bool, error) {
if err != nil { if err != nil {
return false, fmt.Errorf("failed to check access: %w", err) return false, fmt.Errorf("failed to check access: %w", err)
} }
fe.log.Info("User has access") fe.log.Debug("User has access")
return true, nil return true, nil
} }

View File

@ -4,20 +4,38 @@ import (
"net" "net"
"strings" "strings"
"github.com/google/uuid"
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
log "github.com/sirupsen/logrus"
) )
type BindRequest struct {
BindDN string
BindPW string
id string
conn net.Conn
log *log.Entry
}
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) {
bindDN = strings.ToLower(bindDN) bindDN = strings.ToLower(bindDN)
ls.log.WithField("bindDN", bindDN).Info("bind") rid := uuid.New().String()
req := BindRequest{
BindDN: bindDN,
BindPW: bindPW,
conn: conn,
log: ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", conn.RemoteAddr().String()),
id: rid,
}
req.log.Info("Bind request")
for _, instance := range ls.providers { for _, instance := range ls.providers {
username, err := instance.getUsername(bindDN) username, err := instance.getUsername(bindDN)
if err == nil { if err == nil {
return instance.Bind(username, bindDN, bindPW, conn) return instance.Bind(username, req)
} else { } else {
ls.log.WithError(err).Debug("Username not for instance") ls.log.WithError(err).Debug("Username not for instance")
} }
} }
ls.log.WithField("bindDN", bindDN).WithField("request", "bind").Warning("No provider found for request") req.log.WithField("request", "bind").Warning("No provider found for request")
return ldap.LDAPResultOperationsError, nil return ldap.LDAPResultOperationsError, nil
} }

View File

@ -3,12 +3,12 @@ package ldap
import ( import (
"context" "context"
"errors" "errors"
"net"
"strings" "strings"
"time" "time"
goldap "github.com/go-ldap/ldap/v3" goldap "github.com/go-ldap/ldap/v3"
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
log "github.com/sirupsen/logrus"
"goauthentik.io/api" "goauthentik.io/api"
"goauthentik.io/internal/outpost" "goauthentik.io/internal/outpost"
) )
@ -33,60 +33,68 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) {
return "", errors.New("failed to find cn") return "", errors.New("failed to find cn")
} }
func (pi *ProviderInstance) Bind(username string, bindDN, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) { func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPResultCode, error) {
fe := outpost.NewFlowExecutor(pi.flowSlug, pi.s.ac.Client.GetConfig()) fe := outpost.NewFlowExecutor(pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
fe.DelegateClientIP(conn.RemoteAddr()) "bindDN": req.BindDN,
"client": req.conn.RemoteAddr().String(),
"requestId": req.id,
})
fe.DelegateClientIP(req.conn.RemoteAddr())
fe.Params.Add("goauthentik.io/outpost/ldap", "true") fe.Params.Add("goauthentik.io/outpost/ldap", "true")
fe.Answers[outpost.StageIdentification] = username fe.Answers[outpost.StageIdentification] = username
fe.Answers[outpost.StagePassword] = bindPW fe.Answers[outpost.StagePassword] = req.BindPW
passed, err := fe.Execute() passed, err := fe.Execute()
if !passed { if !passed {
return ldap.LDAPResultInvalidCredentials, nil return ldap.LDAPResultInvalidCredentials, nil
} }
if err != nil { if err != nil {
pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to execute flow") req.log.WithError(err).Warning("failed to execute flow")
return ldap.LDAPResultOperationsError, nil return ldap.LDAPResultOperationsError, nil
} }
access, err := fe.CheckApplicationAccess(pi.appSlug) access, err := fe.CheckApplicationAccess(pi.appSlug)
if !access { if !access {
pi.log.WithField("bindDN", bindDN).Info("Access denied for user") req.log.Info("Access denied for user")
return ldap.LDAPResultInsufficientAccessRights, nil return ldap.LDAPResultInsufficientAccessRights, nil
} }
if err != nil { if err != nil {
pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to check access") req.log.WithError(err).Warning("failed to check access")
return ldap.LDAPResultOperationsError, nil return ldap.LDAPResultOperationsError, nil
} }
pi.log.WithField("bindDN", bindDN).Info("User has access") req.log.Info("User has access")
// Get user info to store in context // Get user info to store in context
userInfo, _, err := fe.ApiClient().CoreApi.CoreUsersMeRetrieve(context.Background()).Execute() userInfo, _, err := fe.ApiClient().CoreApi.CoreUsersMeRetrieve(context.Background()).Execute()
if err != nil { if err != nil {
pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to get user info") req.log.WithError(err).Warning("failed to get user info")
return ldap.LDAPResultOperationsError, nil return ldap.LDAPResultOperationsError, nil
} }
pi.boundUsersMutex.Lock() pi.boundUsersMutex.Lock()
pi.boundUsers[bindDN] = UserFlags{ cs := pi.SearchAccessCheck(userInfo.User)
pi.boundUsers[req.BindDN] = UserFlags{
UserInfo: userInfo.User, UserInfo: userInfo.User,
CanSearch: pi.SearchAccessCheck(userInfo.User), CanSearch: cs != nil,
} }
if pi.boundUsers[req.BindDN].CanSearch {
req.log.WithField("group", cs).Info("Allowed access to search")
}
defer pi.boundUsersMutex.Unlock() defer pi.boundUsersMutex.Unlock()
pi.delayDeleteUserInfo(username) pi.delayDeleteUserInfo(username)
return ldap.LDAPResultSuccess, nil return ldap.LDAPResultSuccess, nil
} }
// SearchAccessCheck Check if the current user is allowed to search // SearchAccessCheck Check if the current user is allowed to search
func (pi *ProviderInstance) SearchAccessCheck(user api.User) bool { func (pi *ProviderInstance) SearchAccessCheck(user api.User) *string {
for _, group := range user.Groups { for _, group := range user.Groups {
for _, allowedGroup := range pi.searchAllowedGroups { for _, allowedGroup := range pi.searchAllowedGroups {
pi.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access") pi.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access")
if group.Pk == allowedGroup.String() { if group.Pk == allowedGroup.String() {
pi.log.WithField("group", group.Name).Info("Allowed access to search") return &group.Name
return true
} }
} }
} }
return false return nil
} }
func (pi *ProviderInstance) delayDeleteUserInfo(dn string) { func (pi *ProviderInstance) delayDeleteUserInfo(dn string) {

View File

@ -4,50 +4,48 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net"
"strings" "strings"
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
"goauthentik.io/api" "goauthentik.io/api"
) )
func (pi *ProviderInstance) SearchMe(user api.User, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { func (pi *ProviderInstance) SearchMe(user api.User) (ldap.ServerSearchResult, error) {
entries := make([]*ldap.Entry, 1) entries := make([]*ldap.Entry, 1)
entries[0] = pi.UserEntry(user) entries[0] = pi.UserEntry(user)
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
} }
func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { func (pi *ProviderInstance) Search(req SearchRequest) (ldap.ServerSearchResult, error) {
bindDN = strings.ToLower(bindDN)
baseDN := strings.ToLower("," + pi.BaseDN) baseDN := strings.ToLower("," + pi.BaseDN)
entries := []*ldap.Entry{} entries := []*ldap.Entry{}
filterEntity, err := ldap.GetFilterObjectClass(searchReq.Filter) filterEntity, err := ldap.GetFilterObjectClass(req.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}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
} }
if len(bindDN) < 1 { if len(req.BindDN) < 1 {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", bindDN) return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
} }
if !strings.HasSuffix(bindDN, baseDN) { if !strings.HasSuffix(req.BindDN, baseDN) {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", bindDN, pi.BaseDN) return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", req.BindDN, pi.BaseDN)
} }
pi.boundUsersMutex.RLock() pi.boundUsersMutex.RLock()
defer pi.boundUsersMutex.RUnlock() defer pi.boundUsersMutex.RUnlock()
flags, ok := pi.boundUsers[bindDN] flags, ok := pi.boundUsers[req.BindDN]
if !ok { if !ok {
pi.log.Debug("User info not cached") pi.log.Debug("User info not cached")
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied") return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
} }
if !flags.CanSearch { if !flags.CanSearch {
pi.log.Debug("User can't search, showing info about user") pi.log.Debug("User can't search, showing info about user")
return pi.SearchMe(flags.UserInfo, searchReq, conn) return pi.SearchMe(flags.UserInfo)
} }
switch filterEntity { switch filterEntity {
default: default:
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, searchReq.Filter) return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, req.Filter)
case GroupObjectClass: case GroupObjectClass:
groups, _, err := pi.s.ac.Client.CoreApi.CoreGroupsList(context.Background()).Execute() groups, _, err := pi.s.ac.Client.CoreApi.CoreGroupsList(context.Background()).Execute()
if err != nil { if err != nil {
@ -76,7 +74,7 @@ func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest,
entries = append(entries, pi.UserEntry(u)) entries = append(entries, pi.UserEntry(u))
} }
} }
pi.log.WithField("filter", searchReq.Filter).Debug("Search OK") pi.log.WithField("filter", req.Filter).Debug("Search OK")
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
} }

View File

@ -3,25 +3,46 @@ package ldap
import ( import (
"errors" "errors"
"net" "net"
"strings"
goldap "github.com/go-ldap/ldap/v3" goldap "github.com/go-ldap/ldap/v3"
"github.com/google/uuid"
"github.com/nmcclain/ldap" "github.com/nmcclain/ldap"
log "github.com/sirupsen/logrus"
) )
type SearchRequest struct {
ldap.SearchRequest
BindDN string
id string
conn net.Conn
log *log.Entry
}
func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
ls.log.WithField("bindDN", bindDN).WithField("baseDN", searchReq.BaseDN).Info("search") bindDN = strings.ToLower(bindDN)
rid := uuid.New().String()
req := SearchRequest{
SearchRequest: searchReq,
BindDN: bindDN,
conn: conn,
log: ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", conn.RemoteAddr().String()).WithField("filter", searchReq.Filter).WithField("baseDN", searchReq.BaseDN),
id: rid,
}
req.log.Info("Search request")
if searchReq.BaseDN == "" { if searchReq.BaseDN == "" {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultSuccess}, nil return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultSuccess}, nil
} }
bd, err := goldap.ParseDN(searchReq.BaseDN) bd, err := goldap.ParseDN(searchReq.BaseDN)
if err != nil { if err != nil {
ls.log.WithField("baseDN", searchReq.BaseDN).WithError(err).Info("failed to parse basedn") req.log.WithError(err).Info("failed to parse basedn")
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("invalid DN") return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("invalid DN")
} }
for _, provider := range ls.providers { for _, provider := range ls.providers {
providerBase, _ := goldap.ParseDN(provider.BaseDN) providerBase, _ := goldap.ParseDN(provider.BaseDN)
if providerBase.AncestorOf(bd) { if providerBase.AncestorOf(bd) {
return provider.Search(bindDN, searchReq, conn) return provider.Search(req)
} }
} }
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("no provider could handle request") return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("no provider could handle request")