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