Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RBAC: Improve performance of dashboard filter query #56813

Merged
merged 25 commits into from Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0b3836e
RBAC: Move UserRolesFilter to domain package
kalleep Oct 12, 2022
f79589c
Dashboard Permissions: Rewrite rbac filter to check access in sql
kalleep Oct 12, 2022
e7f26dd
RBAC: Add break when wildcard is found
kalleep Oct 12, 2022
169d1f9
Annotations: Fix rbac tests by creating role and permisisons for user
kalleep Oct 12, 2022
19faaad
RBAC: Fix action
kalleep Oct 12, 2022
3c82359
Lint
kalleep Oct 13, 2022
5213a24
RBAC: Use correct prefix for folders
kalleep Oct 13, 2022
c0af53d
RBAC: check for correct type and use correct action
kalleep Oct 13, 2022
25b5bd0
RBAC: fix folder checks
kalleep Oct 13, 2022
c19d6cc
RBAC: Add tests for dashboard filter
kalleep Oct 13, 2022
d3a840c
RBAC: Update tests
kalleep Oct 13, 2022
289e89c
RBAC: Cover more test cases
kalleep Oct 13, 2022
0b17bbb
RBAC: Update query to support checking several actions
kalleep Oct 13, 2022
0e3f80b
Lint
kalleep Oct 13, 2022
e3d00d2
Lint
kalleep Oct 14, 2022
5010cd4
Update test values
kalleep Oct 14, 2022
4e25bee
RBAC: Mark as not folders
kalleep Oct 14, 2022
d50c3f2
RBAC: Fix dashboard direct permissions
kalleep Oct 14, 2022
7df4766
RBAC: Fix dashboard read with folder scope and folder read
kalleep Oct 14, 2022
7f328ad
Remove bench file
kalleep Oct 14, 2022
cd425e9
RBAC: Add benchmarks
kalleep Oct 18, 2022
d04c4e8
RBAC: Add benchmarks
kalleep Oct 18, 2022
9b8e446
Update pkg/services/sqlstore/permissions/dashboard.go
kalleep Oct 19, 2022
dbad0a7
Fix import
kalleep Oct 19, 2022
290b335
RBAC: Change to counting actions
kalleep Oct 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 1 addition & 56 deletions pkg/services/accesscontrol/database/database.go
Expand Up @@ -9,10 +9,6 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
)

const (
globalOrgID = 0
)

func ProvideService(sql db.DB) *AccessControlStore {
return &AccessControlStore{sql}
}
Expand All @@ -29,7 +25,7 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces
return nil
}

filter, params := userRolesFilter(query.OrgID, query.UserID, query.TeamIDs, query.Roles)
filter, params := accesscontrol.UserRolesFilter(query.OrgID, query.UserID, query.TeamIDs, query.Roles)

