Access control: SQL filtering for annotation listing (#47467)

* pass in user to attribute scope resolver

* add SQL filter to annotation listing

* check annotation FGAC permissions before exposing them for commenting

* remove the requirement to be able to list all annotations from annotation listing endpoint

* adding tests for annotation listing

* remove changes that got moved to a different PR

* unused var

* Update pkg/services/sqlstore/annotation.go

Co-authored-by: Ezequiel Victorero <evictorero@gmail.com>

* remove unneeded check

* remove unneeded check

* undo accidental change

* undo accidental change

* doc update

* move tests

* redo the approach for passing the user in for scope resolution

* accidental change

* cleanup

* error handling

Co-authored-by: Ezequiel Victorero <evictorero@gmail.com>
This commit is contained in:
Ieva
2022-04-11 13:18:38 +01:00
committed by GitHub
parent 4bc582570e
commit ef4c2672b3
10 changed files with 258 additions and 39 deletions

View File

@@ -9,7 +9,11 @@ import (
"time"
"github.com/grafana/grafana/pkg/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
)
// Update the item so that EpochEnd >= Epoch
@@ -33,6 +37,10 @@ type SQLAnnotationRepo struct {
sql *SQLStore
}
func NewSQLAnnotationRepo(sql *SQLStore) SQLAnnotationRepo {
return SQLAnnotationRepo{sql: sql}
}
func (r *SQLAnnotationRepo) Save(item *annotations.Item) error {
return inTransaction(func(sess *DBSession) error {
tags := models.ParseTagPairs(item.Tags)
@@ -221,6 +229,15 @@ func (r *SQLAnnotationRepo) Find(ctx context.Context, query *annotations.ItemQue
}
}
if r.sql.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
acFilter, acArgs, err := getAccessControlFilter(query.SignedInUser)
if err != nil {
return err
}
sql.WriteString(fmt.Sprintf(" AND (%s)", acFilter))
params = append(params, acArgs...)
}
if query.Limit == 0 {
query.Limit = 100
}
@@ -239,6 +256,37 @@ func (r *SQLAnnotationRepo) Find(ctx context.Context, query *annotations.ItemQue
return items, err
}
func getAccessControlFilter(user *models.SignedInUser) (string, []interface{}, error) {
if user == nil || user.Permissions[user.OrgId] == nil {
return "", nil, errors.New("missing permissions")
}
scopes, has := user.Permissions[user.OrgId][ac.ActionAnnotationsRead]
if !has {
return "", nil, errors.New("missing permissions")
}
types, hasWildcardScope := ac.ParseScopes(ac.ScopeAnnotationsProvider.GetResourceScopeType(""), scopes)
if hasWildcardScope {
types = map[interface{}]struct{}{annotations.Dashboard.String(): {}, annotations.Organization.String(): {}}
}
var filters []string
var params []interface{}
for t := range types {
// annotation read permission with scope annotations:type:organization allows listing annotations that are not associated with a dashboard
if t == annotations.Organization.String() {
filters = append(filters, "a.dashboard_id = 0")
}
// annotation read permission with scope annotations:type:dashboard allows listing annotations from dashboards which the user can view
if t == annotations.Dashboard.String() {
dashboardFilter, dashboardParams := permissions.NewAccessControlDashboardPermissionFilter(user, models.PERMISSION_VIEW, searchstore.TypeDashboard).Where()
filter := fmt.Sprintf("a.dashboard_id IN(SELECT id FROM dashboard WHERE %s)", dashboardFilter)
filters = append(filters, filter)
params = dashboardParams
}
}
return strings.Join(filters, " OR "), params, nil
}
func (r *SQLAnnotationRepo) Delete(ctx context.Context, params *annotations.DeleteParams) error {
return r.sql.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
var (

View File

@@ -1,27 +1,32 @@
//go:build integration
// +build integration
package sqlstore
package sqlstore_test
import (
"context"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
func TestAnnotations(t *testing.T) {
mockTimeNow()
defer resetTimeNow()
repo := SQLAnnotationRepo{}
repo.sql = InitTestDB(t)
sql := sqlstore.InitTestDB(t)
repo := sqlstore.NewSQLAnnotationRepo(sql)
t.Run("Testing annotation create, read, update and delete", func(t *testing.T) {
t.Cleanup(func() {
err := repo.sql.WithDbSession(context.Background(), func(dbSession *DBSession) error {
err := sql.WithDbSession(context.Background(), func(dbSession *sqlstore.DBSession) error {
_, err := dbSession.Exec("DELETE FROM annotation WHERE 1=1")
if err != nil {
return err
@@ -331,3 +336,136 @@ func TestAnnotations(t *testing.T) {
})
})
}
func TestAnnotationListingWithFGAC(t *testing.T) {
sql := sqlstore.InitTestDB(t)
sql.Cfg.IsFeatureToggleEnabled = func(key string) bool {
return key == featuremgmt.FlagAccesscontrol
}
repo := sqlstore.NewSQLAnnotationRepo(sql)
dashboardStore := dashboardstore.ProvideDashboardStore(sql)
testDashboard1 := models.SaveDashboardCommand{
UserId: 1,
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dashboard 1",
}),
}
dashboard, err := dashboardStore.SaveDashboard(testDashboard1)
require.NoError(t, err)
dash1UID := dashboard.Uid
testDashboard2 := models.SaveDashboardCommand{
UserId: 1,
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dashboard 2",
}),
}
_, err = dashboardStore.SaveDashboard(testDashboard2)
require.NoError(t, err)
dash1Annotation := &annotations.Item{
OrgId: 1,
DashboardId: 1,
Epoch: 10,
}
err = repo.Save(dash1Annotation)
require.NoError(t, err)
dash2Annotation := &annotations.Item{
OrgId: 1,
DashboardId: 2,
Epoch: 10,
}
err = repo.Save(dash2Annotation)
require.NoError(t, err)
organizationAnnotation := &annotations.Item{
OrgId: 1,
Epoch: 10,
}
err = repo.Save(organizationAnnotation)
require.NoError(t, err)
user := &models.SignedInUser{
UserId: 1,
OrgId: 1,
}
type testStruct struct {
description string
permissions map[string][]string
expectedAnnotationIds []int64
expectedError bool
}
testCases := []testStruct{
{
description: "Should find all annotations when has permissions to list all annotations and read all dashboards",
permissions: map[string][]string{
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsAll},
accesscontrol.ActionDashboardsRead: {accesscontrol.ScopeDashboardsAll},
},
expectedAnnotationIds: []int64{dash1Annotation.Id, dash2Annotation.Id, organizationAnnotation.Id},
},
{
description: "Should find all dashboard annotations",
permissions: map[string][]string{
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeDashboard},
accesscontrol.ActionDashboardsRead: {accesscontrol.ScopeDashboardsAll},
},
expectedAnnotationIds: []int64{dash1Annotation.Id, dash2Annotation.Id},
},
{
description: "Should find only annotations from dashboards that user can read",
permissions: map[string][]string{
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeDashboard},
accesscontrol.ActionDashboardsRead: {fmt.Sprintf("dashboards:uid:%s", dash1UID)},
},
expectedAnnotationIds: []int64{dash1Annotation.Id},
},
{
description: "Should find no annotations if user can't view dashboards or organization annotations",
permissions: map[string][]string{
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeDashboard},
},
expectedAnnotationIds: []int64{},
},
{
description: "Should find only organization annotations",
permissions: map[string][]string{
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeOrganization},
accesscontrol.ActionDashboardsRead: {accesscontrol.ScopeDashboardsAll},
},
expectedAnnotationIds: []int64{organizationAnnotation.Id},
},
{
description: "Should error if user doesn't have annotation read permissions",
permissions: map[string][]string{
accesscontrol.ActionDashboardsRead: {accesscontrol.ScopeDashboardsAll},
},
expectedError: true,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
user.Permissions = map[int64]map[string][]string{1: tc.permissions}
results, err := repo.Find(context.Background(), &annotations.ItemQuery{
OrgId: 1,
SignedInUser: user,
})
if tc.expectedError {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Len(t, results, len(tc.expectedAnnotationIds))
for _, r := range results {
assert.Contains(t, tc.expectedAnnotationIds, r.Id)
}
})
}
}