Search v1: Add support for inherited folder permissions if nested folders are enabled (#63275)

* Add features dependency to SQLBuilder

* Add features dependency to AccessControlDashboardPermissionFilter

* Add test for folder inheritance

* Dashboard permissions: Return recursive query

* Recursive query for inherited folders

* Modify search builder

* Adjust db.SQLBuilder

* Pass flag to SQLbuilder if CTEs are supported

* Add support for mysql < 8.0

* Add benchmarking for search with nested folders

* Set features to AlertStore

* Update pkg/infra/db/sqlbuilder.go

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* Set features to LibraryElementService

* SQLBuilder tests with nested folder flag set

* Apply suggestion from code review

Co-authored-by: IevaVasiljeva <ieva.vasiljeva@grafana.com>
Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>
This commit is contained in:
Sofia Papagiannaki
2023-04-06 11:16:15 +03:00
committed by GitHub
co-authored by Ieva Emil Tullstedt
parent 2648fcb833
commit 988a120d6d
24 changed files with 1487 additions and 105 deletions
@@ -44,6 +44,9 @@ func (b *Builder) ToSQL(limit, page int64) (string, []interface{}) {
}
func (b *Builder) buildSelect() {
var recQuery string
var recQueryParams []interface{}
b.sql.WriteString(
`SELECT
dashboard.id,
@@ -61,9 +64,25 @@ func (b *Builder) buildSelect() {
if f, ok := f.(FilterSelect); ok {
b.sql.WriteString(fmt.Sprintf(", %s", f.Select()))
}
if f, ok := f.(FilterWith); ok {
recQuery, recQueryParams = f.With()
}
}
b.sql.WriteString(` FROM `)
if recQuery == "" {
return
}
// prepend recursive queries
var bf bytes.Buffer
bf.WriteString(recQuery)
bf.WriteString(b.sql.String())
b.sql = bf
b.params = append(recQueryParams, b.params...)
}
func (b *Builder) applyFilters() (ordering string) {
@@ -14,6 +14,12 @@ type FilterWhere interface {
Where() (string, []interface{})
}
// FilterWith returns any recursive CTE queries (if supported)
// and their parameters
type FilterWith interface {
With() (string, []interface{})
}
// FilterGroupBy should be used after performing an outer join on the
// search result to ensure there is only one of each ID in the results.
// The id column must be present in the result.
@@ -10,7 +10,9 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
@@ -148,6 +150,145 @@ func TestBuilder_Permissions(t *testing.T) {
assert.Len(t, res, 0)
}
func TestBuilder_RBAC(t *testing.T) {
testsCases := []struct {
desc string
userPermissions []accesscontrol.Permission
features featuremgmt.FeatureToggles
expectedParams []interface{}
}{
{
desc: "no user permissions",
features: featuremgmt.WithFeatures(),
expectedParams: []interface{}{
int64(1),
},
},
{
desc: "user with view permission",
userPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"},
},
features: featuremgmt.WithFeatures(),
expectedParams: []interface{}{
int64(1),
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"dashboards:read",
"dashboards:write",
2,
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"dashboards:read",
"dashboards:write",
2,
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"folders:read",
"dashboards:create",
2,
},
},
{
desc: "user with view permission with nesting",
userPermissions: []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"},
},
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
expectedParams: []interface{}{
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"dashboards:read",
"dashboards:write",
2,
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"folders:read",
"dashboards:create",
2,
int64(1),
int64(1),
int64(1),
0,
"Viewer",
int64(1),
0,
"dashboards:read",
"dashboards:write",
2,
},
},
}
user := &user.SignedInUser{
UserID: 1,
OrgID: 1,
OrgRole: org.RoleViewer,
}
store := setupTestEnvironment(t)
createDashboards(t, store, 0, 1, user.OrgID)
recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
require.NoError(t, err)
for _, tc := range testsCases {
t.Run(tc.desc, func(t *testing.T) {
if len(tc.userPermissions) > 0 {
user.Permissions = map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tc.userPermissions)}
}
level := dashboards.PERMISSION_EDIT
builder := &searchstore.Builder{
Filters: []interface{}{
searchstore.OrgFilter{OrgId: user.OrgID},
searchstore.TitleSorter{},
permissions.NewAccessControlDashboardPermissionFilter(
user,
level,
"",
tc.features,
recursiveQueriesAreSupported,
),
},
Dialect: store.GetDialect(),
}
res := []dashboards.DashboardSearchProjection{}
err := store.WithDbSession(context.Background(), func(sess *db.Session) error {
sql, params := builder.ToSQL(limit, page)
// TODO: replace with a proper test
assert.Equal(t, tc.expectedParams, params)
return sess.SQL(sql, params...).Find(&res)
})
require.NoError(t, err)
assert.Len(t, res, 0)
})
}
}
func setupTestEnvironment(t *testing.T) db.DB {
t.Helper()
store := db.InitTestDB(t)