outposts: initial ldap outpost implementation
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
15d5b91642
commit
4f5e1fb86b
|
@ -77,6 +77,7 @@ class OutpostType(models.TextChoices):
|
|||
"""Outpost types, currently only the reverse proxy is available"""
|
||||
|
||||
PROXY = "proxy"
|
||||
LDAP = "ldap"
|
||||
|
||||
|
||||
def default_outpost_config(host: Optional[str] = None):
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"goauthentik.io/outpost/pkg/ak"
|
||||
"goauthentik.io/outpost/pkg/ldap"
|
||||
)
|
||||
|
||||
const helpMessage = `authentik ldap
|
||||
|
||||
Required environment variables:
|
||||
- AUTHENTIK_HOST: URL to connect to (format "http://authentik.company")
|
||||
- AUTHENTIK_TOKEN: Token to authenticate with
|
||||
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
|
||||
|
||||
func main() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
pbURL, found := os.LookupEnv("AUTHENTIK_HOST")
|
||||
if !found {
|
||||
fmt.Println("env AUTHENTIK_HOST not set!")
|
||||
fmt.Println(helpMessage)
|
||||
os.Exit(1)
|
||||
}
|
||||
pbToken, found := os.LookupEnv("AUTHENTIK_TOKEN")
|
||||
if !found {
|
||||
fmt.Println("env AUTHENTIK_TOKEN not set!")
|
||||
fmt.Println(helpMessage)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pbURLActual, err := url.Parse(pbURL)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println(helpMessage)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
ac := ak.NewAPIController(*pbURLActual, pbToken)
|
||||
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
|
||||
ac.Server = ldap.NewServer(ac)
|
||||
|
||||
ac.Start()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-interrupt:
|
||||
ac.Shutdown()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,8 @@ require (
|
|||
github.com/kr/pretty v0.2.1 // indirect
|
||||
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
|
||||
|
|
|
@ -483,6 +483,10 @@ github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
|
|||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
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=
|
||||
|
|
Binary file not shown.
|
@ -31,7 +31,7 @@ func doGlobalSetup(config map[string]interface{}) {
|
|||
default:
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
log.WithField("version", pkg.VERSION).Info("Starting authentik proxy")
|
||||
log.WithField("version", pkg.VERSION).Info("Starting authentik outpost")
|
||||
|
||||
var dsn string
|
||||
if config[ConfigErrorReportingEnabled].(bool) {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (ls *LDAPServer) Refresh() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) Start() error {
|
||||
listen := "0.0.0.0:3389"
|
||||
log.Debugf("Listening on %s", listen)
|
||||
err := ls.s.ListenAndServe(listen)
|
||||
if err != nil {
|
||||
ls.log.Errorf("LDAP Server Failed: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
)
|
||||
|
||||
func (ls *LDAPServer) Bind(bindDN string, bindSimplePw string, conn net.Conn) (ldap.LDAPResultCode, error) {
|
||||
ls.log.WithField("dn", bindDN).WithField("pw", bindSimplePw).Debug("bind")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/outpost/pkg/ak"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
)
|
||||
|
||||
const GroupObjectClass = "group"
|
||||
const UserObjectClass = "user"
|
||||
|
||||
type LDAPServer struct {
|
||||
BaseDN string
|
||||
|
||||
userDN string
|
||||
groupDN string
|
||||
|
||||
s *ldap.Server
|
||||
log *log.Entry
|
||||
ac *ak.APIController
|
||||
}
|
||||
|
||||
func NewServer(ac *ak.APIController) *LDAPServer {
|
||||
s := ldap.NewServer()
|
||||
s.EnforceLDAP = true
|
||||
ls := &LDAPServer{
|
||||
s: s,
|
||||
log: log.WithField("logger", "ldap-server"),
|
||||
ac: ac,
|
||||
|
||||
BaseDN: "DC=ldap,DC=goauthentik,DC=io",
|
||||
}
|
||||
ls.userDN = strings.ToLower(fmt.Sprintf("cn=users,%s", ls.BaseDN))
|
||||
ls.groupDN = strings.ToLower(fmt.Sprintf("cn=groups,%s", ls.BaseDN))
|
||||
s.BindFunc("", ls)
|
||||
s.SearchFunc("", ls)
|
||||
return ls
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/outpost/pkg/client/core"
|
||||
)
|
||||
|
||||
func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
baseDN := strings.ToLower("," + ls.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, ls.BaseDN)
|
||||
}
|
||||
|
||||
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 := ls.ac.Client.Core.CoreGroupsList(core.NewCoreGroupsListParams(), ls.ac.Auth)
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
|
||||
}
|
||||
for _, g := range groups.Payload.Results {
|
||||
attrs := []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "cn",
|
||||
Values: []string{*g.Name},
|
||||
},
|
||||
{
|
||||
Name: "uid",
|
||||
Values: []string{strconv.Itoa(int(g.Pk))},
|
||||
},
|
||||
{
|
||||
Name: "objectClass",
|
||||
Values: []string{GroupObjectClass, "goauthentik.io/ldap/group"},
|
||||
},
|
||||
}
|
||||
attrs = append(attrs, AKAttrsToLDAP(g.Attributes)...)
|
||||
// attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s", g.Name)}})
|
||||
// attrs = append(attrs, &ldap.EntryAttribute{Name: "gidNumber", Values: []string{fmt.Sprintf("%d", g.UnixID)}})
|
||||
// attrs = append(attrs, &ldap.EntryAttribute{Name: "uniqueMember", Values: h.getGroupMembers(g.UnixID)})
|
||||
// attrs = append(attrs, &ldap.EntryAttribute{Name: "memberUid", Values: h.getGroupMemberIDs(g.UnixID)})
|
||||
dn := fmt.Sprintf("cn=%s,%s", *g.Name, ls.groupDN)
|
||||
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
|
||||
}
|
||||
case UserObjectClass, "":
|
||||
users, err := ls.ac.Client.Core.CoreUsersList(core.NewCoreUsersListParams(), ls.ac.Auth)
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
|
||||
}
|
||||
for _, u := range users.Payload.Results {
|
||||
attrs := []*ldap.EntryAttribute{
|
||||
{
|
||||
Name: "cn",
|
||||
Values: []string{*u.Username},
|
||||
},
|
||||
{
|
||||
Name: "uid",
|
||||
Values: []string{strconv.Itoa(int(u.Pk))},
|
||||
},
|
||||
{
|
||||
Name: "name",
|
||||
Values: []string{*u.Name},
|
||||
},
|
||||
{
|
||||
Name: "displayName",
|
||||
Values: []string{*u.Name},
|
||||
},
|
||||
{
|
||||
Name: "mail",
|
||||
Values: []string{u.Email.String()},
|
||||
},
|
||||
{
|
||||
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: h.getGroupDNs(append(u.OtherGroups, u.PrimaryGroup))})
|
||||
|
||||
attrs = append(attrs, AKAttrsToLDAP(u.Attributes)...)
|
||||
|
||||
dn := fmt.Sprintf("cn=%s,%s", *u.Name, ls.userDN)
|
||||
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
|
||||
}
|
||||
}
|
||||
ls.log.Debug(fmt.Sprintf("AP: Search OK: %s", searchReq.Filter))
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"github.com/nmcclain/ldap"
|
||||
)
|
||||
|
||||
func AKAttrsToLDAP(attrs interface{}) []*ldap.EntryAttribute {
|
||||
attrList := []*ldap.EntryAttribute{}
|
||||
for attrKey, attrValue := range attrs.(map[string]interface{}) {
|
||||
entry := &ldap.EntryAttribute{Name: attrKey}
|
||||
switch attrValue.(type) {
|
||||
case []string:
|
||||
entry.Values = attrValue.([]string)
|
||||
case string:
|
||||
entry.Values = []string{attrValue.(string)}
|
||||
}
|
||||
attrList = append(attrList, entry)
|
||||
}
|
||||
return attrList
|
||||
}
|
Reference in New Issue