q := `
SELECT
Expand Down Expand Up @@ -59,57 +55,6 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces
return result, err
}

func userRolesFilter(orgID, userID int64, teamIDs []int64, roles []string) (string, []interface{}) {
var params []interface{}
builder := strings.Builder{}

// This is an additional security. We should never have permissions granted to userID 0.
// Only allow real users to get user/team permissions (anonymous/apikeys)
if userID > 0 {
builder.WriteString(`
SELECT ur.role_id
FROM user_role AS ur
WHERE ur.user_id = ?
AND (ur.org_id = ? OR ur.org_id = ?)
`)
params = []interface{}{userID, orgID, globalOrgID}
}

if len(teamIDs) > 0 {
if builder.Len() > 0 {
builder.WriteString("UNION")
}
builder.WriteString(`
SELECT tr.role_id FROM team_role as tr
WHERE tr.team_id IN(?` + strings.Repeat(", ?", len(teamIDs)-1) + `)
AND tr.org_id = ?
`)
for _, id := range teamIDs {
params = append(params, id)
}
params = append(params, orgID)
}

if len(roles) != 0 {
if builder.Len() > 0 {
builder.WriteString("UNION")
}

builder.WriteString(`
SELECT br.role_id FROM builtin_role AS br
WHERE br.role IN (?` + strings.Repeat(", ?", len(roles)-1) + `)
AND (br.org_id = ? OR br.org_id = ?)
`)
for _, role := range roles {
params = append(params, role)
}

params = append(params, orgID, globalOrgID)
}

return "INNER JOIN (" + builder.String() + ") as all_role ON role.id = all_role.role_id", params
}

func (s *AccessControlStore) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error {
err := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
roleDeleteQuery := "DELETE FROM user_role WHERE user_id = ?"
Expand Down
51 changes: 51 additions & 0 deletions pkg/services/accesscontrol/filter.go
Expand Up @@ -125,3 +125,54 @@ func SetAcceptListForTest(list map[string]struct{}) func() {
sqlIDAcceptList = original
}
}

func UserRolesFilter(orgID, userID int64, teamIDs []int64, roles []string) (string, []interface{}) {
var params []interface{}
builder := strings.Builder{}

// This is an additional security. We should never have permissions granted to userID 0.
// Only allow real users to get user/team permissions (anonymous/apikeys)
if userID > 0 {
builder.WriteString(`
SELECT ur.role_id
FROM user_role AS ur
WHERE ur.user_id = ?
AND (ur.org_id = ? OR ur.org_id = ?)
`)
params = []interface{}{userID, orgID, GlobalOrgID}
}

if len(teamIDs) > 0 {
if builder.Len() > 0 {
builder.WriteString("UNION")
}
builder.WriteString(`
SELECT tr.role_id FROM team_role as tr
WHERE tr.team_id IN(?` + strings.Repeat(", ?", len(teamIDs)-1) + `)
AND tr.org_id = ?
`)
for _, id := range teamIDs {
params = append(params, id)
}
params = append(params, orgID)
}

if len(roles) != 0 {
if builder.Len() > 0 {
builder.WriteString("UNION")
}

builder.WriteString(`
SELECT br.role_id FROM builtin_role AS br
WHERE br.role IN (?` + strings.Repeat(", ?", len(roles)-1) + `)
AND (br.org_id = ? OR br.org_id = ?)
`)
for _, role := range roles {
params = append(params, role)
}

params = append(params, orgID, GlobalOrgID)
}

return "INNER JOIN (" + builder.String() + ") as all_role ON role.id = all_role.role_id", params
}
69 changes: 67 additions & 2 deletions pkg/services/annotations/annotationsimpl/xorm_store_test.go
Expand Up @@ -5,7 +5,9 @@ import (
"fmt"
"strings"
"testing"
"time"

"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -407,13 +409,15 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
t.Skip("skipping integration test")
}
sql := db.InitTestDB(t)

var maximumTagsLength int64 = 60
repo := xormRepositoryImpl{db: sql, cfg: setting.NewCfg(), log: log.New("annotation.test"), tagService: tagimpl.ProvideService(sql, sql.Cfg), maximumTagsLength: maximumTagsLength}
dashboardStore := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg))

testDashboard1 := models.SaveDashboardCommand{
UserId: 1,
OrgId: 1,
UserId: 1,
OrgId: 1,
IsFolder: false,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dashboard 1",
}),
Expand Down Expand Up @@ -459,6 +463,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
UserID: 1,
OrgID: 1,
}
role := setupRBACRole(t, repo, user)

type testStruct struct {
description string
Expand Down Expand Up @@ -519,6 +524,8 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
user.Permissions = map[int64]map[string][]string{1: tc.permissions}
setupRBACPermission(t, repo, role, user)

results, err := repo.Get(context.Background(), &annotations.ItemQuery{
OrgId: 1,
SignedInUser: user,
Expand All @@ -535,3 +542,61 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
})
}
}

func setupRBACRole(t *testing.T, repo xormRepositoryImpl, user *user.SignedInUser) *accesscontrol.Role {
t.Helper()
var role *accesscontrol.Role
err := repo.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, repo xormRepositoryImpl, role *accesscontrol.Role, user *user.SignedInUser) {
t.Helper()
err := repo.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)
}
83 changes: 62 additions & 21 deletions pkg/services/sqlstore/permissions/dashboard.go
Expand Up @@ -80,9 +80,9 @@ func (d DashboardPermissionFilter) Where() (string, []interface{}) {
}

type AccessControlDashboardPermissionFilter struct {
User *user.SignedInUser
dashboardActions []string
user *user.SignedInUser
folderActions []string
dashboardActions []string
}

// NewAccessControlDashboardPermissionFilter creates a new AccessControlDashboardPermissionFilter that is configured with specific actions calculated based on the models.PermissionType and query type
Expand All @@ -102,39 +102,80 @@ func NewAccessControlDashboardPermissionFilter(user *user.SignedInUser, permissi
dashboardActions = append(dashboardActions, dashboards.ActionDashboardsWrite)
}
}
return AccessControlDashboardPermissionFilter{User: user, folderActions: folderActions, dashboardActions: dashboardActions}
return AccessControlDashboardPermissionFilter{user: user, folderActions: folderActions, dashboardActions: dashboardActions}
}

func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{}) {
if f.user == nil || f.user.Permissions == nil || f.user.Permissions[f.user.OrgID] == nil {
return "(1 = 0)", nil
}
dashWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeDashboardsPrefix)
folderWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix)

filter, params := accesscontrol.UserRolesFilter(f.user.OrgID, f.user.UserID, f.user.Teams, accesscontrol.GetOrgRoles(f.user))
rolesFilter := "AND role_id IN(SELECT distinct id FROM role " + filter + ")"
var args []interface{}
builder := strings.Builder{}
builder.WriteString("(")

builder.WriteRune('(')
if len(f.dashboardActions) > 0 {
builder.WriteString("((")

dashFilter, _ := accesscontrol.Filter(f.User, "dashboard.uid", dashboards.ScopeDashboardsPrefix, f.dashboardActions...)
builder.WriteString(dashFilter.Where)
args = append(args, dashFilter.Args...)
actionsToCheck := make([]interface{}, 0, len(f.dashboardActions))
for _, action := range f.dashboardActions {
var hasWildcard bool
for _, scope := range f.user.Permissions[f.user.OrgID][action] {
if dashWildcards.Contains(scope) || folderWildcards.Contains(scope) {
hasWildcard = true
break
}
}
if !hasWildcard {
actionsToCheck = append(actionsToCheck, action)
}
}
Comment on lines +121 to +133
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would place this in a function to dedup some code and ease reading.


builder.WriteString(" OR dashboard.folder_id IN(SELECT id FROM dashboard WHERE ")
dashFolderFilter, _ := accesscontrol.Filter(f.User, "dashboard.uid", dashboards.ScopeFoldersPrefix, f.dashboardActions...)
if len(actionsToCheck) > 0 {
builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 16) FROM permission WHERE action IN (?" + strings.Repeat(", ?", len(actionsToCheck)-1) + ") AND scope LIKE 'dashboards:uid:%' " + rolesFilter + " GROUP BY role_id, scope HAVING COUNT(action) = ?) AND NOT dashboard.is_folder)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't the GROUP BY role_id, scope HAVING COUNT(action) = ? assumes that the actions have been granted with the same role?
If we assume users will go through managed permissions, I guess that's ok given this is the case.

Edit: How likely is the edge case of actions been granted through different roles? Not that likely.

args = append(args, actionsToCheck...)
args = append(args, params...)
args = append(args, len(actionsToCheck))
kalleep marked this conversation as resolved.
Show resolved Hide resolved

builder.WriteString(dashFolderFilter.Where)
builder.WriteString(")) AND NOT dashboard.is_folder)")
args = append(args, dashFolderFilter.Args...)
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 action IN (?" + strings.Repeat(", ?", len(actionsToCheck)-1) + ") AND scope LIKE 'folders:uid:%' " + rolesFilter + " GROUP BY role_id, scope HAVING COUNT(action) = ?)) AND NOT dashboard.is_folder)")
args = append(args, actionsToCheck...)
args = append(args, params...)
args = append(args, len(actionsToCheck))
} else {
builder.WriteString("NOT dashboard.is_folder")
}
}

if len(f.folderActions) > 0 {
if len(f.dashboardActions) > 0 {
builder.WriteString(" OR ")
}
builder.WriteString("(")
folderFilter, _ := accesscontrol.Filter(f.User, "dashboard.uid", dashboards.ScopeFoldersPrefix, f.folderActions...)
builder.WriteString(folderFilter.Where)
builder.WriteString(" AND dashboard.is_folder)")
args = append(args, folderFilter.Args...)

actionsToCheck := make([]interface{}, 0, len(f.folderActions))
for _, action := range f.folderActions {
var hasWildcard bool
for _, scope := range f.user.Permissions[f.user.OrgID][action] {
if folderWildcards.Contains(scope) {
hasWildcard = true
break
}
}
if !hasWildcard {
actionsToCheck = append(actionsToCheck, action)
}
}

if len(actionsToCheck) > 0 {
builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 13) FROM permission WHERE action IN (?" + strings.Repeat(", ?", len(actionsToCheck)-1) + ") AND scope LIKE 'folders:uid:%' " + rolesFilter + " GROUP BY role_id, scope HAVING COUNT(action) = ?) AND dashboard.is_folder)")
args = append(args, actionsToCheck...)
args = append(args, params...)
args = append(args, len(actionsToCheck))
} else {
builder.WriteString("dashboard.is_folder")
}
}
builder.WriteString(")")
builder.WriteRune(')')
return builder.String(), args
}