mirror of https://github.com/grafana/grafana
Alerting: Include access control metadata in k8s receiver LIST & GET (#93013)
* Include access control metadata in k8s receiver List & Get * Add tests for receiver access * Simplify receiver access provisioning extension - prevents edge case infinite recursion - removes read requirement from createpull/93160/head
parent
0aa87fd1d4
commit
ff6a20f54a
@ -0,0 +1,351 @@ |
||||
package accesscontrol |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity" |
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/models" |
||||
"github.com/grafana/grafana/pkg/services/org" |
||||
) |
||||
|
||||
func TestReceiverAccess(t *testing.T) { |
||||
recv1 := models.ReceiverGen(models.ReceiverMuts.WithName("test receiver 1"), models.ReceiverMuts.WithValidIntegration("slack"))() |
||||
recv2 := models.ReceiverGen(models.ReceiverMuts.WithName("test receiver 2"), models.ReceiverMuts.WithValidIntegration("email"))() |
||||
recv3 := models.ReceiverGen(models.ReceiverMuts.WithName("test receiver 3"), models.ReceiverMuts.WithValidIntegration("webhook"))() |
||||
|
||||
allReceivers := []*models.Receiver{ |
||||
&recv1, |
||||
&recv2, |
||||
&recv3, |
||||
} |
||||
|
||||
permissions := func(perms ...models.ReceiverPermission) models.ReceiverPermissionSet { |
||||
set := models.NewReceiverPermissionSet() |
||||
for _, v := range models.ReceiverPermissions() { |
||||
set.Set(v, false) |
||||
} |
||||
for _, v := range perms { |
||||
set.Set(v, true) |
||||
} |
||||
return set |
||||
} |
||||
|
||||
testCases := []struct { |
||||
name string |
||||
user identity.Requester |
||||
expected map[string]models.ReceiverPermissionSet |
||||
expectedWithProvisioning map[string]models.ReceiverPermissionSet |
||||
}{ |
||||
// Legacy read.
|
||||
{ |
||||
name: "legacy global reader should have no elevated permissions", |
||||
user: newEmptyUser(ac.Permission{Action: ac.ActionAlertingNotificationsRead}), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "legacy global notifications provisioning reader should have no elevated permissions", |
||||
user: newEmptyUser(ac.Permission{Action: ac.ActionAlertingNotificationsProvisioningRead}), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "legacy global provisioning reader should have no elevated permissions", |
||||
user: newEmptyUser(ac.Permission{Action: ac.ActionAlertingProvisioningRead}), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "legacy global provisioning secret reader should have secret permissions on provisioning only", |
||||
user: newEmptyUser(ac.Permission{Action: ac.ActionAlertingProvisioningReadSecrets}), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(), |
||||
}, |
||||
expectedWithProvisioning: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionReadSecret), |
||||
recv2.UID: permissions(models.ReceiverPermissionReadSecret), |
||||
recv3.UID: permissions(models.ReceiverPermissionReadSecret), |
||||
}, |
||||
}, |
||||
// Receiver read.
|
||||
{ |
||||
name: "global receiver reader should have no elevated permissions", |
||||
user: newEmptyUser(ac.Permission{Action: ac.ActionAlertingReceiversRead, Scope: ScopeReceiversAll}), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "global receiver secret reader should have secret permissions", |
||||
user: newEmptyUser(ac.Permission{Action: ac.ActionAlertingReceiversReadSecrets, Scope: ScopeReceiversAll}), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionReadSecret), |
||||
recv2.UID: permissions(models.ReceiverPermissionReadSecret), |
||||
recv3.UID: permissions(models.ReceiverPermissionReadSecret), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "per-receiver secret reader should have per-receiver", |
||||
user: newEmptyUser( |
||||
ac.Permission{Action: ac.ActionAlertingReceiversReadSecrets, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversReadSecrets, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, |
||||
), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionReadSecret), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(models.ReceiverPermissionReadSecret), |
||||
}, |
||||
}, |
||||
// Legacy write.
|
||||
{ |
||||
name: "legacy global writer should have full write", |
||||
user: newViewUser(ac.Permission{Action: ac.ActionAlertingNotificationsWrite}), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionDelete), |
||||
recv2.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionDelete), |
||||
recv3.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionDelete), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "legacy writers should require read", |
||||
user: newEmptyUser(ac.Permission{Action: ac.ActionAlertingNotificationsWrite}), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(), |
||||
}, |
||||
}, |
||||
//{
|
||||
// name: "legacy global notifications provisioning writer should have full write on provisioning only",
|
||||
// user: newViewUser(ac.Permission{Action: ac.ActionAlertingNotificationsProvisioningWrite}),
|
||||
// expected: map[string]models.ReceiverPermissionSet{
|
||||
// recv1.UID: permissions(),
|
||||
// recv2.UID: permissions(),
|
||||
// recv3.UID: permissions(),
|
||||
// },
|
||||
// expectedWithProvisioning: map[string]models.ReceiverPermissionSet{
|
||||
// recv1.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionDelete),
|
||||
// recv2.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionDelete),
|
||||
// recv3.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionDelete),
|
||||
// },
|
||||
//},
|
||||
//{
|
||||
// name: "legacy global provisioning writer should have full write on provisioning only",
|
||||
// user: newViewUser(ac.Permission{Action: ac.ActionAlertingProvisioningWrite}),
|
||||
// expected: map[string]models.ReceiverPermissionSet{
|
||||
// recv1.UID: permissions(),
|
||||
// recv2.UID: permissions(),
|
||||
// recv3.UID: permissions(),
|
||||
// },
|
||||
// expectedWithProvisioning: map[string]models.ReceiverPermissionSet{
|
||||
// recv1.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionDelete),
|
||||
// recv2.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionDelete),
|
||||
// recv3.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionDelete),
|
||||
// },
|
||||
//},
|
||||
// Receiver create
|
||||
{ |
||||
name: "receiver create should not have write", |
||||
user: newEmptyUser(ac.Permission{Action: ac.ActionAlertingReceiversCreate}), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(), |
||||
}, |
||||
}, |
||||
// Receiver update.
|
||||
{ |
||||
name: "global receiver update should have write but no delete", |
||||
user: newViewUser(ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: ScopeReceiversAll}), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionWrite), |
||||
recv2.UID: permissions(models.ReceiverPermissionWrite), |
||||
recv3.UID: permissions(models.ReceiverPermissionWrite), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "per-receiver update should have per-receiver write but no delete", |
||||
user: newViewUser( |
||||
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, |
||||
), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionWrite), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(models.ReceiverPermissionWrite), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "per-receiver update should require read", |
||||
user: newEmptyUser( |
||||
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, |
||||
), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(), |
||||
}, |
||||
}, |
||||
// Receiver delete.
|
||||
{ |
||||
name: "global receiver delete should have delete but no write", |
||||
user: newViewUser(ac.Permission{Action: ac.ActionAlertingReceiversDelete, Scope: ScopeReceiversAll}), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionDelete), |
||||
recv2.UID: permissions(models.ReceiverPermissionDelete), |
||||
recv3.UID: permissions(models.ReceiverPermissionDelete), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "per-receiver delete should have per-receiver delete but no write", |
||||
user: newViewUser( |
||||
ac.Permission{Action: ac.ActionAlertingReceiversDelete, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversDelete, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, |
||||
), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionDelete), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(models.ReceiverPermissionDelete), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "per-receiver delete should require read", |
||||
user: newEmptyUser( |
||||
ac.Permission{Action: ac.ActionAlertingReceiversDelete, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversDelete, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, |
||||
), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(), |
||||
}, |
||||
}, |
||||
// Mixed permissions.
|
||||
{ |
||||
name: "legacy provisioning secret read, receiver write", |
||||
user: newViewUser( |
||||
ac.Permission{Action: ac.ActionAlertingProvisioningReadSecrets}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv2.UID)}, |
||||
), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(), |
||||
recv2.UID: permissions(models.ReceiverPermissionWrite), |
||||
recv3.UID: permissions(), |
||||
}, |
||||
expectedWithProvisioning: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionReadSecret), |
||||
recv2.UID: permissions(models.ReceiverPermissionReadSecret, models.ReceiverPermissionWrite), |
||||
recv3.UID: permissions(models.ReceiverPermissionReadSecret), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "legacy provisioning secret read, receiver delete", |
||||
user: newViewUser( |
||||
ac.Permission{Action: ac.ActionAlertingProvisioningReadSecrets}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversDelete, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv2.UID)}, |
||||
), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(), |
||||
recv2.UID: permissions(models.ReceiverPermissionDelete), |
||||
recv3.UID: permissions(), |
||||
}, |
||||
expectedWithProvisioning: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionReadSecret), |
||||
recv2.UID: permissions(models.ReceiverPermissionReadSecret, models.ReceiverPermissionDelete), |
||||
recv3.UID: permissions(models.ReceiverPermissionReadSecret), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "legacy write, receiver secret", |
||||
user: newViewUser( |
||||
ac.Permission{Action: ac.ActionAlertingNotificationsWrite}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversReadSecrets, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv2.UID)}, |
||||
), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionDelete), |
||||
recv2.UID: permissions(models.ReceiverPermissionReadSecret, models.ReceiverPermissionWrite, models.ReceiverPermissionDelete), |
||||
recv3.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionDelete), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "mixed secret / delete / write", |
||||
user: newViewUser( |
||||
ac.Permission{Action: ac.ActionAlertingReceiversReadSecrets, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversReadSecrets, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv2.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversDelete, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv2.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversDelete, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, |
||||
), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionReadSecret, models.ReceiverPermissionWrite), |
||||
recv2.UID: permissions(models.ReceiverPermissionWrite, models.ReceiverPermissionDelete), |
||||
recv3.UID: permissions(models.ReceiverPermissionReadSecret, models.ReceiverPermissionDelete), |
||||
}, |
||||
}, |
||||
{ |
||||
name: "mixed requires read", |
||||
user: newEmptyUser( |
||||
ac.Permission{Action: ac.ActionAlertingReceiversReadSecrets, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversReadSecrets, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv1.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversUpdate, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv2.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversDelete, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv2.UID)}, |
||||
ac.Permission{Action: ac.ActionAlertingReceiversDelete, Scope: ScopeReceiversProvider.GetResourceScopeUID(recv3.UID)}, |
||||
), |
||||
expected: map[string]models.ReceiverPermissionSet{ |
||||
recv1.UID: permissions(models.ReceiverPermissionReadSecret, models.ReceiverPermissionWrite), |
||||
recv2.UID: permissions(), |
||||
recv3.UID: permissions(models.ReceiverPermissionReadSecret, models.ReceiverPermissionDelete), |
||||
}, |
||||
}, |
||||
} |
||||
for _, testCase := range testCases { |
||||
t.Run(testCase.name, func(t *testing.T) { |
||||
svc := NewReceiverAccess[*models.Receiver](&recordingAccessControlFake{}, false) |
||||
|
||||
actual, err := svc.Access(context.Background(), testCase.user, allReceivers...) |
||||
|
||||
assert.NoError(t, err) |
||||
assert.Equalf(t, testCase.expected, actual, "expected: %v, actual: %v", testCase.expected, actual) |
||||
|
||||
provisioningPerms := testCase.expected |
||||
if testCase.expectedWithProvisioning != nil { |
||||
provisioningPerms = testCase.expectedWithProvisioning |
||||
} |
||||
svc = NewReceiverAccess[*models.Receiver](&recordingAccessControlFake{}, true) |
||||
actual, err = svc.Access(context.Background(), testCase.user, allReceivers...) |
||||
assert.NoError(t, err) |
||||
assert.Equalf(t, provisioningPerms, actual, "expectedWithProvisioning: %v, actual: %v", provisioningPerms, actual) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func newEmptyUser(permissions ...ac.Permission) identity.Requester { |
||||
return ac.BackgroundUser("test", orgID, org.RoleNone, permissions) |
||||
} |
||||
|
||||
func newViewUser(permissions ...ac.Permission) identity.Requester { |
||||
return ac.BackgroundUser("test", orgID, org.RoleNone, append([]ac.Permission{ |
||||
{Action: ac.ActionAlertingReceiversRead, Scope: ScopeReceiversAll}, |
||||
{Action: ac.ActionAlertingNotificationsRead}, |
||||
}, permissions...)) |
||||
} |
||||
@ -0,0 +1,75 @@ |
||||
package models |
||||
|
||||
import ( |
||||
"maps" |
||||
"slices" |
||||
) |
||||
|
||||
// ReceiverPermission is a type for representing permission to perform a receiver action.
|
||||
type ReceiverPermission string |
||||
|
||||
const ( |
||||
ReceiverPermissionReadSecret ReceiverPermission = "secrets" |
||||
//ReceiverPermissionAdmin ReceiverPermission = "admin" // TODO: Add when resource permissions are implemented.
|
||||
ReceiverPermissionWrite ReceiverPermission = "write" |
||||
ReceiverPermissionDelete ReceiverPermission = "delete" |
||||
) |
||||
|
||||
// ReceiverPermissions returns all possible silence permissions.
|
||||
func ReceiverPermissions() []ReceiverPermission { |
||||
return []ReceiverPermission{ |
||||
ReceiverPermissionReadSecret, |
||||
//ReceiverPermissionAdmin, // TODO: Add when resource permissions are implemented.
|
||||
ReceiverPermissionWrite, |
||||
ReceiverPermissionDelete, |
||||
} |
||||
} |
||||
|
||||
// ReceiverPermissionSet represents a set of permissions for a receiver.
|
||||
type ReceiverPermissionSet = PermissionSet[ReceiverPermission] |
||||
|
||||
func NewReceiverPermissionSet() ReceiverPermissionSet { |
||||
return NewPermissionSet(ReceiverPermissions()) |
||||
} |
||||
|
||||
// PermissionSet represents a set of permissions on a resource.
|
||||
type PermissionSet[T ~string] struct { |
||||
set map[T]bool |
||||
all []T |
||||
} |
||||
|
||||
func NewPermissionSet[T ~string](all []T) PermissionSet[T] { |
||||
return PermissionSet[T]{ |
||||
set: make(map[T]bool), |
||||
all: slices.Clone(all), |
||||
} |
||||
} |
||||
|
||||
// Clone returns a deep copy of the permission set.
|
||||
func (p PermissionSet[T]) Clone() PermissionSet[T] { |
||||
return PermissionSet[T]{ |
||||
set: maps.Clone(p.set), |
||||
all: p.all, |
||||
} |
||||
} |
||||
|
||||
// AllSet returns true if all possible permissions are set.
|
||||
func (p PermissionSet[T]) AllSet() bool { |
||||
for _, permission := range p.all { |
||||
if _, ok := p.set[permission]; !ok { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
// Has returns true if the given permission is allowed in the set.
|
||||
func (p PermissionSet[T]) Has(permission T) (bool, bool) { |
||||
allowed, ok := p.set[permission] |
||||
return allowed, ok |
||||
} |
||||
|
||||
// Set sets the given permission to the given allowed state.
|
||||
func (p PermissionSet[T]) Set(permission T, allowed bool) { |
||||
p.set[permission] = allowed |
||||
} |
||||
Loading…
Reference in new issue