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:
Jo 2023-02-09 16:31:31 +01:00 committed by GitHub
parent e42eaeb175
commit 7862ae8abf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 129 additions and 52 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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(

View 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
}

View File

@ -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) {

View File

@ -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