SAML catalog: Add metrics for saml catalog logins (#109904)

* Add samlCatalog metric

* Add samlCatalog metric to stats

* Define hook for successful SamlCatalog metrics

* Register new hook

* Add tests

* Rework the collected stats and split it into versions
This commit is contained in:
linoman 2025-08-25 16:21:10 +02:00 committed by GitHub
parent 9646a06a91
commit 539b413584
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 126 additions and 0 deletions

View File

@ -152,6 +152,7 @@ func ProvideRegistration(
authnSvc.RegisterPostAuthHook(rbacSync.SyncPermissionsHook, 120)
authnSvc.RegisterPostLoginHook(orgSync.SetDefaultOrgHook, 140)
authnSvc.RegisterPostLoginHook(userSync.CatalogLoginHook, 145)
authnSvc.RegisterPostLoginHook(rbacSync.ClearUserPermissionCacheHook, 170)
nsSync := sync.ProvideNamespaceSync(cfg)

View File

@ -5,8 +5,10 @@ import (
"errors"
"fmt"
"strconv"
"sync"
"sync/atomic"
"github.com/Masterminds/semver/v3"
claims "github.com/grafana/authlib/types"
"go.opentelemetry.io/otel/attribute"
"golang.org/x/sync/singleflight"
@ -128,6 +130,7 @@ type UserSync struct {
scimUtil *scimutil.SCIMUtil
staticConfig *StaticSCIMConfig
scimSuccessfulLogin atomic.Bool
samlCatalogStats sync.Map
}
// GetUsageStats implements registry.ProvidesUsageStats
@ -138,9 +141,43 @@ func (s *UserSync) GetUsageStats(ctx context.Context) map[string]any {
} else {
stats["stats.features.scim.has_successful_login.count"] = 0
}
s.samlCatalogStats.Range(func(key, value interface{}) bool {
version := key.(string)
flag := value.(*atomic.Bool)
if flag.Load() {
stats[fmt.Sprintf("stats.features.saml.catalog_version_%s.count", version)] = 1
} else {
stats[fmt.Sprintf("stats.features.saml.catalog_version_%s.count", version)] = 0
}
return true
})
return stats
}
func (s *UserSync) setSamlCatalogVersion(version string) {
value, loaded := s.samlCatalogStats.LoadOrStore(version, &atomic.Bool{})
flag := value.(*atomic.Bool)
flag.Store(true)
if !loaded {
s.log.Info("New SAML catalog version detected", "version", version)
}
}
func (s *UserSync) CatalogLoginHook(_ context.Context, identity *authn.Identity, r *authn.Request, err error) {
if err != nil || identity == nil || !identity.ClientParams.SyncUser || r == nil {
return
}
catalogVersion := r.GetMeta("catalog_version")
if _, err := semver.NewVersion(catalogVersion); err != nil {
s.log.Warn("The SAML catalog used for this login has an incorrect version format", "catalogVersion", catalogVersion)
return
}
s.setSamlCatalogVersion(catalogVersion)
}
// ValidateUserProvisioningHook validates if a user should be allowed access based on provisioning status and configuration
func (s *UserSync) ValidateUserProvisioningHook(ctx context.Context, currentIdentity *authn.Identity, _ *authn.Request) error {
log := s.log.FromContext(ctx).New("auth_module", currentIdentity.AuthenticatedBy, "auth_id", currentIdentity.AuthID)

View File

@ -3,6 +3,7 @@ package sync
import (
"context"
"errors"
"fmt"
"strconv"
"testing"
@ -975,6 +976,93 @@ func TestUserSync_FetchSyncedUserHook(t *testing.T) {
}
}
func TestUserSync_CatalogLoginHook(t *testing.T) {
type testCase struct {
name string
identity *authn.Identity
expectFlagSet bool
catalogVersion string
}
tests := []testCase{
{
name: "should skip hook when SyncUser flag is not enabled",
identity: &authn.Identity{
ClientParams: authn.ClientParams{
SyncUser: false,
},
},
expectFlagSet: false,
},
{
name: "should skip hook when request is nil",
identity: &authn.Identity{
ClientParams: authn.ClientParams{
SyncUser: true,
},
},
},
{
name: "should skip hook when catalog version is not set",
identity: &authn.Identity{
ClientParams: authn.ClientParams{
SyncUser: true,
},
},
expectFlagSet: false,
},
{
name: "should not set loginflag when catalog version is set incorrectly",
identity: &authn.Identity{
ClientParams: authn.ClientParams{
SyncUser: true,
},
},
catalogVersion: "v0aplha1",
expectFlagSet: false,
},
{
name: "should not set loginflag when catalog version is empty",
identity: &authn.Identity{
ClientParams: authn.ClientParams{
SyncUser: true,
},
},
expectFlagSet: false,
},
{
name: "should set successful loginflag when catalog version is set correctly",
identity: &authn.Identity{
ClientParams: authn.ClientParams{
SyncUser: true,
},
},
catalogVersion: "1.0.0",
expectFlagSet: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := UserSync{
tracer: tracing.InitializeTracerForTest(),
log: log.New("test"),
}
req := authn.Request{}
if tt.catalogVersion != "" {
req.SetMeta("catalog_version", tt.catalogVersion)
}
s.CatalogLoginHook(context.Background(), tt.identity, &req, nil)
usageStats := s.GetUsageStats(context.Background())
countIndex := fmt.Sprintf("stats.features.saml.catalog_version_%s.count", tt.catalogVersion)
countResult := usageStats[countIndex] != nil && usageStats[countIndex].(int) == 1
assert.Equal(t, tt.expectFlagSet, countResult)
})
}
}
func TestUserSync_EnableDisabledUserHook(t *testing.T) {
type testCase struct {
desc string