SCIM: Validate provisioned users (#102099)

* Validate authID when user is provisioned

* Add new `user_unique_id` to `user_auth` table

* Validate provisioned user with saml assertion

* Rework `ExternalUID`

* Validate for ExternalUID only

* Enhance verbosity

* Move ExternalUID to saml config

* Rename db variable for externalUID

* Add verbosity to debug ExternalUID

* Assign new error for ExternalUID mismatch

* Add `GetByLoginFn`

* Add new configuration to saml tests

* add validation for empty externalUID
This commit is contained in:
linoman
2025-03-24 09:28:51 +01:00
committed by GitHub
parent 07b520378f
commit cb532cafef
8 changed files with 135 additions and 85 deletions
+38 -3
View File
@@ -7,6 +7,7 @@ import (
"strconv"
claims "github.com/grafana/authlib/types"
"go.opentelemetry.io/otel/attribute"
"golang.org/x/sync/singleflight"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
@@ -45,6 +46,14 @@ var (
"user.sync.fetch-not-found",
errutil.WithPublicMessage("User not found"),
)
errMismatchedExternalUID = errutil.Unauthorized(
"user.sync.mismatched-externalUID",
errutil.WithPublicMessage("Mismatched externalUID"),
)
errEmptyExternalUID = errutil.Unauthorized(
"user.sync.empty-externalUID",
errutil.WithPublicMessage("Empty externalUID"),
)
)
var (
@@ -231,7 +240,7 @@ func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, ident
return nil
}
// If a user does not a connection to a specific auth module, create it.
// If a user does not have a connection to a specific auth module, create it.
// This can happen when: using multiple auth client where the same user exists in several or
// changing to new auth client
if createConnection {
@@ -265,6 +274,8 @@ func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id
ctx, span := s.tracer.Start(ctx, "user.sync.updateUserAttributes")
defer span.End()
needsConnectionCreation := userAuth == nil
if errProtection := s.userProtectionService.AllowUserMapping(usr, id.AuthenticatedBy); errProtection != nil {
return errUserProtection.Errorf("user mapping not allowed: %w", errProtection)
}
@@ -305,14 +316,38 @@ func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id
needsUpdate = true
}
if needsUpdate {
span.SetAttributes(
attribute.String("identity.ID", id.ID),
attribute.String("identity.ExternalUID", id.ExternalUID),
)
if usr.IsProvisioned {
s.log.Debug("User is provisioned", "id,UID", id.UID)
needsConnectionCreation = false
authInfo, err := s.authInfoService.GetAuthInfo(ctx, &login.GetAuthInfoQuery{UserId: usr.ID, AuthModule: id.AuthenticatedBy})
if err != nil {
s.log.Error("Error getting auth info", "error", err)
return err
}
if id.ExternalUID == "" {
s.log.Error("externalUID is empty", "id", id.UID)
return errEmptyExternalUID.Errorf("externalUID is empty")
}
if id.ExternalUID != authInfo.ExternalUID {
s.log.Error("mismatched externalUID", "provisioned_externalUID", authInfo.ExternalUID, "identity_externalUID", id.ExternalUID)
return errMismatchedExternalUID.Errorf("externalUID mistmatch")
}
}
if needsUpdate && !usr.IsProvisioned {
s.log.FromContext(ctx).Debug("Syncing user info", "id", id.ID, "update", fmt.Sprintf("%v", updateCmd))
if err := s.userService.Update(ctx, updateCmd); err != nil {
return err
}
}
return s.upsertAuthConnection(ctx, usr.ID, id, userAuth == nil)
return s.upsertAuthConnection(ctx, usr.ID, id, needsConnectionCreation)
}
func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.User, error) {