mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 12:14:08 -06:00
e4afc8d518
The arching goal of this commit is to enable single user synchronisation with LDAP. Also, it included minor fixes of style, error messages and minor bug fixing. The changes are: - bug: The `multildap` package has its own errors when the user is not found. We fixed the conditional branch on this error by asserting on the `multildap` errors as opposed to the `ldap` one - bug: The previous interface usage of `RevokeAllUserTokens` did not work as expected. This replaces the manual injection of the service by leveraging the service injected as part of the `server` struct. - chore: Better error messages around not finding the user in LDAP. - fix: Enable the single sync button and disable it when we receive an error from LDAP. Please note, that you can enable it by dispatching the error. This allows you to try again without having to reload the page. - fix: Move the sync info to the top, then move the sync button above that information and clearfix to have more harmony with the UI.
567 lines
14 KiB
Go
567 lines
14 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/services/auth"
|
|
"github.com/grafana/grafana/pkg/services/ldap"
|
|
"github.com/grafana/grafana/pkg/services/multildap"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type LDAPMock struct {
|
|
Results []*models.ExternalUserInfo
|
|
}
|
|
|
|
var userSearchResult *models.ExternalUserInfo
|
|
var userSearchConfig ldap.ServerConfig
|
|
var userSearchError error
|
|
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
|
|
}
|
|
|
|
func (m *LDAPMock) Users(logins []string) ([]*models.ExternalUserInfo, error) {
|
|
s := []*models.ExternalUserInfo{}
|
|
return s, nil
|
|
}
|
|
|
|
func (m *LDAPMock) User(login string) (*models.ExternalUserInfo, ldap.ServerConfig, error) {
|
|
return userSearchResult, userSearchConfig, userSearchError
|
|
}
|
|
|
|
//***
|
|
// 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 {
|
|
sc.context = c
|
|
return hs.GetUserFromLDAP(c)
|
|
})
|
|
|
|
sc.m.Get("/api/admin/ldap/:username", sc.defaultHandler)
|
|
|
|
sc.resp = httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodGet, requestURL, nil)
|
|
sc.req = req
|
|
sc.exec()
|
|
|
|
return sc
|
|
}
|
|
|
|
func TestGetUserFromLDAPApiEndpoint_UserNotFound(t *testing.T) {
|
|
getLDAPConfig = func() (*ldap.Config, error) {
|
|
return &ldap.Config{}, nil
|
|
}
|
|
|
|
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
|
return &LDAPMock{}
|
|
}
|
|
|
|
userSearchResult = nil
|
|
|
|
sc := getUserFromLDAPContext(t, "/api/admin/ldap/user-that-does-not-exist")
|
|
|
|
require.Equal(t, sc.resp.Code, http.StatusNotFound)
|
|
assert.JSONEq(t, "{\"message\":\"No user was found in the LDAP server(s) with that username\"}", sc.resp.Body.String())
|
|
}
|
|
|
|
func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) {
|
|
isAdmin := true
|
|
userSearchResult = &models.ExternalUserInfo{
|
|
Name: "John Doe",
|
|
Email: "john.doe@example.com",
|
|
Login: "johndoe",
|
|
Groups: []string{"cn=admins,ou=groups,dc=grafana,dc=org"},
|
|
OrgRoles: map[int64]models.RoleType{1: models.ROLE_ADMIN, 2: models.ROLE_VIEWER},
|
|
IsGrafanaAdmin: &isAdmin,
|
|
}
|
|
|
|
userSearchConfig = ldap.ServerConfig{
|
|
Attr: ldap.AttributeMap{
|
|
Name: "ldap-name",
|
|
Surname: "ldap-surname",
|
|
Email: "ldap-email",
|
|
Username: "ldap-username",
|
|
},
|
|
Groups: []*ldap.GroupToOrgRole{
|
|
{
|
|
GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org",
|
|
OrgId: 1,
|
|
OrgRole: models.ROLE_ADMIN,
|
|
},
|
|
{
|
|
GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org",
|
|
OrgId: 2,
|
|
OrgRole: models.ROLE_VIEWER,
|
|
},
|
|
},
|
|
}
|
|
|
|
mockOrgSearchResult := []*models.OrgDTO{
|
|
{Id: 1, Name: "Main Org."},
|
|
}
|
|
|
|
bus.AddHandler("test", func(query *models.SearchOrgsQuery) error {
|
|
query.Result = mockOrgSearchResult
|
|
return nil
|
|
})
|
|
|
|
getLDAPConfig = func() (*ldap.Config, error) {
|
|
return &ldap.Config{}, nil
|
|
}
|
|
|
|
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
|
return &LDAPMock{}
|
|
}
|
|
|
|
sc := getUserFromLDAPContext(t, "/api/admin/ldap/johndoe")
|
|
|
|
require.Equal(t, http.StatusBadRequest, sc.resp.Code)
|
|
|
|
expected := `
|
|
{
|
|
"error": "Unable to find organization with ID '2'",
|
|
"message": "An oganization was not found - Please verify your LDAP configuration"
|
|
}
|
|
`
|
|
assert.JSONEq(t, expected, sc.resp.Body.String())
|
|
}
|
|
|
|
func TestGetUserFromLDAPApiEndpoint(t *testing.T) {
|
|
isAdmin := true
|
|
userSearchResult = &models.ExternalUserInfo{
|
|
Name: "John Doe",
|
|
Email: "john.doe@example.com",
|
|
Login: "johndoe",
|
|
Groups: []string{"cn=admins,ou=groups,dc=grafana,dc=org", "another-group-not-matched"},
|
|
OrgRoles: map[int64]models.RoleType{1: models.ROLE_ADMIN},
|
|
IsGrafanaAdmin: &isAdmin,
|
|
}
|
|
|
|
userSearchConfig = ldap.ServerConfig{
|
|
Attr: ldap.AttributeMap{
|
|
Name: "ldap-name",
|
|
Surname: "ldap-surname",
|
|
Email: "ldap-email",
|
|
Username: "ldap-username",
|
|
},
|
|
Groups: []*ldap.GroupToOrgRole{
|
|
{
|
|
GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org",
|
|
OrgId: 1,
|
|
OrgRole: models.ROLE_ADMIN,
|
|
},
|
|
{
|
|
GroupDN: "cn=admins2,ou=groups,dc=grafana,dc=org",
|
|
OrgId: 1,
|
|
OrgRole: models.ROLE_ADMIN,
|
|
},
|
|
},
|
|
}
|
|
|
|
mockOrgSearchResult := []*models.OrgDTO{
|
|
{Id: 1, Name: "Main Org."},
|
|
}
|
|
|
|
bus.AddHandler("test", func(query *models.SearchOrgsQuery) error {
|
|
query.Result = mockOrgSearchResult
|
|
return nil
|
|
})
|
|
|
|
getLDAPConfig = func() (*ldap.Config, error) {
|
|
return &ldap.Config{}, nil
|
|
}
|
|
|
|
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
|
return &LDAPMock{}
|
|
}
|
|
|
|
sc := getUserFromLDAPContext(t, "/api/admin/ldap/johndoe")
|
|
|
|
assert.Equal(t, sc.resp.Code, http.StatusOK)
|
|
|
|
expected := `
|
|
{
|
|
"name": {
|
|
"cfgAttrValue": "ldap-name", "ldapValue": "John"
|
|
},
|
|
"surname": {
|
|
"cfgAttrValue": "ldap-surname", "ldapValue": "Doe"
|
|
},
|
|
"email": {
|
|
"cfgAttrValue": "ldap-email", "ldapValue": "john.doe@example.com"
|
|
},
|
|
"login": {
|
|
"cfgAttrValue": "ldap-username", "ldapValue": "johndoe"
|
|
},
|
|
"isGrafanaAdmin": true,
|
|
"isDisabled": false,
|
|
"roles": [
|
|
{ "orgId": 1, "orgRole": "Admin", "orgName": "Main Org.", "groupDN": "cn=admins,ou=groups,dc=grafana,dc=org" },
|
|
{ "orgId": 0, "orgRole": "", "orgName": "", "groupDN": "another-group-not-matched" }
|
|
],
|
|
"teams": null
|
|
}
|
|
`
|
|
|
|
assert.JSONEq(t, expected, sc.resp.Body.String())
|
|
}
|
|
|
|
func TestGetUserFromLDAPApiEndpoint_WithTeamHandler(t *testing.T) {
|
|
isAdmin := true
|
|
userSearchResult = &models.ExternalUserInfo{
|
|
Name: "John Doe",
|
|
Email: "john.doe@example.com",
|
|
Login: "johndoe",
|
|
Groups: []string{"cn=admins,ou=groups,dc=grafana,dc=org"},
|
|
OrgRoles: map[int64]models.RoleType{1: models.ROLE_ADMIN},
|
|
IsGrafanaAdmin: &isAdmin,
|
|
}
|
|
|
|
userSearchConfig = ldap.ServerConfig{
|
|
Attr: ldap.AttributeMap{
|
|
Name: "ldap-name",
|
|
Surname: "ldap-surname",
|
|
Email: "ldap-email",
|
|
Username: "ldap-username",
|
|
},
|
|
Groups: []*ldap.GroupToOrgRole{
|
|
{
|
|
GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org",
|
|
OrgId: 1,
|
|
OrgRole: models.ROLE_ADMIN,
|
|
},
|
|
},
|
|
}
|
|
|
|
mockOrgSearchResult := []*models.OrgDTO{
|
|
{Id: 1, Name: "Main Org."},
|
|
}
|
|
|
|
bus.AddHandler("test", func(query *models.SearchOrgsQuery) error {
|
|
query.Result = mockOrgSearchResult
|
|
return nil
|
|
})
|
|
|
|
bus.AddHandler("test", func(cmd *models.GetTeamsForLDAPGroupCommand) error {
|
|
cmd.Result = []models.TeamOrgGroupDTO{}
|
|
return nil
|
|
})
|
|
|
|
getLDAPConfig = func() (*ldap.Config, error) {
|
|
return &ldap.Config{}, nil
|
|
}
|
|
|
|
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
|
return &LDAPMock{}
|
|
}
|
|
|
|
sc := getUserFromLDAPContext(t, "/api/admin/ldap/johndoe")
|
|
|
|
require.Equal(t, sc.resp.Code, http.StatusOK)
|
|
|
|
expected := `
|
|
{
|
|
"name": {
|
|
"cfgAttrValue": "ldap-name", "ldapValue": "John"
|
|
},
|
|
"surname": {
|
|
"cfgAttrValue": "ldap-surname", "ldapValue": "Doe"
|
|
},
|
|
"email": {
|
|
"cfgAttrValue": "ldap-email", "ldapValue": "john.doe@example.com"
|
|
},
|
|
"login": {
|
|
"cfgAttrValue": "ldap-username", "ldapValue": "johndoe"
|
|
},
|
|
"isGrafanaAdmin": true,
|
|
"isDisabled": false,
|
|
"roles": [
|
|
{ "orgId": 1, "orgRole": "Admin", "orgName": "Main Org.", "groupDN": "cn=admins,ou=groups,dc=grafana,dc=org" }
|
|
],
|
|
"teams": []
|
|
}
|
|
`
|
|
|
|
assert.JSONEq(t, expected, sc.resp.Body.String())
|
|
}
|
|
|
|
//***
|
|
// 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)
|
|
|
|
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" }
|
|
]
|
|
`
|
|
assert.JSONEq(t, expected, sc.resp.Body.String())
|
|
}
|
|
|
|
//***
|
|
// PostSyncUserWithLDAP tests
|
|
//***
|
|
|
|
func postSyncUserWithLDAPContext(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(), AuthTokenService: auth.NewFakeUserAuthTokenService()}
|
|
|
|
sc.defaultHandler = Wrap(func(c *models.ReqContext) Response {
|
|
sc.context = c
|
|
return hs.PostSyncUserWithLDAP(c)
|
|
})
|
|
|
|
sc.m.Post("/api/admin/ldap/sync/:id", sc.defaultHandler)
|
|
|
|
sc.resp = httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodPost, requestURL, nil)
|
|
sc.req = req
|
|
sc.exec()
|
|
|
|
return sc
|
|
}
|
|
|
|
func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) {
|
|
getLDAPConfig = func() (*ldap.Config, error) {
|
|
return &ldap.Config{}, nil
|
|
}
|
|
|
|
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
|
return &LDAPMock{}
|
|
}
|
|
|
|
userSearchResult = &models.ExternalUserInfo{
|
|
Login: "ldap-daniel",
|
|
}
|
|
|
|
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
|
|
require.Equal(t, "ldap-daniel", cmd.ExternalUser.Login)
|
|
return nil
|
|
})
|
|
|
|
bus.AddHandler("test", func(q *models.GetUserByIdQuery) error {
|
|
require.Equal(t, q.Id, int64(34))
|
|
|
|
q.Result = &models.User{Login: "ldap-daniel", Id: 34}
|
|
return nil
|
|
})
|
|
|
|
bus.AddHandler("test", func(q *models.GetAuthInfoQuery) error {
|
|
require.Equal(t, q.UserId, int64(34))
|
|
require.Equal(t, q.AuthModule, models.AuthModuleLDAP)
|
|
|
|
return nil
|
|
})
|
|
|
|
sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34")
|
|
|
|
assert.Equal(t, http.StatusOK, sc.resp.Code)
|
|
|
|
expected := `
|
|
{
|
|
"message": "User synced successfully"
|
|
}
|
|
`
|
|
|
|
assert.JSONEq(t, expected, sc.resp.Body.String())
|
|
}
|
|
|
|
func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) {
|
|
getLDAPConfig = func() (*ldap.Config, error) {
|
|
return &ldap.Config{}, nil
|
|
}
|
|
|
|
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
|
return &LDAPMock{}
|
|
}
|
|
|
|
bus.AddHandler("test", func(q *models.GetUserByIdQuery) error {
|
|
require.Equal(t, q.Id, int64(34))
|
|
|
|
return models.ErrUserNotFound
|
|
})
|
|
|
|
sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34")
|
|
|
|
assert.Equal(t, http.StatusNotFound, sc.resp.Code)
|
|
|
|
expected := `
|
|
{
|
|
"message": "User not found"
|
|
}
|
|
`
|
|
|
|
assert.JSONEq(t, expected, sc.resp.Body.String())
|
|
}
|
|
|
|
func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) {
|
|
getLDAPConfig = func() (*ldap.Config, error) {
|
|
return &ldap.Config{}, nil
|
|
}
|
|
|
|
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
|
return &LDAPMock{}
|
|
}
|
|
|
|
userSearchError = multildap.ErrDidNotFindUser
|
|
|
|
admin := setting.AdminUser
|
|
setting.AdminUser = "ldap-daniel"
|
|
defer func() { setting.AdminUser = admin }()
|
|
|
|
bus.AddHandler("test", func(q *models.GetUserByIdQuery) error {
|
|
require.Equal(t, q.Id, int64(34))
|
|
|
|
q.Result = &models.User{Login: "ldap-daniel", Id: 34}
|
|
return nil
|
|
})
|
|
|
|
bus.AddHandler("test", func(q *models.GetAuthInfoQuery) error {
|
|
require.Equal(t, q.UserId, int64(34))
|
|
require.Equal(t, q.AuthModule, models.AuthModuleLDAP)
|
|
|
|
return nil
|
|
})
|
|
|
|
sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34")
|
|
|
|
assert.Equal(t, http.StatusBadRequest, sc.resp.Code)
|
|
|
|
expected := `
|
|
{
|
|
"error": "Did not find a user",
|
|
"message": "Refusing to sync grafana super admin \"ldap-daniel\" - it would be disabled"
|
|
}
|
|
`
|
|
|
|
assert.JSONEq(t, expected, sc.resp.Body.String())
|
|
}
|
|
|
|
func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) {
|
|
getLDAPConfig = func() (*ldap.Config, error) {
|
|
return &ldap.Config{}, nil
|
|
}
|
|
|
|
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
|
return &LDAPMock{}
|
|
}
|
|
|
|
userSearchResult = nil
|
|
|
|
bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
|
|
require.Equal(t, "ldap-daniel", cmd.ExternalUser.Login)
|
|
return nil
|
|
})
|
|
|
|
bus.AddHandler("test", func(q *models.GetUserByIdQuery) error {
|
|
require.Equal(t, q.Id, int64(34))
|
|
|
|
q.Result = &models.User{Login: "ldap-daniel", Id: 34}
|
|
return nil
|
|
})
|
|
|
|
bus.AddHandler("test", func(q *models.GetExternalUserInfoByLoginQuery) error {
|
|
assert.Equal(t, "ldap-daniel", q.LoginOrEmail)
|
|
q.Result = &models.ExternalUserInfo{IsDisabled: true, UserId: 34}
|
|
|
|
return nil
|
|
})
|
|
|
|
bus.AddHandler("test", func(cmd *models.DisableUserCommand) error {
|
|
assert.Equal(t, 34, cmd.UserId)
|
|
return nil
|
|
})
|
|
|
|
sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34")
|
|
|
|
assert.Equal(t, http.StatusBadRequest, sc.resp.Code)
|
|
|
|
expected := `
|
|
{
|
|
"message": "User not found in LDAP. Disabled the user without updating information"
|
|
}
|
|
`
|
|
|
|
assert.JSONEq(t, expected, sc.resp.Body.String())
|
|
}
|