Access Control: Refactor scope resolvers with support to resolve into several scopes (#48202)

* Refactor Scope resolver to support resolving into several scopes

* Change permission evaluator to match at least one of passed scopes
This commit is contained in:
Karl Persson
2022-05-02 09:29:30 +02:00
committed by GitHub
parent 9622e7457e
commit de50f39c12
18 changed files with 453 additions and 434 deletions

View File

@@ -28,49 +28,47 @@ var (
ScopeFoldersProvider = ac.NewScopeProvider(ScopeFoldersRoot)
)
// NewNameScopeResolver provides an AttributeScopeResolver that is able to convert a scope prefixed with "folders:name:" into an uid based scope.
func NewNameScopeResolver(db Store) (string, ac.AttributeScopeResolveFunc) {
// NewFolderNameScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:name:" into an uid based scope.
func NewFolderNameScopeResolver(db Store) (string, ac.ScopeAttributeResolver) {
prefix := ScopeFoldersProvider.GetResourceScopeName("")
resolver := func(ctx context.Context, orgID int64, scope string) (string, error) {
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
if !strings.HasPrefix(scope, prefix) {
return "", ac.ErrInvalidScope
return nil, ac.ErrInvalidScope
}
nsName := scope[len(prefix):]
if len(nsName) == 0 {
return "", ac.ErrInvalidScope
return nil, ac.ErrInvalidScope
}
folder, err := db.GetFolderByTitle(ctx, orgID, nsName)
if err != nil {
return "", err
return nil, err
}
return ScopeFoldersProvider.GetResourceScopeUID(folder.Uid), nil
}
return prefix, resolver
return []string{ScopeFoldersProvider.GetResourceScopeUID(folder.Uid)}, nil
})
}
// NewIDScopeResolver provides an AttributeScopeResolver that is able to convert a scope prefixed with "folders:id:" into an uid based scope.
func NewIDScopeResolver(db Store) (string, ac.AttributeScopeResolveFunc) {
// NewFolderIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:id:" into an uid based scope.
func NewFolderIDScopeResolver(db Store) (string, ac.ScopeAttributeResolver) {
prefix := ScopeFoldersProvider.GetResourceScope("")
resolver := func(ctx context.Context, orgID int64, scope string) (string, error) {
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
if !strings.HasPrefix(scope, prefix) {
return "", ac.ErrInvalidScope
return nil, ac.ErrInvalidScope
}
id, err := strconv.ParseInt(scope[len(prefix):], 10, 64)
if err != nil {
return "", ac.ErrInvalidScope
return nil, ac.ErrInvalidScope
}
if id == 0 {
return ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID), nil
return []string{ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)}, nil
}
folder, err := db.GetFolderByID(ctx, orgID, id)
if err != nil {
return "", err
return nil, err
}
return ScopeFoldersProvider.GetResourceScopeUID(folder.Uid), nil
}
return prefix, resolver
return []string{ScopeFoldersProvider.GetResourceScopeUID(folder.Uid)}, nil
})
}

View File

