outposts: initial ldap outpost implementation
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -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): | ||||
|  | ||||
							
								
								
									
										64
									
								
								outpost/cmd/ldap/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								outpost/cmd/ldap/server.go
									
									
									
									
									
										Normal file
									
								
							| @ -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= | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								outpost/outpost
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								outpost/outpost
									
									
									
									
									
										Executable file
									
								
							
										
											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) { | ||||
|  | ||||
							
								
								
									
										20
									
								
								outpost/pkg/ldap/api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								outpost/pkg/ldap/api.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| } | ||||
							
								
								
									
										12
									
								
								outpost/pkg/ldap/bind.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								outpost/pkg/ldap/bind.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| } | ||||
							
								
								
									
										42
									
								
								outpost/pkg/ldap/ldap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								outpost/pkg/ldap/ldap.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| } | ||||
							
								
								
									
										115
									
								
								outpost/pkg/ldap/search.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								outpost/pkg/ldap/search.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| } | ||||
							
								
								
									
										20
									
								
								outpost/pkg/ldap/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								outpost/pkg/ldap/utils.go
									
									
									
									
									
										Normal file
									
								
							| @ -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
	
	Block a user
	 Jens Langhammer
					Jens Langhammer