grafana/pkg/services/ngalert/migration/ualert_test.go
Matthew Jacobson 5f48619c9a
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.
2023-10-12 18:12:40 -04:00

161 lines
4.5 KiB
Go
Raw Blame History

package migration
import (
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/folder"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/util"
)
func Test_validateAlertmanagerConfig(t *testing.T) {
tc := []struct {
name string
receivers []*apimodels.PostableGrafanaReceiver
err error
}{
{
name: "when a slack receiver does not have a valid URL - it should error",
receivers: []*apimodels.PostableGrafanaReceiver{
{
UID: "test-uid",
Name: "SlackWithBadURL",
Type: "slack",
Settings: mustRawMessage(map[string]any{}),
SecureSettings: map[string]string{"url": invalidUri},
},
},
err: fmt.Errorf("failed to validate integration \"SlackWithBadURL\" (UID test-uid) of type \"slack\": invalid URL %q", invalidUri),
},
{
name: "when a slack receiver has an invalid recipient - it should not error",
receivers: []*apimodels.PostableGrafanaReceiver{
{
UID: util.GenerateShortUID(),
Name: "SlackWithBadRecipient",
Type: "slack",
Settings: mustRawMessage(map[string]any{"recipient": "this passes"}),
SecureSettings: map[string]string{"url": "http://webhook.slack.com/myuser"},
},
},
},
{
name: "when the configuration is valid - it should not error",
receivers: []*apimodels.PostableGrafanaReceiver{
{
UID: util.GenerateShortUID(),
Name: "SlackWithBadURL",
Type: "slack",
Settings: mustRawMessage(map[string]interface{}{"recipient": "#a-good-channel"}),
SecureSettings: map[string]string{"url": "http://webhook.slack.com/myuser"},
},
},
},
}
sqlStore := db.InitTestDB(t)
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
service := NewTestMigrationService(t, sqlStore, nil)
mg := service.newOrgMigration(1)
config := configFromReceivers(t, tt.receivers)
require.NoError(t, encryptSecureSettings(config, mg)) // make sure we encrypt the settings
err := mg.validateAlertmanagerConfig(config)
if tt.err != nil {
require.Error(t, err)
require.EqualError(t, err, tt.err.Error())
} else {
require.NoError(t, err)
}
})
}
}
func configFromReceivers(t *testing.T, receivers []*apimodels.PostableGrafanaReceiver) *apimodels.PostableUserConfig {
t.Helper()
return &apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Receivers: []*apimodels.PostableApiReceiver{
{PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: receivers}},
},
},
}
}
func encryptSecureSettings(c *apimodels.PostableUserConfig, m *OrgMigration) error {
for _, r := range c.AlertmanagerConfig.Receivers {
for _, gr := range r.GrafanaManagedReceivers {
err := m.encryptSecureSettings(gr.SecureSettings)
if err != nil {
return err
}
}
}
return nil
}
const invalidUri = "<22>6<EFBFBD>M<EFBFBD><4D>)uk譹1(<28>h`$<24>o<EFBFBD>N>mĕ<6D><C495><EFBFBD><EFBFBD>cS2<53>dh![ę<> <09><><EFBFBD>`csB<73>!<21><>OSxP<78>{<7B>"
func Test_getAlertFolderNameFromDashboard(t *testing.T) {
t.Run("should include full title", func(t *testing.T) {
hash := util.GenerateShortUID()
f := &folder.Folder{
Title: "TEST",
}
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 := ""
for {
title += util.GenerateShortUID()
if len(title) > MaxFolderName {
title = title[:MaxFolderName]
break
}
}
hash := util.GenerateShortUID()
f := &folder.Folder{
Title: title,
}
name := generateAlertFolderName(f, permissionHash(hash))
require.Len(t, name, MaxFolderName)
require.Contains(t, name, hash)
})
}
func Test_shortUIDCaseInsensitiveConflicts(t *testing.T) {
s := Deduplicator{
set: make(map[string]struct{}),
caseInsensitive: true,
}
// 10000 uids seems to be enough to cause a collision in almost every run if using util.GenerateShortUID directly.
for i := 0; i < 10000; i++ {
s.add(util.GenerateShortUID())
}
// check if any are case-insensitive duplicates.
deduped := make(map[string]struct{})
for k := range s.set {
deduped[strings.ToLower(k)] = struct{}{}
}
require.Equal(t, len(s.set), len(deduped))
}
func mustRawMessage[T any](s T) apimodels.RawMessage {
js, _ := json.Marshal(s)
return js
}