2017-08-10 08:35:23 +00:00
|
|
|
package ldap
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/portainer/portainer"
|
|
|
|
"github.com/portainer/portainer/crypto"
|
|
|
|
|
|
|
|
"gopkg.in/ldap.v2"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// ErrUserNotFound defines an error raised when the user is not found via LDAP search
|
|
|
|
// or that too many entries (> 1) are returned.
|
|
|
|
ErrUserNotFound = portainer.Error("User not found or too many entries returned")
|
|
|
|
)
|
|
|
|
|
|
|
|
// Service represents a service used to authenticate users against a LDAP/AD.
|
|
|
|
type Service struct{}
|
|
|
|
|
|
|
|
func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearchSettings) (string, error) {
|
|
|
|
var userDN string
|
|
|
|
found := false
|
2018-09-24 23:10:41 +00:00
|
|
|
usernameEscaped := ldap.EscapeFilter(username)
|
|
|
|
|
2017-08-10 08:35:23 +00:00
|
|
|
for _, searchSettings := range settings {
|
|
|
|
searchRequest := ldap.NewSearchRequest(
|
|
|
|
searchSettings.BaseDN,
|
|
|
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
2018-09-24 23:10:41 +00:00
|
|
|
fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, usernameEscaped),
|
2017-08-10 08:35:23 +00:00
|
|
|
[]string{"dn"},
|
|
|
|
nil,
|
|
|
|
)
|
|
|
|
|
|
|
|
// Deliberately skip errors on the search request so that we can jump to other search settings
|
|
|
|
// if any issue arise with the current one.
|
2017-09-20 18:58:09 +00:00
|
|
|
sr, err := conn.Search(searchRequest)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
2017-08-10 08:35:23 +00:00
|
|
|
|
|
|
|
if len(sr.Entries) == 1 {
|
|
|
|
found = true
|
|
|
|
userDN = sr.Entries[0].DN
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !found {
|
|
|
|
return "", ErrUserNotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
return userDN, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
|
|
|
|
|
|
|
|
if settings.TLSConfig.TLS || settings.StartTLS {
|
2018-05-19 14:25:11 +00:00
|
|
|
config, err := crypto.CreateTLSConfigurationFromDisk(settings.TLSConfig.TLSCACertPath, settings.TLSConfig.TLSCertPath, settings.TLSConfig.TLSKeyPath, settings.TLSConfig.TLSSkipVerify)
|
2017-08-10 08:35:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
config.ServerName = strings.Split(settings.URL, ":")[0]
|
|
|
|
|
|
|
|
if settings.TLSConfig.TLS {
|
|
|
|
return ldap.DialTLS("tcp", settings.URL, config)
|
|
|
|
}
|
|
|
|
|
|
|
|
conn, err := ldap.Dial("tcp", settings.URL)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = conn.StartTLS(config)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return conn, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return ldap.Dial("tcp", settings.URL)
|
|
|
|
}
|
|
|
|
|
|
|
|
// AuthenticateUser is used to authenticate a user against a LDAP/AD.
|
|
|
|
func (*Service) AuthenticateUser(username, password string, settings *portainer.LDAPSettings) error {
|
|
|
|
|
|
|
|
connection, err := createConnection(settings)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer connection.Close()
|
|
|
|
|
|
|
|
err = connection.Bind(settings.ReaderDN, settings.Password)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
userDN, err := searchUser(username, connection, settings.SearchSettings)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = connection.Bind(userDN, password)
|
|
|
|
if err != nil {
|
2018-07-23 04:57:38 +00:00
|
|
|
return portainer.ErrUnauthorized
|
2017-08-10 08:35:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-07-23 04:57:38 +00:00
|
|
|
// GetUserGroups is used to retrieve user groups from LDAP/AD.
|
|
|
|
func (*Service) GetUserGroups(username string, settings *portainer.LDAPSettings) ([]string, error) {
|
|
|
|
connection, err := createConnection(settings)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer connection.Close()
|
|
|
|
|
|
|
|
err = connection.Bind(settings.ReaderDN, settings.Password)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
userDN, err := searchUser(username, connection, settings.SearchSettings)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
userGroups := getGroups(userDN, connection, settings.GroupSearchSettings)
|
|
|
|
|
|
|
|
return userGroups, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get a list of group names for specified user from LDAP/AD
|
|
|
|
func getGroups(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSearchSettings) []string {
|
|
|
|
groups := make([]string, 0)
|
2018-09-24 23:10:41 +00:00
|
|
|
userDNEscaped := ldap.EscapeFilter(userDN)
|
2018-07-23 04:57:38 +00:00
|
|
|
|
|
|
|
for _, searchSettings := range settings {
|
|
|
|
searchRequest := ldap.NewSearchRequest(
|
|
|
|
searchSettings.GroupBaseDN,
|
|
|
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
2018-09-24 23:10:41 +00:00
|
|
|
fmt.Sprintf("(&%s(%s=%s))", searchSettings.GroupFilter, searchSettings.GroupAttribute, userDNEscaped),
|
2018-07-23 04:57:38 +00:00
|
|
|
[]string{"cn"},
|
|
|
|
nil,
|
|
|
|
)
|
|
|
|
|
|
|
|
// Deliberately skip errors on the search request so that we can jump to other search settings
|
|
|
|
// if any issue arise with the current one.
|
|
|
|
sr, err := conn.Search(searchRequest)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, entry := range sr.Entries {
|
|
|
|
for _, attr := range entry.Attributes {
|
|
|
|
groups = append(groups, attr.Values[0])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return groups
|
|
|
|
}
|
|
|
|
|
2017-08-10 08:35:23 +00:00
|
|
|
// TestConnectivity is used to test a connection against the LDAP server using the credentials
|
|
|
|
// specified in the LDAPSettings.
|
|
|
|
func (*Service) TestConnectivity(settings *portainer.LDAPSettings) error {
|
|
|
|
|
|
|
|
connection, err := createConnection(settings)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer connection.Close()
|
|
|
|
|
|
|
|
err = connection.Bind(settings.ReaderDN, settings.Password)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|