LDAP: Move to single package cluster (#63035)

* move multildap to ldap package

* move LDAP api and tests to ldap package

* register background service

* lint
This commit is contained in:
Jo
2023-02-08 09:32:59 +01:00
committed by GitHub
parent dea108a8b9
commit 6322fce725
16 changed files with 395 additions and 264 deletions
@@ -0,0 +1,41 @@
package multildap
import (
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/user"
)
type MultiLDAPmock struct {
MultiLDAP
ID int64
UserCalled bool
LoginCalled bool
UserInfo *user.User
AuthModule string
ExpectedErr error
}
func (m *MultiLDAPmock) Login(query *login.LoginUserQuery) (
*login.ExternalUserInfo, error,
) {
m.LoginCalled = true
query.User = m.UserInfo
query.AuthModule = m.AuthModule
result := &login.ExternalUserInfo{
UserId: m.ID,
}
return result, m.ExpectedErr
}
func (m *MultiLDAPmock) User(loginstr string) (
*login.ExternalUserInfo,
ldap.ServerConfig,
error,
) {
m.UserCalled = true
result := &login.ExternalUserInfo{
UserId: m.ID,
}
return result, ldap.ServerConfig{}, nil
}
+262
View File
@@ -0,0 +1,262 @@
package multildap
import (
"errors"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/login"
)
// 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 *login.LoginUserQuery) (
*login.ExternalUserInfo, error,
)
Users(logins []string) (
[]*login.ExternalUserInfo, error,
)
User(login string) (
*login.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 *login.LoginUserQuery) (
*login.ExternalUserInfo, error,
) {
if len(multiples.configs) == 0 {
return nil, ErrNoLDAPServers
}
ldapSilentErrors := []error{}
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)
if err != nil {
if isSilentError(err) {
ldapSilentErrors = append(ldapSilentErrors, err)
logger.Debug(
"unable to login with LDAP - skipping server",
"host", config.Host,
"port", config.Port,
"error", err,
)
continue
}
return nil, err
}
if user != nil {
return user, nil
}
}
// Return ErrInvalidCredentials in case any of the errors was ErrInvalidCredentials (means that the authentication has failed at least once)
for _, ldapErr := range ldapSilentErrors {
if errors.Is(ldapErr, ErrInvalidCredentials) {
return nil, ErrInvalidCredentials
}
}
// Return ErrCouldNotFindUser if all of the configured LDAP servers returned with ErrCouldNotFindUser
return nil, ErrCouldNotFindUser
}
// 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) (
*login.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) (
[]*login.ExternalUserInfo,
error,
) {
var result []*login.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,
)
}
@@ -0,0 +1,523 @@
package multildap
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/login"
//TODO(sh0rez): remove once import cycle resolved
_ "github.com/grafana/grafana/pkg/api/response"
)
func TestMultiLDAP(t *testing.T) {
t.Run("Ping()", func(t *testing.T) {
t.Run("Should return error for absent config list", func(t *testing.T) {
setup()
multi := New([]*ldap.ServerConfig{})
_, err := multi.Ping()
require.Error(t, err)
require.Equal(t, ErrNoLDAPServers, err)
teardown()
})
t.Run("Should return an unavailable status on dial error", func(t *testing.T) {
mock := setup()
expectedErr := errors.New("Dial error")
mock.dialErrReturn = expectedErr
multi := New([]*ldap.ServerConfig{
{Host: "10.0.0.1", Port: 361},
})
statuses, err := multi.Ping()
require.Nil(t, err)
require.Equal(t, "10.0.0.1", statuses[0].Host)
require.Equal(t, 361, statuses[0].Port)
require.False(t, statuses[0].Available)
require.Equal(t, expectedErr, statuses[0].Error)
require.Equal(t, 0, mock.closeCalledTimes)
teardown()
})
t.Run("Should get the LDAP server statuses", func(t *testing.T) {
mock := setup()
multi := New([]*ldap.ServerConfig{
{Host: "10.0.0.1", Port: 361},
})
statuses, err := multi.Ping()
require.Nil(t, err)
require.Equal(t, "10.0.0.1", statuses[0].Host)
require.Equal(t, 361, statuses[0].Port)
require.True(t, statuses[0].Available)
require.Nil(t, statuses[0].Error)
require.Equal(t, 1, mock.closeCalledTimes)
teardown()
})
})
t.Run("Login()", func(t *testing.T) {
t.Run("Should return error for absent config list", func(t *testing.T) {
setup()
multi := New([]*ldap.ServerConfig{})
_, err := multi.Login(&login.LoginUserQuery{})
require.Error(t, err)
require.Equal(t, ErrNoLDAPServers, err)
teardown()
})
t.Run("Should return a dial error", func(t *testing.T) {
mock := setup()
expected := errors.New("Dial error")
mock.dialErrReturn = expected
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&login.LoginUserQuery{})
require.Error(t, err)
require.Equal(t, expected, err)
teardown()
})
t.Run("Should call underlying LDAP methods", func(t *testing.T) {
mock := setup()
mock.loginErrReturn = ErrInvalidCredentials
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&login.LoginUserQuery{})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, 2, mock.loginCalledTimes)
require.Equal(t, 2, mock.closeCalledTimes)
require.Equal(t, ldap.ErrInvalidCredentials, err)
teardown()
})
t.Run("Should get login result", func(t *testing.T) {
mock := setup()
mock.loginReturn = &login.ExternalUserInfo{
Login: "killa",
}
multi := New([]*ldap.ServerConfig{
{}, {},
})
result, err := multi.Login(&login.LoginUserQuery{})
require.Equal(t, 1, mock.dialCalledTimes)
require.Equal(t, 1, mock.loginCalledTimes)
require.Equal(t, 1, mock.closeCalledTimes)
require.Equal(t, "killa", result.Login)
require.Nil(t, err)
teardown()
})
t.Run("Should still call a second error for invalid not found error", func(t *testing.T) {
mock := setup()
mock.loginErrReturn = ErrCouldNotFindUser
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&login.LoginUserQuery{})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, 2, mock.loginCalledTimes)
require.Equal(t, 2, mock.closeCalledTimes)
require.Equal(t, ErrCouldNotFindUser, err)
teardown()
})
t.Run("Should still try to auth with the second server after receiving an invalid credentials error from the first", func(t *testing.T) {
mock := setup()
mock.loginErrReturn = ErrInvalidCredentials
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&login.LoginUserQuery{})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, 2, mock.loginCalledTimes)
require.Equal(t, 2, mock.closeCalledTimes)
require.Equal(t, ErrInvalidCredentials, err)
teardown()
})
t.Run("Should still try to auth with the second server after receiving a dial error from the first", func(t *testing.T) {
mock := setup()
expectedError := errors.New("Dial error")
mock.dialErrReturn = expectedError
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&login.LoginUserQuery{})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, expectedError, err)
teardown()
})
t.Run("Should return unknown error", func(t *testing.T) {
mock := setup()
expected := errors.New("Something unknown")
mock.loginErrReturn = expected
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Login(&login.LoginUserQuery{})
require.Equal(t, 1, mock.dialCalledTimes)
require.Equal(t, 1, mock.loginCalledTimes)
require.Equal(t, 1, mock.closeCalledTimes)
require.Equal(t, expected, err)
teardown()
})
})
t.Run("User()", func(t *testing.T) {
t.Run("Should return error for absent config list", func(t *testing.T) {
setup()
multi := New([]*ldap.ServerConfig{})
_, _, err := multi.User("test")
require.Error(t, err)
require.Equal(t, ErrNoLDAPServers, err)
teardown()
})
t.Run("Should return a dial error", func(t *testing.T) {
mock := setup()
expected := errors.New("Dial error")
mock.dialErrReturn = expected
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, _, err := multi.User("test")
require.Error(t, err)
require.Equal(t, expected, err)
teardown()
})
t.Run("Should call underlying LDAP methods", func(t *testing.T) {
mock := setup()
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, _, err := multi.User("test")
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, 2, mock.usersCalledTimes)
require.Equal(t, 2, mock.closeCalledTimes)
require.Equal(t, ErrDidNotFindUser, err)
teardown()
})
t.Run("Should return some error", func(t *testing.T) {
mock := setup()
expected := errors.New("Killa Gorilla")
mock.usersErrReturn = expected
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, _, err := multi.User("test")
require.Equal(t, 1, mock.dialCalledTimes)
require.Equal(t, 1, mock.usersCalledTimes)
require.Equal(t, 1, mock.closeCalledTimes)
require.Equal(t, expected, err)
teardown()
})
t.Run("Should get only one user", func(t *testing.T) {
mock := setup()
mock.usersFirstReturn = []*login.ExternalUserInfo{
{
Login: "one",
},
{
Login: "two",
},
}
multi := New([]*ldap.ServerConfig{
{}, {},
})
user, _, err := multi.User("test")
require.Equal(t, 1, mock.dialCalledTimes)
require.Equal(t, 1, mock.usersCalledTimes)
require.Equal(t, 1, mock.closeCalledTimes)
require.Nil(t, err)
require.Equal(t, "one", user.Login)
teardown()
})
t.Run("Should still try to auth with the second server after receiving a dial error from the first", func(t *testing.T) {
mock := setup()
expectedError := errors.New("Dial error")
mock.dialErrReturn = expectedError
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, _, err := multi.User("test")
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, expectedError, err)
teardown()
})
})
t.Run("Users()", func(t *testing.T) {
t.Run("Should still try to auth with the second server after receiving a dial error from the first", func(t *testing.T) {
mock := setup()
expectedError := errors.New("Dial error")
mock.dialErrReturn = expectedError
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Users([]string{"test"})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, expectedError, err)
teardown()
})
t.Run("Should return error for absent config list", func(t *testing.T) {
setup()
multi := New([]*ldap.ServerConfig{})
_, err := multi.Users([]string{"test"})
require.Error(t, err)
require.Equal(t, ErrNoLDAPServers, err)
teardown()
})
t.Run("Should return a dial error", func(t *testing.T) {
mock := setup()
expected := errors.New("Dial error")
mock.dialErrReturn = expected
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Users([]string{"test"})
require.Error(t, err)
require.Equal(t, expected, err)
teardown()
})
t.Run("Should call underlying LDAP methods", func(t *testing.T) {
mock := setup()
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Users([]string{"test"})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, 2, mock.usersCalledTimes)
require.Equal(t, 2, mock.closeCalledTimes)
require.Nil(t, err)
teardown()
})
t.Run("Should return some error", func(t *testing.T) {
mock := setup()
expected := errors.New("Killa Gorilla")
mock.usersErrReturn = expected
multi := New([]*ldap.ServerConfig{
{}, {},
})
_, err := multi.Users([]string{"test"})
require.Equal(t, 1, mock.dialCalledTimes)
require.Equal(t, 1, mock.usersCalledTimes)
require.Equal(t, 1, mock.closeCalledTimes)
require.Equal(t, expected, err)
teardown()
})
t.Run("Should get users", func(t *testing.T) {
mock := setup()
mock.usersFirstReturn = []*login.ExternalUserInfo{
{
Login: "one",
},
{
Login: "two",
},
}
mock.usersRestReturn = []*login.ExternalUserInfo{
{
Login: "three",
},
}
multi := New([]*ldap.ServerConfig{
{}, {},
})
users, err := multi.Users([]string{"test"})
require.Equal(t, 2, mock.dialCalledTimes)
require.Equal(t, 2, mock.usersCalledTimes)
require.Equal(t, 2, mock.closeCalledTimes)
require.Nil(t, err)
require.Equal(t, "one", users[0].Login)
require.Equal(t, "two", users[1].Login)
require.Equal(t, "three", users[2].Login)
teardown()
})
})
}
// mockLDAP represents testing struct for ldap testing
type mockLDAP struct {
dialCalledTimes int
loginCalledTimes int
closeCalledTimes int
usersCalledTimes int
bindCalledTimes int
dialErrReturn error
loginErrReturn error
loginReturn *login.ExternalUserInfo
bindErrReturn error
usersErrReturn error
usersFirstReturn []*login.ExternalUserInfo
usersRestReturn []*login.ExternalUserInfo
}
// Login test fn
func (mock *mockLDAP) Login(*login.LoginUserQuery) (*login.ExternalUserInfo, error) {
mock.loginCalledTimes++
return mock.loginReturn, mock.loginErrReturn
}
// Users test fn
func (mock *mockLDAP) Users([]string) ([]*login.ExternalUserInfo, error) {
mock.usersCalledTimes++
if mock.usersCalledTimes == 1 {
return mock.usersFirstReturn, mock.usersErrReturn
}
return mock.usersRestReturn, mock.usersErrReturn
}
// UserBind test fn
func (mock *mockLDAP) UserBind(string, string) error {
return nil
}
// Dial test fn
func (mock *mockLDAP) Dial() error {
mock.dialCalledTimes++
return mock.dialErrReturn
}
// Close test fn
func (mock *mockLDAP) Close() {
mock.closeCalledTimes++
}
func (mock *mockLDAP) Bind() error {
mock.bindCalledTimes++
return mock.bindErrReturn
}
func setup() *mockLDAP {
mock := &mockLDAP{}
newLDAP = func(config *ldap.ServerConfig) ldap.IServer {
return mock
}
return mock
}
func teardown() {
newLDAP = ldap.New
}