mirror of
https://github.com/grafana/grafana.git
synced 2024-11-27 03:11:01 -06:00
Alerting: Handle custom dashboard permissions in migration service (#74504)
* Fix migration of custom dashboard permissions Dashboard alert permissions were determined by both its dashboard and folder scoped permissions, while UA alert rules only have folder scoped permissions. This means, when migrating an alert, we'll need to decide if the parent folder is a correct location for the newly created alert rule so that users, teams, and org roles have the same access to it as they did in legacy. To do this, we translate both the folder and dashboard resource permissions to two sets of SetResourcePermissionCommands. Each of these encapsulates a mapping of all: OrgRoles -> Viewer/Editor/Admin Teams -> Viewer/Editor/Admin Users -> Viewer/Editor/Admin When the dashboard permissions (including those inherited from the parent folder) differ from the parent folder permissions alone, we need to create a new folder to represent the access-level of the legacy dashboard. Compromises: When determining the SetResourcePermissionCommands we only take into account managed and basic roles. Fixed and custom roles introduce significant complexity and synchronicity hurdles. Instead, we log a warning they had the potential to override the newly created folder permissions. Also, we don't attempt to reconcile datasource permissions that were not necessary in legacy alerting. Users without access to the necessary datasources to edit an alert rule will need to obtain said access separate from the migration.
This commit is contained in:
parent
372082d254
commit
5f48619c9a
@ -9,6 +9,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
|
||||
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
@ -40,14 +41,14 @@ func addMigrationInfo(da *migrationStore.DashAlert, dashboardUID string) (map[st
|
||||
}
|
||||
|
||||
// MigrateAlert migrates a single dashboard alert from legacy alerting to unified alerting.
|
||||
func (om *OrgMigration) migrateAlert(ctx context.Context, l log.Logger, da *migrationStore.DashAlert, dashboardUID string, folderUID string) (*ngmodels.AlertRule, error) {
|
||||
func (om *OrgMigration) migrateAlert(ctx context.Context, l log.Logger, da *migrationStore.DashAlert, info migmodels.DashboardUpgradeInfo) (*ngmodels.AlertRule, error) {
|
||||
l.Debug("Migrating alert rule to Unified Alerting")
|
||||
cond, err := transConditions(ctx, da, om.migrationStore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("transform conditions: %w", err)
|
||||
}
|
||||
|
||||
lbls, annotations := addMigrationInfo(da, dashboardUID)
|
||||
lbls, annotations := addMigrationInfo(da, info.DashboardUID)
|
||||
|
||||
message := MigrateTmpl(l.New("field", "message"), da.Message)
|
||||
annotations["message"] = message
|
||||
@ -63,15 +64,29 @@ func (om *OrgMigration) migrateAlert(ctx context.Context, l log.Logger, da *migr
|
||||
}
|
||||
|
||||
// Here we ensure that the alert rule title is unique within the folder.
|
||||
dedupSet := om.AlertTitleDeduplicator(folderUID)
|
||||
name := truncateRuleName(da.Name)
|
||||
if dedupSet.contains(name) {
|
||||
dedupedName := dedupSet.deduplicate(name)
|
||||
l.Debug("Duplicate alert rule name detected, renaming", "old_name", name, "new_name", dedupedName)
|
||||
titleDedupSet := om.AlertTitleDeduplicator(info.NewFolderUID)
|
||||
name := truncate(da.Name, store.AlertDefinitionMaxTitleLength)
|
||||
if titleDedupSet.contains(name) {
|
||||
dedupedName := titleDedupSet.deduplicate(name)
|
||||
l.Debug("Duplicate alert rule name detected, renaming", "oldName", name, "newName", dedupedName)
|
||||
name = dedupedName
|
||||
}
|
||||
dedupSet.add(name)
|
||||
titleDedupSet.add(name)
|
||||
|
||||
// Here we ensure that the alert rule group is unique within the folder.
|
||||
// This is so that we don't have to ensure that the alerts rules have the same interval.
|
||||
groupDedupSet := om.AlertTitleDeduplicator(info.NewFolderUID)
|
||||
panelSuffix := fmt.Sprintf(" - %d", da.PanelID)
|
||||
truncatedDashboard := truncate(info.DashboardName, store.AlertRuleMaxRuleGroupNameLength-len(panelSuffix))
|
||||
groupName := fmt.Sprintf("%s%s", truncatedDashboard, panelSuffix) // Unique to this dash alert but still contains useful info.
|
||||
if groupDedupSet.contains(groupName) {
|
||||
dedupedGroupName := groupDedupSet.deduplicate(groupName)
|
||||
l.Debug("Duplicate alert rule group name detected, renaming", "oldGroup", groupName, "newGroup", dedupedGroupName)
|
||||
groupName = dedupedGroupName
|
||||
}
|
||||
groupDedupSet.add(groupName)
|
||||
|
||||
dashUID := info.DashboardUID
|
||||
ar := &ngmodels.AlertRule{
|
||||
OrgID: da.OrgID,
|
||||
Title: name,
|
||||
@ -80,10 +95,10 @@ func (om *OrgMigration) migrateAlert(ctx context.Context, l log.Logger, da *migr
|
||||
Data: data,
|
||||
IntervalSeconds: ruleAdjustInterval(da.Frequency),
|
||||
Version: 1,
|
||||
NamespaceUID: folderUID, // Folder already created, comes from env var.
|
||||
DashboardUID: &dashboardUID,
|
||||
NamespaceUID: info.NewFolderUID,
|
||||
DashboardUID: &dashUID,
|
||||
PanelID: &da.PanelID,
|
||||
RuleGroup: name,
|
||||
RuleGroup: groupName,
|
||||
For: da.For,
|
||||
Updated: time.Now().UTC(),
|
||||
Annotations: annotations,
|
||||
@ -264,10 +279,10 @@ func transExecErr(l log.Logger, s string) ngmodels.ExecutionErrorState {
|
||||
}
|
||||
}
|
||||
|
||||
// truncateRuleName truncates the rule name to the maximum allowed length.
|
||||
func truncateRuleName(daName string) string {
|
||||
if len(daName) > store.AlertDefinitionMaxTitleLength {
|
||||
return daName[:store.AlertDefinitionMaxTitleLength]
|
||||
// truncate truncates the given name to the maximum allowed length.
|
||||
func truncate(daName string, length int) string {
|
||||
if len(daName) > length {
|
||||
return daName[:length]
|
||||
}
|
||||
return daName
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -12,6 +13,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
||||
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
|
||||
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
@ -127,17 +129,22 @@ func TestAddMigrationInfo(t *testing.T) {
|
||||
|
||||
func TestMakeAlertRule(t *testing.T) {
|
||||
sqlStore := db.InitTestDB(t)
|
||||
info := migmodels.DashboardUpgradeInfo{
|
||||
DashboardUID: "dashboarduid",
|
||||
DashboardName: "dashboardname",
|
||||
NewFolderUID: "ewfolderuid",
|
||||
NewFolderName: "newfoldername",
|
||||
}
|
||||
t.Run("when mapping rule names", func(t *testing.T) {
|
||||
t.Run("leaves basic names untouched", func(t *testing.T) {
|
||||
service := NewTestMigrationService(t, sqlStore, nil)
|
||||
m := service.newOrgMigration(1)
|
||||
da := createTestDashAlert()
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, "dashboard", "folder")
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, da.Name, ar.Title)
|
||||
require.Equal(t, ar.Title, ar.RuleGroup)
|
||||
})
|
||||
|
||||
t.Run("truncates very long names to max length", func(t *testing.T) {
|
||||
@ -146,7 +153,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
da := createTestDashAlert()
|
||||
da.Name = strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1)
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, "dashboard", "folder")
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ar.Title, store.AlertDefinitionMaxTitleLength)
|
||||
@ -158,7 +165,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
da := createTestDashAlert()
|
||||
da.Name = strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1)
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, "dashboard", "folder")
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ar.Title, store.AlertDefinitionMaxTitleLength)
|
||||
@ -166,7 +173,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
da = createTestDashAlert()
|
||||
da.Name = strings.Repeat("a", store.AlertDefinitionMaxTitleLength+1)
|
||||
|
||||
ar, err = m.migrateAlert(context.Background(), &logtest.Fake{}, &da, "dashboard", "folder")
|
||||
ar, err = m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ar.Title, store.AlertDefinitionMaxTitleLength)
|
||||
@ -174,7 +181,6 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
require.Len(t, parts, 2)
|
||||
require.Greater(t, len(parts[1]), 8, "unique identifier should be longer than 9 characters")
|
||||
require.Equal(t, store.AlertDefinitionMaxTitleLength-1, len(parts[0])+len(parts[1]), "truncated name + underscore + unique identifier should together be DefaultFieldMaxLength")
|
||||
require.Equal(t, ar.Title, ar.RuleGroup)
|
||||
})
|
||||
})
|
||||
|
||||
@ -183,7 +189,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
m := service.newOrgMigration(1)
|
||||
da := createTestDashAlert()
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, "dashboard", "folder")
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
require.NoError(t, err)
|
||||
require.False(t, ar.IsPaused)
|
||||
})
|
||||
@ -194,7 +200,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
da := createTestDashAlert()
|
||||
da.State = "paused"
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, "dashboard", "folder")
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ar.IsPaused)
|
||||
})
|
||||
@ -205,7 +211,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
da := createTestDashAlert()
|
||||
da.ParsedSettings.NoDataState = uuid.NewString()
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, "dashboard", "folder")
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, models.NoData, ar.NoDataState)
|
||||
})
|
||||
@ -216,7 +222,7 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
da := createTestDashAlert()
|
||||
da.ParsedSettings.ExecutionErrorState = uuid.NewString()
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, "dashboard", "folder")
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, models.ErrorErrState, ar.ExecErrState)
|
||||
})
|
||||
@ -227,13 +233,78 @@ func TestMakeAlertRule(t *testing.T) {
|
||||
da := createTestDashAlert()
|
||||
da.Message = "Instance ${instance} is down"
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, "dashboard", "folder")
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
require.Nil(t, err)
|
||||
expected :=
|
||||
"{{- $mergedLabels := mergeLabelValues $values -}}\n" +
|
||||
"Instance {{$mergedLabels.instance}} is down"
|
||||
require.Equal(t, expected, ar.Annotations["message"])
|
||||
})
|
||||
|
||||
t.Run("create unique group from dashboard title and panel", func(t *testing.T) {
|
||||
service := NewTestMigrationService(t, sqlStore, nil)
|
||||
m := service.newOrgMigration(1)
|
||||
da := createTestDashAlert()
|
||||
da.PanelID = 42
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("%s - %d", info.DashboardName, da.PanelID), ar.RuleGroup)
|
||||
})
|
||||
|
||||
t.Run("truncate rule group if dashboard name + panel id is too long", func(t *testing.T) {
|
||||
service := NewTestMigrationService(t, sqlStore, nil)
|
||||
m := service.newOrgMigration(1)
|
||||
da := createTestDashAlert()
|
||||
da.PanelID = 42
|
||||
info := migmodels.DashboardUpgradeInfo{
|
||||
DashboardUID: "dashboarduid",
|
||||
DashboardName: strings.Repeat("a", store.AlertRuleMaxRuleGroupNameLength-1),
|
||||
NewFolderUID: "newfolderuid",
|
||||
NewFolderName: "newfoldername",
|
||||
}
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ar.RuleGroup, store.AlertRuleMaxRuleGroupNameLength)
|
||||
require.Equal(t, fmt.Sprintf("%s - %d", strings.Repeat("a", store.AlertRuleMaxRuleGroupNameLength-5), da.PanelID), ar.RuleGroup)
|
||||
})
|
||||
|
||||
t.Run("deduplicate rule group name if truncation is not unique", func(t *testing.T) {
|
||||
service := NewTestMigrationService(t, sqlStore, nil)
|
||||
m := service.newOrgMigration(1)
|
||||
da := createTestDashAlert()
|
||||
da.PanelID = 42
|
||||
info := migmodels.DashboardUpgradeInfo{
|
||||
DashboardUID: "dashboarduid",
|
||||
DashboardName: strings.Repeat("a", store.AlertRuleMaxRuleGroupNameLength-1),
|
||||
NewFolderUID: "newfolderuid",
|
||||
NewFolderName: "newfoldername",
|
||||
}
|
||||
|
||||
_, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
require.NoError(t, err)
|
||||
|
||||
da = createTestDashAlert()
|
||||
da.PanelID = 42
|
||||
info = migmodels.DashboardUpgradeInfo{
|
||||
DashboardUID: "dashboarduid",
|
||||
DashboardName: strings.Repeat("a", store.AlertRuleMaxRuleGroupNameLength-1),
|
||||
NewFolderUID: "newfolderuid",
|
||||
NewFolderName: "newfoldername",
|
||||
}
|
||||
|
||||
ar, err := m.migrateAlert(context.Background(), &logtest.Fake{}, &da, info)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ar.RuleGroup, store.AlertRuleMaxRuleGroupNameLength)
|
||||
parts := strings.SplitN(ar.RuleGroup, "_", 2)
|
||||
require.Len(t, parts, 2)
|
||||
require.Greater(t, len(parts[1]), 8, "unique identifier should be longer than 9 characters")
|
||||
require.Equal(t, store.AlertDefinitionMaxTitleLength-1, len(parts[0])+len(parts[1]), "truncated name + underscore + unique identifier should together be DefaultFieldMaxLength")
|
||||
})
|
||||
}
|
||||
|
||||
func createTestDashAlert() migrationStore.DashAlert {
|
||||
|
@ -697,7 +697,7 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
NamespaceUID: "folder5-1",
|
||||
DashboardUID: pointer("dash1-1"),
|
||||
PanelID: pointer(int64(1)),
|
||||
RuleGroup: "alert1",
|
||||
RuleGroup: "dash1-1",
|
||||
RuleGroupIndex: 1,
|
||||
NoDataState: ngModels.NoData,
|
||||
ExecErrState: ngModels.AlertingErrState,
|
||||
@ -713,6 +713,8 @@ func TestDashAlertQueryMigration(t *testing.T) {
|
||||
mutator(rule)
|
||||
}
|
||||
|
||||
rule.RuleGroup = fmt.Sprintf("%s - %d", *rule.DashboardUID, *rule.PanelID)
|
||||
|
||||
rule.Annotations["__dashboardUid__"] = *rule.DashboardUID
|
||||
rule.Annotations["__panelId__"] = strconv.FormatInt(*rule.PanelID, 10)
|
||||
return rule
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
pb "github.com/prometheus/alertmanager/silence/silencepb"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
|
||||
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
||||
@ -28,9 +29,12 @@ type OrgMigration struct {
|
||||
seenUIDs Deduplicator
|
||||
silences []*pb.MeshSilence
|
||||
alertRuleTitleDedup map[string]Deduplicator // Folder -> Deduplicator (Title).
|
||||
alertRuleGroupDedup map[string]Deduplicator // Folder -> Deduplicator (Group).
|
||||
|
||||
// cache for folders created for dashboards that have custom permissions
|
||||
folderCache map[string]*folder.Folder
|
||||
// Migrated folder for a dashboard based on permissions. Parent Folder ID -> unique dashboard permission -> custom folder.
|
||||
permissionsMap map[int64]map[permissionHash]*folder.Folder
|
||||
folderCache map[int64]*folder.Folder // Folder ID -> Folder.
|
||||
folderPermissionCache map[string][]accesscontrol.ResourcePermission // Folder UID -> Folder Permissions.
|
||||
generalAlertingFolder *folder.Folder
|
||||
|
||||
state *migmodels.OrgMigrationState
|
||||
@ -51,7 +55,12 @@ func (ms *MigrationService) newOrgMigration(orgID int64) *OrgMigration {
|
||||
silences: make([]*pb.MeshSilence, 0),
|
||||
alertRuleTitleDedup: make(map[string]Deduplicator),
|
||||
|
||||
folderCache: make(map[string]*folder.Folder),
|
||||
// We deduplicate alert rule groups so that we don't have to ensure that the alerts rules have the same interval.
|
||||
alertRuleGroupDedup: make(map[string]Deduplicator),
|
||||
|
||||
permissionsMap: make(map[int64]map[permissionHash]*folder.Folder),
|
||||
folderCache: make(map[int64]*folder.Folder),
|
||||
folderPermissionCache: make(map[string][]accesscontrol.ResourcePermission),
|
||||
|
||||
state: &migmodels.OrgMigrationState{
|
||||
OrgID: orgID,
|
||||
@ -71,6 +80,17 @@ func (om *OrgMigration) AlertTitleDeduplicator(folderUID string) Deduplicator {
|
||||
return om.alertRuleTitleDedup[folderUID]
|
||||
}
|
||||
|
||||
func (om *OrgMigration) AlertGroupDeduplicator(folderUID string) Deduplicator {
|
||||
if _, ok := om.alertRuleGroupDedup[folderUID]; !ok {
|
||||
om.alertRuleGroupDedup[folderUID] = Deduplicator{
|
||||
set: make(map[string]struct{}),
|
||||
caseInsensitive: om.migrationStore.CaseInsensitive(),
|
||||
maxLen: store.AlertRuleMaxRuleGroupNameLength,
|
||||
}
|
||||
}
|
||||
return om.alertRuleGroupDedup[folderUID]
|
||||
}
|
||||
|
||||
type AlertPair struct {
|
||||
AlertRule *models.AlertRule
|
||||
DashAlert *migrationStore.DashAlert
|
||||
|
@ -5,3 +5,11 @@ type OrgMigrationState struct {
|
||||
OrgID int64 `json:"orgId"`
|
||||
CreatedFolders []string `json:"createdFolders"`
|
||||
}
|
||||
|
||||
// DashboardUpgradeInfo contains information about a dashboard that was upgraded.
|
||||
type DashboardUpgradeInfo struct {
|
||||
DashboardUID string
|
||||
DashboardName string
|
||||
NewFolderUID string
|
||||
NewFolderName string
|
||||
}
|
||||
|
@ -2,8 +2,12 @@ package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@ -11,26 +15,40 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
// DASHBOARD_FOLDER is the format used to generate the folder name for migrated dashboards with custom permissions.
|
||||
const DASHBOARD_FOLDER = "%s Alerts - %s"
|
||||
|
||||
// MaxFolderName is the maximum length of the folder name generated using DASHBOARD_FOLDER format
|
||||
const MaxFolderName = 255
|
||||
|
||||
var (
|
||||
// migratorPermissions are the permissions required for the background user to migrate alerts.
|
||||
migratorPermissions = []accesscontrol.Permission{
|
||||
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
|
||||
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll},
|
||||
{Action: dashboards.ActionFoldersPermissionsRead, Scope: dashboards.ScopeFoldersAll},
|
||||
{Action: dashboards.ActionDashboardsPermissionsRead, Scope: dashboards.ScopeDashboardsAll},
|
||||
{Action: dashboards.ActionFoldersCreate},
|
||||
{Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll},
|
||||
{Action: datasources.ActionRead, Scope: datasources.ScopeAll},
|
||||
{Action: accesscontrol.ActionOrgUsersRead, Scope: accesscontrol.ScopeUsersAll},
|
||||
{Action: accesscontrol.ActionTeamsRead, Scope: accesscontrol.ScopeTeamsAll},
|
||||
}
|
||||
|
||||
// generalAlertingFolderTitle is the title of the general alerting folder. This is used for dashboard alerts in the general folder.
|
||||
generalAlertingFolderTitle = "General Alerting"
|
||||
|
||||
// permissionMap maps the "friendly" permission name for a ResourcePermissions actions to the dashboards.PermissionType.
|
||||
// A sort of reverse accesscontrol Service.MapActions similar to api.dashboardPermissionMap.
|
||||
permissionMap = map[string]dashboards.PermissionType{
|
||||
"View": dashboards.PERMISSION_VIEW,
|
||||
"Edit": dashboards.PERMISSION_EDIT,
|
||||
"Admin": dashboards.PERMISSION_ADMIN,
|
||||
}
|
||||
)
|
||||
|
||||
// getMigrationUser returns a background user for the given orgID with permissions to execute migration-related tasks.
|
||||
@ -38,106 +56,327 @@ func getMigrationUser(orgID int64) identity.Requester {
|
||||
return accesscontrol.BackgroundUser("ngalert_migration", orgID, org.RoleAdmin, migratorPermissions)
|
||||
}
|
||||
|
||||
// getAlertFolderNameFromDashboard generates a folder name for alerts that belong to a dashboard. Formats the string according to DASHBOARD_FOLDER format.
|
||||
// If the resulting string exceeds the migrations.MaxTitleLength, the dashboard title is stripped to be at the maximum length
|
||||
func getAlertFolderNameFromDashboard(dash *dashboards.Dashboard) string {
|
||||
maxLen := MaxFolderName - len(fmt.Sprintf(DASHBOARD_FOLDER, "", dash.UID))
|
||||
title := dash.Title
|
||||
func (om *OrgMigration) migratedFolder(ctx context.Context, log log.Logger, dashID int64) (*migmodels.DashboardUpgradeInfo, error) {
|
||||
dash, err := om.migrationStore.GetDashboard(ctx, om.orgID, dashID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l := log.New("dashboardTitle", dash.Title, "dashboardUid", dash.UID)
|
||||
|
||||
dashFolder, err := om.getFolder(ctx, dash)
|
||||
if err != nil {
|
||||
l.Warn("Failed to find folder for dashboard", "missing_folder_id", dash.FolderID, "error", err)
|
||||
}
|
||||
if dashFolder != nil {
|
||||
l = l.New("folderUid", dashFolder.UID, "folderName", dashFolder.Title)
|
||||
}
|
||||
|
||||
migratedFolder, err := om.getOrCreateMigratedFolder(ctx, l, dash, dashFolder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &migmodels.DashboardUpgradeInfo{
|
||||
DashboardUID: dash.UID,
|
||||
DashboardName: dash.Title,
|
||||
NewFolderUID: migratedFolder.UID,
|
||||
NewFolderName: migratedFolder.Title,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getOrCreateMigratedFolder returns the folder that alerts in a given dashboard should migrate to.
|
||||
// If the dashboard has no custom permissions, this should be the same folder as dash.FolderID.
|
||||
// If the dashboard has custom permissions that affect access, this should be a new folder with migrated permissions relating to both the dashboard and parent folder.
|
||||
// Any dashboard that has greater read/write permissions for an orgRole/team/user compared to its folder will necessitate creating a new folder with the same permissions as the dashboard.
|
||||
func (om *OrgMigration) getOrCreateMigratedFolder(ctx context.Context, l log.Logger, dash *dashboards.Dashboard, parentFolder *folder.Folder) (*folder.Folder, error) {
|
||||
// If parentFolder does not exist then the dashboard is an orphan. We migrate the alert to the general alerting folder.
|
||||
// The general alerting folder is only accessible to admins.
|
||||
if parentFolder == nil {
|
||||
l.Warn("Migrating alert to the general alerting folder: original folder not found")
|
||||
f, err := om.getOrCreateGeneralAlertingFolder(ctx, om.orgID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("general alerting folder: %w", err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Check if the dashboard has custom permissions. If it does, we need to create a new folder for it.
|
||||
// This folder will be cached for re-use for each dashboard in the folder with the same permissions.
|
||||
permissionsToFolder, ok := om.permissionsMap[parentFolder.ID]
|
||||
if !ok {
|
||||
permissionsToFolder = make(map[permissionHash]*folder.Folder)
|
||||
om.permissionsMap[dash.FolderID] = permissionsToFolder
|
||||
|
||||
folderPerms, err := om.getFolderPermissions(ctx, parentFolder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("folder permissions: %w", err)
|
||||
}
|
||||
newFolderPerms, _ := om.convertResourcePerms(folderPerms)
|
||||
|
||||
// We assign the folder to the cache so that any dashboards with identical equivalent permissions will use the parent folder instead of creating a new one.
|
||||
folderPermsHash, err := createHash(newFolderPerms)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hash of folder permissions: %w", err)
|
||||
}
|
||||
permissionsToFolder[folderPermsHash] = parentFolder
|
||||
}
|
||||
|
||||
// Now we compute the hash of the dashboard permissions and check if we have a folder for it. If not, we create a new one.
|
||||
perms, err := om.getDashboardPermissions(ctx, dash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dashboard permissions: %w", err)
|
||||
}
|
||||
newPerms, unusedPerms := om.convertResourcePerms(perms)
|
||||
hash, err := createHash(newPerms)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hash of dashboard permissions: %w", err)
|
||||
}
|
||||
|
||||
customFolder, ok := permissionsToFolder[hash]
|
||||
if !ok {
|
||||
folderName := generateAlertFolderName(parentFolder, hash)
|
||||
l.Info("Dashboard has custom permissions, create a new folder for alerts.", "newFolder", folderName)
|
||||
f, err := om.createFolder(ctx, om.orgID, folderName, newPerms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the role is not managed or basic we don't attempt to migrate its permissions. This is because
|
||||
// the logic to migrate would be complex, error-prone, and even if done correctly would have significant
|
||||
// drawbacks in the case of custom provisioned roles. Instead, we log if the role has dashboard permissions that could
|
||||
// potentially override the folder permissions. These overrides would always be to increase permissions not decrease them,
|
||||
// so the risk of giving users access to alerts they shouldn't have access to is mitigated.
|
||||
overrides := potentialOverrides(unusedPerms, newPerms)
|
||||
if len(overrides) > 0 {
|
||||
l.Warn("Some roles were not migrated but had the potential to allow additional access. Please verify the permissions of the new folder.", "roles", overrides, "newFolder", folderName)
|
||||
}
|
||||
|
||||
permissionsToFolder[hash] = f
|
||||
return f, nil
|
||||
}
|
||||
|
||||
return customFolder, nil
|
||||
}
|
||||
|
||||
// generateAlertFolderName generates a folder name for alerts that belong to a dashboard with custom permissions.
|
||||
// Formats the string according to DASHBOARD_FOLDER format.
|
||||
// If the resulting string's length exceeds migration.MaxFolderName, the dashboard title is stripped to be at the maximum length.
|
||||
func generateAlertFolderName(f *folder.Folder, hash permissionHash) string {
|
||||
maxLen := MaxFolderName - len(fmt.Sprintf(DASHBOARD_FOLDER, "", hash))
|
||||
title := f.Title
|
||||
if len(title) > maxLen {
|
||||
title = title[:maxLen]
|
||||
}
|
||||
return fmt.Sprintf(DASHBOARD_FOLDER, title, dash.UID) // include UID to the name to avoid collision
|
||||
return fmt.Sprintf(DASHBOARD_FOLDER, title, hash) // Include hash in the name to avoid collision.
|
||||
}
|
||||
|
||||
func (om *OrgMigration) getOrCreateMigratedFolder(ctx context.Context, log log.Logger, dashID int64) (*dashboards.Dashboard, *folder.Folder, error) {
|
||||
dash, err := om.migrationStore.GetDashboard(ctx, om.orgID, dashID)
|
||||
if err != nil {
|
||||
if errors.Is(err, dashboards.ErrFolderNotFound) {
|
||||
return nil, nil, fmt.Errorf("dashboard with ID %v under organisation %d not found: %w", dashID, om.orgID, err)
|
||||
}
|
||||
return nil, nil, fmt.Errorf("failed to get dashboard with ID %v under organisation %d: %w", dashID, om.orgID, err)
|
||||
}
|
||||
l := log.New(
|
||||
"dashboardTitle", dash.Title,
|
||||
"dashboardUID", dash.UID,
|
||||
)
|
||||
// isBasic returns true if the given roleName is a basic role.
|
||||
func isBasic(roleName string) bool {
|
||||
return strings.HasPrefix(roleName, accesscontrol.BasicRolePrefix)
|
||||
}
|
||||
|
||||
var migratedFolder *folder.Folder
|
||||
switch {
|
||||
case dash.HasACL:
|
||||
folderName := getAlertFolderNameFromDashboard(dash)
|
||||
f, ok := om.folderCache[folderName]
|
||||
if !ok {
|
||||
l.Info("create a new folder for alerts that belongs to dashboard because it has custom permissions", "folder", folderName)
|
||||
// create folder and assign the permissions of the dashboard (included default and inherited)
|
||||
f, err = om.createFolder(ctx, om.orgID, folderName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("create new folder: %w", err)
|
||||
}
|
||||
permissions, err := om.migrationStore.GetACL(ctx, dash.OrgID, dash.ID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get dashboard %d under organisation %d permissions: %w", dash.ID, dash.OrgID, err)
|
||||
}
|
||||
err = om.migrationStore.SetACL(ctx, f.OrgID, f.ID, permissions)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to set folder %d under organisation %d permissions: %w", f.ID, f.OrgID, err)
|
||||
}
|
||||
om.folderCache[folderName] = f
|
||||
}
|
||||
migratedFolder = f
|
||||
case dash.FolderID > 0:
|
||||
// get folder if exists
|
||||
f, err := om.migrationStore.GetFolder(ctx, &folder.GetFolderQuery{ID: &dash.FolderID, OrgID: dash.OrgID, SignedInUser: getMigrationUser(dash.OrgID)})
|
||||
if err != nil {
|
||||
// If folder does not exist then the dashboard is an orphan and we migrate the alert to the general folder.
|
||||
l.Warn("Failed to find folder for dashboard. Migrate rule to the default folder", "missing_folder_id", dash.FolderID, "error", err)
|
||||
migratedFolder, err = om.getOrCreateGeneralFolder(ctx, dash.OrgID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
// convertResourcePerms converts the given resource permissions (from a dashboard or folder) to a set of unique, sorted SetResourcePermissionCommands.
|
||||
// This is done by iterating over the managed, basic, and inherited resource permissions and adding the highest permission for each orgrole/user/team.
|
||||
//
|
||||
// # Details
|
||||
//
|
||||
// There are two role types that we consider:
|
||||
// - managed (ex. managed:users:1:permissions, managed:builtins:editor:permissions, managed:teams:1:permissions):
|
||||
// These are the only roles that exist in OSS. For each of these roles, we add the actions of the highest
|
||||
// dashboards.PermissionType between the folder and the dashboard. Permissions from the folder are inherited.
|
||||
// The added actions should have scope=folder:uid:xxxxxx, where xxxxxx is the new folder uid.
|
||||
// - basic (ex. basic:admin, basic:editor):
|
||||
// These are roles used in enterprise. Every user should have one of these roles. They should be considered
|
||||
// equivalent to managed:builtins. The highest dashboards.PermissionType between the two should be used.
|
||||
//
|
||||
// There are two role types that we do not consider:
|
||||
// - fixed: (ex. fixed:dashboards:reader, fixed:dashboards:writer):
|
||||
// These are roles with fixed actions/scopes. They should not be given any extra actions/scopes because they
|
||||
// can be overwritten. Because of this, to ensure that all users with this role have the correct access to the
|
||||
// new folder we would need to find all users with this role and add a permission for
|
||||
// action folders:read/write -> folder:uid:xxxxxx to their managed:users:X:permissions.
|
||||
// This will eventually fall out of sync.
|
||||
// - custom: Custom roles created via API or provisioning.
|
||||
// Similar to fixed roles, we can't give them any extra actions/scopes because they can be overwritten.
|
||||
//
|
||||
// For now, we choose the simpler approach of handling managed and basic roles. Fixed and custom roles will not
|
||||
// be taken into account, but we will log a warning if they had the potential to override the folder permissions.
|
||||
func (om *OrgMigration) convertResourcePerms(rperms []accesscontrol.ResourcePermission) ([]accesscontrol.SetResourcePermissionCommand, []accesscontrol.ResourcePermission) {
|
||||
keep := make(map[accesscontrol.SetResourcePermissionCommand]dashboards.PermissionType)
|
||||
unusedPerms := make([]accesscontrol.ResourcePermission, 0)
|
||||
for _, p := range rperms {
|
||||
if p.IsManaged || p.IsInherited || isBasic(p.RoleName) {
|
||||
if permission := om.migrationStore.MapActions(p); permission != "" {
|
||||
sp := accesscontrol.SetResourcePermissionCommand{
|
||||
UserID: p.UserId,
|
||||
TeamID: p.TeamId,
|
||||
BuiltinRole: p.BuiltInRole,
|
||||
}
|
||||
// We could have redundant perms, ex: if one is inherited from the parent folder, or we have basic roles from enterprise.
|
||||
// We use the highest permission available.
|
||||
pType := permissionMap[permission]
|
||||
current, ok := keep[sp]
|
||||
if !ok || pType > current {
|
||||
keep[sp] = pType
|
||||
}
|
||||
}
|
||||
} else {
|
||||
migratedFolder = f
|
||||
}
|
||||
default:
|
||||
migratedFolder, err = om.getOrCreateGeneralFolder(ctx, dash.OrgID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
// Keep track of unused perms, so we can later log a warning if they had the potential to override the folder permissions.
|
||||
unusedPerms = append(unusedPerms, p)
|
||||
}
|
||||
}
|
||||
|
||||
if migratedFolder.UID == "" {
|
||||
return nil, nil, fmt.Errorf("empty folder identifier")
|
||||
permissions := make([]accesscontrol.SetResourcePermissionCommand, 0, len(keep))
|
||||
for p, pType := range keep {
|
||||
p.Permission = pType.String()
|
||||
permissions = append(permissions, p)
|
||||
}
|
||||
|
||||
return dash, migratedFolder, nil
|
||||
// Stable sort since we will be creating a hash of this to compare dashboard perms to folder perms.
|
||||
sort.SliceStable(permissions, func(i, j int) bool {
|
||||
if permissions[i].BuiltinRole != permissions[j].BuiltinRole {
|
||||
return permissions[i].BuiltinRole < permissions[j].BuiltinRole
|
||||
}
|
||||
if permissions[i].UserID != permissions[j].UserID {
|
||||
return permissions[i].UserID < permissions[j].UserID
|
||||
}
|
||||
if permissions[i].TeamID != permissions[j].TeamID {
|
||||
return permissions[i].TeamID < permissions[j].TeamID
|
||||
}
|
||||
return permissions[i].Permission < permissions[j].Permission
|
||||
})
|
||||
|
||||
return permissions, unusedPerms
|
||||
}
|
||||
|
||||
// getOrCreateGeneralFolder returns the general folder under the specific organisation
|
||||
// If the general folder does not exist it creates it.
|
||||
func (om *OrgMigration) getOrCreateGeneralFolder(ctx context.Context, orgID int64) (*folder.Folder, error) {
|
||||
// potentialOverrides returns a map of roles from unusedOldPerms that have dashboard permissions that could potentially
|
||||
// override the given folder permissions in newPerms. These overrides are always to increase permissions not decrease them.
|
||||
func potentialOverrides(unusedOldPerms []accesscontrol.ResourcePermission, newPerms []accesscontrol.SetResourcePermissionCommand) map[string]dashboards.PermissionType {
|
||||
var lowestPermission dashboards.PermissionType
|
||||
for _, p := range newPerms {
|
||||
if p.BuiltinRole == string(org.RoleEditor) || p.BuiltinRole == string(org.RoleViewer) {
|
||||
pType := permissionMap[p.Permission]
|
||||
if pType < lowestPermission {
|
||||
lowestPermission = pType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nonManagedPermissionTypes := make(map[string]dashboards.PermissionType)
|
||||
for _, p := range unusedOldPerms {
|
||||
existing, ok := nonManagedPermissionTypes[p.RoleName]
|
||||
if ok && existing == dashboards.PERMISSION_EDIT {
|
||||
// We've already handled the highest permission we care about, no need to check this role anymore.
|
||||
continue
|
||||
}
|
||||
|
||||
if p.Contains([]string{dashboards.ActionDashboardsWrite}) {
|
||||
existing = dashboards.PERMISSION_EDIT
|
||||
} else if p.Contains([]string{dashboards.ActionDashboardsRead}) {
|
||||
existing = dashboards.PERMISSION_VIEW
|
||||
}
|
||||
|
||||
if existing > lowestPermission && existing > nonManagedPermissionTypes[p.RoleName] {
|
||||
nonManagedPermissionTypes[p.RoleName] = existing
|
||||
}
|
||||
}
|
||||
|
||||
return nonManagedPermissionTypes
|
||||
}
|
||||
|
||||
type permissionHash string
|
||||
|
||||
// createHash returns a hash of the given permissions.
|
||||
func createHash(setResourcePermissionCommands []accesscontrol.SetResourcePermissionCommand) (permissionHash, error) {
|
||||
// Speed is not particularly important here.
|
||||
digester := crypto.MD5.New()
|
||||
var separator = []byte{255}
|
||||
for _, perm := range setResourcePermissionCommands {
|
||||
_, err := fmt.Fprint(digester, separator)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = fmt.Fprint(digester, perm)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return permissionHash(hex.EncodeToString(digester.Sum(nil))), nil
|
||||
}
|
||||
|
||||
// getFolderPermissions Get permissions for folder.
|
||||
func (om *OrgMigration) getFolderPermissions(ctx context.Context, f *folder.Folder) ([]accesscontrol.ResourcePermission, error) {
|
||||
if p, ok := om.folderPermissionCache[f.UID]; ok {
|
||||
return p, nil
|
||||
}
|
||||
p, err := om.migrationStore.GetFolderPermissions(ctx, getMigrationUser(f.OrgID), f.UID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
om.folderPermissionCache[f.UID] = p
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// getDashboardPermissions Get permissions for dashboard.
|
||||
func (om *OrgMigration) getDashboardPermissions(ctx context.Context, d *dashboards.Dashboard) ([]accesscontrol.ResourcePermission, error) {
|
||||
p, err := om.migrationStore.GetDashboardPermissions(ctx, getMigrationUser(om.orgID), d.UID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// getFolder returns the parent folder for the given dashboard. If the dashboard is in the general folder, it returns the general alerting folder.
|
||||
func (om *OrgMigration) getFolder(ctx context.Context, dash *dashboards.Dashboard) (*folder.Folder, error) {
|
||||
if f, ok := om.folderCache[dash.FolderID]; ok {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
if dash.FolderID <= 0 {
|
||||
// Don't use general folder since it has no uid, instead we use a new "General Alerting" folder.
|
||||
migratedFolder, err := om.getOrCreateGeneralAlertingFolder(ctx, om.orgID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get or create general folder: %w", err)
|
||||
}
|
||||
return migratedFolder, err
|
||||
}
|
||||
|
||||
f, err := om.migrationStore.GetFolder(ctx, &folder.GetFolderQuery{ID: &dash.FolderID, OrgID: om.orgID, SignedInUser: getMigrationUser(om.orgID)})
|
||||
if err != nil {
|
||||
if errors.Is(err, dashboards.ErrFolderNotFound) {
|
||||
return nil, fmt.Errorf("folder with id %v not found", dash.FolderID)
|
||||
}
|
||||
return nil, fmt.Errorf("get folder %d: %w", dash.FolderID, err)
|
||||
}
|
||||
om.folderCache[dash.FolderID] = f
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// getOrCreateGeneralAlertingFolder returns the general alerting folder under the specific organisation
|
||||
// If the general alerting folder does not exist it creates it.
|
||||
func (om *OrgMigration) getOrCreateGeneralAlertingFolder(ctx context.Context, orgID int64) (*folder.Folder, error) {
|
||||
if om.generalAlertingFolder != nil {
|
||||
return om.generalAlertingFolder, nil
|
||||
}
|
||||
f, err := om.migrationStore.GetFolder(ctx, &folder.GetFolderQuery{OrgID: orgID, Title: &generalAlertingFolderTitle, SignedInUser: getMigrationUser(orgID)})
|
||||
if err != nil {
|
||||
if errors.Is(err, dashboards.ErrFolderNotFound) {
|
||||
// create folder
|
||||
generalAlertingFolder, err := om.createFolder(ctx, orgID, generalAlertingFolderTitle)
|
||||
// create general alerting folder without permissions to mimic the general folder.
|
||||
f, err := om.createFolder(ctx, orgID, generalAlertingFolderTitle, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create general alerting folder '%s': %w", generalAlertingFolderTitle, err)
|
||||
return nil, fmt.Errorf("create general alerting folder: %w", err)
|
||||
}
|
||||
om.generalAlertingFolder = generalAlertingFolder
|
||||
return om.generalAlertingFolder, nil
|
||||
om.generalAlertingFolder = f
|
||||
return f, err
|
||||
}
|
||||
return nil, fmt.Errorf("get general alerting folder '%s': %w", generalAlertingFolderTitle, err)
|
||||
return nil, fmt.Errorf("get folder '%s': %w", generalAlertingFolderTitle, err)
|
||||
}
|
||||
om.generalAlertingFolder = f
|
||||
|
||||
return om.generalAlertingFolder, nil
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// createFolder creates a new folder with given permissions.
|
||||
func (om *OrgMigration) createFolder(ctx context.Context, orgID int64, title string) (*folder.Folder, error) {
|
||||
func (om *OrgMigration) createFolder(ctx context.Context, orgID int64, title string, newPerms []accesscontrol.SetResourcePermissionCommand) (*folder.Folder, error) {
|
||||
f, err := om.migrationStore.CreateFolder(ctx, &folder.CreateFolderCommand{
|
||||
OrgID: orgID,
|
||||
Title: title,
|
||||
@ -147,6 +386,13 @@ func (om *OrgMigration) createFolder(ctx context.Context, orgID int64, title str
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(newPerms) > 0 {
|
||||
_, err = om.migrationStore.SetFolderPermissions(ctx, orgID, f.UID, newPerms...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("set permissions: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
om.state.CreatedFolders = append(om.state.CreatedFolders, f.UID)
|
||||
|
||||
return f, nil
|
||||
|
820
pkg/services/ngalert/migration/permissions_test.go
Normal file
820
pkg/services/ngalert/migration/permissions_test.go
Normal file
@ -0,0 +1,820 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
ngModels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/team"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// TestDashAlertPermissionMigration tests the execution of the migration specifically for dashboards with custom permissions.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func TestDashAlertPermissionMigration(t *testing.T) {
|
||||
genLegacyAlert := func(name string, dashboardId int64, mutators ...func(*models.Alert)) *models.Alert {
|
||||
a := createAlert(t, 1, int(dashboardId), 1, name, nil)
|
||||
if len(mutators) > 0 {
|
||||
for _, mutator := range mutators {
|
||||
mutator(a)
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
genAlert := func(title string, namespaceUID string, dashboardUID string, mutators ...func(*ngModels.AlertRule)) *ngModels.AlertRule {
|
||||
a := &ngModels.AlertRule{
|
||||
ID: 1,
|
||||
OrgID: 1,
|
||||
Title: title,
|
||||
Condition: "A",
|
||||
Data: []ngModels.AlertQuery{
|
||||
{
|
||||
RefID: "A",
|
||||
DatasourceUID: "__expr__",
|
||||
Model: json.RawMessage(`{"conditions":[],"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"classic_conditions"}`),
|
||||
},
|
||||
},
|
||||
NamespaceUID: namespaceUID,
|
||||
DashboardUID: &dashboardUID,
|
||||
RuleGroup: fmt.Sprintf("Dashboard Title %s - %d", dashboardUID, 1),
|
||||
IntervalSeconds: 60,
|
||||
Version: 1,
|
||||
PanelID: pointer(int64(1)),
|
||||
RuleGroupIndex: 1,
|
||||
NoDataState: ngModels.NoData,
|
||||
ExecErrState: ngModels.AlertingErrState,
|
||||
For: 60 * time.Second,
|
||||
Annotations: map[string]string{
|
||||
"message": "message",
|
||||
"__dashboardUid__": dashboardUID,
|
||||
"__panelId__": "1",
|
||||
},
|
||||
Labels: map[string]string{},
|
||||
IsPaused: false,
|
||||
}
|
||||
if len(mutators) > 0 {
|
||||
for _, mutator := range mutators {
|
||||
mutator(a)
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
withPanelId := func(id int64) func(*ngModels.AlertRule) {
|
||||
return func(a *ngModels.AlertRule) {
|
||||
a.PanelID = pointer(id)
|
||||
a.Annotations["__panelId__"] = fmt.Sprintf("%d", id)
|
||||
a.RuleGroup = fmt.Sprintf("Dashboard Title %s - %d", *a.DashboardUID, id)
|
||||
}
|
||||
}
|
||||
|
||||
genFolder := func(t *testing.T, id int64, uid string, mutators ...func(f *dashboards.Dashboard)) *dashboards.Dashboard {
|
||||
d := createFolder(t, id, 1, uid)
|
||||
d.Title = "Original Folder " + uid
|
||||
if len(mutators) > 0 {
|
||||
for _, mutator := range mutators {
|
||||
mutator(d)
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
genCreatedFolder := func(t *testing.T, title string, mutators ...func(f *dashboards.Dashboard)) *dashboards.Dashboard {
|
||||
d := createFolder(t, 1, 1, "") // Leave generated UID blank, so we don't compare.
|
||||
d.Title = title
|
||||
d.CreatedBy = -1
|
||||
d.UpdatedBy = -1
|
||||
if len(mutators) > 0 {
|
||||
for _, mutator := range mutators {
|
||||
mutator(d)
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
genDashboard := func(t *testing.T, id int64, uid string, folderId int64, mutators ...func(f *dashboards.Dashboard)) *dashboards.Dashboard {
|
||||
d := createDashboard(t, id, 1, uid, folderId, nil)
|
||||
d.Title = "Dashboard Title " + uid
|
||||
if len(mutators) > 0 {
|
||||
for _, mutator := range mutators {
|
||||
mutator(d)
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
genPerms := func(perms ...accesscontrol.SetResourcePermissionCommand) []accesscontrol.SetResourcePermissionCommand {
|
||||
return perms
|
||||
}
|
||||
|
||||
type expectedAlertMigration struct {
|
||||
Alert *ngModels.AlertRule
|
||||
Folder *dashboards.Dashboard
|
||||
Perms []accesscontrol.SetResourcePermissionCommand
|
||||
}
|
||||
|
||||
type testcase struct {
|
||||
name string
|
||||
enterprise bool
|
||||
folders []*dashboards.Dashboard
|
||||
folderPerms map[string][]accesscontrol.SetResourcePermissionCommand // UID -> Perms
|
||||
dashboards []*dashboards.Dashboard
|
||||
dashboardPerms map[string][]accesscontrol.SetResourcePermissionCommand // UID -> Perms
|
||||
alerts []*models.Alert
|
||||
roles map[accesscontrol.Role][]accesscontrol.Permission
|
||||
|
||||
expected []expectedAlertMigration
|
||||
}
|
||||
|
||||
// Used to perform the same tests for each of builtins, users, and teams.
|
||||
splitTestcase := func(raw testcase) []testcase {
|
||||
permTypes := make(map[string]func(accesscontrol.SetResourcePermissionCommand) accesscontrol.SetResourcePermissionCommand, 3)
|
||||
permTypes["builtins"] = func(p accesscontrol.SetResourcePermissionCommand) accesscontrol.SetResourcePermissionCommand {
|
||||
return p
|
||||
}
|
||||
mapping := map[string]int64{
|
||||
string(org.RoleEditor): 1,
|
||||
string(org.RoleViewer): 2,
|
||||
}
|
||||
permTypes["users"] = func(p accesscontrol.SetResourcePermissionCommand) accesscontrol.SetResourcePermissionCommand {
|
||||
id, ok := mapping[p.BuiltinRole]
|
||||
if !ok {
|
||||
return p
|
||||
}
|
||||
p.UserID = id
|
||||
p.BuiltinRole = ""
|
||||
return p
|
||||
}
|
||||
permTypes["teams"] = func(p accesscontrol.SetResourcePermissionCommand) accesscontrol.SetResourcePermissionCommand {
|
||||
id, ok := mapping[p.BuiltinRole]
|
||||
if !ok {
|
||||
return p
|
||||
}
|
||||
p.TeamID = id
|
||||
p.BuiltinRole = ""
|
||||
return p
|
||||
}
|
||||
|
||||
applyTransform := func(tt testcase, pfunc func(p accesscontrol.SetResourcePermissionCommand) accesscontrol.SetResourcePermissionCommand) testcase {
|
||||
folderPerms := make(map[string][]accesscontrol.SetResourcePermissionCommand, len(tt.folderPerms))
|
||||
for _, f := range tt.folders {
|
||||
perms := make([]accesscontrol.SetResourcePermissionCommand, 0, len(tt.folderPerms[f.UID]))
|
||||
for _, p := range tt.folderPerms[f.UID] {
|
||||
perms = append(perms, pfunc(p))
|
||||
}
|
||||
folderPerms[f.UID] = perms
|
||||
}
|
||||
tt.folderPerms = folderPerms
|
||||
|
||||
dashboardPerms := make(map[string][]accesscontrol.SetResourcePermissionCommand, len(tt.dashboardPerms))
|
||||
for _, d := range tt.dashboards {
|
||||
perms := make([]accesscontrol.SetResourcePermissionCommand, 0, len(tt.dashboardPerms[d.UID]))
|
||||
for _, p := range tt.dashboardPerms[d.UID] {
|
||||
perms = append(perms, pfunc(p))
|
||||
}
|
||||
dashboardPerms[d.UID] = perms
|
||||
}
|
||||
tt.dashboardPerms = dashboardPerms
|
||||
|
||||
expected := make([]expectedAlertMigration, 0, len(tt.expected))
|
||||
for _, ex := range tt.expected {
|
||||
permissions := make([]accesscontrol.SetResourcePermissionCommand, 0, len(ex.Perms))
|
||||
for _, p := range ex.Perms {
|
||||
permissions = append(permissions, pfunc(p))
|
||||
}
|
||||
ex.Perms = permissions
|
||||
|
||||
sort.SliceStable(permissions, func(i, j int) bool {
|
||||
if permissions[i].BuiltinRole != permissions[j].BuiltinRole {
|
||||
return permissions[i].BuiltinRole < permissions[j].BuiltinRole
|
||||
}
|
||||
if permissions[i].UserID != permissions[j].UserID {
|
||||
return permissions[i].UserID < permissions[j].UserID
|
||||
}
|
||||
if permissions[i].TeamID != permissions[j].TeamID {
|
||||
return permissions[i].TeamID < permissions[j].TeamID
|
||||
}
|
||||
return permissions[i].Permission < permissions[j].Permission
|
||||
})
|
||||
|
||||
f := *ex.Folder
|
||||
if strings.Contains(f.Title, "%s") {
|
||||
hash, err := createHash(permissions)
|
||||
require.NoError(t, err)
|
||||
f.Title = fmt.Sprintf(f.Title, hash)
|
||||
}
|
||||
|
||||
expected = append(expected, expectedAlertMigration{
|
||||
Alert: ex.Alert,
|
||||
Folder: &f,
|
||||
Perms: permissions,
|
||||
})
|
||||
}
|
||||
tt.expected = expected
|
||||
|
||||
return tt
|
||||
}
|
||||
|
||||
cases := make([]testcase, 0, 3)
|
||||
for k, pfunc := range permTypes {
|
||||
tt := applyTransform(raw, pfunc)
|
||||
tt.name = k
|
||||
cases = append(cases, tt)
|
||||
}
|
||||
return cases
|
||||
}
|
||||
|
||||
basicFolder := genFolder(t, 1, "f_1")
|
||||
basicDashboard := genDashboard(t, 2, "d_1", basicFolder.ID)
|
||||
defaultPerms := genPerms(
|
||||
accesscontrol.SetResourcePermissionCommand{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
accesscontrol.SetResourcePermissionCommand{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()},
|
||||
)
|
||||
|
||||
basicAlert1 := genLegacyAlert("alert1", basicDashboard.ID, func(a *models.Alert) { a.PanelID = 1 })
|
||||
basicAlert2 := genLegacyAlert("alert2", basicDashboard.ID, func(a *models.Alert) { a.PanelID = 2 })
|
||||
|
||||
basicPerms := func() map[accesscontrol.Role][]accesscontrol.Permission {
|
||||
basic := make(map[accesscontrol.Role][]accesscontrol.Permission)
|
||||
var permissions []accesscontrol.Permission
|
||||
ts := time.Now()
|
||||
for _, action := range append(ossaccesscontrol.DashboardAdminActions, ossaccesscontrol.FolderAdminActions...) {
|
||||
if isDashboardAction := strings.HasPrefix(action, "dashboards"); isDashboardAction {
|
||||
permissions = append(permissions, accesscontrol.Permission{
|
||||
Action: action,
|
||||
Scope: dashboards.ScopeDashboardsAll,
|
||||
Created: ts,
|
||||
Updated: ts,
|
||||
})
|
||||
}
|
||||
permissions = append(permissions, accesscontrol.Permission{
|
||||
Action: action,
|
||||
Scope: dashboards.ScopeFoldersAll,
|
||||
Created: ts,
|
||||
Updated: ts,
|
||||
})
|
||||
}
|
||||
basic[accesscontrol.Role{Name: accesscontrol.BasicRolePrefix + "admin"}] = permissions
|
||||
return basic
|
||||
}
|
||||
|
||||
tc := []testcase{
|
||||
{
|
||||
name: "alerts in dashboard and folder with default permissions migrate to same folder",
|
||||
folders: []*dashboards.Dashboard{basicFolder},
|
||||
folderPerms: map[string][]accesscontrol.SetResourcePermissionCommand{basicFolder.UID: defaultPerms},
|
||||
dashboards: []*dashboards.Dashboard{basicDashboard},
|
||||
dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{basicDashboard.UID: defaultPerms},
|
||||
alerts: []*models.Alert{basicAlert1, basicAlert2},
|
||||
expected: []expectedAlertMigration{
|
||||
{
|
||||
Alert: genAlert(basicAlert1.Name, basicFolder.UID, basicDashboard.UID, withPanelId(basicAlert1.PanelID)),
|
||||
Folder: basicFolder,
|
||||
Perms: defaultPerms,
|
||||
},
|
||||
{
|
||||
Alert: genAlert(basicAlert2.Name, basicFolder.UID, basicDashboard.UID, withPanelId(basicAlert2.PanelID)),
|
||||
Folder: basicFolder,
|
||||
Perms: defaultPerms,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard override cannot lessen folder permissions",
|
||||
folders: []*dashboards.Dashboard{basicFolder},
|
||||
folderPerms: map[string][]accesscontrol.SetResourcePermissionCommand{basicFolder.UID: defaultPerms},
|
||||
dashboards: []*dashboards.Dashboard{basicDashboard},
|
||||
dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{
|
||||
basicDashboard.UID: {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_VIEW.String()}, // Change.
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()},
|
||||
},
|
||||
},
|
||||
alerts: []*models.Alert{basicAlert1},
|
||||
expected: []expectedAlertMigration{
|
||||
{
|
||||
Alert: genAlert(basicAlert1.Name, basicFolder.UID, basicDashboard.UID),
|
||||
Folder: basicFolder,
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()}, // Inherits from Folder.
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard with various permission overrides should create new folder",
|
||||
folders: []*dashboards.Dashboard{basicFolder},
|
||||
folderPerms: map[string][]accesscontrol.SetResourcePermissionCommand{basicFolder.UID: defaultPerms},
|
||||
dashboards: []*dashboards.Dashboard{
|
||||
genDashboard(t, 2, "d_1", basicFolder.ID),
|
||||
genDashboard(t, 3, "d_2", basicFolder.ID),
|
||||
genDashboard(t, 4, "d_3", basicFolder.ID),
|
||||
genDashboard(t, 5, "d_4", basicFolder.ID),
|
||||
genDashboard(t, 6, "d_5", basicFolder.ID),
|
||||
},
|
||||
dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{
|
||||
"d_1": {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_EDIT.String()}, // Change.
|
||||
},
|
||||
"d_2": {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_ADMIN.String()}, // Change.
|
||||
},
|
||||
"d_3": {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_ADMIN.String()}, // Change.
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_EDIT.String()}, // Change.
|
||||
},
|
||||
"d_4": {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_ADMIN.String()}, // Change.
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()},
|
||||
},
|
||||
"d_5": {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_ADMIN.String()}, // Change.
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_ADMIN.String()}, // Change.
|
||||
},
|
||||
},
|
||||
alerts: []*models.Alert{genLegacyAlert("alert1", 2), genLegacyAlert("alert2", 3), genLegacyAlert("alert3", 4), genLegacyAlert("alert4", 5), genLegacyAlert("alert5", 6)},
|
||||
expected: []expectedAlertMigration{
|
||||
{
|
||||
Alert: genAlert("alert1", "", "d_1"),
|
||||
Folder: genCreatedFolder(t, "Original Folder f_1 Alerts - %s"),
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_EDIT.String()}, // Change.
|
||||
},
|
||||
},
|
||||
{
|
||||
Alert: genAlert("alert2", "", "d_2"),
|
||||
Folder: genCreatedFolder(t, "Original Folder f_1 Alerts - %s"),
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_ADMIN.String()}, // Change.
|
||||
},
|
||||
},
|
||||
{
|
||||
Alert: genAlert("alert3", "", "d_3"),
|
||||
Folder: genCreatedFolder(t, "Original Folder f_1 Alerts - %s"),
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_ADMIN.String()}, // Change.
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_EDIT.String()}, // Change.
|
||||
},
|
||||
},
|
||||
{
|
||||
Alert: genAlert("alert4", "", "d_4"),
|
||||
Folder: genCreatedFolder(t, "Original Folder f_1 Alerts - %s"),
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_ADMIN.String()}, // Change.
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()},
|
||||
},
|
||||
},
|
||||
{
|
||||
Alert: genAlert("alert5", "", "d_5"),
|
||||
Folder: genCreatedFolder(t, "Original Folder f_1 Alerts - %s"),
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_ADMIN.String()}, // Change.
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_ADMIN.String()}, // Change.
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing dashboard permission is inherited from folder",
|
||||
folders: []*dashboards.Dashboard{genFolder(t, 1, "f_1"), genFolder(t, 2, "f_2")},
|
||||
folderPerms: map[string][]accesscontrol.SetResourcePermissionCommand{
|
||||
"f_1": {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_ADMIN.String()},
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_ADMIN.String()},
|
||||
},
|
||||
"f_2": {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_VIEW.String()},
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()},
|
||||
},
|
||||
},
|
||||
dashboards: []*dashboards.Dashboard{
|
||||
genDashboard(t, 3, "d_1", 1),
|
||||
genDashboard(t, 4, "d_2", 1),
|
||||
genDashboard(t, 5, "d_3", 2),
|
||||
genDashboard(t, 6, "d_4", 2),
|
||||
},
|
||||
dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{
|
||||
"d_1": {
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()},
|
||||
},
|
||||
"d_2": {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
},
|
||||
"d_3": {
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()},
|
||||
},
|
||||
"d_4": {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
},
|
||||
},
|
||||
alerts: []*models.Alert{genLegacyAlert("alert1", 3), genLegacyAlert("alert2", 4), genLegacyAlert("alert3", 5), genLegacyAlert("alert4", 6)},
|
||||
expected: []expectedAlertMigration{
|
||||
{
|
||||
Alert: genAlert("alert1", "f_1", "d_1"),
|
||||
Folder: genFolder(t, 1, "f_1"), // Original folder since the perms didn't change.
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_ADMIN.String()}, // Inherits from Folder.
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_ADMIN.String()}, // Overrides from Folder.
|
||||
},
|
||||
},
|
||||
{
|
||||
Alert: genAlert("alert2", "f_1", "d_2"),
|
||||
Folder: genFolder(t, 1, "f_1"), // Original folder since the perms didn't change.
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_ADMIN.String()}, // Overrides from Folder.
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_ADMIN.String()}, // Inherits from Folder.
|
||||
},
|
||||
},
|
||||
{
|
||||
Alert: genAlert("alert3", "f_2", "d_3"),
|
||||
Folder: genFolder(t, 2, "f_2"), // Original folder since the perms didn't change.
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_VIEW.String()}, // Inherits from Folder.
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()},
|
||||
},
|
||||
},
|
||||
{
|
||||
Alert: genAlert("alert4", "", "d_4"),
|
||||
Folder: genCreatedFolder(t, "Original Folder f_2 Alerts - %s"),
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()}, // Inherits from Folder.
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing dashboard and folder view permission is still missing",
|
||||
folders: []*dashboards.Dashboard{basicFolder},
|
||||
folderPerms: map[string][]accesscontrol.SetResourcePermissionCommand{
|
||||
basicFolder.UID: {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
},
|
||||
},
|
||||
dashboards: []*dashboards.Dashboard{basicDashboard},
|
||||
dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{
|
||||
basicDashboard.UID: {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_VIEW.String()},
|
||||
},
|
||||
},
|
||||
alerts: []*models.Alert{basicAlert1},
|
||||
expected: []expectedAlertMigration{
|
||||
{
|
||||
Alert: genAlert(basicAlert1.Name, basicFolder.UID, basicDashboard.UID),
|
||||
Folder: basicFolder,
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// General folder.
|
||||
{
|
||||
name: "dashboard in general folder with default permissions migrates to General Alerting subfolder for permission",
|
||||
dashboards: []*dashboards.Dashboard{genDashboard(t, 1, "d_1", 0)}, // Dashboard in general folder.
|
||||
dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{
|
||||
"d_1": defaultPerms,
|
||||
},
|
||||
alerts: []*models.Alert{genLegacyAlert("alert1", 1)},
|
||||
expected: []expectedAlertMigration{
|
||||
{
|
||||
Alert: genAlert("alert1", "f_1", "d_1"),
|
||||
Folder: genCreatedFolder(t, "General Alerting Alerts - %s"),
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()}, // From Dashboard.
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()}, // From Dashboard.
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard in general folder with some perms migrates to General Alerting subfolder with correct permissions",
|
||||
dashboards: []*dashboards.Dashboard{genDashboard(t, 1, "d_1", 0)}, // Dashboard in general folder.
|
||||
dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{
|
||||
"d_1": { // Missing viewer.
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
},
|
||||
},
|
||||
alerts: []*models.Alert{genLegacyAlert("alert1", 1)},
|
||||
expected: []expectedAlertMigration{
|
||||
{
|
||||
Alert: genAlert("alert1", "f_1", "d_1"),
|
||||
Folder: genCreatedFolder(t, "General Alerting Alerts - %s"),
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()}, // From Dashboard.
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard in general folder with empty perms migrates to General Alerting",
|
||||
dashboards: []*dashboards.Dashboard{genDashboard(t, 1, "d_1", 0)}, // Dashboard in general folder.
|
||||
alerts: []*models.Alert{genLegacyAlert("alert1", 1)},
|
||||
expected: []expectedAlertMigration{
|
||||
{
|
||||
Alert: genAlert("alert1", "f_1", "d_1"),
|
||||
Folder: genCreatedFolder(t, "General Alerting"),
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// The following tests handled extra requirements of enterprise RBAC in that they include basic, fixed, and custom roles.
|
||||
{
|
||||
name: "should handle basic roles the same as managed builtin roles",
|
||||
enterprise: true,
|
||||
roles: basicPerms(),
|
||||
folders: []*dashboards.Dashboard{basicFolder},
|
||||
folderPerms: map[string][]accesscontrol.SetResourcePermissionCommand{basicFolder.UID: defaultPerms},
|
||||
dashboards: []*dashboards.Dashboard{
|
||||
genDashboard(t, 2, "d_1", basicFolder.ID),
|
||||
},
|
||||
dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{
|
||||
"d_1": {
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_EDIT.String()}, // Change.
|
||||
},
|
||||
},
|
||||
alerts: []*models.Alert{genLegacyAlert("alert1", 2)},
|
||||
expected: []expectedAlertMigration{
|
||||
{
|
||||
Alert: genAlert("alert1", "", "d_1"),
|
||||
Folder: genCreatedFolder(t, "Original Folder f_1 Alerts - %s"),
|
||||
Perms: []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleAdmin), Permission: dashboards.PERMISSION_ADMIN.String()}, // From basic:admin.
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()},
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_EDIT.String()}, // Change.
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should ignore fixed roles even if they would affect access",
|
||||
enterprise: true,
|
||||
roles: map[accesscontrol.Role][]accesscontrol.Permission{
|
||||
accesscontrol.Role{Name: "fixed:dashboards:writer"}: {
|
||||
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll},
|
||||
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll},
|
||||
{Action: dashboards.ActionDashboardsDelete, Scope: dashboards.ScopeDashboardsAll},
|
||||
{Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll},
|
||||
{Action: dashboards.ActionDashboardsPermissionsRead, Scope: dashboards.ScopeDashboardsAll},
|
||||
{Action: dashboards.ActionDashboardsPermissionsWrite, Scope: dashboards.ScopeDashboardsAll},
|
||||
},
|
||||
},
|
||||
folders: []*dashboards.Dashboard{basicFolder},
|
||||
folderPerms: map[string][]accesscontrol.SetResourcePermissionCommand{basicFolder.UID: defaultPerms},
|
||||
dashboards: []*dashboards.Dashboard{basicDashboard},
|
||||
dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{basicDashboard.UID: defaultPerms},
|
||||
alerts: []*models.Alert{basicAlert1},
|
||||
expected: []expectedAlertMigration{ // Expect no new folder.
|
||||
{
|
||||
Alert: genAlert(basicAlert1.Name, basicFolder.UID, basicDashboard.UID),
|
||||
Folder: basicFolder,
|
||||
Perms: defaultPerms,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should ignore custom roles even if they would affect access",
|
||||
enterprise: true,
|
||||
roles: map[accesscontrol.Role][]accesscontrol.Permission{
|
||||
accesscontrol.Role{Name: "custom role"}: {
|
||||
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll},
|
||||
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll},
|
||||
{Action: dashboards.ActionDashboardsDelete, Scope: dashboards.ScopeDashboardsAll},
|
||||
{Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll},
|
||||
{Action: dashboards.ActionDashboardsPermissionsRead, Scope: dashboards.ScopeDashboardsAll},
|
||||
{Action: dashboards.ActionDashboardsPermissionsWrite, Scope: dashboards.ScopeDashboardsAll},
|
||||
},
|
||||
},
|
||||
folders: []*dashboards.Dashboard{basicFolder},
|
||||
folderPerms: map[string][]accesscontrol.SetResourcePermissionCommand{basicFolder.UID: defaultPerms},
|
||||
dashboards: []*dashboards.Dashboard{basicDashboard},
|
||||
dashboardPerms: map[string][]accesscontrol.SetResourcePermissionCommand{basicDashboard.UID: defaultPerms},
|
||||
alerts: []*models.Alert{basicAlert1},
|
||||
expected: []expectedAlertMigration{ // Expect no new folder.
|
||||
{
|
||||
Alert: genAlert(basicAlert1.Name, basicFolder.UID, basicDashboard.UID),
|
||||
Folder: basicFolder,
|
||||
Perms: defaultPerms,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, ttRaw := range tc {
|
||||
t.Run(ttRaw.name, func(t *testing.T) {
|
||||
for _, tt := range splitTestcase(ttRaw) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sqlStore := db.InitTestDB(t)
|
||||
x := sqlStore.GetEngine()
|
||||
|
||||
if tt.enterprise {
|
||||
createRoles(t, context.Background(), sqlStore, tt.roles)
|
||||
}
|
||||
|
||||
service := NewTestMigrationService(t, sqlStore, &setting.Cfg{})
|
||||
setupLegacyAlertsTables(t, x, nil, tt.alerts, tt.folders, tt.dashboards)
|
||||
|
||||
for i := 1; i < 3; i++ {
|
||||
_, err := x.Insert(user.User{
|
||||
ID: int64(i),
|
||||
OrgID: 1,
|
||||
Name: fmt.Sprintf("user%v", i),
|
||||
Login: fmt.Sprintf("user%v", i),
|
||||
Email: fmt.Sprintf("user%v@example.org", i),
|
||||
Created: now,
|
||||
Updated: now,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
for i := 1; i < 3; i++ {
|
||||
_, err := x.Insert(team.Team{
|
||||
ID: int64(i),
|
||||
OrgID: 1,
|
||||
UID: fmt.Sprintf("team%v", i),
|
||||
Name: fmt.Sprintf("team%v", i),
|
||||
Created: now,
|
||||
Updated: now,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
for _, f := range tt.folders {
|
||||
_, err := service.migrationStore.SetFolderPermissions(context.Background(), 1, f.UID, tt.folderPerms[f.UID]...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
for _, d := range tt.dashboards {
|
||||
_, err := service.migrationStore.SetDashboardPermissions(context.Background(), 1, d.UID, tt.dashboardPerms[d.UID]...)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err := service.Run(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// construct actuals.
|
||||
orgId := int64(1)
|
||||
rules := getAlertRules(t, x, orgId)
|
||||
actual := make([]expectedAlertMigration, 0, len(rules))
|
||||
for i, r := range rules {
|
||||
// Remove generated fields.
|
||||
require.NotEqual(t, r.Labels["rule_uid"], "")
|
||||
delete(r.Labels, "rule_uid")
|
||||
require.NotEqual(t, r.Annotations["__alertId__"], "")
|
||||
delete(r.Annotations, "__alertId__")
|
||||
|
||||
folder := getDashboard(t, x, orgId, r.NamespaceUID)
|
||||
rperms, err := service.migrationStore.GetFolderPermissions(context.Background(), getMigrationUser(orgId), folder.UID)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := tt.expected[i]
|
||||
if expected.Folder.UID == "" {
|
||||
// We're expecting the UID to be generated, so remove it from comparison.
|
||||
folder.UID = ""
|
||||
r.NamespaceUID = ""
|
||||
expected.Alert.NamespaceUID = ""
|
||||
}
|
||||
|
||||
keep := make(map[accesscontrol.SetResourcePermissionCommand]dashboards.PermissionType)
|
||||
for _, p := range rperms {
|
||||
if permission := service.migrationStore.MapActions(p); permission != "" {
|
||||
sp := accesscontrol.SetResourcePermissionCommand{
|
||||
UserID: p.UserId,
|
||||
TeamID: p.TeamId,
|
||||
BuiltinRole: p.BuiltInRole,
|
||||
}
|
||||
pType := permissionMap[permission]
|
||||
current, ok := keep[sp]
|
||||
if !ok || pType > current {
|
||||
keep[sp] = pType
|
||||
}
|
||||
}
|
||||
}
|
||||
perms := make([]accesscontrol.SetResourcePermissionCommand, 0, len(keep))
|
||||
for p, pType := range keep {
|
||||
p.Permission = pType.String()
|
||||
perms = append(perms, p)
|
||||
}
|
||||
|
||||
actual = append(actual, expectedAlertMigration{
|
||||
Alert: r,
|
||||
Folder: folder,
|
||||
Perms: perms,
|
||||
})
|
||||
}
|
||||
|
||||
cOpt := []cmp.Option{
|
||||
cmpopts.SortSlices(func(a, b expectedAlertMigration) bool {
|
||||
return a.Alert.Title < b.Alert.Title
|
||||
}),
|
||||
cmpopts.SortSlices(func(a, b accesscontrol.SetResourcePermissionCommand) bool {
|
||||
if a.BuiltinRole != b.BuiltinRole {
|
||||
return a.BuiltinRole < b.BuiltinRole
|
||||
}
|
||||
if a.UserID != b.UserID {
|
||||
return a.UserID < b.UserID
|
||||
}
|
||||
if a.TeamID != b.TeamID {
|
||||
return a.TeamID < b.TeamID
|
||||
}
|
||||
return a.Permission < b.Permission
|
||||
}),
|
||||
cmpopts.IgnoreUnexported(ngModels.AlertRule{}, ngModels.AlertQuery{}),
|
||||
cmpopts.IgnoreFields(ngModels.AlertRule{}, "ID", "Updated", "UID"),
|
||||
cmpopts.IgnoreFields(dashboards.Dashboard{}, "ID", "Created", "Updated", "Data", "Slug"),
|
||||
}
|
||||
if !cmp.Equal(tt.expected, actual, cOpt...) {
|
||||
t.Errorf("Unexpected Rule: %v", cmp.Diff(tt.expected, actual, cOpt...))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createRoles(t testing.TB, ctx context.Context, store db.DB, rolePerms map[accesscontrol.Role][]accesscontrol.Permission) {
|
||||
_ = store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
ts := time.Now()
|
||||
var roles []accesscontrol.Role
|
||||
|
||||
basic := accesscontrol.BuildBasicRoleDefinitions()
|
||||
|
||||
var permissions []accesscontrol.Permission
|
||||
var builtinRoleAssignments []accesscontrol.BuiltinRole
|
||||
var userRoleAssignments []accesscontrol.UserRole
|
||||
var teamRoleAssignments []accesscontrol.TeamRole
|
||||
i := int64(1)
|
||||
for role, perms := range rolePerms {
|
||||
if role.IsBasic() {
|
||||
for roleType, br := range basic {
|
||||
if br.Name == role.Name {
|
||||
role = br.Role()
|
||||
builtinRoleAssignments = append(builtinRoleAssignments, accesscontrol.BuiltinRole{
|
||||
OrgID: accesscontrol.GlobalOrgID, RoleID: i, Role: roleType, Created: ts, Updated: ts,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
userRoleAssignments = append(userRoleAssignments, accesscontrol.UserRole{
|
||||
OrgID: accesscontrol.GlobalOrgID, RoleID: i, UserID: 1, Created: ts,
|
||||
})
|
||||
teamRoleAssignments = append(teamRoleAssignments, accesscontrol.TeamRole{
|
||||
OrgID: accesscontrol.GlobalOrgID, RoleID: i, TeamID: 1, Created: ts,
|
||||
})
|
||||
}
|
||||
role.ID = i
|
||||
role.Created = ts
|
||||
role.Updated = ts
|
||||
|
||||
roles = append(roles, role)
|
||||
|
||||
for _, p := range perms {
|
||||
permissions = append(permissions, accesscontrol.Permission{
|
||||
RoleID: role.ID, Action: p.Action, Scope: p.Scope, Created: ts, Updated: ts,
|
||||
})
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
_, err := sess.InsertMulti(&roles)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = sess.InsertMulti(&permissions)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = sess.InsertMulti(&builtinRoleAssignments)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = sess.InsertMulti(&userRoleAssignments)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = sess.InsertMulti(&teamRoleAssignments)
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
@ -26,6 +26,7 @@ const (
|
||||
ErrorAlertName = "DatasourceError"
|
||||
)
|
||||
|
||||
// addErrorSilence adds a silence for the given rule to the orgMigration if the ExecutionErrorState was set to keep_state.
|
||||
func (om *OrgMigration) addErrorSilence(rule *models.AlertRule) error {
|
||||
uid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
@ -58,6 +59,7 @@ func (om *OrgMigration) addErrorSilence(rule *models.AlertRule) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// addNoDataSilence adds a silence for the given rule to the orgMigration if the NoDataState was set to keep_state.
|
||||
func (om *OrgMigration) addNoDataSilence(rule *models.AlertRule) error {
|
||||
uid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||
@ -44,8 +43,11 @@ type Store interface {
|
||||
|
||||
GetOrgDashboardAlerts(ctx context.Context, orgID int64) (map[int64][]*DashAlert, int, error)
|
||||
|
||||
GetACL(ctx context.Context, orgID int64, dashID int64) ([]*DashboardACL, error)
|
||||
SetACL(ctx context.Context, orgID int64, dashboardID int64, items []*DashboardACL) error
|
||||
GetDashboardPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error)
|
||||
GetFolderPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error)
|
||||
SetDashboardPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error)
|
||||
SetFolderPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error)
|
||||
MapActions(permission accesscontrol.ResourcePermission) string
|
||||
|
||||
GetDashboard(ctx context.Context, orgID int64, id int64) (*dashboards.Dashboard, error)
|
||||
GetFolder(ctx context.Context, cmd *folder.GetFolderQuery) (*folder.Folder, error)
|
||||
@ -62,15 +64,18 @@ type Store interface {
|
||||
}
|
||||
|
||||
type migrationStore struct {
|
||||
store db.DB
|
||||
cfg *setting.Cfg
|
||||
log log.Logger
|
||||
kv kvstore.KVStore
|
||||
alertingStore *store.DBstore
|
||||
dashboardService dashboards.DashboardService
|
||||
folderService folder.Service
|
||||
dataSourceCache datasources.CacheService
|
||||
orgService org.Service
|
||||
store db.DB
|
||||
cfg *setting.Cfg
|
||||
log log.Logger
|
||||
kv kvstore.KVStore
|
||||
alertingStore *store.DBstore
|
||||
dashboardService dashboards.DashboardService
|
||||
folderService folder.Service
|
||||
dataSourceCache datasources.CacheService
|
||||
folderPermissions accesscontrol.FolderPermissionsService
|
||||
dashboardPermissions accesscontrol.DashboardPermissionsService
|
||||
orgService org.Service
|
||||
|
||||
legacyAlertNotificationService *legacyalerting.AlertNotificationService
|
||||
}
|
||||
|
||||
@ -85,6 +90,8 @@ func ProvideMigrationStore(
|
||||
dashboardService dashboards.DashboardService,
|
||||
folderService folder.Service,
|
||||
dataSourceCache datasources.CacheService,
|
||||
folderPermissions accesscontrol.FolderPermissionsService,
|
||||
dashboardPermissions accesscontrol.DashboardPermissionsService,
|
||||
orgService org.Service,
|
||||
legacyAlertNotificationService *legacyalerting.AlertNotificationService,
|
||||
) (Store, error) {
|
||||
@ -97,6 +104,8 @@ func ProvideMigrationStore(
|
||||
dashboardService: dashboardService,
|
||||
folderService: folderService,
|
||||
dataSourceCache: dataSourceCache,
|
||||
folderPermissions: folderPermissions,
|
||||
dashboardPermissions: dashboardPermissions,
|
||||
orgService: orgService,
|
||||
legacyAlertNotificationService: legacyAlertNotificationService,
|
||||
}, nil
|
||||
@ -325,155 +334,6 @@ func (ms *migrationStore) GetNotificationChannels(ctx context.Context, orgID int
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *migrationStore) GetFolder(ctx context.Context, cmd *folder.GetFolderQuery) (*folder.Folder, error) {
|
||||
return ms.folderService.Get(ctx, cmd)
|
||||
}
|
||||
|
||||
func (ms *migrationStore) CreateFolder(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
|
||||
return ms.folderService.Create(ctx, cmd)
|
||||
}
|
||||
|
||||
// based on SQLStore.GetDashboardACLInfoList()
|
||||
func (ms *migrationStore) GetACL(ctx context.Context, orgID, dashboardID int64) ([]*DashboardACL, error) {
|
||||
var err error
|
||||
|
||||
falseStr := ms.store.GetDialect().BooleanStr(false)
|
||||
|
||||
result := make([]*DashboardACL, 0)
|
||||
rawSQL := `
|
||||
-- get distinct permissions for the dashboard and its parent folder
|
||||
SELECT DISTINCT
|
||||
da.id,
|
||||
da.user_id,
|
||||
da.team_id,
|
||||
da.permission,
|
||||
da.role
|
||||
FROM dashboard as d
|
||||
LEFT JOIN dashboard folder on folder.id = d.folder_id
|
||||
LEFT JOIN dashboard_acl AS da ON
|
||||
da.dashboard_id = d.id OR
|
||||
da.dashboard_id = d.folder_id OR
|
||||
(
|
||||
-- include default permissions --
|
||||
da.org_id = -1 AND (
|
||||
(folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR
|
||||
(folder.id IS NULL AND d.has_acl = ` + falseStr + `)
|
||||
)
|
||||
)
|
||||
WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL
|
||||
ORDER BY da.id ASC
|
||||
`
|
||||
err = ms.store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
return sess.SQL(rawSQL, orgID, dashboardID).Find(&result)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// based on SQLStore.UpdateDashboardACL()
|
||||
// it should be called from inside a transaction
|
||||
func (ms *migrationStore) SetACL(ctx context.Context, orgID int64, dashboardID int64, items []*DashboardACL) error {
|
||||
if dashboardID <= 0 {
|
||||
return fmt.Errorf("folder id must be greater than zero for a folder permission")
|
||||
}
|
||||
return ms.store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
// userPermissionsMap is a map keeping the highest permission per user
|
||||
// for handling conficting inherited (folder) and non-inherited (dashboard) user permissions
|
||||
userPermissionsMap := make(map[int64]*DashboardACL, len(items))
|
||||
// teamPermissionsMap is a map keeping the highest permission per team
|
||||
// for handling conficting inherited (folder) and non-inherited (dashboard) team permissions
|
||||
teamPermissionsMap := make(map[int64]*DashboardACL, len(items))
|
||||
for _, item := range items {
|
||||
if item.UserID != 0 {
|
||||
acl, ok := userPermissionsMap[item.UserID]
|
||||
if !ok {
|
||||
userPermissionsMap[item.UserID] = item
|
||||
} else {
|
||||
if item.Permission > acl.Permission {
|
||||
// the higher permission wins
|
||||
userPermissionsMap[item.UserID] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if item.TeamID != 0 {
|
||||
acl, ok := teamPermissionsMap[item.TeamID]
|
||||
if !ok {
|
||||
teamPermissionsMap[item.TeamID] = item
|
||||
} else {
|
||||
if item.Permission > acl.Permission {
|
||||
// the higher permission wins
|
||||
teamPermissionsMap[item.TeamID] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type keyType struct {
|
||||
UserID int64 `xorm:"user_id"`
|
||||
TeamID int64 `xorm:"team_id"`
|
||||
Role RoleType
|
||||
Permission permissionType
|
||||
}
|
||||
// seen keeps track of inserted perrmissions to avoid duplicates (due to inheritance)
|
||||
seen := make(map[keyType]struct{}, len(items))
|
||||
for _, item := range items {
|
||||
if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) {
|
||||
return dashboards.ErrDashboardACLInfoMissing
|
||||
}
|
||||
|
||||
// ignore duplicate user permissions
|
||||
if item.UserID != 0 {
|
||||
acl, ok := userPermissionsMap[item.UserID]
|
||||
if ok {
|
||||
if acl.Id != item.Id {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ignore duplicate team permissions
|
||||
if item.TeamID != 0 {
|
||||
acl, ok := teamPermissionsMap[item.TeamID]
|
||||
if ok {
|
||||
if acl.Id != item.Id {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key := keyType{UserID: item.UserID, TeamID: item.TeamID, Role: "", Permission: item.Permission}
|
||||
if item.Role != nil {
|
||||
key.Role = *item.Role
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// unset Id so that the new record will get a different one
|
||||
item.Id = 0
|
||||
item.OrgID = orgID
|
||||
item.DashboardID = dashboardID
|
||||
item.Created = time.Now()
|
||||
item.Updated = time.Now()
|
||||
|
||||
sess.Nullable("user_id", "team_id")
|
||||
if _, err := sess.Insert(item); err != nil {
|
||||
return err
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
|
||||
// Update dashboard HasACL flag
|
||||
dashboard := dashboards.Dashboard{HasACL: true}
|
||||
_, err := sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard)
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// GetOrgDashboardAlerts loads all legacy dashboard alerts for the given org mapped by dashboard id.
|
||||
func (ms *migrationStore) GetOrgDashboardAlerts(ctx context.Context, orgID int64) (map[int64][]*DashAlert, int, error) {
|
||||
var alerts []legacymodels.Alert
|
||||
@ -506,6 +366,34 @@ func (ms *migrationStore) GetOrgDashboardAlerts(ctx context.Context, orgID int64
|
||||
return mappedAlerts, len(alerts), nil
|
||||
}
|
||||
|
||||
func (ms *migrationStore) GetDashboardPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error) {
|
||||
return ms.dashboardPermissions.GetPermissions(ctx, user, resourceID)
|
||||
}
|
||||
|
||||
func (ms *migrationStore) GetFolderPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error) {
|
||||
return ms.folderPermissions.GetPermissions(ctx, user, resourceID)
|
||||
}
|
||||
|
||||
func (ms *migrationStore) GetFolder(ctx context.Context, cmd *folder.GetFolderQuery) (*folder.Folder, error) {
|
||||
return ms.folderService.Get(ctx, cmd)
|
||||
}
|
||||
|
||||
func (ms *migrationStore) CreateFolder(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
|
||||
return ms.folderService.Create(ctx, cmd)
|
||||
}
|
||||
|
||||
func (ms *migrationStore) SetDashboardPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) {
|
||||
return ms.dashboardPermissions.SetPermissions(ctx, orgID, resourceID, commands...)
|
||||
}
|
||||
|
||||
func (ms *migrationStore) SetFolderPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) {
|
||||
return ms.folderPermissions.SetPermissions(ctx, orgID, resourceID, commands...)
|
||||
}
|
||||
|
||||
func (ms *migrationStore) MapActions(permission accesscontrol.ResourcePermission) string {
|
||||
return ms.dashboardPermissions.MapActions(permission)
|
||||
}
|
||||
|
||||
func (ms *migrationStore) CaseInsensitive() bool {
|
||||
return ms.store.GetDialect().SupportEngine()
|
||||
}
|
||||
|
@ -2,43 +2,10 @@ package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
)
|
||||
|
||||
type RoleType string
|
||||
|
||||
const (
|
||||
RoleNone RoleType = "None"
|
||||
RoleViewer RoleType = "Viewer"
|
||||
RoleEditor RoleType = "Editor"
|
||||
RoleAdmin RoleType = "Admin"
|
||||
)
|
||||
|
||||
func (r RoleType) IsValid() bool {
|
||||
return r == RoleViewer || r == RoleAdmin || r == RoleEditor || r == RoleNone
|
||||
}
|
||||
|
||||
type permissionType int
|
||||
|
||||
type DashboardACL struct {
|
||||
// nolint:stylecheck
|
||||
Id int64
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
DashboardID int64 `xorm:"dashboard_id"`
|
||||
|
||||
UserID int64 `xorm:"user_id"`
|
||||
TeamID int64 `xorm:"team_id"`
|
||||
Role *RoleType // pointer to be nullable
|
||||
Permission permissionType
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
func (p DashboardACL) TableName() string { return "dashboard_acl" }
|
||||
|
||||
// uidOrID for both uid and ID, primarily used for mapping legacy channel to migrated receiver.
|
||||
type UidOrID any
|
||||
|
||||
|
@ -1,26 +1,35 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||
legacyalerting "github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/datasources/guardian"
|
||||
datasourceService "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/testutil"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry"
|
||||
"github.com/grafana/grafana/pkg/services/team/teamimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@ -28,6 +37,8 @@ func NewTestMigrationStore(t *testing.T, sqlStore *sqlstore.SQLStore, cfg *setti
|
||||
if cfg.UnifiedAlerting.BaseInterval == 0 {
|
||||
cfg.UnifiedAlerting.BaseInterval = time.Second * 10
|
||||
}
|
||||
features := featuremgmt.WithFeatures()
|
||||
cfg.IsFeatureToggleEnabled = features.IsEnabled
|
||||
alertingStore := store.DBstore{
|
||||
SQLStore: sqlStore,
|
||||
Cfg: cfg.UnifiedAlerting,
|
||||
@ -37,11 +48,31 @@ func NewTestMigrationStore(t *testing.T, sqlStore *sqlstore.SQLStore, cfg *setti
|
||||
dashboardService, dashboardStore := testutil.SetupDashboardService(t, sqlStore, folderStore, cfg)
|
||||
folderService := testutil.SetupFolderService(t, cfg, sqlStore, dashboardStore, folderStore, bus)
|
||||
|
||||
cache := localcache.ProvideService()
|
||||
quotaService := "atest.FakeQuotaService{}
|
||||
orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService)
|
||||
ac := acimpl.ProvideAccessControl(cfg)
|
||||
routeRegister := routing.ProvideRegister()
|
||||
acSvc, err := acimpl.ProvideService(cfg, sqlStore, routing.ProvideRegister(), cache, ac, features)
|
||||
require.NoError(t, err)
|
||||
|
||||
license := licensingtest.NewFakeLicensing()
|
||||
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
|
||||
teamSvc := teamimpl.ProvideService(sqlStore, cfg)
|
||||
orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService)
|
||||
require.NoError(t, err)
|
||||
userSvc, err := userimpl.ProvideService(sqlStore, orgService, cfg, teamSvc, cache, quotaService, bundleregistry.ProvideService())
|
||||
require.NoError(t, err)
|
||||
|
||||
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
|
||||
features, routeRegister, sqlStore, ac, license, dashboardStore, folderService, acSvc, teamSvc, userSvc)
|
||||
require.NoError(t, err)
|
||||
dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions(
|
||||
features, routeRegister, sqlStore, ac, license, dashboardStore, folderService, acSvc, teamSvc, userSvc)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = acSvc.RegisterFixedRoles(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
cache := localcache.ProvideService()
|
||||
return &migrationStore{
|
||||
log: &logtest.Fake{},
|
||||
cfg: cfg,
|
||||
@ -51,6 +82,8 @@ func NewTestMigrationStore(t *testing.T, sqlStore *sqlstore.SQLStore, cfg *setti
|
||||
dashboardService: dashboardService,
|
||||
folderService: folderService,
|
||||
dataSourceCache: datasourceService.ProvideCacheService(cache, sqlStore, guardian.ProvideGuardian()),
|
||||
folderPermissions: folderPermissions,
|
||||
dashboardPermissions: dashboardPermissions,
|
||||
orgService: orgService,
|
||||
legacyAlertNotificationService: legacyalerting.ProvideService(sqlStore, encryptionservice.SetupTestService(t), nil),
|
||||
}
|
||||
|
@ -5,20 +5,23 @@ import (
|
||||
"fmt"
|
||||
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
|
||||
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
func (om *OrgMigration) migrateAlerts(ctx context.Context, alerts []*migrationStore.DashAlert, dashboardUID string, folderUID string) ([]*AlertPair, error) {
|
||||
func (om *OrgMigration) migrateAlerts(ctx context.Context, alerts []*migrationStore.DashAlert, info migmodels.DashboardUpgradeInfo) ([]*AlertPair, error) {
|
||||
log := om.log.New(
|
||||
"dashboardUID", dashboardUID,
|
||||
"newFolderUID", folderUID,
|
||||
"dashboardUid", info.DashboardUID,
|
||||
"dashboardName", info.DashboardName,
|
||||
"newFolderUid", info.NewFolderUID,
|
||||
"newFolderNane", info.NewFolderName,
|
||||
)
|
||||
|
||||
pairs := make([]*AlertPair, 0, len(alerts))
|
||||
for _, da := range alerts {
|
||||
al := log.New("ruleID", da.ID, "ruleName", da.Name)
|
||||
alertRule, err := om.migrateAlert(ctx, al, da, dashboardUID, folderUID)
|
||||
alertRule, err := om.migrateAlert(ctx, al, da, info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migrate alert: %w", err)
|
||||
}
|
||||
@ -29,12 +32,11 @@ func (om *OrgMigration) migrateAlerts(ctx context.Context, alerts []*migrationSt
|
||||
}
|
||||
|
||||
func (om *OrgMigration) migrateDashboard(ctx context.Context, dashID int64, alerts []*migrationStore.DashAlert) ([]*AlertPair, error) {
|
||||
dash, newFolder, err := om.getOrCreateMigratedFolder(ctx, om.log, dashID)
|
||||
info, err := om.migratedFolder(ctx, om.log, dashID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get or create migrated folder: %w", err)
|
||||
}
|
||||
|
||||
pairs, err := om.migrateAlerts(ctx, alerts, dash.UID, newFolder.UID)
|
||||
pairs, err := om.migrateAlerts(ctx, alerts, *info)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migrate and save alerts: %w", err)
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@ -106,13 +106,13 @@ const invalidUri = "<22>6<EFBFBD>M<EFBFBD><4D>)uk譹1(<28>h`$<24>o<EFBFBD>N>mĕ<6D><C495><EFBFBD><EFBFBD>cS2<53>dh
|
||||
|
||||
func Test_getAlertFolderNameFromDashboard(t *testing.T) {
|
||||
t.Run("should include full title", func(t *testing.T) {
|
||||
dash := &dashboards.Dashboard{
|
||||
UID: util.GenerateShortUID(),
|
||||
hash := util.GenerateShortUID()
|
||||
f := &folder.Folder{
|
||||
Title: "TEST",
|
||||
}
|
||||
folder := getAlertFolderNameFromDashboard(dash)
|
||||
require.Contains(t, folder, dash.Title)
|
||||
require.Contains(t, folder, dash.UID)
|
||||
name := generateAlertFolderName(f, permissionHash(hash))
|
||||
require.Contains(t, name, f.Title)
|
||||
require.Contains(t, name, hash)
|
||||
})
|
||||
t.Run("should cut title to the length", func(t *testing.T) {
|
||||
title := ""
|
||||
@ -124,13 +124,13 @@ func Test_getAlertFolderNameFromDashboard(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
dash := &dashboards.Dashboard{
|
||||
UID: util.GenerateShortUID(),
|
||||
hash := util.GenerateShortUID()
|
||||
f := &folder.Folder{
|
||||
Title: title,
|
||||
}
|
||||
folder := getAlertFolderNameFromDashboard(dash)
|
||||
require.Len(t, folder, MaxFolderName)
|
||||
require.Contains(t, folder, dash.UID)
|
||||
name := generateAlertFolderName(f, permissionHash(hash))
|
||||
require.Len(t, name, MaxFolderName)
|
||||
require.Contains(t, name, hash)
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user