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:
Gabriel MABILLE
2021-07-30 09:52:09 +02:00
committed by GitHub
parent faf1653230
commit 88c11f1cc0
14 changed files with 954 additions and 232 deletions

View 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)
})
}
}

View File

@@ -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))

View File

@@ -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

View File

@@ -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
View 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)
}