Skip to content

Commit 32f06c6

Browse files
authored
Alerting: Receiver API complete core implementation (grafana#91738)
* Replace global authz abstraction with one compatible with uid scope * Replace GettableApiReceiver with models.Receiver in receiver_svc * GrafanaIntegrationConfig -> models.Integration * Implement Create/Update methods * Add optimistic concurrency to receiver API * Add scope to ReceiversRead & ReceiversReadSecrets migrates existing permissions to include implicit global scope * Add receiver create, update, delete actions * Check if receiver is used by rules before delete * On receiver name change update in routes and notification settings * Improve errors * Linting * Include read permissions are requirements for create/update/delete * Alias ngalert/models to ngmodels to differentiate from v0alpha1 model * Ensure integration UIDs are valid, unique, and generated if empty * Validate integration settings on create/update * Leverage UidToName to GetReceiver instead of GetReceivers * Remove some unnecessary uses of simplejson * alerting.notifications.receiver -> alerting.notifications.receivers * validator -> provenanceValidator * Only validate the modified receiver stops existing invalid receivers from preventing modification of a valid receiver. * Improve error in Integration.Encrypt * Remove scope from alert.notifications.receivers:create * Add todos for receiver renaming * Use receiverAC precondition checks in k8s api * Linting * Optional optimistic concurrency for delete * make update-workspace * More specific auth checks in k8s authorize.go * Add debug log when delete optimistic concurrency is skipped * Improve error message on authorizer.DecisionDeny * Keep error for non-forbidden errutil errors
1 parent 22ad1cc commit 32f06c6

36 files changed

+3574
-539
lines changed

pkg/registry/apis/alerting/notifications/receiver/authorize.go

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,26 @@ package receiver
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
57

68
"k8s.io/apiserver/pkg/authorization/authorizer"
79

10+
"github.com/grafana/grafana/pkg/apimachinery/errutil"
811
"github.com/grafana/grafana/pkg/apimachinery/identity"
9-
"github.com/grafana/grafana/pkg/services/accesscontrol"
12+
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
1013
)
1114

12-
func Authorize(ctx context.Context, ac accesscontrol.AccessControl, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
15+
// AccessControlService provides access control for receivers.
16+
type AccessControlService interface {
17+
AuthorizeReadSome(ctx context.Context, user identity.Requester) error
18+
AuthorizeReadByUID(context.Context, identity.Requester, string) error
19+
AuthorizeCreate(context.Context, identity.Requester) error
20+
AuthorizeUpdateByUID(context.Context, identity.Requester, string) error
21+
AuthorizeDeleteByUID(context.Context, identity.Requester, string) error
22+
}
23+
24+
func Authorize(ctx context.Context, ac AccessControlService, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
1325
if attr.GetResource() != resourceInfo.GroupResource().Resource {
1426
return authorizer.DecisionNoOpinion, "", nil
1527
}
@@ -18,36 +30,55 @@ func Authorize(ctx context.Context, ac accesscontrol.AccessControl, attr authori
1830
return authorizer.DecisionDeny, "valid user is required", err
1931
}
2032

21-
var action accesscontrol.Evaluator
33+
uid := attr.GetName()
34+
35+
deny := func(err error) (authorizer.Decision, string, error) {
36+
var utilErr errutil.Error
37+
if errors.As(err, &utilErr) && utilErr.Reason.Status() == errutil.StatusForbidden {
38+
if errors.Is(err, accesscontrol.ErrAuthorizationBase) {
39+
return authorizer.DecisionDeny, fmt.Sprintf("required permissions: %s", utilErr.PublicPayload["permissions"]), nil
40+
}
41+
return authorizer.DecisionDeny, utilErr.PublicMessage, nil
42+
}
43+
44+
return authorizer.DecisionDeny, "", err
45+
}
46+
2247
switch attr.GetVerb() {
48+
case "get":
49+
if uid == "" {
50+
return authorizer.DecisionDeny, "", nil
51+
}
52+
if err := ac.AuthorizeReadByUID(ctx, user, uid); err != nil {
53+
return deny(err)
54+
}
55+
case "list":
56+
if err := ac.AuthorizeReadSome(ctx, user); err != nil { // Preconditions, further checks are done downstream.
57+
return deny(err)
58+
}
59+
case "create":
60+
if err := ac.AuthorizeCreate(ctx, user); err != nil {
61+
return deny(err)
62+
}
2363
case "patch":
2464
fallthrough
25-
case "create":
26-
fallthrough // TODO: Add alert.notifications.receivers:create permission
2765
case "update":
28-
action = accesscontrol.EvalAny(
29-
accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsWrite), // TODO: Add alert.notifications.receivers:write permission
30-
)
31-
case "deletecollection":
32-
fallthrough
66+
if uid == "" {
67+
return deny(err)
68+
}
69+
if err := ac.AuthorizeUpdateByUID(ctx, user, uid); err != nil {
70+
return deny(err)
71+
}
3372
case "delete":
34-
action = accesscontrol.EvalAny(
35-
accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsWrite), // TODO: Add alert.notifications.receivers:delete permission
36-
)
37-
}
38-
39-
eval := accesscontrol.EvalAny(
40-
accesscontrol.EvalPermission(accesscontrol.ActionAlertingReceiversRead),
41-
accesscontrol.EvalPermission(accesscontrol.ActionAlertingReceiversReadSecrets),
42-
accesscontrol.EvalPermission(accesscontrol.ActionAlertingNotificationsRead),
43-
)
44-
if action != nil {
45-
eval = accesscontrol.EvalAll(eval, action)
73+
if uid == "" {
74+
return deny(err)
75+
}
76+
if err := ac.AuthorizeDeleteByUID(ctx, user, uid); err != nil {
77+
return deny(err)
78+
}
79+
default:
80+
return authorizer.DecisionNoOpinion, "", nil
4681
}
4782

