diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index d0f88f50b63..a6d0f6af76f 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -98,6 +98,7 @@ Alpha features might be changed or removed without prior notice. | `alertingBacktesting` | Rule backtesting API for alerting | | `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | | `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | +| `onlyExternalOrgRoleSync` | Prohibits a user from changing organization roles synced with external auth providers | | `prometheusMetricEncyclopedia` | Replaces the Prometheus query builder metric select option with a paginated and filterable component | | `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | | `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 17bd4880dee..5c5deca216d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -91,6 +91,7 @@ export interface FeatureToggles { topNavCommandPalette?: boolean; logsSampleInExplore?: boolean; logsContextDatasourceUi?: boolean; + onlyExternalOrgRoleSync?: boolean; prometheusMetricEncyclopedia?: boolean; influxdbBackendMigration?: boolean; alertStateHistoryLokiSecondary?: boolean; diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index c102776f67d..d0bfcbe0807 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -105,7 +105,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *contextmodel.ReqContext) (map[st "appSubUrl": hs.Cfg.AppSubURL, "allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin, "authProxyEnabled": setting.AuthProxyEnabled, - "ldapEnabled": hs.Cfg.LDAPEnabled, + "ldapEnabled": hs.Cfg.LDAPAuthEnabled, "jwtHeaderName": hs.Cfg.JWTAuthHeaderName, "jwtUrlLogin": hs.Cfg.JWTAuthURLLogin, "alertingEnabled": setting.AlertingEnabled, @@ -148,7 +148,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *contextmodel.ReqContext) (map[st "OAuthSkipOrgRoleUpdateSync": hs.Cfg.OAuthSkipOrgRoleUpdateSync, "SAMLSkipOrgRoleSync": hs.Cfg.SectionWithEnvOverrides("auth.saml").Key("skip_org_role_sync").MustBool(false), "LDAPSkipOrgRoleSync": hs.Cfg.LDAPSkipOrgRoleSync, - "GithubSkipOrgRoleSync": hs.Cfg.GithubSkipOrgRoleSync, + "GithubSkipOrgRoleSync": hs.Cfg.GitHubSkipOrgRoleSync, "GoogleSkipOrgRoleSync": hs.Cfg.GoogleSkipOrgRoleSync, "JWTAuthSkipOrgRoleSync": hs.Cfg.JWTAuthSkipOrgRoleSync, "GrafanaComSkipOrgRoleSync": hs.Cfg.GrafanaComSkipOrgRoleSync, diff --git a/pkg/api/ldap_debug_test.go b/pkg/api/ldap_debug_test.go index 5e4e51fd750..69f5b30eefa 100644 --- a/pkg/api/ldap_debug_test.go +++ b/pkg/api/ldap_debug_test.go @@ -66,9 +66,9 @@ func getUserFromLDAPContext(t *testing.T, requestURL string, searchOrgRst []*org sc := setupScenarioContext(t, requestURL) - origLDAP := setting.LDAPEnabled - setting.LDAPEnabled = true - t.Cleanup(func() { setting.LDAPEnabled = origLDAP }) + origLDAP := setting.LDAPAuthEnabled + setting.LDAPAuthEnabled = true + t.Cleanup(func() { setting.LDAPAuthEnabled = origLDAP }) hs := &HTTPServer{Cfg: setting.NewCfg(), ldapGroups: ldap.ProvideGroupsService(), orgService: &orgtest.FakeOrgService{ExpectedOrgs: searchOrgRst}} @@ -313,9 +313,9 @@ func getLDAPStatusContext(t *testing.T) *scenarioContext { requestURL := "/api/admin/ldap/status" sc := setupScenarioContext(t, requestURL) - ldap := setting.LDAPEnabled - setting.LDAPEnabled = true - t.Cleanup(func() { setting.LDAPEnabled = ldap }) + ldap := setting.LDAPAuthEnabled + setting.LDAPAuthEnabled = true + t.Cleanup(func() { setting.LDAPAuthEnabled = ldap }) hs := &HTTPServer{Cfg: setting.NewCfg()} @@ -373,11 +373,11 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(* sc := setupScenarioContext(t, requestURL) sc.authInfoService = &logintest.AuthInfoServiceFake{} - ldap := setting.LDAPEnabled + ldap := setting.LDAPAuthEnabled t.Cleanup(func() { - setting.LDAPEnabled = ldap + setting.LDAPAuthEnabled = ldap }) - setting.LDAPEnabled = true + setting.LDAPAuthEnabled = true hs := &HTTPServer{ Cfg: sc.cfg, @@ -600,22 +600,22 @@ func TestLDAP_AccessControl(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - enabled := setting.LDAPEnabled + enabled := setting.LDAPAuthEnabled configFile := setting.LDAPConfigFile t.Cleanup(func() { - setting.LDAPEnabled = enabled + setting.LDAPAuthEnabled = enabled setting.LDAPConfigFile = configFile }) - setting.LDAPEnabled = true + setting.LDAPAuthEnabled = true path, err := filepath.Abs("../../conf/ldap.toml") assert.NoError(t, err) setting.LDAPConfigFile = path server := SetupAPITestServer(t, func(hs *HTTPServer) { cfg := setting.NewCfg() - cfg.LDAPEnabled = true + cfg.LDAPAuthEnabled = true hs.Cfg = cfg hs.SQLStore = dbtest.NewFakeDB() hs.orgService = orgtest.NewOrgServiceFake() diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index b0c7c807cc0..ef55ee52c71 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" @@ -296,6 +297,7 @@ func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *or userIDs[fmt.Sprint(user.UserID)] = true authLabelsUserIDs = append(authLabelsUserIDs, user.UserID) + filteredUsers = append(filteredUsers, user) } @@ -313,6 +315,7 @@ func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *or filteredUsers[i].AccessControl = accessControlMetadata[fmt.Sprint(filteredUsers[i].UserID)] if module, ok := modules[filteredUsers[i].UserID]; ok { filteredUsers[i].AuthLabels = []string{login.GetAuthProviderLabel(module)} + filteredUsers[i].IsExternallySynced = login.IsExternallySynced(hs.Cfg, module) } } @@ -386,6 +389,22 @@ func (hs *HTTPServer) updateOrgUserHelper(c *contextmodel.ReqContext, cmd org.Up if !c.OrgRole.Includes(cmd.Role) && !c.IsGrafanaAdmin { return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil) } + if hs.Features.IsEnabled(featuremgmt.FlagOnlyExternalOrgRoleSync) { + // we do not allow to change role for external synced users + qAuth := login.GetAuthInfoQuery{UserId: cmd.UserID} + err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &qAuth) + if err != nil { + if errors.Is(err, user.ErrUserNotFound) { + hs.log.Debug("Failed to get user auth info for basic auth user", cmd.UserID, nil) + } else { + hs.log.Error("Failed to get user auth info for external sync check", cmd.UserID, err) + return response.Error(http.StatusInternalServerError, "Failed to get user auth info", nil) + } + } + if qAuth.Result != nil && qAuth.Result.AuthModule != "" && login.IsExternallySynced(hs.Cfg, qAuth.Result.AuthModule) { + return response.Err(org.ErrCannotChangeRoleForExternallySyncedUser.Errorf("Cannot change role for externally synced user")) + } + } if err := hs.orgService.UpdateOrgUser(c.Req.Context(), &cmd); err != nil { if errors.Is(err, org.ErrLastOrgAdmin) { return response.Error(400, "Cannot change role so that there is no organization admin left", nil) diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index 57978021e1e..19c83b9e801 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -17,9 +17,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/org/orgtest" @@ -30,8 +34,10 @@ import ( "github.com/grafana/grafana/pkg/services/temp_user/tempuserimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/web/webtest" ) func setUpGetOrgUsersDB(t *testing.T, sqlStore *sqlstore.SQLStore) { @@ -200,6 +206,99 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) { }) } +func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) { + type testCase struct { + desc string + SkipOrgRoleSync bool + AuthEnabled bool + AuthModule string + expectedCode int + } + permissions := []accesscontrol.Permission{ + {Action: accesscontrol.ActionOrgUsersRead, Scope: "users:*"}, + {Action: accesscontrol.ActionOrgUsersWrite, Scope: "users:*"}, + {Action: accesscontrol.ActionOrgUsersAdd, Scope: "users:*"}, + {Action: accesscontrol.ActionOrgUsersRemove, Scope: "users:*"}, + } + tests := []testCase{ + { + desc: "should be able to change basicRole when skip_org_role_sync true", + SkipOrgRoleSync: true, + AuthEnabled: true, + AuthModule: login.LDAPAuthModule, + expectedCode: http.StatusOK, + }, + { + desc: "should not be able to change basicRole when skip_org_role_sync false", + SkipOrgRoleSync: false, + AuthEnabled: true, + AuthModule: login.LDAPAuthModule, + expectedCode: http.StatusForbidden, + }, + { + desc: "should not be able to change basicRole with a different provider", + SkipOrgRoleSync: false, + AuthEnabled: true, + AuthModule: login.GenericOAuthModule, + expectedCode: http.StatusForbidden, + }, + { + desc: "should be able to change basicRole with a basic Auth", + SkipOrgRoleSync: false, + AuthEnabled: false, + AuthModule: "", + expectedCode: http.StatusOK, + }, + { + desc: "should be able to change basicRole with a basic Auth", + SkipOrgRoleSync: true, + AuthEnabled: true, + AuthModule: "", + expectedCode: http.StatusOK, + }, + } + + userWithPermissions := userWithPermissions(1, permissions) + userRequesting := &user.User{ID: 2, OrgID: 1} + reqBody := `{"userId": "1", "role": "Admin", "orgId": "1"}` + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = setting.NewCfg() + hs.Cfg.LDAPAuthEnabled = tt.AuthEnabled + if tt.AuthModule == login.LDAPAuthModule { + hs.Cfg.LDAPAuthEnabled = tt.AuthEnabled + hs.Cfg.LDAPSkipOrgRoleSync = tt.SkipOrgRoleSync + } else if tt.AuthModule == login.GenericOAuthModule { + hs.Cfg.GenericOAuthAuthEnabled = tt.AuthEnabled + hs.Cfg.GenericOAuthSkipOrgRoleSync = tt.SkipOrgRoleSync + } else if tt.AuthModule == "" { + // authmodule empty means basic auth + } else { + t.Errorf("invalid auth module for test: %s", tt.AuthModule) + } + + hs.authInfoService = &logintest.AuthInfoServiceFake{ + ExpectedUserAuth: &login.UserAuth{AuthModule: tt.AuthModule}, + } + hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagOnlyExternalOrgRoleSync, true) + hs.userService = &usertest.FakeUserService{ExpectedSignedInUser: userWithPermissions} + hs.orgService = &orgtest.FakeOrgService{} + hs.accesscontrolService = &actest.FakeService{ + ExpectedPermissions: permissions, + } + }) + req := server.NewRequest(http.MethodPatch, fmt.Sprintf("/api/orgs/%d/users/%d", userRequesting.OrgID, userRequesting.ID), strings.NewReader(reqBody)) + req.Header.Set("Content-Type", "application/json") + userWithPermissions.OrgRole = roletype.RoleAdmin + res, err := server.Send(webtest.RequestWithSignedInUser(req, userWithPermissions)) + require.NoError(t, err) + assert.Equal(t, tt.expectedCode, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + } +} + func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { cfg := setting.NewCfg() cfg.RBACEnabled = false diff --git a/pkg/api/user.go b/pkg/api/user.go index 997b86881fa..b13e8d53443 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -69,6 +69,7 @@ func (hs *HTTPServer) getUserUserProfile(c *contextmodel.ReqContext, userID int6 authLabel := login.GetAuthProviderLabel(getAuthQuery.Result.AuthModule) userProfile.AuthLabels = append(userProfile.AuthLabels, authLabel) userProfile.IsExternal = true + userProfile.IsExternallySynced = login.IsExternallySynced(hs.Cfg, getAuthQuery.Result.AuthModule) } userProfile.AccessControl = hs.getAccessControlMetadata(c, c.OrgID, "global.users:id:", strconv.FormatInt(userID, 10)) diff --git a/pkg/infra/usagestats/service/usage_stats_test.go b/pkg/infra/usagestats/service/usage_stats_test.go index 31ad226729f..df63b2b663f 100644 --- a/pkg/infra/usagestats/service/usage_stats_test.go +++ b/pkg/infra/usagestats/service/usage_stats_test.go @@ -78,7 +78,7 @@ func TestMetrics(t *testing.T) { BuildVersion: "5.0.0", AnonymousEnabled: true, BasicAuthEnabled: true, - LDAPEnabled: true, + LDAPAuthEnabled: true, AuthProxyEnabled: true, Packaging: "deb", ReportingDistributor: "hosted-grafana", diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go index 0219e6b264e..debcbe4b195 100644 --- a/pkg/infra/usagestats/statscollector/service_test.go +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -148,7 +148,7 @@ func TestCollectingUsageStats(t *testing.T) { BuildVersion: "5.0.0", AnonymousEnabled: true, BasicAuthEnabled: true, - LDAPEnabled: true, + LDAPAuthEnabled: true, AuthProxyEnabled: true, Packaging: "deb", ReportingDistributor: "hosted-grafana", @@ -210,7 +210,7 @@ func TestElasticStats(t *testing.T) { BuildVersion: "5.0.0", AnonymousEnabled: true, BasicAuthEnabled: true, - LDAPEnabled: true, + LDAPAuthEnabled: true, AuthProxyEnabled: true, Packaging: "deb", ReportingDistributor: "hosted-grafana", diff --git a/pkg/login/ldap_login_test.go b/pkg/login/ldap_login_test.go index ac4dc47205c..1b54183b05e 100644 --- a/pkg/login/ldap_login_test.go +++ b/pkg/login/ldap_login_test.go @@ -19,7 +19,7 @@ var errTest = errors.New("test error") func TestLoginUsingLDAP(t *testing.T) { LDAPLoginScenario(t, "When LDAP enabled and no server configured", func(sc *LDAPLoginScenarioContext) { - setting.LDAPEnabled = true + setting.LDAPAuthEnabled = true sc.withLoginResult(false) getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) { @@ -39,7 +39,7 @@ func TestLoginUsingLDAP(t *testing.T) { }) LDAPLoginScenario(t, "When LDAP disabled", func(sc *LDAPLoginScenarioContext) { - setting.LDAPEnabled = false + setting.LDAPAuthEnabled = false sc.withLoginResult(false) loginService := &logintest.LoginServiceFake{} @@ -135,11 +135,11 @@ func LDAPLoginScenario(t *testing.T, desc string, fn LDAPLoginScenarioFunc) { origNewLDAP := newLDAP origGetLDAPConfig := getLDAPConfig - origLDAPEnabled := setting.LDAPEnabled + origLDAPEnabled := setting.LDAPAuthEnabled t.Cleanup(func() { newLDAP = origNewLDAP getLDAPConfig = origGetLDAPConfig - setting.LDAPEnabled = origLDAPEnabled + setting.LDAPAuthEnabled = origLDAPEnabled }) getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) { diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index e1fde5d4916..a0deff10cc4 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -151,7 +151,7 @@ func ProvideService(cfg *setting.Cfg, apiUrl: info.ApiUrl, teamIds: sec.Key("team_ids").Ints(","), allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()), - skipOrgRoleSync: cfg.GithubSkipOrgRoleSync, + skipOrgRoleSync: cfg.GitHubSkipOrgRoleSync, } } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index cb508054bf1..d2b6663cfa1 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -583,7 +583,7 @@ func TestMiddlewareContext(t *testing.T) { configure := func(cfg *setting.Cfg) { cfg.AuthProxyEnabled = true cfg.AuthProxyAutoSignUp = true - cfg.LDAPEnabled = true + cfg.LDAPAuthEnabled = true cfg.AuthProxyHeaderName = "X-WEBAUTH-USER" cfg.AuthProxyHeaderProperty = "username" cfg.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS", "Role": "X-WEBAUTH-ROLE"} @@ -627,7 +627,7 @@ func TestMiddlewareContext(t *testing.T) { assert.Nil(t, sc.context) }, func(cfg *setting.Cfg) { configure(cfg) - cfg.LDAPEnabled = false + cfg.LDAPAuthEnabled = false cfg.AuthProxyAutoSignUp = false }) @@ -648,7 +648,7 @@ func TestMiddlewareContext(t *testing.T) { require.Contains(t, list.Items, "X-WEBAUTH-ROLE") }, func(cfg *setting.Cfg) { configure(cfg) - cfg.LDAPEnabled = false + cfg.LDAPAuthEnabled = false cfg.AuthProxyAutoSignUp = true }) @@ -671,7 +671,7 @@ func TestMiddlewareContext(t *testing.T) { assert.Equal(t, orgRole, string(sc.context.OrgRole)) }, func(cfg *setting.Cfg) { configure(cfg) - cfg.LDAPEnabled = false + cfg.LDAPAuthEnabled = false cfg.AuthProxyAutoSignUp = true }) @@ -697,7 +697,7 @@ func TestMiddlewareContext(t *testing.T) { assert.Equal(t, "", string(sc.context.OrgRole)) }, func(cfg *setting.Cfg) { configure(cfg) - cfg.LDAPEnabled = false + cfg.LDAPAuthEnabled = false cfg.AuthProxyAutoSignUp = true }) @@ -715,7 +715,7 @@ func TestMiddlewareContext(t *testing.T) { assert.Equal(t, targetOrgID, sc.context.OrgID) }, func(cfg *setting.Cfg) { configure(cfg) - cfg.LDAPEnabled = false + cfg.LDAPAuthEnabled = false cfg.AuthProxyAutoSignUp = true }) @@ -789,7 +789,7 @@ func TestMiddlewareContext(t *testing.T) { assert.Equal(t, orgID, sc.context.OrgID) }, func(cfg *setting.Cfg) { configure(cfg) - cfg.LDAPEnabled = false + cfg.LDAPAuthEnabled = false }) middlewareScenario(t, "Should allow the request from whitelist IP", func(t *testing.T, sc *scenarioContext) { @@ -807,7 +807,7 @@ func TestMiddlewareContext(t *testing.T) { }, func(cfg *setting.Cfg) { configure(cfg) cfg.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120" - cfg.LDAPEnabled = false + cfg.LDAPAuthEnabled = false }) middlewareScenario(t, "Should not allow the request from whitelisted IP", func(t *testing.T, sc *scenarioContext) { @@ -823,7 +823,7 @@ func TestMiddlewareContext(t *testing.T) { }, func(cfg *setting.Cfg) { configure(cfg) cfg.AuthProxyWhitelist = "8.8.8.8" - cfg.LDAPEnabled = false + cfg.LDAPAuthEnabled = false }) middlewareScenario(t, "Should return 407 status code if LDAP says no", func(t *testing.T, sc *scenarioContext) { diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index b18b83dc2fe..7015a558a77 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -85,7 +85,7 @@ func ProvideService( var proxyClients []authn.ProxyClient var passwordClients []authn.PasswordClient - if s.cfg.LDAPEnabled { + if s.cfg.LDAPAuthEnabled { ldap := clients.ProvideLDAP(cfg) proxyClients = append(proxyClients, ldap) passwordClients = append(passwordClients, ldap) diff --git a/pkg/services/authn/authnimpl/usage_stats.go b/pkg/services/authn/authnimpl/usage_stats.go index 1dd9f1d36c0..cc999d1f944 100644 --- a/pkg/services/authn/authnimpl/usage_stats.go +++ b/pkg/services/authn/authnimpl/usage_stats.go @@ -13,7 +13,7 @@ func (s *Service) getUsageStats(ctx context.Context) (map[string]interface{}, er // Add stats about auth configuration authTypes := map[string]bool{} authTypes["basic_auth"] = s.cfg.BasicAuthEnabled - authTypes["ldap"] = s.cfg.LDAPEnabled + authTypes["ldap"] = s.cfg.LDAPAuthEnabled authTypes["auth_proxy"] = s.cfg.AuthProxyEnabled authTypes["anonymous"] = s.cfg.AnonymousEnabled diff --git a/pkg/services/contexthandler/authproxy/authproxy.go b/pkg/services/contexthandler/authproxy/authproxy.go index 6a6fbda21b7..3f397286973 100644 --- a/pkg/services/contexthandler/authproxy/authproxy.go +++ b/pkg/services/contexthandler/authproxy/authproxy.go @@ -38,10 +38,10 @@ var getLDAPConfig = ldap.GetConfig // isLDAPEnabled checks if LDAP is enabled var isLDAPEnabled = func(cfg *setting.Cfg) bool { if cfg != nil { - return cfg.LDAPEnabled + return cfg.LDAPAuthEnabled } - return setting.LDAPEnabled + return setting.LDAPAuthEnabled } // newLDAP creates multiple LDAP instance diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 1bdec29efa9..22c0da95f47 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -423,6 +423,11 @@ var ( State: FeatureStateAlpha, FrontendOnly: true, }, + { + Name: "onlyExternalOrgRoleSync", + Description: "Prohibits a user from changing organization roles synced with external auth providers", + State: FeatureStateAlpha, + }, { Name: "prometheusMetricEncyclopedia", Description: "Replaces the Prometheus query builder metric select option with a paginated and filterable component", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index ccbcb11c149..7c26960d05b 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -307,6 +307,10 @@ const ( // Allow datasource to provide custom UI for context view FlagLogsContextDatasourceUi = "logsContextDatasourceUi" + // FlagOnlyExternalOrgRoleSync + // Prohibits a user from changing organization roles synced with external auth providers + FlagOnlyExternalOrgRoleSync = "onlyExternalOrgRoleSync" + // FlagPrometheusMetricEncyclopedia // Replaces the Prometheus query builder metric select option with a paginated and filterable component FlagPrometheusMetricEncyclopedia = "prometheusMetricEncyclopedia" diff --git a/pkg/services/ldap/ldap_private_test.go b/pkg/services/ldap/ldap_private_test.go index 1bbcfb6c48c..c3c5311da2f 100644 --- a/pkg/services/ldap/ldap_private_test.go +++ b/pkg/services/ldap/ldap_private_test.go @@ -154,7 +154,7 @@ func TestServer_validateGrafanaUser(t *testing.T) { Config: &ServerConfig{ Groups: []*GroupToOrgRole{}, }, - log: logger.New("test"), + log: log.New("test"), } user := &login.ExternalUserInfo{ @@ -174,7 +174,7 @@ func TestServer_validateGrafanaUser(t *testing.T) { }, }, }, - log: logger.New("test"), + log: log.New("test"), } user := &login.ExternalUserInfo{ diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index 450b43b078a..12d1e1abf37 100644 --- a/pkg/services/ldap/settings.go +++ b/pkg/services/ldap/settings.go @@ -73,7 +73,7 @@ var loadingMutex = &sync.Mutex{} // IsEnabled checks if ldap is enabled func IsEnabled() bool { - return setting.LDAPEnabled + return setting.LDAPAuthEnabled } func SkipOrgRoleSync() bool { @@ -102,7 +102,7 @@ var config *Config // the config or it reads it and caches it first. func GetConfig(cfg *setting.Cfg) (*Config, error) { if cfg != nil { - if !cfg.LDAPEnabled { + if !cfg.LDAPAuthEnabled { return nil, nil } } else if !IsEnabled() { diff --git a/pkg/services/login/authinfo.go b/pkg/services/login/authinfo.go index ee0146cd5d4..db6caa7f91a 100644 --- a/pkg/services/login/authinfo.go +++ b/pkg/services/login/authinfo.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" ) type AuthInfoService interface { @@ -22,8 +23,103 @@ const ( AuthProxyAuthModule = "authproxy" JWTModule = "jwt" RenderModule = "render" + // OAuth provider modules + AzureADAuthModule = "oauth_azuread" + GoogleAuthModule = "oauth_google" + GitLabAuthModule = "oauth_gitlab" + GithubAuthModule = "oauth_github" + GenericOAuthModule = "oauth_generic_oauth" + GrafanaComAuthModule = "oauth_grafana_com" + GrafanaNetAuthModule = "oauth_grafananet" + OktaAuthModule = "oauth_okta" + + // labels + SAMLLabel = "SAML" + LDAPLabel = "LDAP" + JWTLabel = "JWT" + // OAuth provider labels + AuthProxyLabel = "Auth Proxy" + AzureADLabel = "AzureAD" + GoogleLabel = "Google" + GenericOAuthLabel = "Generic OAuth" + GitLabLabel = "GitLab" + GithubLabel = "GitHub" + GrafanaComLabel = "grafana.com" + 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) bool { + // provider enabled in config + if !IsProviderEnabled(cfg, authModule) { + return false + } + // first check SAML, LDAP and JWT + switch authModule { + case SAMLAuthModule: + return !cfg.SAMLSkipOrgRoleSync + case LDAPAuthModule: + return !cfg.LDAPSkipOrgRoleSync + case JWTModule: + return !cfg.JWTAuthSkipOrgRoleSync + } + // 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: + return !cfg.GoogleSkipOrgRoleSync + case OktaAuthModule: + return !cfg.OktaSkipOrgRoleSync + case AzureADAuthModule: + return !cfg.AzureADSkipOrgRoleSync + case GitLabAuthModule: + return !cfg.GitLabSkipOrgRoleSync + case GithubAuthModule: + return !cfg.GitHubSkipOrgRoleSync + case GrafanaComAuthModule: + return !cfg.GrafanaComSkipOrgRoleSync + case GenericOAuthModule: + return !cfg.GenericOAuthSkipOrgRoleSync + } + return true +} + +func IsProviderEnabled(cfg *setting.Cfg, authModule string) bool { + switch authModule { + case SAMLAuthModule: + return cfg.SAMLAuthEnabled + case LDAPAuthModule: + return cfg.LDAPAuthEnabled + case JWTModule: + return cfg.JWTAuthEnabled + case GoogleAuthModule: + return cfg.GoogleAuthEnabled + case OktaAuthModule: + return cfg.OktaAuthEnabled + case AzureADAuthModule: + return cfg.AzureADEnabled + case GitLabAuthModule: + return cfg.GitLabAuthEnabled + case GithubAuthModule: + return cfg.GitHubAuthEnabled + case GrafanaComAuthModule: + return cfg.GrafanaComAuthEnabled + case GenericOAuthModule: + return cfg.GenericOAuthAuthEnabled + } + return false +} + +// used for frontend to display a more user friendly label func GetAuthProviderLabel(authModule string) string { switch authModule { case "oauth_github": @@ -45,7 +141,9 @@ func GetAuthProviderLabel(authModule string) string { case JWTModule: return "JWT" case AuthProxyAuthModule: - return "Auth Proxy" + return AuthProxyLabel + case GenericOAuthModule: + return GenericOAuthLabel default: return "OAuth" // FIXME: replace with "Unknown" and handle generic oauth as a case } diff --git a/pkg/services/login/authinfo_test.go b/pkg/services/login/authinfo_test.go new file mode 100644 index 00000000000..f65e24662ba --- /dev/null +++ b/pkg/services/login/authinfo_test.go @@ -0,0 +1,248 @@ +package login + +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" +) + +func TestIsExternallySynced(t *testing.T) { + testcases := []struct { + name string + cfg *setting.Cfg + provider string + expected bool + }{ + // azure + { + name: "AzureAD synced user should return that it is externally synced", + cfg: &setting.Cfg{AzureADEnabled: true, AzureADSkipOrgRoleSync: false}, + provider: AzureADAuthModule, + expected: true, + }, + { + name: "AzureAD synced user should return that it is not externally synced when org role sync is set", + cfg: &setting.Cfg{AzureADEnabled: true, AzureADSkipOrgRoleSync: 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{AzureADEnabled: true, AzureADSkipOrgRoleSync: false, OAuthSkipOrgRoleUpdateSync: true}, + provider: AzureADAuthModule, + expected: false, + }, + // google + { + name: "Google synced user should return that it is externally synced", + cfg: &setting.Cfg{GoogleAuthEnabled: true, GoogleSkipOrgRoleSync: false}, + provider: GoogleAuthModule, + expected: true, + }, + { + name: "Google synced user should return that it is not externally synced when org role sync is set", + cfg: &setting.Cfg{GoogleAuthEnabled: true, GoogleSkipOrgRoleSync: true}, + provider: GoogleAuthModule, + 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: "google external user should return that it is not externally synced when oauth org role sync is set", + cfg: &setting.Cfg{GoogleAuthEnabled: true, GoogleSkipOrgRoleSync: false, OAuthSkipOrgRoleUpdateSync: true}, + provider: GoogleAuthModule, + expected: false, + }, + { + name: "external user should return that it is not externally synced when oauth org role sync is set and google skip org role sync set", + cfg: &setting.Cfg{GoogleAuthEnabled: true, GoogleSkipOrgRoleSync: true, OAuthSkipOrgRoleUpdateSync: true}, + provider: GoogleAuthModule, + expected: false, + }, + // okta + { + name: "Okta synced user should return that it is externally synced", + cfg: &setting.Cfg{OktaAuthEnabled: true, OktaSkipOrgRoleSync: false}, + provider: OktaAuthModule, + expected: true, + }, + { + name: "Okta synced user should return that it is not externally synced when org role sync is set", + cfg: &setting.Cfg{OktaAuthEnabled: true, OktaSkipOrgRoleSync: true}, + + provider: OktaAuthModule, + 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: "okta external user should return that it is not externally synced when oauth org role sync is set", + cfg: &setting.Cfg{OktaAuthEnabled: true, OktaSkipOrgRoleSync: false, OAuthSkipOrgRoleUpdateSync: true}, + provider: OktaAuthModule, + expected: false, + }, + // github + { + name: "Github synced user should return that it is externally synced", + cfg: &setting.Cfg{GitHubAuthEnabled: true, GitHubSkipOrgRoleSync: false}, + provider: GithubAuthModule, + expected: true, + }, + { + name: "Github synced user should return that it is not externally synced when org role sync is set", + cfg: &setting.Cfg{GitHubAuthEnabled: true, GitHubSkipOrgRoleSync: true}, + provider: GithubAuthModule, + 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: "github external user should return that it is not externally synced when oauth org role sync is set", + cfg: &setting.Cfg{GitHubAuthEnabled: true, GitHubSkipOrgRoleSync: false, OAuthSkipOrgRoleUpdateSync: true}, + provider: GithubAuthModule, + expected: false, + }, + // gitlab + { + name: "Gitlab synced user should return that it is externally synced", + cfg: &setting.Cfg{GitLabAuthEnabled: true, GitLabSkipOrgRoleSync: false}, + provider: GitLabAuthModule, + expected: true, + }, + { + name: "Gitlab synced user should return that it is not externally synced when org role sync is set", + cfg: &setting.Cfg{GitLabAuthEnabled: true, GitLabSkipOrgRoleSync: true}, + provider: GitLabAuthModule, + 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: "gitlab external user should return that it is not externally synced when oauth org role sync is set", + cfg: &setting.Cfg{GitLabAuthEnabled: true, GitLabSkipOrgRoleSync: false, OAuthSkipOrgRoleUpdateSync: true}, + provider: GitLabAuthModule, + expected: false, + }, + // grafana.com + { + name: "Grafana.com synced user should return that it is externally synced", + cfg: &setting.Cfg{GrafanaComAuthEnabled: true, GrafanaComSkipOrgRoleSync: false}, + provider: GrafanaComAuthModule, + expected: true, + }, + { + name: "Grafana.com synced user should return that it is not externally synced when org role sync is set", + cfg: &setting.Cfg{GrafanaComAuthEnabled: true, GrafanaComSkipOrgRoleSync: true}, + provider: GrafanaComAuthModule, + 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: "grafanacom external user should return that it is not externally synced when oauth org role sync is set", + cfg: &setting.Cfg{GrafanaComAuthEnabled: true, GrafanaComSkipOrgRoleSync: false, OAuthSkipOrgRoleUpdateSync: true}, + provider: GrafanaComAuthModule, + expected: false, + }, + // generic oauth + { + name: "OAuth synced user should return that it is externally synced", + cfg: &setting.Cfg{GenericOAuthAuthEnabled: true, OAuthSkipOrgRoleUpdateSync: false}, + // this could be any of the external oauth providers + provider: GenericOAuthModule, + expected: true, + }, + { + name: "OAuth synced user should return that it is not externally synced when org role sync is set", + cfg: &setting.Cfg{GenericOAuthAuthEnabled: true, OAuthSkipOrgRoleUpdateSync: true}, + // this could be any of the external oauth providers + provider: GenericOAuthModule, + 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: "generic oauth external user should return that it is not externally synced when oauth org role sync is set", + cfg: &setting.Cfg{GenericOAuthAuthEnabled: true, GenericOAuthSkipOrgRoleSync: false, OAuthSkipOrgRoleUpdateSync: true}, + provider: GenericOAuthModule, + 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{JWTAuthEnabled: true, JWTAuthSkipOrgRoleSync: 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{JWTAuthEnabled: true, JWTAuthSkipOrgRoleSync: true}, + provider: JWTModule, + expected: false, + }, + // IsProvider test + { + name: "If no provider enabled should return false", + cfg: &setting.Cfg{JWTAuthSkipOrgRoleSync: 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)) + }) + } +} + +func TestIsProviderEnabled(t *testing.T) { + testcases := []struct { + name string + cfg *setting.Cfg + provider string + expected bool + }{ + // github + { + name: "Github should return true if enabled", + cfg: &setting.Cfg{GitHubAuthEnabled: true}, + provider: GithubAuthModule, + expected: true, + }, + { + name: "Github should return false if not enabled", + cfg: &setting.Cfg{}, + provider: GithubAuthModule, + expected: false, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, IsProviderEnabled(tc.cfg, tc.provider)) + }) + } +} diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 9a565dc37d4..4f365fb067c 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -182,7 +182,7 @@ func (s *ServiceImpl) getServerAdminNode(c *contextmodel.ReqContext) *navtree.Na } } - if s.cfg.LDAPEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)) { + if s.cfg.LDAPAuthEnabled && hasAccess(ac.ReqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)) { adminNavLinks = append(adminNavLinks, &navtree.NavLink{ Text: "LDAP", Id: "ldap", Url: s.cfg.AppSubURL + "/admin/ldap", Icon: "book", }) diff --git a/pkg/services/org/model.go b/pkg/services/org/model.go index 081bc60fbf2..f9f566dfe31 100644 --- a/pkg/services/org/model.go +++ b/pkg/services/org/model.go @@ -7,15 +7,17 @@ import ( "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/util/errutil" ) // Typed errors var ( - ErrOrgNotFound = errors.New("organization not found") - ErrOrgNameTaken = errors.New("organization name is taken") - ErrLastOrgAdmin = errors.New("cannot remove last organization admin") - ErrOrgUserNotFound = errors.New("cannot find the organization user") - ErrOrgUserAlreadyAdded = errors.New("user is already added to organization") + ErrOrgNameTaken = errors.New("organization name is taken") + ErrLastOrgAdmin = errors.New("cannot remove last organization admin") + ErrOrgUserNotFound = errors.New("cannot find the organization user") + ErrOrgUserAlreadyAdded = errors.New("user is already added to organization") + ErrOrgNotFound = errors.New("organization not found") + ErrCannotChangeRoleForExternallySyncedUser = errutil.NewBase(errutil.StatusForbidden, "org.externallySynced", errutil.WithPublicMessage("cannot change role for externally synced user")) ) type Org struct { @@ -138,20 +140,21 @@ type UpdateOrgUserCommand struct { } type OrgUserDTO struct { - OrgID int64 `json:"orgId" xorm:"org_id"` - UserID int64 `json:"userId" xorm:"user_id"` - Email string `json:"email"` - Name string `json:"name"` - AvatarURL string `json:"avatarUrl" xorm:"avatar_url"` - Login string `json:"login"` - Role string `json:"role"` - LastSeenAt time.Time `json:"lastSeenAt"` - Updated time.Time `json:"-"` - Created time.Time `json:"-"` - LastSeenAtAge string `json:"lastSeenAtAge"` - AccessControl map[string]bool `json:"accessControl,omitempty"` - IsDisabled bool `json:"isDisabled"` - AuthLabels []string `json:"authLabels" xorm:"-"` + OrgID int64 `json:"orgId" xorm:"org_id"` + UserID int64 `json:"userId" xorm:"user_id"` + Email string `json:"email"` + Name string `json:"name"` + AvatarURL string `json:"avatarUrl" xorm:"avatar_url"` + Login string `json:"login"` + Role string `json:"role"` + LastSeenAt time.Time `json:"lastSeenAt"` + Updated time.Time `json:"-"` + Created time.Time `json:"-"` + LastSeenAtAge string `json:"lastSeenAtAge"` + AccessControl map[string]bool `json:"accessControl,omitempty"` + IsDisabled bool `json:"isDisabled"` + AuthLabels []string `json:"authLabels" xorm:"-"` + IsExternallySynced bool `json:"isExternallySynced"` } type RemoveOrgUserCommand struct { diff --git a/pkg/services/user/model.go b/pkg/services/user/model.go index a7904604c9a..70a28a94ef6 100644 --- a/pkg/services/user/model.go +++ b/pkg/services/user/model.go @@ -140,20 +140,21 @@ type GetUserProfileQuery struct { } type UserProfileDTO struct { - ID int64 `json:"id"` - Email string `json:"email"` - Name string `json:"name"` - Login string `json:"login"` - Theme string `json:"theme"` - OrgID int64 `json:"orgId,omitempty"` - IsGrafanaAdmin bool `json:"isGrafanaAdmin"` - IsDisabled bool `json:"isDisabled"` - IsExternal bool `json:"isExternal"` - AuthLabels []string `json:"authLabels"` - UpdatedAt time.Time `json:"updatedAt"` - CreatedAt time.Time `json:"createdAt"` - AvatarUrl string `json:"avatarUrl"` - AccessControl map[string]bool `json:"accessControl,omitempty"` + ID int64 `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + Login string `json:"login"` + Theme string `json:"theme"` + OrgID int64 `json:"orgId,omitempty"` + IsGrafanaAdmin bool `json:"isGrafanaAdmin"` + IsDisabled bool `json:"isDisabled"` + IsExternal bool `json:"isExternal"` + IsExternallySynced bool `json:"isExternallySynced"` + AuthLabels []string `json:"authLabels"` + UpdatedAt time.Time `json:"updatedAt"` + CreatedAt time.Time `json:"createdAt"` + AvatarUrl string `json:"avatarUrl"` + AccessControl map[string]bool `json:"accessControl,omitempty"` } // implement Conversion interface to define custom field mapping (xorm feature) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 2dfc9fc9ea5..dbc4aaeb654 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -140,7 +140,7 @@ var ( RudderstackConfigUrl string // LDAP - LDAPEnabled bool + LDAPAuthEnabled bool LDAPSkipOrgRoleSync bool LDAPConfigFile string LDAPSyncCron string @@ -428,18 +428,28 @@ type Cfg struct { // Frontend analytics IntercomSecret string // AzureAD + AzureADEnabled bool AzureADSkipOrgRoleSync bool // Google + GoogleAuthEnabled bool GoogleSkipOrgRoleSync bool // Gitlab + GitLabAuthEnabled bool GitLabSkipOrgRoleSync bool + // Generic OAuth + GenericOAuthAuthEnabled bool + GenericOAuthSkipOrgRoleSync bool + // LDAP - LDAPEnabled bool - LDAPSkipOrgRoleSync bool - LDAPAllowSignup bool + LDAPAuthEnabled bool + LDAPSkipOrgRoleSync bool + LDAPConfigFilePath string + LDAPAllowSignup bool + LDAPActiveSyncEnabled bool + LDAPSyncCron string DefaultTheme string DefaultLanguage string @@ -470,8 +480,9 @@ type Cfg struct { // then Live uses AppURL as the only allowed origin. LiveAllowedOrigins []string - // Github OAuth - GithubSkipOrgRoleSync bool + // GitHub OAuth + GitHubAuthEnabled bool + GitHubSkipOrgRoleSync bool // Grafana.com URL, used for OAuth redirect. GrafanaComURL string @@ -479,6 +490,8 @@ type Cfg struct { // in case API is not publicly accessible. // Defaults to GrafanaComURL setting + "/api" if unset. GrafanaComAPIURL string + // Grafana.com Auth enabled + GrafanaComAuthEnabled bool // GrafanaComSkipOrgRoleSync can be set for // letting users set org roles from within Grafana and // skip the org roles coming from GrafanaCom @@ -502,7 +515,12 @@ type Cfg struct { SecureSocksDSProxy SecureSocksDSProxySettings + // SAML Auth + SAMLAuthEnabled bool + SAMLSkipOrgRoleSync bool + // Okta OAuth + OktaAuthEnabled bool OktaSkipOrgRoleSync bool // Access Control @@ -1094,6 +1112,7 @@ func (cfg *Cfg) Load(args CommandLineArgs) error { } cfg.readLDAPConfig() + cfg.readSAMLConfig() cfg.handleAWSConfig() cfg.readAzureSettings() cfg.readSessionConfig() @@ -1192,17 +1211,26 @@ type RemoteCacheOptions struct { Encryption bool } +func (cfg *Cfg) readSAMLConfig() { + samlSec := cfg.Raw.Section("auth.saml") + cfg.SAMLAuthEnabled = samlSec.Key("enabled").MustBool(false) + cfg.SAMLSkipOrgRoleSync = samlSec.Key("skip_org_role_sync").MustBool(false) +} + func (cfg *Cfg) readLDAPConfig() { ldapSec := cfg.Raw.Section("auth.ldap") LDAPConfigFile = ldapSec.Key("config_file").String() LDAPSyncCron = ldapSec.Key("sync_cron").String() - LDAPEnabled = ldapSec.Key("enabled").MustBool(false) - cfg.LDAPEnabled = LDAPEnabled + LDAPAuthEnabled = ldapSec.Key("enabled").MustBool(false) + cfg.LDAPConfigFilePath = ldapSec.Key("config_file").String() + cfg.LDAPSyncCron = ldapSec.Key("sync_cron").String() + cfg.LDAPAuthEnabled = ldapSec.Key("enabled").MustBool(false) LDAPSkipOrgRoleSync = ldapSec.Key("skip_org_role_sync").MustBool(false) - cfg.LDAPSkipOrgRoleSync = LDAPSkipOrgRoleSync + cfg.LDAPSkipOrgRoleSync = ldapSec.Key("skip_org_role_sync").MustBool(false) LDAPActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false) + cfg.LDAPActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false) LDAPAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true) - cfg.LDAPAllowSignup = LDAPAllowSignup + cfg.LDAPAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true) } func (cfg *Cfg) handleAWSConfig() { @@ -1377,31 +1405,43 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error { } func readAuthAzureADSettings(iniFile *ini.File, cfg *Cfg) { sec := iniFile.Section("auth.azuread") + cfg.AzureADEnabled = sec.Key("enabled").MustBool(false) cfg.AzureADSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) } func readAuthGrafanaComSettings(iniFile *ini.File, cfg *Cfg) { sec := iniFile.Section("auth.grafana_com") + cfg.GrafanaComAuthEnabled = sec.Key("enabled").MustBool(false) cfg.GrafanaComSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) } func readAuthGithubSettings(iniFile *ini.File, cfg *Cfg) { sec := iniFile.Section("auth.github") - cfg.GithubSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) + cfg.GitHubAuthEnabled = sec.Key("enabled").MustBool(false) + cfg.GitHubSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) } func readAuthGoogleSettings(iniFile *ini.File, cfg *Cfg) { sec := iniFile.Section("auth.google") + cfg.GoogleAuthEnabled = sec.Key("enabled").MustBool(false) cfg.GoogleSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) } func readAuthGitlabSettings(iniFile *ini.File, cfg *Cfg) { sec := iniFile.Section("auth.gitlab") + cfg.GitLabAuthEnabled = sec.Key("enabled").MustBool(false) cfg.GitLabSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) } +func readGenericOAuthSettings(iniFile *ini.File, cfg *Cfg) { + sec := iniFile.Section("auth.generic_oauth") + cfg.GenericOAuthAuthEnabled = sec.Key("enabled").MustBool(false) + cfg.GenericOAuthSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) +} + func readAuthOktaSettings(iniFile *ini.File, cfg *Cfg) { sec := iniFile.Section("auth.okta") + cfg.OktaAuthEnabled = sec.Key("enabled").MustBool(false) cfg.OktaSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) } @@ -1464,6 +1504,8 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { // GitLab Auth readAuthGitlabSettings(iniFile, cfg) + // genericOAuth + readGenericOAuthSettings(iniFile, cfg) // Okta Auth readAuthOktaSettings(iniFile, cfg) diff --git a/public/app/features/users/UsersTable.tsx b/public/app/features/users/UsersTable.tsx index ac8f6c7c917..de46b1989b3 100644 --- a/public/app/features/users/UsersTable.tsx +++ b/public/app/features/users/UsersTable.tsx @@ -5,6 +5,7 @@ import { Button, ConfirmModal } from '@grafana/ui'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; +import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { AccessControlAction, OrgUser, Role } from 'app/types'; @@ -49,12 +50,17 @@ export const UsersTable = ({ users, orgId, onRoleChange, onRemoveUser }: Props)