mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 23:53:10 +08:00
Access Control: Add fine-grained access control to GET stats and settings handlers (#35622)
* add accesscontrol action for stats read * use accesscontrol middleware for stats route * add fixed role with permissions to read sever stats * add accesscontrol action for settings read * use accesscontrol middleware for settings route * add fixed role with permissions to read settings * add accesscontrol tests for AdminGetSettings and AdminGetStats * add ability to scope settings * add tests for AdminGetSettings
This commit is contained in:
@ -22,11 +22,13 @@ Fixed roles | Permissions | Descriptions
|
||||
`fixed:users:org:edit` | All permissions from `fixed:users:org:read` and <br>`org.users:add`<br>`org.users:remove`<br>`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`<br>`ldap.status:read` | Allows to read LDAP information and status.
|
||||
`fixed:ldap:admin:edit` | All permissions from `fixed:ldap:admin:read` and <br>`ldap.user:sync`<br>`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<br>`settings:write` | Update settings
|
||||
|
||||
## Default built-in role assignments
|
||||
|
||||
Built-in roles | Associated roles | Descriptions
|
||||
--- | --- | ---
|
||||
Grafana Admin | `fixed:permissions:admin:edit`<br>`fixed:permissions:admin:read`<br>`fixed:reporting:admin:edit`<br>`fixed:reporting:admin:read`<br>`fixed:users:admin:edit`<br>`fixed:users:admin:read`<br>`fixed:users:org:edit`<br>`fixed:users:org:read`<br>`fixed:ldap:admin:edit`<br>`fixed:ldap:admin:read`<br>`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`<br>`fixed:permissions:admin:read`<br>`fixed:reporting:admin:edit`<br>`fixed:reporting:admin:read`<br>`fixed:users:admin:edit`<br>`fixed:users:admin:read`<br>`fixed:users:org:edit`<br>`fixed:users:org:read`<br>`fixed:ldap:admin:edit`<br>`fixed:ldap:admin:read`<br>`fixed:server:admin:read`<br>`fixed:settings:admin:read`<br>`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`<br>`fixed:users:org:read`<br>`fixed:reporting:admin:edit`<br>`fixed:reporting:admin:read` | Allows access to resource which [Admin]({{< relref "../../permissions/organization_roles.md" >}}) has permissions by default.
|
||||
|
@ -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:**`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Read settings
|
||||
`settings:write` | `settings:**`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Update settings
|
||||
`server.stats:read` | n/a | Read server stats
|
||||
|
||||
## Scope definitions
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
159
pkg/api/admin_test.go
Normal file
159
pkg/api/admin_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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:*"
|
||||
)
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user