package multildap import ( "errors" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/ldap" ) // logger to log var logger = log.New("ldap") // GetConfig gets LDAP config var GetConfig = ldap.GetConfig // IsEnabled checks if LDAP is enabled var IsEnabled = ldap.IsEnabled // newLDAP return instance of the single LDAP server var newLDAP = ldap.New // ErrInvalidCredentials is returned if username and password do not match var ErrInvalidCredentials = ldap.ErrInvalidCredentials // ErrCouldNotFindUser is returned when username hasn't been found (not username+password) var ErrCouldNotFindUser = ldap.ErrCouldNotFindUser // ErrNoLDAPServers is returned when there is no LDAP servers specified var ErrNoLDAPServers = errors.New("no LDAP servers are configured") // ErrDidNotFindUser if request for user is unsuccessful var ErrDidNotFindUser = errors.New("did not find a user") // ServerStatus holds the LDAP server status type ServerStatus struct { Host string Port int Available bool Error error } // IMultiLDAP is interface for MultiLDAP type IMultiLDAP interface { Ping() ([]*ServerStatus, error) Login(query *models.LoginUserQuery) ( *models.ExternalUserInfo, error, ) Users(logins []string) ( []*models.ExternalUserInfo, error, ) User(login string) ( *models.ExternalUserInfo, ldap.ServerConfig, error, ) } // MultiLDAP is basic struct of LDAP authorization type MultiLDAP struct { configs []*ldap.ServerConfig } // New creates the new LDAP auth func New(configs []*ldap.ServerConfig) IMultiLDAP { return &MultiLDAP{ configs: configs, } } // Ping dials each of the LDAP servers and returns their status. If the server is unavailable, it also returns the error. func (multiples *MultiLDAP) Ping() ([]*ServerStatus, error) { if len(multiples.configs) == 0 { return nil, ErrNoLDAPServers } serverStatuses := []*ServerStatus{} for _, config := range multiples.configs { status := &ServerStatus{} status.Host = config.Host status.Port = config.Port server := newLDAP(config) err := server.Dial() if err == nil { status.Available = true serverStatuses = append(serverStatuses, status) server.Close() } else { status.Available = false status.Error = err serverStatuses = append(serverStatuses, status) } } return serverStatuses, nil } // Login tries to log in the user in multiples LDAP func (multiples *MultiLDAP) Login(query *models.LoginUserQuery) ( *models.ExternalUserInfo, error, ) { if len(multiples.configs) == 0 { return nil, ErrNoLDAPServers } for index, config := range multiples.configs { server := newLDAP(config) if err := server.Dial(); err != nil { logDialFailure(err, config) // Only return an error if it is the last server so we can try next server if index == len(multiples.configs)-1 { return nil, err } continue } defer server.Close() user, err := server.Login(query) // FIXME if user != nil { return user, nil } if err != nil { if isSilentError(err) { logger.Debug( "unable to login with LDAP - skipping server", "host", config.Host, "port", config.Port, "error", err, ) continue } return nil, err } } // Return invalid credentials if we couldn't find the user anywhere return nil, ErrInvalidCredentials } // User attempts to find an user by login/username by searching into all of the configured LDAP servers. Then, if the user is found it returns the user alongisde the server it was found. func (multiples *MultiLDAP) User(login string) ( *models.ExternalUserInfo, ldap.ServerConfig, error, ) { if len(multiples.configs) == 0 { return nil, ldap.ServerConfig{}, ErrNoLDAPServers } search := []string{login} for index, config := range multiples.configs { server := newLDAP(config) if err := server.Dial(); err != nil { logDialFailure(err, config) // Only return an error if it is the last server so we can try next server if index == len(multiples.configs)-1 { return nil, *config, err } continue } defer server.Close() if err := server.Bind(); err != nil { return nil, *config, err } users, err := server.Users(search) if err != nil { return nil, *config, err } if len(users) != 0 { return users[0], *config, nil } } return nil, ldap.ServerConfig{}, ErrDidNotFindUser } // Users gets users from multiple LDAP servers func (multiples *MultiLDAP) Users(logins []string) ( []*models.ExternalUserInfo, error, ) { var result []*models.ExternalUserInfo if len(multiples.configs) == 0 { return nil, ErrNoLDAPServers } for index, config := range multiples.configs { server := newLDAP(config) if err := server.Dial(); err != nil { logDialFailure(err, config) // Only return an error if it is the last server so we can try next server if index == len(multiples.configs)-1 { return nil, err } continue } defer server.Close() if err := server.Bind(); err != nil { return nil, err } users, err := server.Users(logins) if err != nil { return nil, err } result = append(result, users...) } return result, nil } // isSilentError evaluates an error and tells whenever we should fail the LDAP request // immediately or if we should continue into other LDAP servers func isSilentError(err error) bool { continueErrs := []error{ErrInvalidCredentials, ErrCouldNotFindUser} for _, cerr := range continueErrs { if errors.Is(err, cerr) { return true } } return false } func logDialFailure(err error, config *ldap.ServerConfig) { logger.Debug( "unable to dial LDAP server", "host", config.Host, "port", config.Port, "error", err, ) }