From 7c6dd1868bdaf0058501e0099e692c3e5c40f7b3 Mon Sep 17 00:00:00 2001 From: gotjosh Date: Wed, 4 Sep 2019 15:29:14 +0100 Subject: [PATCH] LDAP: Add API endpoint to query the LDAP server(s) status (#18868) * LDAP: Add API endpoint to query the LDAP server(s) status| This endpoint returns the current status(es) of the configured LDAP server(s). The status of each server is verified by dialling and if no error is returned we assume the server is operational. This is the last piece I'll produce as an API before moving into #18759 and see the view come to life. --- pkg/api/api.go | 1 + pkg/api/ldap_debug.go | 60 +++++++++++++++-- pkg/api/ldap_debug_test.go | 84 +++++++++++++++++++++++- pkg/login/ldap_login_test.go | 7 ++ pkg/services/multildap/multildap.go | 42 ++++++++++++ pkg/services/multildap/multildap_test.go | 50 ++++++++++++++ pkg/services/multildap/testing.go | 7 ++ 7 files changed, 244 insertions(+), 7 deletions(-) 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") } -// 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 { +// 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(400, "Failed to obtain the LDAP configuration. Please ", err) + 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(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,