diff --git a/pkg/services/ngalert/accesscontrol/models.go b/pkg/services/ngalert/accesscontrol/models.go index 9768378364e..8bad1a0906f 100644 --- a/pkg/services/ngalert/accesscontrol/models.go +++ b/pkg/services/ngalert/accesscontrol/models.go @@ -1,17 +1,19 @@ package accesscontrol import ( + "context" "fmt" "github.com/grafana/grafana/pkg/apimachinery/errutil" - "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/apimachinery/identity" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" ) var ( ErrAuthorizationBase = errutil.Forbidden("alerting.unauthorized") ) -func NewAuthorizationErrorWithPermissions(action string, eval accesscontrol.Evaluator) error { +func NewAuthorizationErrorWithPermissions(action string, eval ac.Evaluator) error { msg := fmt.Sprintf("user is not authorized to %s", action) err := ErrAuthorizationBase.Errorf(msg) err.PublicMessage = msg @@ -26,3 +28,93 @@ func NewAuthorizationErrorWithPermissions(action string, eval accesscontrol.Eval func NewAuthorizationErrorGeneric(action string) error { return NewAuthorizationErrorWithPermissions(action, nil) } + +// actionAccess is a helper struct that provides common access control methods for a specific resource type and action. +type actionAccess[T any] struct { + genericService + + // authorizeSome evaluates to true if user has access to some (any) resources. + // This is used as a precondition check, if this evaluates to false then user does not have access to any resources. + authorizeSome ac.Evaluator + + // authorizeAll evaluates to true if user has access to all resources. + authorizeAll ac.Evaluator + + // authorizeOne returns an evaluator that checks if user has access to a specific resource. + authorizeOne func(T) ac.Evaluator + + // action is the action that user is trying to perform on the resource. Used in error messages. + action string + + // resource is the name of the resource. Used in error messages. + resource string +} + +// Filter filters the given list of resources based on access control permissions of the user. +// This method is preferred when many resources need to be checked. +func (s actionAccess[T]) Filter(ctx context.Context, user identity.Requester, resources ...T) ([]T, error) { + canAll, err := s.authorizePreConditions(ctx, user) + if err != nil { + return nil, err + } + if canAll { + return resources, nil + } + result := make([]T, 0, len(resources)) + for _, r := range resources { + if hasAccess := s.authorize(ctx, user, r); hasAccess == nil { + result = append(result, r) + } + } + return result, nil +} + +// Authorize checks if user has access to a resource. Returns an error if user does not have access. +func (s actionAccess[T]) Authorize(ctx context.Context, user identity.Requester, resource T) error { + canAll, err := s.authorizePreConditions(ctx, user) + if canAll || err != nil { // Return early if user can either access all or there is an error. + return err + } + + return s.authorize(ctx, user, resource) +} + +// Has checks if user has access to a resource. Returns false if user does not have access. +func (s actionAccess[T]) Has(ctx context.Context, user identity.Requester, resource T) (bool, error) { + canAll, err := s.authorizePreConditions(ctx, user) + if canAll || err != nil { // Return early if user can either access all or there is an error. + return canAll, err + } + + return s.has(ctx, user, resource) +} + +// authorizePreConditions checks necessary preconditions for resources. Returns true if user has access for all +// resources. Returns error if user does not have access to on any resources. +func (s actionAccess[T]) authorizePreConditions(ctx context.Context, user identity.Requester) (bool, error) { + canAll, err := s.HasAccess(ctx, user, s.authorizeAll) + if canAll || err != nil { // Return early if user can either access all or there is an error. + return canAll, err + } + + can, err := s.HasAccess(ctx, user, s.authorizeSome) + if err != nil { + return false, err + } + if !can { // User does not have any resource permissions at all. + return false, NewAuthorizationErrorWithPermissions(fmt.Sprintf("%s any %s", s.action, s.resource), s.authorizeSome) + } + return false, nil +} + +// authorize checks if user has access to a specific resource given precondition checks have already passed. Returns an error if user does not have access. +func (s actionAccess[T]) authorize(ctx context.Context, user identity.Requester, resource T) error { + return s.HasAccessOrError(ctx, user, s.authorizeOne(resource), func() string { + return fmt.Sprintf("%s %s", s.action, s.resource) + }) +} + +// has checks if user has access to a specific resource given precondition checks have already passed. Returns false if user does not have access. +func (s actionAccess[T]) has(ctx context.Context, user identity.Requester, resource T) (bool, error) { + return s.HasAccess(ctx, user, s.authorizeOne(resource)) +} diff --git a/pkg/services/ngalert/accesscontrol/receivers.go b/pkg/services/ngalert/accesscontrol/receivers.go new file mode 100644 index 00000000000..66338f9846d --- /dev/null +++ b/pkg/services/ngalert/accesscontrol/receivers.go @@ -0,0 +1,174 @@ +package accesscontrol + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +var ( + // Asserts pre-conditions for read access to redacted receivers. If this evaluates to false, the user cannot read any redacted receivers. + readRedactedReceiversPreConditionsEval = ac.EvalAny( + ac.EvalPermission(ac.ActionAlertingNotificationsRead), // Global action for all AM config. Org scope. + ac.EvalPermission(ac.ActionAlertingReceiversRead), // Action for redacted receivers. UID scope. + readDecryptedReceiversPreConditionsEval, + ) + // Asserts pre-conditions for read access to decrypted receivers. If this evaluates to false, the user cannot read any decrypted receivers. + readDecryptedReceiversPreConditionsEval = ac.EvalAny( + ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets), // Action for decrypted receivers. UID scope. + ) + + // Asserts read-only access to all redacted receivers. + readRedactedAllReceiversEval = ac.EvalAny( + ac.EvalPermission(ac.ActionAlertingNotificationsRead), + + // TODO: The following should be scoped, but are currently interpreted as global. Needs a db migration when scope is added. + ac.EvalPermission(ac.ActionAlertingReceiversRead), // TODO: Add global scope with fgac. + readDecryptedAllReceiversEval, + ) + // Asserts read-only access to all decrypted receivers. + readDecryptedAllReceiversEval = ac.EvalAny( + ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets), // TODO: Add global scope with fgac. + ) + + // Asserts read-only access to a specific redacted receiver. + readRedactedReceiverEval = func(uid string) ac.Evaluator { + return ac.EvalAny( + ac.EvalPermission(ac.ActionAlertingNotificationsRead), + + // TODO: The following should be scoped, but are currently interpreted as global. Needs a db migration when scope is added. + ac.EvalPermission(ac.ActionAlertingReceiversRead), // TODO: Add uid scope with fgac. + readDecryptedReceiverEval(uid), + ) + } + + // Asserts read-only access to a specific decrypted receiver. + readDecryptedReceiverEval = func(uid string) ac.Evaluator { + return ac.EvalAny( + ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets), // TODO: Add uid scope with fgac. + ) + } + + // Asserts read-only access to list redacted receivers. // TODO: Remove this with fgac. + readRedactedReceiversListEval = ac.EvalAny( + ac.EvalPermission(ac.ActionAlertingReceiversList), + ) + + // Extra permissions that give read-only access to all redacted receivers when called from provisioning api. + provisioningExtraReadRedactedPermissions = ac.EvalAny( + ac.EvalPermission(ac.ActionAlertingProvisioningRead), // Global provisioning action for all AM config. Org scope. + ac.EvalPermission(ac.ActionAlertingNotificationsProvisioningRead), // Global provisioning action for receivers. Org scope. + provisioningExtraReadDecryptedPermissions, + ) + + // Extra permissions that give read-only access to all decrypted receivers when called from provisioning api. + provisioningExtraReadDecryptedPermissions = ac.EvalAny( + ac.EvalPermission(ac.ActionAlertingProvisioningReadSecrets), // Global provisioning action for all AM config + secrets. Org scope. + ) +) + +type ReceiverAccess[T models.Identified] struct { + read actionAccess[T] + readDecrypted 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] { + rcvAccess := &ReceiverAccess[T]{ + read: actionAccess[T]{ + genericService: genericService{ + ac: a, + }, + resource: "receiver", + action: "read", + authorizeSome: readRedactedReceiversPreConditionsEval, + authorizeOne: func(receiver T) ac.Evaluator { + return readRedactedReceiverEval(receiver.GetUID()) + }, + authorizeAll: readRedactedAllReceiversEval, + }, + readDecrypted: actionAccess[T]{ + genericService: genericService{ + ac: a, + }, + resource: "decrypted receiver", + action: "read", + authorizeSome: readDecryptedReceiversPreConditionsEval, + authorizeOne: func(receiver T) ac.Evaluator { + return readDecryptedReceiverEval(receiver.GetUID()) + }, + authorizeAll: readDecryptedAllReceiversEval, + }, + } + + // If this service is meant for the provisioning API, we include the provisioning actions as possible permissions. + if includeProvisioningActions { + rcvAccess.read.authorizeSome = ac.EvalAny(provisioningExtraReadRedactedPermissions, rcvAccess.read.authorizeSome) + rcvAccess.readDecrypted.authorizeSome = ac.EvalAny(provisioningExtraReadDecryptedPermissions, rcvAccess.readDecrypted.authorizeSome) + + rcvAccess.read.authorizeOne = func(receiver T) ac.Evaluator { + return ac.EvalAny(provisioningExtraReadRedactedPermissions, rcvAccess.read.authorizeOne(receiver)) + } + rcvAccess.readDecrypted.authorizeOne = func(receiver T) ac.Evaluator { + return ac.EvalAny(provisioningExtraReadDecryptedPermissions, rcvAccess.readDecrypted.authorizeOne(receiver)) + } + + rcvAccess.read.authorizeAll = ac.EvalAny(provisioningExtraReadRedactedPermissions, rcvAccess.read.authorizeAll) + rcvAccess.readDecrypted.authorizeAll = ac.EvalAny(provisioningExtraReadDecryptedPermissions, rcvAccess.readDecrypted.authorizeAll) + } + + return rcvAccess +} + +// 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) +} + +// FilterRead filters the given list of receivers based on the read redacted access control permissions of the user. +// This method is preferred when many receivers need to be checked. +func (s ReceiverAccess[T]) FilterRead(ctx context.Context, user identity.Requester, receivers ...T) ([]T, error) { + return s.read.Filter(ctx, user, receivers...) +} + +// AuthorizeRead checks if user has access to read a redacted receiver. Returns an error if user does not have access. +func (s ReceiverAccess[T]) AuthorizeRead(ctx context.Context, user identity.Requester, receiver T) error { + return s.read.Authorize(ctx, user, receiver) +} + +// HasRead checks if user has access to read a redacted receiver. Returns false if user does not have access. +func (s ReceiverAccess[T]) HasRead(ctx context.Context, user identity.Requester, receiver T) (bool, error) { + return s.read.Has(ctx, user, receiver) +} + +// HasReadAll checks if user has access to read all redacted receivers. Returns false if user does not have access. +func (s ReceiverAccess[T]) HasReadAll(ctx context.Context, user identity.Requester) (bool, error) { // TODO: Temporary for legacy compatibility. + return s.read.HasAccess(ctx, user, s.read.authorizeAll) +} + +// FilterReadDecrypted filters the given list of receivers based on the read decrypted access control permissions of the user. +// This method is preferred when many receivers need to be checked. +func (s ReceiverAccess[T]) FilterReadDecrypted(ctx context.Context, user identity.Requester, receivers ...T) ([]T, error) { + return s.readDecrypted.Filter(ctx, user, receivers...) +} + +// AuthorizeReadDecrypted checks if user has access to read a decrypted receiver. +func (s ReceiverAccess[T]) AuthorizeReadDecrypted(ctx context.Context, user identity.Requester, receiver T) error { + return s.readDecrypted.Authorize(ctx, user, receiver) +} + +// HasReadDecrypted checks if user has access to read a decrypted receiver. Returns false if user does not have access. +func (s ReceiverAccess[T]) HasReadDecrypted(ctx context.Context, user identity.Requester, receiver T) (bool, error) { + return s.readDecrypted.Has(ctx, user, receiver) +} + +// AuthorizeReadDecryptedAll checks if user has access to read all decrypted receiver. Returns an error if user does not have access. +func (s ReceiverAccess[T]) AuthorizeReadDecryptedAll(ctx context.Context, user identity.Requester) error { // TODO: Temporary for legacy compatibility. + return s.readDecrypted.HasAccessOrError(ctx, user, s.readDecrypted.authorizeAll, func() string { + return fmt.Sprintf("%s %s", s.readDecrypted.action, s.readDecrypted.resource) + }) +} diff --git a/pkg/services/ngalert/api/api_notifications_test.go b/pkg/services/ngalert/api/api_notifications_test.go index 3c2bae6f209..3e9349a1026 100644 --- a/pkg/services/ngalert/api/api_notifications_test.go +++ b/pkg/services/ngalert/api/api_notifications_test.go @@ -202,7 +202,7 @@ func TestRouteGetReceiversResponses(t *testing.T) { env := createTestEnv(t, testConfig) env.ac = &recordingAccessControlFake{ Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { - if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingProvisioningReadSecrets) { + if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingReceiversReadSecrets) { recPermCheck = true } return false, nil @@ -397,7 +397,7 @@ func createNotificationSrvSutFromEnv(t *testing.T, env *testEnvironment) Notific t.Helper() receiverSvc := notifier.NewReceiverService( - env.ac, + ac.NewReceiverAccess[*models.Receiver](env.ac, false), legacy_storage.NewAlertmanagerConfigStore(env.configs), env.prov, env.secrets, diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 7c1b876d1eb..763d88e2204 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -35,6 +35,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" + ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -1583,6 +1584,9 @@ func TestProvisioningApiContactPointExport(t *testing.T) { env := createTestEnv(t, testConfig) env.ac = &recordingAccessControlFake{ Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingReceiversList) { + return true, nil + } if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingProvisioningReadSecrets) { recPermCheck = true } @@ -1888,7 +1892,7 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi configStore := legacy_storage.NewAlertmanagerConfigStore(env.configs) receiverSvc := notifier.NewReceiverService( - env.ac, + ac.NewReceiverAccess[*models.Receiver](env.ac, true), configStore, env.prov, env.secrets, diff --git a/pkg/services/ngalert/models/receivers.go b/pkg/services/ngalert/models/receivers.go index 1497395eeec..152c7533c34 100644 --- a/pkg/services/ngalert/models/receivers.go +++ b/pkg/services/ngalert/models/receivers.go @@ -1,5 +1,7 @@ package models +import "github.com/grafana/alerting/notify" + // GetReceiverQuery represents a query for a single receiver. type GetReceiverQuery struct { OrgID int64 @@ -15,3 +17,20 @@ type GetReceiversQuery struct { Offset int Decrypt bool } + +// Receiver is the domain model representation of a receiver / contact point. +type Receiver struct { + UID string + Name string + Integrations []*notify.GrafanaIntegrationConfig + Provenance Provenance +} + +// Identified describes a class of resources that have a UID. Created to abstract required fields for authorization. +type Identified interface { + GetUID() string +} + +func (r Receiver) GetUID() string { + return r.UID +} diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 96457f04aa7..a127e84879c 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -411,7 +411,15 @@ func (ng *AlertNG) init() error { configStore := legacy_storage.NewAlertmanagerConfigStore(ng.store) receiverService := notifier.NewReceiverService( - ng.accesscontrol, + ac.NewReceiverAccess[*models.Receiver](ng.accesscontrol, true), // TODO: Remove provisioning actions from regular API. + configStore, + ng.store, + ng.SecretsService, + ng.store, + ng.Log, + ) + provisioningReceiverService := notifier.NewReceiverService( + ac.NewReceiverAccess[*models.Receiver](ng.accesscontrol, true), configStore, ng.store, ng.SecretsService, @@ -421,7 +429,7 @@ func (ng *AlertNG) init() error { // Provisioning policyService := provisioning.NewNotificationPolicyService(configStore, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.Log) - contactPointService := provisioning.NewContactPointService(configStore, ng.SecretsService, ng.store, ng.store, receiverService, ng.Log, ng.store) + contactPointService := provisioning.NewContactPointService(configStore, ng.SecretsService, ng.store, ng.store, provisioningReceiverService, ng.Log, ng.store) templateService := provisioning.NewTemplateService(configStore, ng.store, ng.store, ng.Log) muteTimingService := provisioning.NewMuteTimingService(configStore, ng.store, ng.store, ng.Log, ng.store) alertRuleService := provisioning.NewAlertRuleService(ng.store, ng.store, ng.folderService, ng.QuotaService, ng.store, diff --git a/pkg/services/ngalert/notifier/receiver_svc.go b/pkg/services/ngalert/notifier/receiver_svc.go index bfa3f530304..5c346385877 100644 --- a/pkg/services/ngalert/notifier/receiver_svc.go +++ b/pkg/services/ngalert/notifier/receiver_svc.go @@ -9,8 +9,6 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/accesscontrol" - ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" @@ -25,7 +23,7 @@ var ( // ReceiverService is the service for managing alertmanager receivers. type ReceiverService struct { - ac accesscontrol.AccessControl + authz receiverAccessControlService provisioningStore provisoningStore cfgStore alertmanagerConfigStore encryptionService secrets.Service @@ -34,6 +32,13 @@ type ReceiverService struct { validator validation.ProvenanceStatusTransitionValidator } +// receiverAccessControlService provides access control for receivers. +type receiverAccessControlService interface { + HasList(ctx context.Context, user identity.Requester) (bool, error) + HasReadAll(ctx context.Context, user identity.Requester) (bool, error) + AuthorizeReadDecryptedAll(ctx context.Context, user identity.Requester) error +} + type alertmanagerConfigStore interface { Get(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) Save(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64) error @@ -50,7 +55,7 @@ type transactionManager interface { } func NewReceiverService( - ac accesscontrol.AccessControl, + authz receiverAccessControlService, cfgStore alertmanagerConfigStore, provisioningStore provisoningStore, encryptionService secrets.Service, @@ -58,7 +63,7 @@ func NewReceiverService( log log.Logger, ) *ReceiverService { return &ReceiverService{ - ac: ac, + authz: authz, provisioningStore: provisioningStore, cfgStore: cfgStore, encryptionService: encryptionService, @@ -69,50 +74,19 @@ func NewReceiverService( } func (rs *ReceiverService) shouldDecrypt(ctx context.Context, user identity.Requester, reqDecrypt bool) (bool, error) { - decryptAccess, err := rs.hasReadDecrypted(ctx, user) - if err != nil { + if !reqDecrypt { + return false, nil + } + if err := rs.authz.AuthorizeReadDecryptedAll(ctx, user); err != nil { return false, err } - if reqDecrypt && !decryptAccess { - return false, ac.NewAuthorizationErrorWithPermissions("read any decrypted receiver", nil) // TODO: Replace with authz service. - } - - return decryptAccess && reqDecrypt, nil -} - -// hasReadDecrypted checks if the user has permission to read decrypted secure settings. -func (rs *ReceiverService) hasReadDecrypted(ctx context.Context, user identity.Requester) (bool, error) { - return rs.ac.Evaluate(ctx, user, accesscontrol.EvalAny( - accesscontrol.EvalPermission(accesscontrol.ActionAlertingReceiversReadSecrets), - accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningReadSecrets), // TODO: Add scope all when we implement FGAC. - )) -} - -// hasReadRedacted checks if the user has permission to read redacted secure settings. -func (rs *ReceiverService) hasReadRedacted(ctx context.Context, user identity.Requester) (bool, error) { - return rs.ac.Evaluate(ctx, user, accesscontrol.EvalAny( - accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningRead), - accesscontrol.EvalPermission(accesscontrol.ActionAlertingProvisioningReadSecrets), - accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsProvisioningRead), - accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsRead), - //accesscontrol.EvalPermission(accesscontrol.ActionAlertingReceiversRead, ScopeReceiversProvider.GetResourceAllScope()), // TODO: Add new permissions. - //accesscontrol.EvalPermission(accesscontrol.ActionAlertingReceiversReadSecrets, ScopeReceiversProvider.GetResourceAllScope(), - )) -} - -// hasList checks if the user has permission to list receivers. -func (rs *ReceiverService) hasList(ctx context.Context, user identity.Requester) (bool, error) { - return rs.ac.Evaluate(ctx, user, accesscontrol.EvalPermission(accesscontrol.ActionAlertingReceiversList)) + return true, nil } // GetReceiver returns a receiver by name. // The receiver's secure settings are decrypted if requested and the user has access to do so. func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiverQuery, user identity.Requester) (definitions.GettableApiReceiver, error) { - if q.Decrypt && user == nil { - return definitions.GettableApiReceiver{}, ac.NewAuthorizationErrorWithPermissions("read any decrypted receiver", nil) // TODO: Replace with authz service. - } - revision, err := rs.cfgStore.Get(ctx, q.OrgID) if err != nil { return definitions.GettableApiReceiver{}, err @@ -139,10 +113,6 @@ func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiver // GetReceivers returns a list of receivers a user has access to. // Receivers can be filtered by name, and secure settings are decrypted if requested and the user has access to do so. func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceiversQuery, user identity.Requester) ([]definitions.GettableApiReceiver, error) { - if q.Decrypt && user == nil { - return nil, ac.NewAuthorizationErrorWithPermissions("read any decrypted receiver", nil) // TODO: Replace with authz service. - } - uids := make([]string, 0, len(q.Names)) for _, name := range q.Names { uids = append(uids, legacy_storage.NameToUid(name)) @@ -159,12 +129,17 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive return nil, err } - readRedactedAccess, err := rs.hasReadRedacted(ctx, user) + decrypt, err := rs.shouldDecrypt(ctx, user, q.Decrypt) if err != nil { return nil, err } - listAccess, err := rs.hasList(ctx, user) + readRedactedAccess, err := rs.authz.HasReadAll(ctx, user) + if err != nil { + return nil, err + } + + listAccess, err := rs.authz.HasList(ctx, user) if err != nil { return nil, err } @@ -172,18 +147,13 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive // User doesn't have any permissions on the receivers. // This is mostly a safeguard as it should not be possible with current API endpoints + middleware authentication. if !listAccess && !readRedactedAccess { - return nil, ac.NewAuthorizationErrorWithPermissions("read any receiver", nil) // TODO: Replace with authz service. + return nil, nil } var output []definitions.GettableApiReceiver for i := q.Offset; i < len(postables); i++ { r := postables[i] - decrypt, err := rs.shouldDecrypt(ctx, user, q.Decrypt) - if err != nil { - return nil, err - } - decryptFn := rs.decryptOrRedact(ctx, decrypt, r.Name, "") // Only has permission to list. This reduces from: diff --git a/pkg/services/ngalert/notifier/receiver_svc_test.go b/pkg/services/ngalert/notifier/receiver_svc_test.go index 806ab86530d..1788788f945 100644 --- a/pkg/services/ngalert/notifier/receiver_svc_test.go +++ b/pkg/services/ngalert/notifier/receiver_svc_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/featuremgmt" + ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" @@ -123,13 +124,13 @@ func TestReceiverService_DecryptRedact(t *testing.T) { name: "service returns error when trying to decrypt without permission", decrypt: true, user: readUser, - err: "[alerting.unauthorized] user is not authorized to read any decrypted receiver", + err: "[alerting.unauthorized] user is not authorized to read decrypted receiver", }, { name: "service returns error if user is nil and decrypt is true", decrypt: true, user: nil, - err: "[alerting.unauthorized] user is not authorized to read any decrypted receiver", + err: "[alerting.unauthorized] user is not authorized to read decrypted receiver", }, { name: "service decrypts receivers with permission", @@ -189,7 +190,7 @@ func createReceiverServiceSut(t *testing.T, encryptSvc secrets.Service) *Receive provisioningStore := fakes.NewFakeProvisioningStore() return NewReceiverService( - acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), + ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), true), legacy_storage.NewAlertmanagerConfigStore(store), provisioningStore, encryptSvc, diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index 9ae599166e1..9ce9af52f93 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -338,7 +338,7 @@ func createContactPointServiceSutWithConfigStore(t *testing.T, secretService sec provisioningStore := fakes.NewFakeProvisioningStore() receiverService := notifier.NewReceiverService( - acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), + ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), true), legacy_storage.NewAlertmanagerConfigStore(configStore), provisioningStore, secretService, diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 7dd48c52359..de8ccbf479f 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/folder" alertingauthz "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" @@ -273,7 +274,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error ) configStore := legacy_storage.NewAlertmanagerConfigStore(&st) receiverSvc := notifier.NewReceiverService( - ps.ac, + alertingauthz.NewReceiverAccess[*ngmodels.Receiver](ps.ac, true), configStore, st, ps.secretService,