[release-12.0.2] SCIM: Change SCIM hook registration (#106255)

SCIM: Change SCIM hook registration (#106200)

* Add function to skip provisioning hook

* Rework provisioning hook to PostLoginHook

* Revert back to PostAuthHook and remove unused tests

* Fix tests

(cherry picked from commit 374bd5bec7)

Co-authored-by: linoman <2051016+linoman@users.noreply.github.com>
pull/106290/head
grafana-delivery-bot[bot] 4 weeks ago committed by GitHub
parent 9ca8c572e3
commit 94ee07eebf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      pkg/services/authn/authnimpl/registration.go
  2. 57
      pkg/services/authn/authnimpl/sync/user_sync.go
  3. 111
      pkg/services/authn/authnimpl/sync/user_sync_test.go

@ -135,12 +135,15 @@ func ProvideRegistration(
orgSync := sync.ProvideOrgSync(userService, orgService, accessControlService, cfg, tracer) orgSync := sync.ProvideOrgSync(userService, orgService, accessControlService, cfg, tracer)
authnSvc.RegisterPostAuthHook(userSync.SyncUserHook, 10) authnSvc.RegisterPostAuthHook(userSync.SyncUserHook, 10)
authnSvc.RegisterPostAuthHook(userSync.EnableUserHook, 20) authnSvc.RegisterPostAuthHook(userSync.EnableUserHook, 20)
authnSvc.RegisterPostAuthHook(userSync.ValidateUserProvisioningHook, 30)
authnSvc.RegisterPostAuthHook(orgSync.SyncOrgRolesHook, 40) authnSvc.RegisterPostAuthHook(orgSync.SyncOrgRolesHook, 40)
authnSvc.RegisterPostAuthHook(userSync.SyncLastSeenHook, 130) authnSvc.RegisterPostAuthHook(userSync.SyncLastSeenHook, 130)
authnSvc.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService, tracer, features).SyncOauthTokenHook, 60) authnSvc.RegisterPostAuthHook(sync.ProvideOAuthTokenSync(oauthTokenService, sessionService, socialService, tracer, features).SyncOauthTokenHook, 60)
authnSvc.RegisterPostAuthHook(userSync.FetchSyncedUserHook, 100) authnSvc.RegisterPostAuthHook(userSync.FetchSyncedUserHook, 100)
if features.IsEnabledGlobally(featuremgmt.FlagEnableSCIM) {
authnSvc.RegisterPostAuthHook(userSync.ValidateUserProvisioningHook, 30)
}
rbacSync := sync.ProvideRBACSync(accessControlService, tracer, permRegistry) rbacSync := sync.ProvideRBACSync(accessControlService, tracer, permRegistry)
if features.IsEnabledGlobally(featuremgmt.FlagCloudRBACRoles) { if features.IsEnabledGlobally(featuremgmt.FlagCloudRBACRoles) {
authnSvc.RegisterPostAuthHook(rbacSync.SyncCloudRoles, 110) authnSvc.RegisterPostAuthHook(rbacSync.SyncCloudRoles, 110)

@ -111,28 +111,20 @@ type UserSync struct {
} }
// ValidateUserProvisioningHook validates if a user should be allowed access based on provisioning status and configuration // ValidateUserProvisioningHook validates if a user should be allowed access based on provisioning status and configuration
func (s *UserSync) ValidateUserProvisioningHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error { func (s *UserSync) ValidateUserProvisioningHook(ctx context.Context, currentIdentity *authn.Identity, _ *authn.Request) error {
log := s.log.FromContext(ctx).New("auth_module", id.AuthenticatedBy, "auth_id", id.AuthID) log := s.log.FromContext(ctx).New("auth_module", currentIdentity.AuthenticatedBy, "auth_id", currentIdentity.AuthID)
log.Debug("Validating user provisioning") if !currentIdentity.ClientParams.SyncUser {
ctx, span := s.tracer.Start(ctx, "user.sync.ValidateUserProvisioningHook") log.Debug("Skipping user provisioning validation, syncUser is disabled")
defer span.End()
// Skip validation if user provisioning is disabled
if !s.isUserProvisioningEnabled {
log.Debug("User provisioning is disabled, skipping validation")
return nil return nil
} }
// Skip validation if non-provisioned users are allowed log.Debug("Validating user provisioning")
if s.allowNonProvisionedUsers { ctx, span := s.tracer.Start(ctx, "user.sync.ValidateUserProvisioningHook")
log.Debug("User provisioning is enabled, but non-provisioned users are allowed, skipping validation") defer span.End()
return nil
}
// Skip validation if the auth module is GrafanaComAuthModule if s.skipProvisioningValidation(ctx, currentIdentity) {
if id.AuthenticatedBy == login.GrafanaComAuthModule { log.Debug("Skipping user provisioning validation")
log.Debug("User is authenticated via GrafanaComAuthModule, skipping validation")
return nil return nil
} }
@ -140,7 +132,7 @@ func (s *UserSync) ValidateUserProvisioningHook(ctx context.Context, id *authn.I
// we must validate the authinfo.ExternalUID with the identity.ExternalUID // we must validate the authinfo.ExternalUID with the identity.ExternalUID
// Retrieve user and authinfo from database // Retrieve user and authinfo from database
usr, authInfo, err := s.getUser(ctx, id) usr, authInfo, err := s.getUser(ctx, currentIdentity)
if err != nil { if err != nil {
if errors.Is(err, user.ErrUserNotFound) { if errors.Is(err, user.ErrUserNotFound) {
return nil return nil
@ -154,14 +146,8 @@ func (s *UserSync) ValidateUserProvisioningHook(ctx context.Context, id *authn.I
return errUnableToRetrieveUser.Errorf("unable to retrieve user for validation") return errUnableToRetrieveUser.Errorf("unable to retrieve user for validation")
} }
// Validate the provisioned user.ExternalUID with the authinfo.ExternalUID
if usr.IsProvisioned { if usr.IsProvisioned {
// Allow non-SAML requests for SAML-provisioned users to proceed if incoming ExternalUID is empty (e.g. session access). if authInfo.ExternalUID == "" || authInfo.ExternalUID != currentIdentity.ExternalUID {
if authInfo.AuthModule == login.SAMLAuthModule && id.AuthenticatedBy != login.SAMLAuthModule && authInfo.ExternalUID != "" && id.ExternalUID == "" {
log.Debug("Skipping ExternalUID validation for non-SAML request to SAML-provisioned user")
return nil
}
if authInfo.ExternalUID == "" || authInfo.ExternalUID != id.ExternalUID {
log.Error("The provisioned user.ExternalUID does not match the authinfo.ExternalUID") log.Error("The provisioned user.ExternalUID does not match the authinfo.ExternalUID")
return errUserExternalUIDMismatch.Errorf("the provisioned user.ExternalUID does not match the authinfo.ExternalUID") return errUserExternalUIDMismatch.Errorf("the provisioned user.ExternalUID does not match the authinfo.ExternalUID")
} }
@ -174,6 +160,27 @@ func (s *UserSync) ValidateUserProvisioningHook(ctx context.Context, id *authn.I
return errUserNotProvisioned.Errorf("user is not provisioned") return errUserNotProvisioned.Errorf("user is not provisioned")
} }
func (s *UserSync) skipProvisioningValidation(ctx context.Context, currentIdentity *authn.Identity) bool {
log := s.log.FromContext(ctx).New("auth_module", currentIdentity.AuthenticatedBy, "auth_id", currentIdentity.AuthID, "id", currentIdentity.ID)
if !s.isUserProvisioningEnabled {
log.Debug("User provisioning is disabled, skipping validation")
return true
}
if s.allowNonProvisionedUsers {
log.Debug("Non-provisioned users are allowed, skipping validation")
return true
}
if currentIdentity.AuthenticatedBy == login.GrafanaComAuthModule {
log.Debug("User is authenticated via GrafanaComAuthModule, skipping validation")
return true
}
return false
}
// SyncUserHook syncs a user with the database // SyncUserHook syncs a user with the database
func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error { func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error {
ctx, span := s.tracer.Start(ctx, "user.sync.SyncUserHook") ctx, span := s.tracer.Start(ctx, "user.sync.SyncUserHook")

@ -691,6 +691,21 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
} }
tests := []testCase{ tests := []testCase{
{
desc: "it should skip validation if the user identity is not syncying a user",
userSyncServiceSetup: func() *UserSync {
userSyncService := initUserSyncService()
userSyncService.isUserProvisioningEnabled = true
return userSyncService
},
identity: &authn.Identity{
ID: "1",
Type: claims.TypeAPIKey,
ClientParams: authn.ClientParams{
SyncUser: false,
},
},
},
{ {
desc: "it should skip validation if the user provisioning is disabled", desc: "it should skip validation if the user provisioning is disabled",
userSyncServiceSetup: func() *UserSync { userSyncServiceSetup: func() *UserSync {
@ -701,6 +716,9 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
identity: &authn.Identity{ identity: &authn.Identity{
AuthenticatedBy: login.GenericOAuthModule, AuthenticatedBy: login.GenericOAuthModule,
AuthID: "1", AuthID: "1",
ClientParams: authn.ClientParams{
SyncUser: true,
},
}, },
}, },
{ {
@ -714,6 +732,9 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
identity: &authn.Identity{ identity: &authn.Identity{
AuthenticatedBy: login.GenericOAuthModule, AuthenticatedBy: login.GenericOAuthModule,
AuthID: "1", AuthID: "1",
ClientParams: authn.ClientParams{
SyncUser: true,
},
}, },
}, },
{ {
@ -727,6 +748,9 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
identity: &authn.Identity{ identity: &authn.Identity{
AuthenticatedBy: login.GrafanaComAuthModule, AuthenticatedBy: login.GrafanaComAuthModule,
AuthID: "1", AuthID: "1",
ClientParams: authn.ClientParams{
SyncUser: true,
},
}, },
}, },
{ {
@ -744,6 +768,9 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
AuthenticatedBy: login.SAMLAuthModule, AuthenticatedBy: login.SAMLAuthModule,
AuthID: "1", AuthID: "1",
ExternalUID: "random-external-uid", ExternalUID: "random-external-uid",
ClientParams: authn.ClientParams{
SyncUser: true,
},
}, },
expectedErr: errUnableToRetrieveUserOrAuthInfo.Errorf("unable to retrieve user or authInfo for validation"), expectedErr: errUnableToRetrieveUserOrAuthInfo.Errorf("unable to retrieve user or authInfo for validation"),
}, },
@ -760,6 +787,9 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
AuthenticatedBy: login.SAMLAuthModule, AuthenticatedBy: login.SAMLAuthModule,
AuthID: "1", AuthID: "1",
ExternalUID: "random-external-uid", ExternalUID: "random-external-uid",
ClientParams: authn.ClientParams{
SyncUser: true,
},
}, },
expectedErr: errUnableToRetrieveUser.Errorf("unable to retrieve user for validation"), expectedErr: errUnableToRetrieveUser.Errorf("unable to retrieve user for validation"),
}, },
@ -788,6 +818,9 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
AuthenticatedBy: login.SAMLAuthModule, AuthenticatedBy: login.SAMLAuthModule,
AuthID: "1", AuthID: "1",
ExternalUID: "random-external-uid", ExternalUID: "random-external-uid",
ClientParams: authn.ClientParams{
SyncUser: true,
},
}, },
expectedErr: errUserExternalUIDMismatch.Errorf("the provisioned user.ExternalUID does not match the authinfo.ExternalUID"), expectedErr: errUserExternalUIDMismatch.Errorf("the provisioned user.ExternalUID does not match the authinfo.ExternalUID"),
}, },
@ -817,6 +850,9 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
AuthenticatedBy: login.SAMLAuthModule, AuthenticatedBy: login.SAMLAuthModule,
AuthID: "1", AuthID: "1",
ExternalUID: "random-external-uid", ExternalUID: "random-external-uid",
ClientParams: authn.ClientParams{
SyncUser: true,
},
}, },
expectedErr: errUserExternalUIDMismatch.Errorf("the provisioned user.ExternalUID does not match the authinfo.ExternalUID"), expectedErr: errUserExternalUIDMismatch.Errorf("the provisioned user.ExternalUID does not match the authinfo.ExternalUID"),
}, },
@ -846,6 +882,9 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
AuthenticatedBy: login.SAMLAuthModule, AuthenticatedBy: login.SAMLAuthModule,
AuthID: "1", AuthID: "1",
ExternalUID: "random-external-uid", ExternalUID: "random-external-uid",
ClientParams: authn.ClientParams{
SyncUser: true,
},
}, },
}, },
{ {
@ -874,38 +913,12 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
AuthenticatedBy: login.SAMLAuthModule, AuthenticatedBy: login.SAMLAuthModule,
AuthID: "1", AuthID: "1",
ExternalUID: "random-external-uid", ExternalUID: "random-external-uid",
ClientParams: authn.ClientParams{
SyncUser: true,
},
}, },
expectedErr: errUserNotProvisioned.Errorf("user is not provisioned"), expectedErr: errUserNotProvisioned.Errorf("user is not provisioned"),
}, },
{
desc: "it should skip validation if identity is incomplete because it's not from the SAML auth flow",
userSyncServiceSetup: func() *UserSync {
userSyncService := initUserSyncService()
userSyncService.allowNonProvisionedUsers = false
userSyncService.isUserProvisioningEnabled = true
userSyncService.userService = &usertest.FakeUserService{
ExpectedUser: &user.User{
ID: 1,
IsProvisioned: true,
},
}
userSyncService.authInfoService = &authinfotest.FakeService{
ExpectedUserAuth: &login.UserAuth{
UserId: 1,
AuthModule: login.SAMLAuthModule,
AuthId: "1",
ExternalUID: "random-external-uid",
},
}
return userSyncService
},
identity: &authn.Identity{
AuthenticatedBy: login.GenericOAuthModule,
AuthID: "1",
ExternalUID: "",
},
expectedErr: nil,
},
{ {
desc: "ValidateProvisioning: DB ExternalUID is empty, Incoming ExternalUID is empty - expect mismatch (stricter logic)", desc: "ValidateProvisioning: DB ExternalUID is empty, Incoming ExternalUID is empty - expect mismatch (stricter logic)",
userSyncServiceSetup: func() *UserSync { userSyncServiceSetup: func() *UserSync {
@ -915,19 +928,14 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
userSyncService.authInfoService = &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: ""}} userSyncService.authInfoService = &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: ""}}
return userSyncService return userSyncService
}, },
identity: &authn.Identity{AuthenticatedBy: login.SAMLAuthModule, AuthID: "1", ExternalUID: ""}, identity: &authn.Identity{
expectedErr: errUserExternalUIDMismatch, AuthenticatedBy: login.SAMLAuthModule,
}, AuthID: "1",
{ ClientParams: authn.ClientParams{
desc: "ValidateProvisioning: DB ExternalUID non-empty, Incoming ExternalUID is empty - expect mismatch", SyncUser: true,
userSyncServiceSetup: func() *UserSync { },
userSyncService := initUserSyncService() ExternalUID: "",
userSyncService.isUserProvisioningEnabled = true
userSyncService.userService = &usertest.FakeUserService{ExpectedUser: &user.User{ID: 1, IsProvisioned: true}}
userSyncService.authInfoService = &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: "valid-uid"}}
return userSyncService
}, },
identity: &authn.Identity{AuthenticatedBy: login.SAMLAuthModule, AuthID: "1", ExternalUID: ""},
expectedErr: errUserExternalUIDMismatch, expectedErr: errUserExternalUIDMismatch,
}, },
{ {
@ -939,7 +947,14 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
userSyncService.authInfoService = &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: ""}} userSyncService.authInfoService = &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: ""}}
return userSyncService return userSyncService
}, },
identity: &authn.Identity{AuthenticatedBy: login.SAMLAuthModule, AuthID: "1", ExternalUID: "valid-uid"}, identity: &authn.Identity{
AuthenticatedBy: login.SAMLAuthModule,
AuthID: "1",
ClientParams: authn.ClientParams{
SyncUser: true,
},
ExternalUID: "valid-uid",
},
expectedErr: errUserExternalUIDMismatch, expectedErr: errUserExternalUIDMismatch,
}, },
{ {
@ -951,7 +966,14 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
userSyncService.authInfoService = &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: "db-uid"}} userSyncService.authInfoService = &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: "db-uid"}}
return userSyncService return userSyncService
}, },
identity: &authn.Identity{AuthenticatedBy: login.SAMLAuthModule, AuthID: "1", ExternalUID: "incoming-uid"}, identity: &authn.Identity{
AuthenticatedBy: login.SAMLAuthModule,
AuthID: "1",
ClientParams: authn.ClientParams{
SyncUser: true,
},
ExternalUID: "incoming-uid",
},
expectedErr: errUserExternalUIDMismatch, expectedErr: errUserExternalUIDMismatch,
}, },
{ {
@ -1009,6 +1031,9 @@ func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
AuthenticatedBy: login.SAMLAuthModule, AuthenticatedBy: login.SAMLAuthModule,
AuthID: "1", AuthID: "1",
ExternalUID: "", ExternalUID: "",
ClientParams: authn.ClientParams{
SyncUser: true,
},
}, },
expectedErr: errUserExternalUIDMismatch.Errorf("the provisioned user.ExternalUID does not match the authinfo.ExternalUID"), expectedErr: errUserExternalUIDMismatch.Errorf("the provisioned user.ExternalUID does not match the authinfo.ExternalUID"),
}, },

Loading…
Cancel
Save