diff --git a/docs/sources/enterprise/access-control/fine-grained-access-control-references.md b/docs/sources/enterprise/access-control/fine-grained-access-control-references.md
index 65e1d7f7cc2..f9104b23241 100644
--- a/docs/sources/enterprise/access-control/fine-grained-access-control-references.md
+++ b/docs/sources/enterprise/access-control/fine-grained-access-control-references.md
@@ -22,11 +22,13 @@ Fixed roles | Permissions | Descriptions
`fixed:users:org:edit` | All permissions from `fixed:users:org:read` and
`org.users:add`
`org.users:remove`
`org.users.role:update` | Allows every read action for user organizations and in addition allows to administer user organizations.
`fixed:ldap:admin:read` | `ldap.user:read`
`ldap.status:read` | Allows to read LDAP information and status.
`fixed:ldap:admin:edit` | All permissions from `fixed:ldap:admin:read` and
`ldap.user:sync`
`ldap.config:reload` | Allows every read action for LDAP and in addition allows to administer LDAP.
-`fixed:settings:admin:edit` | `settings:write` | Update all settings
+`fixed:server:admin:read` | `server.stats:read` | Read server stats
+`fixed:settings:admin:read` | `settings:read` | Read settings
+`fixed:settings:admin:edit` | All permissions from `fixed:settings:admin:read` and
`settings:write` | Update settings
## Default built-in role assignments
Built-in roles | Associated roles | Descriptions
--- | --- | ---
-Grafana Admin | `fixed:permissions:admin:edit`
`fixed:permissions:admin:read`
`fixed:reporting:admin:edit`
`fixed:reporting:admin:read`
`fixed:users:admin:edit`
`fixed:users:admin:read`
`fixed:users:org:edit`
`fixed:users:org:read`
`fixed:ldap:admin:edit`
`fixed:ldap:admin:read`
`fixed:settings:admin:edit` | Allows access to resources which [Grafana Server Admin]({{< relref "../../permissions/_index.md#grafana-server-admin-role" >}}) has permissions by default.
+Grafana Admin | `fixed:permissions:admin:edit`
`fixed:permissions:admin:read`
`fixed:reporting:admin:edit`
`fixed:reporting:admin:read`
`fixed:users:admin:edit`
`fixed:users:admin:read`
`fixed:users:org:edit`
`fixed:users:org:read`
`fixed:ldap:admin:edit`
`fixed:ldap:admin:read`
`fixed:server:admin:read`
`fixed:settings:admin:read`
`fixed:settings:admin:edit` | Allows access to resources which [Grafana Server Admin]({{< relref "../../permissions/_index.md#grafana-server-admin-role" >}}) has permissions by default.
Admin | `fixed:users:org:edit`
`fixed:users:org:read`
`fixed:reporting:admin:edit`
`fixed:reporting:admin:read` | Allows access to resource which [Admin]({{< relref "../../permissions/organization_roles.md" >}}) has permissions by default.
diff --git a/docs/sources/enterprise/access-control/permissions.md b/docs/sources/enterprise/access-control/permissions.md
index 25f87ddcd45..fff815e3bb7 100644
--- a/docs/sources/enterprise/access-control/permissions.md
+++ b/docs/sources/enterprise/access-control/permissions.md
@@ -63,7 +63,9 @@ Actions | Applicable scopes | Descriptions
`ldap.status:read` | n/a | Verify the LDAP servers’ availability.
`ldap.config:reload` | n/a | Reload the LDAP configuration.
`status:accesscontrol` | `service:accesscontrol` | Get access-control enabled status.
+`settings:read` | `settings:**`
`settings:auth.saml:*`
`settings:auth.saml:enabled` (property level) | Read settings
`settings:write` | `settings:**`
`settings:auth.saml:*`
`settings:auth.saml:enabled` (property level) | Update settings
+`server.stats:read` | n/a | Read server stats
## Scope definitions
diff --git a/pkg/api/admin.go b/pkg/api/admin.go
index 643db782be2..a21d59fd2bd 100644
--- a/pkg/api/admin.go
+++ b/pkg/api/admin.go
@@ -1,13 +1,23 @@
package api
import (
+ "context"
+ "fmt"
+ "net/http"
+
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/accesscontrol"
+ "github.com/grafana/grafana/pkg/setting"
)
-func (hs *HTTPServer) AdminGetSettings(_ *models.ReqContext) response.Response {
- return response.JSON(200, hs.SettingsProvider.Current())
+func (hs *HTTPServer) AdminGetSettings(c *models.ReqContext) response.Response {
+ settings, err := hs.getAuthorizedSettings(c.Req.Context(), c.SignedInUser, hs.SettingsProvider.Current())
+ if err != nil {
+ return response.Error(http.StatusForbidden, "Failed to authorize settings", err)
+ }
+ return response.JSON(http.StatusOK, settings)
}
func AdminGetStats(c *models.ReqContext) response.Response {
@@ -19,3 +29,52 @@ func AdminGetStats(c *models.ReqContext) response.Response {
return response.JSON(200, statsQuery.Result)
}
+
+func (hs *HTTPServer) getAuthorizedSettings(ctx context.Context, user *models.SignedInUser, bag setting.SettingsBag) (setting.SettingsBag, error) {
+ if hs.AccessControl.IsDisabled() {
+ return bag, nil
+ }
+
+ eval := func(scopes ...string) (bool, error) {
+ return hs.AccessControl.Evaluate(ctx, user, accesscontrol.ActionSettingsRead, scopes...)
+ }
+
+ ok, err := eval(accesscontrol.ScopeSettingsAll)
+ if err != nil {
+ return nil, err
+ }
+ if ok {
+ return bag, nil
+ }
+
+ authorizedBag := make(setting.SettingsBag)
+
+ for section, keys := range bag {
+ ok, err := eval(getSettingsScope(section, "*"))
+ if err != nil {
+ return nil, err
+ }
+ if ok {
+ authorizedBag[section] = keys
+ continue
+ }
+
+ for key := range keys {
+ ok, err := eval(getSettingsScope(section, key))
+ if err != nil {
+ return nil, err
+ }
+ if ok {
+ if _, exists := authorizedBag[section]; !exists {
+ authorizedBag[section] = make(map[string]string)
+ }
+ authorizedBag[section][key] = bag[section][key]
+ }
+ }
+ }
+ return authorizedBag, nil
+}
+
+func getSettingsScope(section, key string) string {
+ return fmt.Sprintf("settings:%s:%s", section, key)
+}
diff --git a/pkg/api/admin_test.go b/pkg/api/admin_test.go
new file mode 100644
index 00000000000..f24aa263306
--- /dev/null
+++ b/pkg/api/admin_test.go
@@ -0,0 +1,159 @@
+package api
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/grafana/grafana/pkg/services/accesscontrol"
+ "github.com/grafana/grafana/pkg/setting"
+ "github.com/stretchr/testify/assert"
+)
+
+type getSettingsTestCase struct {
+ desc string
+ expectedCode int
+ expectedBody string
+ permissions []*accesscontrol.Permission
+}
+
+func TestAPI_AdminGetSettings(t *testing.T) {
+ tests := []getSettingsTestCase{
+ {
+ desc: "should return all settings",
+ expectedCode: http.StatusOK,
+ expectedBody: `{"auth.proxy":{"enable_login_token":"false","enabled":"false"},"auth.saml":{"allow_idp_initiated":"false","enabled":"true"}}`,
+ permissions: []*accesscontrol.Permission{
+ {
+ Action: accesscontrol.ActionSettingsRead,
+ Scope: accesscontrol.ScopeSettingsAll,
+ },
+ },
+ },
+ {
+ desc: "should only return auth.saml settings",
+ expectedCode: http.StatusOK,
+ expectedBody: `{"auth.saml":{"allow_idp_initiated":"false","enabled":"true"}}`,
+ permissions: []*accesscontrol.Permission{
+ {
+ Action: accesscontrol.ActionSettingsRead,
+ Scope: "settings:auth.saml:*",
+ },
+ },
+ },
+ {
+ desc: "should only partial properties from auth.saml and auth.proxy settings",
+ expectedCode: http.StatusOK,
+ expectedBody: `{"auth.proxy":{"enable_login_token":"false"},"auth.saml":{"enabled":"true"}}`,
+ permissions: []*accesscontrol.Permission{
+ {
+ Action: accesscontrol.ActionSettingsRead,
+ Scope: "settings:auth.saml:enabled",
+ },
+ {
+ Action: accesscontrol.ActionSettingsRead,
+ Scope: "settings:auth.proxy:enable_login_token",
+ },
+ },
+ },
+ }
+
+ cfg := setting.NewCfg()
+ //seed sections and keys
+ cfg.Raw.DeleteSection("DEFAULT")
+ saml, err := cfg.Raw.NewSection("auth.saml")
+ assert.NoError(t, err)
+ _, err = saml.NewKey("enabled", "true")
+ assert.NoError(t, err)
+ _, err = saml.NewKey("allow_idp_initiated", "false")
+ assert.NoError(t, err)
+
+ proxy, err := cfg.Raw.NewSection("auth.proxy")
+ assert.NoError(t, err)
+ _, err = proxy.NewKey("enabled", "false")
+ assert.NoError(t, err)
+ _, err = proxy.NewKey("enable_login_token", "false")
+ assert.NoError(t, err)
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ sc, hs := setupAccessControlScenarioContext(t, cfg, "/api/admin/settings", test.permissions)
+ hs.SettingsProvider = &setting.OSSImpl{Cfg: cfg}
+
+ sc.resp = httptest.NewRecorder()
+ var err error
+ sc.req, err = http.NewRequest(http.MethodGet, "/api/admin/settings", nil)
+ assert.NoError(t, err)
+
+ sc.exec()
+
+ assert.Equal(t, test.expectedCode, sc.resp.Code)
+ assert.Equal(t, test.expectedBody, sc.resp.Body.String())
+ })
+ }
+}
+
+func TestAdmin_AccessControl(t *testing.T) {
+ tests := []accessControlTestCase{
+ {
+ expectedCode: http.StatusOK,
+ desc: "AdminGetStats should return 200 for user with correct permissions",
+ url: "/api/admin/stats",
+ method: http.MethodGet,
+ permissions: []*accesscontrol.Permission{
+ {
+ Action: accesscontrol.ActionServerStatsRead,
+ },
+ },
+ },
+ {
+ expectedCode: http.StatusForbidden,
+ desc: "AdminGetStats should return 403 for user without required permissions",
+ url: "/api/admin/stats",
+ method: http.MethodGet,
+ permissions: []*accesscontrol.Permission{
+ {
+ Action: "wrong",
+ },
+ },
+ },
+ {
+ expectedCode: http.StatusOK,
+ desc: "AdminGetSettings should return 200 for user with correct permissions",
+ url: "/api/admin/settings",
+ method: http.MethodGet,
+ permissions: []*accesscontrol.Permission{
+ {
+ Action: accesscontrol.ActionSettingsRead,
+ },
+ },
+ },
+ {
+ expectedCode: http.StatusForbidden,
+ desc: "AdminGetSettings should return 403 for user without required permissions",
+ url: "/api/admin/settings",
+ method: http.MethodGet,
+ permissions: []*accesscontrol.Permission{
+ {
+ Action: "wrong",
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ cfg := setting.NewCfg()
+ sc, hs := setupAccessControlScenarioContext(t, cfg, test.url, test.permissions)
+ sc.resp = httptest.NewRecorder()
+ hs.SettingsProvider = &setting.OSSImpl{Cfg: cfg}
+
+ var err error
+ sc.req, err = http.NewRequest(test.method, test.url, nil)
+ assert.NoError(t, err)
+
+ sc.exec()
+ assert.Equal(t, test.expectedCode, sc.resp.Code)
+ })
+ }
+}
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 8e0724bd1ff..d0d1e6ec9d7 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -63,13 +63,13 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/dashboard/import/", reqSignedIn, hs.Index)
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
r.Get("/admin", reqGrafanaAdmin, hs.Index)
- r.Get("/admin/settings", reqGrafanaAdmin, hs.Index)
+ r.Get("/admin/settings", authorize(reqGrafanaAdmin, accesscontrol.ActionSettingsRead), hs.Index)
r.Get("/admin/users", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersRead, accesscontrol.ScopeGlobalUsersAll), hs.Index)
r.Get("/admin/users/create", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersCreate), hs.Index)
r.Get("/admin/users/edit/:id", authorize(reqGrafanaAdmin, accesscontrol.ActionUsersRead), hs.Index)
r.Get("/admin/orgs", reqGrafanaAdmin, hs.Index)
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, hs.Index)
- r.Get("/admin/stats", reqGrafanaAdmin, hs.Index)
+ r.Get("/admin/stats", authorize(reqGrafanaAdmin, accesscontrol.ActionServerStatsRead), hs.Index)
r.Get("/admin/ldap", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPStatusRead), hs.Index)
r.Get("/styleguide", reqSignedIn, hs.Index)
@@ -433,8 +433,8 @@ func (hs *HTTPServer) registerRoutes() {
// admin api
r.Group("/api/admin", func(adminRoute routing.RouteRegister) {
- adminRoute.Get("/settings", reqGrafanaAdmin, routing.Wrap(hs.AdminGetSettings))
- adminRoute.Get("/stats", reqGrafanaAdmin, routing.Wrap(AdminGetStats))
+ adminRoute.Get("/settings", authorize(reqGrafanaAdmin, accesscontrol.ActionSettingsRead), routing.Wrap(hs.AdminGetSettings))
+ adminRoute.Get("/stats", authorize(reqGrafanaAdmin, accesscontrol.ActionServerStatsRead), routing.Wrap(AdminGetStats))
adminRoute.Post("/pause-all-alerts", reqGrafanaAdmin, bind(dtos.PauseAllAlertsCommand{}), routing.Wrap(PauseAllAlerts))
adminRoute.Post("/provisioning/dashboards/reload", reqGrafanaAdmin, routing.Wrap(hs.AdminProvisioningReloadDashboards))
diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go
index 9da77f27b10..921f31532cb 100644
--- a/pkg/api/common_test.go
+++ b/pkg/api/common_test.go
@@ -253,7 +253,7 @@ func (f *fakeAccessControl) IsDisabled() bool {
return f.isDisabled
}
-func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []*accesscontrol.Permission) *scenarioContext {
+func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []*accesscontrol.Permission) (*scenarioContext, *HTTPServer) {
cfg.FeatureToggles = make(map[string]bool)
cfg.FeatureToggles["accesscontrol"] = true
@@ -268,5 +268,13 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin
hs.registerRoutes()
hs.RouteRegister.Register(sc.m.Router)
- return sc
+ return sc, hs
+}
+
+type accessControlTestCase struct {
+ expectedCode int
+ desc string
+ url string
+ method string
+ permissions []*accesscontrol.Permission
}
diff --git a/pkg/api/index.go b/pkg/api/index.go
index 1c536164204..e9081600b15 100644
--- a/pkg/api/index.go
+++ b/pkg/api/index.go
@@ -357,12 +357,21 @@ func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink {
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
Text: "Orgs", Id: "global-orgs", Url: hs.Cfg.AppSubURL + "/admin/orgs", Icon: "building",
})
+ }
+
+ if hasAccess(ac.ReqGrafanaAdmin, ac.ActionSettingsRead) {
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
Text: "Settings", Id: "server-settings", Url: hs.Cfg.AppSubURL + "/admin/settings", Icon: "sliders-v-alt",
})
+ }
+
+ if hasAccess(ac.ReqGrafanaAdmin, ac.ActionServerStatsRead) {
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
Text: "Stats", Id: "server-stats", Url: hs.Cfg.AppSubURL + "/admin/stats", Icon: "graph-bar",
})
+ }
+
+ if c.IsGrafanaAdmin {
if hs.Cfg.PluginAdminEnabled {
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
Text: "Plugin catalog", Id: "plugin-catalog", Url: hs.Cfg.AppSubURL + "/a/grafana-plugin-admin-app", Icon: "plug",
diff --git a/pkg/api/ldap_debug_test.go b/pkg/api/ldap_debug_test.go
index 5868d9118cd..b8d31cc1b26 100644
--- a/pkg/api/ldap_debug_test.go
+++ b/pkg/api/ldap_debug_test.go
@@ -581,16 +581,8 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) {
// Access control tests for ldap endpoints
// ***
-type ldapAccessControlTestCase struct {
- expectedCode int
- desc string
- url string
- method string
- permissions []*accesscontrol.Permission
-}
-
func TestLDAP_AccessControl(t *testing.T) {
- tests := []ldapAccessControlTestCase{
+ tests := []accessControlTestCase{
{
url: "/api/admin/ldap/reload",
method: http.MethodPost,
@@ -667,8 +659,6 @@ func TestLDAP_AccessControl(t *testing.T) {
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
- t.Helper()
-
enabled := setting.LDAPEnabled
configFile := setting.LDAPConfigFile
@@ -685,7 +675,7 @@ func TestLDAP_AccessControl(t *testing.T) {
cfg := setting.NewCfg()
cfg.LDAPEnabled = true
- sc := setupAccessControlScenarioContext(t, cfg, test.url, test.permissions)
+ sc, _ := setupAccessControlScenarioContext(t, cfg, test.url, test.permissions)
sc.resp = httptest.NewRecorder()
sc.req, err = http.NewRequest(test.method, test.url, nil)
assert.NoError(t, err)
diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go
index 2d1e91edc78..09fe491b43e 100644
--- a/pkg/services/accesscontrol/models.go
+++ b/pkg/services/accesscontrol/models.go
@@ -80,12 +80,22 @@ const (
ActionLDAPStatusRead = "ldap.status:read"
ActionLDAPConfigReload = "ldap.config:reload"
+ // Server actions
+ ActionServerStatsRead = "server.stats:read"
+
+ // Settings actions
+ ActionSettingsRead = "settings:read"
+
// Global Scopes
ScopeGlobalUsersAll = "global:users:*"
+ // Users scopes
ScopeUsersSelf = "users:self"
ScopeUsersAll = "users:*"
+ // Settings scope
+ ScopeSettingsAll = "settings:**"
+
// Services Scopes
ScopeServicesAll = "service:*"
)
diff --git a/pkg/services/accesscontrol/roles.go b/pkg/services/accesscontrol/roles.go
index 270c3915d3d..6dc8c484816 100644
--- a/pkg/services/accesscontrol/roles.go
+++ b/pkg/services/accesscontrol/roles.go
@@ -28,6 +28,27 @@ var ldapAdminEditRole = RoleDTO{
}),
}
+var serverAdminReadRole = RoleDTO{
+ Version: 1,
+ Name: serverAdminRead,
+ Permissions: []Permission{
+ {
+ Action: ActionServerStatsRead,
+ },
+ },
+}
+
+var settingsAdminReadRole = RoleDTO{
+ Version: 1,
+ Name: settingsAdminRead,
+ Permissions: []Permission{
+ {
+ Action: ActionSettingsRead,
+ Scope: ScopeSettingsAll,
+ },
+ },
+}
+
var usersOrgReadRole = RoleDTO{
Name: usersOrgRead,
Version: 1,
@@ -145,6 +166,10 @@ var provisioningAdminRole = RoleDTO{
// resource. FixedRoleGrants lists which built-in roles are
// assigned which fixed roles in this list.
var FixedRoles = map[string]RoleDTO{
+ serverAdminRead: serverAdminReadRole,
+
+ settingsAdminRead: settingsAdminReadRole,
+
usersAdminRead: usersAdminReadRole,
usersAdminEdit: usersAdminEditRole,
@@ -158,6 +183,10 @@ var FixedRoles = map[string]RoleDTO{
}
const (
+ serverAdminRead = "fixed:server:admin:read"
+
+ settingsAdminRead = "fixed:settings:admin:read"
+
usersAdminEdit = "fixed:users:admin:edit"
usersAdminRead = "fixed:users:admin:read"
@@ -177,6 +206,8 @@ var FixedRoleGrants = map[string][]string{
ldapAdminEdit,
ldapAdminRead,
provisioningAdmin,
+ serverAdminRead,
+ settingsAdminRead,
usersAdminEdit,
usersAdminRead,
usersOrgEdit,