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"""
from dataclasses import asdict, dataclass, field
from datetime import datetime
from typing import Iterable, Optional, Union
from typing import Any, Iterable, Optional, Union
from uuid import uuid4
from dacite import from_dict
@ -37,6 +37,7 @@ from authentik.outposts.docker_tls import DockerInlineTLS
OUR_VERSION = parse(__version__)
OUTPOST_HELLO_INTERVAL = 10
LOGGER = get_logger()
USER_ATTRIBUTE_LDAP_CAN_SEARCH = "goauthentik.io/ldap/can-search"
class ServiceConnectionInvalid(SentryIgnoredException):
@ -321,13 +322,21 @@ class Outpost(models.Model):
"""Username for service user"""
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
def user(self) -> User:
"""Get/create user with access to all required objects"""
users = User.objects.filter(username=self.user_identifier)
if not users.exists():
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.save()
else:

View File

@ -16,7 +16,6 @@ require (
github.com/go-openapi/validate v0.20.2
github.com/go-redis/redis/v7 v7.4.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/gorilla/websocket v1.4.2
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
@ -25,6 +24,7 @@ require (
github.com/magiconair/properties v1.8.5 // indirect
github.com/mailru/easyjson v0.7.7 // 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/pelletier/go-toml v1.9.0 // indirect
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/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/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/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
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/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/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/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oauth2-proxy/oauth2-proxy v0.0.0-20200831161845-e4e5580852dc h1:jf/4meI7lkRwGoiD7Ex/ns0BekEPKZ8nsB3u2oLhLGM=

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"strings"
"sync"
log "github.com/sirupsen/logrus"
"goauthentik.io/outpost/pkg/client/outposts"
@ -29,6 +30,8 @@ func (ls *LDAPServer) Refresh() error {
flowSlug: *provider.BindFlowSlug,
s: ls,
log: log.WithField("logger", "authentik.outpost.ldap").WithField("provider", provider.Name),
boundUsersMutex: sync.RWMutex{},
boundUsers: make(map[string]UserFlags),
}
}
ls.providers = providers

View File

@ -1,18 +1,17 @@
package ldap
import (
"context"
"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")
for _, instance := range ls.providers {
username, err := instance.getUsername(bindDN)
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")

View File

@ -8,10 +8,11 @@ import (
"net/http"
"net/http/cookiejar"
"strings"
"time"
goldap "github.com/go-ldap/ldap/v3"
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/flows"
)
@ -44,7 +45,7 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) {
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)
if err != nil {
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")
// Get user info to store in context
userInfo, err := pi.s.ac.Client.Core.CoreUsersMe(&core.CoreUsersMeParams{
Context: ctx,
Context: context.Background(),
HTTPClient: client,
}, httptransport.PassThroughAuth)
if err != nil {
pi.log.WithField("boundDN", username).WithError(err).Warning("failed to get user info")
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
}
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) {
challenge, err := pi.s.ac.Client.Flows.FlowsExecutorGet(&flows.FlowsExecutorGetParams{
FlowSlug: pi.flowSlug,

View File

@ -1,12 +1,13 @@
package ldap
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/goauthentik/ldap"
"github.com/nmcclain/ldap"
"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)
}
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 {
default:
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
import (
"sync"
log "github.com/sirupsen/logrus"
"goauthentik.io/outpost/pkg/ak"
"goauthentik.io/outpost/pkg/models"
"github.com/goauthentik/ldap"
"github.com/nmcclain/ldap"
)
const GroupObjectClass = "group"
@ -20,6 +23,14 @@ type ProviderInstance struct {
flowSlug string
s *LDAPServer
log *log.Entry
boundUsersMutex sync.RWMutex
boundUsers map[string]UserFlags
}
type UserFlags struct {
UserInfo *models.User
CanSearch bool
}
type LDAPServer struct {

View File

@ -5,7 +5,7 @@ import (
"net"
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) {

View File

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