mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
RBAC: Add dash and folder action sets where they are missing (#92832)
* add dash and folder action sets where they are missing * remove an empty line, try to make linting pass
This commit is contained in:
parent
77e1f222a0
commit
6eeef432de
@ -0,0 +1,141 @@
|
|||||||
|
package accesscontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"xorm.io/xorm"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
|
)
|
||||||
|
|
||||||
|
const AddActionSetMigrationID = "adding action set permissions"
|
||||||
|
|
||||||
|
func AddActionSetPermissionsMigrator(mg *migrator.Migrator) {
|
||||||
|
mg.AddMigration(AddActionSetMigrationID, &actionSetMigrator{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionSetMigrator struct {
|
||||||
|
sess *xorm.Session
|
||||||
|
migrator *migrator.Migrator
|
||||||
|
migrator.MigrationBase
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ migrator.CodeMigration = new(actionSetMigrator)
|
||||||
|
|
||||||
|
func (m *actionSetMigrator) SQL(migrator.Dialect) string {
|
||||||
|
return "code migration"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *actionSetMigrator) Exec(sess *xorm.Session, migrator *migrator.Migrator) error {
|
||||||
|
m.sess = sess
|
||||||
|
m.migrator = migrator
|
||||||
|
return m.addActionSetActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *actionSetMigrator) addActionSetActions() error {
|
||||||
|
var results []accesscontrol.Permission
|
||||||
|
|
||||||
|
// Find action sets and dashboard permissions for managed roles
|
||||||
|
// We don't need all dashboard permissions, just enough to help us determine what action set permissions to add
|
||||||
|
sql := `
|
||||||
|
SELECT permission.role_id, permission.action, permission.scope FROM permission
|
||||||
|
LEFT JOIN role ON permission.role_id = role.id
|
||||||
|
WHERE permission.action IN ('dashboards:read', 'dashboards:write', 'dashboards.permissions:read', 'dashboards:view', 'dashboards:edit', 'dashboards:admin', 'folders:view', 'folders:edit', 'folders:admin')
|
||||||
|
AND role.name LIKE 'managed:%'
|
||||||
|
`
|
||||||
|
if err := m.sess.SQL(sql).Find(&results); err != nil {
|
||||||
|
return fmt.Errorf("failed to query permissions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// group permissions by map[roleID]map[scope]actionSet
|
||||||
|
groupedPermissions := make(map[int64]map[string]string)
|
||||||
|
hasActionSet := make(map[int64]map[string]bool)
|
||||||
|
for _, result := range results {
|
||||||
|
// keep track of which dash/folder permission grants already have an action set permission
|
||||||
|
if isActionSetAction(result.Action) {
|
||||||
|
if _, ok := hasActionSet[result.RoleID]; !ok {
|
||||||
|
hasActionSet[result.RoleID] = make(map[string]bool)
|
||||||
|
}
|
||||||
|
hasActionSet[result.RoleID][result.Scope] = true
|
||||||
|
delete(groupedPermissions[result.RoleID], result.Scope)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't add action set permissions where they already exist
|
||||||
|
if _, has := hasActionSet[result.RoleID]; has && hasActionSet[result.RoleID][result.Scope] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := groupedPermissions[result.RoleID]; !ok {
|
||||||
|
groupedPermissions[result.RoleID] = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the most permissive action set permission
|
||||||
|
currentActionSet := groupedPermissions[result.RoleID][result.Scope]
|
||||||
|
switch result.Action {
|
||||||
|
case "dashboards:read":
|
||||||
|
if currentActionSet == "" {
|
||||||
|
groupedPermissions[result.RoleID][result.Scope] = "view"
|
||||||
|
}
|
||||||
|
case "dashboards:write":
|
||||||
|
if currentActionSet != "admin" {
|
||||||
|
groupedPermissions[result.RoleID][result.Scope] = "edit"
|
||||||
|
}
|
||||||
|
case "dashboards.permissions:read":
|
||||||
|
groupedPermissions[result.RoleID][result.Scope] = "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toAdd := make([]accesscontrol.Permission, 0, len(groupedPermissions))
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for roleID, permissions := range groupedPermissions {
|
||||||
|
for scope, action := range permissions {
|
||||||
|
// should never be the case, but keeping this check for extra safety
|
||||||
|
if _, ok := hasActionSet[roleID][scope]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(scope, "folders:") {
|
||||||
|
action = fmt.Sprintf("folders:%s", action)
|
||||||
|
} else {
|
||||||
|
action = fmt.Sprintf("dashboards:%s", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
kind, attr, identifier := accesscontrol.SplitScope(scope)
|
||||||
|
toAdd = append(toAdd, accesscontrol.Permission{
|
||||||
|
RoleID: roleID,
|
||||||
|
Scope: scope,
|
||||||
|
Action: action,
|
||||||
|
Kind: kind,
|
||||||
|
Attribute: attr,
|
||||||
|
Identifier: identifier,
|
||||||
|
Created: now,
|
||||||
|
Updated: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(toAdd) > 0 {
|
||||||
|
err := batch(len(toAdd), batchSize, func(start, end int) error {
|
||||||
|
m.migrator.Logger.Debug(fmt.Sprintf("inserting permissions %v", toAdd[start:end]))
|
||||||
|
if _, err := m.sess.InsertMulti(toAdd[start:end]); err != nil {
|
||||||
|
return fmt.Errorf("failed to add action sets: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.migrator.Logger.Debug("updated managed roles with dash and folder action set permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isActionSetAction(action string) bool {
|
||||||
|
return action == "dashboards:view" || action == "dashboards:edit" || action == "dashboards:admin" || action == "folders:view" || action == "folders:edit" || action == "folders:admin"
|
||||||
|
}
|
@ -0,0 +1,282 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||||
|
acmig "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestActionSetMigration(t *testing.T) {
|
||||||
|
// Run initial migration to have a working DB
|
||||||
|
x := setupTestDB(t)
|
||||||
|
|
||||||
|
type migrationTestCase struct {
|
||||||
|
desc string
|
||||||
|
existingRolePerms map[string]map[string][]string
|
||||||
|
expectedActionSets map[string]map[string]string
|
||||||
|
}
|
||||||
|
testCases := []migrationTestCase{
|
||||||
|
{
|
||||||
|
desc: "empty perms",
|
||||||
|
existingRolePerms: map[string]map[string][]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "dashboard permissions that are not managed don't get an action set",
|
||||||
|
existingRolePerms: map[string]map[string][]string{
|
||||||
|
"my_custom_role": {
|
||||||
|
"dashboards:uid:1": ossaccesscontrol.DashboardViewActions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "managed permissions that are not dashboard permissions don't get an action set",
|
||||||
|
existingRolePerms: map[string]map[string][]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"datasources:uid:1": {"datasources:query", "datasources:read"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "managed dash viewer gets a viewer action set",
|
||||||
|
existingRolePerms: map[string]map[string][]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"dashboards:uid:1": ossaccesscontrol.DashboardViewActions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedActionSets: map[string]map[string]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"dashboards:uid:1": "dashboards:view",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "managed dash editor gets an editor action set",
|
||||||
|
existingRolePerms: map[string]map[string][]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"dashboards:uid:1": ossaccesscontrol.DashboardEditActions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedActionSets: map[string]map[string]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"dashboards:uid:1": "dashboards:edit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "managed dash admin gets an admin action set",
|
||||||
|
existingRolePerms: map[string]map[string][]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"dashboards:uid:1": ossaccesscontrol.DashboardAdminActions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedActionSets: map[string]map[string]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"dashboards:uid:1": "dashboards:admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "managed folder viewer gets a viewer action set",
|
||||||
|
existingRolePerms: map[string]map[string][]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"folders:uid:1": append(ossaccesscontrol.FolderViewActions, ossaccesscontrol.DashboardViewActions...),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedActionSets: map[string]map[string]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"folders:uid:1": "folders:view",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "managed folder editor gets an editor action set",
|
||||||
|
existingRolePerms: map[string]map[string][]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"folders:uid:1": append(ossaccesscontrol.FolderEditActions, ossaccesscontrol.DashboardEditActions...),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedActionSets: map[string]map[string]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"folders:uid:1": "folders:edit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "managed folder admin gets an admin action set",
|
||||||
|
existingRolePerms: map[string]map[string][]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"folders:uid:1": append(ossaccesscontrol.FolderAdminActions, ossaccesscontrol.DashboardAdminActions...),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedActionSets: map[string]map[string]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"folders:uid:1": "folders:admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "can add action sets for multiple folders and dashboards under the same managed permission",
|
||||||
|
existingRolePerms: map[string]map[string][]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"folders:uid:1": append(ossaccesscontrol.FolderAdminActions, ossaccesscontrol.DashboardAdminActions...),
|
||||||
|
"dashboards:uid:1": ossaccesscontrol.DashboardEditActions,
|
||||||
|
"datasources:uid:1": {"datasources:query", "datasources:read"},
|
||||||
|
"folders:uid:2": append(ossaccesscontrol.FolderViewActions, ossaccesscontrol.DashboardViewActions...),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedActionSets: map[string]map[string]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"folders:uid:1": "folders:admin",
|
||||||
|
"folders:uid:2": "folders:view",
|
||||||
|
"dashboards:uid:1": "dashboards:edit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "can add action sets for multiple managed roles",
|
||||||
|
existingRolePerms: map[string]map[string][]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"folders:uid:1": append(ossaccesscontrol.FolderAdminActions, ossaccesscontrol.DashboardAdminActions...),
|
||||||
|
"folders:uid:2": append(ossaccesscontrol.FolderViewActions, ossaccesscontrol.DashboardViewActions...),
|
||||||
|
},
|
||||||
|
"managed:users:1:permissions": {
|
||||||
|
"folders:uid:1": append(ossaccesscontrol.FolderEditActions, ossaccesscontrol.DashboardEditActions...),
|
||||||
|
"dashboards:uid:1": ossaccesscontrol.DashboardEditActions,
|
||||||
|
},
|
||||||
|
"managed:teams:1:permissions": {
|
||||||
|
"folders:uid:1": append(ossaccesscontrol.FolderEditActions, ossaccesscontrol.DashboardEditActions...),
|
||||||
|
"folders:uid:2": append(ossaccesscontrol.FolderAdminActions, ossaccesscontrol.DashboardAdminActions...),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedActionSets: map[string]map[string]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"folders:uid:1": "folders:admin",
|
||||||
|
"folders:uid:2": "folders:view",
|
||||||
|
},
|
||||||
|
"managed:users:1:permissions": {
|
||||||
|
"folders:uid:1": "folders:edit",
|
||||||
|
"dashboards:uid:1": "dashboards:edit",
|
||||||
|
},
|
||||||
|
"managed:teams:1:permissions": {
|
||||||
|
"folders:uid:1": "folders:edit",
|
||||||
|
"folders:uid:2": "folders:admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "can handle existing action sets",
|
||||||
|
existingRolePerms: map[string]map[string][]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"dashboards:uid:1": append(ossaccesscontrol.DashboardAdminActions, "dashboards:admin"),
|
||||||
|
"dashboards:uid:2": ossaccesscontrol.DashboardViewActions,
|
||||||
|
"dashboards:uid:4": append(ossaccesscontrol.DashboardViewActions, "dashboards:view"),
|
||||||
|
},
|
||||||
|
"managed:users:1:permissions": {
|
||||||
|
"dashboards:uid:1": append(ossaccesscontrol.DashboardEditActions, "dashboards:edit"),
|
||||||
|
"dashboards:uid:2": append(ossaccesscontrol.DashboardViewActions, "dashboards:view"),
|
||||||
|
"dashboards:uid:3": ossaccesscontrol.DashboardEditActions,
|
||||||
|
"dashboards:uid:4": ossaccesscontrol.DashboardAdminActions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedActionSets: map[string]map[string]string{
|
||||||
|
"managed:builtins:viewer:permissions": {
|
||||||
|
"dashboards:uid:1": "dashboards:admin",
|
||||||
|
"dashboards:uid:2": "dashboards:view",
|
||||||
|
"dashboards:uid:4": "dashboards:view",
|
||||||
|
},
|
||||||
|
"managed:users:1:permissions": {
|
||||||
|
"dashboards:uid:1": "dashboards:edit",
|
||||||
|
"dashboards:uid:2": "dashboards:view",
|
||||||
|
"dashboards:uid:3": "dashboards:edit",
|
||||||
|
"dashboards:uid:4": "dashboards:admin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
// Remove migration, roles and permissions
|
||||||
|
_, errDeleteMig := x.Exec(`DELETE FROM migration_log WHERE migration_id = ?`, acmig.AddActionSetMigrationID)
|
||||||
|
require.NoError(t, errDeleteMig)
|
||||||
|
_, errDeleteRole := x.Exec(`DELETE FROM role`)
|
||||||
|
require.NoError(t, errDeleteRole)
|
||||||
|
_, errDeletePerms := x.Exec(`DELETE FROM permission`)
|
||||||
|
require.NoError(t, errDeletePerms)
|
||||||
|
|
||||||
|
orgID := 1
|
||||||
|
rolePerms := map[string][]rawPermission{}
|
||||||
|
for roleName, permissions := range tc.existingRolePerms {
|
||||||
|
rawPerms := []rawPermission{}
|
||||||
|
for scope, actions := range permissions {
|
||||||
|
for _, action := range actions {
|
||||||
|
rawPerms = append(rawPerms, rawPermission{Scope: scope, Action: action})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rolePerms[roleName] = rawPerms
|
||||||
|
}
|
||||||
|
perms := map[int64]map[string][]rawPermission{int64(orgID): rolePerms}
|
||||||
|
|
||||||
|
// seed DB with permissions
|
||||||
|
putTestPermissions(t, x, perms)
|
||||||
|
|
||||||
|
// Run action set migration
|
||||||
|
acmigrator := migrator.NewMigrator(x, &setting.Cfg{Logger: log.New("acmigration.test")})
|
||||||
|
acmig.AddActionSetPermissionsMigrator(acmigrator)
|
||||||
|
|
||||||
|
errRunningMig := acmigrator.Start(false, 0)
|
||||||
|
require.NoError(t, errRunningMig)
|
||||||
|
|
||||||
|
// verify got == want
|
||||||
|
for roleName, existingPerms := range tc.existingRolePerms {
|
||||||
|
// Check the role exists
|
||||||
|
role := accesscontrol.Role{}
|
||||||
|
hasRole, err := x.Table("role").Where("org_id = ? AND name = ?", orgID, roleName).Get(&role)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, hasRole, "expected role to exist", "role", roleName)
|
||||||
|
|
||||||
|
// Check permissions associated with each role
|
||||||
|
perms := []accesscontrol.Permission{}
|
||||||
|
_, err = x.Table("permission").Where("role_id = ?", role.ID).FindAndCount(&perms)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
gotRawPerms := convertToScopeActionMap(perms)
|
||||||
|
expectedPerms := getExpectedPerms(existingPerms, tc.expectedActionSets[roleName])
|
||||||
|
require.Equal(t, len(gotRawPerms), len(expectedPerms), "expected role to contain the same amount of scopes", "role", roleName)
|
||||||
|
for scope, actions := range expectedPerms {
|
||||||
|
require.ElementsMatch(t, gotRawPerms[scope], actions, "expected role to have the same permissions", "role", roleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertToScopeActionMap(perms []accesscontrol.Permission) map[string][]string {
|
||||||
|
result := map[string][]string{}
|
||||||
|
for _, perm := range perms {
|
||||||
|
if _, ok := result[perm.Scope]; !ok {
|
||||||
|
result[perm.Scope] = []string{}
|
||||||
|
}
|
||||||
|
result[perm.Scope] = append(result[perm.Scope], perm.Action)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExpectedPerms(existingPerms map[string][]string, actionSets map[string]string) map[string][]string {
|
||||||
|
for scope := range existingPerms {
|
||||||
|
if actionSet, ok := actionSets[scope]; ok {
|
||||||
|
if !slices.Contains(existingPerms[scope], actionSet) {
|
||||||
|
existingPerms[scope] = append(existingPerms[scope], actionSets[scope])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return existingPerms
|
||||||
|
}
|
@ -133,6 +133,8 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) {
|
|||||||
ualert.AddRuleMetadata(mg)
|
ualert.AddRuleMetadata(mg)
|
||||||
|
|
||||||
accesscontrol.AddOrphanedMigrations(mg)
|
accesscontrol.AddOrphanedMigrations(mg)
|
||||||
|
|
||||||
|
accesscontrol.AddActionSetPermissionsMigrator(mg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addStarMigrations(mg *Migrator) {
|
func addStarMigrations(mg *Migrator) {
|
||||||
|
Loading…
Reference in New Issue
Block a user