[release-10.4.18] Auth: Fix SAML user IsExternallySynced not being set correctly (#98487) (#103177)

* Auth: Fix SAML user IsExternallySynced not being set correctly (#98487)

(cherry picked from commit 345757c3ae)

* Test fixes

* lint

---------

Co-authored-by: xavi <114113189+volcanonoodle@users.noreply.github.com>
This commit is contained in:
Misi
2025-04-02 10:16:48 +02:00
committed by GitHub
co-authored by xavi
parent c3b1d0bdef
commit ad990cf8e1
11 changed files with 321 additions and 242 deletions
+2 -2
View File
@@ -129,7 +129,7 @@ func (hs *HTTPServer) AdminUpdateUserPassword(c *contextmodel.ReqContext) respon
getAuthQuery := login.GetAuthInfoQuery{UserId: usr.ID}
if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil {
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
if login.IsProviderEnabled(hs.Cfg, authInfo.AuthModule, oauthInfo) {
if hs.isProviderEnabled(hs.Cfg, authInfo.AuthModule, oauthInfo) {
return response.Error(http.StatusBadRequest, "Cannot update external user password", err)
}
}
@@ -188,7 +188,7 @@ func (hs *HTTPServer) AdminUpdateUserPermissions(c *contextmodel.ReqContext) res
getAuthQuery := login.GetAuthInfoQuery{UserId: userID}
if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil && authInfo != nil {
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
if login.IsGrafanaAdminExternallySynced(hs.Cfg, oauthInfo, authInfo.AuthModule) {
if hs.isGrafanaAdminExternallySynced(hs.Cfg, authInfo.AuthModule, oauthInfo) {
return response.Error(http.StatusForbidden, "Cannot change Grafana Admin role for externally synced user", nil)
}
}
+82
View File
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/network"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/middleware/cookies"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/identity"
@@ -22,6 +23,7 @@ import (
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil"
)
@@ -348,6 +350,14 @@ func (hs *HTTPServer) samlAutoLoginEnabled() bool {
return hs.samlEnabled() && hs.SettingsProvider.KeyValue("auth.saml", "auto_login").MustBool(false)
}
func (hs *HTTPServer) samlSkipOrgRoleSyncEnabled() bool {
return hs.samlEnabled() && hs.SettingsProvider.KeyValue("auth.saml", "skip_org_role_sync").MustBool(false)
}
func (hs *HTTPServer) samlAllowAssignGrafanaAdminEnabled() bool {
return hs.samlEnabled() && hs.SettingsProvider.KeyValue("auth.saml", "role_values_grafana_admin").MustString("") != ""
}
func getLoginExternalError(err error) string {
var createTokenErr *auth.CreateTokenErr
if errors.As(err, &createTokenErr) {
@@ -377,3 +387,75 @@ func getFirstPublicErrorMessage(err *errutil.Error) string {
return errPublic.Message
}
// isExternalySynced is used to tell if the user roles are externally synced
// true means that the org role sync is handled by Grafana
// Note: currently the users authinfo is overridden each time the user logs in
// https://github.com/grafana/grafana/blob/4181acec72f76df7ad02badce13769bae4a1f840/pkg/services/login/authinfoservice/database/database.go#L61
// this means that if the user has multiple auth providers and one of them is set to sync org roles
// then isExternallySynced will be true for this one provider and false for the others
func (hs *HTTPServer) isExternallySynced(cfg *setting.Cfg, authModule string, oauthInfo *social.OAuthInfo) bool {
// provider enabled in config
if !hs.isProviderEnabled(cfg, authModule, oauthInfo) {
return false
}
// first check SAML, LDAP and JWT
switch authModule {
case loginservice.SAMLAuthModule:
return !hs.samlSkipOrgRoleSyncEnabled()
case loginservice.LDAPAuthModule:
return !cfg.LDAPSkipOrgRoleSync
case loginservice.JWTModule:
return !cfg.JWTAuth.SkipOrgRoleSync
}
if cfg.OAuthSkipOrgRoleUpdateSync {
return false
}
switch authModule {
case loginservice.GoogleAuthModule, loginservice.OktaAuthModule, loginservice.AzureADAuthModule, loginservice.GitLabAuthModule, loginservice.GithubAuthModule, loginservice.GrafanaComAuthModule, loginservice.GenericOAuthModule:
if oauthInfo == nil {
return false
}
return !oauthInfo.SkipOrgRoleSync
}
return true
}
// isGrafanaAdminExternallySynced returns true if Grafana server admin role is being managed by an external auth provider, and false otherwise.
// Grafana admin role sync is available for SAML, JWT, OAuth providers and LDAP.
// For JWT and OAuth providers there is an additional config option `allow_assign_grafana_admin` that has to be enabled for Grafana Admin role to be synced.
func (hs *HTTPServer) isGrafanaAdminExternallySynced(cfg *setting.Cfg, authModule string, oauthInfo *social.OAuthInfo) bool {
if !hs.isExternallySynced(cfg, authModule, oauthInfo) {
return false
}
switch authModule {
case loginservice.JWTModule:
return cfg.JWTAuth.AllowAssignGrafanaAdmin
case loginservice.SAMLAuthModule:
return hs.samlAllowAssignGrafanaAdminEnabled()
case loginservice.LDAPAuthModule:
return true
default:
return oauthInfo != nil && oauthInfo.AllowAssignGrafanaAdmin
}
}
func (hs *HTTPServer) isProviderEnabled(cfg *setting.Cfg, authModule string, oauthInfo *social.OAuthInfo) bool {
switch authModule {
case loginservice.SAMLAuthModule:
return hs.samlEnabled()
case loginservice.LDAPAuthModule:
return cfg.LDAPAuthEnabled
case loginservice.JWTModule:
return cfg.JWTAuth.Enabled
case loginservice.GoogleAuthModule, loginservice.OktaAuthModule, loginservice.AzureADAuthModule, loginservice.GitLabAuthModule, loginservice.GithubAuthModule, loginservice.GrafanaComAuthModule, loginservice.GenericOAuthModule:
if oauthInfo == nil {
return false
}
return oauthInfo.Enabled
}
return false
}
+160
View File
@@ -680,6 +680,166 @@ func TestLogoutSaml(t *testing.T) {
require.Equal(t, 302, sc.resp.Code)
}
func TestIsExternallySynced(t *testing.T) {
testcases := []struct {
name string
cfg *setting.Cfg
rawCfg func(*testing.T, *setting.Cfg)
oauthInfo *social.OAuthInfo
provider string
expected bool
}{
// Same for all of the OAuth providers
{
name: "AzureAD external user should return that it is externally synced",
cfg: &setting.Cfg{},
oauthInfo: &social.OAuthInfo{Enabled: true, SkipOrgRoleSync: false},
provider: loginservice.AzureADAuthModule,
expected: true,
},
{
name: "AzureAD external user should return that it is not externally synced when org role sync is set",
cfg: &setting.Cfg{},
oauthInfo: &social.OAuthInfo{Enabled: true, SkipOrgRoleSync: true},
provider: loginservice.AzureADAuthModule,
expected: false,
},
// FIXME: remove this test as soon as we remove the deprecated setting for skipping org role sync for all external oauth providers
{
name: "AzureAD external user should return that it is not externally synced when oauth org role sync is set",
cfg: &setting.Cfg{OAuthSkipOrgRoleUpdateSync: true},
oauthInfo: &social.OAuthInfo{Enabled: true, SkipOrgRoleSync: false},
provider: loginservice.AzureADAuthModule,
expected: false,
},
{
name: "AzureAD external user should return that it is not externally synced when the provider is not enabled",
cfg: &setting.Cfg{},
oauthInfo: &social.OAuthInfo{Enabled: false, SkipOrgRoleSync: false},
provider: loginservice.AzureADAuthModule,
expected: false,
},
{
name: "AzureAD synced user should return that it is not externally synced when the provider is not enabled and nil",
cfg: &setting.Cfg{},
oauthInfo: nil,
provider: loginservice.AzureADAuthModule,
expected: false,
},
// saml
{
name: "SAML synced user should return that it is externally synced",
rawCfg: func(t *testing.T, cfg *setting.Cfg) {
saml, err := cfg.Raw.NewSection("auth.saml")
require.NoError(t, err)
_, err = saml.NewKey("enabled", "true")
require.NoError(t, err)
_, err = saml.NewKey("skip_org_role_sync", "false")
require.NoError(t, err)
},
provider: loginservice.SAMLAuthModule,
expected: true,
},
{
name: "SAML synced user should return that it is not externally synced when org role sync is set",
cfg: &setting.Cfg{SAMLAuthEnabled: true, SAMLSkipOrgRoleSync: true},
rawCfg: func(t *testing.T, cfg *setting.Cfg) {
saml, err := cfg.Raw.NewSection("auth.saml")
require.NoError(t, err)
_, err = saml.NewKey("enabled", "true")
require.NoError(t, err)
_, err = saml.NewKey("skip_org_role_sync", "true")
require.NoError(t, err)
},
provider: loginservice.SAMLAuthModule,
expected: false,
},
// ldap
{
name: "LDAP synced user should return that it is externally synced",
cfg: &setting.Cfg{LDAPAuthEnabled: true, LDAPSkipOrgRoleSync: false},
provider: loginservice.LDAPAuthModule,
expected: true,
},
{
name: "LDAP synced user should return that it is not externally synced when org role sync is set",
cfg: &setting.Cfg{LDAPAuthEnabled: true, LDAPSkipOrgRoleSync: true},
provider: loginservice.LDAPAuthModule,
expected: false,
},
// jwt
{
name: "JWT synced user should return that it is externally synced",
cfg: &setting.Cfg{JWTAuth: setting.AuthJWTSettings{Enabled: true, SkipOrgRoleSync: false}},
provider: loginservice.JWTModule,
expected: true,
},
{
name: "JWT synced user should return that it is not externally synced when org role sync is set",
cfg: &setting.Cfg{JWTAuth: setting.AuthJWTSettings{Enabled: true, SkipOrgRoleSync: true}},
provider: loginservice.JWTModule,
expected: false,
},
// IsProvider test
{
name: "If no provider enabled should return false",
cfg: &setting.Cfg{JWTAuth: setting.AuthJWTSettings{Enabled: false, SkipOrgRoleSync: true}},
provider: loginservice.JWTModule,
expected: false,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
license := licensingtest.NewFakeLicensing()
license.On("FeatureEnabled", "saml").Return(true).Maybe()
cfg := setting.NewCfg()
if tc.rawCfg != nil {
tc.rawCfg(t, cfg)
} else {
cfg = tc.cfg
}
hs := &HTTPServer{
SettingsProvider: setting.ProvideProvider(cfg),
License: license,
}
assert.Equal(t, tc.expected, hs.isExternallySynced(tc.cfg, tc.provider, tc.oauthInfo))
})
}
}
func TestIsProviderEnabled(t *testing.T) {
testcases := []struct {
name string
oauthInfo *social.OAuthInfo
provider string
expected bool
}{
// github
{
name: "Github should return true if enabled",
oauthInfo: &social.OAuthInfo{Enabled: true},
provider: loginservice.GithubAuthModule,
expected: true,
},
{
name: "Github should return false if not enabled",
oauthInfo: &social.OAuthInfo{Enabled: false},
provider: loginservice.GithubAuthModule,
expected: false,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
hs := &HTTPServer{}
assert.Equal(t, tc.expected, hs.isProviderEnabled(setting.NewCfg(), tc.provider, tc.oauthInfo))
})
}
}
type mockSocialService struct {
oAuthInfo *social.OAuthInfo
oAuthInfos map[string]*social.OAuthInfo
+2 -2
View File
@@ -336,7 +336,7 @@ func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *or
if module, ok := modules[filteredUsers[i].UserID]; ok {
oauthInfo := hs.SocialService.GetOAuthInfoProvider(module)
filteredUsers[i].AuthLabels = []string{login.GetAuthProviderLabel(module)}
filteredUsers[i].IsExternallySynced = login.IsExternallySynced(hs.Cfg, module, oauthInfo)
filteredUsers[i].IsExternallySynced = hs.isExternallySynced(hs.Cfg, module, oauthInfo)
}
}
@@ -424,7 +424,7 @@ func (hs *HTTPServer) updateOrgUserHelper(c *contextmodel.ReqContext, cmd org.Up
}
if authInfo != nil && authInfo.AuthModule != "" {
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
if login.IsExternallySynced(hs.Cfg, authInfo.AuthModule, oauthInfo) {
if hs.isExternallySynced(hs.Cfg, authInfo.AuthModule, oauthInfo) {
return response.Err(org.ErrCannotChangeRoleForExternallySyncedUser.Errorf("Cannot change role for externally synced user"))
}
}
+2 -2
View File
@@ -41,7 +41,7 @@ func (hs *HTTPServer) SendResetPasswordEmail(c *contextmodel.ReqContext) respons
getAuthQuery := login.GetAuthInfoQuery{UserId: usr.ID}
if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil {
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
if login.IsProviderEnabled(hs.Cfg, authInfo.AuthModule, oauthInfo) {
if hs.isProviderEnabled(hs.Cfg, authInfo.AuthModule, oauthInfo) {
c.Logger.Info("Requested password reset for external user", nil)
return response.Error(http.StatusOK, "Email sent", nil)
}
@@ -88,7 +88,7 @@ func (hs *HTTPServer) ResetPassword(c *contextmodel.ReqContext) response.Respons
getAuthQuery := login.GetAuthInfoQuery{UserId: userResult.ID}
if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil {
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
if login.IsProviderEnabled(hs.Cfg, authInfo.AuthModule, oauthInfo) {
if hs.isProviderEnabled(hs.Cfg, authInfo.AuthModule, oauthInfo) {
return response.Error(http.StatusBadRequest, "Cannot update external user password", err)
}
}
+3 -17
View File
@@ -79,8 +79,8 @@ func (hs *HTTPServer) getUserUserProfile(c *contextmodel.ReqContext, userID int6
userProfile.IsExternal = true
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
userProfile.IsExternallySynced = login.IsExternallySynced(hs.Cfg, authInfo.AuthModule, oauthInfo)
userProfile.IsGrafanaAdminExternallySynced = login.IsGrafanaAdminExternallySynced(hs.Cfg, oauthInfo, authInfo.AuthModule)
userProfile.IsExternallySynced = hs.isExternallySynced(hs.Cfg, authInfo.AuthModule, oauthInfo)
userProfile.IsGrafanaAdminExternallySynced = hs.isGrafanaAdminExternallySynced(hs.Cfg, authInfo.AuthModule, oauthInfo)
}
userProfile.AccessControl = hs.getAccessControlMetadata(c, c.SignedInUser.GetOrgID(), "global.users:id:", strconv.FormatInt(userID, 10))
@@ -376,20 +376,6 @@ func (hs *HTTPServer) UpdateUserEmail(c *contextmodel.ReqContext) response.Respo
return response.Redirect(hs.Cfg.AppSubURL + "/profile")
}
func (hs *HTTPServer) isExternalUser(ctx context.Context, userID int64) (bool, error) {
getAuthQuery := login.GetAuthInfoQuery{UserId: userID}
var err error
if _, err = hs.authInfoService.GetAuthInfo(ctx, &getAuthQuery); err == nil {
return true, nil
}
if errors.Is(err, user.ErrUserNotFound) {
return false, nil
}
return false, err
}
// swagger:route GET /user/orgs signed_in_user getSignedInUserOrgList
//
// Organizations of the actual User.
@@ -624,7 +610,7 @@ func (hs *HTTPServer) ChangeUserPassword(c *contextmodel.ReqContext) response.Re
getAuthQuery := login.GetAuthInfoQuery{UserId: usr.ID}
if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil {
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
if login.IsProviderEnabled(hs.Cfg, authInfo.AuthModule, oauthInfo) {
if hs.isProviderEnabled(hs.Cfg, authInfo.AuthModule, oauthInfo) {
return response.Error(http.StatusBadRequest, "Cannot update external user password", err)
}
}
+51 -4
View File
@@ -11,6 +11,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
tempuser "github.com/grafana/grafana/pkg/services/temp_user"
@@ -234,6 +235,7 @@ func Test_GetUserByID(t *testing.T) {
authEnabled bool
skipOrgRoleSync bool
expectedIsGrafanaAdminSynced bool
expectedIsExternallySynced bool
}{
{
name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced OAuth user if Grafana Admin role is not synced",
@@ -242,6 +244,7 @@ func Test_GetUserByID(t *testing.T) {
allowAssignGrafanaAdmin: false,
skipOrgRoleSync: false,
expectedIsGrafanaAdminSynced: false,
expectedIsExternallySynced: true,
},
{
name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced OAuth user if OAuth provider is not enabled",
@@ -250,6 +253,7 @@ func Test_GetUserByID(t *testing.T) {
allowAssignGrafanaAdmin: true,
skipOrgRoleSync: false,
expectedIsGrafanaAdminSynced: false,
expectedIsExternallySynced: false,
},
{
name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced OAuth user if org roles are not being synced",
@@ -258,6 +262,7 @@ func Test_GetUserByID(t *testing.T) {
allowAssignGrafanaAdmin: true,
skipOrgRoleSync: true,
expectedIsGrafanaAdminSynced: false,
expectedIsExternallySynced: false,
},
{
name: "Should return IsGrafanaAdminExternallySynced = true for an externally synced OAuth user",
@@ -266,6 +271,7 @@ func Test_GetUserByID(t *testing.T) {
allowAssignGrafanaAdmin: true,
skipOrgRoleSync: false,
expectedIsGrafanaAdminSynced: true,
expectedIsExternallySynced: true,
},
{
name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced JWT user if Grafana Admin role is not synced",
@@ -274,6 +280,7 @@ func Test_GetUserByID(t *testing.T) {
allowAssignGrafanaAdmin: false,
skipOrgRoleSync: false,
expectedIsGrafanaAdminSynced: false,
expectedIsExternallySynced: true,
},
{
name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced JWT user if JWT provider is not enabled",
@@ -282,6 +289,7 @@ func Test_GetUserByID(t *testing.T) {
allowAssignGrafanaAdmin: true,
skipOrgRoleSync: false,
expectedIsGrafanaAdminSynced: false,
expectedIsExternallySynced: false,
},
{
name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced JWT user if org roles are not being synced",
@@ -290,6 +298,7 @@ func Test_GetUserByID(t *testing.T) {
allowAssignGrafanaAdmin: true,
skipOrgRoleSync: true,
expectedIsGrafanaAdminSynced: false,
expectedIsExternallySynced: false,
},
{
name: "Should return IsGrafanaAdminExternallySynced = true for an externally synced JWT user",
@@ -298,6 +307,31 @@ func Test_GetUserByID(t *testing.T) {
allowAssignGrafanaAdmin: true,
skipOrgRoleSync: false,
expectedIsGrafanaAdminSynced: true,
expectedIsExternallySynced: true,
},
{
name: "Should return IsExternallySynced = true for an externally synced SAML user",
authModule: login.SAMLAuthModule,
authEnabled: true,
skipOrgRoleSync: false,
expectedIsGrafanaAdminSynced: false,
expectedIsExternallySynced: true,
},
{
name: "Should return IsExternallySynced = false for an externally synced SAML user if SAML provider is not enabled",
authModule: login.SAMLAuthModule,
authEnabled: false,
skipOrgRoleSync: false,
expectedIsGrafanaAdminSynced: false,
expectedIsExternallySynced: false,
},
{
name: "Should return IsExternallySynced = false for an externally synced SAML user if if org roles are not being synced",
authModule: login.SAMLAuthModule,
authEnabled: true,
skipOrgRoleSync: true,
expectedIsGrafanaAdminSynced: false,
expectedIsExternallySynced: false,
},
}
for _, tc := range testcases {
@@ -315,13 +349,25 @@ func Test_GetUserByID(t *testing.T) {
cfg.JWTAuth.Enabled = tc.authEnabled
cfg.JWTAuth.SkipOrgRoleSync = tc.skipOrgRoleSync
cfg.JWTAuth.AllowAssignGrafanaAdmin = tc.allowAssignGrafanaAdmin
case login.SAMLAuthModule:
saml, err := cfg.Raw.NewSection("auth.saml")
require.NoError(t, err)
_, err = saml.NewKey("enabled", fmt.Sprintf("%t", tc.authEnabled))
require.NoError(t, err)
_, err = saml.NewKey("skip_org_role_sync", fmt.Sprintf("%t", tc.skipOrgRoleSync))
require.NoError(t, err)
}
license := licensingtest.NewFakeLicensing()
license.On("FeatureEnabled", "saml").Return(true).Maybe()
hs := &HTTPServer{
Cfg: cfg,
authInfoService: authInfoService,
SocialService: socialService,
userService: userService,
Cfg: cfg,
authInfoService: authInfoService,
SocialService: socialService,
userService: userService,
SettingsProvider: setting.ProvideProvider(cfg),
License: license,
}
sc := setupScenarioContext(t, "/api/users/1")
@@ -339,6 +385,7 @@ func Test_GetUserByID(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, tc.expectedIsGrafanaAdminSynced, resp.IsGrafanaAdminExternallySynced)
assert.Equal(t, tc.expectedIsExternallySynced, resp.IsExternallySynced)
})
}
}
+18
View File
@@ -1,10 +1,14 @@
package api
import (
"context"
"errors"
"net/mail"
"github.com/grafana/grafana/pkg/middleware/cookies"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/user"
)
func (hs *HTTPServer) GetRedirectURL(c *contextmodel.ReqContext) string {
@@ -20,6 +24,20 @@ func (hs *HTTPServer) GetRedirectURL(c *contextmodel.ReqContext) string {
return redirectURL
}
func (hs *HTTPServer) isExternalUser(ctx context.Context, userID int64) (bool, error) {
getAuthQuery := login.GetAuthInfoQuery{UserId: userID}
var err error
if _, err = hs.authInfoService.GetAuthInfo(ctx, &getAuthQuery); err == nil {
return true, nil
}
if errors.Is(err, user.ErrUserNotFound) {
return false, nil
}
return false, err
}
func ValidateAndNormalizeEmail(email string) (string, error) {
if email == "" {
return "", nil
+1
View File
@@ -18,6 +18,7 @@ type FakeService struct {
ExpectedErrs []error
ExpectedIdentities []*authn.Identity
CurrentIndex int
EnabledClients []string
}
func (f *FakeService) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
-76
View File
@@ -2,9 +2,6 @@ package login
import (
"context"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/setting"
)
type AuthInfoService interface {
@@ -58,79 +55,6 @@ const (
OktaLabel = "Okta"
)
// IsExternnalySynced is used to tell if the user roles are externally synced
// true means that the org role sync is handled by Grafana
// Note: currently the users authinfo is overridden each time the user logs in
// https://github.com/grafana/grafana/blob/4181acec72f76df7ad02badce13769bae4a1f840/pkg/services/login/authinfoservice/database/database.go#L61
// this means that if the user has multiple auth providers and one of them is set to sync org roles
// then IsExternallySynced will be true for this one provider and false for the others
func IsExternallySynced(cfg *setting.Cfg, authModule string, oauthInfo *social.OAuthInfo) bool {
// provider enabled in config
if !IsProviderEnabled(cfg, authModule, oauthInfo) {
return false
}
// first check SAML, LDAP and JWT
switch authModule {
case SAMLAuthModule:
return !cfg.SAMLSkipOrgRoleSync
case LDAPAuthModule:
return !cfg.LDAPSkipOrgRoleSync
case JWTModule:
return !cfg.JWTAuth.SkipOrgRoleSync
}
// then check the rest of the oauth providers
// FIXME: remove this once we remove the setting
// is a deprecated setting that is used to skip org role sync for all external oauth providers
if cfg.OAuthSkipOrgRoleUpdateSync {
return false
}
switch authModule {
case GoogleAuthModule, OktaAuthModule, AzureADAuthModule, GitLabAuthModule, GithubAuthModule, GrafanaComAuthModule, GenericOAuthModule:
if oauthInfo == nil {
return false
}
return !oauthInfo.SkipOrgRoleSync
}
return true
}
// IsGrafanaAdminExternallySynced returns true if Grafana server admin role is being managed by an external auth provider, and false otherwise.
// Grafana admin role sync is available for JWT, OAuth providers and LDAP.
// For JWT and OAuth providers there is an additional config option `allow_assign_grafana_admin` that has to be enabled for Grafana Admin role to be synced.
func IsGrafanaAdminExternallySynced(cfg *setting.Cfg, oauthInfo *social.OAuthInfo, authModule string) bool {
if !IsExternallySynced(cfg, authModule, oauthInfo) {
return false
}
switch authModule {
case JWTModule:
return cfg.JWTAuth.AllowAssignGrafanaAdmin
case SAMLAuthModule:
return cfg.SAMLRoleValuesGrafanaAdmin != ""
case LDAPAuthModule:
return true
default:
return oauthInfo != nil && oauthInfo.AllowAssignGrafanaAdmin
}
}
func IsProviderEnabled(cfg *setting.Cfg, authModule string, oauthInfo *social.OAuthInfo) bool {
switch authModule {
case SAMLAuthModule:
return cfg.SAMLAuthEnabled
case LDAPAuthModule:
return cfg.LDAPAuthEnabled
case JWTModule:
return cfg.JWTAuth.Enabled
case GoogleAuthModule, OktaAuthModule, AzureADAuthModule, GitLabAuthModule, GithubAuthModule, GrafanaComAuthModule, GenericOAuthModule:
if oauthInfo == nil {
return false
}
return oauthInfo.Enabled
}
return false
}
// used for frontend to display a more user friendly label
func GetAuthProviderLabel(authModule string) string {
switch authModule {
-139
View File
@@ -1,139 +0,0 @@
package login
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/setting"
)
func TestIsExternallySynced(t *testing.T) {
testcases := []struct {
name string
cfg *setting.Cfg
oauthInfo *social.OAuthInfo
provider string
expected bool
}{
// Same for all of the OAuth providers
{
name: "AzureAD external user should return that it is externally synced",
cfg: &setting.Cfg{},
oauthInfo: &social.OAuthInfo{Enabled: true, SkipOrgRoleSync: false},
provider: AzureADAuthModule,
expected: true,
},
{
name: "AzureAD external user should return that it is not externally synced when org role sync is set",
cfg: &setting.Cfg{},
oauthInfo: &social.OAuthInfo{Enabled: true, SkipOrgRoleSync: true},
provider: AzureADAuthModule,
expected: false,
},
// FIXME: remove this test as soon as we remove the deprecated setting for skipping org role sync for all external oauth providers
{
name: "AzureAD external user should return that it is not externally synced when oauth org role sync is set",
cfg: &setting.Cfg{OAuthSkipOrgRoleUpdateSync: true},
oauthInfo: &social.OAuthInfo{Enabled: true, SkipOrgRoleSync: false},
provider: AzureADAuthModule,
expected: false,
},
{
name: "AzureAD external user should return that it is not externally synced when the provider is not enabled",
cfg: &setting.Cfg{},
oauthInfo: &social.OAuthInfo{Enabled: false, SkipOrgRoleSync: false},
provider: AzureADAuthModule,
expected: false,
},
{
name: "AzureAD synced user should return that it is not externally synced when the provider is not enabled and nil",
cfg: &setting.Cfg{},
oauthInfo: nil,
provider: AzureADAuthModule,
expected: false,
},
// saml
{
name: "SAML synced user should return that it is externally synced",
cfg: &setting.Cfg{SAMLAuthEnabled: true, SAMLSkipOrgRoleSync: false},
provider: SAMLAuthModule,
expected: true,
},
{
name: "SAML synced user should return that it is not externally synced when org role sync is set",
cfg: &setting.Cfg{SAMLAuthEnabled: true, SAMLSkipOrgRoleSync: true},
provider: SAMLAuthModule,
expected: false,
},
// ldap
{
name: "LDAP synced user should return that it is externally synced",
cfg: &setting.Cfg{LDAPAuthEnabled: true, LDAPSkipOrgRoleSync: false},
provider: LDAPAuthModule,
expected: true,
},
{
name: "LDAP synced user should return that it is not externally synced when org role sync is set",
cfg: &setting.Cfg{LDAPAuthEnabled: true, LDAPSkipOrgRoleSync: true},
provider: LDAPAuthModule,
expected: false,
},
// jwt
{
name: "JWT synced user should return that it is externally synced",
cfg: &setting.Cfg{JWTAuth: setting.AuthJWTSettings{Enabled: true, SkipOrgRoleSync: false}},
provider: JWTModule,
expected: true,
},
{
name: "JWT synced user should return that it is not externally synced when org role sync is set",
cfg: &setting.Cfg{JWTAuth: setting.AuthJWTSettings{Enabled: true, SkipOrgRoleSync: true}},
provider: JWTModule,
expected: false,
},
// IsProvider test
{
name: "If no provider enabled should return false",
cfg: &setting.Cfg{JWTAuth: setting.AuthJWTSettings{Enabled: false, SkipOrgRoleSync: true}},
provider: JWTModule,
expected: false,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, IsExternallySynced(tc.cfg, tc.provider, tc.oauthInfo))
})
}
}
func TestIsProviderEnabled(t *testing.T) {
testcases := []struct {
name string
oauthInfo *social.OAuthInfo
provider string
expected bool
}{
// github
{
name: "Github should return true if enabled",
oauthInfo: &social.OAuthInfo{Enabled: true},
provider: GithubAuthModule,
expected: true,
},
{
name: "Github should return false if not enabled",
oauthInfo: &social.OAuthInfo{Enabled: false},
provider: GithubAuthModule,
expected: false,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, IsProviderEnabled(setting.NewCfg(), tc.provider, tc.oauthInfo))
})
}
}