mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Implement receiver auth service (#90857)
This commit is contained in:
parent
b982259950
commit
62f67e38b8
@ -1,17 +1,19 @@
|
|||||||
package accesscontrol
|
package accesscontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
"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 (
|
var (
|
||||||
ErrAuthorizationBase = errutil.Forbidden("alerting.unauthorized")
|
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)
|
msg := fmt.Sprintf("user is not authorized to %s", action)
|
||||||
err := ErrAuthorizationBase.Errorf(msg)
|
err := ErrAuthorizationBase.Errorf(msg)
|
||||||
err.PublicMessage = msg
|
err.PublicMessage = msg
|
||||||
@ -26,3 +28,93 @@ func NewAuthorizationErrorWithPermissions(action string, eval accesscontrol.Eval
|
|||||||
func NewAuthorizationErrorGeneric(action string) error {
|
func NewAuthorizationErrorGeneric(action string) error {
|
||||||
return NewAuthorizationErrorWithPermissions(action, nil)
|
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))
|
||||||
|
}
|
||||||
|
174
pkg/services/ngalert/accesscontrol/receivers.go
Normal file
174
pkg/services/ngalert/accesscontrol/receivers.go
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
@ -202,7 +202,7 @@ func TestRouteGetReceiversResponses(t *testing.T) {
|
|||||||
env := createTestEnv(t, testConfig)
|
env := createTestEnv(t, testConfig)
|
||||||
env.ac = &recordingAccessControlFake{
|
env.ac = &recordingAccessControlFake{
|
||||||
Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
|
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
|
recPermCheck = true
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
@ -397,7 +397,7 @@ func createNotificationSrvSutFromEnv(t *testing.T, env *testEnvironment) Notific
|
|||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
receiverSvc := notifier.NewReceiverService(
|
receiverSvc := notifier.NewReceiverService(
|
||||||
env.ac,
|
ac.NewReceiverAccess[*models.Receiver](env.ac, false),
|
||||||
legacy_storage.NewAlertmanagerConfigStore(env.configs),
|
legacy_storage.NewAlertmanagerConfigStore(env.configs),
|
||||||
env.prov,
|
env.prov,
|
||||||
env.secrets,
|
env.secrets,
|
||||||
|
@ -35,6 +35,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||||
"github.com/grafana/grafana/pkg/services/guardian"
|
"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/accesscontrol/fakes"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
"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/models"
|
||||||
@ -1583,6 +1584,9 @@ func TestProvisioningApiContactPointExport(t *testing.T) {
|
|||||||
env := createTestEnv(t, testConfig)
|
env := createTestEnv(t, testConfig)
|
||||||
env.ac = &recordingAccessControlFake{
|
env.ac = &recordingAccessControlFake{
|
||||||
Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) {
|
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) {
|
if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingProvisioningReadSecrets) {
|
||||||
recPermCheck = true
|
recPermCheck = true
|
||||||
}
|
}
|
||||||
@ -1888,7 +1892,7 @@ func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) Provisi
|
|||||||
|
|
||||||
configStore := legacy_storage.NewAlertmanagerConfigStore(env.configs)
|
configStore := legacy_storage.NewAlertmanagerConfigStore(env.configs)
|
||||||
receiverSvc := notifier.NewReceiverService(
|
receiverSvc := notifier.NewReceiverService(
|
||||||
env.ac,
|
ac.NewReceiverAccess[*models.Receiver](env.ac, true),
|
||||||
configStore,
|
configStore,
|
||||||
env.prov,
|
env.prov,
|
||||||
env.secrets,
|
env.secrets,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
import "github.com/grafana/alerting/notify"
|
||||||
|
|
||||||
// GetReceiverQuery represents a query for a single receiver.
|
// GetReceiverQuery represents a query for a single receiver.
|
||||||
type GetReceiverQuery struct {
|
type GetReceiverQuery struct {
|
||||||
OrgID int64
|
OrgID int64
|
||||||
@ -15,3 +17,20 @@ type GetReceiversQuery struct {
|
|||||||
Offset int
|
Offset int
|
||||||
Decrypt bool
|
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
|
||||||
|
}
|
||||||
|
@ -411,7 +411,15 @@ func (ng *AlertNG) init() error {
|
|||||||
|
|
||||||
configStore := legacy_storage.NewAlertmanagerConfigStore(ng.store)
|
configStore := legacy_storage.NewAlertmanagerConfigStore(ng.store)
|
||||||
receiverService := notifier.NewReceiverService(
|
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,
|
configStore,
|
||||||
ng.store,
|
ng.store,
|
||||||
ng.SecretsService,
|
ng.SecretsService,
|
||||||
@ -421,7 +429,7 @@ func (ng *AlertNG) init() error {
|
|||||||
|
|
||||||
// Provisioning
|
// Provisioning
|
||||||
policyService := provisioning.NewNotificationPolicyService(configStore, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.Log)
|
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)
|
templateService := provisioning.NewTemplateService(configStore, ng.store, ng.store, ng.Log)
|
||||||
muteTimingService := provisioning.NewMuteTimingService(configStore, ng.store, ng.store, ng.Log, ng.store)
|
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,
|
alertRuleService := provisioning.NewAlertRuleService(ng.store, ng.store, ng.folderService, ng.QuotaService, ng.store,
|
||||||
|
@ -9,8 +9,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"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/api/tooling/definitions"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
||||||
@ -25,7 +23,7 @@ var (
|
|||||||
|
|
||||||
// ReceiverService is the service for managing alertmanager receivers.
|
// ReceiverService is the service for managing alertmanager receivers.
|
||||||
type ReceiverService struct {
|
type ReceiverService struct {
|
||||||
ac accesscontrol.AccessControl
|
authz receiverAccessControlService
|
||||||
provisioningStore provisoningStore
|
provisioningStore provisoningStore
|
||||||
cfgStore alertmanagerConfigStore
|
cfgStore alertmanagerConfigStore
|
||||||
encryptionService secrets.Service
|
encryptionService secrets.Service
|
||||||
@ -34,6 +32,13 @@ type ReceiverService struct {
|
|||||||
validator validation.ProvenanceStatusTransitionValidator
|
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 {
|
type alertmanagerConfigStore interface {
|
||||||
Get(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error)
|
Get(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error)
|
||||||
Save(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64) error
|
Save(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64) error
|
||||||
@ -50,7 +55,7 @@ type transactionManager interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewReceiverService(
|
func NewReceiverService(
|
||||||
ac accesscontrol.AccessControl,
|
authz receiverAccessControlService,
|
||||||
cfgStore alertmanagerConfigStore,
|
cfgStore alertmanagerConfigStore,
|
||||||
provisioningStore provisoningStore,
|
provisioningStore provisoningStore,
|
||||||
encryptionService secrets.Service,
|
encryptionService secrets.Service,
|
||||||
@ -58,7 +63,7 @@ func NewReceiverService(
|
|||||||
log log.Logger,
|
log log.Logger,
|
||||||
) *ReceiverService {
|
) *ReceiverService {
|
||||||
return &ReceiverService{
|
return &ReceiverService{
|
||||||
ac: ac,
|
authz: authz,
|
||||||
provisioningStore: provisioningStore,
|
provisioningStore: provisioningStore,
|
||||||
cfgStore: cfgStore,
|
cfgStore: cfgStore,
|
||||||
encryptionService: encryptionService,
|
encryptionService: encryptionService,
|
||||||
@ -69,50 +74,19 @@ func NewReceiverService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ReceiverService) shouldDecrypt(ctx context.Context, user identity.Requester, reqDecrypt bool) (bool, error) {
|
func (rs *ReceiverService) shouldDecrypt(ctx context.Context, user identity.Requester, reqDecrypt bool) (bool, error) {
|
||||||
decryptAccess, err := rs.hasReadDecrypted(ctx, user)
|
if !reqDecrypt {
|
||||||
if err != nil {
|
return false, nil
|
||||||
|
}
|
||||||
|
if err := rs.authz.AuthorizeReadDecryptedAll(ctx, user); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if reqDecrypt && !decryptAccess {
|
return true, nil
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetReceiver returns a receiver by name.
|
// GetReceiver returns a receiver by name.
|
||||||
// The receiver's secure settings are decrypted if requested and the user has access to do so.
|
// 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) {
|
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)
|
revision, err := rs.cfgStore.Get(ctx, q.OrgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return definitions.GettableApiReceiver{}, err
|
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.
|
// 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.
|
// 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) {
|
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))
|
uids := make([]string, 0, len(q.Names))
|
||||||
for _, name := range q.Names {
|
for _, name := range q.Names {
|
||||||
uids = append(uids, legacy_storage.NameToUid(name))
|
uids = append(uids, legacy_storage.NameToUid(name))
|
||||||
@ -159,12 +129,17 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
readRedactedAccess, err := rs.hasReadRedacted(ctx, user)
|
decrypt, err := rs.shouldDecrypt(ctx, user, q.Decrypt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// 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.
|
// This is mostly a safeguard as it should not be possible with current API endpoints + middleware authentication.
|
||||||
if !listAccess && !readRedactedAccess {
|
if !listAccess && !readRedactedAccess {
|
||||||
return nil, ac.NewAuthorizationErrorWithPermissions("read any receiver", nil) // TODO: Replace with authz service.
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var output []definitions.GettableApiReceiver
|
var output []definitions.GettableApiReceiver
|
||||||
for i := q.Offset; i < len(postables); i++ {
|
for i := q.Offset; i < len(postables); i++ {
|
||||||
r := 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, "")
|
decryptFn := rs.decryptOrRedact(ctx, decrypt, r.Name, "")
|
||||||
|
|
||||||
// Only has permission to list. This reduces from:
|
// Only has permission to list. This reduces from:
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"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/api/tooling/definitions"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
"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",
|
name: "service returns error when trying to decrypt without permission",
|
||||||
decrypt: true,
|
decrypt: true,
|
||||||
user: readUser,
|
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",
|
name: "service returns error if user is nil and decrypt is true",
|
||||||
decrypt: true,
|
decrypt: true,
|
||||||
user: nil,
|
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",
|
name: "service decrypts receivers with permission",
|
||||||
@ -189,7 +190,7 @@ func createReceiverServiceSut(t *testing.T, encryptSvc secrets.Service) *Receive
|
|||||||
provisioningStore := fakes.NewFakeProvisioningStore()
|
provisioningStore := fakes.NewFakeProvisioningStore()
|
||||||
|
|
||||||
return NewReceiverService(
|
return NewReceiverService(
|
||||||
acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()),
|
ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), true),
|
||||||
legacy_storage.NewAlertmanagerConfigStore(store),
|
legacy_storage.NewAlertmanagerConfigStore(store),
|
||||||
provisioningStore,
|
provisioningStore,
|
||||||
encryptSvc,
|
encryptSvc,
|
||||||
|
@ -338,7 +338,7 @@ func createContactPointServiceSutWithConfigStore(t *testing.T, secretService sec
|
|||||||
provisioningStore := fakes.NewFakeProvisioningStore()
|
provisioningStore := fakes.NewFakeProvisioningStore()
|
||||||
|
|
||||||
receiverService := notifier.NewReceiverService(
|
receiverService := notifier.NewReceiverService(
|
||||||
acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()),
|
ac.NewReceiverAccess[*models.Receiver](acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), true),
|
||||||
legacy_storage.NewAlertmanagerConfigStore(configStore),
|
legacy_storage.NewAlertmanagerConfigStore(configStore),
|
||||||
provisioningStore,
|
provisioningStore,
|
||||||
secretService,
|
secretService,
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/encryption"
|
"github.com/grafana/grafana/pkg/services/encryption"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
alertingauthz "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
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"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
"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)
|
configStore := legacy_storage.NewAlertmanagerConfigStore(&st)
|
||||||
receiverSvc := notifier.NewReceiverService(
|
receiverSvc := notifier.NewReceiverService(
|
||||||
ps.ac,
|
alertingauthz.NewReceiverAccess[*ngmodels.Receiver](ps.ac, true),
|
||||||
configStore,
|
configStore,
|
||||||
st,
|
st,
|
||||||
ps.secretService,
|
ps.secretService,
|
||||||
|
Loading…
Reference in New Issue
Block a user