grafana/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go

1532 lines
56 KiB
Go
Raw Normal View History

package receivers
import (
"context"
"embed"
"encoding/json"
"fmt"
"maps"
"net/http"
"path"
"slices"
"strings"
"testing"
"github.com/grafana/alerting/notify/notifytest"
"github.com/grafana/alerting/receivers/line"
"github.com/grafana/alerting/receivers/schema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"github.com/grafana/alerting/notify"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications/routingtree"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
alertingac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/api"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/tests/api/alerting"
"github.com/grafana/grafana/pkg/tests/apis"
test_common "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/testutil"
)
//go:embed test-data/*.*
var testData embed.FS
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func getTestHelper(t *testing.T) *apis.K8sTestHelper {
return apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{})
}
func TestIntegrationResourceIdentifier(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := getTestHelper(t)
client := test_common.NewReceiverClient(t, helper.Org1.Admin)
newResource := &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "Test-Receiver",
Integrations: []v0alpha1.ReceiverIntegration{},
},
}
t.Run("create should fail if object name is specified", func(t *testing.T) {
resource := newResource.Copy().(*v0alpha1.Receiver)
resource.Name = "new-receiver"
_, err := client.Create(ctx, resource, v1.CreateOptions{})
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
})
var resourceID string
t.Run("create should succeed and provide resource name", func(t *testing.T) {
actual, err := client.Create(ctx, newResource, v1.CreateOptions{})
require.NoError(t, err)
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
require.NotEmptyf(t, actual.UID, "Resource UID should not be empty")
resourceID = actual.Name
})
t.Run("resource should be available by the identifier", func(t *testing.T) {
actual, err := client.Get(ctx, resourceID, v1.GetOptions{})
require.NoError(t, err)
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
require.Equal(t, newResource.Spec, actual.Spec)
})
t.Run("update should rename receiver if name in the specification changes", func(t *testing.T) {
existing, err := client.Get(ctx, resourceID, v1.GetOptions{})
require.NoError(t, err)
updated := existing.Copy().(*v0alpha1.Receiver)
updated.Spec.Title = "another-newReceiver"
actual, err := client.Update(ctx, updated, v1.UpdateOptions{})
require.NoError(t, err)
require.Equal(t, updated.Spec, actual.Spec)
require.NotEqualf(t, updated.Name, actual.Name, "Update should change the resource name but it didn't")
require.NotEqualf(t, updated.ResourceVersion, actual.ResourceVersion, "Update should change the resource version but it didn't")
resource, err := client.Get(ctx, actual.Name, v1.GetOptions{})
require.NoError(t, err)
require.Equal(t, actual.Spec, resource.Spec)
require.Equal(t, actual.Name, resource.Name)
require.Equal(t, actual.ResourceVersion, resource.ResourceVersion)
})
}
// TestIntegrationResourcePermissions focuses on testing resource permissions for the alerting receiver resource. It
// verifies that access is correctly set when creating resources and assigning permissions to users, teams, and roles.
func TestIntegrationResourcePermissions(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := getTestHelper(t)
org1 := helper.Org1
noneUser := org1.None
creator := helper.CreateUser("creator", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(
accesscontrol.ActionAlertingReceiversCreate,
),
})
admin := org1.Admin
viewer := org1.Viewer
editor := org1.Editor
adminClient := test_common.NewReceiverClient(t, admin)
writeACMetadata := []string{"canWrite", "canDelete"}
allACMetadata := []string{"canWrite", "canDelete", "canReadSecrets", "canAdmin"}
mustID := func(user apis.User) int64 {
id, err := user.Identity.GetInternalID()
require.NoError(t, err)
return id
}
for _, tc := range []struct {
name string
creatingUser apis.User
testUser apis.User
assignments []accesscontrol.SetResourcePermissionCommand
expACMetadata []string
expRead bool
}{
// Basic access.
{
name: "Admin creates and has all metadata and access",
creatingUser: admin,
testUser: admin,
assignments: nil,
expACMetadata: allACMetadata,
expRead: true,
},
{
name: "Creator creates and has all metadata and access",
creatingUser: creator,
testUser: creator,
assignments: nil,
expACMetadata: allACMetadata,
expRead: true,
},
{
name: "Admin creates, noneUser has no metadata and no access",
creatingUser: admin,
testUser: noneUser,
assignments: nil,
expACMetadata: nil,
expRead: false,
},
{
name: "Admin creates, viewer has no metadata but has access",
creatingUser: admin,
testUser: viewer,
expACMetadata: nil,
expRead: true,
},
{
name: "Admin creates, editor has write metadata and access",
creatingUser: admin,
testUser: editor,
expACMetadata: writeACMetadata,
expRead: true,
},
// User-based assignments.
{
name: "Admin creates, assigns read, noneUser has no metadata but has access",
creatingUser: admin,
testUser: noneUser,
assignments: []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(noneUser), Permission: string(alertingac.ReceiverPermissionView)}},
expACMetadata: nil,
expRead: true,
},
{
name: "Admin creates, assigns write, noneUser has write metadata and access",
creatingUser: admin,
testUser: noneUser,
assignments: []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(noneUser), Permission: string(alertingac.ReceiverPermissionEdit)}},
expACMetadata: writeACMetadata,
expRead: true,
},
{
name: "Admin creates, assigns admin, noneUser has all metadata and access",
creatingUser: admin,
testUser: noneUser,
assignments: []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(noneUser), Permission: string(alertingac.ReceiverPermissionAdmin)}},
expACMetadata: allACMetadata,
expRead: true,
},
// Other users don't get assignments.
{
name: "Admin creates, assigns read to noneUser, creator has no metadata and no access",
creatingUser: admin,
testUser: creator,
assignments: []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(noneUser), Permission: string(alertingac.ReceiverPermissionView)}},
expACMetadata: nil,
expRead: false,
},
{
name: "Admin creates, assigns write to noneUser, creator has no metadata and no access",
creatingUser: admin,
testUser: creator,
assignments: []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(noneUser), Permission: string(alertingac.ReceiverPermissionEdit)}},
expACMetadata: nil,
expRead: false,
},
{
name: "Admin creates, assigns admin to noneUser, creator has no metadata and no access",
creatingUser: admin,
testUser: creator,
assignments: []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(noneUser), Permission: string(alertingac.ReceiverPermissionAdmin)}},
expACMetadata: nil,
expRead: false,
},
// Role-based access.
{
name: "Admin creates, assigns editor, viewer has write metadata and access",
creatingUser: admin,
testUser: viewer,
assignments: []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(viewer), Permission: string(alertingac.ReceiverPermissionEdit)}},
expACMetadata: writeACMetadata,
expRead: true,
},
{
name: "Admin creates, assigns admin, viewer has all metadata and access",
creatingUser: admin,
testUser: viewer,
assignments: []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(viewer), Permission: string(alertingac.ReceiverPermissionAdmin)}},
expACMetadata: allACMetadata,
expRead: true,
},
{
name: "Admin creates, assigns admin, editor has all metadata and access",
creatingUser: admin,
testUser: editor,
assignments: []accesscontrol.SetResourcePermissionCommand{{UserID: mustID(editor), Permission: string(alertingac.ReceiverPermissionAdmin)}},
expACMetadata: allACMetadata,
expRead: true,
},
// Team-based access. Staff team has editor+admin but not viewer in it.
{
name: "Admin creates, assigns admin to staff, viewer has no metadata and access",
creatingUser: admin,
testUser: viewer,
assignments: []accesscontrol.SetResourcePermissionCommand{{TeamID: org1.Staff.ID, Permission: string(alertingac.ReceiverPermissionAdmin)}},
expACMetadata: nil,
expRead: true,
},
{
name: "Admin creates, assigns admin to staff, editor has all metadata and access",
creatingUser: admin,
testUser: editor,
assignments: []accesscontrol.SetResourcePermissionCommand{{TeamID: org1.Staff.ID, Permission: string(alertingac.ReceiverPermissionAdmin)}},
expACMetadata: allACMetadata,
expRead: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
createClient := test_common.NewReceiverClient(t, tc.creatingUser)
client := test_common.NewReceiverClient(t, tc.testUser)
var created = &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "receiver-1",
Integrations: []v0alpha1.ReceiverIntegration{},
},
}
d, err := json.Marshal(created)
require.NoError(t, err)
// Create receiver with creatingUser
created, err = createClient.Create(ctx, created, v1.CreateOptions{})
require.NoErrorf(t, err, "Payload %s", string(d))
require.NotNil(t, created)
defer func() {
_ = adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
}()
// Assign resource permissions
cliCfg := helper.Org1.Admin.NewRestConfig()
alertingApi := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
for _, permission := range tc.assignments {
status, body := alertingApi.AssignReceiverPermission(t, created.Name, permission)
require.Equalf(t, http.StatusOK, status, "Expected status 200 but got %d: %s", status, body)
}
// Test read
if tc.expRead {
// Helper methods.
extractReceiverFromList := func(list *v0alpha1.ReceiverList, name string) *v0alpha1.Receiver {
for i := range list.Items {
if list.Items[i].Name == name {
return list.Items[i].Copy().(*v0alpha1.Receiver)
}
}
return nil
}
// Obtain expected responses using admin client as source of truth.
expectedGetWithMetadata, expectedListWithMetadata := func() (*v0alpha1.Receiver, *v0alpha1.Receiver) {
expectedGet, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, expectedGet)
// Set expected metadata.
expectedGetWithMetadata := expectedGet.Copy().(*v0alpha1.Receiver)
// Clear any existing access control metadata.
for _, k := range allACMetadata {
delete(expectedGetWithMetadata.Annotations, v0alpha1.AccessControlAnnotation(k))
}
for _, ac := range tc.expACMetadata {
expectedGetWithMetadata.SetAccessControl(ac)
}
expectedList, err := adminClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
expectedListWithMetadata := extractReceiverFromList(expectedList, created.Name)
require.NotNil(t, expectedListWithMetadata)
expectedListWithMetadata = expectedListWithMetadata.Copy().(*v0alpha1.Receiver)
// Clear any existing access control metadata.
for _, k := range allACMetadata {
delete(expectedListWithMetadata.Annotations, v0alpha1.AccessControlAnnotation(k))
}
for _, ac := range tc.expACMetadata {
expectedListWithMetadata.SetAccessControl(ac)
}
return expectedGetWithMetadata, expectedListWithMetadata
}()
t.Run("should be able to list receivers", func(t *testing.T) {
list, err := client.List(ctx, v1.ListOptions{})
require.NoError(t, err)
listedReceiver := extractReceiverFromList(list, created.Name)
assert.Equalf(t, expectedListWithMetadata, listedReceiver, "Expected %v but got %v", expectedListWithMetadata, listedReceiver)
})
t.Run("should be able to read receiver by resource identifier", func(t *testing.T) {
got, err := client.Get(ctx, expectedGetWithMetadata.Name, v1.GetOptions{})
require.NoError(t, err)
assert.Equalf(t, expectedGetWithMetadata, got, "Expected %v but got %v", expectedGetWithMetadata, got)
})
} else {
Alerting: Add manage permissions UI logic for Contact Points (#92885) * Add showPolicies prop * Add manage permissions component for easier reuse within alerting * Add method for checking whether to show access control within alerting * Remove accidental console.log from main * Tweak styling for contact point width and add manage permissions drawer * Improve typing for access control type response * Add basic test for manage permissions on contact points list * Only show manage permissions if grafana AM and alertingApiServer is enabled * Update i18n * Add test utils for turning features on and back off * Add access control handlers * Update tests with new util * Pass AM in and add tests * Receiver OSS resource permissions There is a complication that is not fully addressed: Viewer defaults to read:* and Editor defaults to read+write+delete:* This is different to other resource permissions where non-admin are not granted any global permissions and instead access is handled solely by resource-specific permissions that are populated on create and removed on delete. This allows them to easily remove permission to view or edit a single resource from basic roles. The reason this is tricky here is that we have multiple APIs that can create/delete receivers: config api, provisioning api, and k8s receivers api. Config api in particular is not well-equipped to determine when creates/deletes are happening and thus ensuring that the proper resource-specific permissions are created/deleted is finicky. We would also have to create a migration to populate resource-specific permissions for all current receivers. This migration would need to be reset so it can run again if the flag is disabled. * Add access control permissions * Pass in contact point ID to receivers form * Temporarily remove access control check for contact points * Include access control metadata in k8s receiver List & Get GET: Always included. LIST: Included by adding a label selector with value `grafana.com/accessControl` * Include new permissions for contact points navbar * Fix receiver creator fixed role to not give global read * Include in-use metadata in k8s receiver List & Get GET: Always included. LIST: Included by adding a label selector with value `grafana.com/inUse` * Add receiver creator permission to receiver writer * Add receiver creator permission to navbar * Always allow listing receivers, don't return 403 * Remove receiver read precondition from receiver create Otherwise, Creator role will not be able to create their first receiver * Update routes permissions * Add further support for RBAC in contact points * Update routes permissions * Update contact points header logic * Back out test feature toggle refactor Not working atm, not sure why * Tidy up imports * Update mock permissions * Revert more test changes * Update i18n * Sync inuse metadata pr * Add back canAdmin permissions after main merge * Split out check for policies navtree item * Tidy up utils and imports and fix rules in use * Fix contact point tests and act warnings * Add missing ReceiverPermissionAdmin after merge conflict * Move contact points permissions * Only show contact points filter when permissions are correct * Move to constants * Fallback to empty array and remove labelSelectors (not needed) * Allow `toAbility` to take multiple actions * Show builtin alertmanager if contact points permission * Add empty state and hide templates if missing permissions * Translations * Tidy up mock data * Fix tests and templates permission * Update message for unused contact points * Don't return 403 when user lists receivers and has access to none * Fix receiver create not adding empty uid permissions * Move SetDefaultPermissions to ReceiverPermissionService * Have SetDefaultPermissions use uid from string Fixes circular dependency * Add FakeReceiverPermissionsService and fix test wiring * Implement resource permission handling in provisioning API and renames Create: Sets to default permissions Delete: Removes permissions Update: If receiver name is modified and the new name doesn't exist, it copies the permissions from the old receiver to the newly created one. If old receiver is now empty, it removes the old permissions as well. * Split contact point permissions checks for read/modify * Generalise getting annotation values from k8s entities * Proxy RouteDeleteAlertingConfig through MultiOrgAlertmanager * Cleanup permissions on config api reset and restore * Cleanup permissions on config api POST note this is still not available with feature flag enabled * Gate the permission manager behind FF until initial migration is added * Sync changes from config api PR * Switch to named export * Revert unnecessary changes * Revert Filter auth change and implement in k8s api only * Don't allow new scoped permissions to give access without FF Prevents complications around mixed support for the scoped permissions causing oddities in the UI. * Fix integration tests to account for list permission change * Move to `permissions` file * Add additional tests for contact points * Fix redirect for viewer on edit page * Combine alerting test utils and move to new file location * Allow new permissions to access provisioning export paths with FF * Always allow exporting if its grafana flavoured * Fix logic for showing auto generated policies * Fix delete logic for contact point only referenced by a rule * Suppress warning message when renaming a contact point * Clear team and role perm cache on receiver rename Prevents temporarily broken UI permissions after rename when a user's source of elevated permissions comes from a cached team or basic role permission. * Debug log failed cache clear on CopyPermissions --------- Co-authored-by: Matt Jacobson <matthew.jacobson@grafana.com>
2024-09-28 02:56:32 +08:00
t.Run("list receivers should be empty", func(t *testing.T) {
list, err := client.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Emptyf(t, list.Items, "Expected no receivers but got %v", list.Items)
})
t.Run("should be forbidden to read receiver by name", func(t *testing.T) {
_, err := client.Get(ctx, created.Name, v1.GetOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
}
})
}
}
func TestIntegrationAccessControl(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := getTestHelper(t)
org1 := helper.Org1
type testCase struct {
user apis.User
canRead bool
canUpdate bool
canCreate bool
canDelete bool
canReadSecrets bool
canAdmin bool
}
// region users
unauthorized := helper.CreateUser("unauthorized", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{})
reader := helper.CreateUser("reader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(accesscontrol.ActionAlertingReceiversRead),
})
secretsReader := helper.CreateUser("secretsReader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(accesscontrol.ActionAlertingReceiversReadSecrets),
})
creator := helper.CreateUser("creator", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(
accesscontrol.ActionAlertingReceiversRead,
accesscontrol.ActionAlertingReceiversCreate,
),
})
updater := helper.CreateUser("updater", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(
accesscontrol.ActionAlertingReceiversRead,
accesscontrol.ActionAlertingReceiversUpdate,
),
})
deleter := helper.CreateUser("deleter", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(
accesscontrol.ActionAlertingReceiversRead,
accesscontrol.ActionAlertingReceiversDelete,
),
})
legacyReader := helper.CreateUser("legacyReader", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
{
Actions: []string{
accesscontrol.ActionAlertingNotificationsRead,
},
},
})
legacyWriter := helper.CreateUser("legacyWriter", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
{
Actions: []string{
accesscontrol.ActionAlertingNotificationsRead,
accesscontrol.ActionAlertingNotificationsWrite,
},
},
})
adminLikeUser := helper.CreateUser("adminLikeUser", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(append(
[]string{accesscontrol.ActionAlertingReceiversCreate},
ossaccesscontrol.ReceiversAdminActions...,
)...),
})
// Test receivers with uids longer than 40 characters. User name is used in receiver name.
adminLikeUserLongName := helper.CreateUser("adminLikeUserCreatingAReallyLongReceiverName", apis.Org1, org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{
createWildcardPermission(append(
[]string{accesscontrol.ActionAlertingReceiversCreate},
ossaccesscontrol.ReceiversAdminActions...,
)...),
})
// endregion
testCases := []testCase{
{
user: unauthorized,
canRead: false,
canUpdate: false,
canCreate: false,
canDelete: false,
},
{
user: org1.Admin,
canRead: true,
canCreate: true,
canUpdate: true,
canDelete: true,
canAdmin: true,
canReadSecrets: true,
},
{
user: org1.Editor,
canRead: true,
canUpdate: true,
canCreate: true,
canDelete: true,
},
{
user: org1.Viewer,
canRead: true,
},
{
user: reader,
canRead: true,
},
{
user: secretsReader,
canRead: true,
canReadSecrets: true,
},
{
user: creator,
canRead: true,
canCreate: true,
},
{
user: updater,
canRead: true,
canUpdate: true,
},
{
user: deleter,
canRead: true,
canDelete: true,
},
{
user: legacyReader,
canRead: true,
},
{
user: legacyWriter,
canRead: true,
canCreate: true,
canUpdate: true,
canDelete: true,
},
{
user: adminLikeUser,
canRead: true,
canCreate: true,
canUpdate: true,
canDelete: true,
canAdmin: true,
canReadSecrets: true,
},
{
user: adminLikeUserLongName,
canRead: true,
canCreate: true,
canUpdate: true,
canDelete: true,
canAdmin: true,
canReadSecrets: true,
},
}
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
for _, tc := range testCases {
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
client := test_common.NewReceiverClient(t, tc.user)
var expected = &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: fmt.Sprintf("receiver-1-%s", tc.user.Identity.GetLogin()),
Integrations: []v0alpha1.ReceiverIntegration{},
},
}
d, err := json.Marshal(expected)
require.NoError(t, err)
newReceiver := expected.Copy().(*v0alpha1.Receiver)
newReceiver.Spec.Title = fmt.Sprintf("receiver-2-%s", tc.user.Identity.GetLogin())
if tc.canCreate {
t.Run("should be able to create receiver", func(t *testing.T) {
actual, err := client.Create(ctx, newReceiver, v1.CreateOptions{})
require.NoErrorf(t, err, "Payload %s", string(d))
require.Equal(t, newReceiver.Spec, actual.Spec)
t.Run("should fail if already exists", func(t *testing.T) {
_, err := client.Create(ctx, newReceiver, v1.CreateOptions{})
require.Truef(t, errors.IsConflict(err), "expected bad request but got %s", err)
})
// Cleanup.
require.NoError(t, adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{}))
})
} else {
t.Run("should be forbidden to create", func(t *testing.T) {
_, err := client.Create(ctx, newReceiver, v1.CreateOptions{})
require.Truef(t, errors.IsForbidden(err), "Payload %s", string(d))
})
}
// create resource to proceed with other tests. We don't use the one created above because the user will always
// have admin permissions on it.
expected, err = adminClient.Create(ctx, expected, v1.CreateOptions{})
require.NoErrorf(t, err, "Payload %s", string(d))
require.NotNil(t, expected)
if tc.canRead {
// Set expected metadata.
expectedWithMetadata := expected.Copy().(*v0alpha1.Receiver)
expectedWithMetadata.SetInUse(0, nil)
expectedWithMetadata.SetCanUse(true)
if tc.canUpdate {
expectedWithMetadata.SetAccessControl("canWrite")
}
if tc.canDelete {
expectedWithMetadata.SetAccessControl("canDelete")
}
if tc.canReadSecrets {
expectedWithMetadata.SetAccessControl("canReadSecrets")
}
if tc.canAdmin {
expectedWithMetadata.SetAccessControl("canAdmin")
}
t.Run("should be able to list receivers", func(t *testing.T) {
list, err := client.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, list.Items, 2) // default + created
})
t.Run("should be able to read receiver by resource identifier", func(t *testing.T) {
got, err := client.Get(ctx, expected.Name, v1.GetOptions{})
require.NoError(t, err)
require.Equal(t, expectedWithMetadata, got)
t.Run("should get NotFound if resource does not exist", func(t *testing.T) {
_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
})
})
} else {
Alerting: Add manage permissions UI logic for Contact Points (#92885) * Add showPolicies prop * Add manage permissions component for easier reuse within alerting * Add method for checking whether to show access control within alerting * Remove accidental console.log from main * Tweak styling for contact point width and add manage permissions drawer * Improve typing for access control type response * Add basic test for manage permissions on contact points list * Only show manage permissions if grafana AM and alertingApiServer is enabled * Update i18n * Add test utils for turning features on and back off * Add access control handlers * Update tests with new util * Pass AM in and add tests * Receiver OSS resource permissions There is a complication that is not fully addressed: Viewer defaults to read:* and Editor defaults to read+write+delete:* This is different to other resource permissions where non-admin are not granted any global permissions and instead access is handled solely by resource-specific permissions that are populated on create and removed on delete. This allows them to easily remove permission to view or edit a single resource from basic roles. The reason this is tricky here is that we have multiple APIs that can create/delete receivers: config api, provisioning api, and k8s receivers api. Config api in particular is not well-equipped to determine when creates/deletes are happening and thus ensuring that the proper resource-specific permissions are created/deleted is finicky. We would also have to create a migration to populate resource-specific permissions for all current receivers. This migration would need to be reset so it can run again if the flag is disabled. * Add access control permissions * Pass in contact point ID to receivers form * Temporarily remove access control check for contact points * Include access control metadata in k8s receiver List & Get GET: Always included. LIST: Included by adding a label selector with value `grafana.com/accessControl` * Include new permissions for contact points navbar * Fix receiver creator fixed role to not give global read * Include in-use metadata in k8s receiver List & Get GET: Always included. LIST: Included by adding a label selector with value `grafana.com/inUse` * Add receiver creator permission to receiver writer * Add receiver creator permission to navbar * Always allow listing receivers, don't return 403 * Remove receiver read precondition from receiver create Otherwise, Creator role will not be able to create their first receiver * Update routes permissions * Add further support for RBAC in contact points * Update routes permissions * Update contact points header logic * Back out test feature toggle refactor Not working atm, not sure why * Tidy up imports * Update mock permissions * Revert more test changes * Update i18n * Sync inuse metadata pr * Add back canAdmin permissions after main merge * Split out check for policies navtree item * Tidy up utils and imports and fix rules in use * Fix contact point tests and act warnings * Add missing ReceiverPermissionAdmin after merge conflict * Move contact points permissions * Only show contact points filter when permissions are correct * Move to constants * Fallback to empty array and remove labelSelectors (not needed) * Allow `toAbility` to take multiple actions * Show builtin alertmanager if contact points permission * Add empty state and hide templates if missing permissions * Translations * Tidy up mock data * Fix tests and templates permission * Update message for unused contact points * Don't return 403 when user lists receivers and has access to none * Fix receiver create not adding empty uid permissions * Move SetDefaultPermissions to ReceiverPermissionService * Have SetDefaultPermissions use uid from string Fixes circular dependency * Add FakeReceiverPermissionsService and fix test wiring * Implement resource permission handling in provisioning API and renames Create: Sets to default permissions Delete: Removes permissions Update: If receiver name is modified and the new name doesn't exist, it copies the permissions from the old receiver to the newly created one. If old receiver is now empty, it removes the old permissions as well. * Split contact point permissions checks for read/modify * Generalise getting annotation values from k8s entities * Proxy RouteDeleteAlertingConfig through MultiOrgAlertmanager * Cleanup permissions on config api reset and restore * Cleanup permissions on config api POST note this is still not available with feature flag enabled * Gate the permission manager behind FF until initial migration is added * Sync changes from config api PR * Switch to named export * Revert unnecessary changes * Revert Filter auth change and implement in k8s api only * Don't allow new scoped permissions to give access without FF Prevents complications around mixed support for the scoped permissions causing oddities in the UI. * Fix integration tests to account for list permission change * Move to `permissions` file * Add additional tests for contact points * Fix redirect for viewer on edit page * Combine alerting test utils and move to new file location * Allow new permissions to access provisioning export paths with FF * Always allow exporting if its grafana flavoured * Fix logic for showing auto generated policies * Fix delete logic for contact point only referenced by a rule * Suppress warning message when renaming a contact point * Clear team and role perm cache on receiver rename Prevents temporarily broken UI permissions after rename when a user's source of elevated permissions comes from a cached team or basic role permission. * Debug log failed cache clear on CopyPermissions --------- Co-authored-by: Matt Jacobson <matthew.jacobson@grafana.com>
2024-09-28 02:56:32 +08:00
t.Run("list receivers should be empty", func(t *testing.T) {
list, err := client.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Emptyf(t, list.Items, "Expected no receivers but got %v", list.Items)
})
t.Run("should be forbidden to read receiver by name", func(t *testing.T) {
_, err := client.Get(ctx, expected.Name, v1.GetOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
t.Run("should get forbidden even if name does not exist", func(t *testing.T) {
_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
})
}
updatedExpected := expected.Copy().(*v0alpha1.Receiver)
updatedExpected.Spec.Integrations = append(updatedExpected.Spec.Integrations, createIntegration(t, "email"))
d, err = json.Marshal(updatedExpected)
require.NoError(t, err)
if tc.canUpdate {
t.Run("should be able to update receiver", func(t *testing.T) {
updated, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
require.NoErrorf(t, err, "Payload %s", string(d))
expected = updated
t.Run("should get NotFound if name does not exist", func(t *testing.T) {
up := updatedExpected.Copy().(*v0alpha1.Receiver)
up.Name = "notFound"
_, err := client.Update(ctx, up, v1.UpdateOptions{})
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
})
})
} else {
t.Run("should be forbidden to update receiver", func(t *testing.T) {
_, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
t.Run("should get forbidden even if resource does not exist", func(t *testing.T) {
up := updatedExpected.Copy().(*v0alpha1.Receiver)
up.Name = "notFound"
_, err := client.Update(ctx, up, v1.UpdateOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
})
}
deleteOptions := v1.DeleteOptions{Preconditions: &v1.Preconditions{ResourceVersion: util.Pointer(expected.ResourceVersion)}}
if tc.canDelete {
t.Run("should be able to delete receiver", func(t *testing.T) {
err := client.Delete(ctx, expected.Name, deleteOptions)
require.NoError(t, err)
t.Run("should get NotFound if name does not exist", func(t *testing.T) {
err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
})
})
} else {
t.Run("should be forbidden to delete receiver", func(t *testing.T) {
err := client.Delete(ctx, expected.Name, deleteOptions)
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
t.Run("should be forbidden even if resource does not exist", func(t *testing.T) {
err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
})
require.NoError(t, adminClient.Delete(ctx, expected.Name, v1.DeleteOptions{}))
}
if tc.canRead {
t.Run("should get empty list if no receivers", func(t *testing.T) {
list, err := client.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, list.Items, 1)
})
}
})
}
}
func TestIntegrationInUseMetadata(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := getTestHelper(t)
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
// Prepare environment and create notification policy and rule that use receiver
alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
require.NoError(t, err)
var amConfig definitions.PostableUserConfig
require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig))
// Add more references to the receiver in other routes.
route1 := *amConfig.AlertmanagerConfig.Route.Routes[0]
route1.Routes = nil
route2 := route1
parentRoute := *amConfig.AlertmanagerConfig.Route.Routes[0]
parentRoute.Routes = []*definitions.Route{&route1, &route2}
amConfig.AlertmanagerConfig.Route.Routes = append(amConfig.AlertmanagerConfig.Route.Routes, &parentRoute)
persistInitialConfig(t, amConfig)
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
require.NoError(t, err)
var ruleGroup definitions.PostableRuleGroupConfig
require.NoError(t, json.Unmarshal(postGroupRaw, &ruleGroup))
// Add more references to the receiver by creating adding same rule with a different title.
ruleGen := func() definitions.PostableGrafanaRule { return *ruleGroup.Rules[0].GrafanaManagedAlert }
rule2 := ruleGen()
rule2.Title = "Rule2"
rule2.NotificationSettings = &definitions.AlertRuleNotificationSettings{Receiver: "grafana-default-email"}
rule3 := ruleGen()
rule3.Title = "Rule3"
ruleGroup.Rules = append(ruleGroup.Rules,
definitions.PostableExtendedRuleNode{
ApiRuleNode: ruleGroup.Rules[0].ApiRuleNode,
GrafanaManagedAlert: &rule2,
},
definitions.PostableExtendedRuleNode{
ApiRuleNode: ruleGroup.Rules[0].ApiRuleNode,
GrafanaManagedAlert: &rule3,
},
)
folderUID := "test-folder"
legacyCli.CreateFolder(t, folderUID, "TEST")
_, status, data := legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false)
require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)
requestReceivers := func(t *testing.T, title string) (v0alpha1.Receiver, v0alpha1.Receiver) {
t.Helper()
receivers, err := adminClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, receivers.Items, 2)
idx := slices.IndexFunc(receivers.Items, func(interval v0alpha1.Receiver) bool {
return interval.Spec.Title == title
})
receiverListed := receivers.Items[idx]
receiverGet, err := adminClient.Get(ctx, receiverListed.Name, v1.GetOptions{})
require.NoError(t, err)
return receiverListed, *receiverGet
}
checkInUse := func(t *testing.T, receiverList, receiverGet v0alpha1.Receiver, routes, rules int) {
t.Helper()
assert.Equalf(t, fmt.Sprintf("%d", routes), receiverList.Annotations[v0alpha1.InUseAnnotation("routes")], "LIST: Expected %s used by %d routes", receiverList.Spec.Title, routes)
assert.Equalf(t, fmt.Sprintf("%d", rules), receiverList.Annotations[v0alpha1.InUseAnnotation("rules")], "LIST: Expected %s used by %d rules", receiverList.Spec.Title, rules)
assert.Equalf(t, fmt.Sprintf("%d", routes), receiverGet.Annotations[v0alpha1.InUseAnnotation("routes")], "GET: Expected %s used by %d routes", receiverGet.Spec.Title, routes)
assert.Equalf(t, fmt.Sprintf("%d", rules), receiverGet.Annotations[v0alpha1.InUseAnnotation("rules")], "GET: Expected %s used by %d rules", receiverGet.Spec.Title, rules)
}
receiverListed, receiverGet := requestReceivers(t, "user-defined")
checkInUse(t, receiverListed, receiverGet, 4, 2)
// Verify the default.
receiverListed, receiverGet = requestReceivers(t, "grafana-default-email")
checkInUse(t, receiverListed, receiverGet, 1, 1)
// Removing the new extra route should leave only 1.
amConfig.AlertmanagerConfig.Route.Routes = amConfig.AlertmanagerConfig.Route.Routes[:1]
v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
require.NoError(t, err)
routeAdminClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin)
_, err = routeAdminClient.Update(ctx, v1Route, v1.UpdateOptions{})
require.NoError(t, err)
receiverListed, receiverGet = requestReceivers(t, "user-defined")
checkInUse(t, receiverListed, receiverGet, 1, 2)
// Remove the extra rules.
ruleGroup.Rules = ruleGroup.Rules[:1]
_, status, data = legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false)
require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)
receiverListed, receiverGet = requestReceivers(t, "user-defined")
checkInUse(t, receiverListed, receiverGet, 1, 1)
receiverListed, receiverGet = requestReceivers(t, "grafana-default-email")
checkInUse(t, receiverListed, receiverGet, 1, 0)
// Remove the remaining routes.
amConfig.AlertmanagerConfig.Route.Routes = nil
v1route, err := routingtree.ConvertToK8sResource(1, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
require.NoError(t, err)
_, err = routeAdminClient.Update(ctx, v1route, v1.UpdateOptions{})
require.NoError(t, err)
// Remove the remaining rules.
ruleGroup.Rules = nil
_, status, data = legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false)
require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)
receiverListed, receiverGet = requestReceivers(t, "user-defined")
checkInUse(t, receiverListed, receiverGet, 0, 0)
receiverListed, receiverGet = requestReceivers(t, "grafana-default-email")
checkInUse(t, receiverListed, receiverGet, 1, 0)
}
func TestIntegrationProvisioning(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := getTestHelper(t)
org := helper.Org1
admin := org.Admin
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
require.NoError(t, err)
created, err := adminClient.Create(ctx, &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "test-receiver-1",
Integrations: []v0alpha1.ReceiverIntegration{
createIntegration(t, "email"),
},
},
}, v1.CreateOptions{})
require.NoError(t, err)
require.Equal(t, "none", created.GetProvenanceStatus())
t.Run("should provide provenance status", func(t *testing.T) {
require.NoError(t, db.SetProvenance(ctx, &definitions.EmbeddedContactPoint{
UID: *created.Spec.Integrations[0].Uid,
}, admin.Identity.GetOrgID(), "API"))
got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
require.Equal(t, "API", got.GetProvenanceStatus())
})
t.Run("should not let update if provisioned", func(t *testing.T) {
got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
updated := got.Copy().(*v0alpha1.Receiver)
updated.Spec.Integrations = append(updated.Spec.Integrations, createIntegration(t, "email"))
_, err = adminClient.Update(ctx, updated, v1.UpdateOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
t.Run("should not let delete if provisioned", func(t *testing.T) {
err := adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
})
}
func TestIntegrationOptimisticConcurrency(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := getTestHelper(t)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
receiver := v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "receiver-1",
Integrations: []v0alpha1.ReceiverIntegration{},
},
}
created, err := adminClient.Create(ctx, &receiver, v1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, created)
require.NotEmpty(t, created.ResourceVersion)
t.Run("should forbid if version does not match", func(t *testing.T) {
updated := created.Copy().(*v0alpha1.Receiver)
updated.ResourceVersion = "test"
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
})
t.Run("should update if version matches", func(t *testing.T) {
updated := created.Copy().(*v0alpha1.Receiver)
updated.Spec.Integrations = append(updated.Spec.Integrations, createIntegration(t, "email"))
actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
require.NoError(t, err)
for i, integration := range actualUpdated.Spec.Integrations {
updated.Spec.Integrations[i].Uid = integration.Uid
}
require.EqualValues(t, updated.Spec, actualUpdated.Spec)
require.NotEqual(t, updated.ResourceVersion, actualUpdated.ResourceVersion)
})
t.Run("should fail to update if version is empty", func(t *testing.T) {
updated := created.Copy().(*v0alpha1.Receiver)
updated.ResourceVersion = ""
updated.Spec.Integrations = append(updated.Spec.Integrations, createIntegration(t, "webhook"))
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err) // TODO Change that? K8s returns 400 instead.
})
t.Run("should fail to delete if version does not match", func(t *testing.T) {
actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
Preconditions: &v1.Preconditions{
ResourceVersion: util.Pointer("something"),
},
})
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
})
t.Run("should succeed if version matches", func(t *testing.T) {
actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
Preconditions: &v1.Preconditions{
ResourceVersion: util.Pointer(actual.ResourceVersion),
},
})
require.NoError(t, err)
})
t.Run("should succeed if version is empty", func(t *testing.T) {
actual, err := adminClient.Create(ctx, &receiver, v1.CreateOptions{})
require.NoError(t, err)
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
Preconditions: &v1.Preconditions{
ResourceVersion: util.Pointer(actual.ResourceVersion),
},
})
require.NoError(t, err)
})
}
func TestIntegrationPatch(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := getTestHelper(t)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
receiver := v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "receiver",
Integrations: []v0alpha1.ReceiverIntegration{
createIntegration(t, "email"),
createIntegration(t, "webhook"),
createIntegration(t, "sns"),
},
},
}
current, err := adminClient.Create(ctx, &receiver, v1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, current)
t.Run("should patch with json patch", func(t *testing.T) {
current, err := adminClient.Get(ctx, current.Name, v1.GetOptions{})
require.NoError(t, err)
index := slices.IndexFunc(current.Spec.Integrations, func(t v0alpha1.ReceiverIntegration) bool {
return t.Type == "webhook"
})
patch := []map[string]any{
{
"op": "remove",
"path": fmt.Sprintf("/spec/integrations/%d/settings/username", index),
},
{
"op": "remove",
"path": fmt.Sprintf("/spec/integrations/%d/secureFields/password", index),
},
{
"op": "replace",
"path": fmt.Sprintf("/spec/integrations/%d/settings/authorization_scheme", index),
"value": "bearer",
},
{
"op": "add",
"path": fmt.Sprintf("/spec/integrations/%d/settings/authorization_credentials", index),
"value": "authz-token",
},
{
"op": "remove",
"path": fmt.Sprintf("/spec/integrations/%d/secureFields/authorization_credentials", index),
},
}
expected := current.Spec.Integrations[index]
delete(expected.Settings, "username")
delete(expected.Settings, "password")
expected.Settings["authorization_scheme"] = "bearer"
delete(expected.SecureFields, "password")
expected.SecureFields["authorization_credentials"] = true
patchData, err := json.Marshal(patch)
require.NoError(t, err)
result, err := adminClient.Patch(ctx, current.Name, types.JSONPatchType, patchData, v1.PatchOptions{})
require.NoError(t, err)
require.EqualValues(t, expected, result.Spec.Integrations[index])
// Use export endpoint because it's the only way to get decrypted secrets fast.
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
export := legacyCli.ExportReceiverTyped(t, current.Spec.Title, true)
// we need special unmarshaler to parse settings
cp, err := api.ContactPointFromContactPointExport(export)
require.NoError(t, err)
assert.Len(t, cp.Sns, 1)
assert.Len(t, cp.Email, 1)
require.Len(t, cp.Webhook, 1)
settings := cp.Webhook[0]
assert.EqualValues(t, "authz-token", *settings.AuthorizationCredentials)
assert.EqualValues(t, "bearer", *settings.AuthorizationScheme)
assert.Nil(t, settings.Password)
assert.Nil(t, settings.User)
})
}
func TestIntegrationReferentialIntegrity(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := getTestHelper(t)
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
require.NoError(t, err)
orgID := helper.Org1.Admin.Identity.GetOrgID()
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
// Prepare environment and create notification policy and rule that use time receiver
alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
require.NoError(t, err)
var amConfig definitions.PostableUserConfig
require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig))
persistInitialConfig(t, amConfig)
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
require.NoError(t, err)
var ruleGroup definitions.PostableRuleGroupConfig
require.NoError(t, json.Unmarshal(postGroupRaw, &ruleGroup))
folderUID := "test-folder"
legacyCli.CreateFolder(t, folderUID, "TEST")
_, status, data := legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false)
require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)
receivers, err := adminClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, receivers.Items, 2)
idx := slices.IndexFunc(receivers.Items, func(interval v0alpha1.Receiver) bool {
return interval.Spec.Title == "user-defined"
})
receiver := receivers.Items[idx]
currentRoute := legacyCli.GetRoute(t)
currentRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
require.Equal(t, http.StatusAccepted, status)
t.Run("Update", func(t *testing.T) {
t.Run("should rename all references if name changes", func(t *testing.T) {
renamed := receiver.Copy().(*v0alpha1.Receiver)
expectedTitle := renamed.Spec.Title + "-new"
renamed.Spec.Title = expectedTitle
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
require.NoError(t, err)
updatedRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
require.Equal(t, http.StatusAccepted, status)
for idx, rule := range updatedRuleGroup.Rules {
assert.Equalf(t, expectedTitle, rule.GrafanaManagedAlert.NotificationSettings.Receiver, "receiver in rule %d should have been renamed but it did not", idx)
}
updatedRoute := legacyCli.GetRoute(t)
for _, route := range updatedRoute.Routes {
assert.Equalf(t, expectedTitle, route.Receiver, "time receiver in routes should have been renamed but it did not")
}
actual, err = adminClient.Get(ctx, actual.Name, v1.GetOptions{})
require.NoError(t, err)
receiver = *actual
})
t.Run("should fail if at least one resource is provisioned", func(t *testing.T) {
require.NoError(t, err)
renamed := receiver.Copy().(*v0alpha1.Receiver)
renamed.Spec.Title += util.GenerateShortUID()
t.Run("provisioned route", func(t *testing.T) {
require.NoError(t, db.SetProvenance(ctx, &currentRoute, orgID, "API"))
t.Cleanup(func() {
require.NoError(t, db.DeleteProvenance(ctx, &currentRoute, orgID))
})
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
require.Errorf(t, err, "Expected error but got successful result: %v", actual)
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
})
t.Run("provisioned rules", func(t *testing.T) {
ruleUid := currentRuleGroup.Rules[0].GrafanaManagedAlert.UID
resource := &ngmodels.AlertRule{UID: ruleUid}
require.NoError(t, db.SetProvenance(ctx, resource, orgID, "API"))
t.Cleanup(func() {
require.NoError(t, db.DeleteProvenance(ctx, resource, orgID))
})
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
require.Errorf(t, err, "Expected error but got successful result: %v", actual)
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
})
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("should fail to delete if receiver is used in rule and routes", func(t *testing.T) {
err := adminClient.Delete(ctx, receiver.Name, v1.DeleteOptions{})
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
})
t.Run("should fail to delete if receiver is used in only rule", func(t *testing.T) {
route := legacyCli.GetRoute(t)
route.Routes[0].Receiver = ""
legacyCli.UpdateRoute(t, route, true)
err = adminClient.Delete(ctx, receiver.Name, v1.DeleteOptions{})
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
})
})
}
func TestIntegrationCRUD(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := getTestHelper(t)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
var defaultReceiver *v0alpha1.Receiver
t.Run("should list the default receiver", func(t *testing.T) {
items, err := adminClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
assert.Len(t, items.Items, 1)
defaultReceiver = &items.Items[0]
assert.Equal(t, "grafana-default-email", defaultReceiver.Spec.Title)
assert.NotEmpty(t, defaultReceiver.UID)
assert.NotEmpty(t, defaultReceiver.Name)
assert.NotEmpty(t, defaultReceiver.ResourceVersion)
defaultReceiver, err = adminClient.Get(ctx, defaultReceiver.Name, v1.GetOptions{})
require.NoError(t, err)
assert.NotEmpty(t, defaultReceiver.UID)
assert.NotEmpty(t, defaultReceiver.Name)
assert.NotEmpty(t, defaultReceiver.ResourceVersion)
assert.Len(t, defaultReceiver.Spec.Integrations, 1)
})
t.Run("should be able to update default receiver", func(t *testing.T) {
require.NotNil(t, defaultReceiver)
newDefault := defaultReceiver.Copy().(*v0alpha1.Receiver)
newDefault.Spec.Integrations = append(newDefault.Spec.Integrations, createIntegration(t, line.Type))
updatedReceiver, err := adminClient.Update(ctx, newDefault, v1.UpdateOptions{})
require.NoError(t, err)
expected := newDefault.Copy().(*v0alpha1.Receiver)
expected.Spec.Integrations[0].Uid = updatedReceiver.Spec.Integrations[0].Uid // default integration does not have UID before first update
lineIntegration := expected.Spec.Integrations[1]
lineIntegration.SecureFields = map[string]bool{
"token": true,
}
delete(lineIntegration.Settings, "token")
assert.Equal(t, "LINE", updatedReceiver.Spec.Integrations[1].Type) // this type is in the schema but not in backend
lineIntegration.Type = "LINE"
lineIntegration.Uid = updatedReceiver.Spec.Integrations[1].Uid
expected.Spec.Integrations[1] = lineIntegration
assert.Equal(t, expected.Spec, updatedReceiver.Spec)
})
t.Run("should fail to create receiver with the existing name", func(t *testing.T) {
newReceiver := &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: defaultReceiver.Spec.Title,
Integrations: []v0alpha1.ReceiverIntegration{},
},
}
_, err := adminClient.Create(ctx, newReceiver, v1.CreateOptions{})
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
})
t.Run("should not let delete default receiver", func(t *testing.T) {
err := adminClient.Delete(ctx, defaultReceiver.Name, v1.DeleteOptions{})
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
})
var receiver *v0alpha1.Receiver
t.Run("should correctly persist all known integrations", func(t *testing.T) {
integrations := make([]v0alpha1.ReceiverIntegration, 0, len(notifytest.AllKnownV1ConfigsForTesting))
keysIter := maps.Keys(notifytest.AllKnownV1ConfigsForTesting)
keys := slices.Collect(keysIter)
slices.Sort(keys)
for _, key := range keys {
integrations = append(integrations, createIntegration(t, key))
}
var err error
receiver, err = adminClient.Create(ctx, &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "all-receivers",
Integrations: integrations,
},
}, v1.CreateOptions{})
require.NoError(t, err)
require.Len(t, receiver.Spec.Integrations, len(integrations))
// Set expected metadata
receiver.SetAccessControl("canWrite")
receiver.SetAccessControl("canDelete")
receiver.SetAccessControl("canReadSecrets")
receiver.SetAccessControl("canAdmin")
receiver.SetInUse(0, nil)
receiver.SetCanUse(true)
// Use export endpoint because it's the only way to get decrypted secrets fast.
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
export := legacyCli.ExportReceiverTyped(t, receiver.Spec.Title, true)
for _, integration := range export.Receivers {
expected := notifytest.AllKnownV1ConfigsForTesting[schema.IntegrationType(integration.Type)]
assert.JSONEqf(t, expected.Config, string(integration.Settings), "integration %s", integration.Type)
}
})
t.Run("should be able read what it is created", func(t *testing.T) {
get, err := adminClient.Get(ctx, receiver.Name, v1.GetOptions{})
require.NoError(t, err)
require.Equal(t, receiver, get)
t.Run("should return secrets in secureFields but not settings", func(t *testing.T) {
for _, integration := range get.Spec.Integrations {
integrationType := schema.IntegrationType(integration.Type)
t.Run(integration.Type, func(t *testing.T) {
expected := notifytest.AllKnownV1ConfigsForTesting[schema.IntegrationType(integration.Type)]
var fields map[string]any
require.NoError(t, json.Unmarshal([]byte(expected.Config), &fields))
typeSchema, ok := notify.GetSchemaVersionForIntegration(integrationType, schema.V1)
require.True(t, ok)
secretFields := typeSchema.GetSecretFieldsPaths()
for _, fieldPath := range secretFields {
field := fieldPath.String()
if _, ok := fields[field]; !ok { // skip field that is not in the original setting
continue
}
assert.Contains(t, integration.SecureFields, field)
assert.Truef(t, integration.SecureFields[field], "secure field should be always true")
value, ok, err := unstructured.NestedString(integration.Settings, strings.Split(field, ".")...)
assert.NoErrorf(t, err, "failed to read field %s from settings", field)
assert.Falsef(t, ok, "secret field %s should not be in settings, value [%s]", field, value)
}
})
}
})
})
t.Run("should fail to persist receiver with invalid config", func(t *testing.T) {
keysIter := maps.Keys(notifytest.AllKnownV1ConfigsForTesting)
keys := slices.Collect(keysIter)
slices.Sort(keys)
for _, key := range keys {
t.Run(string(key), func(t *testing.T) {
integration := createIntegration(t, key)
// Make the integration invalid, so it fails to create. This is usually done by sending empty settings.
clear(integration.Settings)
if key == "webex" {
// Webex integration is special case and passes validation without any settings so we instead set an invalid URL.
integration.Settings["api_url"] = "(*^$*^%!@#$*()"
}
receiver, err := adminClient.Create(ctx, &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: fmt.Sprintf("invalid-%s", key),
Integrations: []v0alpha1.ReceiverIntegration{integration},
},
}, v1.CreateOptions{})
require.Errorf(t, err, "Expected error but got successful result: %v", receiver)
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest, got: %s", err)
})
}
})
}
func TestIntegrationReceiverListSelector(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := getTestHelper(t)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
recv1 := &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "test-receiver-1",
Integrations: []v0alpha1.ReceiverIntegration{
createIntegration(t, "email"),
},
},
}
recv1, err := adminClient.Create(ctx, recv1, v1.CreateOptions{})
require.NoError(t, err)
recv2 := &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: "test-receiver-2",
Integrations: []v0alpha1.ReceiverIntegration{
createIntegration(t, "email"),
},
},
}
recv2, err = adminClient.Create(ctx, recv2, v1.CreateOptions{})
require.NoError(t, err)
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
require.NoError(t, err)
require.NoError(t, db.SetProvenance(ctx, &definitions.EmbeddedContactPoint{
UID: *recv2.Spec.Integrations[0].Uid,
}, helper.Org1.Admin.Identity.GetOrgID(), "API"))
recv2, err = adminClient.Get(ctx, recv2.Name, v1.GetOptions{})
require.NoError(t, err)
receivers, err := adminClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, receivers.Items, 3) // Includes default.
t.Run("should filter by receiver name", func(t *testing.T) {
t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported
list, err := adminClient.List(ctx, v1.ListOptions{
FieldSelector: "spec.title=" + recv1.Spec.Title,
})
require.NoError(t, err)
require.Len(t, list.Items, 1)
require.Equal(t, recv1.Name, list.Items[0].Name)
})
t.Run("should filter by metadata name", func(t *testing.T) {
list, err := adminClient.List(ctx, v1.ListOptions{
FieldSelector: "metadata.name=" + recv2.Name,
})
require.NoError(t, err)
require.Len(t, list.Items, 1)
require.Equal(t, recv2.Name, list.Items[0].Name)
})
t.Run("should filter by multiple filters", func(t *testing.T) {
t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported
list, err := adminClient.List(ctx, v1.ListOptions{
FieldSelector: fmt.Sprintf("metadata.name=%s,spec.title=%s", recv2.Name, recv2.Spec.Title),
})
require.NoError(t, err)
require.Len(t, list.Items, 1)
require.Equal(t, recv2.Name, list.Items[0].Name)
})
t.Run("should be empty when filter does not match", func(t *testing.T) {
list, err := adminClient.List(ctx, v1.ListOptions{
FieldSelector: fmt.Sprintf("metadata.name=%s", "unknown"),
})
require.NoError(t, err)
require.Empty(t, list.Items)
})
}
// persistInitialConfig helps create an initial config with new receivers using legacy json. Config API blocks receiver
// modifications, so we need to use k8s API to create new receivers before posting the config.
func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig) {
ctx := context.Background()
helper := getTestHelper(t)
receiverClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
for _, receiver := range amConfig.AlertmanagerConfig.Receivers {
if receiver.Name == "grafana-default-email" {
continue
}
toCreate := v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1.ReceiverSpec{
Title: receiver.Name,
Integrations: []v0alpha1.ReceiverIntegration{},
},
}
for _, integration := range receiver.GrafanaManagedReceivers {
settings := common.Unstructured{}
require.NoError(t, settings.UnmarshalJSON(integration.Settings))
toCreate.Spec.Integrations = append(toCreate.Spec.Integrations, v0alpha1.ReceiverIntegration{
Settings: settings.Object,
Type: integration.Type,
DisableResolveMessage: util.Pointer(false),
})
}
created, err := receiverClient.Create(ctx, &toCreate, v1.CreateOptions{})
require.NoError(t, err)
for i, integration := range created.Spec.Integrations {
receiver.GrafanaManagedReceivers[i].UID = *integration.Uid
}
}
nsMapper := func(_ int64) string { return "default" }
routeClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin)
v1route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", nsMapper)
require.NoError(t, err)
_, err = routeClient.Update(ctx, v1route, v1.UpdateOptions{})
require.NoError(t, err)
}
func createIntegration(t *testing.T, integrationType schema.IntegrationType) v0alpha1.ReceiverIntegration {
cfg, ok := notifytest.AllKnownV1ConfigsForTesting[integrationType]
require.Truef(t, ok, "no known config for integration type %s", integrationType)
return createIntegrationWithSettings(t, integrationType, schema.V1, cfg.Config)
}
func createIntegrationWithSettings(t *testing.T, integrationType schema.IntegrationType, integrationVersion schema.Version, settingsJson string) v0alpha1.ReceiverIntegration {
settings := common.Unstructured{}
require.NoError(t, settings.UnmarshalJSON([]byte(settingsJson)))
return v0alpha1.ReceiverIntegration{
Settings: settings.Object,
Type: string(integrationType),
Version: string(integrationVersion),
DisableResolveMessage: util.Pointer(false),
}
}
func createWildcardPermission(actions ...string) resourcepermissions.SetResourcePermissionCommand {
return resourcepermissions.SetResourcePermissionCommand{
Actions: actions,
Resource: "receivers",
ResourceAttribute: "uid",
ResourceID: "*",
}
}