48-
ok, err := ac.Evaluate(ctx, user, eval)
49-
if ok {
50-
return authorizer.DecisionAllow, "", nil
51-
}
52-
return authorizer.DecisionDeny, "", err
83+
return authorizer.DecisionAllow, "", nil
5384
}
Lines changed: 42 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
package receiver
22

33
import (
4-
"encoding/json"
5-
"fmt"
4+
"maps"
65

7-
"github.com/prometheus/alertmanager/config"
86
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
97
"k8s.io/apimachinery/pkg/types"
108

119
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
1210
model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
1311
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
14-
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
15-
"github.com/grafana/grafana/pkg/services/ngalert/models"
12+
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
1613
"github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage"
1714
)
1815

19-
func getUID(t definitions.GettableApiReceiver) string {
20-
return legacy_storage.NameToUid(t.Name)
21-
}
22-
23-
func convertToK8sResources(orgID int64, receivers []definitions.GettableApiReceiver, namespacer request.NamespaceMapper) (*model.ReceiverList, error) {
16+
func convertToK8sResources(orgID int64, receivers []*ngmodels.Receiver, namespacer request.NamespaceMapper) (*model.ReceiverList, error) {
2417
result := &model.ReceiverList{
2518
Items: make([]model.Receiver, 0, len(receivers)),
2619
}
@@ -34,76 +27,75 @@ func convertToK8sResources(orgID int64, receivers []definitions.GettableApiRecei
3427
return result, nil
3528
}
3629

37-
func convertToK8sResource(orgID int64, receiver definitions.GettableApiReceiver, namespacer request.NamespaceMapper) (*model.Receiver, error) {
30+
func convertToK8sResource(orgID int64, receiver *ngmodels.Receiver, namespacer request.NamespaceMapper) (*model.Receiver, error) {
3831
spec := model.ReceiverSpec{
39-
Title: receiver.Receiver.Name,
32+
Title: receiver.Name,
4033
}
41-
provenance := definitions.Provenance(models.ProvenanceNone)
42-
for _, integration := range receiver.GrafanaManagedReceivers {
43-
if integration.Provenance != receiver.GrafanaManagedReceivers[0].Provenance {
44-
return nil, fmt.Errorf("all integrations must have the same provenance")
45-
}
46-
provenance = integration.Provenance
47-
unstruct := common.Unstructured{}
48-
err := json.Unmarshal(integration.Settings, &unstruct)
49-
if err != nil {
50-
return nil, fmt.Errorf("integration '%s' of receiver '%s' has settings that cannot be parsed as JSON: %w", integration.Type, receiver.Name, err)
51-
}
34+
for _, integration := range receiver.Integrations {
5235
spec.Integrations = append(spec.Integrations, model.Integration{
5336
Uid: &integration.UID,
54-
Type: integration.Type,
37+
Type: integration.Config.Type,
5538
DisableResolveMessage: &integration.DisableResolveMessage,
56-
Settings: unstruct,
57-
SecureFields: integration.SecureFields,
39+
Settings: common.Unstructured{Object: maps.Clone(integration.Settings)},
40+
SecureFields: integration.SecureFields(),
5841
})
5942
}
6043

61-
uid := getUID(receiver) // TODO replace to stable UID when we switch to normal storage
6244
r := &model.Receiver{
6345
TypeMeta: resourceInfo.TypeMeta(),
6446
ObjectMeta: metav1.ObjectMeta{
65-
UID: types.UID(uid), // This is needed to make PATCH work
66-
Name: uid, // TODO replace to stable UID when we switch to normal storage
47+
UID: types.UID(receiver.GetUID()), // This is needed to make PATCH work
48+
Name: receiver.GetUID(),
6749
Namespace: namespacer(orgID),
68-
ResourceVersion: "", // TODO: Implement optimistic concurrency.
50+
ResourceVersion: receiver.Version,
6951
},
7052
Spec: spec,
7153
}
72-
r.SetProvenanceStatus(string(provenance))
54+
r.SetProvenanceStatus(string(receiver.Provenance))
7355
return r, nil
7456
}
7557

76-
func convertToDomainModel(receiver *model.Receiver) (definitions.GettableApiReceiver, error) {
77-
// TODO: Using GettableApiReceiver instead of PostableApiReceiver so that SecureFields type matches.
78-
gettable := definitions.GettableApiReceiver{
79-
Receiver: config.Receiver{
80-
Name: receiver.Spec.Title,
81-
},
82-
GettableGrafanaReceivers: definitions.GettableGrafanaReceivers{
83-
GrafanaManagedReceivers: []*definitions.GettableGrafanaReceiver{},
84-
},
58+
func convertToDomainModel(receiver *model.Receiver) (*ngmodels.Receiver, map[string][]string, error) {
59+
domain := &ngmodels.Receiver{
60+
UID: legacy_storage.NameToUid(receiver.Spec.Title),
61+
Name: receiver.Spec.Title,
62+
Integrations: make([]*ngmodels.Integration, 0, len(receiver.Spec.Integrations)),
63+
Version: receiver.ResourceVersion,
64+
Provenance: ngmodels.ProvenanceNone,
8565
}
8666

67+
storedSecureFields := make(map[string][]string, len(receiver.Spec.Integrations))
8768
for _, integration := range receiver.Spec.Integrations {
88-
data, err := integration.Settings.MarshalJSON()
69+
config, err := ngmodels.IntegrationConfigFromType(integration.Type)
8970
if err != nil {
90-
return definitions.GettableApiReceiver{}, fmt.Errorf("integration '%s' of receiver '%s' is invalid: failed to convert unstructured data to bytes: %w", integration.Type, receiver.Name, err)
71+
return nil, nil, err
9172
}
92-
grafanaIntegration := definitions.GettableGrafanaReceiver{
93-
Name: receiver.Spec.Title,
94-
Type: integration.Type,
95-
Settings: definitions.RawMessage(data),
96-
SecureFields: integration.SecureFields,
97-
Provenance: definitions.Provenance(models.ProvenanceNone),
73+
grafanaIntegration := ngmodels.Integration{
74+
Name: receiver.Spec.Title,
75+
Config: config,
76+
Settings: maps.Clone(integration.Settings.UnstructuredContent()),
77+
SecureSettings: make(map[string]string),
9878
}
9979
if integration.Uid != nil {
10080
grafanaIntegration.UID = *integration.Uid
10181
}
10282
if integration.DisableResolveMessage != nil {
10383
grafanaIntegration.DisableResolveMessage = *integration.DisableResolveMessage
10484
}
105-
gettable.GettableGrafanaReceivers.GrafanaManagedReceivers = append(gettable.GettableGrafanaReceivers.GrafanaManagedReceivers, &grafanaIntegration)
85+
86+
domain.Integrations = append(domain.Integrations, &grafanaIntegration)
87+
88+
if grafanaIntegration.UID != "" {
89+
// This is an existing integration, so we track the secure fields being requested to copy over from existing values.
90+
secureFields := make([]string, 0, len(integration.SecureFields))
91+
for k, isSecure := range integration.SecureFields {
92+
if isSecure {
93+
secureFields = append(secureFields, k)
94+
}
95+
}
96+
storedSecureFields[grafanaIntegration.UID] = secureFields
97+
}
10698
}
10799

108-
return gettable, nil
100+
return domain, storedSecureFields, nil
109101
}

0 commit comments

Comments
 (0)