diff --git a/pkg/login/social/socialimpl/service_test.go b/pkg/login/social/socialimpl/service_test.go index e708f74129d..9beeae7abd3 100644 --- a/pkg/login/social/socialimpl/service_test.go +++ b/pkg/login/social/socialimpl/service_test.go @@ -73,7 +73,7 @@ func TestSocialService_ProvideService(t *testing.T) { accessControl := acimpl.ProvideAccessControl(cfg) sqlStore := db.InitTestDB(t) - ssoSettingsSvc := ssosettingsimpl.ProvideService(cfg, sqlStore, accessControl, routing.NewRouteRegister(), featuremgmt.WithFeatures(), secrets) + ssoSettingsSvc := ssosettingsimpl.ProvideService(cfg, sqlStore, accessControl, routing.NewRouteRegister(), featuremgmt.WithFeatures(), secrets, &usagestats.UsageStatsMock{}) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/registry/backgroundsvcs/background_services.go b/pkg/registry/backgroundsvcs/background_services.go index 3e73398ffb6..7c8a0821833 100644 --- a/pkg/registry/backgroundsvcs/background_services.go +++ b/pkg/registry/backgroundsvcs/background_services.go @@ -59,7 +59,7 @@ func ProvideBackgroundServiceRegistry( keyRetriever *dynamic.KeyRetriever, dynamicAngularDetectorsProvider *angulardetectorsprovider.Dynamic, grafanaAPIServer grafanaapiserver.Service, anon *anonimpl.AnonDeviceService, - ssoSettings *ssosettingsimpl.SSOSettingsService, + ssoSettings *ssosettingsimpl.Service, // Need to make sure these are initialized, is there a better place to put them? _ dashboardsnapshots.Service, _ *alerting.AlertNotificationService, _ serviceaccounts.Service, _ *guardian.Provider, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 94640ea2ffe..f64b19ff8a8 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -380,7 +380,7 @@ var wireBasicSet = wire.NewSet( signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), ssoSettingsImpl.ProvideService, - wire.Bind(new(ssosettings.Service), new(*ssoSettingsImpl.SSOSettingsService)), + wire.Bind(new(ssosettings.Service), new(*ssoSettingsImpl.Service)), idimpl.ProvideService, wire.Bind(new(auth.IDService), new(*idimpl.Service)), cloudmigrations.ProvideService, diff --git a/pkg/services/ssosettings/ssosettingsimpl/service.go b/pkg/services/ssosettings/ssosettingsimpl/service.go index 7c25ad5743b..894725efef9 100644 --- a/pkg/services/ssosettings/ssosettingsimpl/service.go +++ b/pkg/services/ssosettings/ssosettingsimpl/service.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/usagestats" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/secrets" @@ -22,9 +23,9 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -var _ ssosettings.Service = (*SSOSettingsService)(nil) +var _ ssosettings.Service = (*Service)(nil) -type SSOSettingsService struct { +type Service struct { logger log.Logger cfg *setting.Cfg store ssosettings.Store @@ -37,7 +38,7 @@ type SSOSettingsService struct { func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl, routeRegister routing.RouteRegister, features featuremgmt.FeatureToggles, - secrets secrets.Service) *SSOSettingsService { + secrets secrets.Service, usageStats usagestats.Service) *Service { strategies := []ssosettings.FallbackStrategy{ strategies.NewOAuthStrategy(cfg), // register other strategies here, for example SAML @@ -45,7 +46,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl, store := database.ProvideStore(sqlStore) - svc := &SSOSettingsService{ + svc := &Service{ logger: log.New("ssosettings.service"), cfg: cfg, store: store, @@ -55,6 +56,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl, reloadables: make(map[string]ssosettings.Reloadable), } + usageStats.RegisterMetricsFunc(svc.getUsageStats) + if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) { ssoSettingsApi := api.ProvideApi(svc, routeRegister, ac) ssoSettingsApi.RegisterAPIEndpoints() @@ -63,9 +66,9 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl, return svc } -var _ ssosettings.Service = (*SSOSettingsService)(nil) +var _ ssosettings.Service = (*Service)(nil) -func (s *SSOSettingsService) GetForProvider(ctx context.Context, provider string) (*models.SSOSettings, error) { +func (s *Service) GetForProvider(ctx context.Context, provider string) (*models.SSOSettings, error) { dbSettings, err := s.store.Get(ctx, provider) if err != nil && !errors.Is(err, ssosettings.ErrNotFound) { return nil, err @@ -87,7 +90,7 @@ func (s *SSOSettingsService) GetForProvider(ctx context.Context, provider string return s.mergeSSOSettings(dbSettings, systemSettings), nil } -func (s *SSOSettingsService) GetForProviderWithRedactedSecrets(ctx context.Context, provider string) (*models.SSOSettings, error) { +func (s *Service) GetForProviderWithRedactedSecrets(ctx context.Context, provider string) (*models.SSOSettings, error) { if !s.isProviderConfigurable(provider) { return nil, ssosettings.ErrNotConfigurable } @@ -106,7 +109,7 @@ func (s *SSOSettingsService) GetForProviderWithRedactedSecrets(ctx context.Conte return storeSettings, nil } -func (s *SSOSettingsService) List(ctx context.Context) ([]*models.SSOSettings, error) { +func (s *Service) List(ctx context.Context) ([]*models.SSOSettings, error) { result := make([]*models.SSOSettings, 0, len(ssosettings.AllOAuthProviders)) storedSettings, err := s.store.List(ctx) @@ -134,7 +137,7 @@ func (s *SSOSettingsService) List(ctx context.Context) ([]*models.SSOSettings, e return result, nil } -func (s *SSOSettingsService) ListWithRedactedSecrets(ctx context.Context) ([]*models.SSOSettings, error) { +func (s *Service) ListWithRedactedSecrets(ctx context.Context) ([]*models.SSOSettings, error) { storeSettings, err := s.List(ctx) if err != nil { return nil, err @@ -158,7 +161,7 @@ func (s *SSOSettingsService) ListWithRedactedSecrets(ctx context.Context) ([]*mo return configurableSettings, nil } -func (s *SSOSettingsService) Upsert(ctx context.Context, settings *models.SSOSettings) error { +func (s *Service) Upsert(ctx context.Context, settings *models.SSOSettings) error { if !s.isProviderConfigurable(settings.Provider) { return ssosettings.ErrNotConfigurable } @@ -201,33 +204,33 @@ func (s *SSOSettingsService) Upsert(ctx context.Context, settings *models.SSOSet return nil } -func (s *SSOSettingsService) Patch(ctx context.Context, provider string, data map[string]any) error { +func (s *Service) Patch(ctx context.Context, provider string, data map[string]any) error { panic("not implemented") // TODO: Implement } -func (s *SSOSettingsService) Delete(ctx context.Context, provider string) error { +func (s *Service) Delete(ctx context.Context, provider string) error { if !s.isProviderConfigurable(provider) { return ssosettings.ErrNotConfigurable } return s.store.Delete(ctx, provider) } -func (s *SSOSettingsService) Reload(ctx context.Context, provider string) { +func (s *Service) Reload(ctx context.Context, provider string) { panic("not implemented") // TODO: Implement } -func (s *SSOSettingsService) RegisterReloadable(provider string, reloadable ssosettings.Reloadable) { +func (s *Service) RegisterReloadable(provider string, reloadable ssosettings.Reloadable) { if s.reloadables == nil { s.reloadables = make(map[string]ssosettings.Reloadable) } s.reloadables[provider] = reloadable } -func (s *SSOSettingsService) RegisterFallbackStrategy(providerRegex string, strategy ssosettings.FallbackStrategy) { +func (s *Service) RegisterFallbackStrategy(providerRegex string, strategy ssosettings.FallbackStrategy) { s.fbStrategies = append(s.fbStrategies, strategy) } -func (s *SSOSettingsService) loadSettingsUsingFallbackStrategy(ctx context.Context, provider string) (*models.SSOSettings, error) { +func (s *Service) loadSettingsUsingFallbackStrategy(ctx context.Context, provider string) (*models.SSOSettings, error) { loadStrategy, ok := s.getFallbackStrategyFor(provider) if !ok { return nil, errors.New("no fallback strategy found for provider: " + provider) @@ -254,7 +257,7 @@ func getSettingByProvider(provider string, settings []*models.SSOSettings) *mode return nil } -func (s *SSOSettingsService) getFallbackStrategyFor(provider string) (ssosettings.FallbackStrategy, bool) { +func (s *Service) getFallbackStrategyFor(provider string) (ssosettings.FallbackStrategy, bool) { for _, strategy := range s.fbStrategies { if strategy.IsMatch(provider) { return strategy, true @@ -263,7 +266,7 @@ func (s *SSOSettingsService) getFallbackStrategyFor(provider string) (ssosetting return nil, false } -func (s *SSOSettingsService) encryptSecrets(ctx context.Context, settings, storedSettings map[string]any) (map[string]any, error) { +func (s *Service) encryptSecrets(ctx context.Context, settings, storedSettings map[string]any) (map[string]any, error) { result := make(map[string]any) for k, v := range settings { if isSecret(k) && v != "" { @@ -289,7 +292,7 @@ func (s *SSOSettingsService) encryptSecrets(ctx context.Context, settings, store return result, nil } -func (s *SSOSettingsService) Run(ctx context.Context) error { +func (s *Service) Run(ctx context.Context) error { interval := s.cfg.SSOSettingsReloadInterval if interval == 0 { return nil @@ -310,7 +313,7 @@ func (s *SSOSettingsService) Run(ctx context.Context) error { } } -func (s *SSOSettingsService) doReload(ctx context.Context) { +func (s *Service) doReload(ctx context.Context) { s.logger.Debug("reloading SSO Settings for all providers") settingsList, err := s.List(ctx) @@ -333,7 +336,7 @@ func (s *SSOSettingsService) doReload(ctx context.Context) { // mergeSSOSettings merges the settings from the database with the system settings // Required because it is possible that the user has configured some of the settings (current Advanced OAuth settings) // and the rest of the settings have to be loaded from the system settings -func (s *SSOSettingsService) mergeSSOSettings(dbSettings, systemSettings *models.SSOSettings) *models.SSOSettings { +func (s *Service) mergeSSOSettings(dbSettings, systemSettings *models.SSOSettings) *models.SSOSettings { if dbSettings == nil { s.logger.Debug("No SSO Settings found in the database, using system settings") return systemSettings @@ -352,7 +355,7 @@ func (s *SSOSettingsService) mergeSSOSettings(dbSettings, systemSettings *models return result } -func (s *SSOSettingsService) decryptSecrets(ctx context.Context, settings map[string]any) (map[string]any, error) { +func (s *Service) decryptSecrets(ctx context.Context, settings map[string]any) (map[string]any, error) { for k, v := range settings { if isSecret(k) && v != "" { strValue, ok := v.(string) @@ -379,7 +382,7 @@ func (s *SSOSettingsService) decryptSecrets(ctx context.Context, settings map[st return settings, nil } -func (s *SSOSettingsService) isProviderConfigurable(provider string) bool { +func (s *Service) isProviderConfigurable(provider string) bool { _, ok := s.cfg.SSOSettingsConfigurableProviders[provider] return ok } diff --git a/pkg/services/ssosettings/ssosettingsimpl/service_test.go b/pkg/services/ssosettings/ssosettingsimpl/service_test.go index cdc58760921..7537dbe369c 100644 --- a/pkg/services/ssosettings/ssosettingsimpl/service_test.go +++ b/pkg/services/ssosettings/ssosettingsimpl/service_test.go @@ -24,7 +24,7 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func TestSSOSettingsService_GetForProvider(t *testing.T) { +func TestService_GetForProvider(t *testing.T) { testCases := []struct { name string setup func(env testEnv) @@ -205,7 +205,7 @@ func TestSSOSettingsService_GetForProvider(t *testing.T) { } } -func TestSSOSettingsService_GetForProviderWithRedactedSecrets(t *testing.T) { +func TestService_GetForProviderWithRedactedSecrets(t *testing.T) { testCases := []struct { name string setup func(env testEnv) @@ -304,7 +304,7 @@ func TestSSOSettingsService_GetForProviderWithRedactedSecrets(t *testing.T) { } } -func TestSSOSettingsService_List(t *testing.T) { +func TestService_List(t *testing.T) { testCases := []struct { name string setup func(env testEnv) @@ -447,7 +447,7 @@ func TestSSOSettingsService_List(t *testing.T) { } } -func TestSSOSettingsService_ListWithRedactedSecrets(t *testing.T) { +func TestService_ListWithRedactedSecrets(t *testing.T) { testCases := []struct { name string setup func(env testEnv) @@ -741,7 +741,7 @@ func TestSSOSettingsService_ListWithRedactedSecrets(t *testing.T) { } } -func TestSSOSettingsService_Upsert(t *testing.T) { +func TestService_Upsert(t *testing.T) { t.Run("successfully upsert SSO settings", func(t *testing.T) { env := setupTestEnv(t) @@ -1003,7 +1003,7 @@ func TestSSOSettingsService_Upsert(t *testing.T) { }) } -func TestSSOSettingsService_Delete(t *testing.T) { +func TestService_Delete(t *testing.T) { t.Run("successfully delete SSO settings", func(t *testing.T) { env := setupTestEnv(t) @@ -1048,7 +1048,7 @@ func TestSSOSettingsService_Delete(t *testing.T) { }) } -func TestSSOSettingsService_DoReload(t *testing.T) { +func TestService_DoReload(t *testing.T) { t.Run("successfully reload settings", func(t *testing.T) { env := setupTestEnv(t) @@ -1101,7 +1101,7 @@ func TestSSOSettingsService_DoReload(t *testing.T) { }) } -func TestSSOSettingsService_decryptSecrets(t *testing.T) { +func TestService_decryptSecrets(t *testing.T) { testCases := []struct { name string setup func(env testEnv) @@ -1216,7 +1216,7 @@ func setupTestEnv(t *testing.T) testEnv { }, } - svc := &SSOSettingsService{ + svc := &Service{ logger: log.NewNopLogger(), cfg: cfg, store: store, @@ -1239,7 +1239,7 @@ func setupTestEnv(t *testing.T) testEnv { type testEnv struct { cfg *setting.Cfg - service *SSOSettingsService + service *Service store *ssosettingstests.FakeStore ac accesscontrol.AccessControl fallbackStrategy *ssosettingstests.FakeFallbackStrategy diff --git a/pkg/services/ssosettings/ssosettingsimpl/usage_stats.go b/pkg/services/ssosettings/ssosettingsimpl/usage_stats.go new file mode 100644 index 00000000000..747415d2d75 --- /dev/null +++ b/pkg/services/ssosettings/ssosettingsimpl/usage_stats.go @@ -0,0 +1,30 @@ +package ssosettingsimpl + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/ssosettings/models" +) + +func (s *Service) getUsageStats(ctx context.Context) (map[string]any, error) { + m := map[string]any{} + + settings, err := s.store.List(ctx) + if err != nil { + return nil, err + } + + configuredInDbCounter := 0 + for _, setting := range settings { + enabledValue := 0 + if setting.Source == models.DB { + configuredInDbCounter++ + enabledValue = 1 + } + m["stats.sso."+setting.Provider+".config.database.count"] = enabledValue + } + + m["stats.sso.configured_in_db.count"] = configuredInDbCounter + + return m, nil +} diff --git a/pkg/services/ssosettings/ssosettingsimpl/usage_stats_test.go b/pkg/services/ssosettings/ssosettingsimpl/usage_stats_test.go new file mode 100644 index 00000000000..bd7d633ffdd --- /dev/null +++ b/pkg/services/ssosettings/ssosettingsimpl/usage_stats_test.go @@ -0,0 +1,68 @@ +package ssosettingsimpl + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ssosettings/models" + "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/require" +) + +func TestService_getUsageStats(t *testing.T) { + fakeStore := &ssosettingstests.FakeStore{ + ExpectedSSOSettings: []*models.SSOSettings{ + { + Provider: "google", + Source: models.DB, + }, + { + Provider: "github", + Source: models.System, + }, + { + Provider: "grafana_com", + Source: models.System, + }, + { + Provider: "generic_oauth", + Source: models.DB, + }, + { + Provider: "okta", + Source: models.DB, + }, + { + Provider: "azuread", + Source: models.DB, + }, + { + Provider: "gitlab", + Source: models.DB, + }, + }, + } + svc := &Service{ + logger: log.New("test"), + store: fakeStore, + cfg: &setting.Cfg{}, + } + + actual, err := svc.getUsageStats(context.Background()) + require.NoError(t, err) + + expected := map[string]any{ + "stats.sso.configured_in_db.count": 5, + "stats.sso.azuread.config.database.count": 1, + "stats.sso.gitlab.config.database.count": 1, + "stats.sso.google.config.database.count": 1, + "stats.sso.okta.config.database.count": 1, + "stats.sso.generic_oauth.config.database.count": 1, + "stats.sso.grafana_com.config.database.count": 0, + "stats.sso.github.config.database.count": 0, + } + + require.EqualValues(t, expected, actual) +}