812c0b34ea
* feat(ldap): simplify ldap configuration refactor(auth): move ldap settings to a component feat(ldap): add username style autofill feat(ldap): customs for ad feat(app): introduce box selector refactor(auth-settings): use box selector feat(ldap): style changes refactor(ldap): move connectivity check button to a component refactor(settings): move ldap security settings to a component refactor(ldap): move user search to component refactor(ldap): move group search to component style(ldap): remove comment refactor(auth-settings): move auto-user-toggle to component feat(ldap): provide methods to search for users and groups refactor(ldap): move group/user settings into component refactor(ldap): provide labels for components refactor(ldap): separate custom and ad settings fix(ldap): search for users feat(ldap): search users feat(ldap): complete password if missing feat(ldap): search for users feat(ldap): show a list of users feat(ldap): get user uid feat(ldap): search groups without password feat(groups): show group results feat(ldap): add display types feat(ldap): search for groups refactor(ldap): clean code fix(ldap): sort users table fix(ldap): show settings by type feat(ldap): parse values from basedn feat(ldap): parse values feat(app): emit on change event from box-selector feat(ldap): user search filter feat(ldap): search username attribute feat(ldap): remove format around search filter feat(ldap): ad group search refactor(ldap): move dn builder to component feat(ldap): use base dn builder for group search feat(ldap): search for ad groups refactor(ldap): replace domain root object feat(ldap): openldap settings refactor(ldap): delete empty controllers feat(ldap): remove warning on wrong group filter feat(ldap): clear username and pass if not AD feat(ldap): clear basedn when switch from openldap to ad feat(ldap): clear ldap settings when switich from ldap to ad feat(ldap): set dn only if there are values feat(ldap): support more cases of domains feat(ldap): parse openldap domain correctly refactor(ldap): move server type check feat(ldap): move entries feat(ldap): show username format style(ldap): remove comments feat(ldap): clear group filter when no groups refactor(ldap): replace generic payload feat(ldap): allow the user to test login feat(ldap): add test login to custom and open ldap settings feat(ldap): style fixes fix(ldap): style fix fix(ldap): style fixes refactor(ldap): move components to module feat(ldap): add group entries feat(ldap): add borders around each group entry feat(ldap): parse user filter feat(ldap): add/remove group feat(ldap): set ad anonymous mode to false feat(ldap): add group name feat(ldap): fix parentheses feat(ldap): separate between each search config fix(ldap): fix parsing of group dn feat(ldap): style fixes feat(ldap): remove of change of filter refactor(ldap): remove user display style feat(ldap): rename group entries field refactor(auth): move auto user provision refactor(ldap): refactor box selector feat(ldap): move ad settings to be a global setting style(ldap): remove comments feat(ldap): add auto user toggle refactor(auth/ad): rename ad component fix(auth/ad): fix the use of a certificate refactor(ldap): rename components fix(ldap): show user and group search fix(ldap): design group settings feat(ldap): search users and groups feat(ldap): add margins refactor(ldap): separate ldap and ad settings refactor(auth): use central check for auth method feat(ldap): clear margins feat(ldap): add port if missing feat(ldap): fix ad name fix(ldap): rename fields feat(ldap): add domain root field feat(auth/ad): remove domain root field feat(ldap): rename base dn to root domain feat(ldap/openldap): get suffix feat(ldap/open): change base filter fix(ldap): align feat(db): introduce migration for ldap server type refactor(ldap): move service to ldap module refactor(ldap): sync between client and server constants fix(ldap): use post for check style(ldap): fix handler comments fix(ldap): check for errors style(ldap): fix tyop fix(ldap): check equality style(ldap): add comments fix(ldap): allow anonymous mode fix(ldap): show errors on search users feat(lasp): use custom settings for each server fix(ldap): supply default group filter fix(ldap): show domain suffix in new settings fix(ldap): replace icon with text refactor(components): remove box-selector-wrapper * fix(ldap): enable test when form is valid * fix(ldap): add port if missing
298 lines
7.3 KiB
Go
298 lines
7.3 KiB
Go
package ldap
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
ldap "github.com/go-ldap/ldap/v3"
|
|
"github.com/portainer/portainer/api"
|
|
"github.com/portainer/portainer/api/crypto"
|
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
|
)
|
|
|
|
var (
|
|
// errUserNotFound defines an error raised when the user is not found via LDAP search
|
|
// or that too many entries (> 1) are returned.
|
|
errUserNotFound = errors.New("User not found or too many entries returned")
|
|
)
|
|
|
|
// Service represents a service used to authenticate users against a LDAP/AD.
|
|
type Service struct{}
|
|
|
|
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
|
|
for _, url := range settings.URLs {
|
|
conn, err := createConnectionForURL(url, settings)
|
|
if err != nil {
|
|
log.Printf("[DEBUG] [ldap] [message: failed creating LDAP connection] [error: %s]", err)
|
|
} else {
|
|
return conn, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("No valid connection")
|
|
}
|
|
|
|
func createConnectionForURL(url string, settings *portainer.LDAPSettings) (*ldap.Conn, error) {
|
|
if settings.TLSConfig.TLS || settings.StartTLS {
|
|
config, err := crypto.CreateTLSConfigurationFromDisk(settings.TLSConfig.TLSCACertPath, settings.TLSConfig.TLSCertPath, settings.TLSConfig.TLSKeyPath, settings.TLSConfig.TLSSkipVerify)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.ServerName = strings.Split(url, ":")[0]
|
|
|
|
if settings.TLSConfig.TLS {
|
|
return ldap.DialTLS("tcp", url, config)
|
|
}
|
|
|
|
conn, err := ldap.Dial("tcp", url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = conn.StartTLS(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
return ldap.Dial("tcp", 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()
|
|
|
|
if !settings.AnonymousMode {
|
|
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 {
|
|
return httperrors.ErrUnauthorized
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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()
|
|
|
|
if !settings.AnonymousMode {
|
|
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 := getGroupsByUser(userDN, connection, settings.GroupSearchSettings)
|
|
|
|
return userGroups, nil
|
|
}
|
|
|
|
// SearchUsers searches for users with the specified settings
|
|
func (*Service) SearchUsers(settings *portainer.LDAPSettings) ([]string, error) {
|
|
connection, err := createConnection(settings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer connection.Close()
|
|
|
|
if !settings.AnonymousMode {
|
|
err = connection.Bind(settings.ReaderDN, settings.Password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
users := make([]string, 0)
|
|
|
|
for _, searchSettings := range settings.SearchSettings {
|
|
searchRequest := ldap.NewSearchRequest(
|
|
searchSettings.BaseDN,
|
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
|
searchSettings.Filter,
|
|
[]string{"dn", searchSettings.UserNameAttribute},
|
|
nil,
|
|
)
|
|
|
|
sr, err := connection.Search(searchRequest)
|
|
if err != nil {
|
|
return users, err
|
|
}
|
|
|
|
for _, user := range sr.Entries {
|
|
users = append(users, user.GetAttributeValue(searchSettings.UserNameAttribute))
|
|
}
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// SearchGroups searches for groups with the specified settings
|
|
func (*Service) SearchGroups(settings *portainer.LDAPSettings) ([]portainer.LDAPUser, error) {
|
|
|
|
connection, err := createConnection(settings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer connection.Close()
|
|
|
|
if !settings.AnonymousMode {
|
|
err = connection.Bind(settings.ReaderDN, settings.Password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
users := []portainer.LDAPUser{}
|
|
|
|
for _, searchSettings := range settings.GroupSearchSettings {
|
|
searchRequest := ldap.NewSearchRequest(
|
|
searchSettings.GroupBaseDN,
|
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
|
searchSettings.GroupFilter,
|
|
[]string{"cn", searchSettings.GroupAttribute},
|
|
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 := connection.Search(searchRequest)
|
|
if err != nil {
|
|
return users, err
|
|
}
|
|
|
|
for _, entry := range sr.Entries {
|
|
members := entry.GetAttributeValues(searchSettings.GroupAttribute)
|
|
for _, username := range members {
|
|
user := portainer.LDAPUser{
|
|
Name: username,
|
|
Group: entry.GetAttributeValue("cn"),
|
|
}
|
|
users = append(users, user)
|
|
}
|
|
}
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearchSettings) (string, error) {
|
|
var userDN string
|
|
found := false
|
|
usernameEscaped := ldap.EscapeFilter(username)
|
|
|
|
for _, searchSettings := range settings {
|
|
searchRequest := ldap.NewSearchRequest(
|
|
searchSettings.BaseDN,
|
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
|
fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, usernameEscaped),
|
|
[]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.
|
|
sr, err := conn.Search(searchRequest)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if len(sr.Entries) == 1 {
|
|
found = true
|
|
userDN = sr.Entries[0].DN
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return "", errUserNotFound
|
|
}
|
|
|
|
return userDN, nil
|
|
}
|
|
|
|
// Get a list of group names for specified user from LDAP/AD
|
|
func getGroupsByUser(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSearchSettings) []string {
|
|
groups := make([]string, 0)
|
|
userDNEscaped := ldap.EscapeFilter(userDN)
|
|
|
|
for _, searchSettings := range settings {
|
|
searchRequest := ldap.NewSearchRequest(
|
|
searchSettings.GroupBaseDN,
|
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
|
fmt.Sprintf("(&%s(%s=%s))", searchSettings.GroupFilter, searchSettings.GroupAttribute, userDNEscaped),
|
|
[]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
|
|
}
|
|
|
|
// 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()
|
|
|
|
if !settings.AnonymousMode {
|
|
err = connection.Bind(settings.ReaderDN, settings.Password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
} else {
|
|
err = connection.UnauthenticatedBind("")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|