mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LDAP: Move to single package cluster (#63035)
* move multildap to ldap package * move LDAP api and tests to ldap package * register background service * lint
This commit is contained in:
@@ -5,8 +5,8 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/ldap/multildap"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/multildap"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/ldap"
|
||||
"github.com/grafana/grafana/pkg/services/ldap/multildap"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/multildap"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/ldap"
|
||||
"github.com/grafana/grafana/pkg/services/ldap/multildap"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/multildap"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/ldap"
|
||||
"github.com/grafana/grafana/pkg/services/ldap/multildap"
|
||||
"github.com/grafana/grafana/pkg/services/login/loginservice"
|
||||
"github.com/grafana/grafana/pkg/services/multildap"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
|
||||
437
pkg/services/ldap/api/service.go
Normal file
437
pkg/services/ldap/api/service.go
Normal file
@@ -0,0 +1,437 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/ldap"
|
||||
"github.com/grafana/grafana/pkg/services/ldap/multildap"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
cfg *setting.Cfg
|
||||
userService user.Service
|
||||
authInfoService login.AuthInfoService
|
||||
ldapGroupsService ldap.Groups
|
||||
loginService login.Service
|
||||
orgService org.Service
|
||||
sessionService auth.UserTokenService
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, router routing.RouteRegister, accessControl ac.AccessControl,
|
||||
userService user.Service, authInfoService login.AuthInfoService, ldapGroupsService ldap.Groups,
|
||||
loginService login.Service, orgService org.Service, sessionService auth.UserTokenService) *Service {
|
||||
s := &Service{
|
||||
cfg: cfg,
|
||||
userService: userService,
|
||||
authInfoService: authInfoService,
|
||||
ldapGroupsService: ldapGroupsService,
|
||||
loginService: loginService,
|
||||
orgService: orgService,
|
||||
sessionService: sessionService,
|
||||
log: log.New("ldap.api"),
|
||||
}
|
||||
|
||||
authorize := ac.Middleware(accessControl)
|
||||
reqGrafanaAdmin := middleware.ReqGrafanaAdmin
|
||||
|
||||
router.Group("/api/admin", func(adminRoute routing.RouteRegister) {
|
||||
adminRoute.Post("/ldap/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPConfigReload)), routing.Wrap(s.ReloadLDAPCfg))
|
||||
adminRoute.Post("/ldap/sync/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPUsersSync)), routing.Wrap(s.PostSyncUserWithLDAP))
|
||||
adminRoute.Get("/ldap/:username", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPUsersRead)), routing.Wrap(s.GetUserFromLDAP))
|
||||
adminRoute.Get("/ldap/status", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)), routing.Wrap(s.GetLDAPStatus))
|
||||
}, middleware.ReqSignedIn)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
var (
|
||||
getLDAPConfig = multildap.GetConfig
|
||||
newLDAP = multildap.New
|
||||
|
||||
errOrganizationNotFound = func(orgId int64) error {
|
||||
return fmt.Errorf("unable to find organization with ID '%d'", orgId)
|
||||
}
|
||||
)
|
||||
|
||||
// LDAPAttribute is a serializer for user attributes mapped from LDAP. Is meant to display both the serialized value and the LDAP key we received it from.
|
||||
type LDAPAttribute struct {
|
||||
ConfigAttributeValue string `json:"cfgAttrValue"`
|
||||
LDAPAttributeValue string `json:"ldapValue"`
|
||||
}
|
||||
|
||||
// RoleDTO is a serializer for mapped roles from LDAP
|
||||
type LDAPRoleDTO struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
OrgName string `json:"orgName"`
|
||||
OrgRole org.RoleType `json:"orgRole"`
|
||||
GroupDN string `json:"groupDN"`
|
||||
}
|
||||
|
||||
// LDAPUserDTO is a serializer for users mapped from LDAP
|
||||
type LDAPUserDTO struct {
|
||||
Name *LDAPAttribute `json:"name"`
|
||||
Surname *LDAPAttribute `json:"surname"`
|
||||
Email *LDAPAttribute `json:"email"`
|
||||
Username *LDAPAttribute `json:"login"`
|
||||
IsGrafanaAdmin *bool `json:"isGrafanaAdmin"`
|
||||
IsDisabled bool `json:"isDisabled"`
|
||||
OrgRoles []LDAPRoleDTO `json:"roles"`
|
||||
Teams []ldap.TeamOrgGroupDTO `json:"teams"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// FetchOrgs fetches the organization(s) information by executing a single query to the database. Then, populating the DTO with the information retrieved.
|
||||
func (user *LDAPUserDTO) FetchOrgs(ctx context.Context, orga org.Service) error {
|
||||
orgIds := []int64{}
|
||||
|
||||
for _, or := range user.OrgRoles {
|
||||
orgIds = append(orgIds, or.OrgId)
|
||||
}
|
||||
|
||||
q := &org.SearchOrgsQuery{}
|
||||
q.IDs = orgIds
|
||||
|
||||
result, err := orga.Search(ctx, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
orgNamesById := map[int64]string{}
|
||||
for _, org := range result {
|
||||
orgNamesById[org.ID] = org.Name
|
||||
}
|
||||
|
||||
for i, orgDTO := range user.OrgRoles {
|
||||
if orgDTO.OrgId < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
orgName := orgNamesById[orgDTO.OrgId]
|
||||
|
||||
if orgName != "" {
|
||||
user.OrgRoles[i].OrgName = orgName
|
||||
} else {
|
||||
return errOrganizationNotFound(orgDTO.OrgId)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// swagger:route POST /admin/ldap/reload admin_ldap reloadLDAPCfg
|
||||
//
|
||||
// Reloads the LDAP configuration.
|
||||
//
|
||||
// If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.config:reload`.
|
||||
//
|
||||
// Security:
|
||||
// - basic:
|
||||
//
|
||||
// Responses:
|
||||
// 200: okResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 500: internalServerError
|
||||
func (s *Service) ReloadLDAPCfg(c *contextmodel.ReqContext) response.Response {
|
||||
if !ldap.IsEnabled() {
|
||||
return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil)
|
||||
}
|
||||
|
||||
err := ldap.ReloadConfig()
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to reload LDAP config", err)
|
||||
}
|
||||
return response.Success("LDAP config reloaded")
|
||||
}
|
||||
|
||||
// swagger:route GET /admin/ldap/status admin_ldap getLDAPStatus
|
||||
//
|
||||
// Attempts to connect to all the configured LDAP servers and returns information on whenever they're available or not.
|
||||
//
|
||||
// If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.status:read`.
|
||||
//
|
||||
// Security:
|
||||
// - basic:
|
||||
//
|
||||
// Responses:
|
||||
// 200: okResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 500: internalServerError
|
||||
func (s *Service) GetLDAPStatus(c *contextmodel.ReqContext) response.Response {
|
||||
if !ldap.IsEnabled() {
|
||||
return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil)
|
||||
}
|
||||
|
||||
ldapConfig, err := getLDAPConfig(s.cfg)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Failed to obtain the LDAP configuration. Please verify the configuration and try again", err)
|
||||
}
|
||||
|
||||
ldap := newLDAP(ldapConfig.Servers)
|
||||
|
||||
if ldap == nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to find the LDAP server", nil)
|
||||
}
|
||||
|
||||
statuses, err := ldap.Ping()
|
||||
if err != nil {
|
||||
return response.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 response.JSON(http.StatusOK, serverDTOs)
|
||||
}
|
||||
|
||||
// swagger:route POST /admin/ldap/sync/{user_id} admin_ldap postSyncUserWithLDAP
|
||||
//
|
||||
// Enables a single Grafana user to be synchronized against LDAP.
|
||||
//
|
||||
// If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.user:sync`.
|
||||
//
|
||||
// Security:
|
||||
// - basic:
|
||||
//
|
||||
// Responses:
|
||||
// 200: okResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 500: internalServerError
|
||||
func (s *Service) PostSyncUserWithLDAP(c *contextmodel.ReqContext) response.Response {
|
||||
if !ldap.IsEnabled() {
|
||||
return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil)
|
||||
}
|
||||
|
||||
ldapConfig, err := getLDAPConfig(s.cfg)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Failed to obtain the LDAP configuration. Please verify the configuration and try again", err)
|
||||
}
|
||||
|
||||
userId, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "id is invalid", err)
|
||||
}
|
||||
|
||||
query := user.GetUserByIDQuery{ID: userId}
|
||||
|
||||
usr, err := s.userService.GetByID(c.Req.Context(), &query)
|
||||
if err != nil { // validate the userId exists
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
return response.Error(404, user.ErrUserNotFound.Error(), nil)
|
||||
}
|
||||
|
||||
return response.Error(500, "Failed to get user", err)
|
||||
}
|
||||
|
||||
authModuleQuery := &login.GetAuthInfoQuery{UserId: usr.ID, AuthModule: login.LDAPAuthModule}
|
||||
if err := s.authInfoService.GetAuthInfo(c.Req.Context(), authModuleQuery); err != nil { // validate the userId comes from LDAP
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
return response.Error(404, user.ErrUserNotFound.Error(), nil)
|
||||
}
|
||||
|
||||
return response.Error(500, "Failed to get user", err)
|
||||
}
|
||||
|
||||
ldapServer := newLDAP(ldapConfig.Servers)
|
||||
userInfo, _, err := ldapServer.User(usr.Login)
|
||||
if err != nil {
|
||||
if errors.Is(err, multildap.ErrDidNotFindUser) { // User was not in the LDAP server - we need to take action:
|
||||
if s.cfg.AdminUser == usr.Login { // User is *the* Grafana Admin. We cannot disable it.
|
||||
errMsg := fmt.Sprintf(`Refusing to sync grafana super admin "%s" - it would be disabled`, usr.Login)
|
||||
s.log.Error(errMsg)
|
||||
return response.Error(http.StatusBadRequest, errMsg, err)
|
||||
}
|
||||
|
||||
// Since the user was not in the LDAP server. Let's disable it.
|
||||
err := s.loginService.DisableExternalUser(c.Req.Context(), usr.Login)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to disable the user", err)
|
||||
}
|
||||
|
||||
err = s.sessionService.RevokeAllUserTokens(c.Req.Context(), userId)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to remove session tokens for the user", err)
|
||||
}
|
||||
|
||||
return response.Error(http.StatusBadRequest, "User not found in LDAP. Disabled the user without updating information", nil) // should this be a success?
|
||||
}
|
||||
|
||||
s.log.Debug("Failed to sync the user with LDAP", "err", err)
|
||||
return response.Error(http.StatusBadRequest, "Something went wrong while finding the user in LDAP", err)
|
||||
}
|
||||
|
||||
upsertCmd := &login.UpsertUserCommand{
|
||||
ReqContext: c,
|
||||
ExternalUser: userInfo,
|
||||
SignupAllowed: s.cfg.LDAPAllowSignup,
|
||||
UserLookupParams: login.UserLookupParams{
|
||||
UserID: &usr.ID, // Upsert by ID only
|
||||
Email: nil,
|
||||
Login: nil,
|
||||
},
|
||||
}
|
||||
|
||||
err = s.loginService.UpsertUser(c.Req.Context(), upsertCmd)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to update the user", err)
|
||||
}
|
||||
|
||||
return response.Success("User synced successfully")
|
||||
}
|
||||
|
||||
// swagger:route GET /admin/ldap/{user_name} admin_ldap getUserFromLDAP
|
||||
//
|
||||
// Finds an user based on a username in LDAP. This helps illustrate how would the particular user be mapped in Grafana when synced.
|
||||
//
|
||||
// If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.user:read`.
|
||||
//
|
||||
// Security:
|
||||
// - basic:
|
||||
//
|
||||
// Responses:
|
||||
// 200: okResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 500: internalServerError
|
||||
func (s *Service) GetUserFromLDAP(c *contextmodel.ReqContext) response.Response {
|
||||
if !ldap.IsEnabled() {
|
||||
return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil)
|
||||
}
|
||||
|
||||
ldapConfig, err := getLDAPConfig(s.cfg)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Failed to obtain the LDAP configuration", err)
|
||||
}
|
||||
|
||||
multiLDAP := newLDAP(ldapConfig.Servers)
|
||||
|
||||
username := web.Params(c.Req)[":username"]
|
||||
|
||||
if len(username) == 0 {
|
||||
return response.Error(http.StatusBadRequest, "Validation error. You must specify an username", nil)
|
||||
}
|
||||
|
||||
user, serverConfig, err := multiLDAP.User(username)
|
||||
if user == nil || err != nil {
|
||||
return response.Error(http.StatusNotFound, "No user was found in the LDAP server(s) with that username", err)
|
||||
}
|
||||
|
||||
s.log.Debug("user found", "user", user)
|
||||
|
||||
name, surname := splitName(user.Name)
|
||||
|
||||
u := &LDAPUserDTO{
|
||||
Name: &LDAPAttribute{serverConfig.Attr.Name, name},
|
||||
Surname: &LDAPAttribute{serverConfig.Attr.Surname, surname},
|
||||
Email: &LDAPAttribute{serverConfig.Attr.Email, user.Email},
|
||||
Username: &LDAPAttribute{serverConfig.Attr.Username, user.Login},
|
||||
IsGrafanaAdmin: user.IsGrafanaAdmin,
|
||||
IsDisabled: user.IsDisabled,
|
||||
}
|
||||
|
||||
unmappedUserGroups := map[string]struct{}{}
|
||||
for _, userGroup := range user.Groups {
|
||||
unmappedUserGroups[strings.ToLower(userGroup)] = struct{}{}
|
||||
}
|
||||
|
||||
orgIDs := []int64{} // IDs of the orgs the user is a member of
|
||||
orgRolesMap := map[int64]org.RoleType{}
|
||||
for _, group := range serverConfig.Groups {
|
||||
// only use the first match for each org
|
||||
if orgRolesMap[group.OrgId] != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if ldap.IsMemberOf(user.Groups, group.GroupDN) {
|
||||
orgRolesMap[group.OrgId] = group.OrgRole
|
||||
u.OrgRoles = append(u.OrgRoles, LDAPRoleDTO{GroupDN: group.GroupDN,
|
||||
OrgId: group.OrgId, OrgRole: group.OrgRole})
|
||||
delete(unmappedUserGroups, strings.ToLower(group.GroupDN))
|
||||
orgIDs = append(orgIDs, group.OrgId)
|
||||
}
|
||||
}
|
||||
|
||||
for userGroup := range unmappedUserGroups {
|
||||
u.OrgRoles = append(u.OrgRoles, LDAPRoleDTO{GroupDN: userGroup})
|
||||
}
|
||||
|
||||
s.log.Debug("mapping org roles", "orgsRoles", u.OrgRoles)
|
||||
if err := u.FetchOrgs(c.Req.Context(), s.orgService); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "An organization was not found - Please verify your LDAP configuration", err)
|
||||
}
|
||||
|
||||
u.Teams, err = s.ldapGroupsService.GetTeams(user.Groups, orgIDs)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Unable to find the teams for this user", err)
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, u)
|
||||
}
|
||||
|
||||
// splitName receives the full name of a user and splits it into two parts: A name and a surname.
|
||||
func splitName(name string) (string, string) {
|
||||
names := util.SplitString(name)
|
||||
|
||||
switch len(names) {
|
||||
case 0:
|
||||
return "", ""
|
||||
case 1:
|
||||
return names[0], ""
|
||||
default:
|
||||
return names[0], names[1]
|
||||
}
|
||||
}
|
||||
|
||||
// swagger:parameters getUserFromLDAP
|
||||
type GetLDAPUserParams struct {
|
||||
// in:path
|
||||
// required:true
|
||||
UserName string `json:"user_name"`
|
||||
}
|
||||
|
||||
// swagger:parameters postSyncUserWithLDAP
|
||||
type SyncLDAPUserParams struct {
|
||||
// in:path
|
||||
// required:true
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
736
pkg/services/ldap/api/service_test.go
Normal file
736
pkg/services/ldap/api/service_test.go
Normal file
@@ -0,0 +1,736 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"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/ldap"
|
||||
"github.com/grafana/grafana/pkg/services/ldap/multildap"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/login/logintest"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
||||
"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
|
||||
var 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()
|
||||
|
||||
a := ProvideService(cfg,
|
||||
router,
|
||||
acimpl.ProvideAccessControl(cfg),
|
||||
usertest.NewUserServiceFake(),
|
||||
&logintest.AuthInfoServiceFake{},
|
||||
ldap.ProvideGroupsService(),
|
||||
&logintest.LoginServiceFake{},
|
||||
&orgtest.FakeOrgService{},
|
||||
authtest.NewFakeUserAuthTokenService(),
|
||||
)
|
||||
|
||||
for _, o := range opts {
|
||||
o(a)
|
||||
}
|
||||
|
||||
server := webtest.NewServer(t, router)
|
||||
|
||||
return a, server
|
||||
}
|
||||
|
||||
func TestGetUserFromLDAPAPIEndpoint_UserNotFound(t *testing.T) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
|
||||
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
|
||||
return &ldap.Config{}, nil
|
||||
}
|
||||
|
||||
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
||||
return &LDAPMock{
|
||||
UserSearchResult: nil,
|
||||
}
|
||||
}
|
||||
|
||||
_, server := setupAPITest(t, func(a *Service) {
|
||||
a.orgService = &orgtest.FakeOrgService{
|
||||
ExpectedOrgs: []*org.OrgDTO{},
|
||||
}
|
||||
})
|
||||
|
||||
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) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
||||
return &LDAPMock{
|
||||
UserSearchResult: userSearchResult,
|
||||
UserSearchConfig: userSearchConfig,
|
||||
}
|
||||
}
|
||||
|
||||
mockOrgSearchResult := []*org.OrgDTO{
|
||||
{ID: 1, Name: "Main Org."},
|
||||
}
|
||||
|
||||
_, server := setupAPITest(t, func(a *Service) {
|
||||
a.orgService = &orgtest.FakeOrgService{
|
||||
ExpectedOrgs: mockOrgSearchResult,
|
||||
}
|
||||
})
|
||||
|
||||
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
|
||||
return &ldap.Config{}, nil
|
||||
}
|
||||
|
||||
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, "unable to find organization with ID '2'", resMap["error"])
|
||||
assert.Equal(t, "An organization was not found - Please verify your LDAP configuration", resMap["message"])
|
||||
}
|
||||
|
||||
func TestGetUserFromLDAPAPIEndpoint(t *testing.T) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
|
||||
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."},
|
||||
}
|
||||
|
||||
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
|
||||
return &ldap.Config{}, nil
|
||||
}
|
||||
|
||||
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
||||
return &LDAPMock{
|
||||
UserSearchResult: userSearchResult,
|
||||
UserSearchConfig: userSearchConfig,
|
||||
}
|
||||
}
|
||||
|
||||
_, server := setupAPITest(t, func(a *Service) {
|
||||
a.orgService = &orgtest.FakeOrgService{
|
||||
ExpectedOrgs: mockOrgSearchResult,
|
||||
}
|
||||
})
|
||||
|
||||
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) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
|
||||
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."},
|
||||
}
|
||||
|
||||
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
|
||||
return &ldap.Config{}, nil
|
||||
}
|
||||
|
||||
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
||||
return &LDAPMock{
|
||||
UserSearchResult: userSearchResult,
|
||||
UserSearchConfig: userSearchConfig,
|
||||
}
|
||||
}
|
||||
|
||||
_, server := setupAPITest(t, func(a *Service) {
|
||||
a.orgService = &orgtest.FakeOrgService{
|
||||
ExpectedOrgs: mockOrgSearchResult,
|
||||
}
|
||||
})
|
||||
|
||||
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) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
|
||||
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(*setting.Cfg) (*ldap.Config, error) {
|
||||
return &ldap.Config{}, nil
|
||||
}
|
||||
|
||||
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
||||
return &LDAPMock{}
|
||||
}
|
||||
|
||||
_, server := setupAPITest(t, func(a *Service) {
|
||||
})
|
||||
|
||||
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) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
|
||||
userServiceMock := usertest.NewUserServiceFake()
|
||||
userServiceMock.ExpectedUser = &user.User{Login: "ldap-daniel", ID: 34}
|
||||
|
||||
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
|
||||
return &ldap.Config{}, nil
|
||||
}
|
||||
|
||||
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
||||
return &LDAPMock{UserSearchResult: &login.ExternalUserInfo{
|
||||
Login: "ldap-daniel",
|
||||
}}
|
||||
}
|
||||
|
||||
_, server := setupAPITest(t, func(a *Service) {
|
||||
a.userService = userServiceMock
|
||||
})
|
||||
|
||||
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) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
userServiceMock := usertest.NewUserServiceFake()
|
||||
userServiceMock.ExpectedError = user.ErrUserNotFound
|
||||
|
||||
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
|
||||
return &ldap.Config{}, nil
|
||||
}
|
||||
|
||||
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
||||
return &LDAPMock{}
|
||||
}
|
||||
|
||||
_, server := setupAPITest(t, func(a *Service) {
|
||||
a.userService = userServiceMock
|
||||
})
|
||||
|
||||
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) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
|
||||
userServiceMock := usertest.NewUserServiceFake()
|
||||
userServiceMock.ExpectedUser = &user.User{Login: "ldap-daniel", ID: 34}
|
||||
|
||||
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
|
||||
return &ldap.Config{}, nil
|
||||
}
|
||||
|
||||
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
||||
return &LDAPMock{UserSearchError: multildap.ErrDidNotFindUser}
|
||||
}
|
||||
|
||||
_, server := setupAPITest(t, func(a *Service) {
|
||||
a.userService = userServiceMock
|
||||
a.cfg.AdminUser = "ldap-daniel"
|
||||
})
|
||||
|
||||
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, "did not find a user", resMap["error"])
|
||||
assert.Equal(t, "Refusing to sync grafana super admin \"ldap-daniel\" - it would be disabled", resMap["message"])
|
||||
}
|
||||
|
||||
func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
|
||||
userServiceMock := usertest.NewUserServiceFake()
|
||||
userServiceMock.ExpectedUser = &user.User{Login: "ldap-daniel", ID: 34}
|
||||
|
||||
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
|
||||
return &ldap.Config{}, nil
|
||||
}
|
||||
|
||||
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
||||
return &LDAPMock{UserSearchError: multildap.ErrDidNotFindUser}
|
||||
}
|
||||
|
||||
_, server := setupAPITest(t, func(a *Service) {
|
||||
a.userService = userServiceMock
|
||||
a.authInfoService = &logintest.AuthInfoServiceFake{ExpectedExternalUser: &login.ExternalUserInfo{IsDisabled: true, UserId: 34}}
|
||||
})
|
||||
|
||||
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) {
|
||||
setting.LDAPEnabled = true
|
||||
|
||||
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)
|
||||
|
||||
setting.LDAPConfigFile = f.Name()
|
||||
|
||||
errF = f.Close()
|
||||
require.NoError(t, errF)
|
||||
|
||||
defer func() {
|
||||
setting.LDAPEnabled = false
|
||||
setting.LDAPConfigFile = ""
|
||||
}()
|
||||
|
||||
getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
|
||||
return &ldap.Config{}, nil
|
||||
}
|
||||
|
||||
newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP {
|
||||
return &LDAPMock{
|
||||
UserSearchResult: &login.ExternalUserInfo{
|
||||
Login: "ldap-daniel",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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}}
|
||||
})
|
||||
// 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.GroupScopesByAction(permissions)}}
|
||||
}
|
||||
Reference in New Issue
Block a user