mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
committed by
GitHub
parent
2648fcb833
commit
988a120d6d
@@ -5,20 +5,26 @@ import (
|
|||||||
|
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewSqlBuilder(cfg *setting.Cfg, dialect migrator.Dialect) SQLBuilder {
|
func NewSqlBuilder(cfg *setting.Cfg, features featuremgmt.FeatureToggles, dialect migrator.Dialect, recursiveQueriesAreSupported bool) SQLBuilder {
|
||||||
return SQLBuilder{cfg: cfg, dialect: dialect}
|
return SQLBuilder{cfg: cfg, features: features, dialect: dialect, recursiveQueriesAreSupported: recursiveQueriesAreSupported}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SQLBuilder struct {
|
type SQLBuilder struct {
|
||||||
cfg *setting.Cfg
|
cfg *setting.Cfg
|
||||||
sql bytes.Buffer
|
features featuremgmt.FeatureToggles
|
||||||
params []interface{}
|
sql bytes.Buffer
|
||||||
|
params []interface{}
|
||||||
|
recQry string
|
||||||
|
recQryParams []interface{}
|
||||||
|
recursiveQueriesAreSupported bool
|
||||||
|
|
||||||
dialect migrator.Dialect
|
dialect migrator.Dialect
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,10 +37,22 @@ func (sb *SQLBuilder) Write(sql string, params ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sb *SQLBuilder) GetSQLString() string {
|
func (sb *SQLBuilder) GetSQLString() string {
|
||||||
return sb.sql.String()
|
if sb.recQry == "" {
|
||||||
|
return sb.sql.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var bf bytes.Buffer
|
||||||
|
bf.WriteString(sb.recQry)
|
||||||
|
bf.WriteString(sb.sql.String())
|
||||||
|
return bf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *SQLBuilder) GetParams() []interface{} {
|
func (sb *SQLBuilder) GetParams() []interface{} {
|
||||||
|
if len(sb.recQryParams) == 0 {
|
||||||
|
return sb.params
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.params = append(sb.recQryParams, sb.params...)
|
||||||
return sb.params
|
return sb.params
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,11 +62,15 @@ func (sb *SQLBuilder) AddParams(params ...interface{}) {
|
|||||||
|
|
||||||
func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, permission dashboards.PermissionType) {
|
func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, permission dashboards.PermissionType) {
|
||||||
var (
|
var (
|
||||||
sql string
|
sql string
|
||||||
params []interface{}
|
params []interface{}
|
||||||
|
recQry string
|
||||||
|
recQryParams []interface{}
|
||||||
)
|
)
|
||||||
if !ac.IsDisabled(sb.cfg) {
|
if !ac.IsDisabled(sb.cfg) {
|
||||||
sql, params = permissions.NewAccessControlDashboardPermissionFilter(user, permission, "").Where()
|
filterRBAC := permissions.NewAccessControlDashboardPermissionFilter(user, permission, "", sb.features, sb.recursiveQueriesAreSupported)
|
||||||
|
sql, params = filterRBAC.Where()
|
||||||
|
recQry, recQryParams = filterRBAC.With()
|
||||||
} else {
|
} else {
|
||||||
sql, params = permissions.DashboardPermissionFilter{
|
sql, params = permissions.DashboardPermissionFilter{
|
||||||
OrgRole: user.OrgRole,
|
OrgRole: user.OrgRole,
|
||||||
@@ -61,4 +83,6 @@ func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, pe
|
|||||||
|
|
||||||
sb.sql.WriteString(" AND " + sql)
|
sb.sql.WriteString(" AND " + sql)
|
||||||
sb.params = append(sb.params, params...)
|
sb.params = append(sb.params, params...)
|
||||||
|
sb.recQry = recQry
|
||||||
|
sb.recQryParams = recQryParams
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
@@ -24,6 +25,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("skipping integration test")
|
t.Skip("skipping integration test")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("WriteDashboardPermissionFilter", func(t *testing.T) {
|
t.Run("WriteDashboardPermissionFilter", func(t *testing.T) {
|
||||||
t.Run("user ACL", func(t *testing.T) {
|
t.Run("user ACL", func(t *testing.T) {
|
||||||
test(t,
|
test(t,
|
||||||
@@ -31,6 +33,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
|
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
|
||||||
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_VIEW},
|
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
shouldFind,
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(t,
|
test(t,
|
||||||
@@ -38,6 +41,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
|
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
|
||||||
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
|
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
shouldNotFind,
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(t,
|
test(t,
|
||||||
@@ -45,6 +49,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_EDIT},
|
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_EDIT},
|
||||||
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
|
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
shouldFind,
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(t,
|
test(t,
|
||||||
@@ -52,6 +57,41 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
|
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
|
||||||
Search{RequiredPermission: dashboards.PERMISSION_VIEW},
|
Search{RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
shouldNotFind,
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("user ACL with nested folders", func(t *testing.T) {
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
|
||||||
|
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
|
||||||
|
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_EDIT},
|
||||||
|
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
|
||||||
|
Search{RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -61,6 +101,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
&DashboardPermission{Role: org.RoleViewer, Permission: dashboards.PERMISSION_VIEW},
|
&DashboardPermission{Role: org.RoleViewer, Permission: dashboards.PERMISSION_VIEW},
|
||||||
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
shouldFind,
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(t,
|
test(t,
|
||||||
@@ -68,6 +109,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
&DashboardPermission{Role: org.RoleViewer, Permission: dashboards.PERMISSION_VIEW},
|
&DashboardPermission{Role: org.RoleViewer, Permission: dashboards.PERMISSION_VIEW},
|
||||||
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_EDIT},
|
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
shouldNotFind,
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(t,
|
test(t,
|
||||||
@@ -75,6 +117,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
&DashboardPermission{Role: org.RoleEditor, Permission: dashboards.PERMISSION_VIEW},
|
&DashboardPermission{Role: org.RoleEditor, Permission: dashboards.PERMISSION_VIEW},
|
||||||
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
shouldNotFind,
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(t,
|
test(t,
|
||||||
@@ -82,6 +125,41 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
&DashboardPermission{Role: org.RoleEditor, Permission: dashboards.PERMISSION_VIEW},
|
&DashboardPermission{Role: org.RoleEditor, Permission: dashboards.PERMISSION_VIEW},
|
||||||
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
shouldNotFind,
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("role ACL with nested folders", func(t *testing.T) {
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
&DashboardPermission{Role: org.RoleViewer, Permission: dashboards.PERMISSION_VIEW},
|
||||||
|
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
&DashboardPermission{Role: org.RoleViewer, Permission: dashboards.PERMISSION_VIEW},
|
||||||
|
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
&DashboardPermission{Role: org.RoleEditor, Permission: dashboards.PERMISSION_VIEW},
|
||||||
|
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
&DashboardPermission{Role: org.RoleEditor, Permission: dashboards.PERMISSION_VIEW},
|
||||||
|
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -91,6 +169,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_VIEW},
|
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_VIEW},
|
||||||
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_VIEW},
|
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
shouldFind,
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(t,
|
test(t,
|
||||||
@@ -98,6 +177,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_VIEW},
|
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_VIEW},
|
||||||
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
|
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
shouldNotFind,
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(t,
|
test(t,
|
||||||
@@ -105,6 +185,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_EDIT},
|
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_EDIT},
|
||||||
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
|
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
shouldFind,
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(t,
|
test(t,
|
||||||
@@ -112,6 +193,41 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_EDIT},
|
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_EDIT},
|
||||||
Search{UserFromACL: false, RequiredPermission: dashboards.PERMISSION_EDIT},
|
Search{UserFromACL: false, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
shouldNotFind,
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("team ACL with nested folders", func(t *testing.T) {
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_VIEW},
|
||||||
|
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_VIEW},
|
||||||
|
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_EDIT},
|
||||||
|
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_EDIT},
|
||||||
|
Search{UserFromACL: false, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -121,6 +237,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
nil,
|
nil,
|
||||||
Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
shouldNotFind,
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(t,
|
test(t,
|
||||||
@@ -128,6 +245,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
nil,
|
nil,
|
||||||
Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
shouldFind,
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(t,
|
test(t,
|
||||||
@@ -135,6 +253,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
nil,
|
nil,
|
||||||
Search{OrgId: -1, UsersOrgRole: org.RoleEditor, RequiredPermission: dashboards.PERMISSION_EDIT},
|
Search{OrgId: -1, UsersOrgRole: org.RoleEditor, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
shouldFind,
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test(t,
|
test(t,
|
||||||
@@ -142,6 +261,41 @@ func TestIntegrationSQLBuilder(t *testing.T) {
|
|||||||
nil,
|
nil,
|
||||||
Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_EDIT},
|
Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
shouldNotFind,
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("defaults for user ACL with nested folders", func(t *testing.T) {
|
||||||
|
test(t,
|
||||||
|
DashboardProps{},
|
||||||
|
nil,
|
||||||
|
Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(t,
|
||||||
|
DashboardProps{OrgId: -1},
|
||||||
|
nil,
|
||||||
|
Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
|
||||||
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(t,
|
||||||
|
DashboardProps{OrgId: -1},
|
||||||
|
nil,
|
||||||
|
Search{OrgId: -1, UsersOrgRole: org.RoleEditor, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
|
shouldFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
|
)
|
||||||
|
|
||||||
|
test(t,
|
||||||
|
DashboardProps{OrgId: -1},
|
||||||
|
nil,
|
||||||
|
Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_EDIT},
|
||||||
|
shouldNotFind,
|
||||||
|
featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -172,7 +326,7 @@ type dashboardResponse struct {
|
|||||||
Id int64
|
Id int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func test(t *testing.T, dashboardProps DashboardProps, dashboardPermission *DashboardPermission, search Search, shouldFind bool) {
|
func test(t *testing.T, dashboardProps DashboardProps, dashboardPermission *DashboardPermission, search Search, shouldFind bool, features featuremgmt.FeatureToggles) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
@@ -186,7 +340,7 @@ func test(t *testing.T, dashboardProps DashboardProps, dashboardPermission *Dash
|
|||||||
aclUserID = createDummyACL(t, sqlStore, dashboardPermission, search, dashboard.ID)
|
aclUserID = createDummyACL(t, sqlStore, dashboardPermission, search, dashboard.ID)
|
||||||
t.Logf("Created ACL with user ID %d\n", aclUserID)
|
t.Logf("Created ACL with user ID %d\n", aclUserID)
|
||||||
}
|
}
|
||||||
dashboards := getDashboards(t, sqlStore, search, aclUserID)
|
dashboards := getDashboards(t, sqlStore, search, aclUserID, features)
|
||||||
|
|
||||||
if shouldFind {
|
if shouldFind {
|
||||||
require.Len(t, dashboards, 1, "Should return one dashboard")
|
require.Len(t, dashboards, 1, "Should return one dashboard")
|
||||||
@@ -292,7 +446,7 @@ func createDummyACL(t *testing.T, sqlStore *sqlstore.SQLStore, dashboardPermissi
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDashboards(t *testing.T, sqlStore *sqlstore.SQLStore, search Search, aclUserID int64) []*dashboardResponse {
|
func getDashboards(t *testing.T, sqlStore *sqlstore.SQLStore, search Search, aclUserID int64, features featuremgmt.FeatureToggles) []*dashboardResponse {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
old := sqlStore.Cfg.RBACEnabled
|
old := sqlStore.Cfg.RBACEnabled
|
||||||
@@ -301,7 +455,10 @@ func getDashboards(t *testing.T, sqlStore *sqlstore.SQLStore, search Search, acl
|
|||||||
sqlStore.Cfg.RBACEnabled = old
|
sqlStore.Cfg.RBACEnabled = old
|
||||||
}()
|
}()
|
||||||
|
|
||||||
builder := NewSqlBuilder(sqlStore.Cfg, sqlStore.GetDialect())
|
recursiveQueriesAreSupported, err := sqlStore.RecursiveQueriesAreSupported()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
builder := NewSqlBuilder(sqlStore.Cfg, features, sqlStore.GetDialect(), recursiveQueriesAreSupported)
|
||||||
signedInUser := &user.SignedInUser{
|
signedInUser := &user.SignedInUser{
|
||||||
UserID: 9999999999,
|
UserID: 9999999999,
|
||||||
}
|
}
|
||||||
@@ -325,7 +482,7 @@ func getDashboards(t *testing.T, sqlStore *sqlstore.SQLStore, search Search, acl
|
|||||||
builder.Write("SELECT * FROM dashboard WHERE true")
|
builder.Write("SELECT * FROM dashboard WHERE true")
|
||||||
builder.WriteDashboardPermissionFilter(signedInUser, search.RequiredPermission)
|
builder.WriteDashboardPermissionFilter(signedInUser, search.RequiredPermission)
|
||||||
t.Logf("Searching for dashboards, SQL: %q\n", builder.GetSQLString())
|
t.Logf("Searching for dashboards, SQL: %q\n", builder.GetSQLString())
|
||||||
err := sqlStore.GetEngine().SQL(builder.GetSQLString(), builder.params...).Find(&res)
|
err = sqlStore.GetEngine().SQL(builder.GetSQLString(), builder.params...).Find(&res)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
fd "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
fd "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/validations"
|
"github.com/grafana/grafana/pkg/services/validations"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
|
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
|
||||||
@@ -141,7 +142,7 @@ func (rh fakeIntervalTestReqHandler) HandleRequest(ctx context.Context, dsInfo *
|
|||||||
func applyScenario(t *testing.T, timeRange string, dataSourceJsonData *simplejson.Json, queryModel string, verifier func(query legacydata.DataSubQuery)) {
|
func applyScenario(t *testing.T, timeRange string, dataSourceJsonData *simplejson.Json, queryModel string, verifier func(query legacydata.DataSubQuery)) {
|
||||||
t.Run("desc", func(t *testing.T) {
|
t.Run("desc", func(t *testing.T) {
|
||||||
db := dbtest.NewFakeDB()
|
db := dbtest.NewFakeDB()
|
||||||
store := alerting.ProvideAlertStore(db, localcache.ProvideService(), &setting.Cfg{}, nil)
|
store := alerting.ProvideAlertStore(db, localcache.ProvideService(), &setting.Cfg{}, nil, featuremgmt.WithFeatures())
|
||||||
|
|
||||||
ctx := &queryIntervalTestContext{}
|
ctx := &queryIntervalTestContext{}
|
||||||
ctx.result = &alerting.EvalContext{
|
ctx.result = &alerting.EvalContext{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
fd "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
fd "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/validations"
|
"github.com/grafana/grafana/pkg/services/validations"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
||||||
@@ -38,7 +39,7 @@ func TestQueryCondition(t *testing.T) {
|
|||||||
setup := func() *queryConditionTestContext {
|
setup := func() *queryConditionTestContext {
|
||||||
ctx := &queryConditionTestContext{}
|
ctx := &queryConditionTestContext{}
|
||||||
db := dbtest.NewFakeDB()
|
db := dbtest.NewFakeDB()
|
||||||
store := alerting.ProvideAlertStore(db, localcache.ProvideService(), &setting.Cfg{}, nil)
|
store := alerting.ProvideAlertStore(db, localcache.ProvideService(), &setting.Cfg{}, nil, featuremgmt.WithFeatures())
|
||||||
ctx.reducer = `{"type":"avg"}`
|
ctx.reducer = `{"type":"avg"}`
|
||||||
ctx.evaluator = `{"type":"gt","params":[100]}`
|
ctx.evaluator = `{"type":"gt","params":[100]}`
|
||||||
ctx.result = &alerting.EvalContext{
|
ctx.result = &alerting.EvalContext{
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources/permissions"
|
"github.com/grafana/grafana/pkg/services/datasources/permissions"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
@@ -42,7 +43,8 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
|
|
||||||
dsService := &fakeDatasourceService{ExpectedDatasource: defaultDs}
|
dsService := &fakeDatasourceService{ExpectedDatasource: defaultDs}
|
||||||
db := dbtest.NewFakeDB()
|
db := dbtest.NewFakeDB()
|
||||||
store := ProvideAlertStore(db, localcache.ProvideService(), &setting.Cfg{}, nil)
|
cfg := &setting.Cfg{}
|
||||||
|
store := ProvideAlertStore(db, localcache.ProvideService(), cfg, nil, featuremgmt.WithFeatures())
|
||||||
extractor := ProvideDashAlertExtractorService(dsPermissions, dsService, store)
|
extractor := ProvideDashAlertExtractorService(dsPermissions, dsService, store)
|
||||||
|
|
||||||
t.Run("Parsing alert rules from dashboard json", func(t *testing.T) {
|
t.Run("Parsing alert rules from dashboard json", func(t *testing.T) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
alertmodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
alertmodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"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/org"
|
||||||
"github.com/grafana/grafana/pkg/services/tag"
|
"github.com/grafana/grafana/pkg/services/tag"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@@ -39,17 +40,19 @@ type sqlStore struct {
|
|||||||
log *log.ConcreteLogger
|
log *log.ConcreteLogger
|
||||||
cfg *setting.Cfg
|
cfg *setting.Cfg
|
||||||
tagService tag.Service
|
tagService tag.Service
|
||||||
|
features featuremgmt.FeatureToggles
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideAlertStore(
|
func ProvideAlertStore(
|
||||||
db db.DB,
|
db db.DB,
|
||||||
cacheService *localcache.CacheService, cfg *setting.Cfg, tagService tag.Service) AlertStore {
|
cacheService *localcache.CacheService, cfg *setting.Cfg, tagService tag.Service, features featuremgmt.FeatureToggles) AlertStore {
|
||||||
return &sqlStore{
|
return &sqlStore{
|
||||||
db: db,
|
db: db,
|
||||||
cache: cacheService,
|
cache: cacheService,
|
||||||
log: log.New("alerting.store"),
|
log: log.New("alerting.store"),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
tagService: tagService,
|
tagService: tagService,
|
||||||
|
features: features,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,8 +110,13 @@ func deleteAlertByIdInternal(alertId int64, reason string, sess *db.Session, log
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ss *sqlStore) HandleAlertsQuery(ctx context.Context, query *alertmodels.GetAlertsQuery) (res []*alertmodels.AlertListItemDTO, err error) {
|
func (ss *sqlStore) HandleAlertsQuery(ctx context.Context, query *alertmodels.GetAlertsQuery) (res []*alertmodels.AlertListItemDTO, err error) {
|
||||||
|
recursiveQueriesAreSupported, err := ss.db.RecursiveQueriesAreSupported()
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
err = ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
err = ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
builder := db.NewSqlBuilder(ss.cfg, ss.db.GetDialect())
|
builder := db.NewSqlBuilder(ss.cfg, ss.features, ss.db.GetDialect(), recursiveQueriesAreSupported)
|
||||||
|
|
||||||
builder.Write(`SELECT
|
builder.Write(`SELECT
|
||||||
alert.id,
|
alert.id,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/tag"
|
"github.com/grafana/grafana/pkg/services/tag"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
@@ -14,10 +15,11 @@ type RepositoryImpl struct {
|
|||||||
store store
|
store store
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(db db.DB, cfg *setting.Cfg, tagService tag.Service) *RepositoryImpl {
|
func ProvideService(db db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tagService tag.Service) *RepositoryImpl {
|
||||||
return &RepositoryImpl{
|
return &RepositoryImpl{
|
||||||
store: &xormRepositoryImpl{
|
store: &xormRepositoryImpl{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
features: features,
|
||||||
db: db,
|
db: db,
|
||||||
log: log.New("annotations"),
|
log: log.New("annotations"),
|
||||||
tagService: tagService,
|
tagService: tagService,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,12 +14,13 @@ type CleanupServiceImpl struct {
|
|||||||
store store
|
store store
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideCleanupService(db db.DB, cfg *setting.Cfg) *CleanupServiceImpl {
|
func ProvideCleanupService(db db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles) *CleanupServiceImpl {
|
||||||
return &CleanupServiceImpl{
|
return &CleanupServiceImpl{
|
||||||
store: &xormRepositoryImpl{
|
store: &xormRepositoryImpl{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
db: db,
|
features: features,
|
||||||
log: log.New("annotations"),
|
db: db,
|
||||||
|
log: log.New("annotations"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ func TestAnnotationCleanUp(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
cfg.AnnotationCleanupJobBatchSize = 1
|
cfg.AnnotationCleanupJobBatchSize = 1
|
||||||
cleaner := ProvideCleanupService(fakeSQL, cfg)
|
cleaner := ProvideCleanupService(fakeSQL, cfg, featuremgmt.WithFeatures())
|
||||||
affectedAnnotations, affectedAnnotationTags, err := cleaner.Run(context.Background(), test.cfg)
|
affectedAnnotations, affectedAnnotationTags, err := cleaner.Run(context.Background(), test.cfg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -146,7 +147,7 @@ func TestOldAnnotationsAreDeletedFirst(t *testing.T) {
|
|||||||
// run the clean up task to keep one annotation.
|
// run the clean up task to keep one annotation.
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
cfg.AnnotationCleanupJobBatchSize = 1
|
cfg.AnnotationCleanupJobBatchSize = 1
|
||||||
cleaner := &xormRepositoryImpl{cfg: cfg, log: log.New("test-logger"), db: fakeSQL}
|
cleaner := &xormRepositoryImpl{cfg: cfg, log: log.New("test-logger"), db: fakeSQL, features: featuremgmt.WithFeatures()}
|
||||||
_, err = cleaner.CleanAnnotations(context.Background(), setting.AnnotationCleanupSettings{MaxCount: 1}, alertAnnotationType)
|
_, err = cleaner.CleanAnnotations(context.Background(), setting.AnnotationCleanupSettings{MaxCount: 1}, alertAnnotationType)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||||
@@ -42,6 +43,7 @@ func validateTimeRange(item *annotations.Item) error {
|
|||||||
|
|
||||||
type xormRepositoryImpl struct {
|
type xormRepositoryImpl struct {
|
||||||
cfg *setting.Cfg
|
cfg *setting.Cfg
|
||||||
|
features featuremgmt.FeatureToggles
|
||||||
db db.DB
|
db db.DB
|
||||||
log log.Logger
|
log log.Logger
|
||||||
maximumTagsLength int64
|
maximumTagsLength int64
|
||||||
@@ -299,13 +301,15 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query *annotations.ItemQue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var acFilter acFilter
|
||||||
if !ac.IsDisabled(r.cfg) {
|
if !ac.IsDisabled(r.cfg) {
|
||||||
acFilter, acArgs, err := getAccessControlFilter(query.SignedInUser)
|
var err error
|
||||||
|
acFilter, err = r.getAccessControlFilter(query.SignedInUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sql.WriteString(fmt.Sprintf(" AND (%s)", acFilter))
|
sql.WriteString(fmt.Sprintf(" AND (%s)", acFilter.where))
|
||||||
params = append(params, acArgs...)
|
params = append(params, acFilter.whereParams...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Limit == 0 {
|
if query.Limit == 0 {
|
||||||
@@ -314,6 +318,14 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query *annotations.ItemQue
|
|||||||
|
|
||||||
// order of ORDER BY arguments match the order of a sql index for performance
|
// order of ORDER BY arguments match the order of a sql index for performance
|
||||||
sql.WriteString(" ORDER BY a.org_id, a.epoch_end DESC, a.epoch DESC" + r.db.GetDialect().Limit(query.Limit) + " ) dt on dt.id = annotation.id")
|
sql.WriteString(" ORDER BY a.org_id, a.epoch_end DESC, a.epoch DESC" + r.db.GetDialect().Limit(query.Limit) + " ) dt on dt.id = annotation.id")
|
||||||
|
if acFilter.recQueries != "" {
|
||||||
|
var sb bytes.Buffer
|
||||||
|
sb.WriteString(acFilter.recQueries)
|
||||||
|
sb.WriteString(sql.String())
|
||||||
|
sql = sb
|
||||||
|
params = append(acFilter.recParams, params...)
|
||||||
|
}
|
||||||
|
|
||||||
if err := sess.SQL(sql.String(), params...).Find(&items); err != nil {
|
if err := sess.SQL(sql.String(), params...).Find(&items); err != nil {
|
||||||
items = nil
|
items = nil
|
||||||
return err
|
return err
|
||||||
@@ -325,13 +337,23 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query *annotations.ItemQue
|
|||||||
return items, err
|
return items, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAccessControlFilter(user *user.SignedInUser) (string, []interface{}, error) {
|
type acFilter struct {
|
||||||
|
where string
|
||||||
|
whereParams []interface{}
|
||||||
|
recQueries string
|
||||||
|
recParams []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *xormRepositoryImpl) getAccessControlFilter(user *user.SignedInUser) (acFilter, error) {
|
||||||
|
var recQueries string
|
||||||
|
var recQueriesParams []interface{}
|
||||||
|
|
||||||
if user == nil || user.Permissions[user.OrgID] == nil {
|
if user == nil || user.Permissions[user.OrgID] == nil {
|
||||||
return "", nil, errors.New("missing permissions")
|
return acFilter{}, errors.New("missing permissions")
|
||||||
}
|
}
|
||||||
scopes, has := user.Permissions[user.OrgID][ac.ActionAnnotationsRead]
|
scopes, has := user.Permissions[user.OrgID][ac.ActionAnnotationsRead]
|
||||||
if !has {
|
if !has {
|
||||||
return "", nil, errors.New("missing permissions")
|
return acFilter{}, errors.New("missing permissions")
|
||||||
}
|
}
|
||||||
types, hasWildcardScope := ac.ParseScopes(ac.ScopeAnnotationsProvider.GetResourceScopeType(""), scopes)
|
types, hasWildcardScope := ac.ParseScopes(ac.ScopeAnnotationsProvider.GetResourceScopeType(""), scopes)
|
||||||
if hasWildcardScope {
|
if hasWildcardScope {
|
||||||
@@ -347,13 +369,27 @@ func getAccessControlFilter(user *user.SignedInUser) (string, []interface{}, err
|
|||||||
}
|
}
|
||||||
// annotation read permission with scope annotations:type:dashboard allows listing annotations from dashboards which the user can view
|
// annotation read permission with scope annotations:type:dashboard allows listing annotations from dashboards which the user can view
|
||||||
if t == annotations.Dashboard.String() {
|
if t == annotations.Dashboard.String() {
|
||||||
dashboardFilter, dashboardParams := permissions.NewAccessControlDashboardPermissionFilter(user, dashboards.PERMISSION_VIEW, searchstore.TypeDashboard).Where()
|
recursiveQueriesAreSupported, err := r.db.RecursiveQueriesAreSupported()
|
||||||
|
if err != nil {
|
||||||
|
return acFilter{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filterRBAC := permissions.NewAccessControlDashboardPermissionFilter(user, dashboards.PERMISSION_VIEW, searchstore.TypeDashboard, r.features, recursiveQueriesAreSupported)
|
||||||
|
dashboardFilter, dashboardParams := filterRBAC.Where()
|
||||||
|
recQueries, recQueriesParams = filterRBAC.With()
|
||||||
filter := fmt.Sprintf("a.dashboard_id IN(SELECT id FROM dashboard WHERE %s)", dashboardFilter)
|
filter := fmt.Sprintf("a.dashboard_id IN(SELECT id FROM dashboard WHERE %s)", dashboardFilter)
|
||||||
filters = append(filters, filter)
|
filters = append(filters, filter)
|
||||||
params = dashboardParams
|
params = dashboardParams
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return strings.Join(filters, " OR "), params, nil
|
|
||||||
|
f := acFilter{
|
||||||
|
where: strings.Join(filters, " OR "),
|
||||||
|
whereParams: params,
|
||||||
|
recQueries: recQueries,
|
||||||
|
recParams: recQueriesParams,
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *xormRepositoryImpl) Delete(ctx context.Context, params *annotations.DeleteParams) error {
|
func (r *xormRepositoryImpl) Delete(ctx context.Context, params *annotations.DeleteParams) error {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package annotationsimpl
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -10,14 +11,20 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database"
|
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
|
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||||
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||||
@@ -495,7 +502,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
|
|||||||
sql := db.InitTestDB(t)
|
sql := db.InitTestDB(t)
|
||||||
|
|
||||||
var maximumTagsLength int64 = 60
|
var maximumTagsLength int64 = 60
|
||||||
repo := xormRepositoryImpl{db: sql, cfg: setting.NewCfg(), log: log.New("annotation.test"), tagService: tagimpl.ProvideService(sql, sql.Cfg), maximumTagsLength: maximumTagsLength}
|
repo := xormRepositoryImpl{db: sql, cfg: setting.NewCfg(), log: log.New("annotation.test"), tagService: tagimpl.ProvideService(sql, sql.Cfg), maximumTagsLength: maximumTagsLength, features: featuremgmt.WithFeatures()}
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
dashboardStore, err := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg), quotaService)
|
dashboardStore, err := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg), quotaService)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -550,7 +557,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
|
|||||||
UserID: 1,
|
UserID: 1,
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
}
|
}
|
||||||
role := setupRBACRole(t, repo, user)
|
role := setupRBACRole(t, sql, user)
|
||||||
|
|
||||||
type testStruct struct {
|
type testStruct struct {
|
||||||
description string
|
description string
|
||||||
@@ -611,7 +618,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.description, func(t *testing.T) {
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
user.Permissions = map[int64]map[string][]string{1: tc.permissions}
|
user.Permissions = map[int64]map[string][]string{1: tc.permissions}
|
||||||
setupRBACPermission(t, repo, role, user)
|
setupRBACPermission(t, sql, role, user)
|
||||||
|
|
||||||
results, err := repo.Get(context.Background(), &annotations.ItemQuery{
|
results, err := repo.Get(context.Background(), &annotations.ItemQuery{
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
@@ -630,10 +637,163 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupRBACRole(t *testing.T, repo xormRepositoryImpl, user *user.SignedInUser) *accesscontrol.Role {
|
func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID := int64(1)
|
||||||
|
permissions := []accesscontrol.Permission{
|
||||||
|
{
|
||||||
|
Action: dashboards.ActionFoldersCreate,
|
||||||
|
}, {
|
||||||
|
Action: dashboards.ActionFoldersWrite,
|
||||||
|
Scope: dashboards.ScopeFoldersAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
usr := &user.SignedInUser{
|
||||||
|
UserID: 1,
|
||||||
|
OrgID: orgID,
|
||||||
|
Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)},
|
||||||
|
}
|
||||||
|
|
||||||
|
var role *accesscontrol.Role
|
||||||
|
|
||||||
|
dashboardIDs := make([]int64, 0, folder.MaxNestedFolderDepth+1)
|
||||||
|
annotationsTexts := make([]string, 0, folder.MaxNestedFolderDepth+1)
|
||||||
|
|
||||||
|
setupFolderStructure := func() *sqlstore.SQLStore {
|
||||||
|
db := db.InitTestDB(t)
|
||||||
|
|
||||||
|
// enable nested folders so that the folder table is populated for all the tests
|
||||||
|
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
|
||||||
|
|
||||||
|
origNewGuardian := guardian.New
|
||||||
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
|
||||||
|
t.Cleanup(func() {
|
||||||
|
guardian.New = origNewGuardian
|
||||||
|
})
|
||||||
|
|
||||||
|
// dashboard store commands that should be called.
|
||||||
|
dashStore, err := dashboardstore.ProvideDashboardStore(db, db.Cfg, features, tagimpl.ProvideService(db, db.Cfg), quotatest.New(false, nil))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), db.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features)
|
||||||
|
|
||||||
|
var maximumTagsLength int64 = 60
|
||||||
|
repo := xormRepositoryImpl{db: db, cfg: setting.NewCfg(), log: log.New("annotation.test"), tagService: tagimpl.ProvideService(db, db.Cfg), maximumTagsLength: maximumTagsLength, features: features}
|
||||||
|
|
||||||
|
parentUID := ""
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
uid := fmt.Sprintf("f%d", i)
|
||||||
|
f, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
||||||
|
UID: uid,
|
||||||
|
OrgID: orgID,
|
||||||
|
Title: uid,
|
||||||
|
SignedInUser: usr,
|
||||||
|
ParentUID: parentUID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, folder.ErrMaximumDepthReached) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("unexpected error", "error", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
dashInFolder := dashboards.SaveDashboardCommand{
|
||||||
|
UserID: usr.UserID,
|
||||||
|
OrgID: orgID,
|
||||||
|
IsFolder: false,
|
||||||
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
|
"title": fmt.Sprintf("Dashboard under %s", f.UID),
|
||||||
|
}),
|
||||||
|
FolderID: f.ID,
|
||||||
|
FolderUID: f.UID,
|
||||||
|
}
|
||||||
|
dashboard, err := dashStore.SaveDashboard(context.Background(), dashInFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dashboardIDs = append(dashboardIDs, dashboard.ID)
|
||||||
|
|
||||||
|
parentUID = f.UID
|
||||||
|
|
||||||
|
annotationTxt := fmt.Sprintf("annotation %d", i)
|
||||||
|
dash1Annotation := &annotations.Item{
|
||||||
|
OrgID: orgID,
|
||||||
|
DashboardID: dashboard.ID,
|
||||||
|
Epoch: 10,
|
||||||
|
Text: annotationTxt,
|
||||||
|
}
|
||||||
|
err = repo.Add(context.Background(), dash1Annotation)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
annotationsTexts = append(annotationsTexts, annotationTxt)
|
||||||
|
}
|
||||||
|
|
||||||
|
role = setupRBACRole(t, db, usr)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
db := setupFolderStructure()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
features featuremgmt.FeatureToggles
|
||||||
|
permissions map[string][]string
|
||||||
|
expectedAnnotationText []string
|
||||||
|
expectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Should find only annotations from dashboards under folders that user can read",
|
||||||
|
features: featuremgmt.WithFeatures(),
|
||||||
|
permissions: map[string][]string{
|
||||||
|
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeDashboard},
|
||||||
|
dashboards.ActionDashboardsRead: {"folders:uid:f0"},
|
||||||
|
},
|
||||||
|
expectedAnnotationText: annotationsTexts[:1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Should find only annotations from dashboards under inherited folders if nested folder are enabled",
|
||||||
|
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
|
||||||
|
permissions: map[string][]string{
|
||||||
|
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeDashboard},
|
||||||
|
dashboards.ActionDashboardsRead: {"folders:uid:f0"},
|
||||||
|
},
|
||||||
|
expectedAnnotationText: annotationsTexts[:],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
var maximumTagsLength int64 = 60
|
||||||
|
repo := xormRepositoryImpl{db: db, cfg: setting.NewCfg(), log: log.New("annotation.test"), tagService: tagimpl.ProvideService(db, db.Cfg), maximumTagsLength: maximumTagsLength, features: tc.features}
|
||||||
|
|
||||||
|
usr.Permissions = map[int64]map[string][]string{1: tc.permissions}
|
||||||
|
setupRBACPermission(t, db, role, usr)
|
||||||
|
|
||||||
|
results, err := repo.Get(context.Background(), &annotations.ItemQuery{
|
||||||
|
OrgID: 1,
|
||||||
|
SignedInUser: usr,
|
||||||
|
})
|
||||||
|
if tc.expectedError {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, results, len(tc.expectedAnnotationText))
|
||||||
|
for _, r := range results {
|
||||||
|
assert.Contains(t, tc.expectedAnnotationText, r.Text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRBACRole(t *testing.T, db *sqlstore.SQLStore, user *user.SignedInUser) *accesscontrol.Role {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var role *accesscontrol.Role
|
var role *accesscontrol.Role
|
||||||
err := repo.db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
role = &accesscontrol.Role{
|
role = &accesscontrol.Role{
|
||||||
OrgID: user.OrgID,
|
OrgID: user.OrgID,
|
||||||
UID: "test_role",
|
UID: "test_role",
|
||||||
@@ -662,9 +822,9 @@ func setupRBACRole(t *testing.T, repo xormRepositoryImpl, user *user.SignedInUse
|
|||||||
return role
|
return role
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupRBACPermission(t *testing.T, repo xormRepositoryImpl, role *accesscontrol.Role, user *user.SignedInUser) {
|
func setupRBACPermission(t *testing.T, db *sqlstore.SQLStore, role *accesscontrol.Role, user *user.SignedInUser) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
err := repo.db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
if _, err := sess.Exec("DELETE FROM permission WHERE role_id = ?", role.ID); err != nil {
|
if _, err := sess.Exec("DELETE FROM permission WHERE role_id = ?", role.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
)
|
)
|
||||||
@@ -103,8 +104,14 @@ func (d *dashboardStore) HasEditPermissionInFolders(ctx context.Context, query *
|
|||||||
queryResult = true
|
queryResult = true
|
||||||
return queryResult, nil
|
return queryResult, nil
|
||||||
}
|
}
|
||||||
err := d.store.WithDbSession(ctx, func(dbSession *db.Session) error {
|
|
||||||
builder := db.NewSqlBuilder(d.cfg, d.store.GetDialect())
|
recursiveQueriesAreSupported, err := d.store.RecursiveQueriesAreSupported()
|
||||||
|
if err != nil {
|
||||||
|
return queryResult, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.store.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||||
|
builder := db.NewSqlBuilder(d.cfg, featuremgmt.WithFeatures(), d.store.GetDialect(), recursiveQueriesAreSupported)
|
||||||
builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?",
|
builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?",
|
||||||
query.SignedInUser.OrgID, d.store.GetDialect().BooleanStr(true))
|
query.SignedInUser.OrgID, d.store.GetDialect().BooleanStr(true))
|
||||||
builder.WriteDashboardPermissionFilter(query.SignedInUser, dashboards.PERMISSION_EDIT)
|
builder.WriteDashboardPermissionFilter(query.SignedInUser, dashboards.PERMISSION_EDIT)
|
||||||
@@ -131,13 +138,18 @@ func (d *dashboardStore) HasEditPermissionInFolders(ctx context.Context, query *
|
|||||||
|
|
||||||
func (d *dashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *folder.HasAdminPermissionInDashboardsOrFoldersQuery) (bool, error) {
|
func (d *dashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *folder.HasAdminPermissionInDashboardsOrFoldersQuery) (bool, error) {
|
||||||
var queryResult bool
|
var queryResult bool
|
||||||
err := d.store.WithDbSession(ctx, func(dbSession *db.Session) error {
|
recursiveQueriesAreSupported, err := d.store.RecursiveQueriesAreSupported()
|
||||||
|
if err != nil {
|
||||||
|
return queryResult, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = d.store.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||||
if query.SignedInUser.HasRole(org.RoleAdmin) {
|
if query.SignedInUser.HasRole(org.RoleAdmin) {
|
||||||
queryResult = true
|
queryResult = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := db.NewSqlBuilder(d.cfg, d.store.GetDialect())
|
builder := db.NewSqlBuilder(d.cfg, featuremgmt.WithFeatures(), d.store.GetDialect(), recursiveQueriesAreSupported)
|
||||||
builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ?", query.SignedInUser.OrgID)
|
builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ?", query.SignedInUser.OrgID)
|
||||||
builder.WriteDashboardPermissionFilter(query.SignedInUser, dashboards.PERMISSION_ADMIN)
|
builder.WriteDashboardPermissionFilter(query.SignedInUser, dashboards.PERMISSION_ADMIN)
|
||||||
|
|
||||||
|
|||||||
@@ -934,9 +934,14 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ac.IsDisabled(d.cfg) {
|
if !ac.IsDisabled(d.cfg) {
|
||||||
|
recursiveQueriesAreSupported, err := d.store.RecursiveQueriesAreSupported()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// if access control is enabled, overwrite the filters so far
|
// if access control is enabled, overwrite the filters so far
|
||||||
filters = []interface{}{
|
filters = []interface{}{
|
||||||
permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type),
|
permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type, d.features, recursiveQueriesAreSupported),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,33 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
|
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||||
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
|
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testFeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch)
|
var testFeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch)
|
||||||
@@ -36,7 +48,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
|
|||||||
sqlStore.Cfg.RBACEnabled = false
|
sqlStore.Cfg.RBACEnabled = false
|
||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
var err error
|
var err error
|
||||||
dashboardStore, err = ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
|
dashboardStore, err = ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
flder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp")
|
flder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp")
|
||||||
dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod", "webapp")
|
dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod", "webapp")
|
||||||
@@ -477,6 +489,216 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
// the maximux nested folder hierarchy starting from parent down to subfolders
|
||||||
|
nestedFolders := make([]*folder.Folder, 0, folder.MaxNestedFolderDepth+1)
|
||||||
|
|
||||||
|
var sqlStore *sqlstore.SQLStore
|
||||||
|
const (
|
||||||
|
dashInRootTitle = "dashboard in root"
|
||||||
|
dashInParentTitle = "dashboard in parent"
|
||||||
|
dashInSubfolderTitle = "dashboard in subfolder"
|
||||||
|
)
|
||||||
|
var viewer user.SignedInUser
|
||||||
|
var role *accesscontrol.Role
|
||||||
|
|
||||||
|
setup := func() {
|
||||||
|
sqlStore = db.InitTestDB(t)
|
||||||
|
sqlStore.Cfg.RBACEnabled = true
|
||||||
|
quotaService := quotatest.New(false, nil)
|
||||||
|
|
||||||
|
// enable nested folders so that the folder table is populated for all the tests
|
||||||
|
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
dashboardWriteStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, features, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
usr := createUser(t, sqlStore, "viewer", "Viewer", false)
|
||||||
|
viewer = user.SignedInUser{
|
||||||
|
UserID: usr.ID,
|
||||||
|
OrgID: usr.OrgID,
|
||||||
|
OrgRole: org.RoleViewer,
|
||||||
|
}
|
||||||
|
|
||||||
|
orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService)
|
||||||
|
require.NoError(t, err)
|
||||||
|
usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// create admin user in the same org
|
||||||
|
currentUserCmd := user.CreateUserCommand{Login: "admin", Email: "admin@test.com", Name: "an admin", IsAdmin: false, OrgID: viewer.OrgID}
|
||||||
|
u, err := usrSvc.Create(context.Background(), ¤tUserCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
admin := user.SignedInUser{
|
||||||
|
UserID: u.ID,
|
||||||
|
OrgID: u.OrgID,
|
||||||
|
OrgRole: org.RoleAdmin,
|
||||||
|
Permissions: map[int64]map[string][]string{u.OrgID: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
|
||||||
|
{
|
||||||
|
Action: dashboards.ActionFoldersCreate,
|
||||||
|
}, {
|
||||||
|
Action: dashboards.ActionFoldersWrite,
|
||||||
|
Scope: dashboards.ScopeFoldersAll,
|
||||||
|
}}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.NotEqual(t, viewer.UserID, admin.UserID)
|
||||||
|
|
||||||
|
origNewGuardian := guardian.New
|
||||||
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
|
||||||
|
t.Cleanup(func() {
|
||||||
|
guardian.New = origNewGuardian
|
||||||
|
})
|
||||||
|
|
||||||
|
folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features)
|
||||||
|
|
||||||
|
parentUID := ""
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
uid := fmt.Sprintf("f%d", i)
|
||||||
|
f, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
||||||
|
UID: uid,
|
||||||
|
OrgID: admin.OrgID,
|
||||||
|
Title: uid,
|
||||||
|
SignedInUser: &admin,
|
||||||
|
ParentUID: parentUID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, folder.ErrMaximumDepthReached) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("unexpected error", "error", err)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
nestedFolders = append(nestedFolders, f)
|
||||||
|
|
||||||
|
parentUID = f.UID
|
||||||
|
}
|
||||||
|
require.LessOrEqual(t, 2, len(nestedFolders))
|
||||||
|
|
||||||
|
saveDashboardCmd := dashboards.SaveDashboardCommand{
|
||||||
|
UserID: admin.UserID,
|
||||||
|
OrgID: admin.OrgID,
|
||||||
|
IsFolder: false,
|
||||||
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
|
"title": dashInRootTitle,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
_, err = dashboardWriteStore.SaveDashboard(context.Background(), saveDashboardCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saveDashboardCmd = dashboards.SaveDashboardCommand{
|
||||||
|
UserID: admin.UserID,
|
||||||
|
OrgID: admin.OrgID,
|
||||||
|
IsFolder: false,
|
||||||
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
|
"title": dashInParentTitle,
|
||||||
|
}),
|
||||||
|
FolderID: nestedFolders[0].ID,
|
||||||
|
FolderUID: nestedFolders[0].UID,
|
||||||
|
}
|
||||||
|
_, err = dashboardWriteStore.SaveDashboard(context.Background(), saveDashboardCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saveDashboardCmd = dashboards.SaveDashboardCommand{
|
||||||
|
UserID: admin.UserID,
|
||||||
|
OrgID: admin.OrgID,
|
||||||
|
IsFolder: false,
|
||||||
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
|
"title": dashInSubfolderTitle,
|
||||||
|
}),
|
||||||
|
FolderID: nestedFolders[1].ID,
|
||||||
|
FolderUID: nestedFolders[1].UID,
|
||||||
|
}
|
||||||
|
_, err = dashboardWriteStore.SaveDashboard(context.Background(), saveDashboardCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
role = setupRBACRole(t, *sqlStore, &viewer)
|
||||||
|
}
|
||||||
|
|
||||||
|
setup()
|
||||||
|
|
||||||
|
nestedFolderTitles := make([]string, 0, len(nestedFolders))
|
||||||
|
for _, f := range nestedFolders {
|
||||||
|
nestedFolderTitles = append(nestedFolderTitles, f.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
features featuremgmt.FeatureToggles
|
||||||
|
permissions map[string][]string
|
||||||
|
expectedTitles []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "it should not return folder if ACL is not set for parent folder",
|
||||||
|
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
|
||||||
|
permissions: nil,
|
||||||
|
expectedTitles: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "it should not return dashboard in subfolder if nested folders are disabled and the user has permission to read dashboards under parent folder",
|
||||||
|
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
|
||||||
|
permissions: map[string][]string{
|
||||||
|
dashboards.ActionDashboardsRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)},
|
||||||
|
},
|
||||||
|
expectedTitles: []string{dashInParentTitle},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "it should return dashboard in subfolder if nested folders are enabled and the user has permission to read dashboards under parent folder",
|
||||||
|
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch, featuremgmt.FlagNestedFolders),
|
||||||
|
permissions: map[string][]string{
|
||||||
|
dashboards.ActionDashboardsRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)},
|
||||||
|
},
|
||||||
|
expectedTitles: []string{dashInParentTitle, dashInSubfolderTitle},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "it should not return subfolder if nested folders are disabled and the user has permission to read folders under parent folder",
|
||||||
|
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
|
||||||
|
permissions: map[string][]string{
|
||||||
|
dashboards.ActionFoldersRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)},
|
||||||
|
},
|
||||||
|
expectedTitles: []string{nestedFolders[0].Title},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "it should return subfolder if nested folders are enabled and the user has permission to read folders under parent folder",
|
||||||
|
features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch, featuremgmt.FlagNestedFolders),
|
||||||
|
permissions: map[string][]string{
|
||||||
|
dashboards.ActionFoldersRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)},
|
||||||
|
},
|
||||||
|
expectedTitles: nestedFolderTitles,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
dashboardReadStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, tc.features, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
viewer.Permissions = map[int64]map[string][]string{viewer.OrgID: tc.permissions}
|
||||||
|
setupRBACPermission(t, *sqlStore, role, &viewer)
|
||||||
|
|
||||||
|
query := &dashboards.FindPersistedDashboardsQuery{
|
||||||
|
SignedInUser: &viewer,
|
||||||
|
OrgId: viewer.OrgID,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := testSearchDashboards(dashboardReadStore, query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, len(tc.expectedTitles), len(res))
|
||||||
|
for i, tlt := range tc.expectedTitles {
|
||||||
|
assert.Equal(t, tlt, res[i].Title)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, dashboard *simplejson.Json,
|
func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, dashboard *simplejson.Json,
|
||||||
newFolderId int64) *dashboards.Dashboard {
|
newFolderId int64) *dashboards.Dashboard {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -492,3 +714,61 @@ func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, d
|
|||||||
|
|
||||||
return dash
|
return dash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupRBACRole(t *testing.T, db sqlstore.SQLStore, user *user.SignedInUser) *accesscontrol.Role {
|
||||||
|
t.Helper()
|
||||||
|
var role *accesscontrol.Role
|
||||||
|
err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
|
role = &accesscontrol.Role{
|
||||||
|
OrgID: user.OrgID,
|
||||||
|
UID: "test_role",
|
||||||
|
Name: "test:role",
|
||||||
|
Updated: time.Now(),
|
||||||
|
Created: time.Now(),
|
||||||
|
}
|
||||||
|
_, err := sess.Insert(role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = sess.Insert(accesscontrol.UserRole{
|
||||||
|
OrgID: role.OrgID,
|
||||||
|
RoleID: role.ID,
|
||||||
|
UserID: user.UserID,
|
||||||
|
Created: time.Now(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
return role
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRBACPermission(t *testing.T, db sqlstore.SQLStore, role *accesscontrol.Role, user *user.SignedInUser) {
|
||||||
|
t.Helper()
|
||||||
|
err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
|
if _, err := sess.Exec("DELETE FROM permission WHERE role_id = ?", role.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var acPermission []accesscontrol.Permission
|
||||||
|
for action, scopes := range user.Permissions[user.OrgID] {
|
||||||
|
for _, scope := range scopes {
|
||||||
|
acPermission = append(acPermission, accesscontrol.Permission{
|
||||||
|
RoleID: role.ID, Action: action, Scope: scope, Created: time.Now(), Updated: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := sess.InsertMulti(&acPermission); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/kinds/librarypanel"
|
"github.com/grafana/grafana/pkg/kinds/librarypanel"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/search"
|
"github.com/grafana/grafana/pkg/services/search"
|
||||||
@@ -227,10 +228,16 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getLibraryElements gets a Library Element where param == value
|
// getLibraryElements gets a Library Element where param == value
|
||||||
func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair) ([]model.LibraryElementDTO, error) {
|
func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair, features featuremgmt.FeatureToggles) ([]model.LibraryElementDTO, error) {
|
||||||
libraryElements := make([]model.LibraryElementWithMeta, 0)
|
libraryElements := make([]model.LibraryElementWithMeta, 0)
|
||||||
err := store.WithDbSession(c, func(session *db.Session) error {
|
|
||||||
builder := db.NewSqlBuilder(cfg, store.GetDialect())
|
recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.WithDbSession(c, func(session *db.Session) error {
|
||||||
|
builder := db.NewSqlBuilder(cfg, features, store.GetDialect(), recursiveQueriesAreSupported)
|
||||||
builder.Write(selectLibraryElementDTOWithMeta)
|
builder.Write(selectLibraryElementDTOWithMeta)
|
||||||
builder.Write(", 'General' as folder_name ")
|
builder.Write(", 'General' as folder_name ")
|
||||||
builder.Write(", '' as folder_uid ")
|
builder.Write(", '' as folder_uid ")
|
||||||
@@ -299,7 +306,7 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed
|
|||||||
|
|
||||||
// getLibraryElementByUid gets a Library Element by uid.
|
// getLibraryElementByUid gets a Library Element by uid.
|
||||||
func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signedInUser *user.SignedInUser, UID string) (model.LibraryElementDTO, error) {
|
func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signedInUser *user.SignedInUser, UID string) (model.LibraryElementDTO, error) {
|
||||||
libraryElements, err := getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: UID}})
|
libraryElements, err := getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: UID}}, l.features)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.LibraryElementDTO{}, err
|
return model.LibraryElementDTO{}, err
|
||||||
}
|
}
|
||||||
@@ -312,13 +319,19 @@ func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signed
|
|||||||
|
|
||||||
// getLibraryElementByName gets a Library Element by name.
|
// getLibraryElementByName gets a Library Element by name.
|
||||||
func (l *LibraryElementService) getLibraryElementsByName(c context.Context, signedInUser *user.SignedInUser, name string) ([]model.LibraryElementDTO, error) {
|
func (l *LibraryElementService) getLibraryElementsByName(c context.Context, signedInUser *user.SignedInUser, name string) ([]model.LibraryElementDTO, error) {
|
||||||
return getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}})
|
return getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}}, l.features)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getAllLibraryElements gets all Library Elements.
|
// getAllLibraryElements gets all Library Elements.
|
||||||
func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedInUser *user.SignedInUser, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) {
|
func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedInUser *user.SignedInUser, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) {
|
||||||
elements := make([]model.LibraryElementWithMeta, 0)
|
elements := make([]model.LibraryElementWithMeta, 0)
|
||||||
result := model.LibraryElementSearchResult{}
|
result := model.LibraryElementSearchResult{}
|
||||||
|
|
||||||
|
recursiveQueriesAreSupported, err := l.SQLStore.RecursiveQueriesAreSupported()
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
if query.PerPage <= 0 {
|
if query.PerPage <= 0 {
|
||||||
query.PerPage = 100
|
query.PerPage = 100
|
||||||
}
|
}
|
||||||
@@ -333,8 +346,8 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI
|
|||||||
if folderFilter.parseError != nil {
|
if folderFilter.parseError != nil {
|
||||||
return model.LibraryElementSearchResult{}, folderFilter.parseError
|
return model.LibraryElementSearchResult{}, folderFilter.parseError
|
||||||
}
|
}
|
||||||
err := l.SQLStore.WithDbSession(c, func(session *db.Session) error {
|
err = l.SQLStore.WithDbSession(c, func(session *db.Session) error {
|
||||||
builder := db.NewSqlBuilder(l.Cfg, l.SQLStore.GetDialect())
|
builder := db.NewSqlBuilder(l.Cfg, l.features, l.SQLStore.GetDialect(), recursiveQueriesAreSupported)
|
||||||
if folderFilter.includeGeneralFolder {
|
if folderFilter.includeGeneralFolder {
|
||||||
builder.Write(selectLibraryElementDTOWithMeta)
|
builder.Write(selectLibraryElementDTOWithMeta)
|
||||||
builder.Write(", 'General' as folder_name ")
|
builder.Write(", 'General' as folder_name ")
|
||||||
@@ -563,13 +576,18 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU
|
|||||||
// getConnections gets all connections for a Library Element.
|
// getConnections gets all connections for a Library Element.
|
||||||
func (l *LibraryElementService) getConnections(c context.Context, signedInUser *user.SignedInUser, uid string) ([]model.LibraryElementConnectionDTO, error) {
|
func (l *LibraryElementService) getConnections(c context.Context, signedInUser *user.SignedInUser, uid string) ([]model.LibraryElementConnectionDTO, error) {
|
||||||
connections := make([]model.LibraryElementConnectionDTO, 0)
|
connections := make([]model.LibraryElementConnectionDTO, 0)
|
||||||
err := l.SQLStore.WithDbSession(c, func(session *db.Session) error {
|
recursiveQueriesAreSupported, err := l.SQLStore.RecursiveQueriesAreSupported()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = l.SQLStore.WithDbSession(c, func(session *db.Session) error {
|
||||||
element, err := getLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.OrgID)
|
element, err := getLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.OrgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var libraryElementConnections []model.LibraryElementConnectionWithMeta
|
var libraryElementConnections []model.LibraryElementConnectionWithMeta
|
||||||
builder := db.NewSqlBuilder(l.Cfg, l.SQLStore.GetDialect())
|
builder := db.NewSqlBuilder(l.Cfg, l.features, l.SQLStore.GetDialect(), recursiveQueriesAreSupported)
|
||||||
builder.Write("SELECT lec.*, u1.login AS created_by_name, u1.email AS created_by_email, dashboard.uid AS connection_uid")
|
builder.Write("SELECT lec.*, u1.login AS created_by_name, u1.email AS created_by_email, dashboard.uid AS connection_uid")
|
||||||
builder.Write(" FROM " + model.LibraryElementConnectionTableName + " AS lec")
|
builder.Write(" FROM " + model.LibraryElementConnectionTableName + " AS lec")
|
||||||
builder.Write(" LEFT JOIN " + l.SQLStore.GetDialect().Quote("user") + " AS u1 ON lec.created_by = u1.id")
|
builder.Write(" LEFT JOIN " + l.SQLStore.GetDialect().Quote("user") + " AS u1 ON lec.created_by = u1.id")
|
||||||
|
|||||||
@@ -6,19 +6,21 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/routing"
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, folderService folder.Service) *LibraryElementService {
|
func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, folderService folder.Service, features featuremgmt.FeatureToggles) *LibraryElementService {
|
||||||
l := &LibraryElementService{
|
l := &LibraryElementService{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
SQLStore: sqlStore,
|
SQLStore: sqlStore,
|
||||||
RouteRegister: routeRegister,
|
RouteRegister: routeRegister,
|
||||||
folderService: folderService,
|
folderService: folderService,
|
||||||
log: log.New("library-elements"),
|
log: log.New("library-elements"),
|
||||||
|
features: features,
|
||||||
}
|
}
|
||||||
l.registerAPIEndpoints()
|
l.registerAPIEndpoints()
|
||||||
return l
|
return l
|
||||||
@@ -41,6 +43,7 @@ type LibraryElementService struct {
|
|||||||
RouteRegister routing.RouteRegister
|
RouteRegister routing.RouteRegister
|
||||||
folderService folder.Service
|
folderService folder.Service
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
features featuremgmt.FeatureToggles
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateElement creates a Library Element.
|
// CreateElement creates a Library Element.
|
||||||
|
|||||||
@@ -843,7 +843,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
|||||||
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
||||||
folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, nil, features)
|
folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, nil, features)
|
||||||
|
|
||||||
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService)
|
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, featuremgmt.WithFeatures())
|
||||||
service := LibraryPanelService{
|
service := LibraryPanelService{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
SQLStore: sqlStore,
|
SQLStore: sqlStore,
|
||||||
|
|||||||
@@ -712,7 +712,7 @@ func TestFindAnnotations(t *testing.T) {
|
|||||||
sqlStore := sqlstore.InitTestDB(t)
|
sqlStore := sqlstore.InitTestDB(t)
|
||||||
config := setting.NewCfg()
|
config := setting.NewCfg()
|
||||||
tagService := tagimpl.ProvideService(sqlStore, sqlStore.Cfg)
|
tagService := tagimpl.ProvideService(sqlStore, sqlStore.Cfg)
|
||||||
annotationsRepo := annotationsimpl.ProvideService(sqlStore, config, tagService)
|
annotationsRepo := annotationsimpl.ProvideService(sqlStore, config, featuremgmt.WithFeatures(), tagService)
|
||||||
fakeStore := FakePublicDashboardStore{}
|
fakeStore := FakePublicDashboardStore{}
|
||||||
service := &PublicDashboardServiceImpl{
|
service := &PublicDashboardServiceImpl{
|
||||||
log: log.New("test.logger"),
|
log: log.New("test.logger"),
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
package permissions
|
package permissions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// maximum possible capacity for recursive queries array: one query for folder and one for dashboard actions
|
||||||
|
const maximumRecursiveQueries = 2
|
||||||
|
|
||||||
type DashboardPermissionFilter struct {
|
type DashboardPermissionFilter struct {
|
||||||
OrgRole org.RoleType
|
OrgRole org.RoleType
|
||||||
Dialect migrator.Dialect
|
Dialect migrator.Dialect
|
||||||
@@ -78,14 +85,25 @@ func (d DashboardPermissionFilter) Where() (string, []interface{}) {
|
|||||||
return sql, params
|
return sql, params
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccessControlDashboardPermissionFilter struct {
|
type clause struct {
|
||||||
|
string
|
||||||
|
params []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type accessControlDashboardPermissionFilter struct {
|
||||||
user *user.SignedInUser
|
user *user.SignedInUser
|
||||||
dashboardActions []string
|
dashboardActions []string
|
||||||
folderActions []string
|
folderActions []string
|
||||||
|
features featuremgmt.FeatureToggles
|
||||||
|
|
||||||
|
where clause
|
||||||
|
// any recursive CTE queries (if supported)
|
||||||
|
recQueries []clause
|
||||||
|
recursiveQueriesAreSupported bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAccessControlDashboardPermissionFilter creates a new AccessControlDashboardPermissionFilter that is configured with specific actions calculated based on the dashboards.PermissionType and query type
|
// NewAccessControlDashboardPermissionFilter creates a new AccessControlDashboardPermissionFilter that is configured with specific actions calculated based on the dashboards.PermissionType and query type
|
||||||
func NewAccessControlDashboardPermissionFilter(user *user.SignedInUser, permissionLevel dashboards.PermissionType, queryType string) AccessControlDashboardPermissionFilter {
|
func NewAccessControlDashboardPermissionFilter(user *user.SignedInUser, permissionLevel dashboards.PermissionType, queryType string, features featuremgmt.FeatureToggles, recursiveQueriesAreSupported bool) *accessControlDashboardPermissionFilter {
|
||||||
needEdit := permissionLevel > dashboards.PERMISSION_VIEW
|
needEdit := permissionLevel > dashboards.PERMISSION_VIEW
|
||||||
|
|
||||||
var folderActions []string
|
var folderActions []string
|
||||||
@@ -121,12 +139,26 @@ func NewAccessControlDashboardPermissionFilter(user *user.SignedInUser, permissi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AccessControlDashboardPermissionFilter{user: user, folderActions: folderActions, dashboardActions: dashboardActions}
|
f := accessControlDashboardPermissionFilter{user: user, folderActions: folderActions, dashboardActions: dashboardActions, features: features,
|
||||||
|
recursiveQueriesAreSupported: recursiveQueriesAreSupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
f.buildClauses()
|
||||||
|
|
||||||
|
return &f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{}) {
|
// Where returns:
|
||||||
|
// - a where clause for filtering dashboards with expected permissions
|
||||||
|
// - an array with the query parameters
|
||||||
|
func (f *accessControlDashboardPermissionFilter) Where() (string, []interface{}) {
|
||||||
|
return f.where.string, f.where.params
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *accessControlDashboardPermissionFilter) buildClauses() {
|
||||||
if f.user == nil || f.user.Permissions == nil || f.user.Permissions[f.user.OrgID] == nil {
|
if f.user == nil || f.user.Permissions == nil || f.user.Permissions[f.user.OrgID] == nil {
|
||||||
return "(1 = 0)", nil
|
f.where = clause{string: "(1 = 0)"}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
dashWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeDashboardsPrefix)
|
dashWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeDashboardsPrefix)
|
||||||
folderWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix)
|
folderWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix)
|
||||||
@@ -136,6 +168,10 @@ func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{})
|
|||||||
var args []interface{}
|
var args []interface{}
|
||||||
builder := strings.Builder{}
|
builder := strings.Builder{}
|
||||||
builder.WriteRune('(')
|
builder.WriteRune('(')
|
||||||
|
|
||||||
|
permSelector := strings.Builder{}
|
||||||
|
var permSelectorArgs []interface{}
|
||||||
|
|
||||||
if len(f.dashboardActions) > 0 {
|
if len(f.dashboardActions) > 0 {
|
||||||
toCheck := actionsToCheck(f.dashboardActions, f.user.Permissions[f.user.OrgID], dashWildcards, folderWildcards)
|
toCheck := actionsToCheck(f.dashboardActions, f.user.Permissions[f.user.OrgID], dashWildcards, folderWildcards)
|
||||||
|
|
||||||
@@ -155,24 +191,50 @@ func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{})
|
|||||||
builder.WriteString(") AND NOT dashboard.is_folder)")
|
builder.WriteString(") AND NOT dashboard.is_folder)")
|
||||||
|
|
||||||
builder.WriteString(" OR ")
|
builder.WriteString(" OR ")
|
||||||
builder.WriteString("(dashboard.folder_id IN (SELECT id FROM dashboard as d WHERE d.uid IN (SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%' ")
|
permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%' ")
|
||||||
builder.WriteString(rolesFilter)
|
permSelector.WriteString(rolesFilter)
|
||||||
args = append(args, params...)
|
permSelectorArgs = append(permSelectorArgs, params...)
|
||||||
|
|
||||||
if len(toCheck) == 1 {
|
if len(toCheck) == 1 {
|
||||||
builder.WriteString(" AND action = ?")
|
permSelector.WriteString(" AND action = ?")
|
||||||
args = append(args, toCheck[0])
|
permSelectorArgs = append(permSelectorArgs, toCheck[0])
|
||||||
} else {
|
} else {
|
||||||
builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
|
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
|
||||||
args = append(args, toCheck...)
|
permSelectorArgs = append(permSelectorArgs, toCheck...)
|
||||||
args = append(args, len(toCheck))
|
permSelectorArgs = append(permSelectorArgs, len(toCheck))
|
||||||
}
|
}
|
||||||
builder.WriteString(")) AND NOT dashboard.is_folder)")
|
permSelector.WriteRune(')')
|
||||||
|
|
||||||
|
switch f.features.IsEnabled(featuremgmt.FlagNestedFolders) {
|
||||||
|
case true:
|
||||||
|
switch f.recursiveQueriesAreSupported {
|
||||||
|
case true:
|
||||||
|
recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
|
||||||
|
f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs)
|
||||||
|
builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
|
||||||
|
builder.WriteString(fmt.Sprintf("WHERE d.uid IN (SELECT uid FROM %s)", recQueryName))
|
||||||
|
default:
|
||||||
|
nestedFoldersSelectors, nestedFoldersArgs := nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "folder_id", "id")
|
||||||
|
builder.WriteRune('(')
|
||||||
|
builder.WriteString(nestedFoldersSelectors)
|
||||||
|
args = append(args, nestedFoldersArgs...)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
|
||||||
|
builder.WriteString("WHERE d.uid IN ")
|
||||||
|
builder.WriteString(permSelector.String())
|
||||||
|
args = append(args, permSelectorArgs...)
|
||||||
|
}
|
||||||
|
builder.WriteString(") AND NOT dashboard.is_folder)")
|
||||||
} else {
|
} else {
|
||||||
builder.WriteString("NOT dashboard.is_folder")
|
builder.WriteString("NOT dashboard.is_folder")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recycle and reuse
|
||||||
|
permSelector.Reset()
|
||||||
|
permSelectorArgs = permSelectorArgs[:0]
|
||||||
|
|
||||||
if len(f.folderActions) > 0 {
|
if len(f.folderActions) > 0 {
|
||||||
if len(f.dashboardActions) > 0 {
|
if len(f.dashboardActions) > 0 {
|
||||||
builder.WriteString(" OR ")
|
builder.WriteString(" OR ")
|
||||||
@@ -180,24 +242,80 @@ func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{})
|
|||||||
|
|
||||||
toCheck := actionsToCheck(f.folderActions, f.user.Permissions[f.user.OrgID], folderWildcards)
|
toCheck := actionsToCheck(f.folderActions, f.user.Permissions[f.user.OrgID], folderWildcards)
|
||||||
if len(toCheck) > 0 {
|
if len(toCheck) > 0 {
|
||||||
builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%'")
|
permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%'")
|
||||||
builder.WriteString(rolesFilter)
|
permSelector.WriteString(rolesFilter)
|
||||||
args = append(args, params...)
|
permSelectorArgs = append(permSelectorArgs, params...)
|
||||||
if len(toCheck) == 1 {
|
if len(toCheck) == 1 {
|
||||||
builder.WriteString(" AND action = ?")
|
permSelector.WriteString(" AND action = ?")
|
||||||
args = append(args, toCheck[0])
|
permSelectorArgs = append(permSelectorArgs, toCheck[0])
|
||||||
} else {
|
} else {
|
||||||
builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
|
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
|
||||||
args = append(args, toCheck...)
|
permSelectorArgs = append(permSelectorArgs, toCheck...)
|
||||||
args = append(args, len(toCheck))
|
permSelectorArgs = append(permSelectorArgs, len(toCheck))
|
||||||
}
|
}
|
||||||
builder.WriteString(") AND dashboard.is_folder)")
|
permSelector.WriteRune(')')
|
||||||
|
|
||||||
|
switch f.features.IsEnabled(featuremgmt.FlagNestedFolders) {
|
||||||
|
case true:
|
||||||
|
switch f.recursiveQueriesAreSupported {
|
||||||
|
case true:
|
||||||
|
recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
|
||||||
|
f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs)
|
||||||
|
builder.WriteString("(dashboard.uid IN ")
|
||||||
|
builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName))
|
||||||
|
default:
|
||||||
|
nestedFoldersSelectors, nestedFoldersArgs := nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "uid", "uid")
|
||||||
|
builder.WriteRune('(')
|
||||||
|
builder.WriteString(nestedFoldersSelectors)
|
||||||
|
builder.WriteRune(')')
|
||||||
|
args = append(args, nestedFoldersArgs...)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
builder.WriteString("(dashboard.uid IN ")
|
||||||
|
builder.WriteString(permSelector.String())
|
||||||
|
args = append(args, permSelectorArgs...)
|
||||||
|
}
|
||||||
|
builder.WriteString(" AND dashboard.is_folder)")
|
||||||
} else {
|
} else {
|
||||||
builder.WriteString("dashboard.is_folder")
|
builder.WriteString("dashboard.is_folder")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.WriteRune(')')
|
builder.WriteRune(')')
|
||||||
return builder.String(), args
|
|
||||||
|
f.where = clause{string: builder.String(), params: args}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With returns:
|
||||||
|
// - a with clause for fetching folders with inherited permissions if nested folders are enabled or an empty string
|
||||||
|
func (f *accessControlDashboardPermissionFilter) With() (string, []interface{}) {
|
||||||
|
var sb bytes.Buffer
|
||||||
|
var params []interface{}
|
||||||
|
if len(f.recQueries) > 0 {
|
||||||
|
sb.WriteString("WITH RECURSIVE ")
|
||||||
|
sb.WriteString(f.recQueries[0].string)
|
||||||
|
params = append(params, f.recQueries[0].params...)
|
||||||
|
for _, r := range f.recQueries[1:] {
|
||||||
|
sb.WriteRune(',')
|
||||||
|
sb.WriteString(r.string)
|
||||||
|
params = append(params, r.params...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String(), params
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *accessControlDashboardPermissionFilter) addRecQry(queryName string, whereUIDSelect string, whereParams []interface{}) {
|
||||||
|
if f.recQueries == nil {
|
||||||
|
f.recQueries = make([]clause, 0, maximumRecursiveQueries)
|
||||||
|
}
|
||||||
|
c := make([]interface{}, len(whereParams))
|
||||||
|
copy(c, whereParams)
|
||||||
|
f.recQueries = append(f.recQueries, clause{
|
||||||
|
string: fmt.Sprintf(`%s AS (
|
||||||
|
SELECT uid, parent_uid, org_id FROM folder WHERE uid IN %s
|
||||||
|
UNION ALL SELECT f.uid, f.parent_uid, f.org_id FROM folder f INNER JOIN %s r ON f.parent_uid = r.uid and f.org_id = r.org_id
|
||||||
|
)`, queryName, whereUIDSelect, queryName),
|
||||||
|
params: c,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionsToCheck(actions []string, permissions map[string][]string, wildcards ...accesscontrol.Wildcards) []interface{} {
|
func actionsToCheck(actions []string, permissions map[string][]string, wildcards ...accesscontrol.Wildcards) []interface{} {
|
||||||
@@ -222,3 +340,28 @@ func actionsToCheck(actions []string, permissions map[string][]string, wildcards
|
|||||||
}
|
}
|
||||||
return toCheck
|
return toCheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nestedFoldersSelectors(permSelector string, permSelectorArgs []interface{}, leftTableCol string, rightTableCol string) (string, []interface{}) {
|
||||||
|
wheres := make([]string, 0, folder.MaxNestedFolderDepth+1)
|
||||||
|
args := make([]interface{}, 0, len(permSelectorArgs)*(folder.MaxNestedFolderDepth+1))
|
||||||
|
|
||||||
|
joins := make([]string, 0, folder.MaxNestedFolderDepth+2)
|
||||||
|
|
||||||
|
tmpl := "INNER JOIN folder %s ON %s.%s = %s.uid AND %s.org_id = %s.org_id "
|
||||||
|
|
||||||
|
prev := "d"
|
||||||
|
onCol := "uid"
|
||||||
|
for i := 1; i <= folder.MaxNestedFolderDepth+2; i++ {
|
||||||
|
t := fmt.Sprintf("f%d", i)
|
||||||
|
s := fmt.Sprintf(tmpl, t, prev, onCol, t, prev, t)
|
||||||
|
joins = append(joins, s)
|
||||||
|
|
||||||
|
wheres = append(wheres, fmt.Sprintf("(dashboard.%s IN (SELECT d.%s FROM dashboard d %s WHERE %s.uid IN %s)", leftTableCol, rightTableCol, strings.Join(joins, " "), t, permSelector))
|
||||||
|
args = append(args, permSelectorArgs...)
|
||||||
|
|
||||||
|
prev = t
|
||||||
|
onCol = "parent_uid"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(wheres, ") OR "), args
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,14 +9,24 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
|
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||||
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||||
|
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,13 +139,18 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
store := setupTest(t, 10, 100, tt.permissions)
|
store := setupTest(t, 10, 100, tt.permissions)
|
||||||
|
recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
usr := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}
|
usr := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}
|
||||||
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tt.permission, tt.queryType)
|
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tt.permission, tt.queryType, featuremgmt.WithFeatures(), recursiveQueriesAreSupported)
|
||||||
|
|
||||||
var result int
|
var result int
|
||||||
err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
err = store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
q, params := filter.Where()
|
q, params := filter.Where()
|
||||||
_, err := sess.SQL("SELECT COUNT(*) FROM dashboard WHERE "+q, params...).Get(&result)
|
recQry, recQryParams := filter.With()
|
||||||
|
params = append(recQryParams, params...)
|
||||||
|
_, err := sess.SQL(recQry+"\nSELECT COUNT(*) FROM dashboard WHERE "+q, params...).Get(&result)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -145,7 +160,115 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
queryType string
|
||||||
|
permission dashboards.PermissionType
|
||||||
|
permissions []accesscontrol.Permission
|
||||||
|
expectedResult []string
|
||||||
|
features featuremgmt.FeatureToggles
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Should be able to view dashboards under inherited folders if nested folders are enabled",
|
||||||
|
queryType: searchstore.TypeDashboard,
|
||||||
|
permission: dashboards.PERMISSION_VIEW,
|
||||||
|
permissions: []accesscontrol.Permission{
|
||||||
|
{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
|
||||||
|
},
|
||||||
|
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
|
||||||
|
expectedResult: []string{"dashboard under parent folder", "dashboard under subfolder"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Should not be able to view dashboards under inherited folders if nested folders are not enabled",
|
||||||
|
queryType: searchstore.TypeDashboard,
|
||||||
|
permission: dashboards.PERMISSION_VIEW,
|
||||||
|
permissions: []accesscontrol.Permission{
|
||||||
|
{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
|
||||||
|
},
|
||||||
|
features: featuremgmt.WithFeatures(),
|
||||||
|
expectedResult: []string{"dashboard under parent folder"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Should be able to view inherited folders if nested folders are enabled",
|
||||||
|
queryType: searchstore.TypeFolder,
|
||||||
|
permission: dashboards.PERMISSION_VIEW,
|
||||||
|
permissions: []accesscontrol.Permission{
|
||||||
|
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
|
||||||
|
},
|
||||||
|
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
|
||||||
|
expectedResult: []string{"parent", "subfolder"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Should not be able to view inherited folders if nested folders are not enabled",
|
||||||
|
queryType: searchstore.TypeFolder,
|
||||||
|
permission: dashboards.PERMISSION_VIEW,
|
||||||
|
permissions: []accesscontrol.Permission{
|
||||||
|
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
|
||||||
|
},
|
||||||
|
features: featuremgmt.WithFeatures(),
|
||||||
|
expectedResult: []string{"parent"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Should be able to view inherited dashboards and folders if nested folders are enabled",
|
||||||
|
permission: dashboards.PERMISSION_VIEW,
|
||||||
|
permissions: []accesscontrol.Permission{
|
||||||
|
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
|
||||||
|
{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
|
||||||
|
},
|
||||||
|
features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
|
||||||
|
expectedResult: []string{"parent", "subfolder", "dashboard under parent folder", "dashboard under subfolder"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Should not be able to view inherited dashboards and folders if nested folders are not enabled",
|
||||||
|
permission: dashboards.PERMISSION_VIEW,
|
||||||
|
permissions: []accesscontrol.Permission{
|
||||||
|
{Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
|
||||||
|
{Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
|
||||||
|
},
|
||||||
|
features: featuremgmt.WithFeatures(),
|
||||||
|
expectedResult: []string{"parent", "dashboard under parent folder"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
origNewGuardian := guardian.New
|
||||||
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
|
||||||
|
t.Cleanup(func() {
|
||||||
|
guardian.New = origNewGuardian
|
||||||
|
})
|
||||||
|
|
||||||
|
var orgID int64 = 1
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
tc.permissions = append(tc.permissions, accesscontrol.Permission{
|
||||||
|
Action: dashboards.ActionFoldersCreate,
|
||||||
|
}, accesscontrol.Permission{
|
||||||
|
Action: dashboards.ActionFoldersWrite,
|
||||||
|
Scope: dashboards.ScopeFoldersAll,
|
||||||
|
})
|
||||||
|
usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(tc.permissions)}}
|
||||||
|
db := setupNestedTest(t, usr, tc.permissions, orgID, tc.features)
|
||||||
|
recursiveQueriesAreSupported, err := db.RecursiveQueriesAreSupported()
|
||||||
|
require.NoError(t, err)
|
||||||
|
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tc.permission, tc.queryType, tc.features, recursiveQueriesAreSupported)
|
||||||
|
var result []string
|
||||||
|
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
|
q, params := filter.Where()
|
||||||
|
recQry, recQryParams := filter.With()
|
||||||
|
params = append(recQryParams, params...)
|
||||||
|
err := sess.SQL(recQry+"\nSELECT title FROM dashboard WHERE "+q, params...).Find(&result)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.expectedResult, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setupTest(t *testing.T, numFolders, numDashboards int, permissions []accesscontrol.Permission) db.DB {
|
func setupTest(t *testing.T, numFolders, numDashboards int, permissions []accesscontrol.Permission) db.DB {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
store := db.InitTestDB(t)
|
store := db.InitTestDB(t)
|
||||||
err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
dashes := make([]dashboards.Dashboard, 0, numFolders+numDashboards)
|
dashes := make([]dashboards.Dashboard, 0, numFolders+numDashboards)
|
||||||
@@ -227,3 +350,95 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol.Permission, orgID int64, features featuremgmt.FeatureToggles) db.DB {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
db := sqlstore.InitTestDB(t)
|
||||||
|
|
||||||
|
// dashboard store commands that should be called.
|
||||||
|
dashStore, err := database.ProvideDashboardStore(db, db.Cfg, features, tagimpl.ProvideService(db, db.Cfg), quotatest.New(false, nil))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), db.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features)
|
||||||
|
|
||||||
|
// create parent folder
|
||||||
|
parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
||||||
|
UID: "parent",
|
||||||
|
OrgID: orgID,
|
||||||
|
Title: "parent",
|
||||||
|
SignedInUser: usr,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// create subfolder
|
||||||
|
subfolder, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
||||||
|
UID: "subfolder",
|
||||||
|
ParentUID: "parent",
|
||||||
|
OrgID: orgID,
|
||||||
|
Title: "subfolder",
|
||||||
|
SignedInUser: usr,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// create dashboard under parent folder
|
||||||
|
_, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{
|
||||||
|
OrgID: orgID,
|
||||||
|
FolderID: parent.ID,
|
||||||
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
|
"title": "dashboard under parent folder",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// create dashboard under subfolder
|
||||||
|
_, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{
|
||||||
|
OrgID: orgID,
|
||||||
|
FolderID: subfolder.ID,
|
||||||
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
|
"title": "dashboard under subfolder",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
|
role := &accesscontrol.Role{
|
||||||
|
OrgID: 0,
|
||||||
|
UID: "basic_viewer",
|
||||||
|
Name: "basic:viewer",
|
||||||
|
Updated: time.Now(),
|
||||||
|
Created: time.Now(),
|
||||||
|
}
|
||||||
|
_, err = sess.Insert(role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = sess.Insert(accesscontrol.BuiltinRole{
|
||||||
|
OrgID: 0,
|
||||||
|
RoleID: role.ID,
|
||||||
|
Role: "Viewer",
|
||||||
|
Created: time.Now(),
|
||||||
|
Updated: time.Now(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range perms {
|
||||||
|
perms[i].RoleID = role.ID
|
||||||
|
perms[i].Created = time.Now()
|
||||||
|
perms[i].Updated = time.Now()
|
||||||
|
}
|
||||||
|
if len(perms) > 0 {
|
||||||
|
_, err = sess.InsertMulti(&perms)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,26 +10,59 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
|
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||||
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
||||||
|
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards int) {
|
func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards, numFolders, nestingLevel int) {
|
||||||
store := setupBenchMark(b, numUsers, numDashboards)
|
usr := user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{
|
||||||
|
1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
|
||||||
|
{
|
||||||
|
Action: dashboards.ActionFoldersCreate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: dashboards.ActionFoldersWrite,
|
||||||
|
Scope: dashboards.ScopeFoldersAll,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
|
||||||
|
features := featuremgmt.WithFeatures()
|
||||||
|
// if nestingLevel > 0 enable nested folders
|
||||||
|
if nestingLevel > 0 {
|
||||||
|
features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
|
||||||
|
}
|
||||||
|
|
||||||
|
store := setupBenchMark(b, usr, features, numUsers, numDashboards, numFolders, nestingLevel)
|
||||||
|
|
||||||
|
recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
|
||||||
|
require.NoError(b, err)
|
||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
usr := &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{1: {}}}
|
filter := permissions.NewAccessControlDashboardPermissionFilter(&usr, dashboards.PERMISSION_VIEW, "", features, recursiveQueriesAreSupported)
|
||||||
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, dashboards.PERMISSION_VIEW, "")
|
|
||||||
var result int
|
var result int
|
||||||
err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
q, params := filter.Where()
|
q, params := filter.Where()
|
||||||
_, err := sess.SQL("SELECT COUNT(*) FROM dashboard WHERE "+q, params...).Get(&result)
|
recQry, recQryParams := filter.With()
|
||||||
|
params = append(recQryParams, params...)
|
||||||
|
_, err := sess.SQL(recQry+"SELECT COUNT(*) FROM dashboard WHERE "+q, params...).Get(&result)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
@@ -37,15 +70,78 @@ func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupBenchMark(b *testing.B, numUsers, numDashboards int) db.DB {
|
func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.FeatureToggles, numUsers, numDashboards, numFolders, nestingLevel int) db.DB {
|
||||||
|
if nestingLevel > folder.MaxNestedFolderDepth {
|
||||||
|
nestingLevel = folder.MaxNestedFolderDepth
|
||||||
|
}
|
||||||
|
|
||||||
store := db.InitTestDB(b)
|
store := db.InitTestDB(b)
|
||||||
now := time.Now()
|
|
||||||
err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
quotaService := quotatest.New(false, nil)
|
||||||
dashes := make([]dashboards.Dashboard, 0, numDashboards)
|
|
||||||
for i := 1; i <= numDashboards; i++ {
|
dashboardWriteStore, err := database.ProvideDashboardStore(store, store.Cfg, features, tagimpl.ProvideService(store, store.Cfg), quotaService)
|
||||||
|
require.NoError(b, err)
|
||||||
|
|
||||||
|
folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), store.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), store, features)
|
||||||
|
|
||||||
|
origNewGuardian := guardian.New
|
||||||
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
|
||||||
|
b.Cleanup(func() {
|
||||||
|
guardian.New = origNewGuardian
|
||||||
|
})
|
||||||
|
|
||||||
|
rootFolders := make([]*folder.Folder, 0, numFolders)
|
||||||
|
dashes := make([]dashboards.Dashboard, 0, numDashboards)
|
||||||
|
parentUID := ""
|
||||||
|
for i := 0; i < numFolders; i++ {
|
||||||
|
uid := fmt.Sprintf("f%d", i)
|
||||||
|
f, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
||||||
|
UID: uid,
|
||||||
|
OrgID: usr.OrgID,
|
||||||
|
Title: uid,
|
||||||
|
SignedInUser: &usr,
|
||||||
|
ParentUID: parentUID,
|
||||||
|
})
|
||||||
|
require.NoError(b, err)
|
||||||
|
rootFolders = append(rootFolders, f)
|
||||||
|
|
||||||
|
parentUID := f.UID
|
||||||
|
var leaf *folder.Folder
|
||||||
|
for j := 1; j <= nestingLevel; j++ {
|
||||||
|
uid := fmt.Sprintf("f%d_%d", i, j)
|
||||||
|
sf, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
||||||
|
UID: uid,
|
||||||
|
OrgID: usr.OrgID,
|
||||||
|
Title: uid,
|
||||||
|
SignedInUser: &usr,
|
||||||
|
ParentUID: parentUID,
|
||||||
|
})
|
||||||
|
require.NoError(b, err)
|
||||||
|
parentUID = sf.UID
|
||||||
|
leaf = sf
|
||||||
|
}
|
||||||
|
|
||||||
|
str := fmt.Sprintf("dashboard under folder %s", leaf.Title)
|
||||||
|
now := time.Now()
|
||||||
|
dashes = append(dashes, dashboards.Dashboard{
|
||||||
|
OrgID: usr.OrgID,
|
||||||
|
IsFolder: false,
|
||||||
|
UID: str,
|
||||||
|
Slug: str,
|
||||||
|
Title: str,
|
||||||
|
Data: simplejson.New(),
|
||||||
|
Created: now,
|
||||||
|
Updated: now,
|
||||||
|
FolderID: leaf.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
|
now := time.Now()
|
||||||
|
for i := len(dashes); i < numDashboards; i++ {
|
||||||
str := strconv.Itoa(i)
|
str := strconv.Itoa(i)
|
||||||
dashes = append(dashes, dashboards.Dashboard{
|
dashes = append(dashes, dashboards.Dashboard{
|
||||||
OrgID: 1,
|
OrgID: usr.OrgID,
|
||||||
IsFolder: false,
|
IsFolder: false,
|
||||||
UID: str,
|
UID: str,
|
||||||
Slug: str,
|
Slug: str,
|
||||||
@@ -79,10 +175,24 @@ func setupBenchMark(b *testing.B, numUsers, numDashboards int) db.DB {
|
|||||||
Created: now,
|
Created: now,
|
||||||
})
|
})
|
||||||
for _, dash := range dashes {
|
for _, dash := range dashes {
|
||||||
|
// add permission to read dashboards under the general
|
||||||
|
if dash.FolderID == 0 {
|
||||||
|
permissions = append(permissions, accesscontrol.Permission{
|
||||||
|
RoleID: int64(i),
|
||||||
|
Action: dashboards.ActionDashboardsRead,
|
||||||
|
Scope: dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID),
|
||||||
|
Updated: now,
|
||||||
|
Created: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range rootFolders {
|
||||||
|
// add permission to read folders under specific folders
|
||||||
permissions = append(permissions, accesscontrol.Permission{
|
permissions = append(permissions, accesscontrol.Permission{
|
||||||
RoleID: int64(i),
|
RoleID: int64(i),
|
||||||
Action: dashboards.ActionDashboardsRead,
|
Action: dashboards.ActionDashboardsRead,
|
||||||
Scope: dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID),
|
Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID),
|
||||||
Updated: now,
|
Updated: now,
|
||||||
Created: now,
|
Created: now,
|
||||||
})
|
})
|
||||||
@@ -113,16 +223,52 @@ func setupBenchMark(b *testing.B, numUsers, numDashboards int) db.DB {
|
|||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkDashboardPermissionFilter_100_100(b *testing.B) {
|
func BenchmarkDashboardPermissionFilter_100_100_0_0(b *testing.B) {
|
||||||
benchmarkDashboardPermissionFilter(b, 100, 100)
|
benchmarkDashboardPermissionFilter(b, 100, 100, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkDashboardPermissionFilter_100_1000(b *testing.B) {
|
func BenchmarkDashboardPermissionFilter_100_100_10_2(b *testing.B) {
|
||||||
benchmarkDashboardPermissionFilter(b, 100, 1000)
|
benchmarkDashboardPermissionFilter(b, 100, 100, 10, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkDashboardPermissionFilter_300_10000(b *testing.B) {
|
func BenchmarkDashboardPermissionFilter_100_100_10_4(b *testing.B) {
|
||||||
benchmarkDashboardPermissionFilter(b, 300, 10000)
|
benchmarkDashboardPermissionFilter(b, 100, 100, 10, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDashboardPermissionFilter_100_100_10_8(b *testing.B) {
|
||||||
|
benchmarkDashboardPermissionFilter(b, 100, 100, 10, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDashboardPermissionFilter_100_1000_0_0(b *testing.B) {
|
||||||
|
benchmarkDashboardPermissionFilter(b, 100, 1000, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDashboardPermissionFilter_100_1000_10_2(b *testing.B) {
|
||||||
|
benchmarkDashboardPermissionFilter(b, 100, 1000, 10, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDashboardPermissionFilter_100_1000_10_4(b *testing.B) {
|
||||||
|
benchmarkDashboardPermissionFilter(b, 100, 1000, 10, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDashboardPermissionFilter_100_1000_10_8(b *testing.B) {
|
||||||
|
benchmarkDashboardPermissionFilter(b, 100, 1000, 10, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDashboardPermissionFilter_300_10000_0_0(b *testing.B) {
|
||||||
|
benchmarkDashboardPermissionFilter(b, 300, 10000, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDashboardPermissionFilter_300_10000_10_2(b *testing.B) {
|
||||||
|
benchmarkDashboardPermissionFilter(b, 300, 10000, 10, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDashboardPermissionFilter_300_10000_10_4(b *testing.B) {
|
||||||
|
benchmarkDashboardPermissionFilter(b, 300, 10000, 10, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDashboardPermissionFilter_300_10000_10_8(b *testing.B) {
|
||||||
|
benchmarkDashboardPermissionFilter(b, 300, 10000, 10, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
func batch(count, batchSize int, eachFn func(start, end int) error) error {
|
func batch(count, batchSize int, eachFn func(start, end int) error) error {
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ func (b *Builder) ToSQL(limit, page int64) (string, []interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) buildSelect() {
|
func (b *Builder) buildSelect() {
|
||||||
|
var recQuery string
|
||||||
|
var recQueryParams []interface{}
|
||||||
|
|
||||||
b.sql.WriteString(
|
b.sql.WriteString(
|
||||||
`SELECT
|
`SELECT
|
||||||
dashboard.id,
|
dashboard.id,
|
||||||
@@ -61,9 +64,25 @@ func (b *Builder) buildSelect() {
|
|||||||
if f, ok := f.(FilterSelect); ok {
|
if f, ok := f.(FilterSelect); ok {
|
||||||
b.sql.WriteString(fmt.Sprintf(", %s", f.Select()))
|
b.sql.WriteString(fmt.Sprintf(", %s", f.Select()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if f, ok := f.(FilterWith); ok {
|
||||||
|
recQuery, recQueryParams = f.With()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.sql.WriteString(` FROM `)
|
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) {
|
func (b *Builder) applyFilters() (ordering string) {
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ type FilterWhere interface {
|
|||||||
Where() (string, []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
|
// 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.
|
// search result to ensure there is only one of each ID in the results.
|
||||||
// The id column must be present in the result.
|
// 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/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"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/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||||
@@ -148,6 +150,145 @@ func TestBuilder_Permissions(t *testing.T) {
|
|||||||
assert.Len(t, res, 0)
|
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 {
|
func setupTestEnvironment(t *testing.T) db.DB {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
store := db.InitTestDB(t)
|
store := db.InitTestDB(t)
|
||||||
|
|||||||
Reference in New Issue
Block a user