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:
		| @ -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()), | ||||
| 						}, | ||||
| 					}, | ||||
| 					{ | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
							
								
								
									
										96
									
								
								internal/outpost/ldap/search/direct/schema.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								internal/outpost/ldap/search/direct/schema.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @ -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 { | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
|  | ||||
| @ -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) | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L