diff --git a/pkg/api/api.go b/pkg/api/api.go index b57e7d9b3b0..730efda4a98 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -396,6 +396,7 @@ func (hs *HTTPServer) registerRoutes() { adminRoute.Post("/provisioning/notifications/reload", Wrap(hs.AdminProvisioningReloadNotifications)) adminRoute.Post("/ldap/reload", Wrap(hs.ReloadLDAPCfg)) adminRoute.Get("/ldap/:username", Wrap(hs.GetUserFromLDAP)) + adminRoute.Get("/ldap/status", Wrap(hs.GetLDAPStatus)) }, reqGrafanaAdmin) // rendering diff --git a/pkg/api/ldap_debug.go b/pkg/api/ldap_debug.go index 842c2f856ee..0f22f9a7ddd 100644 --- a/pkg/api/ldap_debug.go +++ b/pkg/api/ldap_debug.go @@ -86,25 +86,75 @@ func (user *LDAPUserDTO) FetchOrgs() error { return nil } +// LDAPServerDTO is a serializer for LDAP server statuses +type LDAPServerDTO struct { + Host string `json:"host"` + Port int `json:"port"` + Available bool `json:"available"` + Error string `json:"error"` +} + // ReloadLDAPCfg reloads the LDAP configuration func (server *HTTPServer) ReloadLDAPCfg() Response { if !ldap.IsEnabled() { - return Error(400, "LDAP is not enabled", nil) + return Error(http.StatusBadRequest, "LDAP is not enabled", nil) } err := ldap.ReloadConfig() if err != nil { - return Error(500, "Failed to reload ldap config.", err) + return Error(http.StatusInternalServerError, "Failed to reload ldap config.", err) } return Success("LDAP config reloaded") } +// GetLDAPStatus attempts to connect to all the configured LDAP servers and returns information on whenever they're availabe or not. +func (server *HTTPServer) GetLDAPStatus(c *models.ReqContext) Response { + if !ldap.IsEnabled() { + return Error(http.StatusBadRequest, "LDAP is not enabled", nil) + } + + ldapConfig, err := getLDAPConfig() + + if err != nil { + return Error(http.StatusBadRequest, "Failed to obtain the LDAP configuration. Please verify the configuration and try again.", err) + } + + ldap := newLDAP(ldapConfig.Servers) + + statuses, err := ldap.Ping() + + if err != nil { + return Error(http.StatusBadRequest, "Failed to connect to the LDAP server(s)", err) + } + + serverDTOs := []*LDAPServerDTO{} + for _, status := range statuses { + s := &LDAPServerDTO{ + Host: status.Host, + Available: status.Available, + Port: status.Port, + } + + if status.Error != nil { + s.Error = status.Error.Error() + } + + serverDTOs = append(serverDTOs, s) + } + + return JSON(http.StatusOK, serverDTOs) +} + // GetUserFromLDAP finds an user based on a username in LDAP. This helps illustrate how would the particular user be mapped in Grafana when synced. func (server *HTTPServer) GetUserFromLDAP(c *models.ReqContext) Response { + if !ldap.IsEnabled() { + return Error(http.StatusBadRequest, "LDAP is not enabled", nil) + } + ldapConfig, err := getLDAPConfig() if err != nil { - return Error(400, "Failed to obtain the LDAP configuration. Please ", err) + return Error(http.StatusBadRequest, "Failed to obtain the LDAP configuration. Please ", err) } ldap := newLDAP(ldapConfig.Servers) diff --git a/pkg/api/ldap_debug_test.go b/pkg/api/ldap_debug_test.go index ac86f4d3eb2..986ac5083f4 100644 --- a/pkg/api/ldap_debug_test.go +++ b/pkg/api/ldap_debug_test.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "errors" "net/http" "net/http/httptest" "testing" @@ -21,6 +22,12 @@ type LDAPMock struct { var userSearchResult *models.ExternalUserInfo var userSearchConfig ldap.ServerConfig +var pingResult []*multildap.ServerStatus +var pingError error + +func (m *LDAPMock) Ping() ([]*multildap.ServerStatus, error) { + return pingResult, pingError +} func (m *LDAPMock) Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) { return &models.ExternalUserInfo{}, nil @@ -35,11 +42,19 @@ func (m *LDAPMock) User(login string) (*models.ExternalUserInfo, ldap.ServerConf return userSearchResult, userSearchConfig, nil } +//*** +// GetUserFromLDAP tests +//*** + func getUserFromLDAPContext(t *testing.T, requestURL string) *scenarioContext { t.Helper() sc := setupScenarioContext(requestURL) + ldap := setting.LDAPEnabled + setting.LDAPEnabled = true + defer func() { setting.LDAPEnabled = ldap }() + hs := &HTTPServer{Cfg: setting.NewCfg()} sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { @@ -141,7 +156,7 @@ func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) { var expectedJSON interface{} _ = json.Unmarshal([]byte(expected), &expectedJSON) - assert.Equal(t, jsonResponse, expectedJSON) + assert.Equal(t, expectedJSON, jsonResponse) } func TestGetUserFromLDAPApiEndpoint(t *testing.T) { @@ -219,5 +234,70 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) { var expectedJSON interface{} _ = json.Unmarshal([]byte(expected), &expectedJSON) - assert.Equal(t, jsonResponse, expectedJSON) + assert.Equal(t, expectedJSON, jsonResponse) +} + +//*** +// GetLDAPStatus tests +//*** + +func getLDAPStatusContext(t *testing.T) *scenarioContext { + t.Helper() + + requestURL := "/api/admin/ldap/status" + sc := setupScenarioContext(requestURL) + + ldap := setting.LDAPEnabled + setting.LDAPEnabled = true + defer func() { setting.LDAPEnabled = ldap }() + + hs := &HTTPServer{Cfg: setting.NewCfg()} + + sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { + sc.context = c + return hs.GetLDAPStatus(c) + }) + + sc.m.Get("/api/admin/ldap/status", sc.defaultHandler) + + sc.resp = httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, requestURL, nil) + sc.req = req + sc.exec() + + return sc +} + +func TestGetLDAPStatusApiEndpoint(t *testing.T) { + pingResult = []*multildap.ServerStatus{ + {Host: "10.0.0.3", Port: 361, Available: true, Error: nil}, + {Host: "10.0.0.3", Port: 362, Available: true, Error: nil}, + {Host: "10.0.0.5", Port: 361, Available: false, Error: errors.New("something is awfully wrong")}, + } + + getLDAPConfig = func() (*ldap.Config, error) { + return &ldap.Config{}, nil + } + + newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP { + return &LDAPMock{} + } + + sc := getLDAPStatusContext(t) + + require.Equal(t, http.StatusOK, sc.resp.Code) + jsonResponse, err := getJSONbody(sc.resp) + assert.Nil(t, err) + + expected := ` + [ + { "host": "10.0.0.3", "port": 361, "available": true, "error": "" }, + { "host": "10.0.0.3", "port": 362, "available": true, "error": "" }, + { "host": "10.0.0.5", "port": 361, "available": false, "error": "something is awfully wrong" } + ] + ` + var expectedJSON interface{} + _ = json.Unmarshal([]byte(expected), &expectedJSON) + + assert.Equal(t, expectedJSON, jsonResponse) } diff --git a/pkg/login/ldap_login_test.go b/pkg/login/ldap_login_test.go index cf0a7f2c675..fe7ed5cf6b2 100644 --- a/pkg/login/ldap_login_test.go +++ b/pkg/login/ldap_login_test.go @@ -73,6 +73,13 @@ func TestLDAPLogin(t *testing.T) { type mockAuth struct { validLogin bool loginCalled bool + pingCalled bool +} + +func (auth *mockAuth) Ping() ([]*multildap.ServerStatus, error) { + auth.pingCalled = true + + return nil, nil } func (auth *mockAuth) Login(query *models.LoginUserQuery) ( diff --git a/pkg/services/multildap/multildap.go b/pkg/services/multildap/multildap.go index 6c8e500c871..953654ff483 100644 --- a/pkg/services/multildap/multildap.go +++ b/pkg/services/multildap/multildap.go @@ -28,8 +28,17 @@ 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, ) @@ -55,6 +64,39 @@ func New(configs []*ldap.ServerConfig) IMultiLDAP { } } +// 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) + } else { + status.Available = false + status.Error = err + serverStatuses = append(serverStatuses, status) + } + + defer server.Close() + } + + return serverStatuses, nil +} + // Login tries to log in the user in multiples LDAP func (multiples *MultiLDAP) Login(query *models.LoginUserQuery) ( *models.ExternalUserInfo, error, diff --git a/pkg/services/multildap/multildap_test.go b/pkg/services/multildap/multildap_test.go index f192cfb0dd7..89877320847 100644 --- a/pkg/services/multildap/multildap_test.go +++ b/pkg/services/multildap/multildap_test.go @@ -11,6 +11,56 @@ import ( func TestMultiLDAP(t *testing.T) { Convey("Multildap", t, func() { + Convey("Ping()", func() { + Convey("Should return error for absent config list", func() { + setup() + + multi := New([]*ldap.ServerConfig{}) + _, err := multi.Ping() + + So(err, ShouldBeError) + So(err, ShouldEqual, ErrNoLDAPServers) + + teardown() + }) + Convey("Should return an unavailable status on dial error", func() { + 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() + + So(err, ShouldBeNil) + So(statuses[0].Host, ShouldEqual, "10.0.0.1") + So(statuses[0].Port, ShouldEqual, 361) + So(statuses[0].Available, ShouldBeFalse) + So(statuses[0].Error, ShouldEqual, expectedErr) + + teardown() + }) + Convey("Shoudl get the LDAP server statuses", func() { + setup() + + multi := New([]*ldap.ServerConfig{ + {Host: "10.0.0.1", Port: 361}, + }) + + statuses, err := multi.Ping() + + So(err, ShouldBeNil) + So(statuses[0].Host, ShouldEqual, "10.0.0.1") + So(statuses[0].Port, ShouldEqual, 361) + So(statuses[0].Available, ShouldBeTrue) + So(statuses[0].Error, ShouldBeNil) + + teardown() + }) + }) Convey("Login()", func() { Convey("Should return error for absent config list", func() { setup() diff --git a/pkg/services/multildap/testing.go b/pkg/services/multildap/testing.go index 1994fbb0786..65b72c07097 100644 --- a/pkg/services/multildap/testing.go +++ b/pkg/services/multildap/testing.go @@ -69,10 +69,17 @@ type MockMultiLDAP struct { LoginCalledTimes int UsersCalledTimes int UserCalledTimes int + PingCalledTimes int UsersResult []*models.ExternalUserInfo } +func (mock *MockMultiLDAP) Ping() ([]*ServerStatus, error) { + mock.PingCalledTimes = mock.PingCalledTimes + 1 + + return nil, nil +} + // Login test fn func (mock *MockMultiLDAP) Login(query *models.LoginUserQuery) ( *models.ExternalUserInfo, error,