mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
RBAC: Move service and evaluator to acimpl package (#54714)
* RBAC: Move access control evaluator to acimpl package * RBAC: Move service to acimpl package
This commit is contained in:
@@ -1,71 +0,0 @@
|
||||
package ossaccesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var _ accesscontrol.AccessControl = new(AccessControl)
|
||||
|
||||
func ProvideAccessControl(cfg *setting.Cfg, service accesscontrol.Service) *AccessControl {
|
||||
logger := log.New("accesscontrol")
|
||||
return &AccessControl{
|
||||
cfg, logger, accesscontrol.NewResolvers(logger), service,
|
||||
}
|
||||
}
|
||||
|
||||
type AccessControl struct {
|
||||
cfg *setting.Cfg
|
||||
log log.Logger
|
||||
resolvers accesscontrol.Resolvers
|
||||
service accesscontrol.Service
|
||||
}
|
||||
|
||||
func (a *AccessControl) Evaluate(ctx context.Context, user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
|
||||
timer := prometheus.NewTimer(metrics.MAccessEvaluationsSummary)
|
||||
defer timer.ObserveDuration()
|
||||
metrics.MAccessEvaluationCount.Inc()
|
||||
|
||||
if user.Permissions == nil {
|
||||
user.Permissions = map[int64]map[string][]string{}
|
||||
}
|
||||
|
||||
if _, ok := user.Permissions[user.OrgID]; !ok {
|
||||
permissions, err := a.service.GetUserPermissions(ctx, user, accesscontrol.Options{ReloadCache: true})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
user.Permissions[user.OrgID] = accesscontrol.GroupScopesByAction(permissions)
|
||||
}
|
||||
|
||||
// Test evaluation without scope resolver first, this will prevent 403 for wildcard scopes when resource does not exist
|
||||
if evaluator.Evaluate(user.Permissions[user.OrgID]) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
resolvedEvaluator, err := evaluator.MutateScopes(ctx, a.resolvers.GetScopeAttributeMutator(user.OrgID))
|
||||
if err != nil {
|
||||
if errors.Is(err, accesscontrol.ErrResolverNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return resolvedEvaluator.Evaluate(user.Permissions[user.OrgID]), nil
|
||||
}
|
||||
|
||||
func (a *AccessControl) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) {
|
||||
a.resolvers.AddScopeAttributeResolver(prefix, resolver)
|
||||
}
|
||||
|
||||
func (a *AccessControl) IsDisabled() bool {
|
||||
return accesscontrol.IsDisabled(a.cfg)
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package ossaccesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAccessControl_Evaluate(t *testing.T) {
|
||||
type testCase struct {
|
||||
desc string
|
||||
user user.SignedInUser
|
||||
evaluator accesscontrol.Evaluator
|
||||
resolverPrefix string
|
||||
expected bool
|
||||
expectedErr error
|
||||
resolver accesscontrol.ScopeAttributeResolver
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
desc: "expect user to have access when correct permission is stored on user",
|
||||
user: user.SignedInUser{
|
||||
OrgID: 1,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
1: {accesscontrol.ActionTeamsWrite: {"teams:*"}},
|
||||
},
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite, "teams:id:1"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "expect user to not have access without required permissions",
|
||||
user: user.SignedInUser{
|
||||
OrgID: 1,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
1: {accesscontrol.ActionTeamsWrite: {"teams:*"}},
|
||||
},
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission(accesscontrol.ActionOrgUsersWrite, "users:id:1"),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "expect user to have access when resolver translate scope",
|
||||
user: user.SignedInUser{
|
||||
OrgID: 1,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
1: {accesscontrol.ActionTeamsWrite: {"another:scope"}},
|
||||
},
|
||||
},
|
||||
evaluator: accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite, "teams:id:1"),
|
||||
resolverPrefix: "teams:id:",
|
||||
resolver: accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
||||
return []string{"another:scope"}, nil
|
||||
}),
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
fakeService := actest.FakeService{}
|
||||
ac := ProvideAccessControl(setting.NewCfg(), fakeService)
|
||||
|
||||
if tt.resolver != nil {
|
||||
ac.RegisterScopeAttributeResolver(tt.resolverPrefix, tt.resolver)
|
||||
}
|
||||
|
||||
hasAccess, err := ac.Evaluate(context.Background(), &tt.user, tt.evaluator)
|
||||
assert.Equal(t, tt.expected, hasAccess)
|
||||
if tt.expectedErr != nil {
|
||||
assert.Equal(t, tt.expectedErr, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package ossaccesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/api"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, store accesscontrol.Store, routeRegister routing.RouteRegister) (*Service, error) {
|
||||
service := ProvideOSSService(cfg, store)
|
||||
|
||||
if !accesscontrol.IsDisabled(cfg) {
|
||||
api.NewAccessControlAPI(routeRegister, service).RegisterAPIEndpoints()
|
||||
if err := accesscontrol.DeclareFixedRoles(service); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func ProvideOSSService(cfg *setting.Cfg, store accesscontrol.Store) *Service {
|
||||
s := &Service{
|
||||
cfg: cfg,
|
||||
store: store,
|
||||
log: log.New("accesscontrol.service"),
|
||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Service is the service implementing role based access control.
|
||||
type Service struct {
|
||||
log log.Logger
|
||||
cfg *setting.Cfg
|
||||
store accesscontrol.Store
|
||||
registrations accesscontrol.RegistrationList
|
||||
roles map[string]*accesscontrol.RoleDTO
|
||||
}
|
||||
|
||||
func (s *Service) GetUsageStats(_ context.Context) map[string]interface{} {
|
||||
enabled := 0
|
||||
if !accesscontrol.IsDisabled(s.cfg) {
|
||||
enabled = 1
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"stats.oss.accesscontrol.enabled.count": enabled,
|
||||
}
|
||||
}
|
||||
|
||||
var actionsToFetch = append(
|
||||
TeamAdminActions, append(DashboardAdminActions, FolderAdminActions...)...,
|
||||
)
|
||||
|
||||
// GetUserPermissions returns user permissions based on built-in roles
|
||||
func (s *Service) GetUserPermissions(ctx context.Context, user *user.SignedInUser, _ accesscontrol.Options) ([]accesscontrol.Permission, error) {
|
||||
timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary)
|
||||
defer timer.ObserveDuration()
|
||||
|
||||
permissions := make([]accesscontrol.Permission, 0)
|
||||
|
||||
for _, builtin := range accesscontrol.GetOrgRoles(user) {
|
||||
if basicRole, ok := s.roles[builtin]; ok {
|
||||
permissions = append(permissions, basicRole.Permissions...)
|
||||
}
|
||||
}
|
||||
|
||||
dbPermissions, err := s.store.GetUserPermissions(ctx, accesscontrol.GetUserPermissionsQuery{
|
||||
OrgID: user.OrgID,
|
||||
UserID: user.UserID,
|
||||
Roles: accesscontrol.GetOrgRoles(user),
|
||||
TeamIDs: user.Teams,
|
||||
Actions: actionsToFetch,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(permissions, dbPermissions...), nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteUserPermissions(ctx context.Context, orgID int64, userID int64) error {
|
||||
return s.store.DeleteUserPermissions(ctx, orgID, userID)
|
||||
}
|
||||
|
||||
// DeclareFixedRoles allow the caller to declare, to the service, fixed roles and their assignments
|
||||
// to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin"
|
||||
func (s *Service) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error {
|
||||
// If accesscontrol is disabled no need to register roles
|
||||
if accesscontrol.IsDisabled(s.cfg) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, r := range registrations {
|
||||
err := accesscontrol.ValidateFixedRole(r.Role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = accesscontrol.ValidateBuiltInRoles(r.Grants)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.registrations.Append(r)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterFixedRoles registers all declared roles in RAM
|
||||
func (s *Service) RegisterFixedRoles(ctx context.Context) error {
|
||||
// If accesscontrol is disabled no need to register roles
|
||||
if accesscontrol.IsDisabled(s.cfg) {
|
||||
return nil
|
||||
}
|
||||
s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
|
||||
for br := range accesscontrol.BuiltInRolesWithParents(registration.Grants) {
|
||||
if basicRole, ok := s.roles[br]; ok {
|
||||
basicRole.Permissions = append(basicRole.Permissions, registration.Role.Permissions...)
|
||||
} else {
|
||||
s.log.Error("Unknown builtin role", "builtInRole", br)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) IsDisabled() bool {
|
||||
return accesscontrol.IsDisabled(s.cfg)
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
package ossaccesscontrol
|
||||
|
||||
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/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func setupTestEnv(t testing.TB) *Service {
|
||||
t.Helper()
|
||||
cfg := setting.NewCfg()
|
||||
cfg.RBACEnabled = true
|
||||
|
||||
ac := &Service{
|
||||
cfg: cfg,
|
||||
log: log.New("accesscontrol"),
|
||||
registrations: accesscontrol.RegistrationList{},
|
||||
store: database.ProvideService(sqlstore.InitTestDB(t)),
|
||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||
}
|
||||
require.NoError(t, ac.RegisterFixedRoles(context.Background()))
|
||||
return ac
|
||||
}
|
||||
|
||||
func TestUsageMetrics(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
enabled bool
|
||||
expectedValue int
|
||||
}{
|
||||
{
|
||||
name: "Expecting metric with value 0",
|
||||
enabled: false,
|
||||
expectedValue: 0,
|
||||
},
|
||||
{
|
||||
name: "Expecting metric with value 1",
|
||||
enabled: true,
|
||||
expectedValue: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.RBACEnabled = tt.enabled
|
||||
|
||||
s, errInitAc := ProvideService(
|
||||
cfg,
|
||||
database.ProvideService(sqlstore.InitTestDB(t)),
|
||||
routing.NewRouteRegister(),
|
||||
)
|
||||
require.NoError(t, errInitAc)
|
||||
assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_DeclareFixedRoles(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
registrations []accesscontrol.RoleRegistration
|
||||
wantErr bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "should work with empty list",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should add registration",
|
||||
registrations: []accesscontrol.RoleRegistration{
|
||||
{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: "fixed:test:test",
|
||||
},
|
||||
Grants: []string{"Admin"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should fail registration invalid role name",
|
||||
registrations: []accesscontrol.RoleRegistration{
|
||||
{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: "custom:test:test",
|
||||
},
|
||||
Grants: []string{"Admin"},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
err: accesscontrol.ErrFixedRolePrefixMissing,
|
||||
},
|
||||
{
|
||||
name: "should fail registration invalid builtin role assignment",
|
||||
registrations: []accesscontrol.RoleRegistration{
|
||||
{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: "fixed:test:test",
|
||||
},
|
||||
Grants: []string{"WrongAdmin"},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
err: accesscontrol.ErrInvalidBuiltinRole,
|
||||
},
|
||||
{
|
||||
name: "should add multiple registrations at once",
|
||||
registrations: []accesscontrol.RoleRegistration{
|
||||
{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: "fixed:test:test",
|
||||
},
|
||||
Grants: []string{"Admin"},
|
||||
},
|
||||
{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: "fixed:test2:test2",
|
||||
},
|
||||
Grants: []string{"Admin"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ac := setupTestEnv(t)
|
||||
|
||||
// Reset the registations
|
||||
ac.registrations = accesscontrol.RegistrationList{}
|
||||
|
||||
// Test
|
||||
err := ac.DeclareFixedRoles(tt.registrations...)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, tt.err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
registrationCnt := 0
|
||||
ac.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
|
||||
registrationCnt++
|
||||
return true
|
||||
})
|
||||
assert.Equal(t, len(tt.registrations), registrationCnt,
|
||||
"expected service registration list to contain all test registrations")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_RegisterFixedRoles(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
token models.Licensing
|
||||
registrations []accesscontrol.RoleRegistration
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should work with empty list",
|
||||
},
|
||||
{
|
||||
name: "should register and assign role",
|
||||
registrations: []accesscontrol.RoleRegistration{
|
||||
{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: "fixed:test:test",
|
||||
Permissions: []accesscontrol.Permission{{Action: "test:test"}},
|
||||
},
|
||||
Grants: []string{"Editor"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should register and assign multiple roles",
|
||||
registrations: []accesscontrol.RoleRegistration{
|
||||
{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: "fixed:test:test",
|
||||
Permissions: []accesscontrol.Permission{{Action: "test:test"}},
|
||||
},
|
||||
Grants: []string{"Editor"},
|
||||
},
|
||||
{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: "fixed:test2:test2",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "test:test2"},
|
||||
{Action: "test:test3", Scope: "test:*"},
|
||||
},
|
||||
},
|
||||
Grants: []string{"Viewer"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ac := setupTestEnv(t)
|
||||
|
||||
ac.registrations.Append(tt.registrations...)
|
||||
|
||||
// Test
|
||||
err := ac.RegisterFixedRoles(context.Background())
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check
|
||||
for _, registration := range tt.registrations {
|
||||
// Check builtin roles (parents included) have been granted with the permissions
|
||||
for br := range accesscontrol.BuiltInRolesWithParents(registration.Grants) {
|
||||
builtinRole, ok := ac.roles[br]
|
||||
assert.True(t, ok)
|
||||
for _, expectedPermission := range registration.Role.Permissions {
|
||||
assert.Contains(t, builtinRole.Permissions, expectedPermission)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user