mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SupportBundles: Add LDAP bundle collector (#63128)
* fix non-cfg fields used in ldap * fix non-cfg fields * add ldap support bundle * add note on match * add censoring and docs
This commit is contained in:
parent
e42eaeb175
commit
7862ae8abf
@ -30,6 +30,7 @@ A support bundle can include any of the following components:
|
||||
- **Basic information**: Basic information about the Grafana instance (version, memory usage, and so on)
|
||||
- **Settings**: Settings for the Grafana instance
|
||||
- **SAML**: Healthcheck connection and metadata for SAML (only displayed if SAML is enabled)
|
||||
- **LDAP**: Healthcheck connection and metadata for LDAP (only displayed if LDAP is enabled)
|
||||
|
||||
## Steps
|
||||
|
||||
|
@ -19,12 +19,22 @@ import (
|
||||
"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/supportbundles"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
getLDAPConfig = multildap.GetConfig
|
||||
newLDAP = multildap.New
|
||||
|
||||
errOrganizationNotFound = func(orgId int64) error {
|
||||
return fmt.Errorf("unable to find organization with ID '%d'", orgId)
|
||||
}
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
cfg *setting.Cfg
|
||||
userService user.Service
|
||||
@ -38,7 +48,8 @@ type Service struct {
|
||||
|
||||
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 {
|
||||
loginService login.Service, orgService org.Service,
|
||||
sessionService auth.UserTokenService, bundleRegistry supportbundles.Service) *Service {
|
||||
s := &Service{
|
||||
cfg: cfg,
|
||||
userService: userService,
|
||||
@ -60,18 +71,20 @@ func ProvideService(cfg *setting.Cfg, router routing.RouteRegister, accessContro
|
||||
adminRoute.Get("/ldap/status", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)), routing.Wrap(s.GetLDAPStatus))
|
||||
}, middleware.ReqSignedIn)
|
||||
|
||||
if cfg.LDAPEnabled {
|
||||
bundleRegistry.RegisterSupportItemCollector(supportbundles.Collector{
|
||||
UID: "auth-ldap",
|
||||
DisplayName: "LDAP",
|
||||
Description: "LDAP authentication healthcheck and configuration data",
|
||||
IncludedByDefault: false,
|
||||
Default: false,
|
||||
Fn: s.supportBundleCollector,
|
||||
})
|
||||
}
|
||||
|
||||
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"`
|
||||
@ -159,11 +172,11 @@ func (user *LDAPUserDTO) FetchOrgs(ctx context.Context, orga org.Service) error
|
||||
// 403: forbiddenError
|
||||
// 500: internalServerError
|
||||
func (s *Service) ReloadLDAPCfg(c *contextmodel.ReqContext) response.Response {
|
||||
if !ldap.IsEnabled() {
|
||||
if !s.cfg.LDAPEnabled {
|
||||
return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil)
|
||||
}
|
||||
|
||||
err := ldap.ReloadConfig()
|
||||
err := ldap.ReloadConfig(s.cfg.LDAPConfigFilePath)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to reload LDAP config", err)
|
||||
}
|
||||
@ -185,7 +198,7 @@ func (s *Service) ReloadLDAPCfg(c *contextmodel.ReqContext) response.Response {
|
||||
// 403: forbiddenError
|
||||
// 500: internalServerError
|
||||
func (s *Service) GetLDAPStatus(c *contextmodel.ReqContext) response.Response {
|
||||
if !ldap.IsEnabled() {
|
||||
if !s.cfg.LDAPEnabled {
|
||||
return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil)
|
||||
}
|
||||
|
||||
@ -238,7 +251,7 @@ func (s *Service) GetLDAPStatus(c *contextmodel.ReqContext) response.Response {
|
||||
// 403: forbiddenError
|
||||
// 500: internalServerError
|
||||
func (s *Service) PostSyncUserWithLDAP(c *contextmodel.ReqContext) response.Response {
|
||||
if !ldap.IsEnabled() {
|
||||
if !s.cfg.LDAPEnabled {
|
||||
return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil)
|
||||
}
|
||||
|
||||
@ -334,7 +347,7 @@ func (s *Service) PostSyncUserWithLDAP(c *contextmodel.ReqContext) response.Resp
|
||||
// 403: forbiddenError
|
||||
// 500: internalServerError
|
||||
func (s *Service) GetUserFromLDAP(c *contextmodel.ReqContext) response.Response {
|
||||
if !ldap.IsEnabled() {
|
||||
if !s.cfg.LDAPEnabled {
|
||||
return response.Error(http.StatusBadRequest, "LDAP is not enabled", nil)
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"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/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -58,6 +59,7 @@ func setupAPITest(t *testing.T, opts ...func(a *Service)) (*Service, *webtest.Se
|
||||
t.Helper()
|
||||
router := routing.NewRouteRegister()
|
||||
cfg := setting.NewCfg()
|
||||
cfg.LDAPEnabled = true
|
||||
|
||||
a := ProvideService(cfg,
|
||||
router,
|
||||
@ -68,6 +70,7 @@ func setupAPITest(t *testing.T, opts ...func(a *Service)) (*Service, *webtest.Se
|
||||
&logintest.LoginServiceFake{},
|
||||
&orgtest.FakeOrgService{},
|
||||
authtest.NewFakeUserAuthTokenService(),
|
||||
supportbundlestest.NewFakeBundleService(),
|
||||
)
|
||||
|
||||
for _, o := range opts {
|
||||
@ -80,9 +83,6 @@ func setupAPITest(t *testing.T, opts ...func(a *Service)) (*Service, *webtest.Se
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@ -116,9 +116,6 @@ func TestGetUserFromLDAPAPIEndpoint_UserNotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetUserFromLDAPAPIEndpoint_OrgNotfound(t *testing.T) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
|
||||
isAdmin := true
|
||||
userSearchResult := &login.ExternalUserInfo{
|
||||
Name: "John Doe",
|
||||
@ -193,9 +190,6 @@ func TestGetUserFromLDAPAPIEndpoint_OrgNotfound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetUserFromLDAPAPIEndpoint(t *testing.T) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
|
||||
isAdmin := true
|
||||
userSearchResult := &login.ExternalUserInfo{
|
||||
Name: "John Doe",
|
||||
@ -290,9 +284,6 @@ func TestGetUserFromLDAPAPIEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetUserFromLDAPAPIEndpoint_WithTeamHandler(t *testing.T) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
|
||||
isAdmin := true
|
||||
userSearchResult := &login.ExternalUserInfo{
|
||||
Name: "John Doe",
|
||||
@ -381,9 +372,6 @@ func TestGetUserFromLDAPAPIEndpoint_WithTeamHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
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},
|
||||
@ -427,9 +415,6 @@ func TestGetLDAPStatusAPIEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
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}
|
||||
|
||||
@ -471,8 +456,6 @@ func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) {
|
||||
setting.LDAPEnabled = true
|
||||
defer func() { setting.LDAPEnabled = false }()
|
||||
userServiceMock := usertest.NewUserServiceFake()
|
||||
userServiceMock.ExpectedError = user.ErrUserNotFound
|
||||
|
||||
@ -512,9 +495,6 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
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}
|
||||
|
||||
@ -553,9 +533,6 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) {
|
||||
}
|
||||
|
||||
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}
|
||||
|
||||
@ -596,8 +573,6 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLDAP_AccessControl(t *testing.T) {
|
||||
setting.LDAPEnabled = true
|
||||
|
||||
f, errC := os.CreateTemp("", "ldap.toml")
|
||||
require.NoError(t, errC)
|
||||
|
||||
@ -609,16 +584,11 @@ search_filter = "(cn=%s)"
|
||||
search_base_dns = ["dc=grafana,dc=org"]`)
|
||||
require.NoError(t, errF)
|
||||
|
||||
setting.LDAPConfigFile = f.Name()
|
||||
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
|
||||
}
|
||||
@ -717,6 +687,7 @@ search_base_dns = ["dc=grafana,dc=org"]`)
|
||||
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.LDAPConfigFilePath = ldapConfigFile
|
||||
})
|
||||
// Add minimal setup to pass handler
|
||||
res, err := server.Send(
|
||||
|
90
pkg/services/ldap/api/support_bundle.go
Normal file
90
pkg/services/ldap/api/support_bundle.go
Normal file
@ -0,0 +1,90 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func (s *Service) supportBundleCollector(context.Context) (*supportbundles.SupportItem, error) {
|
||||
bWriter := bytes.NewBuffer(nil)
|
||||
bWriter.WriteString("# LDAP information\n\n")
|
||||
|
||||
ldapConfig, err := getLDAPConfig(s.cfg)
|
||||
|
||||
if ldapConfig != nil {
|
||||
bWriter.WriteString("## LDAP Status\n")
|
||||
|
||||
ldapClient := newLDAP(ldapConfig.Servers)
|
||||
|
||||
ldapStatus, err := ldapClient.Ping()
|
||||
if err != nil {
|
||||
bWriter.WriteString(
|
||||
fmt.Sprintf("Unable to ping server\n Err: %s", err))
|
||||
}
|
||||
|
||||
for _, server := range ldapStatus {
|
||||
bWriter.WriteString(fmt.Sprintf("\nHost: %s \n", server.Host))
|
||||
bWriter.WriteString(fmt.Sprintf("Port: %d \n", server.Port))
|
||||
bWriter.WriteString(fmt.Sprintf("Available: %v \n", server.Available))
|
||||
if server.Error != nil {
|
||||
bWriter.WriteString(fmt.Sprintf("Error: %s\n", server.Error))
|
||||
}
|
||||
}
|
||||
|
||||
bWriter.WriteString("\n## LDAP Common Configuration issues\n\n")
|
||||
bWriter.WriteString("- Checked for **Mismatched search attributes**\n\n")
|
||||
issue := false
|
||||
for _, server := range ldapConfig.Servers {
|
||||
server.BindPassword = "********" // censor password on config dump
|
||||
server.ClientKey = "********" // censor client key on config dump
|
||||
|
||||
if !strings.Contains(server.SearchFilter, server.Attr.Username) {
|
||||
bWriter.WriteString(fmt.Sprintf(
|
||||
"Search filter does not match username attribute \n"+
|
||||
"Server: %s \n"+
|
||||
"Search filter: %s \n"+
|
||||
"Username attribute: %s \n",
|
||||
server.Host, server.SearchFilter, server.Attr.Username))
|
||||
issue = true
|
||||
}
|
||||
}
|
||||
if !issue {
|
||||
bWriter.WriteString("No issues found\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
bWriter.WriteString("## LDAP configuration\n\n")
|
||||
|
||||
bWriter.WriteString("```toml\n")
|
||||
errM := toml.NewEncoder(bWriter).Encode(ldapConfig)
|
||||
if errM != nil {
|
||||
bWriter.WriteString(
|
||||
fmt.Sprintf("Unable to encode LDAP configuration \n Err: %s", err))
|
||||
}
|
||||
bWriter.WriteString("```\n\n")
|
||||
|
||||
bWriter.WriteString("## Grafana LDAP configuration\n\n")
|
||||
|
||||
bWriter.WriteString("```ini\n")
|
||||
|
||||
bWriter.WriteString(fmt.Sprintf("enabled = %v\n", s.cfg.LDAPEnabled))
|
||||
bWriter.WriteString(fmt.Sprintf("config_file = %s\n", s.cfg.LDAPConfigFilePath))
|
||||
bWriter.WriteString(fmt.Sprintf("allow_sign_up = %v\n", s.cfg.LDAPAllowSignup))
|
||||
bWriter.WriteString(fmt.Sprintf("sync_cron = %s\n", setting.LDAPSyncCron))
|
||||
bWriter.WriteString(fmt.Sprintf("active_sync_enabled = %v\n", setting.LDAPActiveSyncEnabled))
|
||||
bWriter.WriteString(fmt.Sprintf("skip_org_role_sync = %v\n", setting.LDAPSkipOrgRoleSync))
|
||||
|
||||
bWriter.WriteString("```\n\n")
|
||||
|
||||
return &supportbundles.SupportItem{
|
||||
Filename: "ldap.md",
|
||||
FileBytes: bWriter.Bytes(),
|
||||
}, nil
|
||||
}
|
@ -81,7 +81,7 @@ func SkipOrgRoleSync() bool {
|
||||
}
|
||||
|
||||
// ReloadConfig reads the config from the disk and caches it.
|
||||
func ReloadConfig() error {
|
||||
func ReloadConfig(ldapConfigFilePath string) error {
|
||||
if !IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
@ -90,7 +90,7 @@ func ReloadConfig() error {
|
||||
defer loadingMutex.Unlock()
|
||||
|
||||
var err error
|
||||
config, err = readConfig(setting.LDAPConfigFile)
|
||||
config, err = readConfig(ldapConfigFilePath)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ func GetConfig(cfg *setting.Cfg) (*Config, error) {
|
||||
loadingMutex.Lock()
|
||||
defer loadingMutex.Unlock()
|
||||
|
||||
return readConfig(setting.LDAPConfigFile)
|
||||
return readConfig(cfg.LDAPConfigFilePath)
|
||||
}
|
||||
|
||||
func readConfig(configFile string) (*Config, error) {
|
||||
|
@ -442,6 +442,7 @@ type Cfg struct {
|
||||
// LDAP
|
||||
LDAPEnabled bool
|
||||
LDAPSkipOrgRoleSync bool
|
||||
LDAPConfigFilePath string
|
||||
LDAPAllowSignup bool
|
||||
|
||||
DefaultTheme string
|
||||
@ -1206,6 +1207,7 @@ func (cfg *Cfg) readSAMLConfig() {
|
||||
func (cfg *Cfg) readLDAPConfig() {
|
||||
ldapSec := cfg.Raw.Section("auth.ldap")
|
||||
LDAPConfigFile = ldapSec.Key("config_file").String()
|
||||
cfg.LDAPConfigFilePath = LDAPConfigFile
|
||||
LDAPSyncCron = ldapSec.Key("sync_cron").String()
|
||||
LDAPEnabled = ldapSec.Key("enabled").MustBool(false)
|
||||
cfg.LDAPEnabled = LDAPEnabled
|
||||
|
Loading…
Reference in New Issue
Block a user