2021-04-26 09:53:06 +00:00
package ldap
import (
2021-05-04 19:49:15 +00:00
"errors"
2021-04-26 09:53:06 +00:00
"fmt"
"strings"
2021-07-22 20:45:48 +00:00
"sync"
2021-04-26 09:53:06 +00:00
2021-07-22 17:17:34 +00:00
"github.com/getsentry/sentry-go"
2021-05-04 19:49:15 +00:00
"github.com/nmcclain/ldap"
2021-09-09 13:52:24 +00:00
"github.com/prometheus/client_golang/prometheus"
2021-06-29 14:21:00 +00:00
"goauthentik.io/api"
2021-09-09 13:52:24 +00:00
"goauthentik.io/internal/outpost/ldap/metrics"
"goauthentik.io/internal/utils"
2021-04-26 09:53:06 +00:00
)
2021-08-05 16:16:06 +00:00
func ( pi * ProviderInstance ) SearchMe ( req SearchRequest , f UserFlags ) ( ldap . ServerSearchResult , error ) {
if f . UserInfo == nil {
2021-08-21 14:49:34 +00:00
u , _ , err := pi . s . ac . Client . CoreApi . CoreUsersRetrieve ( req . ctx , f . UserPk ) . Execute ( )
2021-08-05 16:16:06 +00:00
if err != nil {
req . log . WithError ( err ) . Warning ( "Failed to get user info" )
2021-08-21 14:49:34 +00:00
return ldap . ServerSearchResult { ResultCode : ldap . LDAPResultOperationsError } , fmt . Errorf ( "failed to get userinfo" )
2021-08-05 16:16:06 +00:00
}
f . UserInfo = & u
}
2021-05-24 14:09:05 +00:00
entries := make ( [ ] * ldap . Entry , 1 )
2021-08-05 16:16:06 +00:00
entries [ 0 ] = pi . UserEntry ( * f . UserInfo )
2021-05-24 14:09:05 +00:00
return ldap . ServerSearchResult { Entries : entries , Referrals : [ ] string { } , Controls : [ ] ldap . Control { } , ResultCode : ldap . LDAPResultSuccess } , nil
}
2021-07-19 11:41:29 +00:00
func ( pi * ProviderInstance ) Search ( req SearchRequest ) ( ldap . ServerSearchResult , error ) {
2021-07-22 17:17:34 +00:00
accsp := sentry . StartSpan ( req . ctx , "authentik.providers.ldap.search.check_access" )
2021-04-26 09:53:06 +00:00
baseDN := strings . ToLower ( "," + pi . BaseDN )
entries := [ ] * ldap . Entry { }
2021-07-19 11:41:29 +00:00
filterEntity , err := ldap . GetFilterObjectClass ( req . Filter )
2021-04-26 09:53:06 +00:00
if err != nil {
2021-09-09 13:52:24 +00:00
metrics . RequestsRejected . With ( prometheus . Labels {
2021-09-16 08:03:31 +00:00
"outpost_name" : pi . outpostName ,
"type" : "search" ,
"reason" : "filter_parse_fail" ,
"dn" : req . BindDN ,
"client" : utils . GetIP ( req . conn . RemoteAddr ( ) ) ,
2021-09-09 13:52:24 +00:00
} ) . Inc ( )
2021-07-19 11:41:29 +00:00
return ldap . ServerSearchResult { ResultCode : ldap . LDAPResultOperationsError } , fmt . Errorf ( "Search Error: error parsing filter: %s" , req . Filter )
2021-04-26 09:53:06 +00:00
}
2021-07-19 11:41:29 +00:00
if len ( req . BindDN ) < 1 {
2021-09-09 13:52:24 +00:00
metrics . RequestsRejected . With ( prometheus . Labels {
2021-09-16 08:03:31 +00:00
"outpost_name" : pi . outpostName ,
"type" : "search" ,
"reason" : "empty_bind_dn" ,
"dn" : req . BindDN ,
"client" : utils . GetIP ( req . conn . RemoteAddr ( ) ) ,
2021-09-09 13:52:24 +00:00
} ) . Inc ( )
2021-07-19 11:41:29 +00:00
return ldap . ServerSearchResult { ResultCode : ldap . LDAPResultInsufficientAccessRights } , fmt . Errorf ( "Search Error: Anonymous BindDN not allowed %s" , req . BindDN )
2021-04-26 09:53:06 +00:00
}
2021-07-19 11:41:29 +00:00
if ! strings . HasSuffix ( req . BindDN , baseDN ) {
2021-09-09 13:52:24 +00:00
metrics . RequestsRejected . With ( prometheus . Labels {
2021-09-16 08:03:31 +00:00
"outpost_name" : pi . outpostName ,
"type" : "search" ,
"reason" : "invalid_bind_dn" ,
"dn" : req . BindDN ,
"client" : utils . GetIP ( req . conn . RemoteAddr ( ) ) ,
2021-09-09 13:52:24 +00:00
} ) . Inc ( )
2021-07-19 11:41:29 +00:00
return ldap . ServerSearchResult { ResultCode : ldap . LDAPResultInsufficientAccessRights } , fmt . Errorf ( "Search Error: BindDN %s not in our BaseDN %s" , req . BindDN , pi . BaseDN )
2021-04-26 09:53:06 +00:00
}
2021-05-04 19:49:15 +00:00
pi . boundUsersMutex . RLock ( )
2021-07-19 11:41:29 +00:00
flags , ok := pi . boundUsers [ req . BindDN ]
2021-07-22 18:38:30 +00:00
pi . boundUsersMutex . RUnlock ( )
2021-05-04 19:49:15 +00:00
if ! ok {
2021-05-12 16:49:15 +00:00
pi . log . Debug ( "User info not cached" )
2021-09-09 13:52:24 +00:00
metrics . RequestsRejected . With ( prometheus . Labels {
2021-09-16 08:03:31 +00:00
"outpost_name" : pi . outpostName ,
"type" : "search" ,
"reason" : "user_info_not_cached" ,
"dn" : req . BindDN ,
"client" : utils . GetIP ( req . conn . RemoteAddr ( ) ) ,
2021-09-09 13:52:24 +00:00
} ) . Inc ( )
2021-05-08 18:59:31 +00:00
return ldap . ServerSearchResult { ResultCode : ldap . LDAPResultInsufficientAccessRights } , errors . New ( "access denied" )
2021-05-04 19:49:15 +00:00
}
2021-09-26 12:01:22 +00:00
if req . SearchRequest . Scope == ldap . ScopeBaseObject {
pi . log . Debug ( "base scope, showing domain info" )
return pi . SearchBase ( req , flags . CanSearch )
}
2021-05-04 19:49:15 +00:00
if ! flags . CanSearch {
2021-05-24 14:09:05 +00:00
pi . log . Debug ( "User can't search, showing info about user" )
2021-08-05 16:16:06 +00:00
return pi . SearchMe ( req , flags )
2021-05-04 19:49:15 +00:00
}
2021-07-22 17:17:34 +00:00
accsp . Finish ( )
2021-05-04 19:49:15 +00:00
2021-07-23 13:41:09 +00:00
parsedFilter , err := ldap . CompileFilter ( req . Filter )
if err != nil {
2021-09-09 13:52:24 +00:00
metrics . RequestsRejected . With ( prometheus . Labels {
2021-09-16 08:03:31 +00:00
"outpost_name" : pi . outpostName ,
"type" : "search" ,
"reason" : "filter_parse_fail" ,
"dn" : req . BindDN ,
"client" : utils . GetIP ( req . conn . RemoteAddr ( ) ) ,
2021-09-09 13:52:24 +00:00
} ) . Inc ( )
2021-07-23 13:41:09 +00:00
return ldap . ServerSearchResult { ResultCode : ldap . LDAPResultOperationsError } , fmt . Errorf ( "Search Error: error parsing filter: %s" , req . Filter )
}
2021-08-21 15:53:09 +00:00
// Create a custom client to set additional headers
c := api . NewAPIClient ( pi . s . ac . Client . GetConfig ( ) )
c . GetConfig ( ) . AddDefaultHeader ( "X-authentik-outpost-ldap-query" , req . Filter )
2021-04-26 09:53:06 +00:00
switch filterEntity {
default :
2021-09-09 13:52:24 +00:00
metrics . RequestsRejected . With ( prometheus . Labels {
2021-09-16 08:03:31 +00:00
"outpost_name" : pi . outpostName ,
"type" : "search" ,
"reason" : "unhandled_filter_type" ,
"dn" : req . BindDN ,
"client" : utils . GetIP ( req . conn . RemoteAddr ( ) ) ,
2021-09-09 13:52:24 +00:00
} ) . Inc ( )
2021-07-19 11:41:29 +00:00
return ldap . ServerSearchResult { ResultCode : ldap . LDAPResultOperationsError } , fmt . Errorf ( "Search Error: unhandled filter type: %s [%s]" , filterEntity , req . Filter )
2021-09-26 12:42:26 +00:00
case "goauthentik.io/ldap/group" :
fallthrough
case "goauthentik.io/ldap/virtual-group" :
fallthrough
2021-04-26 09:53:06 +00:00
case GroupObjectClass :
2021-07-22 20:45:48 +00:00
wg := sync . WaitGroup { }
wg . Add ( 2 )
2021-07-14 07:17:01 +00:00
2021-07-22 20:45:48 +00:00
gEntries := make ( [ ] * ldap . Entry , 0 )
uEntries := make ( [ ] * ldap . Entry , 0 )
2021-07-14 07:17:01 +00:00
2021-07-22 20:45:48 +00:00
go func ( ) {
defer wg . Done ( )
gapisp := sentry . StartSpan ( req . ctx , "authentik.providers.ldap.search.api_group" )
2021-08-21 15:53:09 +00:00
searchReq , skip := parseFilterForGroup ( c . CoreApi . CoreGroupsList ( gapisp . Context ( ) ) , parsedFilter , false )
if skip {
pi . log . Trace ( "Skip backend request" )
return
}
groups , _ , err := searchReq . Execute ( )
2021-07-22 20:45:48 +00:00
gapisp . Finish ( )
if err != nil {
req . log . WithError ( err ) . Warning ( "failed to get groups" )
return
}
pi . log . WithField ( "count" , len ( groups . Results ) ) . Trace ( "Got results from API" )
2021-07-14 07:17:01 +00:00
2021-07-22 20:45:48 +00:00
for _ , g := range groups . Results {
gEntries = append ( gEntries , pi . GroupEntry ( pi . APIGroupToLDAPGroup ( g ) ) )
}
} ( )
go func ( ) {
defer wg . Done ( )
uapisp := sentry . StartSpan ( req . ctx , "authentik.providers.ldap.search.api_user" )
2021-08-21 15:53:09 +00:00
searchReq , skip := parseFilterForUser ( c . CoreApi . CoreUsersList ( uapisp . Context ( ) ) , parsedFilter , false )
if skip {
pi . log . Trace ( "Skip backend request" )
return
}
users , _ , err := searchReq . Execute ( )
2021-07-22 20:45:48 +00:00
uapisp . Finish ( )
if err != nil {
2021-08-21 14:49:34 +00:00
req . log . WithError ( err ) . Warning ( "failed to get users" )
2021-07-22 20:45:48 +00:00
return
}
for _ , u := range users . Results {
uEntries = append ( uEntries , pi . GroupEntry ( pi . APIUserToLDAPGroup ( u ) ) )
}
} ( )
wg . Wait ( )
entries = append ( gEntries , uEntries ... )
2021-09-26 12:42:26 +00:00
case "" :
fallthrough
case "organizationalPerson" :
fallthrough
case "inetorgperson" :
fallthrough
case "goauthentik.io/ldap/user" :
fallthrough
case UserObjectClass :
2021-07-22 17:17:34 +00:00
uapisp := sentry . StartSpan ( req . ctx , "authentik.providers.ldap.search.api_user" )
2021-08-21 15:53:09 +00:00
searchReq , skip := parseFilterForUser ( c . CoreApi . CoreUsersList ( uapisp . Context ( ) ) , parsedFilter , false )
if skip {
pi . log . Trace ( "Skip backend request" )
return ldap . ServerSearchResult { Entries : entries , Referrals : [ ] string { } , Controls : [ ] ldap . Control { } , ResultCode : ldap . LDAPResultSuccess } , nil
}
users , _ , err := searchReq . Execute ( )
2021-07-22 17:17:34 +00:00
uapisp . Finish ( )
2021-04-26 09:53:06 +00:00
if err != nil {
return ldap . ServerSearchResult { ResultCode : ldap . LDAPResultOperationsError } , fmt . Errorf ( "API Error: %s" , err )
}
2021-05-16 19:07:01 +00:00
for _ , u := range users . Results {
2021-05-24 14:09:05 +00:00
entries = append ( entries , pi . UserEntry ( u ) )
2021-04-26 09:53:06 +00:00
}
}
return ldap . ServerSearchResult { Entries : entries , Referrals : [ ] string { } , Controls : [ ] ldap . Control { } , ResultCode : ldap . LDAPResultSuccess } , nil
}
2021-05-24 14:09:05 +00:00
func ( pi * ProviderInstance ) UserEntry ( u api . User ) * ldap . Entry {
2021-07-13 16:24:18 +00:00
dn := pi . GetUserDN ( u . Username )
2021-09-21 19:59:39 +00:00
attrs := AKAttrsToLDAP ( u . Attributes )
attrs = pi . ensureAttributes ( attrs , map [ string ] [ ] string {
"memberOf" : pi . GroupsForUser ( u ) ,
// Old fields for backwards compatibility
"accountStatus" : { BoolToString ( * u . IsActive ) } ,
"superuser" : { BoolToString ( u . IsSuperuser ) } ,
"goauthentik.io/ldap/active" : { BoolToString ( * u . IsActive ) } ,
"goauthentik.io/ldap/superuser" : { BoolToString ( u . IsSuperuser ) } ,
"cn" : { u . Username } ,
"sAMAccountName" : { u . Username } ,
"uid" : { u . Uid } ,
"name" : { u . Name } ,
"displayName" : { u . Name } ,
"mail" : { * u . Email } ,
2021-09-26 12:42:26 +00:00
"objectClass" : { UserObjectClass , "organizationalPerson" , "inetorgperson" , "goauthentik.io/ldap/user" } ,
2021-09-21 19:59:39 +00:00
"uidNumber" : { pi . GetUidNumber ( u ) } ,
"gidNumber" : { pi . GetUidNumber ( u ) } ,
} )
2021-05-24 14:09:05 +00:00
return & ldap . Entry { DN : dn , Attributes : attrs }
}
2021-07-14 07:17:01 +00:00
func ( pi * ProviderInstance ) GroupEntry ( g LDAPGroup ) * ldap . Entry {
2021-09-21 19:59:39 +00:00
attrs := AKAttrsToLDAP ( g . akAttributes )
2021-07-13 16:24:18 +00:00
2021-09-21 19:59:39 +00:00
objectClass := [ ] string { GroupObjectClass , "goauthentik.io/ldap/group" }
2021-07-14 18:37:27 +00:00
if g . isVirtualGroup {
2021-09-21 19:59:39 +00:00
objectClass = [ ] string { GroupObjectClass , "goauthentik.io/ldap/group" , "goauthentik.io/ldap/virtual-group" }
2021-07-14 07:17:01 +00:00
}
2021-09-21 19:59:39 +00:00
attrs = pi . ensureAttributes ( attrs , map [ string ] [ ] string {
"objectClass" : objectClass ,
"member" : g . member ,
"goauthentik.io/ldap/superuser" : { BoolToString ( g . isSuperuser ) } ,
"cn" : { g . cn } ,
"uid" : { g . uid } ,
"sAMAccountName" : { g . cn } ,
"gidNumber" : { g . gidNumber } ,
} )
2021-07-14 07:17:01 +00:00
return & ldap . Entry { DN : g . dn , Attributes : attrs }
2021-05-24 14:09:05 +00:00
}