root: initial merging of outpost and main project
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
98
internal/outpost/ldap/api.go
Normal file
98
internal/outpost/ldap/api.go
Normal file
@ -0,0 +1,98 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (ls *LDAPServer) Refresh() error {
|
||||
outposts, _, err := ls.ac.Client.OutpostsApi.OutpostsLdapList(context.Background()).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(outposts.Results) < 1 {
|
||||
return errors.New("no ldap provider defined")
|
||||
}
|
||||
providers := make([]*ProviderInstance, len(outposts.Results))
|
||||
for idx, provider := range outposts.Results {
|
||||
userDN := strings.ToLower(fmt.Sprintf("ou=users,%s", *provider.BaseDn))
|
||||
groupDN := strings.ToLower(fmt.Sprintf("ou=groups,%s", *provider.BaseDn))
|
||||
providers[idx] = &ProviderInstance{
|
||||
BaseDN: *provider.BaseDn,
|
||||
GroupDN: groupDN,
|
||||
UserDN: userDN,
|
||||
appSlug: provider.ApplicationSlug,
|
||||
flowSlug: provider.BindFlowSlug,
|
||||
searchAllowedGroups: []*strfmt.UUID{(*strfmt.UUID)(provider.SearchGroup.Get())},
|
||||
boundUsersMutex: sync.RWMutex{},
|
||||
boundUsers: make(map[string]UserFlags),
|
||||
s: ls,
|
||||
log: log.WithField("logger", "authentik.outpost.ldap").WithField("provider", provider.Name),
|
||||
}
|
||||
}
|
||||
ls.providers = providers
|
||||
ls.log.Info("Update providers")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) StartHTTPServer() error {
|
||||
listen := "0.0.0.0:4180" // same port as proxy
|
||||
m := http.NewServeMux()
|
||||
m.HandleFunc("/akprox/ping", func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(204)
|
||||
})
|
||||
ls.log.WithField("listen", listen).Info("Starting http server")
|
||||
return http.ListenAndServe(listen, m)
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) StartLDAPServer() error {
|
||||
listen := "0.0.0.0:3389"
|
||||
ls.log.WithField("listen", listen).Info("Starting ldap server")
|
||||
return ls.s.ListenAndServe(listen)
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) Start() error {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := ls.StartHTTPServer()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := ls.StartLDAPServer()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
type transport struct {
|
||||
headers map[string]string
|
||||
inner http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
for key, value := range t.headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
return t.inner.RoundTrip(req)
|
||||
}
|
||||
func newTransport(inner http.RoundTripper, headers map[string]string) *transport {
|
||||
return &transport{
|
||||
inner: inner,
|
||||
headers: headers,
|
||||
}
|
||||
}
|
||||
23
internal/outpost/ldap/bind.go
Normal file
23
internal/outpost/ldap/bind.go
Normal file
@ -0,0 +1,23 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
)
|
||||
|
||||
func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) {
|
||||
ls.log.WithField("bindDN", bindDN).Info("bind")
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
for _, instance := range ls.providers {
|
||||
username, err := instance.getUsername(bindDN)
|
||||
if err == nil {
|
||||
return instance.Bind(username, bindDN, bindPW, conn)
|
||||
} else {
|
||||
ls.log.WithError(err).Debug("Username not for instance")
|
||||
}
|
||||
}
|
||||
ls.log.WithField("bindDN", bindDN).WithField("request", "bind").Warning("No provider found for request")
|
||||
return ldap.LDAPResultOperationsError, nil
|
||||
}
|
||||
211
internal/outpost/ldap/instance_bind.go
Normal file
211
internal/outpost/ldap/instance_bind.go
Normal file
@ -0,0 +1,211 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/nmcclain/ldap"
|
||||
pkg "goauthentik.io/internal/outpost"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/outpost/api"
|
||||
)
|
||||
|
||||
const ContextUserKey = "ak_user"
|
||||
|
||||
func (pi *ProviderInstance) getUsername(dn string) (string, error) {
|
||||
if !strings.HasSuffix(strings.ToLower(dn), strings.ToLower(pi.BaseDN)) {
|
||||
return "", errors.New("invalid base DN")
|
||||
}
|
||||
dns, err := goldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, part := range dns.RDNs {
|
||||
for _, attribute := range part.Attributes {
|
||||
if strings.ToLower(attribute.Type) == "cn" {
|
||||
return attribute.Value, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", errors.New("failed to find cn")
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) Bind(username string, bindDN, 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")
|
||||
return ldap.LDAPResultOperationsError, nil
|
||||
}
|
||||
host, _, err := net.SplitHostPort(conn.RemoteAddr().String())
|
||||
if err != nil {
|
||||
pi.log.WithError(err).Warning("Failed to get remote IP")
|
||||
return ldap.LDAPResultOperationsError, nil
|
||||
}
|
||||
|
||||
// Create new http client that also sets the correct ip
|
||||
config := api.NewConfiguration()
|
||||
config.Host = pi.s.ac.Client.GetConfig().Host
|
||||
config.Scheme = pi.s.ac.Client.GetConfig().Scheme
|
||||
config.HTTPClient = &http.Client{
|
||||
Jar: jar,
|
||||
Transport: newTransport(ak.SetUserAgent(ak.GetTLSTransport(), pkg.UserAgent()), map[string]string{
|
||||
"X-authentik-remote-ip": host,
|
||||
}),
|
||||
}
|
||||
// create the API client, with the transport
|
||||
apiClient := api.NewAPIClient(config)
|
||||
|
||||
params := url.Values{}
|
||||
params.Add("goauthentik.io/outpost/ldap", "true")
|
||||
passed, rerr := pi.solveFlowChallenge(username, bindPW, apiClient, params.Encode(), 1)
|
||||
if rerr != ldap.LDAPResultSuccess {
|
||||
pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to solve challenge")
|
||||
return rerr, nil
|
||||
}
|
||||
if !passed {
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
p, _, err := apiClient.CoreApi.CoreApplicationsCheckAccessRetrieve(context.Background(), pi.appSlug).Execute()
|
||||
if !p.Passing {
|
||||
pi.log.WithField("bindDN", bindDN).Info("Access denied for user")
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
if err != nil {
|
||||
pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to check access")
|
||||
return ldap.LDAPResultOperationsError, nil
|
||||
}
|
||||
pi.log.WithField("bindDN", bindDN).Info("User has access")
|
||||
// Get user info to store in context
|
||||
userInfo, _, err := apiClient.CoreApi.CoreUsersMeRetrieve(context.Background()).Execute()
|
||||
if err != nil {
|
||||
pi.log.WithField("bindDN", bindDN).WithError(err).Warning("failed to get user info")
|
||||
return ldap.LDAPResultOperationsError, nil
|
||||
}
|
||||
pi.boundUsersMutex.Lock()
|
||||
pi.boundUsers[bindDN] = UserFlags{
|
||||
UserInfo: userInfo.User,
|
||||
CanSearch: pi.SearchAccessCheck(userInfo.User),
|
||||
}
|
||||
defer pi.boundUsersMutex.Unlock()
|
||||
pi.delayDeleteUserInfo(username)
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
// SearchAccessCheck Check if the current user is allowed to search
|
||||
func (pi *ProviderInstance) SearchAccessCheck(user api.User) bool {
|
||||
for _, group := range user.Groups {
|
||||
for _, allowedGroup := range pi.searchAllowedGroups {
|
||||
pi.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access")
|
||||
if group.Pk == allowedGroup.String() {
|
||||
pi.log.WithField("group", group.Name).Info("Allowed access to search")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type ChallengeInt interface {
|
||||
GetComponent() string
|
||||
GetType() api.ChallengeChoices
|
||||
GetResponseErrors() map[string][]api.ErrorDetail
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) solveFlowChallenge(bindDN string, password string, client *api.APIClient, urlParams string, depth int) (bool, ldap.LDAPResultCode) {
|
||||
req := client.FlowsApi.FlowsExecutorGet(context.Background(), pi.flowSlug).Query(urlParams)
|
||||
challenge, _, err := req.Execute()
|
||||
if err != nil {
|
||||
pi.log.WithError(err).Warning("Failed to get challenge")
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
ch := challenge.GetActualInstance().(ChallengeInt)
|
||||
pi.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got challenge")
|
||||
responseReq := client.FlowsApi.FlowsExecutorSolve(context.Background(), pi.flowSlug).Query(urlParams)
|
||||
switch ch.GetComponent() {
|
||||
case "ak-stage-identification":
|
||||
responseReq = responseReq.FlowChallengeResponseRequest(api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewIdentificationChallengeResponseRequest(bindDN)))
|
||||
case "ak-stage-password":
|
||||
responseReq = responseReq.FlowChallengeResponseRequest(api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewPasswordChallengeResponseRequest(password)))
|
||||
case "ak-stage-authenticator-validate":
|
||||
// We only support duo as authenticator, check if that's allowed
|
||||
var deviceChallenge *api.DeviceChallenge
|
||||
for _, devCh := range challenge.AuthenticatorValidationChallenge.DeviceChallenges {
|
||||
if devCh.DeviceClass == string(api.DEVICECLASSESENUM_DUO) {
|
||||
deviceChallenge = &devCh
|
||||
}
|
||||
}
|
||||
if deviceChallenge == nil {
|
||||
pi.log.Warning("got ak-stage-authenticator-validate without duo")
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
devId, err := strconv.Atoi(deviceChallenge.DeviceUid)
|
||||
if err != nil {
|
||||
pi.log.Warning("failed to convert duo device id to int")
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
devId32 := int32(devId)
|
||||
inner := api.NewAuthenticatorValidationChallengeResponseRequest()
|
||||
inner.Duo = &devId32
|
||||
responseReq = responseReq.FlowChallengeResponseRequest(api.AuthenticatorValidationChallengeResponseRequestAsFlowChallengeResponseRequest(inner))
|
||||
case "ak-stage-access-denied":
|
||||
pi.log.Info("got ak-stage-access-denied")
|
||||
return false, ldap.LDAPResultInsufficientAccessRights
|
||||
default:
|
||||
pi.log.WithField("component", ch.GetComponent()).Warning("unsupported challenge type")
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
response, _, err := responseReq.Execute()
|
||||
ch = response.GetActualInstance().(ChallengeInt)
|
||||
pi.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got response")
|
||||
switch ch.GetComponent() {
|
||||
case "ak-stage-access-denied":
|
||||
pi.log.Info("got ak-stage-access-denied")
|
||||
return false, ldap.LDAPResultInsufficientAccessRights
|
||||
}
|
||||
if ch.GetType() == "redirect" {
|
||||
return true, ldap.LDAPResultSuccess
|
||||
}
|
||||
if err != nil {
|
||||
pi.log.WithError(err).Warning("Failed to submit challenge")
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
if len(ch.GetResponseErrors()) > 0 {
|
||||
for key, errs := range ch.GetResponseErrors() {
|
||||
for _, err := range errs {
|
||||
pi.log.WithField("key", key).WithField("code", err.Code).WithField("msg", err.String).Warning("Flow error")
|
||||
return false, ldap.LDAPResultInsufficientAccessRights
|
||||
}
|
||||
}
|
||||
}
|
||||
if depth >= 10 {
|
||||
pi.log.Warning("exceeded stage recursion depth")
|
||||
return false, ldap.LDAPResultOperationsError
|
||||
}
|
||||
return pi.solveFlowChallenge(bindDN, password, client, urlParams, depth+1)
|
||||
}
|
||||
141
internal/outpost/ldap/instance_search.go
Normal file
141
internal/outpost/ldap/instance_search.go
Normal file
@ -0,0 +1,141 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/outpost/api"
|
||||
)
|
||||
|
||||
func (pi *ProviderInstance) SearchMe(user api.User, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
|
||||
entries := make([]*ldap.Entry, 1)
|
||||
entries[0] = pi.UserEntry(user)
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
baseDN := strings.ToLower("," + pi.BaseDN)
|
||||
|
||||
entries := []*ldap.Entry{}
|
||||
filterEntity, err := ldap.GetFilterObjectClass(searchReq.Filter)
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", searchReq.Filter)
|
||||
}
|
||||
if len(bindDN) < 1 {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", bindDN)
|
||||
}
|
||||
if !strings.HasSuffix(bindDN, 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 {
|
||||
pi.log.Debug("User info not cached")
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
|
||||
}
|
||||
if !flags.CanSearch {
|
||||
pi.log.Debug("User can't search, showing info about user")
|
||||
return pi.SearchMe(flags.UserInfo, searchReq, conn)
|
||||
}
|
||||
|
||||
switch filterEntity {
|
||||
default:
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, searchReq.Filter)
|
||||
case GroupObjectClass:
|
||||
groups, _, err := pi.s.ac.Client.CoreApi.CoreGroupsList(context.Background()).Execute()
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
|
||||
}
|
||||
pi.log.WithField("count", len(groups.Results)).Trace("Got results from API")
|
||||
for _, g := range groups.Results {
|
||||
entries = append(entries, pi.GroupEntry(g))
|
||||
}
|
||||
case UserObjectClass, "":
|
||||
users, _, err := pi.s.ac.Client.CoreApi.CoreUsersList(context.Background()).Execute()
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
|
||||
}
|
||||
for _, u := range users.Results {
|
||||
entries = append(entries, pi.UserEntry(u))
|
||||
}
|
||||
}
|
||||
pi.log.WithField("filter", searchReq.Filter).Debug("Search OK")
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
|
||||
attrs := []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "cn",
|
||||
Values: []string{u.Username},
|
||||
},
|
||||
{
|
||||
Name: "uid",
|
||||
Values: []string{u.Uid},
|
||||
},
|
||||
{
|
||||
Name: "name",
|
||||
Values: []string{u.Name},
|
||||
},
|
||||
{
|
||||
Name: "displayName",
|
||||
Values: []string{u.Name},
|
||||
},
|
||||
{
|
||||
Name: "mail",
|
||||
Values: []string{*u.Email},
|
||||
},
|
||||
{
|
||||
Name: "objectClass",
|
||||
Values: []string{UserObjectClass, "organizationalPerson", "goauthentik.io/ldap/user"},
|
||||
},
|
||||
}
|
||||
|
||||
if *u.IsActive {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"inactive"}})
|
||||
} else {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"active"}})
|
||||
}
|
||||
|
||||
if u.IsSuperuser {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"inactive"}})
|
||||
} else {
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "superuser", Values: []string{"active"}})
|
||||
}
|
||||
|
||||
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: pi.GroupsForUser(u)})
|
||||
|
||||
attrs = append(attrs, AKAttrsToLDAP(u.Attributes)...)
|
||||
|
||||
dn := fmt.Sprintf("cn=%s,%s", u.Username, pi.UserDN)
|
||||
|
||||
return &ldap.Entry{DN: dn, Attributes: attrs}
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GroupEntry(g api.Group) *ldap.Entry {
|
||||
attrs := []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "cn",
|
||||
Values: []string{g.Name},
|
||||
},
|
||||
{
|
||||
Name: "uid",
|
||||
Values: []string{string(g.Pk)},
|
||||
},
|
||||
{
|
||||
Name: "objectClass",
|
||||
Values: []string{GroupObjectClass, "goauthentik.io/ldap/group"},
|
||||
},
|
||||
}
|
||||
attrs = append(attrs, AKAttrsToLDAP(g.Attributes)...)
|
||||
|
||||
dn := pi.GetGroupDN(g)
|
||||
return &ldap.Entry{DN: dn, Attributes: attrs}
|
||||
}
|
||||
58
internal/outpost/ldap/ldap.go
Normal file
58
internal/outpost/ldap/ldap.go
Normal file
@ -0,0 +1,58 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/outpost/api"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
)
|
||||
|
||||
const GroupObjectClass = "group"
|
||||
const UserObjectClass = "user"
|
||||
|
||||
type ProviderInstance struct {
|
||||
BaseDN string
|
||||
|
||||
UserDN string
|
||||
GroupDN string
|
||||
|
||||
appSlug string
|
||||
flowSlug string
|
||||
s *LDAPServer
|
||||
log *log.Entry
|
||||
|
||||
searchAllowedGroups []*strfmt.UUID
|
||||
boundUsersMutex sync.RWMutex
|
||||
boundUsers map[string]UserFlags
|
||||
}
|
||||
|
||||
type UserFlags struct {
|
||||
UserInfo api.User
|
||||
CanSearch bool
|
||||
}
|
||||
|
||||
type LDAPServer struct {
|
||||
s *ldap.Server
|
||||
log *log.Entry
|
||||
ac *ak.APIController
|
||||
|
||||
providers []*ProviderInstance
|
||||
}
|
||||
|
||||
func NewServer(ac *ak.APIController) *LDAPServer {
|
||||
s := ldap.NewServer()
|
||||
s.EnforceLDAP = true
|
||||
ls := &LDAPServer{
|
||||
s: s,
|
||||
log: log.WithField("logger", "authentik.outpost.ldap"),
|
||||
ac: ac,
|
||||
providers: []*ProviderInstance{},
|
||||
}
|
||||
s.BindFunc("", ls)
|
||||
s.SearchFunc("", ls)
|
||||
return ls
|
||||
}
|
||||
28
internal/outpost/ldap/search.go
Normal file
28
internal/outpost/ldap/search.go
Normal file
@ -0,0 +1,28 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/nmcclain/ldap"
|
||||
)
|
||||
|
||||
func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
|
||||
ls.log.WithField("bindDN", bindDN).WithField("baseDN", searchReq.BaseDN).Info("search")
|
||||
if searchReq.BaseDN == "" {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
bd, err := goldap.ParseDN(searchReq.BaseDN)
|
||||
if err != nil {
|
||||
ls.log.WithField("baseDN", searchReq.BaseDN).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(provider.BaseDN)
|
||||
if providerBase.AncestorOf(bd) {
|
||||
return provider.Search(bindDN, searchReq, conn)
|
||||
}
|
||||
}
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("no provider could handle request")
|
||||
}
|
||||
36
internal/outpost/ldap/utils.go
Normal file
36
internal/outpost/ldap/utils.go
Normal file
@ -0,0 +1,36 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/outpost/api"
|
||||
)
|
||||
|
||||
func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute {
|
||||
attrList := []*ldap.EntryAttribute{}
|
||||
a := attrs.(*map[string]interface{})
|
||||
for attrKey, attrValue := range *a {
|
||||
entry := &ldap.EntryAttribute{Name: attrKey}
|
||||
switch t := attrValue.(type) {
|
||||
case []string:
|
||||
entry.Values = t
|
||||
case string:
|
||||
entry.Values = []string{t}
|
||||
}
|
||||
attrList = append(attrList, entry)
|
||||
}
|
||||
return attrList
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GroupsForUser(user api.User) []string {
|
||||
groups := make([]string, len(user.Groups))
|
||||
for i, group := range user.Groups {
|
||||
groups[i] = pi.GetGroupDN(group)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetGroupDN(group api.Group) string {
|
||||
return fmt.Sprintf("cn=%s,%s", group.Name, pi.GroupDN)
|
||||
}
|
||||
Reference in New Issue
Block a user