Auth: Add SSO settings usage stats (#81143)

* Add usage stats

* UsageStats test + svc rename

* Fix test
This commit is contained in:
Misi 2024-01-24 15:39:50 +01:00 committed by GitHub
parent c44594d6b3
commit c47b55ae10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 137 additions and 36 deletions

View File

@ -73,7 +73,7 @@ func TestSocialService_ProvideService(t *testing.T) {
accessControl := acimpl.ProvideAccessControl(cfg) accessControl := acimpl.ProvideAccessControl(cfg)
sqlStore := db.InitTestDB(t) 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 { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {

View File

@ -59,7 +59,7 @@ func ProvideBackgroundServiceRegistry(
keyRetriever *dynamic.KeyRetriever, dynamicAngularDetectorsProvider *angulardetectorsprovider.Dynamic, keyRetriever *dynamic.KeyRetriever, dynamicAngularDetectorsProvider *angulardetectorsprovider.Dynamic,
grafanaAPIServer grafanaapiserver.Service, grafanaAPIServer grafanaapiserver.Service,
anon *anonimpl.AnonDeviceService, anon *anonimpl.AnonDeviceService,
ssoSettings *ssosettingsimpl.SSOSettingsService, ssoSettings *ssosettingsimpl.Service,
// Need to make sure these are initialized, is there a better place to put them? // Need to make sure these are initialized, is there a better place to put them?
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService, _ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
_ serviceaccounts.Service, _ *guardian.Provider, _ serviceaccounts.Service, _ *guardian.Provider,

View File

@ -380,7 +380,7 @@ var wireBasicSet = wire.NewSet(
signingkeysimpl.ProvideEmbeddedSigningKeysService, signingkeysimpl.ProvideEmbeddedSigningKeysService,
wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)),
ssoSettingsImpl.ProvideService, ssoSettingsImpl.ProvideService,
wire.Bind(new(ssosettings.Service), new(*ssoSettingsImpl.SSOSettingsService)), wire.Bind(new(ssosettings.Service), new(*ssoSettingsImpl.Service)),
idimpl.ProvideService, idimpl.ProvideService,
wire.Bind(new(auth.IDService), new(*idimpl.Service)), wire.Bind(new(auth.IDService), new(*idimpl.Service)),
cloudmigrations.ProvideService, cloudmigrations.ProvideService,

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
ac "github.com/grafana/grafana/pkg/services/accesscontrol" ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets"
@ -22,9 +23,9 @@ import (
"github.com/grafana/grafana/pkg/setting" "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 logger log.Logger
cfg *setting.Cfg cfg *setting.Cfg
store ssosettings.Store store ssosettings.Store
@ -37,7 +38,7 @@ type SSOSettingsService struct {
func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl, func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl,
routeRegister routing.RouteRegister, features featuremgmt.FeatureToggles, routeRegister routing.RouteRegister, features featuremgmt.FeatureToggles,
secrets secrets.Service) *SSOSettingsService { secrets secrets.Service, usageStats usagestats.Service) *Service {
strategies := []ssosettings.FallbackStrategy{ strategies := []ssosettings.FallbackStrategy{
strategies.NewOAuthStrategy(cfg), strategies.NewOAuthStrategy(cfg),
// register other strategies here, for example SAML // 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) store := database.ProvideStore(sqlStore)
svc := &SSOSettingsService{ svc := &Service{
logger: log.New("ssosettings.service"), logger: log.New("ssosettings.service"),
cfg: cfg, cfg: cfg,
store: store, store: store,
@ -55,6 +56,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl,
reloadables: make(map[string]ssosettings.Reloadable), reloadables: make(map[string]ssosettings.Reloadable),
} }
usageStats.RegisterMetricsFunc(svc.getUsageStats)
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) { if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {
ssoSettingsApi := api.ProvideApi(svc, routeRegister, ac) ssoSettingsApi := api.ProvideApi(svc, routeRegister, ac)
ssoSettingsApi.RegisterAPIEndpoints() ssoSettingsApi.RegisterAPIEndpoints()
@ -63,9 +66,9 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl,
return svc 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) dbSettings, err := s.store.Get(ctx, provider)
if err != nil && !errors.Is(err, ssosettings.ErrNotFound) { if err != nil && !errors.Is(err, ssosettings.ErrNotFound) {
return nil, err return nil, err
@ -87,7 +90,7 @@ func (s *SSOSettingsService) GetForProvider(ctx context.Context, provider string
return s.mergeSSOSettings(dbSettings, systemSettings), nil 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) { if !s.isProviderConfigurable(provider) {
return nil, ssosettings.ErrNotConfigurable return nil, ssosettings.ErrNotConfigurable
} }
@ -106,7 +109,7 @@ func (s *SSOSettingsService) GetForProviderWithRedactedSecrets(ctx context.Conte
return storeSettings, nil 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)) result := make([]*models.SSOSettings, 0, len(ssosettings.AllOAuthProviders))
storedSettings, err := s.store.List(ctx) storedSettings, err := s.store.List(ctx)
@ -134,7 +137,7 @@ func (s *SSOSettingsService) List(ctx context.Context) ([]*models.SSOSettings, e
return result, nil 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) storeSettings, err := s.List(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@ -158,7 +161,7 @@ func (s *SSOSettingsService) ListWithRedactedSecrets(ctx context.Context) ([]*mo
return configurableSettings, nil 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) { if !s.isProviderConfigurable(settings.Provider) {
return ssosettings.ErrNotConfigurable return ssosettings.ErrNotConfigurable
} }
@ -201,33 +204,33 @@ func (s *SSOSettingsService) Upsert(ctx context.Context, settings *models.SSOSet
return nil 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 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) { if !s.isProviderConfigurable(provider) {
return ssosettings.ErrNotConfigurable return ssosettings.ErrNotConfigurable
} }
return s.store.Delete(ctx, provider) 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 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 { if s.reloadables == nil {
s.reloadables = make(map[string]ssosettings.Reloadable) s.reloadables = make(map[string]ssosettings.Reloadable)
} }
s.reloadables[provider] = 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) 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) loadStrategy, ok := s.getFallbackStrategyFor(provider)
if !ok { if !ok {
return nil, errors.New("no fallback strategy found for provider: " + provider) 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 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 { for _, strategy := range s.fbStrategies {
if strategy.IsMatch(provider) { if strategy.IsMatch(provider) {
return strategy, true return strategy, true
@ -263,7 +266,7 @@ func (s *SSOSettingsService) getFallbackStrategyFor(provider string) (ssosetting
return nil, false 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) result := make(map[string]any)
for k, v := range settings { for k, v := range settings {
if isSecret(k) && v != "" { if isSecret(k) && v != "" {
@ -289,7 +292,7 @@ func (s *SSOSettingsService) encryptSecrets(ctx context.Context, settings, store
return result, nil return result, nil
} }
func (s *SSOSettingsService) Run(ctx context.Context) error { func (s *Service) Run(ctx context.Context) error {
interval := s.cfg.SSOSettingsReloadInterval interval := s.cfg.SSOSettingsReloadInterval
if interval == 0 { if interval == 0 {
return nil 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") s.logger.Debug("reloading SSO Settings for all providers")
settingsList, err := s.List(ctx) 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 // 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) // 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 // 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 { if dbSettings == nil {
s.logger.Debug("No SSO Settings found in the database, using system settings") s.logger.Debug("No SSO Settings found in the database, using system settings")
return systemSettings return systemSettings
@ -352,7 +355,7 @@ func (s *SSOSettingsService) mergeSSOSettings(dbSettings, systemSettings *models
return result 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 { for k, v := range settings {
if isSecret(k) && v != "" { if isSecret(k) && v != "" {
strValue, ok := v.(string) strValue, ok := v.(string)
@ -379,7 +382,7 @@ func (s *SSOSettingsService) decryptSecrets(ctx context.Context, settings map[st
return settings, nil return settings, nil
} }
func (s *SSOSettingsService) isProviderConfigurable(provider string) bool { func (s *Service) isProviderConfigurable(provider string) bool {
_, ok := s.cfg.SSOSettingsConfigurableProviders[provider] _, ok := s.cfg.SSOSettingsConfigurableProviders[provider]
return ok return ok
} }

View File

@ -24,7 +24,7 @@ import (
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
func TestSSOSettingsService_GetForProvider(t *testing.T) { func TestService_GetForProvider(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
setup func(env testEnv) 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 { testCases := []struct {
name string name string
setup func(env testEnv) 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 { testCases := []struct {
name string name string
setup func(env testEnv) 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 { testCases := []struct {
name string name string
setup func(env testEnv) 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) { t.Run("successfully upsert SSO settings", func(t *testing.T) {
env := setupTestEnv(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) { t.Run("successfully delete SSO settings", func(t *testing.T) {
env := setupTestEnv(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) { t.Run("successfully reload settings", func(t *testing.T) {
env := setupTestEnv(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 { testCases := []struct {
name string name string
setup func(env testEnv) setup func(env testEnv)
@ -1216,7 +1216,7 @@ func setupTestEnv(t *testing.T) testEnv {
}, },
} }
svc := &SSOSettingsService{ svc := &Service{
logger: log.NewNopLogger(), logger: log.NewNopLogger(),
cfg: cfg, cfg: cfg,
store: store, store: store,
@ -1239,7 +1239,7 @@ func setupTestEnv(t *testing.T) testEnv {
type testEnv struct { type testEnv struct {
cfg *setting.Cfg cfg *setting.Cfg
service *SSOSettingsService service *Service
store *ssosettingstests.FakeStore store *ssosettingstests.FakeStore
ac accesscontrol.AccessControl ac accesscontrol.AccessControl
fallbackStrategy *ssosettingstests.FakeFallbackStrategy fallbackStrategy *ssosettingstests.FakeFallbackStrategy

View File

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

View File

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