AuthZ: Extend /api/search to work with self-contained permissions (#70749)

* Search sql filter draft, unfinished

* Search works for empty roles

* Add current AuthModule to SignedInUser

* clean up, changes to the search

* Use constant prefixes

* Change AuthModule to AuthenticatedBy

* Add tests for using the permissions from the SignedInUser

* Refactor and simplify code

* Fix sql generation for pg and mysql

* Fixes, clean up

* Add test for empty permission list

* Fix

* Fix any vs all in case of edit permission

* Update pkg/services/authn/authn.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* Update pkg/services/sqlstore/permissions/dashboard_test.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* Fixes, changes based on the review

---------

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
Misi
2023-07-12 12:31:36 +02:00
committed by GitHub
co-authored by Gabriel MABILLE
parent e56b2cae00
commit 5efc3386d3
23 changed files with 650 additions and 235 deletions
+122 -34
View File
@@ -9,6 +9,7 @@ import (
"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/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
@@ -172,36 +173,68 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
permSelector := strings.Builder{}
var permSelectorArgs []interface{}
// useSelfContainedPermissions is true if the user's permissions are stored and set from the JWT token
// currently it's used for the extended JWT module (when the user is authenticated via a JWT token generated by Grafana)
useSelfContainedPermissions := f.user.AuthenticatedBy == login.ExtendedJWTModule
if len(f.dashboardActions) > 0 {
toCheck := actionsToCheck(f.dashboardActions, f.user.Permissions[f.user.OrgID], dashWildcards, folderWildcards)
if len(toCheck) > 0 {
builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 16) FROM permission WHERE scope LIKE 'dashboards:uid:%'")
builder.WriteString(rolesFilter)
args = append(args, params...)
if !useSelfContainedPermissions {
builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 16) FROM permission WHERE scope LIKE 'dashboards:uid:%'")
builder.WriteString(rolesFilter)
args = append(args, params...)
if len(toCheck) == 1 {
builder.WriteString(" AND action = ?")
args = append(args, toCheck[0])
if len(toCheck) == 1 {
builder.WriteString(" AND action = ?")
args = append(args, toCheck[0])
} else {
builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
args = append(args, toCheck...)
args = append(args, len(toCheck))
}
builder.WriteString(") AND NOT dashboard.is_folder)")
} else {
builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
args = append(args, toCheck...)
args = append(args, len(toCheck))
actions := parseStringSliceFromInterfaceSlice(toCheck)
args = getAllowedUIDs(actions, f.user, dashboards.ScopeDashboardsPrefix)
// Only add the IN clause if we have any dashboards to check
if len(args) > 0 {
builder.WriteString("(dashboard.uid IN (?" + strings.Repeat(", ?", len(args)-1) + "")
builder.WriteString(") AND NOT dashboard.is_folder)")
} else {
builder.WriteString("(1 = 0)")
}
}
builder.WriteString(") AND NOT dashboard.is_folder)")
builder.WriteString(" OR ")
permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%' ")
permSelector.WriteString(rolesFilter)
permSelectorArgs = append(permSelectorArgs, params...)
if len(toCheck) == 1 {
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
if !useSelfContainedPermissions {
permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%' ")
permSelector.WriteString(rolesFilter)
permSelectorArgs = append(permSelectorArgs, params...)
if len(toCheck) == 1 {
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
} else {
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
permSelectorArgs = append(permSelectorArgs, toCheck...)
permSelectorArgs = append(permSelectorArgs, len(toCheck))
}
} else {
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
permSelectorArgs = append(permSelectorArgs, toCheck...)
permSelectorArgs = append(permSelectorArgs, len(toCheck))
actions := parseStringSliceFromInterfaceSlice(toCheck)
permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix)
// Only add the IN clause if we have any folders to check
if len(permSelectorArgs) > 0 {
permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "")
} else {
permSelector.WriteString("(")
}
}
permSelector.WriteRune(')')
@@ -221,9 +254,13 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
}
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...)
if len(permSelectorArgs) > 0 {
builder.WriteString("WHERE d.uid IN ")
builder.WriteString(permSelector.String())
args = append(args, permSelectorArgs...)
} else {
builder.WriteString("WHERE 1 = 0")
}
}
builder.WriteString(") AND NOT dashboard.is_folder)")
} else {
@@ -242,17 +279,30 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
toCheck := actionsToCheck(f.folderActions, f.user.Permissions[f.user.OrgID], folderWildcards)
if len(toCheck) > 0 {
permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%'")
permSelector.WriteString(rolesFilter)
permSelectorArgs = append(permSelectorArgs, params...)
if len(toCheck) == 1 {
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
if !useSelfContainedPermissions {
permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%'")
permSelector.WriteString(rolesFilter)
permSelectorArgs = append(permSelectorArgs, params...)
if len(toCheck) == 1 {
permSelector.WriteString(" AND action = ?")
permSelectorArgs = append(permSelectorArgs, toCheck[0])
} else {
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
permSelectorArgs = append(permSelectorArgs, toCheck...)
permSelectorArgs = append(permSelectorArgs, len(toCheck))
}
} else {
permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
permSelectorArgs = append(permSelectorArgs, toCheck...)
permSelectorArgs = append(permSelectorArgs, len(toCheck))
actions := parseStringSliceFromInterfaceSlice(toCheck)
permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix)
if len(permSelectorArgs) > 0 {
permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "")
} else {
permSelector.WriteString("(")
}
}
permSelector.WriteRune(')')
switch f.features.IsEnabled(featuremgmt.FlagNestedFolders) {
@@ -271,15 +321,20 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
args = append(args, nestedFoldersArgs...)
}
default:
builder.WriteString("(dashboard.uid IN ")
builder.WriteString(permSelector.String())
args = append(args, permSelectorArgs...)
if len(permSelectorArgs) > 0 {
builder.WriteString("(dashboard.uid IN ")
builder.WriteString(permSelector.String())
args = append(args, permSelectorArgs...)
} else {
builder.WriteString("(1 = 0")
}
}
builder.WriteString(" AND dashboard.is_folder)")
} else {
builder.WriteString("dashboard.is_folder")
}
}
builder.WriteRune(')')
f.where = clause{string: builder.String(), params: args}
@@ -365,3 +420,36 @@ func nestedFoldersSelectors(permSelector string, permSelectorArgs []interface{},
return strings.Join(wheres, ") OR "), args
}
func parseStringSliceFromInterfaceSlice(slice []interface{}) []string {
result := make([]string, 0, len(slice))
for _, s := range slice {
result = append(result, s.(string))
}
return result
}
func getAllowedUIDs(actions []string, user *user.SignedInUser, scopePrefix string) []interface{} {
uidToActions := make(map[string]map[string]struct{})
for _, action := range actions {
for _, uidScope := range user.Permissions[user.OrgID][action] {
if !strings.HasPrefix(uidScope, scopePrefix) {
continue
}
uid := strings.TrimPrefix(uidScope, scopePrefix)
if _, exists := uidToActions[uid]; !exists {
uidToActions[uid] = make(map[string]struct{})
}
uidToActions[uid][action] = struct{}{}
}
}
// args max capacity is the length of the different uids
args := make([]interface{}, 0, len(uidToActions))
for uid, assignedActions := range uidToActions {
if len(assignedActions) == len(actions) {
args = append(args, uid)
}
}
return args
}