outposts/ldap: save user DN to determine who can search

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-05-04 21:49:15 +02:00
parent 99d161e212
commit 08451c15f4
10 changed files with 87 additions and 30 deletions

View File

@ -1,7 +1,7 @@
"""Outpost models""" """Outpost models"""
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from datetime import datetime from datetime import datetime
from typing import Iterable, Optional, Union from typing import Any, Iterable, Optional, Union
from uuid import uuid4 from uuid import uuid4
from dacite import from_dict from dacite import from_dict
@ -37,6 +37,7 @@ from authentik.outposts.docker_tls import DockerInlineTLS
OUR_VERSION = parse(__version__) OUR_VERSION = parse(__version__)
OUTPOST_HELLO_INTERVAL = 10 OUTPOST_HELLO_INTERVAL = 10
LOGGER = get_logger() LOGGER = get_logger()
USER_ATTRIBUTE_LDAP_CAN_SEARCH = "goauthentik.io/ldap/can-search"
class ServiceConnectionInvalid(SentryIgnoredException): class ServiceConnectionInvalid(SentryIgnoredException):
@ -321,13 +322,21 @@ class Outpost(models.Model):
"""Username for service user""" """Username for service user"""
return f"ak-outpost-{self.uuid.hex}" return f"ak-outpost-{self.uuid.hex}"
@property
def user_attributes(self) -> dict[str, Any]:
"""Attributes that will be set on the service account"""
attrs = {USER_ATTRIBUTE_SA: True}
if self.type == OutpostType.LDAP:
attrs[USER_ATTRIBUTE_LDAP_CAN_SEARCH] = True
return attrs
@property @property
def user(self) -> User: def user(self) -> User:
"""Get/create user with access to all required objects""" """Get/create user with access to all required objects"""
users = User.objects.filter(username=self.user_identifier) users = User.objects.filter(username=self.user_identifier)
if not users.exists(): if not users.exists():
user: User = User.objects.create(username=self.user_identifier) user: User = User.objects.create(username=self.user_identifier)
user.attributes[USER_ATTRIBUTE_SA] = True user.attributes = self.user_attributes
user.set_unusable_password() user.set_unusable_password()
user.save() user.save()
else: else:

View File

