mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 18:34:52 -06:00
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 create
This commit is contained in:
parent
0aa87fd1d4
commit
ff6a20f54a
@ -1,6 +1,9 @@
|
||||
package v0alpha1
|
||||
|
||||
const ProvenanceStatusAnnotationKey = "grafana.com/provenance"
|
||||
import "fmt"
|
||||
|
||||
const InternalPrefix = "grafana.com/"
|
||||
const ProvenanceStatusAnnotationKey = InternalPrefix + "provenance"
|
||||
const ProvenanceStatusNone = "none"
|
||||
|
||||
func (o *TimeInterval) GetProvenanceStatus() string {
|
||||
@ -44,3 +47,16 @@ func (o *Receiver) SetProvenanceStatus(status string) {
|
||||
}
|
||||
o.Annotations[ProvenanceStatusAnnotationKey] = status
|
||||
}
|
||||
|
||||
func (o *Receiver) SetAccessControl(action string) {
|
||||
if o.Annotations == nil {
|
||||
o.Annotations = make(map[string]string, 1)
|
||||
}
|
||||
o.Annotations[AccessControlAnnotation(action)] = "true"
|
||||
}
|
||||
|
||||
// AccessControlAnnotation returns the key for the access control annotation for the given action.
|
||||
// Ex. grafana.com/access/canDelete.
|
||||
func AccessControlAnnotation(action string) string {
|
||||
return fmt.Sprintf("%s%s/%s", InternalPrefix, "access", action)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package receiver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@ -14,12 +15,18 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
||||
)
|
||||
|
||||
func convertToK8sResources(orgID int64, receivers []*ngmodels.Receiver, namespacer request.NamespaceMapper, selector fields.Selector) (*model.ReceiverList, error) {
|
||||
func convertToK8sResources(orgID int64, receivers []*ngmodels.Receiver, accesses map[string]ngmodels.ReceiverPermissionSet, namespacer request.NamespaceMapper, selector fields.Selector) (*model.ReceiverList, error) {
|
||||
result := &model.ReceiverList{
|
||||
Items: make([]model.Receiver, 0, len(receivers)),
|
||||
}
|
||||
for _, receiver := range receivers {
|
||||
k8sResource, err := convertToK8sResource(orgID, receiver, namespacer)
|
||||
var access *ngmodels.ReceiverPermissionSet
|
||||
if accesses != nil {
|
||||
if a, ok := accesses[receiver.GetUID()]; ok {
|
||||
access = &a
|
||||
}
|
||||
}
|
||||
k8sResource, err := convertToK8sResource(orgID, receiver, access, namespacer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -31,7 +38,7 @@ func convertToK8sResources(orgID int64, receivers []*ngmodels.Receiver, namespac
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func convertToK8sResource(orgID int64, receiver *ngmodels.Receiver, namespacer request.NamespaceMapper) (*model.Receiver, error) {
|
||||
func convertToK8sResource(orgID int64, receiver *ngmodels.Receiver, access *ngmodels.ReceiverPermissionSet, namespacer request.NamespaceMapper) (*model.Receiver, error) {
|
||||
spec := model.ReceiverSpec{
|
||||
Title: receiver.Name,
|
||||
}
|
||||
@ -56,9 +63,29 @@ func convertToK8sResource(orgID int64, receiver *ngmodels.Receiver, namespacer r
|
||||
Spec: spec,
|
||||
}
|
||||
r.SetProvenanceStatus(string(receiver.Provenance))
|
||||
|
||||
if access != nil {
|
||||
for _, action := range ngmodels.ReceiverPermissions() {
|
||||
mappedAction, ok := permissionMapper[action]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown action %v", action)
|
||||
}
|
||||
if can, _ := access.Has(action); can {
|
||||
r.SetAccessControl(mappedAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
var permissionMapper = map[ngmodels.ReceiverPermission]string{
|
||||
ngmodels.ReceiverPermissionReadSecret: "canReadSecrets",
|
||||
//ngmodels.ReceiverPermissionAdmin: "canAdmin", // TODO: Add when resource permissions are implemented.
|
||||
ngmodels.ReceiverPermissionWrite: "canWrite",
|
||||
ngmodels.ReceiverPermissionDelete: "canDelete",
|
||||
}
|
||||
|
||||
func convertToDomainModel(receiver *model.Receiver) (*ngmodels.Receiver, map[string][]string, error) {
|
||||
domain := &ngmodels.Receiver{
|
||||
UID: legacy_storage.NameToUid(receiver.Spec.Title),
|
||||
|
@ -33,10 +33,15 @@ type ReceiverService interface {
|
||||
DeleteReceiver(ctx context.Context, name string, provenance definitions.Provenance, version string, orgID int64, user identity.Requester) error
|
||||
}
|
||||
|
||||
type MetadataService interface {
|
||||
Access(ctx context.Context, user identity.Requester, receivers ...*ngmodels.Receiver) (map[string]ngmodels.ReceiverPermissionSet, error)
|
||||
}
|
||||
|
||||
type legacyStorage struct {
|
||||
service ReceiverService
|
||||
namespacer request.NamespaceMapper
|
||||
tableConverter rest.TableConvertor
|
||||
metadata MetadataService
|
||||
}
|
||||
|
||||
func (s *legacyStorage) New() runtime.Object {
|
||||
@ -85,7 +90,12 @@ func (s *legacyStorage) List(ctx context.Context, opts *internalversion.ListOpti
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return convertToK8sResources(orgId, res, s.namespacer, opts.FieldSelector)
|
||||
accesses, err := s.metadata.Access(ctx, user, res...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access control metadata: %w", err)
|
||||
}
|
||||
|
||||
return convertToK8sResources(orgId, res, accesses, s.namespacer, opts.FieldSelector)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOptions) (runtime.Object, error) {
|
||||
@ -113,7 +123,18 @@ func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOption
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertToK8sResource(info.OrgID, r, s.namespacer)
|
||||
|
||||
var access *ngmodels.ReceiverPermissionSet
|
||||
accesses, err := s.metadata.Access(ctx, user, r)
|
||||
if err == nil {
|
||||
if a, ok := accesses[r.GetUID()]; ok {
|
||||
access = &a
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to get access control metadata: %w", err)
|
||||
}
|
||||
|
||||
return convertToK8sResource(info.OrgID, r, access, s.namespacer)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Create(ctx context.Context,
|
||||
@ -151,7 +172,7 @@ func (s *legacyStorage) Create(ctx context.Context,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return convertToK8sResource(info.OrgID, out, s.namespacer)
|
||||
return convertToK8sResource(info.OrgID, out, nil, s.namespacer)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Update(ctx context.Context,
|
||||
@ -203,7 +224,7 @@ func (s *legacyStorage) Update(ctx context.Context,
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
r, err := convertToK8sResource(info.OrgID, updated, s.namespacer)
|
||||
r, err := convertToK8sResource(info.OrgID, updated, nil, s.namespacer)
|
||||
return r, false, err
|
||||
}
|
||||
|
||||
|
@ -34,11 +34,13 @@ func NewStorage(
|
||||
scheme *runtime.Scheme,
|
||||
optsGetter generic.RESTOptionsGetter,
|
||||
dualWriteBuilder grafanarest.DualWriteBuilder,
|
||||
metadata MetadataService,
|
||||
) (rest.Storage, error) {
|
||||
legacyStore := &legacyStorage{
|
||||
service: legacySvc,
|
||||
namespacer: namespacer,
|
||||
tableConverter: resourceInfo.TableConverter(),
|
||||
metadata: metadata,
|
||||
}
|
||||
if optsGetter != nil && dualWriteBuilder != nil {
|
||||
strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion())
|
||||
|
@ -85,7 +85,7 @@ func (t *NotificationsAPIBuilder) GetAPIGroupInfo(
|
||||
return nil, fmt.Errorf("failed to initialize time-interval storage: %w", err)
|
||||
}
|
||||
|
||||
recvStorage, err := receiver.NewStorage(t.ng.Api.ReceiverService, t.namespacer, scheme, optsGetter, dualWriteBuilder)
|
||||
recvStorage, err := receiver.NewStorage(t.ng.Api.ReceiverService, t.namespacer, scheme, optsGetter, dualWriteBuilder, ac.NewReceiverAccess[*ngmodels.Receiver](t.ng.Api.AccessControl, false))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize receiver storage: %w", err)
|
||||
}
|
||||
|
@ -132,35 +132,12 @@ type ReceiverAccess[T models.Identified] struct {
|
||||
readDecrypted actionAccess[T]
|
||||
create actionAccess[T]
|
||||
update actionAccess[T]
|
||||
delete actionAccess[models.Identified]
|
||||
delete actionAccess[T]
|
||||
}
|
||||
|
||||
// NewReceiverAccess creates a new ReceiverAccess service. If includeProvisioningActions is true, the service will include
|
||||
// permissions specific to the provisioning API.
|
||||
func NewReceiverAccess[T models.Identified](a ac.AccessControl, includeProvisioningActions bool) *ReceiverAccess[T] {
|
||||
// If this service is meant for the provisioning API, we include the provisioning actions as possible permissions.
|
||||
// TODO: Improve this monkey patching.
|
||||
readRedactedReceiversPreConditionsEval := readRedactedReceiversPreConditionsEval
|
||||
readDecryptedReceiversPreConditionsEval := readDecryptedReceiversPreConditionsEval
|
||||
readRedactedReceiverEval := readRedactedReceiverEval
|
||||
readDecryptedReceiverEval := readDecryptedReceiverEval
|
||||
readRedactedAllReceiversEval := readRedactedAllReceiversEval
|
||||
readDecryptedAllReceiversEval := readDecryptedAllReceiversEval
|
||||
if includeProvisioningActions {
|
||||
readRedactedReceiversPreConditionsEval = ac.EvalAny(provisioningExtraReadRedactedPermissions, readRedactedReceiversPreConditionsEval)
|
||||
readDecryptedReceiversPreConditionsEval = ac.EvalAny(provisioningExtraReadDecryptedPermissions, readDecryptedReceiversPreConditionsEval)
|
||||
|
||||
readRedactedReceiverEval = func(uid string) ac.Evaluator {
|
||||
return ac.EvalAny(provisioningExtraReadRedactedPermissions, readRedactedReceiverEval(uid))
|
||||
}
|
||||
readDecryptedReceiverEval = func(uid string) ac.Evaluator {
|
||||
return ac.EvalAny(provisioningExtraReadDecryptedPermissions, readDecryptedReceiverEval(uid))
|
||||
}
|
||||
|
||||
readRedactedAllReceiversEval = ac.EvalAny(provisioningExtraReadRedactedPermissions, readRedactedAllReceiversEval)
|
||||
readDecryptedAllReceiversEval = ac.EvalAny(provisioningExtraReadDecryptedPermissions, readDecryptedAllReceiversEval)
|
||||
}
|
||||
|
||||
rcvAccess := &ReceiverAccess[T]{
|
||||
read: actionAccess[T]{
|
||||
genericService: genericService{
|
||||
@ -192,11 +169,11 @@ func NewReceiverAccess[T models.Identified](a ac.AccessControl, includeProvision
|
||||
},
|
||||
resource: "receiver",
|
||||
action: "create",
|
||||
authorizeSome: ac.EvalAll(readRedactedReceiversPreConditionsEval, createReceiversEval),
|
||||
authorizeSome: createReceiversEval,
|
||||
authorizeOne: func(receiver models.Identified) ac.Evaluator {
|
||||
return ac.EvalAll(readRedactedReceiversPreConditionsEval, createReceiversEval)
|
||||
return createReceiversEval
|
||||
},
|
||||
authorizeAll: ac.EvalAll(readRedactedReceiversPreConditionsEval, createReceiversEval),
|
||||
authorizeAll: createReceiversEval,
|
||||
},
|
||||
update: actionAccess[T]{
|
||||
genericService: genericService{
|
||||
@ -204,29 +181,67 @@ func NewReceiverAccess[T models.Identified](a ac.AccessControl, includeProvision
|
||||
},
|
||||
resource: "receiver",
|
||||
action: "update",
|
||||
authorizeSome: ac.EvalAll(readRedactedReceiversPreConditionsEval, updateReceiversPreConditionsEval),
|
||||
authorizeSome: updateReceiversPreConditionsEval,
|
||||
authorizeOne: func(receiver models.Identified) ac.Evaluator {
|
||||
return ac.EvalAll(readRedactedReceiverEval(receiver.GetUID()), updateReceiverEval(receiver.GetUID()))
|
||||
return updateReceiverEval(receiver.GetUID())
|
||||
},
|
||||
authorizeAll: ac.EvalAll(readRedactedAllReceiversEval, updateAllReceiversEval),
|
||||
authorizeAll: updateAllReceiversEval,
|
||||
},
|
||||
delete: actionAccess[models.Identified]{
|
||||
delete: actionAccess[T]{
|
||||
genericService: genericService{
|
||||
ac: a,
|
||||
},
|
||||
resource: "receiver",
|
||||
action: "delete",
|
||||
authorizeSome: ac.EvalAll(readRedactedReceiversPreConditionsEval, deleteReceiversPreConditionsEval),
|
||||
authorizeSome: deleteReceiversPreConditionsEval,
|
||||
authorizeOne: func(receiver models.Identified) ac.Evaluator {
|
||||
return ac.EvalAll(readRedactedReceiverEval(receiver.GetUID()), deleteReceiverEval(receiver.GetUID()))
|
||||
return deleteReceiverEval(receiver.GetUID())
|
||||
},
|
||||
authorizeAll: ac.EvalAll(readRedactedAllReceiversEval, deleteAllReceiversEval),
|
||||
authorizeAll: deleteAllReceiversEval,
|
||||
},
|
||||
}
|
||||
|
||||
// If this service is meant for the provisioning API, we include the provisioning actions as possible permissions.
|
||||
if includeProvisioningActions {
|
||||
extendAccessControl(&rcvAccess.read, ac.EvalAny, actionAccess[T]{
|
||||
authorizeSome: provisioningExtraReadRedactedPermissions,
|
||||
authorizeAll: provisioningExtraReadRedactedPermissions,
|
||||
authorizeOne: func(receiver models.Identified) ac.Evaluator {
|
||||
return provisioningExtraReadRedactedPermissions
|
||||
},
|
||||
})
|
||||
extendAccessControl(&rcvAccess.readDecrypted, ac.EvalAny, actionAccess[T]{
|
||||
authorizeSome: provisioningExtraReadDecryptedPermissions,
|
||||
authorizeAll: provisioningExtraReadDecryptedPermissions,
|
||||
authorizeOne: func(receiver models.Identified) ac.Evaluator {
|
||||
return provisioningExtraReadDecryptedPermissions
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Write and delete permissions should require read permissions.
|
||||
extendAccessControl(&rcvAccess.update, ac.EvalAll, rcvAccess.read)
|
||||
extendAccessControl(&rcvAccess.delete, ac.EvalAll, rcvAccess.read)
|
||||
|
||||
return rcvAccess
|
||||
}
|
||||
|
||||
// extendAccessControl extends the access control of base with the extension. The operator function is used to combine
|
||||
// the authorization evaluators.
|
||||
func extendAccessControl[T models.Identified](base *actionAccess[T], operator func(evaluator ...ac.Evaluator) ac.Evaluator, extension actionAccess[T]) {
|
||||
// Prevent infinite recursion.
|
||||
baseSome := base.authorizeSome
|
||||
baseAll := base.authorizeAll
|
||||
baseOne := base.authorizeOne
|
||||
|
||||
// Extend the access control of base with the extension.
|
||||
base.authorizeSome = operator(extension.authorizeSome, baseSome)
|
||||
base.authorizeAll = operator(extension.authorizeAll, baseAll)
|
||||
base.authorizeOne = func(resource models.Identified) ac.Evaluator {
|
||||
return operator(extension.authorizeOne(resource), baseOne(resource))
|
||||
}
|
||||
}
|
||||
|
||||
// HasList checks if user has access to list redacted receivers. Returns false if user does not have access.
|
||||
func (s ReceiverAccess[T]) HasList(ctx context.Context, user identity.Requester) (bool, error) { // TODO: Remove this with fgac.
|
||||
return s.read.HasAccess(ctx, user, readRedactedReceiversListEval)
|
||||
@ -307,3 +322,72 @@ func (s ReceiverAccess[T]) AuthorizeUpdateByUID(ctx context.Context, user identi
|
||||
func (s ReceiverAccess[T]) AuthorizeReadSome(ctx context.Context, user identity.Requester) error {
|
||||
return s.read.AuthorizePreConditions(ctx, user)
|
||||
}
|
||||
|
||||
// All access permissions for a given receiver.
|
||||
|
||||
// Access returns the permission sets for a slice of receivers. The permission set includes secrets, write, and
|
||||
// delete which corresponds the given user being able to read, write, and delete each given receiver.
|
||||
func (s ReceiverAccess[T]) Access(ctx context.Context, user identity.Requester, receivers ...T) (map[string]models.ReceiverPermissionSet, error) {
|
||||
basePerms := models.NewReceiverPermissionSet()
|
||||
if err := s.readDecrypted.AuthorizePreConditions(ctx, user); err != nil {
|
||||
basePerms.Set(models.ReceiverPermissionReadSecret, false) // Doesn't match the preconditions.
|
||||
} else if err := s.readDecrypted.AuthorizeAll(ctx, user); err == nil {
|
||||
basePerms.Set(models.ReceiverPermissionReadSecret, true) // Has access to all receivers.
|
||||
}
|
||||
|
||||
// TODO: Add when resource permissions are implemented.
|
||||
//if err := s.permissions.AuthorizePreConditions(ctx, user); err != nil {
|
||||
// basePerms.Set(models.ReceiverPermissionAdmin, false) // Doesn't match the preconditions.
|
||||
//} else if err := s.permissions.AuthorizeAll(ctx, user); err == nil {
|
||||
// basePerms.Set(models.ReceiverPermissionAdmin, true) // Has access to all receivers.
|
||||
//}
|
||||
|
||||
if err := s.update.AuthorizePreConditions(ctx, user); err != nil {
|
||||
basePerms.Set(models.ReceiverPermissionWrite, false) // Doesn't match the preconditions.
|
||||
} else if err := s.update.AuthorizeAll(ctx, user); err == nil {
|
||||
basePerms.Set(models.ReceiverPermissionWrite, true) // Has access to all receivers.
|
||||
}
|
||||
|
||||
if err := s.delete.AuthorizePreConditions(ctx, user); err != nil {
|
||||
basePerms.Set(models.ReceiverPermissionDelete, false) // Doesn't match the preconditions.
|
||||
} else if err := s.delete.AuthorizeAll(ctx, user); err == nil {
|
||||
basePerms.Set(models.ReceiverPermissionDelete, true) // Has access to all receivers.
|
||||
}
|
||||
|
||||
if basePerms.AllSet() {
|
||||
// Shortcut for the case when all permissions are known based on preconditions.
|
||||
result := make(map[string]models.ReceiverPermissionSet, len(receivers))
|
||||
for _, rcv := range receivers {
|
||||
result[rcv.GetUID()] = basePerms.Clone()
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result := make(map[string]models.ReceiverPermissionSet, len(receivers))
|
||||
for _, rcv := range receivers {
|
||||
permSet := basePerms.Clone()
|
||||
if _, ok := permSet.Has(models.ReceiverPermissionReadSecret); !ok {
|
||||
err := s.readDecrypted.authorize(ctx, user, rcv) // Check permissions ignoring preconditions and all access.
|
||||
permSet.Set(models.ReceiverPermissionReadSecret, err == nil)
|
||||
}
|
||||
|
||||
// TODO: Add when resource permissions are implemented.
|
||||
//if _, ok := permSet.Has(models.ReceiverPermissionAdmin); !ok {
|
||||
// err := s.permissions.authorize(ctx, user, rcv)
|
||||
// permSet.Set(models.ReceiverPermissionAdmin, err == nil)
|
||||
//}
|
||||
|
||||
if _, ok := permSet.Has(models.ReceiverPermissionWrite); !ok {
|
||||
err := s.update.authorize(ctx, user, rcv)
|
||||
permSet.Set(models.ReceiverPermissionWrite, err == nil)
|
||||
}
|
||||
|
||||
if _, ok := permSet.Has(models.ReceiverPermissionDelete); !ok {
|
||||
err := s.delete.authorize(ctx, user, rcv)
|
||||
permSet.Set(models.ReceiverPermissionDelete, err == nil)
|
||||
}
|
||||
|
||||
result[rcv.GetUID()] = permSet
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
351
pkg/services/ngalert/accesscontrol/receivers_test.go
Normal file
351
pkg/services/ngalert/accesscontrol/receivers_test.go
Normal file
@ -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...))
|
||||
}
|
75
pkg/services/ngalert/models/permissions.go
Normal file
75
pkg/services/ngalert/models/permissions.go
Normal file
@ -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
|
||||
}
|
@ -75,7 +75,7 @@ func SilencePermissions() [3]SilencePermission {
|
||||
}
|
||||
|
||||
// SilencePermissionSet represents a set of permissions for a silence.
|
||||
type SilencePermissionSet map[SilencePermission]bool
|
||||
type SilencePermissionSet map[SilencePermission]bool // TODO: Implement using PermissionSet[SilencePermission]
|
||||
|
||||
// Clone returns a deep copy of the permission set.
|
||||
func (p SilencePermissionSet) Clone() SilencePermissionSet {
|
||||
|
@ -825,7 +825,8 @@ func TestReceiverServiceAC_Read(t *testing.T) {
|
||||
permissions map[string][]string
|
||||
existing []models.Receiver
|
||||
|
||||
visible []models.Receiver
|
||||
visible []models.Receiver
|
||||
visibleWithProvisioning []models.Receiver
|
||||
}{
|
||||
{
|
||||
name: "not authorized without permissions",
|
||||
@ -874,6 +875,20 @@ func TestReceiverServiceAC_Read(t *testing.T) {
|
||||
existing: allReceivers(),
|
||||
visible: []models.Receiver{recv1, recv3},
|
||||
},
|
||||
{
|
||||
name: "provisioning read applies to only provisioning",
|
||||
permissions: map[string][]string{accesscontrol.ActionAlertingProvisioningRead: nil},
|
||||
existing: allReceivers(),
|
||||
visible: nil,
|
||||
visibleWithProvisioning: allReceivers(),
|
||||
},
|
||||
{
|
||||
name: "provisioning read secrets applies to only provisioning",
|
||||
permissions: map[string][]string{accesscontrol.ActionAlertingProvisioningReadSecrets: nil},
|
||||
existing: allReceivers(),
|
||||
visible: nil,
|
||||
visibleWithProvisioning: allReceivers(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@ -906,6 +921,28 @@ func TestReceiverServiceAC_Read(t *testing.T) {
|
||||
assert.ErrorIsf(t, err, ac.ErrAuthorizationBase, "receiver '%s' should not be visible, but is", recv.Name)
|
||||
}
|
||||
}
|
||||
|
||||
isVisibleInProvisioning := func(uid string) bool {
|
||||
if tc.visibleWithProvisioning == nil {
|
||||
return isVisible(uid)
|
||||
}
|
||||
for _, recv := range tc.visibleWithProvisioning {
|
||||
if recv.UID == uid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
sut.authz = ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), true)
|
||||
for _, recv := range allReceivers() {
|
||||
response, err := sut.GetReceiver(context.Background(), singleQ(orgId, recv.Name), usr)
|
||||
if isVisibleInProvisioning(recv.UID) {
|
||||
require.NoErrorf(t, err, "receiver '%s' should be visible, but isn't", recv.Name)
|
||||
assert.NotNil(t, response)
|
||||
} else {
|
||||
assert.ErrorIsf(t, err, ac.ErrAuthorizationBase, "receiver '%s' should not be visible, but is", recv.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -933,14 +970,14 @@ func TestReceiverServiceAC_Create(t *testing.T) {
|
||||
hasAccess: nil,
|
||||
},
|
||||
{
|
||||
name: "global legacy permissions - not authorized without read",
|
||||
name: "global legacy permissions - authorized without read",
|
||||
permissions: map[string][]string{accesscontrol.ActionAlertingNotificationsWrite: nil},
|
||||
hasAccess: nil,
|
||||
hasAccess: allReceivers(),
|
||||
},
|
||||
{
|
||||
name: "receivers permissions - not authorized without read",
|
||||
name: "receivers permissions - authorized without read",
|
||||
permissions: map[string][]string{accesscontrol.ActionAlertingReceiversCreate: nil},
|
||||
hasAccess: nil,
|
||||
hasAccess: allReceivers(),
|
||||
},
|
||||
{
|
||||
name: "global legacy permissions - create all",
|
||||
|
@ -133,11 +133,12 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
org1 := helper.Org1
|
||||
|
||||
type testCase struct {
|
||||
user apis.User
|
||||
canRead bool
|
||||
canUpdate bool
|
||||
canCreate bool
|
||||
canDelete bool
|
||||
user apis.User
|
||||
canRead bool
|
||||
canUpdate bool
|
||||
canCreate bool
|
||||
canDelete bool
|
||||
canReadSecrets bool
|
||||
}
|
||||
// region users
|
||||
unauthorized := helper.CreateUser("unauthorized", "Org1", org.RoleNone, []resourcepermissions.SetResourcePermissionCommand{})
|
||||
@ -215,8 +216,9 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
canRead: true,
|
||||
},
|
||||
{
|
||||
user: secretsReader,
|
||||
canRead: true,
|
||||
user: secretsReader,
|
||||
canRead: true,
|
||||
canReadSecrets: true,
|
||||
},
|
||||
{
|
||||
user: creator,
|
||||
@ -298,6 +300,16 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
}
|
||||
|
||||
if tc.canRead {
|
||||
expectedWithMetadata := expected.DeepCopy()
|
||||
if tc.canUpdate {
|
||||
expectedWithMetadata.SetAccessControl("canWrite")
|
||||
}
|
||||
if tc.canDelete {
|
||||
expectedWithMetadata.SetAccessControl("canDelete")
|
||||
}
|
||||
if tc.canReadSecrets {
|
||||
expectedWithMetadata.SetAccessControl("canReadSecrets")
|
||||
}
|
||||
t.Run("should be able to list receivers", func(t *testing.T) {
|
||||
list, err := client.List(ctx, v1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
@ -307,7 +319,7 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
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, expected, got)
|
||||
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{})
|
||||
@ -871,6 +883,10 @@ func TestIntegrationCRUD(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, receiver.Spec.Integrations, len(integrations))
|
||||
|
||||
// Set access control metadata
|
||||
receiver.SetAccessControl("canWrite")
|
||||
receiver.SetAccessControl("canDelete")
|
||||
|
||||
// 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)
|
||||
|
Loading…
Reference in New Issue
Block a user