* 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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user