mirror of https://github.com/grafana/grafana
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>pull/37395/head
parent
faf1653230
commit
88c11f1cc0
@ -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) |
||||
}) |
||||
} |
||||
} |
@ -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) |
||||
} |
@ -0,0 +1,8 @@ |
||||
package accesscontrol |
||||
|
||||
import "errors" |
||||
|
||||
var ( |
||||
ErrFixedRolePrefixMissing = errors.New("fixed role should be prefixed with '" + FixedRolePrefix + "'") |
||||
ErrInvalidBuiltinRole = errors.New("built-in role is not valid") |
||||
) |
Loading…
Reference in new issue