@@ -15,16 +15,16 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func TestNewNameScopeResolver(t *testing.T) {
func TestNewFolderNameScopeResolver(t *testing.T) {
t.Run("prefix should be expected", func(t *testing.T) {
prefix, _ := NewNameScopeResolver(&FakeDashboardStore{})
prefix, _ := NewFolderNameScopeResolver(&FakeDashboardStore{})
require.Equal(t, "folders:name:", prefix)
})
t.Run("resolver should convert to uid scope", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewNameScopeResolver(dashboardStore)
_, resolver := NewFolderNameScopeResolver(dashboardStore)
orgId := rand.Int63()
title := "Very complex :title with: and /" + util.GenerateShortUID()
@@ -36,53 +36,54 @@ func TestNewNameScopeResolver(t *testing.T) {
scope := "folders:name:" + title
resolvedScope, err := resolver(context.Background(), orgId, scope)
resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope)
require.NoError(t, err)
require.Len(t, resolvedScopes, 1)
require.Equal(t, fmt.Sprintf("folders:uid:%v", db.Uid), resolvedScope)
require.Equal(t, fmt.Sprintf("folders:uid:%v", db.Uid), resolvedScopes[0])
dashboardStore.AssertCalled(t, "GetFolderByTitle", mock.Anything, orgId, title)
})
t.Run("resolver should fail if input scope is not expected", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewNameScopeResolver(dashboardStore)
_, resolver := NewFolderNameScopeResolver(dashboardStore)
_, err := resolver(context.Background(), rand.Int63(), "folders:id:123")
_, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:id:123")
require.ErrorIs(t, err, ac.ErrInvalidScope)
})
t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewNameScopeResolver(dashboardStore)
_, resolver := NewFolderNameScopeResolver(dashboardStore)
_, err := resolver(context.Background(), rand.Int63(), "folders:name:")
_, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:name:")
require.ErrorIs(t, err, ac.ErrInvalidScope)
})
t.Run("returns 'not found' if folder does not exist", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewNameScopeResolver(dashboardStore)
_, resolver := NewFolderNameScopeResolver(dashboardStore)
orgId := rand.Int63()
dashboardStore.On("GetFolderByTitle", mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrDashboardNotFound).Once()
scope := "folders:name:" + util.GenerateShortUID()
resolvedScope, err := resolver(context.Background(), orgId, scope)
resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope)
require.ErrorIs(t, err, models.ErrDashboardNotFound)
require.Empty(t, resolvedScope)
require.Nil(t, resolvedScopes)
})
}
func TestNewIDScopeResolver(t *testing.T) {
func TestNewFolderIDScopeResolver(t *testing.T) {
t.Run("prefix should be expected", func(t *testing.T) {
prefix, _ := NewIDScopeResolver(&FakeDashboardStore{})
prefix, _ := NewFolderIDScopeResolver(&FakeDashboardStore{})
require.Equal(t, "folders:id:", prefix)
})
t.Run("resolver should convert to uid scope", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewIDScopeResolver(dashboardStore)
_, resolver := NewFolderIDScopeResolver(dashboardStore)
orgId := rand.Int63()
uid := util.GenerateShortUID()
@@ -92,18 +93,18 @@ func TestNewIDScopeResolver(t *testing.T) {
scope := "folders:id:" + strconv.FormatInt(db.Id, 10)
resolvedScope, err := resolver(context.Background(), orgId, scope)
resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope)
require.NoError(t, err)
require.Equal(t, fmt.Sprintf("folders:uid:%v", db.Uid), resolvedScope)
require.Len(t, resolvedScopes, 1)
require.Equal(t, fmt.Sprintf("folders:uid:%v", db.Uid), resolvedScopes[0])
dashboardStore.AssertCalled(t, "GetFolderByID", mock.Anything, orgId, db.Id)
})
t.Run("resolver should fail if input scope is not expected", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewIDScopeResolver(dashboardStore)
_, resolver := NewFolderIDScopeResolver(dashboardStore)
_, err := resolver(context.Background(), rand.Int63(), "folders:uid:123")
_, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:uid:123")
require.ErrorIs(t, err, ac.ErrInvalidScope)
})
@@ -112,33 +113,34 @@ func TestNewIDScopeResolver(t *testing.T) {
dashboardStore = &FakeDashboardStore{}
orgId = rand.Int63()
scope = "folders:id:0"
_, resolver = NewIDScopeResolver(dashboardStore)
_, resolver = NewFolderIDScopeResolver(dashboardStore)
)
resolvedScope, err := resolver(context.Background(), orgId, scope)
resolved, err := resolver.Resolve(context.Background(), orgId, scope)
require.NoError(t, err)
require.Equal(t, "folders:uid:general", resolvedScope)
require.Len(t, resolved, 1)
require.Equal(t, "folders:uid:general", resolved[0])
})
t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewIDScopeResolver(dashboardStore)
_, resolver := NewFolderIDScopeResolver(dashboardStore)
_, err := resolver(context.Background(), rand.Int63(), "folders:id:")
_, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:id:")
require.ErrorIs(t, err, ac.ErrInvalidScope)
})
t.Run("returns 'not found' if folder does not exist", func(t *testing.T) {
dashboardStore := &FakeDashboardStore{}
_, resolver := NewIDScopeResolver(dashboardStore)
_, resolver := NewFolderIDScopeResolver(dashboardStore)
orgId := rand.Int63()
dashboardStore.On("GetFolderByID", mock.Anything, mock.Anything, mock.Anything).Return(nil, models.ErrDashboardNotFound).Once()
scope := "folders:id:10"
resolvedScope, err := resolver(context.Background(), orgId, scope)
resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope)
require.ErrorIs(t, err, models.ErrDashboardNotFound)
require.Empty(t, resolvedScope)
require.Nil(t, resolvedScopes)
})
}

View File

@@ -32,8 +32,8 @@ func ProvideFolderService(
searchService *search.SearchService, features featuremgmt.FeatureToggles, permissionsServices accesscontrol.PermissionsServices,
ac accesscontrol.AccessControl, sqlStore sqlstore.Store,
) *FolderServiceImpl {
ac.RegisterAttributeScopeResolver(dashboards.NewNameScopeResolver(dashboardStore))
ac.RegisterAttributeScopeResolver(dashboards.NewIDScopeResolver(dashboardStore))
ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(dashboardStore))
ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(dashboardStore))
return &FolderServiceImpl{
cfg: cfg,