From 7862ae8abf5d1c33750433dc1656021632b7d900 Mon Sep 17 00:00:00 2001 From: Jo Date: Thu, 9 Feb 2023 16:31:31 +0100 Subject: [PATCH] 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 --- .../troubleshooting/support-bundles/index.md | 1 + pkg/services/ldap/api/service.go | 43 +++++---- pkg/services/ldap/api/service_test.go | 39 ++------ pkg/services/ldap/api/support_bundle.go | 90 +++++++++++++++++++ pkg/services/ldap/settings.go | 6 +- pkg/setting/setting.go | 2 + 6 files changed, 129 insertions(+), 52 deletions(-) create mode 100644 pkg/services/ldap/api/support_bundle.go diff --git a/docs/sources/troubleshooting/support-bundles/index.md b/docs/sources/troubleshooting/support-bundles/index.md index 06447b1997b..7689df1cff1 100644 --- a/docs/sources/troubleshooting/support-bundles/index.md +++ b/docs/sources/troubleshooting/support-bundles/index.md @@ -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 diff --git a/pkg/services/ldap/api/service.go b/pkg/services/ldap/api/service.go index 1ebbb02c223..063d9b87516 100644 --- a/pkg/services/ldap/api/service.go +++ b/pkg/services/ldap/api/service.go @@ -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) } diff --git a/pkg/services/ldap/api/service_test.go b/pkg/services/ldap/api/service_test.go index ceb183c95ec..c79728414d0 100644 --- a/pkg/services/ldap/api/service_test.go +++ b/pkg/services/ldap/api/service_test.go @@ -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( diff --git a/pkg/services/ldap/api/support_bundle.go b/pkg/services/ldap/api/support_bundle.go new file mode 100644 index 00000000000..3b41fb2342c --- /dev/null +++ b/pkg/services/ldap/api/support_bundle.go @@ -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 +} diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index 450b43b078a..5e29b9b514e 100644 --- a/pkg/services/ldap/settings.go +++ b/pkg/services/ldap/settings.go @@ -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) { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 17e4bc3160c..c46397f9d8f 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -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