mirror of
https://github.com/grafana/grafana.git
synced 2026-08-18 00:54:58 -05:00
AccessControl: keyword scope resolution (#40229)
This commit is contained in:
@@ -17,6 +17,7 @@ func ProvideService(cfg *setting.Cfg, usageStats usagestats.Service) *OSSAccessC
|
|||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
UsageStats: usageStats,
|
UsageStats: usageStats,
|
||||||
Log: log.New("accesscontrol"),
|
Log: log.New("accesscontrol"),
|
||||||
|
scopeResolver: accesscontrol.NewScopeResolver(),
|
||||||
}
|
}
|
||||||
s.registerUsageMetrics()
|
s.registerUsageMetrics()
|
||||||
return s
|
return s
|
||||||
@@ -28,6 +29,7 @@ type OSSAccessControlService struct {
|
|||||||
UsageStats usagestats.Service
|
UsageStats usagestats.Service
|
||||||
Log log.Logger
|
Log log.Logger
|
||||||
registrations accesscontrol.RegistrationList
|
registrations accesscontrol.RegistrationList
|
||||||
|
scopeResolver accesscontrol.ScopeResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *OSSAccessControlService) IsDisabled() bool {
|
func (ac *OSSAccessControlService) IsDisabled() bool {
|
||||||
@@ -65,6 +67,7 @@ func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *models.Si
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return evaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions))
|
return evaluator.Evaluate(accesscontrol.GroupScopesByAction(permissions))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,8 +86,12 @@ func (ac *OSSAccessControlService) GetUserPermissions(ctx context.Context, user
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, p := range role.Permissions {
|
for _, p := range role.Permissions {
|
||||||
permission := p
|
// if the permission has a keyword in its scope it will be resolved
|
||||||
permissions = append(permissions, &permission)
|
permission, err := ac.scopeResolver.ResolveKeyword(user, p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
permissions = append(permissions, permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,15 @@ func removeRoleHelper(role string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractRawPermissionsHelper extracts action and scope fields only from a permission slice
|
||||||
|
func extractRawPermissionsHelper(perms []*accesscontrol.Permission) []*accesscontrol.Permission {
|
||||||
|
res := make([]*accesscontrol.Permission, len(perms))
|
||||||
|
for i, p := range perms {
|
||||||
|
res[i] = &accesscontrol.Permission{Action: p.Action, Scope: p.Scope}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
type evaluatingPermissionsTestCase struct {
|
type evaluatingPermissionsTestCase struct {
|
||||||
desc string
|
desc string
|
||||||
user userTestCase
|
user userTestCase
|
||||||
@@ -490,3 +499,85 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOSSAccessControlService_GetUserPermissions(t *testing.T) {
|
||||||
|
testUser := &models.SignedInUser{
|
||||||
|
UserId: 2,
|
||||||
|
OrgId: 3,
|
||||||
|
OrgName: "TestOrg",
|
||||||
|
OrgRole: models.ROLE_VIEWER,
|
||||||
|
Login: "testUser",
|
||||||
|
Name: "Test User",
|
||||||
|
Email: "testuser@example.org",
|
||||||
|
}
|
||||||
|
registration := accesscontrol.RoleRegistration{
|
||||||
|
Role: accesscontrol.RoleDTO{
|
||||||
|
Version: 1,
|
||||||
|
UID: "fixed:test:test",
|
||||||
|
Name: "fixed:test:test",
|
||||||
|
Description: "Test role",
|
||||||
|
Permissions: []accesscontrol.Permission{},
|
||||||
|
},
|
||||||
|
Grants: []string{"Viewer"},
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
user *models.SignedInUser
|
||||||
|
rawPerm accesscontrol.Permission
|
||||||
|
wantPerm accesscontrol.Permission
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Translate orgs:current",
|
||||||
|
user: testUser,
|
||||||
|
rawPerm: accesscontrol.Permission{Action: "orgs:read", Scope: "orgs:current"},
|
||||||
|
wantPerm: accesscontrol.Permission{Action: "orgs:read", Scope: "orgs:id:3"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Translate users:self",
|
||||||
|
user: testUser,
|
||||||
|
rawPerm: accesscontrol.Permission{Action: "users:read", Scope: "users:self"},
|
||||||
|
wantPerm: accesscontrol.Permission{Action: "users:read", Scope: "users:id:2"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Remove any inserted role after the test case has been run
|
||||||
|
t.Cleanup(func() {
|
||||||
|
removeRoleHelper(registration.Role.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
ac := &OSSAccessControlService{
|
||||||
|
Cfg: setting.NewCfg(),
|
||||||
|
UsageStats: &usagestats.UsageStatsMock{T: t},
|
||||||
|
Log: log.New("accesscontrol-test"),
|
||||||
|
registrations: accesscontrol.RegistrationList{},
|
||||||
|
scopeResolver: accesscontrol.NewScopeResolver(),
|
||||||
|
}
|
||||||
|
ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true}
|
||||||
|
|
||||||
|
registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm}
|
||||||
|
err := ac.DeclareFixedRoles(registration)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = ac.RegisterFixedRoles()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
userPerms, err := ac.GetUserPermissions(context.TODO(), tt.user)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err, "Expected an error with GetUserPermissions.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err, "Did not expect an error with GetUserPermissions.")
|
||||||
|
|
||||||
|
rawUserPerms := extractRawPermissionsHelper(userPerms)
|
||||||
|
|
||||||
|
assert.Contains(t, rawUserPerms, &tt.wantPerm, "Expected resolution of raw permission")
|
||||||
|
assert.NotContains(t, rawUserPerms, &tt.rawPerm, "Expected raw permission to have been resolved")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package accesscontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeywordScopeResolveFunc func(*models.SignedInUser) (string, error)
|
||||||
|
|
||||||
|
// ScopeResolver contains a map of functions to resolve scope keywords such as `self` or `current` into `id` based scopes
|
||||||
|
type ScopeResolver struct {
|
||||||
|
keywordResolvers map[string]KeywordScopeResolveFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScopeResolver() ScopeResolver {
|
||||||
|
return ScopeResolver{
|
||||||
|
keywordResolvers: map[string]KeywordScopeResolveFunc{
|
||||||
|
"orgs:current": resolveCurrentOrg,
|
||||||
|
"users:self": resolveUserSelf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveCurrentOrg(u *models.SignedInUser) (string, error) {
|
||||||
|
return Scope("orgs", "id", fmt.Sprintf("%v", u.OrgId)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveUserSelf(u *models.SignedInUser) (string, error) {
|
||||||
|
return Scope("users", "id", fmt.Sprintf("%v", u.UserId)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveKeyword resolves scope with keywords such as `self` or `current` into `id` based scopes
|
||||||
|
func (s *ScopeResolver) ResolveKeyword(user *models.SignedInUser, permission Permission) (*Permission, error) {
|
||||||
|
if fn, ok := s.keywordResolvers[permission.Scope]; ok {
|
||||||
|
resolvedScope, err := fn(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not resolve %v: %v", permission.Scope, err)
|
||||||
|
}
|
||||||
|
permission.Scope = resolvedScope
|
||||||
|
}
|
||||||
|
return &permission, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package accesscontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testUser = &models.SignedInUser{
|
||||||
|
UserId: 2,
|
||||||
|
OrgId: 3,
|
||||||
|
OrgName: "TestOrg",
|
||||||
|
OrgRole: models.ROLE_VIEWER,
|
||||||
|
Login: "testUser",
|
||||||
|
Name: "Test User",
|
||||||
|
Email: "testuser@example.org",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveKeywordedScope(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
user *models.SignedInUser
|
||||||
|
permission Permission
|
||||||
|
want *Permission
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no scope",
|
||||||
|
user: testUser,
|
||||||
|
permission: Permission{Action: "users:read"},
|
||||||
|
want: &Permission{Action: "users:read"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user if resolution",
|
||||||
|
user: testUser,
|
||||||
|
permission: Permission{Action: "users:read", Scope: "users:self"},
|
||||||
|
want: &Permission{Action: "users:read", Scope: "users:id:2"},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
resolver := NewScopeResolver()
|
||||||
|
resolved, err := resolver.ResolveKeyword(tt.user, tt.permission)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err, "expected an error during the resolution of the scope")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, tt.want, resolved, "permission did not match expected resolution")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user