Commit 7c6dd186 by gotjosh Committed by GitHub

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.
parent a5d8b021
...@@ -396,6 +396,7 @@ func (hs *HTTPServer) registerRoutes() { ...@@ -396,6 +396,7 @@ func (hs *HTTPServer) registerRoutes() {
adminRoute.Post("/provisioning/notifications/reload", Wrap(hs.AdminProvisioningReloadNotifications)) adminRoute.Post("/provisioning/notifications/reload", Wrap(hs.AdminProvisioningReloadNotifications))
adminRoute.Post("/ldap/reload", Wrap(hs.ReloadLDAPCfg)) adminRoute.Post("/ldap/reload", Wrap(hs.ReloadLDAPCfg))
adminRoute.Get("/ldap/:username", Wrap(hs.GetUserFromLDAP)) adminRoute.Get("/ldap/:username", Wrap(hs.GetUserFromLDAP))
adminRoute.Get("/ldap/status", Wrap(hs.GetLDAPStatus))
}, reqGrafanaAdmin) }, reqGrafanaAdmin)
// rendering // rendering
......
...@@ -86,25 +86,75 @@ func (user *LDAPUserDTO) FetchOrgs() error { ...@@ -86,25 +86,75 @@ func (user *LDAPUserDTO) FetchOrgs() error {
return nil 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 // ReloadLDAPCfg reloads the LDAP configuration
func (server *HTTPServer) ReloadLDAPCfg() Response { func (server *HTTPServer) ReloadLDAPCfg() Response {
if !ldap.IsEnabled() { if !ldap.IsEnabled() {
return Error(400, "LDAP is not enabled", nil) return Error(http.StatusBadRequest, "LDAP is not enabled", nil)
} }
err := ldap.ReloadConfig() err := ldap.ReloadConfig()
if err != nil { 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") 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. // 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 { func (server *HTTPServer) GetUserFromLDAP(c *models.ReqContext) Response {
if !ldap.IsEnabled() {
return Error(http.StatusBadRequest, "LDAP is not enabled", nil)
}
ldapConfig, err := getLDAPConfig() ldapConfig, err := getLDAPConfig()
if err != nil { 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) ldap := newLDAP(ldapConfig.Servers)
......
...@@ -2,6 +2,7 @@ package api ...@@ -2,6 +2,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
...@@ -21,6 +22,12 @@ type LDAPMock struct { ...@@ -21,6 +22,12 @@ type LDAPMock struct {
var userSearchResult *models.ExternalUserInfo var userSearchResult *models.ExternalUserInfo
var userSearchConfig ldap.ServerConfig 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) { func (m *LDAPMock) Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) {
return &models.ExternalUserInfo{}, nil return &models.ExternalUserInfo{}, nil
...@@ -35,11 +42,19 @@ func (m *LDAPMock) User(login string) (*models.ExternalUserInfo, ldap.ServerConf ...@@ -35,11 +42,19 @@ func (m *LDAPMock) User(login string) (*models.ExternalUserInfo, ldap.ServerConf
return userSearchResult, userSearchConfig, nil return userSearchResult, userSearchConfig, nil
} }
//***
// GetUserFromLDAP tests
//***
func getUserFromLDAPContext(t *testing.T, requestURL string) *scenarioContext { func getUserFromLDAPContext(t *testing.T, requestURL string) *scenarioContext {
t.Helper() t.Helper()
sc := setupScenarioContext(requestURL) sc := setupScenarioContext(requestURL)
ldap := setting.LDAPEnabled
setting.LDAPEnabled = true
defer func() { setting.LDAPEnabled = ldap }()
hs := &HTTPServer{Cfg: setting.NewCfg()} hs := &HTTPServer{Cfg: setting.NewCfg()}
sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.defaultHandler = Wrap(func(c *models.ReqContext) Response {
...@@ -141,7 +156,7 @@ func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) { ...@@ -141,7 +156,7 @@ func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) {
var expectedJSON interface{} var expectedJSON interface{}
_ = json.Unmarshal([]byte(expected), &expectedJSON) _ = json.Unmarshal([]byte(expected), &expectedJSON)
assert.Equal(t, jsonResponse, expectedJSON) assert.Equal(t, expectedJSON, jsonResponse)
} }
func TestGetUserFromLDAPApiEndpoint(t *testing.T) { func TestGetUserFromLDAPApiEndpoint(t *testing.T) {
...@@ -219,5 +234,70 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) { ...@@ -219,5 +234,70 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) {
var expectedJSON interface{} var expectedJSON interface{}
_ = json.Unmarshal([]byte(expected), &expectedJSON) _ = 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)
} }
...@@ -73,6 +73,13 @@ func TestLDAPLogin(t *testing.T) { ...@@ -73,6 +73,13 @@ func TestLDAPLogin(t *testing.T) {
type mockAuth struct { type mockAuth struct {
validLogin bool validLogin bool
loginCalled 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) ( func (auth *mockAuth) Login(query *models.LoginUserQuery) (
......
...@@ -28,8 +28,17 @@ var ErrNoLDAPServers = errors.New("No LDAP servers are configured") ...@@ -28,8 +28,17 @@ var ErrNoLDAPServers = errors.New("No LDAP servers are configured")
// ErrDidNotFindUser if request for user is unsuccessful // ErrDidNotFindUser if request for user is unsuccessful
var ErrDidNotFindUser = errors.New("Did not find a user") 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 // IMultiLDAP is interface for MultiLDAP
type IMultiLDAP interface { type IMultiLDAP interface {
Ping() ([]*ServerStatus, error)
Login(query *models.LoginUserQuery) ( Login(query *models.LoginUserQuery) (
*models.ExternalUserInfo, error, *models.ExternalUserInfo, error,
) )
...@@ -55,6 +64,39 @@ func New(configs []*ldap.ServerConfig) IMultiLDAP { ...@@ -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 // Login tries to log in the user in multiples LDAP
func (multiples *MultiLDAP) Login(query *models.LoginUserQuery) ( func (multiples *MultiLDAP) Login(query *models.LoginUserQuery) (
*models.ExternalUserInfo, error, *models.ExternalUserInfo, error,
......
...@@ -11,6 +11,56 @@ import ( ...@@ -11,6 +11,56 @@ import (
func TestMultiLDAP(t *testing.T) { func TestMultiLDAP(t *testing.T) {
Convey("Multildap", t, func() { 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("Login()", func() {
Convey("Should return error for absent config list", func() { Convey("Should return error for absent config list", func() {
setup() setup()
......
...@@ -69,10 +69,17 @@ type MockMultiLDAP struct { ...@@ -69,10 +69,17 @@ type MockMultiLDAP struct {
LoginCalledTimes int LoginCalledTimes int
UsersCalledTimes int UsersCalledTimes int
UserCalledTimes int UserCalledTimes int
PingCalledTimes int
UsersResult []*models.ExternalUserInfo UsersResult []*models.ExternalUserInfo
} }
func (mock *MockMultiLDAP) Ping() ([]*ServerStatus, error) {
mock.PingCalledTimes = mock.PingCalledTimes + 1
return nil, nil
}
// Login test fn // Login test fn
func (mock *MockMultiLDAP) Login(query *models.LoginUserQuery) ( func (mock *MockMultiLDAP) Login(query *models.LoginUserQuery) (
*models.ExternalUserInfo, error, *models.ExternalUserInfo, error,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment