mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
586 lines
17 KiB
Go
586 lines
17 KiB
Go
package accesscontrol
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"xorm.io/xorm"
|
|
|
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
|
)
|
|
|
|
var dashboardPermissionTranslation = map[dashboards.PermissionType][]string{
|
|
dashboards.PERMISSION_VIEW: {
|
|
dashboards.ActionDashboardsRead,
|
|
},
|
|
dashboards.PERMISSION_EDIT: {
|
|
dashboards.ActionDashboardsRead,
|
|
dashboards.ActionDashboardsWrite,
|
|
dashboards.ActionDashboardsDelete,
|
|
},
|
|
dashboards.PERMISSION_ADMIN: {
|
|
dashboards.ActionDashboardsRead,
|
|
dashboards.ActionDashboardsWrite,
|
|
dashboards.ActionDashboardsCreate,
|
|
dashboards.ActionDashboardsDelete,
|
|
dashboards.ActionDashboardsPermissionsRead,
|
|
dashboards.ActionDashboardsPermissionsWrite,
|
|
},
|
|
}
|
|
|
|
var folderPermissionTranslation = map[dashboards.PermissionType][]string{
|
|
dashboards.PERMISSION_VIEW: append(dashboardPermissionTranslation[dashboards.PERMISSION_VIEW], []string{
|
|
dashboards.ActionFoldersRead,
|
|
}...),
|
|
dashboards.PERMISSION_EDIT: append(dashboardPermissionTranslation[dashboards.PERMISSION_EDIT], []string{
|
|
dashboards.ActionDashboardsCreate,
|
|
dashboards.ActionFoldersRead,
|
|
dashboards.ActionFoldersWrite,
|
|
dashboards.ActionFoldersDelete,
|
|
}...),
|
|
dashboards.PERMISSION_ADMIN: append(dashboardPermissionTranslation[dashboards.PERMISSION_ADMIN], []string{
|
|
dashboards.ActionFoldersRead,
|
|
dashboards.ActionFoldersWrite,
|
|
dashboards.ActionFoldersDelete,
|
|
dashboards.ActionFoldersPermissionsRead,
|
|
dashboards.ActionFoldersPermissionsWrite,
|
|
}...),
|
|
}
|
|
|
|
func AddDashboardPermissionsMigrator(mg *migrator.Migrator) {
|
|
mg.AddMigration("dashboard permissions", &dashboardPermissionsMigrator{})
|
|
mg.AddMigration("dashboard permissions uid scopes", &dashboardUidPermissionMigrator{})
|
|
mg.AddMigration("drop managed folder create actions", &managedFolderCreateAction{})
|
|
}
|
|
|
|
var _ migrator.CodeMigration = new(dashboardPermissionsMigrator)
|
|
|
|
type dashboardPermissionsMigrator struct {
|
|
permissionMigrator
|
|
}
|
|
|
|
type dashboard struct {
|
|
ID int64 `xorm:"id"`
|
|
FolderID int64 `xorm:"folder_id"`
|
|
OrgID int64 `xorm:"org_id"`
|
|
IsFolder bool
|
|
HasAcl bool `xorm:"has_acl"`
|
|
}
|
|
|
|
func (m dashboardPermissionsMigrator) Exec(sess *xorm.Session, migrator *migrator.Migrator) error {
|
|
m.sess = sess
|
|
m.dialect = migrator.Dialect
|
|
|
|
var dashs []dashboard
|
|
if err := m.sess.SQL("SELECT id, is_folder, folder_id, org_id, has_acl FROM dashboard").Find(&dashs); err != nil {
|
|
return fmt.Errorf("failed to list dashboards: %w", err)
|
|
}
|
|
|
|
var acl []dashboards.DashboardACL
|
|
if err := m.sess.Find(&acl); err != nil {
|
|
return fmt.Errorf("failed to list dashboard ACL: %w", err)
|
|
}
|
|
|
|
aclMap := make(map[int64][]dashboards.DashboardACL, len(acl))
|
|
for _, p := range acl {
|
|
aclMap[p.DashboardID] = append(aclMap[p.DashboardID], p)
|
|
}
|
|
|
|
if err := m.migratePermissions(dashs, aclMap, migrator); err != nil {
|
|
return fmt.Errorf("failed to migrate permissions: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m dashboardPermissionsMigrator) migratePermissions(dashes []dashboard, aclMap map[int64][]dashboards.DashboardACL, migrator *migrator.Migrator) error {
|
|
permissionMap := map[int64]map[string][]*ac.Permission{}
|
|
for _, d := range dashes {
|
|
if d.ID == -1 {
|
|
continue
|
|
}
|
|
acls := aclMap[d.ID]
|
|
if permissionMap[d.OrgID] == nil {
|
|
permissionMap[d.OrgID] = map[string][]*ac.Permission{}
|
|
}
|
|
|
|
if (d.IsFolder || d.FolderID == 0) && len(acls) == 0 && !d.HasAcl {
|
|
permissionMap[d.OrgID]["managed:builtins:editor:permissions"] = append(
|
|
permissionMap[d.OrgID]["managed:builtins:editor:permissions"],
|
|
m.mapPermission(d.ID, dashboards.PERMISSION_EDIT, d.IsFolder)...,
|
|
)
|
|
permissionMap[d.OrgID]["managed:builtins:viewer:permissions"] = append(
|
|
permissionMap[d.OrgID]["managed:builtins:viewer:permissions"],
|
|
m.mapPermission(d.ID, dashboards.PERMISSION_VIEW, d.IsFolder)...,
|
|
)
|
|
} else {
|
|
for _, a := range deduplicateAcl(acls) {
|
|
roleName := getRoleName(a)
|
|
permissionMap[d.OrgID][roleName] = append(
|
|
permissionMap[d.OrgID][roleName],
|
|
m.mapPermission(d.ID, a.Permission, d.IsFolder)...,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
var allRoles []*ac.Role
|
|
rolesToCreate := []*ac.Role{}
|
|
for orgID, roles := range permissionMap {
|
|
for name := range roles {
|
|
role, err := m.findRole(orgID, name)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find role %s: %w", name, err)
|
|
}
|
|
if role.ID == 0 {
|
|
rolesToCreate = append(rolesToCreate, &ac.Role{OrgID: orgID, Name: name})
|
|
} else {
|
|
allRoles = append(allRoles, &role)
|
|
}
|
|
}
|
|
}
|
|
|
|
migrator.Logger.Debug(fmt.Sprintf("bulk-creating roles %v", rolesToCreate))
|
|
createdRoles, err := m.bulkCreateRoles(rolesToCreate)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to bulk-create roles: %w", err)
|
|
}
|
|
|
|
allRoles = append(allRoles, createdRoles...)
|
|
|
|
if err := m.bulkAssignRoles(createdRoles); err != nil {
|
|
return fmt.Errorf("failed to bulk-assign roles: %w", err)
|
|
}
|
|
|
|
return m.setPermissions(allRoles, permissionMap, migrator)
|
|
}
|
|
|
|
func (m dashboardPermissionsMigrator) setPermissions(allRoles []*ac.Role, permissionMap map[int64]map[string][]*ac.Permission, migrator *migrator.Migrator) error {
|
|
now := time.Now()
|
|
for _, role := range allRoles {
|
|
migrator.Logger.Debug(fmt.Sprintf("setting permissions for role %s with ID %d in org %d", role.Name, role.ID, role.OrgID))
|
|
if _, err := m.sess.Exec("DELETE FROM permission WHERE role_id = ? AND (action LIKE ? OR action LIKE ?)", role.ID, "dashboards%", "folders%"); err != nil {
|
|
return fmt.Errorf("failed to clear dashboard and folder permissions for role: %w", err)
|
|
}
|
|
var permissions []ac.Permission
|
|
mappedPermissions := permissionMap[role.OrgID][role.Name]
|
|
for _, p := range mappedPermissions {
|
|
permissions = append(permissions, ac.Permission{
|
|
RoleID: role.ID,
|
|
Action: p.Action,
|
|
Scope: p.Scope,
|
|
Updated: now,
|
|
Created: now,
|
|
})
|
|
}
|
|
|
|
err := batch(len(permissions), batchSize, func(start, end int) error {
|
|
migrator.Logger.Debug(fmt.Sprintf("inserting permissions %v", permissions[start:end]))
|
|
if _, err := m.sess.InsertMulti(permissions[start:end]); err != nil {
|
|
return fmt.Errorf("failed to create permissions for role: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m dashboardPermissionsMigrator) mapPermission(id int64, p dashboards.PermissionType, isFolder bool) []*ac.Permission {
|
|
if isFolder {
|
|
actions := folderPermissionTranslation[p]
|
|
scope := dashboards.ScopeFoldersProvider.GetResourceScope(strconv.FormatInt(id, 10))
|
|
permissions := make([]*ac.Permission, 0, len(actions))
|
|
for _, action := range actions {
|
|
permissions = append(permissions, &ac.Permission{Action: action, Scope: scope})
|
|
}
|
|
return permissions
|
|
}
|
|
|
|
actions := dashboardPermissionTranslation[p]
|
|
scope := ac.Scope("dashboards", "id", strconv.FormatInt(id, 10))
|
|
permissions := make([]*ac.Permission, 0, len(actions))
|
|
for _, action := range actions {
|
|
permissions = append(permissions, &ac.Permission{Action: action, Scope: scope})
|
|
}
|
|
return permissions
|
|
}
|
|
|
|
func getRoleName(p dashboards.DashboardACL) string {
|
|
if p.UserID != 0 {
|
|
return fmt.Sprintf("managed:users:%d:permissions", p.UserID)
|
|
}
|
|
if p.TeamID != 0 {
|
|
return fmt.Sprintf("managed:teams:%d:permissions", p.TeamID)
|
|
}
|
|
return fmt.Sprintf("managed:builtins:%s:permissions", strings.ToLower(string(*p.Role)))
|
|
}
|
|
|
|
func deduplicateAcl(acl []dashboards.DashboardACL) []dashboards.DashboardACL {
|
|
output := make([]dashboards.DashboardACL, 0, len(acl))
|
|
uniqueACL := map[string]dashboards.DashboardACL{}
|
|
for _, item := range acl {
|
|
// acl items with userID or teamID is enforced to be unique by sql constraint, so we can skip those
|
|
if item.UserID > 0 || item.TeamID > 0 {
|
|
output = append(output, item)
|
|
continue
|
|
}
|
|
|
|
// better to make sure so we don't panic
|
|
if item.Role == nil {
|
|
continue
|
|
}
|
|
|
|
current, ok := uniqueACL[string(*item.Role)]
|
|
if !ok {
|
|
uniqueACL[string(*item.Role)] = item
|
|
continue
|
|
}
|
|
|
|
if current.Permission < item.Permission {
|
|
uniqueACL[string(*item.Role)] = item
|
|
}
|
|
}
|
|
|
|
for _, item := range uniqueACL {
|
|
output = append(output, item)
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
var _ migrator.CodeMigration = new(dashboardUidPermissionMigrator)
|
|
|
|
type dashboardUidPermissionMigrator struct {
|
|
migrator.MigrationBase
|
|
}
|
|
|
|
func (d *dashboardUidPermissionMigrator) SQL(dialect migrator.Dialect) string {
|
|
return "code migration"
|
|
}
|
|
|
|
func (d *dashboardUidPermissionMigrator) Exec(sess *xorm.Session, migrator *migrator.Migrator) error {
|
|
if err := d.migrateWildcards(sess); err != nil {
|
|
return err
|
|
}
|
|
return d.migrateIdScopes(sess)
|
|
}
|
|
|
|
func (d *dashboardUidPermissionMigrator) migrateWildcards(sess *xorm.Session) error {
|
|
if _, err := sess.Exec("DELETE FROM permission WHERE action = 'dashboards:create' AND scope LIKE 'dashboards%'"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := sess.Exec("UPDATE permission SET scope = 'dashboards:uid:*' WHERE scope = 'dashboards:id:*'"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := sess.Exec("UPDATE permission SET scope = 'folders:uid:*' WHERE scope = 'folders:id:*'"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *dashboardUidPermissionMigrator) migrateIdScopes(sess *xorm.Session) error {
|
|
type dashboard struct {
|
|
ID int64 `xorm:"id"`
|
|
UID string `xorm:"uid"`
|
|
IsFolder bool
|
|
}
|
|
var dashboards []dashboard
|
|
if err := sess.SQL("SELECT id, uid, is_folder FROM dashboard").Find(&dashboards); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, d := range dashboards {
|
|
var idScope string
|
|
var uidScope string
|
|
|
|
if d.IsFolder {
|
|
idScope = ac.Scope("folders", "id", strconv.FormatInt(d.ID, 10))
|
|
uidScope = ac.Scope("folders", "uid", d.UID)
|
|
} else {
|
|
idScope = ac.Scope("dashboards", "id", strconv.FormatInt(d.ID, 10))
|
|
uidScope = ac.Scope("dashboards", "uid", d.UID)
|
|
}
|
|
|
|
if _, err := sess.Exec("UPDATE permission SET scope = ? WHERE scope = ?", uidScope, idScope); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type managedFolderCreateAction struct {
|
|
migrator.MigrationBase
|
|
}
|
|
|
|
func (m *managedFolderCreateAction) SQL(dialect migrator.Dialect) string {
|
|
return CodeMigrationSQL
|
|
}
|
|
|
|
func (m *managedFolderCreateAction) Exec(sess *xorm.Session, migrator *migrator.Migrator) error {
|
|
if _, err := sess.Exec("DELETE FROM permission WHERE action = 'folders:create' AND scope LIKE 'folders:uid:%'"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const managedFolderAlertActionsMigratorID = "managed folder permissions alert actions migration"
|
|
|
|
func AddManagedFolderAlertActionsMigration(mg *migrator.Migrator) {
|
|
mg.AddMigration(managedFolderAlertActionsMigratorID, &managedFolderAlertActionsMigrator{})
|
|
}
|
|
|
|
type managedFolderAlertActionsMigrator struct {
|
|
migrator.MigrationBase
|
|
}
|
|
|
|
func (m *managedFolderAlertActionsMigrator) SQL(dialect migrator.Dialect) string {
|
|
return CodeMigrationSQL
|
|
}
|
|
|
|
func (m *managedFolderAlertActionsMigrator) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
|
var ids []any
|
|
if err := sess.SQL("SELECT id FROM role WHERE name LIKE 'managed:%'").Find(&ids); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var permissions []ac.Permission
|
|
if err := sess.SQL("SELECT role_id, action, scope FROM permission WHERE role_id IN(?"+strings.Repeat(" ,?", len(ids)-1)+") AND scope LIKE 'folders:%'", ids...).Find(&permissions); err != nil {
|
|
return err
|
|
}
|
|
|
|
mapped := make(map[int64]map[string][]ac.Permission, len(ids)-1)
|
|
for _, p := range permissions {
|
|
if mapped[p.RoleID] == nil {
|
|
mapped[p.RoleID] = make(map[string][]ac.Permission)
|
|
}
|
|
mapped[p.RoleID][p.Scope] = append(mapped[p.RoleID][p.Scope], p)
|
|
}
|
|
|
|
var toAdd []ac.Permission
|
|
now := time.Now()
|
|
|
|
for id, a := range mapped {
|
|
for scope, p := range a {
|
|
if hasFolderView(p) {
|
|
toAdd = append(toAdd, ac.Permission{
|
|
RoleID: id,
|
|
Updated: now,
|
|
Created: now,
|
|
Scope: scope,
|
|
Action: ac.ActionAlertingRuleRead,
|
|
})
|
|
}
|
|
|
|
if hasFolderAdmin(p) || hasFolderEdit(p) {
|
|
toAdd = append(
|
|
toAdd,
|
|
ac.Permission{
|
|
RoleID: id,
|
|
Updated: now,
|
|
Created: now,
|
|
Scope: scope,
|
|
Action: ac.ActionAlertingRuleCreate,
|
|
},
|
|
ac.Permission{
|
|
RoleID: id,
|
|
Updated: now,
|
|
Created: now,
|
|
Scope: scope,
|
|
Action: ac.ActionAlertingRuleDelete,
|
|
},
|
|
ac.Permission{
|
|
RoleID: id,
|
|
Updated: now,
|
|
Created: now,
|
|
Scope: scope,
|
|
Action: ac.ActionAlertingRuleUpdate,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(toAdd) == 0 {
|
|
return nil
|
|
}
|
|
|
|
err := batch(len(toAdd), batchSize, func(start, end int) error {
|
|
if _, err := sess.InsertMulti(toAdd[start:end]); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const managedFolderAlertActionsRepeatMigratorID = "managed folder permissions alert actions repeated migration"
|
|
|
|
/*
|
|
AddManagedFolderAlertActionsMigration has to be run after AddDashboardPermissionsMigrator, as it is only effective if dashboard permissions have already been migrated.
|
|
AddManagedFolderAlertActionsRepeatMigrator ensures that alerting permissions that have already been added won't get added twice.
|
|
*/
|
|
func AddManagedFolderAlertActionsRepeatMigration(mg *migrator.Migrator) {
|
|
mg.AddMigration(managedFolderAlertActionsRepeatMigratorID, &managedFolderAlertActionsRepeatMigrator{})
|
|
}
|
|
|
|
const managedFolderAlertActionsRepeatMigratorFixedID = "managed folder permissions alert actions repeated fixed migration"
|
|
|
|
/*
|
|
AddManagedFolderAlertActionsRepeatFixedMigration is a fixed version of AddManagedFolderAlertActionsRepeatMigration.
|
|
*/
|
|
func AddManagedFolderAlertActionsRepeatFixedMigration(mg *migrator.Migrator) {
|
|
mg.AddMigration(managedFolderAlertActionsRepeatMigratorFixedID, &managedFolderAlertActionsRepeatMigrator{})
|
|
}
|
|
|
|
type managedFolderAlertActionsRepeatMigrator struct {
|
|
migrator.MigrationBase
|
|
}
|
|
|
|
func (m *managedFolderAlertActionsRepeatMigrator) SQL(dialect migrator.Dialect) string {
|
|
return CodeMigrationSQL
|
|
}
|
|
|
|
func (m *managedFolderAlertActionsRepeatMigrator) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
|
var ids []any
|
|
if err := sess.SQL("SELECT id FROM role WHERE name LIKE 'managed:%'").Find(&ids); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var permissions []ac.Permission
|
|
if err := sess.SQL("SELECT role_id, action, scope FROM permission WHERE role_id IN(?"+strings.Repeat(" ,?", len(ids)-1)+") AND scope LIKE 'folders:%'", ids...).Find(&permissions); err != nil {
|
|
return err
|
|
}
|
|
|
|
mapped := make(map[int64]map[string][]ac.Permission, len(ids)-1)
|
|
for _, p := range permissions {
|
|
if mapped[p.RoleID] == nil {
|
|
mapped[p.RoleID] = make(map[string][]ac.Permission)
|
|
}
|
|
mapped[p.RoleID][p.Scope] = append(mapped[p.RoleID][p.Scope], p)
|
|
}
|
|
|
|
var toAdd []ac.Permission
|
|
now := time.Now()
|
|
|
|
for id, a := range mapped {
|
|
for scope, p := range a {
|
|
// previous migration added this permission, but it was not added to the toAdd slice
|
|
// because we were checking all permissions on top of folders, not just the scoped ones
|
|
//
|
|
// what we had:
|
|
// if !hasAction(ac.<Action>, permissions) {
|
|
// should have been:
|
|
// if !hasAction(ac.<Action>, p) {
|
|
//
|
|
// see PR for explanation: https://github.com/grafana/grafana/pull/58054
|
|
if hasFolderView(p) {
|
|
if !hasAction(ac.ActionAlertingRuleRead, p) {
|
|
toAdd = append(toAdd, ac.Permission{
|
|
RoleID: id,
|
|
Updated: now,
|
|
Created: now,
|
|
Scope: scope,
|
|
Action: ac.ActionAlertingRuleRead,
|
|
})
|
|
}
|
|
}
|
|
|
|
if hasFolderAdmin(p) || hasFolderEdit(p) {
|
|
if !hasAction(ac.ActionAlertingRuleCreate, p) {
|
|
toAdd = append(toAdd, ac.Permission{
|
|
RoleID: id,
|
|
Updated: now,
|
|
Created: now,
|
|
Scope: scope,
|
|
Action: ac.ActionAlertingRuleCreate,
|
|
})
|
|
}
|
|
if !hasAction(ac.ActionAlertingRuleDelete, p) {
|
|
toAdd = append(toAdd, ac.Permission{
|
|
RoleID: id,
|
|
Updated: now,
|
|
Created: now,
|
|
Scope: scope,
|
|
Action: ac.ActionAlertingRuleDelete,
|
|
})
|
|
}
|
|
if !hasAction(ac.ActionAlertingRuleUpdate, p) {
|
|
toAdd = append(toAdd, ac.Permission{
|
|
RoleID: id,
|
|
Updated: now,
|
|
Created: now,
|
|
Scope: scope,
|
|
Action: ac.ActionAlertingRuleUpdate,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(toAdd) == 0 {
|
|
return nil
|
|
}
|
|
|
|
err := batch(len(toAdd), batchSize, func(start, end int) error {
|
|
if _, err := sess.InsertMulti(toAdd[start:end]); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func hasFolderAdmin(permissions []ac.Permission) bool {
|
|
return hasActions(folderPermissionTranslation[dashboards.PERMISSION_ADMIN], permissions)
|
|
}
|
|
|
|
func hasFolderEdit(permissions []ac.Permission) bool {
|
|
return hasActions(folderPermissionTranslation[dashboards.PERMISSION_EDIT], permissions)
|
|
}
|
|
|
|
func hasFolderView(permissions []ac.Permission) bool {
|
|
return hasActions(folderPermissionTranslation[dashboards.PERMISSION_VIEW], permissions)
|
|
}
|
|
|
|
func hasActions(actions []string, permissions []ac.Permission) bool {
|
|
var contains int
|
|
for _, action := range actions {
|
|
for _, p := range permissions {
|
|
if action == p.Action {
|
|
contains++
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return contains >= len(actions)
|
|
}
|
|
|
|
func hasAction(action string, permissions []ac.Permission) bool {
|
|
return hasActions([]string{action}, permissions)
|
|
}
|