providers/ldap: rework Schema and DSE (#5838)
* rework Root DSE Signed-off-by: Jens Langhammer <jens@goauthentik.io> * always parse filter objectClass Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start adding LDAP Schema Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add more schema Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update schema more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix cn for schema Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only include main DN in namingContexts Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use schema from gh Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add description Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add response filtering Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix response filtering Signed-off-by: Jens Langhammer <jens@goauthentik.io> * don't return rootDSE entry when searching for singleLevel Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove currentTime Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix attribute filtering Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * set SINGLE-VALUE Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix numbers Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
bf1a363124
commit
54ef88a6fa
|
@ -7,7 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Binder interface {
|
type Binder interface {
|
||||||
GetUsername(string) (string, error)
|
GetUsername(dn string) (string, error)
|
||||||
Bind(username string, req *Request) (ldap.LDAPResultCode, error)
|
Bind(username string, req *Request) (ldap.LDAPResultCode, error)
|
||||||
Unbind(username string, req *Request) (ldap.LDAPResultCode, error)
|
Unbind(username string, req *Request) (ldap.LDAPResultCode, error)
|
||||||
TimerFlowCacheExpiry(context.Context)
|
TimerFlowCacheExpiry(context.Context)
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
package constants
|
package constants
|
||||||
|
|
||||||
|
const OC = "objectClass"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
OCTop = "top"
|
OCTop = "top"
|
||||||
OCDomain = "domain"
|
OCDomain = "domain"
|
||||||
OCNSContainer = "nsContainer"
|
OCNSContainer = "nsContainer"
|
||||||
|
OCSubSchema = "subschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SearchAttributeNone = "1.1"
|
||||||
|
SearchAttributeAllUser = "*"
|
||||||
|
SearchAttributeAllOperational = "+"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -20,7 +29,7 @@ const (
|
||||||
OCOrgPerson = "organizationalPerson"
|
OCOrgPerson = "organizationalPerson"
|
||||||
OCInetOrgPerson = "inetOrgPerson"
|
OCInetOrgPerson = "inetOrgPerson"
|
||||||
OCAKUser = "goauthentik.io/ldap/user"
|
OCAKUser = "goauthentik.io/ldap/user"
|
||||||
OCPosixAccount = "posixAccount"
|
OCPosixAccount = "posixAccount"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -2,16 +2,13 @@ package ldap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"beryju.io/ldap"
|
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"goauthentik.io/api/v3"
|
"goauthentik.io/api/v3"
|
||||||
"goauthentik.io/internal/constants"
|
|
||||||
"goauthentik.io/internal/outpost/ldap/bind"
|
"goauthentik.io/internal/outpost/ldap/bind"
|
||||||
ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
|
ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
|
||||||
"goauthentik.io/internal/outpost/ldap/flags"
|
"goauthentik.io/internal/outpost/ldap/flags"
|
||||||
|
@ -107,43 +104,6 @@ func (pi *ProviderInstance) GetSearchAllowedGroups() []*strfmt.UUID {
|
||||||
return pi.searchAllowedGroups
|
return pi.searchAllowedGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pi *ProviderInstance) GetBaseEntry() *ldap.Entry {
|
|
||||||
return &ldap.Entry{
|
|
||||||
DN: pi.GetBaseDN(),
|
|
||||||
Attributes: []*ldap.EntryAttribute{
|
|
||||||
{
|
|
||||||
Name: "distinguishedName",
|
|
||||||
Values: []string{pi.GetBaseDN()},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "objectClass",
|
|
||||||
Values: []string{ldapConstants.OCTop, ldapConstants.OCDomain},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "supportedLDAPVersion",
|
|
||||||
Values: []string{"3"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "namingContexts",
|
|
||||||
Values: []string{
|
|
||||||
pi.GetBaseDN(),
|
|
||||||
pi.GetBaseUserDN(),
|
|
||||||
pi.GetBaseGroupDN(),
|
|
||||||
pi.GetBaseVirtualGroupDN(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "vendorName",
|
|
||||||
Values: []string{"goauthentik.io"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "vendorVersion",
|
|
||||||
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s", constants.FullVersion())},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pi *ProviderInstance) GetNeededObjects(scope int, baseDN string, filterOC string) (bool, bool) {
|
func (pi *ProviderInstance) GetNeededObjects(scope int, baseDN string, filterOC string) (bool, bool) {
|
||||||
needUsers := false
|
needUsers := false
|
||||||
needGroups := false
|
needGroups := false
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
package ldap
|
package ldap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"beryju.io/ldap"
|
"beryju.io/ldap"
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
goldap "github.com/go-ldap/ldap/v3"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/constants"
|
||||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||||
"goauthentik.io/internal/outpost/ldap/search"
|
"goauthentik.io/internal/outpost/ldap/search"
|
||||||
)
|
)
|
||||||
|
@ -36,38 +34,20 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
|
||||||
sentry.CaptureException(err.(error))
|
sentry.CaptureException(err.(error))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if searchReq.BaseDN == "" {
|
selectedProvider := ls.providerForRequest(req)
|
||||||
return ldap.ServerSearchResult{
|
if selectedProvider == nil {
|
||||||
Entries: []*ldap.Entry{
|
return ls.fallbackRootDSE(req)
|
||||||
{
|
|
||||||
DN: "",
|
|
||||||
Attributes: []*ldap.EntryAttribute{
|
|
||||||
{
|
|
||||||
Name: "objectClass",
|
|
||||||
Values: []string{"top", "OpenLDAProotDSE"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "subschemaSubentry",
|
|
||||||
Values: []string{"cn=subschema"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
bd, err := goldap.ParseDN(strings.ToLower(searchReq.BaseDN))
|
selectedApp = selectedProvider.GetAppSlug()
|
||||||
|
result, err := ls.searchRoute(req, selectedProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.Log().WithError(err).Info("failed to parse basedn")
|
return result, nil
|
||||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("invalid DN")
|
|
||||||
}
|
|
||||||
for _, provider := range ls.providers {
|
|
||||||
providerBase, _ := goldap.ParseDN(strings.ToLower(provider.BaseDN))
|
|
||||||
if providerBase.AncestorOf(bd) || providerBase.Equal(bd) {
|
|
||||||
selectedApp = provider.GetAppSlug()
|
|
||||||
return provider.searcher.Search(req)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return ls.filterResultAttributes(req, result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LDAPServer) fallbackRootDSE(req *search.Request) (ldap.ServerSearchResult, error) {
|
||||||
|
req.Log().Trace("returning fallback Root DSE")
|
||||||
return ldap.ServerSearchResult{
|
return ldap.ServerSearchResult{
|
||||||
Entries: []*ldap.Entry{
|
Entries: []*ldap.Entry{
|
||||||
{
|
{
|
||||||
|
@ -75,15 +55,30 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
|
||||||
Attributes: []*ldap.EntryAttribute{
|
Attributes: []*ldap.EntryAttribute{
|
||||||
{
|
{
|
||||||
Name: "objectClass",
|
Name: "objectClass",
|
||||||
Values: []string{"top", "OpenLDAProotDSE"},
|
Values: []string{constants.OCTop},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "entryDN",
|
||||||
|
Values: []string{""},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "subschemaSubentry",
|
Name: "subschemaSubentry",
|
||||||
Values: []string{"cn=subschema"},
|
Values: []string{"cn=subschema"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "namingContexts",
|
||||||
|
Values: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "description",
|
||||||
|
Values: []string{
|
||||||
|
"This LDAP server requires an authenticated session.",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess,
|
Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,40 +2,51 @@ package direct
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"beryju.io/ldap"
|
"beryju.io/ldap"
|
||||||
"goauthentik.io/internal/constants"
|
"goauthentik.io/internal/constants"
|
||||||
|
ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
|
||||||
"goauthentik.io/internal/outpost/ldap/search"
|
"goauthentik.io/internal/outpost/ldap/search"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ds *DirectSearcher) SearchBase(req *search.Request, authz bool) (ldap.ServerSearchResult, error) {
|
func (ds *DirectSearcher) SearchBase(req *search.Request) (ldap.ServerSearchResult, error) {
|
||||||
dn := ""
|
if req.Scope == ldap.ScopeSingleLevel {
|
||||||
if authz {
|
return ldap.ServerSearchResult{
|
||||||
dn = req.SearchRequest.BaseDN
|
ResultCode: ldap.LDAPResultNoSuchObject,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
return ldap.ServerSearchResult{
|
return ldap.ServerSearchResult{
|
||||||
Entries: []*ldap.Entry{
|
Entries: []*ldap.Entry{
|
||||||
{
|
{
|
||||||
DN: dn,
|
DN: "",
|
||||||
Attributes: []*ldap.EntryAttribute{
|
Attributes: []*ldap.EntryAttribute{
|
||||||
{
|
{
|
||||||
Name: "distinguishedName",
|
Name: "objectClass",
|
||||||
Values: []string{ds.si.GetBaseDN()},
|
Values: []string{ldapConstants.OCTop},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "objectClass",
|
Name: "entryDN",
|
||||||
Values: []string{"top", "domain"},
|
Values: []string{""},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "supportedLDAPVersion",
|
Name: "supportedLDAPVersion",
|
||||||
Values: []string{"3"},
|
Values: []string{"3"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "subschemaSubentry",
|
||||||
|
Values: []string{"cn=subschema"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "namingContexts",
|
Name: "namingContexts",
|
||||||
Values: []string{
|
Values: []string{
|
||||||
ds.si.GetBaseDN(),
|
strings.ToLower(ds.si.GetBaseDN()),
|
||||||
ds.si.GetBaseUserDN(),
|
},
|
||||||
ds.si.GetBaseGroupDN(),
|
},
|
||||||
|
{
|
||||||
|
Name: "rootDomainNamingContext",
|
||||||
|
Values: []string{
|
||||||
|
strings.ToLower(ds.si.GetBaseDN()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -31,7 +31,6 @@ func NewDirectSearcher(si server.LDAPServerInstance) *DirectSearcher {
|
||||||
si: si,
|
si: si,
|
||||||
log: log.WithField("logger", "authentik.outpost.ldap.searcher.direct"),
|
log: log.WithField("logger", "authentik.outpost.ldap.searcher.direct"),
|
||||||
}
|
}
|
||||||
ds.log.Info("initialised direct searcher")
|
|
||||||
return ds
|
return ds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,16 +38,6 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||||
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
|
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
|
||||||
baseDN := ds.si.GetBaseDN()
|
baseDN := ds.si.GetBaseDN()
|
||||||
|
|
||||||
filterOC, err := ldap.GetFilterObjectClass(req.Filter)
|
|
||||||
if err != nil {
|
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
|
||||||
"outpost_name": ds.si.GetOutpostName(),
|
|
||||||
"type": "search",
|
|
||||||
"reason": "filter_parse_fail",
|
|
||||||
"app": ds.si.GetAppSlug(),
|
|
||||||
}).Inc()
|
|
||||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
|
|
||||||
}
|
|
||||||
if len(req.BindDN) < 1 {
|
if len(req.BindDN) < 1 {
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
metrics.RequestsRejected.With(prometheus.Labels{
|
||||||
"outpost_name": ds.si.GetOutpostName(),
|
"outpost_name": ds.si.GetOutpostName(),
|
||||||
|
@ -99,11 +88,17 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||||
c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter)
|
c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter)
|
||||||
|
|
||||||
scope := req.SearchRequest.Scope
|
scope := req.SearchRequest.Scope
|
||||||
needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, filterOC)
|
needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass)
|
||||||
|
|
||||||
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
|
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
|
||||||
if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) {
|
if utils.IncludeObjectClass(req.FilterObjectClass, constants.GetDomainOCs()) {
|
||||||
entries = append(entries, ds.si.GetBaseEntry())
|
rootEntries, _ := ds.SearchBase(req)
|
||||||
|
// Since `SearchBase` returns entries for the root DN, we need to go through the
|
||||||
|
// entries and update the base DN
|
||||||
|
for _, e := range rootEntries.Entries {
|
||||||
|
e.DN = ds.si.GetBaseDN()
|
||||||
|
entries = append(entries, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scope -= 1 // Bring it from WholeSubtree to SingleLevel and so on
|
scope -= 1 // Bring it from WholeSubtree to SingleLevel and so on
|
||||||
|
@ -197,12 +192,12 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||||
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseUserDN())) {
|
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseUserDN())) {
|
||||||
singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseUserDN())
|
singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseUserDN())
|
||||||
|
|
||||||
if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
|
if !singleu && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
|
||||||
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseUserDN(), constants.OUUsers))
|
entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ds.si.GetBaseUserDN(), constants.OUUsers))
|
||||||
scope -= 1
|
scope -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) {
|
if scope >= 0 && users != nil && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetUserOCs()) {
|
||||||
for _, u := range *users {
|
for _, u := range *users {
|
||||||
entry := ds.si.UserEntry(u)
|
entry := ds.si.UserEntry(u)
|
||||||
if strings.EqualFold(req.BaseDN, entry.DN) || !singleu {
|
if strings.EqualFold(req.BaseDN, entry.DN) || !singleu {
|
||||||
|
@ -217,12 +212,12 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||||
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseGroupDN())) {
|
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseGroupDN())) {
|
||||||
singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseGroupDN())
|
singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseGroupDN())
|
||||||
|
|
||||||
if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
|
if !singleg && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
|
||||||
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseGroupDN(), constants.OUGroups))
|
entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ds.si.GetBaseGroupDN(), constants.OUGroups))
|
||||||
scope -= 1
|
scope -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) {
|
if scope >= 0 && groups != nil && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetGroupOCs()) {
|
||||||
for _, g := range *groups {
|
for _, g := range *groups {
|
||||||
entry := group.FromAPIGroup(g, ds.si).Entry()
|
entry := group.FromAPIGroup(g, ds.si).Entry()
|
||||||
if strings.EqualFold(req.BaseDN, entry.DN) || !singleg {
|
if strings.EqualFold(req.BaseDN, entry.DN) || !singleg {
|
||||||
|
@ -237,12 +232,12 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||||
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseVirtualGroupDN())) {
|
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseVirtualGroupDN())) {
|
||||||
singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseVirtualGroupDN())
|
singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseVirtualGroupDN())
|
||||||
|
|
||||||
if !singlevg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
|
if !singlevg && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
|
||||||
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
|
entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ds.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
|
||||||
scope -= 1
|
scope -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) {
|
if scope >= 0 && users != nil && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetVirtualGroupOCs()) {
|
||||||
for _, u := range *users {
|
for _, u := range *users {
|
||||||
entry := group.FromAPIUser(u, ds.si).Entry()
|
entry := group.FromAPIUser(u, ds.si).Entry()
|
||||||
if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg {
|
if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg {
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
package direct
|
||||||
|
|
||||||
|
import (
|
||||||
|
"beryju.io/ldap"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/constants"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/search"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ds *DirectSearcher) SearchSubschema(req *search.Request) (ldap.ServerSearchResult, error) {
|
||||||
|
return ldap.ServerSearchResult{
|
||||||
|
Entries: []*ldap.Entry{
|
||||||
|
{
|
||||||
|
DN: "cn=subschema",
|
||||||
|
Attributes: []*ldap.EntryAttribute{
|
||||||
|
{
|
||||||
|
Name: "cn",
|
||||||
|
Values: []string{"subschema"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: constants.OC,
|
||||||
|
Values: []string{constants.OCTop, "subSchema"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ldapSyntaxes",
|
||||||
|
Values: []string{
|
||||||
|
"( 1.3.6.1.4.1.1466.115.121.1.40 DESC 'Octet String' )",
|
||||||
|
"( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )",
|
||||||
|
"( 1.3.6.1.4.1.1466.115.121.1.7 DESC 'Boolean' )",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "objectClasses",
|
||||||
|
Values: []string{
|
||||||
|
"( 2.5.6.0 NAME 'top' ABSTRACT MUST ( objectClass ) MAY (cn $ description $ displayName $ memberOf $ name ) )",
|
||||||
|
"( 2.5.6.6 NAME 'person' SUP top STRUCTURAL MUST ( cn ) MAY (sn $ telephoneNumber ) )",
|
||||||
|
"( 2.5.6.7 NAME 'organizationalPerson' SUP person STRUCTURAL MAY (c $ l $ o $ ou $ title $ givenName $ co $ department $ company $ division $ mail $ mobile $ telephoneNumber ) )",
|
||||||
|
"( 2.5.6.9 NAME 'groupOfNames' SUP top STRUCTURAL MUST (cn $ member ) MAY (o $ ou ) )",
|
||||||
|
"( 1.2.840.113556.1.5.9 NAME 'user' SUP organizationalPerson STRUCTURAL MAY ( name $ displayName $ uid $ mail ) )",
|
||||||
|
"( 1.3.6.1.1.1.2.0 NAME 'posixAccount' SUP top AUXILIARY MAY (cn $ description $ homeDirectory $ uid $ uidNumber $ gidNumber ) )",
|
||||||
|
"( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' AUX ( posixAccount ) MUST ( sAMAccountName ) MAY ( uidNumber $ gidNumber ))",
|
||||||
|
// Custom attributes
|
||||||
|
// Temporarily use 1.3.6.1.4.1.26027.1.1 as a base
|
||||||
|
// https://docs.oracle.com/cd/E19450-01/820-6169/working-with-object-identifiers.html#obtaining-a-base-oid
|
||||||
|
"( 1.3.6.1.4.1.26027.1.1.1 NAME 'goauthentik.io/ldap/user' SUP organizationalPerson STRUCTURAL MAY ( ak-active $ sAMAccountName $ goauthentikio-user-sources $ goauthentik.io/user/sources $ goauthentik.io/ldap/active $ goauthentik.io/ldap/superuser $ goauthentikio-user-override-ips $ goauthentikio-user-service-account ) )",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "attributeTypes",
|
||||||
|
Values: []string{
|
||||||
|
"( 2.5.4.0 NAME 'objectClass' DESC 'RFC4512: object classes of the entity' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )",
|
||||||
|
"( 1.3.6.1.4.1.1466.101.120.5 NAME 'namingContexts' DESC 'RFC4512: naming contexts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation )",
|
||||||
|
"( 2.5.18.10 NAME 'subschemaSubentry' DESC 'RFC4512: name of controlling subschema entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )",
|
||||||
|
"( 1.3.6.1.4.1.1466.101.120.15 NAME 'supportedLDAPVersion' DESC 'RFC4512: supported LDAP versions' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 USAGE dSAOperation )",
|
||||||
|
"( 1.3.6.1.1.20 NAME 'entryDN' DESC 'DN of the entry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )",
|
||||||
|
"( 1.3.6.1.1.4 NAME 'vendorName' DESC 'RFC3045: name of implementation vendor' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )",
|
||||||
|
"( 1.3.6.1.1.5 NAME 'vendorVersion' DESC 'RFC3045: version of implementation' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE NO-USER-MODIFICATION USAGE dSAOperation )",
|
||||||
|
"( 0.9.2342.19200300.100.1.1 NAME 'uid' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 0.9.2342.19200300.100.1.3 NAME 'mail' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 0.9.2342.19200300.100.1.41 NAME 'mobile' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 1.2.840.113556.1.2.102 NAME 'memberOf' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' NO-USER-MODIFICATION )",
|
||||||
|
"( 1.2.840.113556.1.2.13 NAME 'displayName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 1.2.840.113556.1.4.1 NAME 'name' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE NO-USER-MODIFICATION )",
|
||||||
|
"( 1.2.840.113556.1.2.131 NAME 'co' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 1.2.840.113556.1.2.141 NAME 'department' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 1.2.840.113556.1.2.146 NAME 'company' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 1.2.840.113556.1.4.44 NAME 'homeDirectory' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 1.2.840.113556.1.4.221 NAME 'sAMAccountName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 1.2.840.113556.1.4.261 NAME 'division' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 1.3.6.1.1.1.1.0 NAME 'uidNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
|
||||||
|
"( 1.3.6.1.1.1.1.1 NAME 'gidNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' SINGLE-VALUE )",
|
||||||
|
"( 2.5.4.6 NAME 'c' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 2.5.4.7 NAME 'l' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 2.5.4.10 NAME 'o' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
|
||||||
|
"( 2.5.4.11 NAME 'ou' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
|
||||||
|
"( 2.5.4.20 NAME 'telephoneNumber' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 2.5.4.42 NAME 'givenName' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 2.5.4.0 NAME 'objectClass' SYNTAX '1.3.6.1.4.1.1466.115.121.1.38' NO-USER-MODIFICATION )",
|
||||||
|
"( 2.5.4.3 NAME 'cn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 2.5.4.4 NAME 'sn' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 2.5.4.12 NAME 'title' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' SINGLE-VALUE )",
|
||||||
|
"( 2.5.4.13 NAME 'description' SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )",
|
||||||
|
"( 2.5.4.31 NAME 'member' SYNTAX '1.3.6.1.4.1.1466.115.121.1.12' )",
|
||||||
|
// Custom attributes
|
||||||
|
// Temporarily use 1.3.6.1.4.1.26027.1.1 as a base
|
||||||
|
// https://docs.oracle.com/cd/E19450-01/820-6169/working-with-object-identifiers.html#obtaining-a-base-oid
|
||||||
|
"( 1.3.6.1.4.1.26027.1.1.2 NAME ( 'goauthentik.io/ldap/superuser' 'ak-superuser' ) SYNTAX '1.3.6.1.4.1.1466.115.121.1.7' SINGLE-VALUE )",
|
||||||
|
"( 1.3.6.1.4.1.26027.1.1.3 NAME ( 'goauthentik.io/ldap/active' 'ak-active' ) SYNTAX '1.3.6.1.4.1.1466.115.121.1.7' SINGLE-VALUE )",
|
||||||
|
"( 1.3.6.1.4.1.26027.1.1.4 NAME 'goauthentikio-user-override-ips' SYNTAX '1.3.6.1.4.1.1466.115.121.1.7' SINGLE-VALUE )",
|
||||||
|
"( 1.3.6.1.4.1.26027.1.1.5 NAME 'goauthentikio-user-service-account' SYNTAX '1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE' )",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"goauthentik.io/internal/outpost/ldap/group"
|
"goauthentik.io/internal/outpost/ldap/group"
|
||||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||||
"goauthentik.io/internal/outpost/ldap/search"
|
"goauthentik.io/internal/outpost/ldap/search"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/search/direct"
|
||||||
"goauthentik.io/internal/outpost/ldap/server"
|
"goauthentik.io/internal/outpost/ldap/server"
|
||||||
"goauthentik.io/internal/outpost/ldap/utils"
|
"goauthentik.io/internal/outpost/ldap/utils"
|
||||||
"goauthentik.io/internal/outpost/ldap/utils/paginator"
|
"goauthentik.io/internal/outpost/ldap/utils/paginator"
|
||||||
|
@ -23,6 +24,7 @@ import (
|
||||||
type MemorySearcher struct {
|
type MemorySearcher struct {
|
||||||
si server.LDAPServerInstance
|
si server.LDAPServerInstance
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
|
ds *direct.DirectSearcher
|
||||||
|
|
||||||
users []api.User
|
users []api.User
|
||||||
groups []api.Group
|
groups []api.Group
|
||||||
|
@ -32,6 +34,7 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
|
||||||
ms := &MemorySearcher{
|
ms := &MemorySearcher{
|
||||||
si: si,
|
si: si,
|
||||||
log: log.WithField("logger", "authentik.outpost.ldap.searcher.memory"),
|
log: log.WithField("logger", "authentik.outpost.ldap.searcher.memory"),
|
||||||
|
ds: direct.NewDirectSearcher(si),
|
||||||
}
|
}
|
||||||
ms.log.Debug("initialised memory searcher")
|
ms.log.Debug("initialised memory searcher")
|
||||||
ms.users = paginator.FetchUsers(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO()))
|
ms.users = paginator.FetchUsers(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO()))
|
||||||
|
@ -39,20 +42,18 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
|
||||||
return ms
|
return ms
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ms *MemorySearcher) SearchBase(req *search.Request) (ldap.ServerSearchResult, error) {
|
||||||
|
return ms.ds.SearchBase(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms *MemorySearcher) SearchSubschema(req *search.Request) (ldap.ServerSearchResult, error) {
|
||||||
|
return ms.ds.SearchSubschema(req)
|
||||||
|
}
|
||||||
|
|
||||||
func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) {
|
func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) {
|
||||||
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
|
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
|
||||||
baseDN := ms.si.GetBaseDN()
|
baseDN := ms.si.GetBaseDN()
|
||||||
|
|
||||||
filterOC, err := ldap.GetFilterObjectClass(req.Filter)
|
|
||||||
if err != nil {
|
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
|
||||||
"outpost_name": ms.si.GetOutpostName(),
|
|
||||||
"type": "search",
|
|
||||||
"reason": "filter_parse_fail",
|
|
||||||
"app": ms.si.GetAppSlug(),
|
|
||||||
}).Inc()
|
|
||||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
|
|
||||||
}
|
|
||||||
if len(req.BindDN) < 1 {
|
if len(req.BindDN) < 1 {
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
metrics.RequestsRejected.With(prometheus.Labels{
|
||||||
"outpost_name": ms.si.GetOutpostName(),
|
"outpost_name": ms.si.GetOutpostName(),
|
||||||
|
@ -88,11 +89,15 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||||
entries := make([]*ldap.Entry, 0)
|
entries := make([]*ldap.Entry, 0)
|
||||||
|
|
||||||
scope := req.SearchRequest.Scope
|
scope := req.SearchRequest.Scope
|
||||||
needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, filterOC)
|
needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass)
|
||||||
|
|
||||||
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
|
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
|
||||||
if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) {
|
if utils.IncludeObjectClass(req.FilterObjectClass, constants.GetDomainOCs()) {
|
||||||
entries = append(entries, ms.si.GetBaseEntry())
|
rootEntries, _ := ms.SearchBase(req)
|
||||||
|
for _, e := range rootEntries.Entries {
|
||||||
|
e.DN = ms.si.GetBaseDN()
|
||||||
|
entries = append(entries, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scope -= 1 // Bring it from WholeSubtree to SingleLevel and so on
|
scope -= 1 // Bring it from WholeSubtree to SingleLevel and so on
|
||||||
|
@ -100,6 +105,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||||
|
|
||||||
var users *[]api.User
|
var users *[]api.User
|
||||||
var groups []*group.LDAPGroup
|
var groups []*group.LDAPGroup
|
||||||
|
var err error
|
||||||
|
|
||||||
if needUsers {
|
if needUsers {
|
||||||
if flags.CanSearch {
|
if flags.CanSearch {
|
||||||
|
@ -159,12 +165,12 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||||
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseUserDN())) {
|
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseUserDN())) {
|
||||||
singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseUserDN())
|
singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseUserDN())
|
||||||
|
|
||||||
if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
|
if !singleu && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
|
||||||
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseUserDN(), constants.OUUsers))
|
entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ms.si.GetBaseUserDN(), constants.OUUsers))
|
||||||
scope -= 1
|
scope -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) {
|
if scope >= 0 && users != nil && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetUserOCs()) {
|
||||||
for _, u := range *users {
|
for _, u := range *users {
|
||||||
entry := ms.si.UserEntry(u)
|
entry := ms.si.UserEntry(u)
|
||||||
if strings.EqualFold(req.BaseDN, entry.DN) || !singleu {
|
if strings.EqualFold(req.BaseDN, entry.DN) || !singleu {
|
||||||
|
@ -179,12 +185,12 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||||
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseGroupDN())) {
|
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseGroupDN())) {
|
||||||
singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseGroupDN())
|
singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseGroupDN())
|
||||||
|
|
||||||
if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
|
if !singleg && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
|
||||||
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseGroupDN(), constants.OUGroups))
|
entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ms.si.GetBaseGroupDN(), constants.OUGroups))
|
||||||
scope -= 1
|
scope -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) {
|
if scope >= 0 && groups != nil && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetGroupOCs()) {
|
||||||
for _, g := range groups {
|
for _, g := range groups {
|
||||||
if strings.EqualFold(req.BaseDN, g.DN) || !singleg {
|
if strings.EqualFold(req.BaseDN, g.DN) || !singleg {
|
||||||
entries = append(entries, g.Entry())
|
entries = append(entries, g.Entry())
|
||||||
|
@ -198,12 +204,12 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||||
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseVirtualGroupDN())) {
|
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseVirtualGroupDN())) {
|
||||||
singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseVirtualGroupDN())
|
singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseVirtualGroupDN())
|
||||||
|
|
||||||
if !singlevg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
|
if !singlevg && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
|
||||||
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
|
entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ms.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
|
||||||
scope -= 1
|
scope -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) {
|
if scope >= 0 && users != nil && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetVirtualGroupOCs()) {
|
||||||
for _, u := range *users {
|
for _, u := range *users {
|
||||||
entry := group.FromAPIUser(u, ms.si).Entry()
|
entry := group.FromAPIUser(u, ms.si).Entry()
|
||||||
if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg {
|
if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg {
|
||||||
|
|
|
@ -15,8 +15,9 @@ import (
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
ldap.SearchRequest
|
ldap.SearchRequest
|
||||||
BindDN string
|
BindDN string
|
||||||
log *log.Entry
|
FilterObjectClass string
|
||||||
|
log *log.Entry
|
||||||
|
|
||||||
id string
|
id string
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
|
@ -40,13 +41,26 @@ func NewRequest(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (*Re
|
||||||
})
|
})
|
||||||
span.SetTag("ldap_filter", searchReq.Filter)
|
span.SetTag("ldap_filter", searchReq.Filter)
|
||||||
span.SetTag("ldap_base_dn", searchReq.BaseDN)
|
span.SetTag("ldap_base_dn", searchReq.BaseDN)
|
||||||
|
l := log.WithFields(log.Fields{
|
||||||
|
"bindDN": bindDN,
|
||||||
|
"baseDN": searchReq.BaseDN,
|
||||||
|
"requestId": rid,
|
||||||
|
"scope": ldap.ScopeMap[searchReq.Scope],
|
||||||
|
"client": utils.GetIP(conn.RemoteAddr()),
|
||||||
|
"filter": searchReq.Filter,
|
||||||
|
})
|
||||||
|
filterOC, err := ldap.GetFilterObjectClass(searchReq.Filter)
|
||||||
|
if err != nil && len(searchReq.Filter) > 0 {
|
||||||
|
l.WithError(err).WithField("objectClass", filterOC).Warning("invalid filter object class")
|
||||||
|
}
|
||||||
return &Request{
|
return &Request{
|
||||||
SearchRequest: searchReq,
|
SearchRequest: searchReq,
|
||||||
BindDN: bindDN,
|
BindDN: bindDN,
|
||||||
conn: conn,
|
FilterObjectClass: filterOC,
|
||||||
log: log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("scope", ldap.ScopeMap[searchReq.Scope]).WithField("client", utils.GetIP(conn.RemoteAddr())).WithField("filter", searchReq.Filter).WithField("baseDN", searchReq.BaseDN),
|
conn: conn,
|
||||||
id: rid,
|
log: l,
|
||||||
ctx: span.Context(),
|
id: rid,
|
||||||
|
ctx: span.Context(),
|
||||||
}, span
|
}, span
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,3 +75,19 @@ func (r *Request) Log() *log.Entry {
|
||||||
func (r *Request) RemoteAddr() string {
|
func (r *Request) RemoteAddr() string {
|
||||||
return utils.GetIP(r.conn.RemoteAddr())
|
return utils.GetIP(r.conn.RemoteAddr())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Request) FilterLDAPAttributes(res ldap.ServerSearchResult, cb func(attr *ldap.EntryAttribute) bool) ldap.ServerSearchResult {
|
||||||
|
for _, e := range res.Entries {
|
||||||
|
newAttrs := []*ldap.EntryAttribute{}
|
||||||
|
for _, attr := range e.Attributes {
|
||||||
|
include := cb(attr)
|
||||||
|
if include {
|
||||||
|
newAttrs = append(newAttrs, attr)
|
||||||
|
} else {
|
||||||
|
r.Log().WithField("key", attr.Name).Trace("filtering out field based on LDAP request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.Attributes = newAttrs
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
|
@ -6,4 +6,6 @@ import (
|
||||||
|
|
||||||
type Searcher interface {
|
type Searcher interface {
|
||||||
Search(req *Request) (ldap.ServerSearchResult, error)
|
Search(req *Request) (ldap.ServerSearchResult, error)
|
||||||
|
SearchBase(req *Request) (ldap.ServerSearchResult, error)
|
||||||
|
SearchSubschema(req *Request) (ldap.ServerSearchResult, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
package ldap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"beryju.io/ldap"
|
||||||
|
goldap "github.com/go-ldap/ldap/v3"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/constants"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/search"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ls *LDAPServer) providerForRequest(req *search.Request) *ProviderInstance {
|
||||||
|
parsedBaseDN, err := goldap.ParseDN(strings.ToLower(req.BaseDN))
|
||||||
|
if err != nil {
|
||||||
|
req.Log().WithError(err).Info("failed to parse base DN")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsedBindDN, err := goldap.ParseDN(strings.ToLower(req.BindDN))
|
||||||
|
if err != nil {
|
||||||
|
req.Log().WithError(err).Info("failed to parse bind DN")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var selectedProvider *ProviderInstance
|
||||||
|
longestMatch := 0
|
||||||
|
for _, provider := range ls.providers {
|
||||||
|
providerBase, _ := goldap.ParseDN(strings.ToLower(provider.BaseDN))
|
||||||
|
// Try to match the provider primarily based on the search request's base DN
|
||||||
|
baseDNMatches := providerBase.AncestorOf(parsedBaseDN) || providerBase.Equal(parsedBaseDN)
|
||||||
|
// But also try to match the provider based on the bind DN
|
||||||
|
bindDNMatches := providerBase.AncestorOf(parsedBindDN) || providerBase.Equal(parsedBindDN)
|
||||||
|
if baseDNMatches || bindDNMatches {
|
||||||
|
// Only select the provider if it's a more precise match than previously
|
||||||
|
if len(provider.BaseDN) > longestMatch {
|
||||||
|
req.Log().WithField("provider", provider.BaseDN).Trace("selecting provider for search request")
|
||||||
|
selectedProvider = provider
|
||||||
|
longestMatch = len(provider.BaseDN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LDAPServer) searchRoute(req *search.Request, pi *ProviderInstance) (ldap.ServerSearchResult, error) {
|
||||||
|
// Route based on the base DN
|
||||||
|
if len(req.BaseDN) == 0 {
|
||||||
|
req.Log().Trace("routing to base")
|
||||||
|
return pi.searcher.SearchBase(req)
|
||||||
|
}
|
||||||
|
if strings.EqualFold(req.BaseDN, "cn=subschema") || req.FilterObjectClass == constants.OCSubSchema {
|
||||||
|
req.Log().Trace("routing to subschema")
|
||||||
|
return pi.searcher.SearchSubschema(req)
|
||||||
|
}
|
||||||
|
req.Log().Trace("routing to default")
|
||||||
|
return pi.searcher.Search(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LDAPServer) filterResultAttributes(req *search.Request, result ldap.ServerSearchResult) ldap.ServerSearchResult {
|
||||||
|
allowedAttributes := []string{}
|
||||||
|
if len(req.Attributes) == 1 && req.Attributes[0] == constants.SearchAttributeNone {
|
||||||
|
allowedAttributes = []string{"objectClass"}
|
||||||
|
}
|
||||||
|
if len(req.Attributes) > 0 {
|
||||||
|
// Only strictly filter allowed attributes if we haven't already narrowed the attributes
|
||||||
|
// down
|
||||||
|
if len(allowedAttributes) < 1 {
|
||||||
|
allowedAttributes = req.Attributes
|
||||||
|
}
|
||||||
|
// Filter LDAP returned attributes by search requested attributes, taking "1.1"
|
||||||
|
// into consideration
|
||||||
|
return req.FilterLDAPAttributes(result, func(attr *ldap.EntryAttribute) bool {
|
||||||
|
for _, allowed := range allowedAttributes {
|
||||||
|
if allowed == constants.SearchAttributeAllUser ||
|
||||||
|
allowed == constants.SearchAttributeAllOperational {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.EqualFold(allowed, attr.Name) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
|
@ -35,6 +35,5 @@ type LDAPServerInstance interface {
|
||||||
GetFlags(dn string) *flags.UserFlags
|
GetFlags(dn string) *flags.UserFlags
|
||||||
SetFlags(dn string, flags *flags.UserFlags)
|
SetFlags(dn string, flags *flags.UserFlags)
|
||||||
|
|
||||||
GetBaseEntry() *ldap.Entry
|
GetNeededObjects(scope int, baseDN string, filterOC string) (bool, bool)
|
||||||
GetNeededObjects(int, string, string) (bool, bool)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,88 +238,82 @@ class TestProviderLDAP(SeleniumTestCase):
|
||||||
{
|
{
|
||||||
"dn": f"cn={o_user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
"dn": f"cn={o_user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"cn": [o_user.username],
|
"cn": o_user.username,
|
||||||
"sAMAccountName": [o_user.username],
|
"sAMAccountName": o_user.username,
|
||||||
"uid": [o_user.uid],
|
"uid": o_user.uid,
|
||||||
"name": [o_user.name],
|
"name": o_user.name,
|
||||||
"displayName": [o_user.name],
|
"displayName": o_user.name,
|
||||||
"sn": [o_user.name],
|
"sn": o_user.name,
|
||||||
"mail": [""],
|
"mail": "",
|
||||||
"objectClass": [
|
"objectClass": [
|
||||||
"user",
|
"user",
|
||||||
"organizationalPerson",
|
"organizationalPerson",
|
||||||
"inetOrgPerson",
|
"inetOrgPerson",
|
||||||
"goauthentik.io/ldap/user",
|
"goauthentik.io/ldap/user",
|
||||||
],
|
],
|
||||||
"uidNumber": [str(2000 + o_user.pk)],
|
"uidNumber": 2000 + o_user.pk,
|
||||||
"gidNumber": [str(2000 + o_user.pk)],
|
"gidNumber": 2000 + o_user.pk,
|
||||||
"memberOf": [],
|
"memberOf": [],
|
||||||
"homeDirectory": [
|
"homeDirectory": f"/home/{o_user.username}",
|
||||||
f"/home/{o_user.username}",
|
"ak-active": True,
|
||||||
],
|
"ak-superuser": False,
|
||||||
"ak-active": ["true"],
|
"goauthentikio-user-override-ips": True,
|
||||||
"ak-superuser": ["false"],
|
"goauthentikio-user-service-account": True,
|
||||||
"goauthentikio-user-override-ips": ["true"],
|
|
||||||
"goauthentikio-user-service-account": ["true"],
|
|
||||||
},
|
},
|
||||||
"type": "searchResEntry",
|
"type": "searchResEntry",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dn": f"cn={embedded_account.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
"dn": f"cn={embedded_account.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"cn": [embedded_account.username],
|
"cn": embedded_account.username,
|
||||||
"sAMAccountName": [embedded_account.username],
|
"sAMAccountName": embedded_account.username,
|
||||||
"uid": [embedded_account.uid],
|
"uid": embedded_account.uid,
|
||||||
"name": [embedded_account.name],
|
"name": embedded_account.name,
|
||||||
"displayName": [embedded_account.name],
|
"displayName": embedded_account.name,
|
||||||
"sn": [embedded_account.name],
|
"sn": embedded_account.name,
|
||||||
"mail": [""],
|
"mail": "",
|
||||||
"objectClass": [
|
"objectClass": [
|
||||||
"user",
|
"user",
|
||||||
"organizationalPerson",
|
"organizationalPerson",
|
||||||
"inetOrgPerson",
|
"inetOrgPerson",
|
||||||
"goauthentik.io/ldap/user",
|
"goauthentik.io/ldap/user",
|
||||||
],
|
],
|
||||||
"uidNumber": [str(2000 + embedded_account.pk)],
|
"uidNumber": 2000 + embedded_account.pk,
|
||||||
"gidNumber": [str(2000 + embedded_account.pk)],
|
"gidNumber": 2000 + embedded_account.pk,
|
||||||
"memberOf": [],
|
"memberOf": [],
|
||||||
"homeDirectory": [
|
"homeDirectory": f"/home/{embedded_account.username}",
|
||||||
f"/home/{embedded_account.username}",
|
"ak-active": True,
|
||||||
],
|
"ak-superuser": False,
|
||||||
"ak-active": ["true"],
|
"goauthentikio-user-override-ips": True,
|
||||||
"ak-superuser": ["false"],
|
"goauthentikio-user-service-account": True,
|
||||||
"goauthentikio-user-override-ips": ["true"],
|
|
||||||
"goauthentikio-user-service-account": ["true"],
|
|
||||||
},
|
},
|
||||||
"type": "searchResEntry",
|
"type": "searchResEntry",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dn": f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
"dn": f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"cn": [self.user.username],
|
"cn": self.user.username,
|
||||||
"sAMAccountName": [self.user.username],
|
"sAMAccountName": self.user.username,
|
||||||
"uid": [self.user.uid],
|
"uid": self.user.uid,
|
||||||
"name": [self.user.name],
|
"name": self.user.name,
|
||||||
"displayName": [self.user.name],
|
"displayName": self.user.name,
|
||||||
"sn": [self.user.name],
|
"sn": self.user.name,
|
||||||
"mail": [self.user.email],
|
"mail": self.user.email,
|
||||||
"objectClass": [
|
"objectClass": [
|
||||||
"user",
|
"user",
|
||||||
"organizationalPerson",
|
"organizationalPerson",
|
||||||
"inetOrgPerson",
|
"inetOrgPerson",
|
||||||
"goauthentik.io/ldap/user",
|
"goauthentik.io/ldap/user",
|
||||||
],
|
],
|
||||||
"uidNumber": [str(2000 + self.user.pk)],
|
"uidNumber": 2000 + self.user.pk,
|
||||||
"gidNumber": [str(2000 + self.user.pk)],
|
"gidNumber": 2000 + self.user.pk,
|
||||||
"memberOf": [
|
"memberOf": [
|
||||||
f"cn={group.name},ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
f"cn={group.name},ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
||||||
for group in self.user.ak_groups.all()
|
for group in self.user.ak_groups.all()
|
||||||
],
|
],
|
||||||
"homeDirectory": [
|
"homeDirectory": f"/home/{self.user.username}",
|
||||||
f"/home/{self.user.username}",
|
"ak-active": True,
|
||||||
],
|
"ak-superuser": True,
|
||||||
"ak-active": ["true"],
|
|
||||||
"ak-superuser": ["true"],
|
|
||||||
"extraAttribute": ["bar"],
|
"extraAttribute": ["bar"],
|
||||||
},
|
},
|
||||||
"type": "searchResEntry",
|
"type": "searchResEntry",
|
||||||
|
|
Reference in New Issue