mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AccessControl: Implement a way to register fixed roles (#35641)
* AccessControl: Implement a way to register fixed roles * Add context to register func * Use FixedRoleGrantsMap instead of FixedRoleGrants * Removed FixedRoles map to sync.map * Wrote test for accesscontrol and provisioning * Use mutexes+map instead of sync maps * Create a sync map struct out of a Map and a Mutex * Create a sync map struct for grants as well * Validate builtin roles * Make validation public to access control * Handle errors consistently with what seeder does * Keep errors consistant amongst accesscontrol impl * Handle registration error * Reverse the registration direction thanks to a RoleRegistrant interface * Removed sync map in favor for simple maps since registration now happens during init * Work on the Registrant interface * Remove the Register Role from the interface to have services returning their registrations instead * Adding context to RegisterRegistrantsRoles and update descriptions * little bit of cosmetics * Making sure provisioning is ran after role registration * test for role registration * Change the accesscontrol interface to use a variadic * check if accesscontrol is enabled * Add a new test for RegisterFixedRoles and fix assign which was buggy * Moved RegistrationList def to roles.go * Change provisioning role's description * Better comment on RegisterFixedRoles * Correct comment on ValidateFixedRole * Simplify helper func to removeRoleHelper * Add log to saveFixedRole and assignFixedRole Co-authored-by: Vardan Torosyan <vardants@gmail.com> Co-authored-by: Jeremy Price <Jeremy.price@grafana.com>
This commit is contained in:
169
pkg/api/admin_provisioning_test.go
Normal file
169
pkg/api/admin_provisioning_test.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type reloadProvisioningTestCase struct {
|
||||
desc string
|
||||
url string
|
||||
expectedCode int
|
||||
expectedBody string
|
||||
permissions []*accesscontrol.Permission
|
||||
exit bool
|
||||
checkCall func(mock provisioning.ProvisioningServiceMock)
|
||||
}
|
||||
|
||||
func TestAPI_AdminProvisioningReload_AccessControl(t *testing.T) {
|
||||
tests := []reloadProvisioningTestCase{
|
||||
{
|
||||
desc: "should work for dashboards with specific scope",
|
||||
expectedCode: http.StatusOK,
|
||||
expectedBody: `{"message":"Dashboards config reloaded"}`,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: ActionProvisioningReload,
|
||||
Scope: ScopeProvisionersDashboards,
|
||||
},
|
||||
},
|
||||
url: "/api/admin/provisioning/dashboards/reload",
|
||||
checkCall: func(mock provisioning.ProvisioningServiceMock) {
|
||||
assert.Len(t, mock.Calls.ProvisionDashboards, 1)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should work for dashboards with broader scope",
|
||||
expectedCode: http.StatusOK,
|
||||
expectedBody: `{"message":"Dashboards config reloaded"}`,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: ActionProvisioningReload,
|
||||
Scope: ScopeProvisionersAll,
|
||||
},
|
||||
},
|
||||
url: "/api/admin/provisioning/dashboards/reload",
|
||||
checkCall: func(mock provisioning.ProvisioningServiceMock) {
|
||||
assert.Len(t, mock.Calls.ProvisionDashboards, 1)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should fail for dashboard with wrong scope",
|
||||
expectedCode: http.StatusForbidden,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: ActionProvisioningReload,
|
||||
Scope: "services:noservice",
|
||||
},
|
||||
},
|
||||
url: "/api/admin/provisioning/dashboards/reload",
|
||||
exit: true,
|
||||
},
|
||||
{
|
||||
desc: "should fail for dashboard with no permission",
|
||||
expectedCode: http.StatusForbidden,
|
||||
url: "/api/admin/provisioning/dashboards/reload",
|
||||
exit: true,
|
||||
},
|
||||
{
|
||||
desc: "should work for notifications with specific scope",
|
||||
expectedCode: http.StatusOK,
|
||||
expectedBody: `{"message":"Notifications config reloaded"}`,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: ActionProvisioningReload,
|
||||
Scope: ScopeProvisionersNotifications,
|
||||
},
|
||||
},
|
||||
url: "/api/admin/provisioning/notifications/reload",
|
||||
checkCall: func(mock provisioning.ProvisioningServiceMock) {
|
||||
assert.Len(t, mock.Calls.ProvisionNotifications, 1)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should fail for notifications with no permission",
|
||||
expectedCode: http.StatusForbidden,
|
||||
url: "/api/admin/provisioning/notifications/reload",
|
||||
exit: true,
|
||||
},
|
||||
{
|
||||
desc: "should work for datasources with specific scope",
|
||||
expectedCode: http.StatusOK,
|
||||
expectedBody: `{"message":"Datasources config reloaded"}`,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: ActionProvisioningReload,
|
||||
Scope: ScopeProvisionersDatasources,
|
||||
},
|
||||
},
|
||||
url: "/api/admin/provisioning/datasources/reload",
|
||||
checkCall: func(mock provisioning.ProvisioningServiceMock) {
|
||||
assert.Len(t, mock.Calls.ProvisionDatasources, 1)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should fail for datasources with no permission",
|
||||
expectedCode: http.StatusForbidden,
|
||||
url: "/api/admin/provisioning/datasources/reload",
|
||||
exit: true,
|
||||
},
|
||||
{
|
||||
desc: "should work for plugins with specific scope",
|
||||
expectedCode: http.StatusOK,
|
||||
expectedBody: `{"message":"Plugins config reloaded"}`,
|
||||
permissions: []*accesscontrol.Permission{
|
||||
{
|
||||
Action: ActionProvisioningReload,
|
||||
Scope: ScopeProvisionersPlugins,
|
||||
},
|
||||
},
|
||||
url: "/api/admin/provisioning/plugins/reload",
|
||||
checkCall: func(mock provisioning.ProvisioningServiceMock) {
|
||||
assert.Len(t, mock.Calls.ProvisionPlugins, 1)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "should fail for plugins with no permission",
|
||||
expectedCode: http.StatusForbidden,
|
||||
url: "/api/admin/provisioning/plugins/reload",
|
||||
exit: true,
|
||||
},
|
||||
}
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
sc, hs := setupAccessControlScenarioContext(t, cfg, test.url, test.permissions)
|
||||
|
||||
// Setup the mock
|
||||
provisioningMock := provisioning.NewProvisioningServiceMock()
|
||||
hs.ProvisioningService = provisioningMock
|
||||
|
||||
sc.resp = httptest.NewRecorder()
|
||||
var err error
|
||||
sc.req, err = http.NewRequest(http.MethodPost, test.url, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
sc.exec()
|
||||
|
||||
// Check return code
|
||||
assert.Equal(t, test.expectedCode, sc.resp.Code)
|
||||
if test.exit {
|
||||
return
|
||||
}
|
||||
|
||||
// Check body
|
||||
assert.Equal(t, test.expectedBody, sc.resp.Body.String())
|
||||
|
||||
// Check we actually called the provisioning service
|
||||
test.checkCall(*provisioningMock)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -445,10 +445,11 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
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))
|
||||
adminRoute.Post("/provisioning/plugins/reload", reqGrafanaAdmin, routing.Wrap(hs.AdminProvisioningReloadPlugins))
|
||||
adminRoute.Post("/provisioning/datasources/reload", reqGrafanaAdmin, routing.Wrap(hs.AdminProvisioningReloadDatasources))
|
||||
adminRoute.Post("/provisioning/notifications/reload", reqGrafanaAdmin, routing.Wrap(hs.AdminProvisioningReloadNotifications))
|
||||
adminRoute.Post("/provisioning/dashboards/reload", authorize(reqGrafanaAdmin, ActionProvisioningReload, ScopeProvisionersDashboards), routing.Wrap(hs.AdminProvisioningReloadDashboards))
|
||||
adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ActionProvisioningReload, ScopeProvisionersPlugins), routing.Wrap(hs.AdminProvisioningReloadPlugins))
|
||||
adminRoute.Post("/provisioning/datasources/reload", authorize(reqGrafanaAdmin, ActionProvisioningReload, ScopeProvisionersDatasources), routing.Wrap(hs.AdminProvisioningReloadDatasources))
|
||||
adminRoute.Post("/provisioning/notifications/reload", authorize(reqGrafanaAdmin, ActionProvisioningReload, ScopeProvisionersNotifications), routing.Wrap(hs.AdminProvisioningReloadNotifications))
|
||||
|
||||
adminRoute.Post("/ldap/reload", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPConfigReload), routing.Wrap(hs.ReloadLDAPCfg))
|
||||
adminRoute.Post("/ldap/sync/:id", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPUsersSync), routing.Wrap(hs.PostSyncUserWithLDAP))
|
||||
adminRoute.Get("/ldap/:username", authorize(reqGrafanaAdmin, accesscontrol.ActionLDAPUsersRead), routing.Wrap(hs.GetUserFromLDAP))
|
||||
|
||||
@@ -253,6 +253,10 @@ func (f *fakeAccessControl) IsDisabled() bool {
|
||||
return f.isDisabled
|
||||
}
|
||||
|
||||
func (f *fakeAccessControl) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -117,8 +117,7 @@ func (hs *HTTPServer) Init() error {
|
||||
|
||||
hs.macaron = hs.newMacaron()
|
||||
hs.registerRoutes()
|
||||
|
||||
return nil
|
||||
return hs.declareFixedRoles()
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) AddMiddleware(middleware macaron.Handler) {
|
||||
|
||||
41
pkg/api/roles.go
Normal file
41
pkg/api/roles.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
// API related actions
|
||||
const (
|
||||
ActionProvisioningReload = "provisioning:reload"
|
||||
)
|
||||
|
||||
// API related scopes
|
||||
const (
|
||||
ScopeProvisionersAll = "provisioners:*"
|
||||
ScopeProvisionersDashboards = "provisioners:dashboards"
|
||||
ScopeProvisionersPlugins = "provisioners:plugins"
|
||||
ScopeProvisionersDatasources = "provisioners:datasources"
|
||||
ScopeProvisionersNotifications = "provisioners:notifications"
|
||||
)
|
||||
|
||||
// declareFixedRoles declares to the AccessControl service fixed roles and their
|
||||
// grants to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
|
||||
// that HTTPServer needs
|
||||
func (hs *HTTPServer) declareFixedRoles() error {
|
||||
registration := accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Version: 1,
|
||||
Name: "fixed:provisioning:admin",
|
||||
Description: "Reload provisioning configurations",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{
|
||||
Action: ActionProvisioningReload,
|
||||
Scope: ScopeProvisionersAll,
|
||||
},
|
||||
},
|
||||
},
|
||||
Grants: []string{accesscontrol.RoleGrafanaAdmin},
|
||||
}
|
||||
|
||||
return hs.AccessControl.DeclareFixedRoles(registration)
|
||||
}
|
||||
Reference in New Issue
Block a user