2025-04-01 10:38:02 +09:00

520 lines
14 KiB
Go

package resourcepermissions
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/team/teamimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/userimpl"
"github.com/grafana/grafana/pkg/setting"
)
type setUserPermissionTest struct {
desc string
callHook bool
}
func TestService_SetUserPermission(t *testing.T) {
tests := []setUserPermissionTest{
{
desc: "should call hook when updating user permissions",
callHook: true,
},
{
desc: "should not call hook when updating user permissions",
callHook: false,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
service, usrSvc, _ := setupTestEnvironment(t, Options{
Resource: "dashboards",
Assignments: Assignments{Users: true},
PermissionsToActions: nil,
})
// seed user
user, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "test", OrgID: 1})
require.NoError(t, err)
var hookCalled bool
if tt.callHook {
service.options.OnSetUser = func(session *db.Session, orgID int64, user accesscontrol.User, resourceID, permission string) error {
hookCalled = true
return nil
}
}
_, err = service.SetUserPermission(context.Background(), user.OrgID, accesscontrol.User{ID: user.ID}, "1", "")
require.NoError(t, err)
assert.Equal(t, tt.callHook, hookCalled)
})
}
}
type setTeamPermissionTest struct {
desc string
callHook bool
}
func TestService_SetTeamPermission(t *testing.T) {
tests := []setTeamPermissionTest{
{
desc: "should call hook when updating user permissions",
callHook: true,
},
{
desc: "should not call hook when updating user permissions",
callHook: false,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
service, _, teamSvc := setupTestEnvironment(t, Options{
Resource: "dashboards",
Assignments: Assignments{Teams: true},
PermissionsToActions: nil,
})
// seed team
team, err := teamSvc.CreateTeam(context.Background(), "test", "test@test.com", 1)
require.NoError(t, err)
var hookCalled bool
if tt.callHook {
service.options.OnSetTeam = func(session *db.Session, orgID, teamID int64, resourceID, permission string) error {
hookCalled = true
return nil
}
}
_, err = service.SetTeamPermission(context.Background(), team.OrgID, team.ID, "1", "")
require.NoError(t, err)
assert.Equal(t, tt.callHook, hookCalled)
})
}
}
type setBuiltInRolePermissionTest struct {
desc string
callHook bool
}
func TestService_SetBuiltInRolePermission(t *testing.T) {
tests := []setBuiltInRolePermissionTest{
{
desc: "should call hook when updating user permissions",
callHook: true,
},
{
desc: "should not call hook when updating user permissions",
callHook: false,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
service, _, _ := setupTestEnvironment(t, Options{
Resource: "dashboards",
Assignments: Assignments{BuiltInRoles: true},
PermissionsToActions: nil,
})
var hookCalled bool
if tt.callHook {
service.options.OnSetBuiltInRole = func(session *db.Session, orgID int64, builtInRole, resourceID, permission string) error {
hookCalled = true
return nil
}
}
_, err := service.SetBuiltInRolePermission(context.Background(), 1, "Viewer", "1", "")
require.NoError(t, err)
assert.Equal(t, tt.callHook, hookCalled)
})
}
}
type setPermissionsTest struct {
desc string
options Options
commands []accesscontrol.SetResourcePermissionCommand
expectErr bool
}
func TestService_SetPermissions(t *testing.T) {
tests := []setPermissionsTest{
{
desc: "should set all permissions",
options: Options{
Resource: "dashboards",
Assignments: Assignments{
Users: true,
Teams: true,
BuiltInRoles: true,
},
PermissionsToActions: map[string][]string{
"View": {"dashboards:read"},
},
},
commands: []accesscontrol.SetResourcePermissionCommand{
{UserID: 1, Permission: "View"},
{TeamID: 1, Permission: "View"},
{BuiltinRole: "Editor", Permission: "View"},
},
},
{
desc: "should return error for invalid permission",
options: Options{
Resource: "dashboards",
Assignments: Assignments{
Users: true,
Teams: true,
BuiltInRoles: true,
},
PermissionsToActions: map[string][]string{
"View": {"dashboards:read"},
},
},
commands: []accesscontrol.SetResourcePermissionCommand{
{UserID: 1, Permission: "View"},
{TeamID: 1, Permission: "View"},
{BuiltinRole: "Editor", Permission: "Not real permission"},
},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
service, usrSvc, teamSvc := setupTestEnvironment(t, tt.options)
// seed user
_, err := usrSvc.Create(context.Background(), &user.CreateUserCommand{Login: "user", OrgID: 1})
require.NoError(t, err)
_, err = teamSvc.CreateTeam(context.Background(), "team", "", 1)
require.NoError(t, err)
permissions, err := service.SetPermissions(context.Background(), 1, "1", tt.commands...)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Len(t, permissions, len(tt.commands))
}
})
}
}
func TestService_RegisterActionSets(t *testing.T) {
type registerActionSetsTest struct {
desc string
actionSetsEnabled bool
options Options
expectedActionSets []ActionSet
}
tests := []registerActionSetsTest{
{
desc: "should register folder action sets if action sets are enabled",
actionSetsEnabled: true,
options: Options{
Resource: "folders",
PermissionsToActions: map[string][]string{
"View": {"folders:read", "dashboards:read"},
"Edit": {"folders:read", "dashboards:read", "folders:write", "dashboards:write"},
},
},
expectedActionSets: []ActionSet{
{
Action: "folders:view",
Actions: []string{"folders:read", "dashboards:read"},
},
{
Action: "folders:edit",
Actions: []string{"folders:read", "dashboards:read", "folders:write", "dashboards:write", "folders:create"},
},
},
},
{
desc: "should register dashboard action set if action sets are enabled",
actionSetsEnabled: true,
options: Options{
Resource: "dashboards",
PermissionsToActions: map[string][]string{
"View": {"dashboards:read"},
},
},
expectedActionSets: []ActionSet{
{
Action: "dashboards:view",
Actions: []string{"dashboards:read"},
},
},
},
{
desc: "should not register dashboard action set if action sets are not enabled",
actionSetsEnabled: false,
options: Options{
Resource: "dashboards",
PermissionsToActions: map[string][]string{
"View": {"dashboards:read"},
},
},
expectedActionSets: []ActionSet{},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
features := featuremgmt.WithFeatures()
if tt.actionSetsEnabled {
features = featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets)
}
ac := acimpl.ProvideAccessControl(features)
actionSets := NewActionSetService(features)
_, err := New(
setting.NewCfg(), tt.options, features, routing.NewRouteRegister(), licensingtest.NewFakeLicensing(),
ac, &actest.FakeService{}, db.InitTestDB(t), nil, nil, actionSets,
)
require.NoError(t, err)
if len(tt.expectedActionSets) > 0 {
for _, expectedActionSet := range tt.expectedActionSets {
actionSet := actionSets.ResolveActionSet(expectedActionSet.Action)
assert.ElementsMatch(t, expectedActionSet.Actions, actionSet)
}
} else {
// Check that action sets have not been registered
for permission := range tt.options.PermissionsToActions {
actionSetName := GetActionSetName(tt.options.Resource, permission)
assert.Nil(t, actionSets.ResolveActionSet(actionSetName))
}
}
})
}
}
func TestStore_RegisterActionSet(t *testing.T) {
type actionSetTest struct {
desc string
features featuremgmt.FeatureToggles
pluginID string
pluginActions []plugins.ActionSet
coreActionSets []ActionSet
expectedErr bool
expectedActionSets []ActionSet
}
tests := []actionSetTest{
{
desc: "should be able to register a plugin action set if the right feature toggles are enabled",
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
pluginID: "test-app",
pluginActions: []plugins.ActionSet{
{
Action: "folders:view",
Actions: []string{"test-app.resource:read"},
},
},
expectedActionSets: []ActionSet{
{
Action: "folders:view",
Actions: []string{"test-app.resource:read"},
},
},
},
{
desc: "should not register plugin action set if feature toggles are missing",
features: featuremgmt.WithFeatures(),
pluginID: "test-app",
pluginActions: []plugins.ActionSet{
{
Action: "folders:view",
Actions: []string{"test-app.resource:read"},
},
},
expectedActionSets: []ActionSet{},
},
{
desc: "should be able to register multiple plugin action sets",
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
pluginID: "test-app",
pluginActions: []plugins.ActionSet{
{
Action: "folders:view",
Actions: []string{"test-app.resource:read"},
},
{
Action: "folders:edit",
Actions: []string{"test-app.resource:write", "test-app.resource:delete"},
},
},
expectedActionSets: []ActionSet{
{
Action: "folders:view",
Actions: []string{"test-app.resource:read"},
},
{
Action: "folders:edit",
Actions: []string{"test-app.resource:write", "test-app.resource:delete"},
},
},
},
{
desc: "action set actions should be added not replaced",
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
pluginID: "test-app",
pluginActions: []plugins.ActionSet{
{
Action: "folders:view",
Actions: []string{"test-app.resource:read"},
},
{
Action: "folders:edit",
Actions: []string{"test-app.resource:write", "test-app.resource:delete"},
},
},
coreActionSets: []ActionSet{
{
Action: "folders:view",
Actions: []string{"folders:read"},
},
{
Action: "folders:edit",
Actions: []string{"folders:write", "folders:delete"},
},
{
Action: "folders:admin",
Actions: []string{"folders.permissions:read"},
},
},
expectedActionSets: []ActionSet{
{
Action: "folders:view",
Actions: []string{"folders:read", "test-app.resource:read"},
},
{
Action: "folders:edit",
Actions: []string{"folders:write", "test-app.resource:write", "folders:delete", "test-app.resource:delete"},
},
{
Action: "folders:admin",
Actions: []string{"folders.permissions:read"},
},
},
},
{
desc: "should not be able to register an action that doesn't have a plugin prefix",
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
pluginID: "test-app",
pluginActions: []plugins.ActionSet{
{
Action: "folders:view",
Actions: []string{"test-app.resource:read"},
},
{
Action: "folders:edit",
Actions: []string{"users:read", "test-app.resource:delete"},
},
},
expectedErr: true,
},
{
desc: "should not be able to register action set that is not in the allow list",
features: featuremgmt.WithFeatures(featuremgmt.FlagAccessActionSets),
pluginID: "test-app",
pluginActions: []plugins.ActionSet{
{
Action: "folders:super-admin",
Actions: []string{"test-app.resource:read"},
},
},
expectedErr: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
asService := NewActionSetService(tt.features)
err := asService.RegisterActionSets(context.Background(), tt.pluginID, tt.pluginActions)
if tt.expectedErr {
require.Error(t, err)
return
}
require.NoError(t, err)
for _, set := range tt.coreActionSets {
asService.StoreActionSet(set.Action, set.Actions)
}
for _, expected := range tt.expectedActionSets {
actions := asService.ResolveActionSet(expected.Action)
if expected.Action == "folders:edit" || expected.Action == "folders:admin" {
expected.Actions = append(expected.Actions, "folders:create")
}
assert.ElementsMatch(t, expected.Actions, actions)
}
if len(tt.expectedActionSets) == 0 {
for _, set := range tt.pluginActions {
registeredActions := asService.ResolveActionSet(set.Action)
assert.Empty(t, registeredActions, "no actions from plugin action sets should have been registered")
}
}
})
}
}
func setupTestEnvironment(t *testing.T, ops Options) (*Service, user.Service, team.Service) {
t.Helper()
sql := db.InitTestDB(t)
cfg := setting.NewCfg()
tracer := tracing.InitializeTracerForTest()
teamSvc, err := teamimpl.ProvideService(sql, cfg, tracer)
require.NoError(t, err)
orgSvc, err := orgimpl.ProvideService(sql, cfg, quotatest.New(false, nil))
require.NoError(t, err)
userSvc, err := userimpl.ProvideService(
sql, orgSvc, cfg, teamSvc, nil, tracer,
quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(),
)
require.NoError(t, err)
license := licensingtest.NewFakeLicensing()
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
acService := &actest.FakeService{}
features := featuremgmt.WithFeatures()
ac := acimpl.ProvideAccessControl(features)
service, err := New(
cfg, ops, features, routing.NewRouteRegister(), license,
ac, acService, sql, teamSvc, userSvc, NewActionSetService(features),
)
require.NoError(t, err)
return service, userSvc, teamSvc
}