@ -16,7 +16,6 @@ require (
github.com/go-openapi/validate v0.20.2 github.com/go-openapi/validate v0.20.2
github.com/go-redis/redis/v7 v7.4.0 // indirect github.com/go-redis/redis/v7 v7.4.0 // indirect
github.com/go-swagger/go-swagger v0.27.0 // indirect github.com/go-swagger/go-swagger v0.27.0 // indirect
github.com/goauthentik/ldap v0.0.0-20210429185144-85625dd05305 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
@ -25,6 +24,7 @@ require (
github.com/magiconair/properties v1.8.5 // indirect github.com/magiconair/properties v1.8.5 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 // indirect github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 // indirect
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3 // indirect
github.com/oauth2-proxy/oauth2-proxy v0.0.0-20200831161845-e4e5580852dc github.com/oauth2-proxy/oauth2-proxy v0.0.0-20200831161845-e4e5580852dc
github.com/pelletier/go-toml v1.9.0 // indirect github.com/pelletier/go-toml v1.9.0 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1

View File

@ -262,10 +262,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/go-swagger/go-swagger v0.27.0 h1:K7+nkBuf4oS1jTBrdvWqYFpqD69V5CN8HamZzCDDhAI= github.com/go-swagger/go-swagger v0.27.0 h1:K7+nkBuf4oS1jTBrdvWqYFpqD69V5CN8HamZzCDDhAI=
github.com/go-swagger/go-swagger v0.27.0/go.mod h1:WodZVysInJilkW7e6IRw+dZGp5yW6rlMFZ4cb+THl9A= github.com/go-swagger/go-swagger v0.27.0/go.mod h1:WodZVysInJilkW7e6IRw+dZGp5yW6rlMFZ4cb+THl9A=
github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0= github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0=
github.com/goauthentik/ldap v0.0.0-20191021200707-3b3b69a7e9e3 h1:KQTKvQ6kc4FR5TBrtXqWOiguWRfdgeF78fryWadJWhk=
github.com/goauthentik/ldap v0.0.0-20191021200707-3b3b69a7e9e3/go.mod h1:CHV/6IPAy1W7K0UmTFp5cvr0cywMc0AUpD34fcmSXqM=
github.com/goauthentik/ldap v0.0.0-20210429185144-85625dd05305 h1:L7wiuRMudhGOTqA27d/cZWBJNZuDxwhhOKM9/Xj9MK4=
github.com/goauthentik/ldap v0.0.0-20210429185144-85625dd05305/go.mod h1:5Aek6cM0R+tYrhZh1BVX12a6NPFYPAlB8udRmWhhg6s=
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
@ -509,6 +505,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 h1:D9EvfGQvlkKaDr2CRKN++7HbSXbefUNDrPq60T+g24s= github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 h1:D9EvfGQvlkKaDr2CRKN++7HbSXbefUNDrPq60T+g24s=
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484/go.mod h1:O1EljZ+oHprtxDDPHiMWVo/5dBT6PlvWX5PSwj80aBA= github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484/go.mod h1:O1EljZ+oHprtxDDPHiMWVo/5dBT6PlvWX5PSwj80aBA=
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3 h1:NNis9uuNpG5h97Dvxxo53Scg02qBg+3Nfabg6zjFGu8=
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3/go.mod h1:YtrVB1/v9Td9SyjXpjYVmbdKgj9B0nPTBsdGUxy0i8U=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oauth2-proxy/oauth2-proxy v0.0.0-20200831161845-e4e5580852dc h1:jf/4meI7lkRwGoiD7Ex/ns0BekEPKZ8nsB3u2oLhLGM= github.com/oauth2-proxy/oauth2-proxy v0.0.0-20200831161845-e4e5580852dc h1:jf/4meI7lkRwGoiD7Ex/ns0BekEPKZ8nsB3u2oLhLGM=

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"sync"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"goauthentik.io/outpost/pkg/client/outposts" "goauthentik.io/outpost/pkg/client/outposts"
@ -22,13 +23,15 @@ func (ls *LDAPServer) Refresh() error {
userDN := strings.ToLower(fmt.Sprintf("cn=users,%s", provider.BaseDn)) userDN := strings.ToLower(fmt.Sprintf("cn=users,%s", provider.BaseDn))
groupDN := strings.ToLower(fmt.Sprintf("cn=groups,%s", provider.BaseDn)) groupDN := strings.ToLower(fmt.Sprintf("cn=groups,%s", provider.BaseDn))
providers[idx] = &ProviderInstance{ providers[idx] = &ProviderInstance{
BaseDN: provider.BaseDn, BaseDN: provider.BaseDn,
GroupDN: groupDN, GroupDN: groupDN,
UserDN: userDN, UserDN: userDN,
appSlug: *provider.ApplicationSlug, appSlug: *provider.ApplicationSlug,
flowSlug: *provider.BindFlowSlug, flowSlug: *provider.BindFlowSlug,
s: ls, s: ls,
log: log.WithField("logger", "authentik.outpost.ldap").WithField("provider", provider.Name), log: log.WithField("logger", "authentik.outpost.ldap").WithField("provider", provider.Name),
boundUsersMutex: sync.RWMutex{},
boundUsers: make(map[string]UserFlags),
} }
} }
ls.providers = providers ls.providers = providers

View File

@ -1,18 +1,17 @@
package ldap package ldap
import ( import (
"context"
"net" "net"
"github.com/goauthentik/ldap" "github.com/nmcclain/ldap"
) )
func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn, ctx context.Context) (ldap.LDAPResultCode, error) { func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) {
ls.log.WithField("boundDN", bindDN).Info("bind") ls.log.WithField("boundDN", bindDN).Info("bind")
for _, instance := range ls.providers { for _, instance := range ls.providers {
username, err := instance.getUsername(bindDN) username, err := instance.getUsername(bindDN)
if err == nil { if err == nil {
return instance.Bind(username, bindPW, conn, ctx) return instance.Bind(username, bindPW, conn)
} }
} }
ls.log.WithField("boundDN", bindDN).WithField("request", "bind").Warning("No provider found for request") ls.log.WithField("boundDN", bindDN).WithField("request", "bind").Warning("No provider found for request")

View File

@ -8,10 +8,11 @@ import (
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"strings" "strings"
"time"
goldap "github.com/go-ldap/ldap/v3" goldap "github.com/go-ldap/ldap/v3"
httptransport "github.com/go-openapi/runtime/client" httptransport "github.com/go-openapi/runtime/client"
"github.com/goauthentik/ldap" "github.com/nmcclain/ldap"
"goauthentik.io/outpost/pkg/client/core" "goauthentik.io/outpost/pkg/client/core"
"goauthentik.io/outpost/pkg/client/flows" "goauthentik.io/outpost/pkg/client/flows"
) )
@ -44,7 +45,7 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) {
return "", errors.New("failed to find dn") return "", errors.New("failed to find dn")
} }
func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn, ctx context.Context) (ldap.LDAPResultCode, error) { func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) {
jar, err := cookiejar.New(nil) jar, err := cookiejar.New(nil)
if err != nil { if err != nil {
pi.log.WithError(err).Warning("Failed to create cookiejar") pi.log.WithError(err).Warning("Failed to create cookiejar")
@ -77,17 +78,42 @@ func (pi *ProviderInstance) Bind(username string, bindPW string, conn net.Conn,
pi.log.WithField("boundDN", username).Info("User has access") pi.log.WithField("boundDN", username).Info("User has access")
// Get user info to store in context // Get user info to store in context
userInfo, err := pi.s.ac.Client.Core.CoreUsersMe(&core.CoreUsersMeParams{ userInfo, err := pi.s.ac.Client.Core.CoreUsersMe(&core.CoreUsersMeParams{
Context: ctx, Context: context.Background(),
HTTPClient: client, HTTPClient: client,
}, httptransport.PassThroughAuth) }, httptransport.PassThroughAuth)
if err != nil { if err != nil {
pi.log.WithField("boundDN", username).WithError(err).Warning("failed to get user info") pi.log.WithField("boundDN", username).WithError(err).Warning("failed to get user info")
return ldap.LDAPResultOperationsError, nil return ldap.LDAPResultOperationsError, nil
} }
ctx = context.WithValue(ctx, ContextUserKey, userInfo.Payload.User) pi.boundUsersMutex.Lock()
pi.boundUsers[username] = UserFlags{
UserInfo: userInfo.Payload.User,
CanSearch: userInfo.Payload.User.Attributes.(map[string]bool)["goauthentik.io/ldap/can-search"],
}
pi.boundUsersMutex.Unlock()
pi.delayDeleteUserInfo(username)
return ldap.LDAPResultSuccess, nil return ldap.LDAPResultSuccess, nil
} }
func (pi *ProviderInstance) delayDeleteUserInfo(dn string) {
ticker := time.NewTicker(30 * time.Second)
quit := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
pi.boundUsersMutex.Lock()
delete(pi.boundUsers, dn)
pi.boundUsersMutex.Unlock()
close(quit)
case <-quit:
ticker.Stop()
return
}
}
}()
}
func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, client *http.Client) (bool, error) { func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, client *http.Client) (bool, error) {
challenge, err := pi.s.ac.Client.Flows.FlowsExecutorGet(&flows.FlowsExecutorGetParams{ challenge, err := pi.s.ac.Client.Flows.FlowsExecutorGet(&flows.FlowsExecutorGetParams{
FlowSlug: pi.flowSlug, FlowSlug: pi.flowSlug,

View File

@ -1,12 +1,13 @@
package ldap package ldap
import ( import (
"errors"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
"strings" "strings"
"github.com/goauthentik/ldap" "github.com/nmcclain/ldap"
"goauthentik.io/outpost/pkg/client/core" "goauthentik.io/outpost/pkg/client/core"
) )
@ -26,6 +27,16 @@ func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest,
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", bindDN, pi.BaseDN) return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", bindDN, pi.BaseDN)
} }
pi.boundUsersMutex.RLock()
defer pi.boundUsersMutex.RUnlock()
flags, ok := pi.boundUsers[bindDN]
if !ok {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("Access denied")
}
if !flags.CanSearch {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("Access denied")
}
switch filterEntity { switch filterEntity {
default: default:
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, searchReq.Filter) return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, searchReq.Filter)

View File

@ -1,10 +1,13 @@
package ldap package ldap
import ( import (
"sync"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"goauthentik.io/outpost/pkg/ak" "goauthentik.io/outpost/pkg/ak"
"goauthentik.io/outpost/pkg/models"
"github.com/goauthentik/ldap" "github.com/nmcclain/ldap"
) )
const GroupObjectClass = "group" const GroupObjectClass = "group"
@ -16,10 +19,18 @@ type ProviderInstance struct {
UserDN string UserDN string
GroupDN string GroupDN string
appSlug string appSlug string
flowSlug string flowSlug string
s *LDAPServer s *LDAPServer
log *log.Entry log *log.Entry
boundUsersMutex sync.RWMutex
boundUsers map[string]UserFlags
}
type UserFlags struct {
UserInfo *models.User
CanSearch bool
} }
type LDAPServer struct { type LDAPServer struct {

View File

@ -5,7 +5,7 @@ import (
"net" "net"
goldap "github.com/go-ldap/ldap/v3" goldap "github.com/go-ldap/ldap/v3"
"github.com/goauthentik/ldap" "github.com/nmcclain/ldap"
) )
func (ls *LDAPServer) Search(boundDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) { func (ls *LDAPServer) Search(boundDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {

View File

@ -3,7 +3,7 @@ package ldap
import ( import (
"fmt" "fmt"
"github.com/goauthentik/ldap" "github.com/nmcclain/ldap"
"goauthentik.io/outpost/pkg/models" "goauthentik.io/outpost/pkg/models"
) )