The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ldap/api/service_test.go

679 lines
19 KiB

package api
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ldap"
"github.com/grafana/grafana/pkg/services/ldap/multildap"
"github.com/grafana/grafana/pkg/services/ldap/service"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/authinfotest"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
)
type LDAPMock struct {
Results []*login.ExternalUserInfo
UserSearchResult *login.ExternalUserInfo
UserSearchConfig ldap.ServerConfig
UserSearchError error
}
var (
pingResult []*multildap.ServerStatus
pingError error
)
func (m *LDAPMock) Ping() ([]*multildap.ServerStatus, error) {
return pingResult, pingError
}
func (m *LDAPMock) Login(query *login.LoginUserQuery) (*login.ExternalUserInfo, error) {
return &login.ExternalUserInfo{}, nil
}
func (m *LDAPMock) Users(logins []string) ([]*login.ExternalUserInfo, error) {
s := []*login.ExternalUserInfo{}
return s, nil
}
func (m *LDAPMock) User(login string) (*login.ExternalUserInfo, ldap.ServerConfig, error) {
return m.UserSearchResult, m.UserSearchConfig, m.UserSearchError
}
func setupAPITest(t *testing.T, opts ...func(a *Service)) (*Service, *webtest.Server) {
t.Helper()
router := routing.NewRouteRegister()
cfg := setting.NewCfg()
cfg.LDAPAuthEnabled = true
a := ProvideService(cfg,
router,
acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
usertest.NewUserServiceFake(),
&authinfotest.FakeService{},
ldap.ProvideGroupsService(),
&authntest.FakeService{},
&orgtest.FakeOrgService{},
service.NewLDAPFakeService(),
authtest.NewFakeUserAuthTokenService(),
supportbundlestest.NewFakeBundleService(),
)
for _, o := range opts {
o(a)
}
server := webtest.NewServer(t, router)
return a, server
}
func TestGetUserFromLDAPAPIEndpoint_UserNotFound(t *testing.T) {
_, server := setupAPITest(t, func(a *Service) {
a.orgService = &orgtest.FakeOrgService{
ExpectedOrgs: []*org.OrgDTO{},
}
a.ldapService = &service.LDAPFakeService{
ExpectedClient: &LDAPMock{
UserSearchResult: nil,
},
ExpectedConfig: &ldap.ServersConfig{},
}
})
req := server.NewGetRequest("/api/admin/ldap/user-that-does-not-exist")
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:read": {"*"}},
},
})
res, err := server.Send(req)
defer func() { require.NoError(t, res.Body.Close()) }()
require.NoError(t, err)
assert.Equal(t, http.StatusNotFound, res.StatusCode)
bodyBytes, _ := io.ReadAll(res.Body)
assert.JSONEq(t, "{\"message\":\"No user was found in the LDAP server(s) with that username\"}", string(bodyBytes))
}
func TestGetUserFromLDAPAPIEndpoint_OrgNotfound(t *testing.T) {
isAdmin := true
userSearchResult := &login.ExternalUserInfo{
Name: "John Doe",
Email: "john.doe@example.com",
Login: "johndoe",
Groups: []string{"cn=admins,ou=groups,dc=grafana,dc=org"},
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin, 2: org.RoleViewer},
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: org.RoleAdmin,
},
{
GroupDN: "cn=admins,ou=groups,dc=grafana,dc=org",
OrgId: 2,
OrgRole: org.RoleViewer,
},
},
}
mockOrgSearchResult := []*org.OrgDTO{
{ID: 1, Name: "Main Org."},
}
_, server := setupAPITest(t, func(a *Service) {
a.orgService = &orgtest.FakeOrgService{
ExpectedOrgs: mockOrgSearchResult,
}
a.ldapService = &service.LDAPFakeService{
ExpectedClient: &LDAPMock{
UserSearchResult: userSearchResult,
UserSearchConfig: userSearchConfig,
},
ExpectedConfig: &ldap.ServersConfig{},
}
})
req := server.NewGetRequest("/api/admin/ldap/johndoe")
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:read": {"*"}},
},
})
res, err := server.Send(req)
defer func() { require.NoError(t, res.Body.Close()) }()
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
bodyBytes, _ := io.ReadAll(res.Body)
var resMap map[string]interface{}
err = json.Unmarshal(bodyBytes, &resMap)
assert.NoError(t, err)
assert.Equal(t, "An organization was not found - Please verify your LDAP configuration", resMap["message"])
}
func TestGetUserFromLDAPAPIEndpoint(t *testing.T) {
isAdmin := true
userSearchResult := &login.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]org.RoleType{1: org.RoleAdmin},
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: org.RoleAdmin,
},
{
GroupDN: "cn=admins2,ou=groups,dc=grafana,dc=org",
OrgId: 1,
OrgRole: org.RoleAdmin,
},
},
}
mockOrgSearchResult := []*org.OrgDTO{
{ID: 1, Name: "Main Org."},
}
_, server := setupAPITest(t, func(a *Service) {
a.orgService = &orgtest.FakeOrgService{
ExpectedOrgs: mockOrgSearchResult,
}
a.ldapService = &service.LDAPFakeService{
ExpectedClient: &LDAPMock{
UserSearchResult: userSearchResult,
UserSearchConfig: userSearchConfig,
},
ExpectedConfig: &ldap.ServersConfig{},
}
})
req := server.NewGetRequest("/api/admin/ldap/johndoe")
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:read": {"*"}},
},
})
res, err := server.Send(req)
defer func() { require.NoError(t, res.Body.Close()) }()
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
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
}
`
bodyBytes, _ := io.ReadAll(res.Body)
assert.JSONEq(t, expected, string(bodyBytes))
}
func TestGetUserFromLDAPAPIEndpoint_WithTeamHandler(t *testing.T) {
isAdmin := true
userSearchResult := &login.ExternalUserInfo{
Name: "John Doe",
Email: "john.doe@example.com",
Login: "johndoe",
Groups: []string{"cn=admins,ou=groups,dc=grafana,dc=org"},
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
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: org.RoleAdmin,
},
},
}
mockOrgSearchResult := []*org.OrgDTO{
{ID: 1, Name: "Main Org."},
}
_, server := setupAPITest(t, func(a *Service) {
a.orgService = &orgtest.FakeOrgService{
ExpectedOrgs: mockOrgSearchResult,
}
a.ldapService = &service.LDAPFakeService{
ExpectedClient: &LDAPMock{
UserSearchResult: userSearchResult,
UserSearchConfig: userSearchConfig,
},
ExpectedConfig: &ldap.ServersConfig{},
}
})
req := server.NewGetRequest("/api/admin/ldap/johndoe")
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:read": {"*"}},
},
})
res, err := server.Send(req)
defer func() { require.NoError(t, res.Body.Close()) }()
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
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": null
}
`
bodyBytes, _ := io.ReadAll(res.Body)
assert.JSONEq(t, expected, string(bodyBytes))
}
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")},
}
_, server := setupAPITest(t, func(a *Service) {
a.ldapService = &service.LDAPFakeService{
ExpectedClient: &LDAPMock{},
ExpectedConfig: &ldap.ServersConfig{},
}
})
req := server.NewGetRequest("/api/admin/ldap/status")
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.status:read": {}},
},
})
res, err := server.Send(req)
defer func() { require.NoError(t, res.Body.Close()) }()
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
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" }
]
`
bodyBytes, _ := io.ReadAll(res.Body)
assert.JSONEq(t, expected, string(bodyBytes))
}
func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) {
userServiceMock := usertest.NewUserServiceFake()
userServiceMock.ExpectedUser = &user.User{Login: "ldap-daniel", ID: 34}
_, server := setupAPITest(t, func(a *Service) {
a.userService = userServiceMock
a.ldapService = &service.LDAPFakeService{
ExpectedClient: &LDAPMock{UserSearchResult: &login.ExternalUserInfo{
Login: "ldap-daniel",
}},
ExpectedConfig: &ldap.ServersConfig{},
}
})
req := server.NewPostRequest("/api/admin/ldap/sync/34", nil)
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:sync": {}},
},
})
res, err := server.Send(req)
defer func() { require.NoError(t, res.Body.Close()) }()
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
expected := `
{
"message": "User synced successfully"
}
`
bodyBytes, _ := io.ReadAll(res.Body)
assert.JSONEq(t, expected, string(bodyBytes))
}
func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) {
userServiceMock := usertest.NewUserServiceFake()
userServiceMock.ExpectedError = user.ErrUserNotFound
_, server := setupAPITest(t, func(a *Service) {
a.userService = userServiceMock
a.ldapService = &service.LDAPFakeService{
ExpectedClient: &LDAPMock{},
ExpectedConfig: &ldap.ServersConfig{},
}
})
req := server.NewPostRequest("/api/admin/ldap/sync/34", nil)
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:sync": {}},
},
})
res, err := server.Send(req)
defer func() { require.NoError(t, res.Body.Close()) }()
require.NoError(t, err)
assert.Equal(t, http.StatusNotFound, res.StatusCode)
expected := `
{
"message": "user not found"
}
`
bodyBytes, _ := io.ReadAll(res.Body)
assert.JSONEq(t, expected, string(bodyBytes))
}
func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) {
userServiceMock := usertest.NewUserServiceFake()
userServiceMock.ExpectedUser = &user.User{Login: "ldap-daniel", ID: 34}
_, server := setupAPITest(t, func(a *Service) {
a.userService = userServiceMock
a.adminUser = "ldap-daniel"
a.ldapService = &service.LDAPFakeService{
ExpectedClient: &LDAPMock{UserSearchError: multildap.ErrDidNotFindUser},
ExpectedConfig: &ldap.ServersConfig{},
}
})
req := server.NewPostRequest("/api/admin/ldap/sync/34", nil)
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:sync": {}},
},
})
res, err := server.Send(req)
defer func() { require.NoError(t, res.Body.Close()) }()
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
bodyBytes, _ := io.ReadAll(res.Body)
var resMap map[string]interface{}
err = json.Unmarshal(bodyBytes, &resMap)
assert.NoError(t, err)
assert.Equal(t, "Refusing to sync grafana super admin \"ldap-daniel\" - it would be disabled", resMap["message"])
}
func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) {
userServiceMock := usertest.NewUserServiceFake()
userServiceMock.ExpectedUser = &user.User{Login: "ldap-daniel", ID: 34}
_, server := setupAPITest(t, func(a *Service) {
a.userService = userServiceMock
a.authInfoService = &authinfotest.FakeService{ExpectedExternalUser: &login.ExternalUserInfo{IsDisabled: true, UserId: 34}}
a.ldapService = &service.LDAPFakeService{
ExpectedClient: &LDAPMock{UserSearchError: multildap.ErrDidNotFindUser},
ExpectedConfig: &ldap.ServersConfig{},
}
})
req := server.NewPostRequest("/api/admin/ldap/sync/34", nil)
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:sync": {}},
},
})
res, err := server.Send(req)
defer func() { require.NoError(t, res.Body.Close()) }()
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
expected := `
{
"message": "User not found in LDAP. Disabled the user without updating information"
}
`
bodyBytes, _ := io.ReadAll(res.Body)
assert.JSONEq(t, expected, string(bodyBytes))
}
func TestLDAP_AccessControl(t *testing.T) {
f, errC := os.CreateTemp("", "ldap.toml")
require.NoError(t, errC)
_, errF := f.WriteString(
`[[servers]]
host = "127.0.0.1"
port = 389
search_filter = "(cn=%s)"
search_base_dns = ["dc=grafana,dc=org"]`)
require.NoError(t, errF)
ldapConfigFile := f.Name()
errF = f.Close()
require.NoError(t, errF)
type testCase struct {
desc string
method string
url string
expectedCode int
permissions []accesscontrol.Permission
}
tests := []testCase{
{
url: "/api/admin/ldap/reload",
method: http.MethodPost,
desc: "ReloadLDAPCfg should return 200 for user with correct permissions",
expectedCode: http.StatusOK,
permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionLDAPConfigReload},
},
},
{
url: "/api/admin/ldap/reload",
method: http.MethodPost,
desc: "ReloadLDAPCfg should return 403 for user without required permissions",
expectedCode: http.StatusForbidden,
permissions: []accesscontrol.Permission{
{Action: "wrong"},
},
},
{
url: "/api/admin/ldap/status",
method: http.MethodGet,
desc: "GetLDAPStatus should return 200 for user without required permissions",
expectedCode: http.StatusOK,
permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionLDAPStatusRead},
},
},
{
url: "/api/admin/ldap/status",
method: http.MethodGet,
desc: "GetLDAPStatus should return 200 for user without required permissions",
expectedCode: http.StatusForbidden,
permissions: []accesscontrol.Permission{
{Action: "wrong"},
},
},
{
url: "/api/admin/ldap/test",
method: http.MethodGet,
desc: "GetUserFromLDAP should return 200 for user with required permissions",
expectedCode: http.StatusOK,
permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionLDAPUsersRead},
},
},
{
url: "/api/admin/ldap/test",
method: http.MethodGet,
desc: "GetUserFromLDAP should return 403 for user without required permissions",
expectedCode: http.StatusForbidden,
permissions: []accesscontrol.Permission{
{Action: "wrong"},
},
},
{
url: "/api/admin/ldap/sync/1",
method: http.MethodPost,
desc: "PostSyncUserWithLDAP should return 200 for user without required permissions",
expectedCode: http.StatusOK,
permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionLDAPUsersSync},
},
},
{
url: "/api/admin/ldap/sync/1",
method: http.MethodPost,
desc: "PostSyncUserWithLDAP should return 200 for user without required permissions",
expectedCode: http.StatusForbidden,
permissions: []accesscontrol.Permission{
{Action: "wrong"},
},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
_, server := setupAPITest(t, func(a *Service) {
a.userService = &usertest.FakeUserService{ExpectedUser: &user.User{Login: "ldap-daniel", ID: 1}}
a.cfg.ConfigFilePath = ldapConfigFile
a.ldapService = &service.LDAPFakeService{
ExpectedClient: &LDAPMock{UserSearchResult: &login.ExternalUserInfo{
Login: "ldap-daniel",
}},
ExpectedConfig: &ldap.ServersConfig{},
}
})
// Add minimal setup to pass handler
res, err := server.Send(
webtest.RequestWithSignedInUser(server.NewRequest(tt.method, tt.url, nil),
userWithPermissions(1, tt.permissions)))
require.NoError(t, err)
bodyBytes, _ := io.ReadAll(res.Body)
assert.Equal(t, tt.expectedCode, res.StatusCode, string(bodyBytes))
require.NoError(t, res.Body.Close())
})
}
}
func userWithPermissions(orgID int64, permissions []accesscontrol.Permission) *user.SignedInUser {
return &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByActionContext(context.Background(), permissions)}}
}