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:
Jens L 2023-06-08 15:16:40 +02:00 committed by GitHub
parent bf1a363124
commit 54ef88a6fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 369 additions and 188 deletions

View File

@ -7,7 +7,7 @@ import (
)
type Binder interface {
GetUsername(string) (string, error)
GetUsername(dn string) (string, error)
Bind(username string, req *Request) (ldap.LDAPResultCode, error)
Unbind(username string, req *Request) (ldap.LDAPResultCode, error)
TimerFlowCacheExpiry(context.Context)

View File

@ -1,9 +1,18 @@
package constants
const OC = "objectClass"
const (
OCTop = "top"
OCDomain = "domain"
OCNSContainer = "nsContainer"
OCSubSchema = "subschema"
)
const (
SearchAttributeNone = "1.1"
SearchAttributeAllUser = "*"
SearchAttributeAllOperational = "+"
)
const (
@ -20,7 +29,7 @@ const (
OCOrgPerson = "organizationalPerson"
OCInetOrgPerson = "inetOrgPerson"
OCAKUser = "goauthentik.io/ldap/user"
OCPosixAccount = "posixAccount"
OCPosixAccount = "posixAccount"
)
const (

View File

@ -2,16 +2,13 @@ package ldap
import (
"crypto/tls"
"fmt"
"strings"
"sync"
"beryju.io/ldap"
"github.com/go-openapi/strfmt"
log "github.com/sirupsen/logrus"
"goauthentik.io/api/v3"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ldap/bind"
ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/flags"
@ -107,43 +104,6 @@ func (pi *ProviderInstance) GetSearchAllowedGroups() []*strfmt.UUID {
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) {
needUsers := false
needGroups := false

View File

@ -1,15 +1,13 @@
package ldap
import (
"errors"
"net"
"strings"
"beryju.io/ldap"
"github.com/getsentry/sentry-go"
goldap "github.com/go-ldap/ldap/v3"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/metrics"
"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))
}()
if searchReq.BaseDN == "" {
return ldap.ServerSearchResult{
Entries: []*ldap.Entry{
{
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
selectedProvider := ls.providerForRequest(req)
if selectedProvider == nil {
return ls.fallbackRootDSE(req)
}
bd, err := goldap.ParseDN(strings.ToLower(searchReq.BaseDN))
selectedApp = selectedProvider.GetAppSlug()
result, err := ls.searchRoute(req, selectedProvider)
if err != nil {
req.Log().WithError(err).Info("failed to parse basedn")
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 result, nil
}
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{
Entries: []*ldap.Entry{
{
@ -75,15 +55,30 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
Attributes: []*ldap.EntryAttribute{
{
Name: "objectClass",
Values: []string{"top", "OpenLDAProotDSE"},
Values: []string{constants.OCTop},
},
{
Name: "entryDN",
Values: []string{""},
},
{
Name: "subschemaSubentry",
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,
}, nil
}

View File

@ -2,40 +2,51 @@ package direct
import (
"fmt"
"strings"
"beryju.io/ldap"
"goauthentik.io/internal/constants"
ldapConstants "goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/search"
)
func (ds *DirectSearcher) SearchBase(req *search.Request, authz bool) (ldap.ServerSearchResult, error) {
dn := ""
if authz {
dn = req.SearchRequest.BaseDN
func (ds *DirectSearcher) SearchBase(req *search.Request) (ldap.ServerSearchResult, error) {
if req.Scope == ldap.ScopeSingleLevel {
return ldap.ServerSearchResult{
ResultCode: ldap.LDAPResultNoSuchObject,
}, nil
}
return ldap.ServerSearchResult{
Entries: []*ldap.Entry{
{
DN: dn,
DN: "",
Attributes: []*ldap.EntryAttribute{
{
Name: "distinguishedName",
Values: []string{ds.si.GetBaseDN()},
Name: "objectClass",
Values: []string{ldapConstants.OCTop},
},
{
Name: "objectClass",
Values: []string{"top", "domain"},
Name: "entryDN",
Values: []string{""},
},
{
Name: "supportedLDAPVersion",
Values: []string{"3"},
},
{
Name: "subschemaSubentry",
Values: []string{"cn=subschema"},
},
{
Name: "namingContexts",
Values: []string{
ds.si.GetBaseDN(),
ds.si.GetBaseUserDN(),
ds.si.GetBaseGroupDN(),
strings.ToLower(ds.si.GetBaseDN()),
},
},
{
Name: "rootDomainNamingContext",
Values: []string{
strings.ToLower(ds.si.GetBaseDN()),
},
},
{

View File

@ -31,7 +31,6 @@ func NewDirectSearcher(si server.LDAPServerInstance) *DirectSearcher {
si: si,
log: log.WithField("logger", "authentik.outpost.ldap.searcher.direct"),
}
ds.log.Info("initialised direct searcher")
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")
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 {
metrics.RequestsRejected.With(prometheus.Labels{
"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)
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 utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) {
entries = append(entries, ds.si.GetBaseEntry())
if utils.IncludeObjectClass(req.FilterObjectClass, constants.GetDomainOCs()) {
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
@ -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())) {
singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseUserDN())
if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseUserDN(), constants.OUUsers))
if !singleu && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ds.si.GetBaseUserDN(), constants.OUUsers))
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 {
entry := ds.si.UserEntry(u)
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())) {
singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseGroupDN())
if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseGroupDN(), constants.OUGroups))
if !singleg && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ds.si.GetBaseGroupDN(), constants.OUGroups))
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 {
entry := group.FromAPIGroup(g, ds.si).Entry()
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())) {
singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseVirtualGroupDN())
if !singlevg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
if !singlevg && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ds.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
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 {
entry := group.FromAPIUser(u, ds.si).Entry()
if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg {

View File

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

View File

@ -15,6 +15,7 @@ import (
"goauthentik.io/internal/outpost/ldap/group"
"goauthentik.io/internal/outpost/ldap/metrics"
"goauthentik.io/internal/outpost/ldap/search"
"goauthentik.io/internal/outpost/ldap/search/direct"
"goauthentik.io/internal/outpost/ldap/server"
"goauthentik.io/internal/outpost/ldap/utils"
"goauthentik.io/internal/outpost/ldap/utils/paginator"
@ -23,6 +24,7 @@ import (
type MemorySearcher struct {
si server.LDAPServerInstance
log *log.Entry
ds *direct.DirectSearcher
users []api.User
groups []api.Group
@ -32,6 +34,7 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
ms := &MemorySearcher{
si: si,
log: log.WithField("logger", "authentik.outpost.ldap.searcher.memory"),
ds: direct.NewDirectSearcher(si),
}
ms.log.Debug("initialised memory searcher")
ms.users = paginator.FetchUsers(ms.si.GetAPIClient().CoreApi.CoreUsersList(context.TODO()))
@ -39,20 +42,18 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
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) {
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
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 {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ms.si.GetOutpostName(),
@ -88,11 +89,15 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
entries := make([]*ldap.Entry, 0)
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 utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) {
entries = append(entries, ms.si.GetBaseEntry())
if utils.IncludeObjectClass(req.FilterObjectClass, constants.GetDomainOCs()) {
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
@ -100,6 +105,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
var users *[]api.User
var groups []*group.LDAPGroup
var err error
if needUsers {
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())) {
singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseUserDN())
if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseUserDN(), constants.OUUsers))
if !singleu && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ms.si.GetBaseUserDN(), constants.OUUsers))
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 {
entry := ms.si.UserEntry(u)
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())) {
singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseGroupDN())
if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseGroupDN(), constants.OUGroups))
if !singleg && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ms.si.GetBaseGroupDN(), constants.OUGroups))
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 {
if strings.EqualFold(req.BaseDN, g.DN) || !singleg {
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())) {
singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseVirtualGroupDN())
if !singlevg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
if !singlevg && utils.IncludeObjectClass(req.FilterObjectClass, constants.GetContainerOCs()) {
entries = append(entries, utils.GetContainerEntry(req.FilterObjectClass, ms.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
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 {
entry := group.FromAPIUser(u, ms.si).Entry()
if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg {

View File

@ -15,8 +15,9 @@ import (
type Request struct {
ldap.SearchRequest
BindDN string
log *log.Entry
BindDN string
FilterObjectClass string
log *log.Entry
id string
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_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{
SearchRequest: searchReq,
BindDN: bindDN,
conn: conn,
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),
id: rid,
ctx: span.Context(),
SearchRequest: searchReq,
BindDN: bindDN,
FilterObjectClass: filterOC,
conn: conn,
log: l,
id: rid,
ctx: span.Context(),
}, span
}
@ -61,3 +75,19 @@ func (r *Request) Log() *log.Entry {
func (r *Request) RemoteAddr() string {
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
}

View File

@ -6,4 +6,6 @@ import (
type Searcher interface {
Search(req *Request) (ldap.ServerSearchResult, error)
SearchBase(req *Request) (ldap.ServerSearchResult, error)
SearchSubschema(req *Request) (ldap.ServerSearchResult, error)
}

View File

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

View File

@ -35,6 +35,5 @@ type LDAPServerInstance interface {
GetFlags(dn string) *flags.UserFlags
SetFlags(dn string, flags *flags.UserFlags)
GetBaseEntry() *ldap.Entry
GetNeededObjects(int, string, string) (bool, bool)
GetNeededObjects(scope int, baseDN string, filterOC string) (bool, bool)
}

View File

@ -238,88 +238,82 @@ class TestProviderLDAP(SeleniumTestCase):
{
"dn": f"cn={o_user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": {
"cn": [o_user.username],
"sAMAccountName": [o_user.username],
"uid": [o_user.uid],
"name": [o_user.name],
"displayName": [o_user.name],
"sn": [o_user.name],
"mail": [""],
"cn": o_user.username,
"sAMAccountName": o_user.username,
"uid": o_user.uid,
"name": o_user.name,
"displayName": o_user.name,
"sn": o_user.name,
"mail": "",
"objectClass": [
"user",
"organizationalPerson",
"inetOrgPerson",
"goauthentik.io/ldap/user",
],
"uidNumber": [str(2000 + o_user.pk)],
"gidNumber": [str(2000 + o_user.pk)],
"uidNumber": 2000 + o_user.pk,
"gidNumber": 2000 + o_user.pk,
"memberOf": [],
"homeDirectory": [
f"/home/{o_user.username}",
],
"ak-active": ["true"],
"ak-superuser": ["false"],
"goauthentikio-user-override-ips": ["true"],
"goauthentikio-user-service-account": ["true"],
"homeDirectory": f"/home/{o_user.username}",
"ak-active": True,
"ak-superuser": False,
"goauthentikio-user-override-ips": True,
"goauthentikio-user-service-account": True,
},
"type": "searchResEntry",
},
{
"dn": f"cn={embedded_account.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": {
"cn": [embedded_account.username],
"sAMAccountName": [embedded_account.username],
"uid": [embedded_account.uid],
"name": [embedded_account.name],
"displayName": [embedded_account.name],
"sn": [embedded_account.name],
"mail": [""],
"cn": embedded_account.username,
"sAMAccountName": embedded_account.username,
"uid": embedded_account.uid,
"name": embedded_account.name,
"displayName": embedded_account.name,
"sn": embedded_account.name,
"mail": "",
"objectClass": [
"user",
"organizationalPerson",
"inetOrgPerson",
"goauthentik.io/ldap/user",
],
"uidNumber": [str(2000 + embedded_account.pk)],
"gidNumber": [str(2000 + embedded_account.pk)],
"uidNumber": 2000 + embedded_account.pk,
"gidNumber": 2000 + embedded_account.pk,
"memberOf": [],
"homeDirectory": [
f"/home/{embedded_account.username}",
],
"ak-active": ["true"],
"ak-superuser": ["false"],
"goauthentikio-user-override-ips": ["true"],
"goauthentikio-user-service-account": ["true"],
"homeDirectory": f"/home/{embedded_account.username}",
"ak-active": True,
"ak-superuser": False,
"goauthentikio-user-override-ips": True,
"goauthentikio-user-service-account": True,
},
"type": "searchResEntry",
},
{
"dn": f"cn={self.user.username},ou=users,dc=ldap,dc=goauthentik,dc=io",
"attributes": {
"cn": [self.user.username],
"sAMAccountName": [self.user.username],
"uid": [self.user.uid],
"name": [self.user.name],
"displayName": [self.user.name],
"sn": [self.user.name],
"mail": [self.user.email],
"cn": self.user.username,
"sAMAccountName": self.user.username,
"uid": self.user.uid,
"name": self.user.name,
"displayName": self.user.name,
"sn": self.user.name,
"mail": self.user.email,
"objectClass": [
"user",
"organizationalPerson",
"inetOrgPerson",
"goauthentik.io/ldap/user",
],
"uidNumber": [str(2000 + self.user.pk)],
"gidNumber": [str(2000 + self.user.pk)],
"uidNumber": 2000 + self.user.pk,
"gidNumber": 2000 + self.user.pk,
"memberOf": [
f"cn={group.name},ou=groups,dc=ldap,dc=goauthentik,dc=io"
for group in self.user.ak_groups.all()
],
"homeDirectory": [
f"/home/{self.user.username}",
],
"ak-active": ["true"],
"ak-superuser": ["true"],
"homeDirectory": f"/home/{self.user.username}",
"ak-active": True,
"ak-superuser": True,
"extraAttribute": ["bar"],
},
"type": "searchResEntry",