diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 85617774261..45333a7f24d 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -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) } } diff --git a/pkg/api/login.go b/pkg/api/login.go index 7f758150b36..d8e7d9f7abd 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -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 +} diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 35048e3cedb..b5aaae36e93 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -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 diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 2181539199d..6baa57c5b05 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -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")) } } diff --git a/pkg/api/password.go b/pkg/api/password.go index f3b265029e4..825ab252a24 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -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) } } diff --git a/pkg/api/user.go b/pkg/api/user.go index 785a9d7cae5..7d86ae380a5 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -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) } } diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index ee1ffe50417..187de6a1c4a 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -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) }) } } diff --git a/pkg/api/utils.go b/pkg/api/utils.go index 08a1c525393..55b5948f06b 100644 --- a/pkg/api/utils.go +++ b/pkg/api/utils.go @@ -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 diff --git a/pkg/services/authn/authntest/fake.go b/pkg/services/authn/authntest/fake.go index 81a93636585..07b27e2d8ed 100644 --- a/pkg/services/authn/authntest/fake.go +++ b/pkg/services/authn/authntest/fake.go @@ -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) { diff --git a/pkg/services/login/authinfo.go b/pkg/services/login/authinfo.go index 4f3d95bd931..eccf6490a30 100644 --- a/pkg/services/login/authinfo.go +++ b/pkg/services/login/authinfo.go @@ -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 { diff --git a/pkg/services/login/authinfo_test.go b/pkg/services/login/authinfo_test.go deleted file mode 100644 index 39746ef8745..00000000000 --- a/pkg/services/login/authinfo_test.go +++ /dev/null @@ -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)) - }) - } -}