Alerting: Remove the POST endpoint for the internal Grafana Alertmanager config (#103819)

* Remove POST config for Grafana Alertmanager

* Delete auth + test for removed path

* Alerting: Remove check for `alertingApiServer` toggle in UI (#103805)

* Remove check for alertingApiServer in UI

* Update tests to no longer care about alertingApiServer

* Add contact points handlers now that we use alertingApiServer all the time

* Fix test broken from removing camelCase for UIDs

---------

Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
This commit is contained in:
William Wernert
2025-04-11 17:38:53 -04:00
committed by GitHub
co-authored by Tom Ratcliffe
parent c47ab101d1
commit a5288db624
50 changed files with 687 additions and 1928 deletions
+6 -12
View File
@@ -429,20 +429,14 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
contactPointsPerms := []ac.Evaluator{
ac.EvalPermission(ac.ActionAlertingNotificationsRead),
ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead),
}
// With the new alerting API, we have other permissions to consider. We don't want to consider these with the old
// alerting API to maintain backwards compatibility.
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingApiServer) {
contactPointsPerms = append(contactPointsPerms,
ac.EvalPermission(ac.ActionAlertingReceiversRead),
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets),
ac.EvalPermission(ac.ActionAlertingReceiversCreate),
ac.EvalPermission(ac.ActionAlertingReceiversRead),
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets),
ac.EvalPermission(ac.ActionAlertingReceiversCreate),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesRead),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesWrite),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesDelete),
)
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesRead),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesWrite),
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesDelete),
}
if hasAccess(ac.EvalAny(contactPointsPerms...)) {
+3 -4
View File
@@ -381,10 +381,9 @@ func DeclareFixedRoles(service accesscontrol.Service, features featuremgmt.Featu
instancesReaderRole, instancesWriterRole,
notificationsReaderRole, notificationsWriterRole,
alertingReaderRole, alertingWriterRole, alertingAdminRole, alertingProvisionerRole, alertingProvisioningReaderWithSecretsRole, alertingProvisioningStatus,
}
if features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) {
fixedRoles = append(fixedRoles, receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole, timeIntervalsReaderRole, timeIntervalsWriterRole, routesReaderRole, routesWriterRole)
// k8s roles
receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole,
timeIntervalsReaderRole, timeIntervalsWriterRole, routesReaderRole, routesWriterRole,
}
return service.DeclareFixedRoles(fixedRoles...)
@@ -188,52 +188,6 @@ func (srv AlertmanagerSrv) RoutePostGrafanaAlertingConfigHistoryActivate(c *cont
return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration activated"})
}
func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *contextmodel.ReqContext, body apimodels.PostableUserConfig) response.Response {
// Remove autogenerated config from the user config before checking provenance guard and eventually saving it.
// TODO: This and provenance guard should be moved to the notifier package.
notifier.RemoveAutogenConfigIfExists(body.AlertmanagerConfig.Route)
currentConfig, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.GetOrgID(), false)
// If a config is present and valid we proceed with the guard, otherwise we
// just bypass the guard which is okay as we are anyway in an invalid state.
if err == nil {
if err := srv.provenanceGuard(currentConfig, body); err != nil {
return ErrResp(http.StatusBadRequest, err, "")
}
}
if srv.featureManager.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingApiServer) {
if err != nil {
// Unclear if returning an error here is the right thing to do, preventing the user from posting a new config
// when the current one is legitimately invalid is not optimal, but we need to ensure receiver
// permissions are maintained and prevent potential access control bypasses. The workaround is to use the
// various new k8s API endpoints to fix the configuration.
return ErrResp(http.StatusInternalServerError, err, "")
}
if err := srv.k8sApiServiceGuard(currentConfig, body); err != nil {
return ErrResp(http.StatusBadRequest, err, "")
}
}
err = srv.mam.SaveAndApplyAlertmanagerConfiguration(c.Req.Context(), c.GetOrgID(), body)
if err == nil {
return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration created"})
}
var unknownReceiverError notifier.UnknownReceiverError
if errors.As(err, &unknownReceiverError) {
return ErrResp(http.StatusBadRequest, unknownReceiverError, "")
}
var configRejectedError notifier.AlertmanagerConfigRejectedError
if errors.As(err, &configRejectedError) {
return ErrResp(http.StatusBadRequest, configRejectedError, "")
}
if errors.Is(err, notifier.ErrNoAlertmanagerForOrg) {
return response.Error(http.StatusNotFound, err.Error(), err)
}
if errors.Is(err, notifier.ErrAlertmanagerNotReady) {
return response.Error(http.StatusConflict, err.Error(), err)
}
return response.ErrOrFallback(http.StatusInternalServerError, err.Error(), err)
}
func (srv AlertmanagerSrv) RouteGetReceivers(c *contextmodel.ReqContext) response.Response {
am, errResp := srv.AlertmanagerFor(c.GetOrgID())
if errResp != nil {
@@ -16,46 +16,6 @@ import (
"github.com/grafana/grafana/pkg/util/cmputil"
)
func (srv AlertmanagerSrv) provenanceGuard(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error {
if err := checkRoutes(currentConfig, newConfig); err != nil {
return err
}
if err := checkTemplates(currentConfig, newConfig); err != nil {
return err
}
if err := checkContactPoints(currentConfig.AlertmanagerConfig.Receivers, newConfig.AlertmanagerConfig.Receivers); err != nil {
return err
}
if err := checkMuteTimes(currentConfig, newConfig); err != nil {
return err
}
return nil
}
func (srv AlertmanagerSrv) k8sApiServiceGuard(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error {
// Modifications to receivers via this API is tricky with new per-receiver RBAC. Assuming we restrict the API to only
// those users with global edit permissions, we would still need to consider the following:
// - Since the UIDs stored in the database for the purposes of per-receiver RBAC are generated based on the receiver
// name, we would need to ensure continuity of permissions when a receiver is renamed. This would, preferably,
// require detecting renames and updating the permissions UID in the database.
// - Editors don't have permission to manage all receiver permissions by default, only admin users do. This means that
// certain combined operations (e.g. swapping the name of receivers) would require careful handling to ensure
// that the correct permissions are maintained, and considering receivers don't have a unique identifier outside
// their name, this is non-trivial.
// Neither of these are insurmountable, but considering this endpoint will be removed once FlagAlertingApiServer
// becomes GA, the complexity may not be worthwhile. To that end, for now we reject any request that attempts to
// update receivers, while allowing operations that add or remove receivers.
delta, err := calculateReceiversDelta(currentConfig.AlertmanagerConfig.Receivers, newConfig.AlertmanagerConfig.Receivers)
if err != nil {
return err
}
if len(delta.Updated) > 0 {
return fmt.Errorf("cannot update receivers using this API while per-receiver RBAC is enabled; either disable the `alertingApiServer` feature flag or use an API that supports per-receiver RBAC (e.g. provisioning or receivers API)")
}
return nil
}
func checkRoutes(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error {
reporter := cmputil.DiffReporter{}
options := []cmp.Option{cmp.Reporter(&reporter), cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(labels.Matcher{}), cmp.Transformer("", func(regexp amConfig.Regexp) any {
+12 -160
View File
@@ -2,7 +2,6 @@ package api
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/json"
"net/http"
@@ -101,90 +100,6 @@ func TestContextWithTimeoutFromRequest(t *testing.T) {
}
func TestAlertmanagerConfig(t *testing.T) {
sut := createSut(t)
t.Run("assert 404 Not Found when applying config to nonexistent org", func(t *testing.T) {
rc := contextmodel.ReqContext{
Context: &web.Context{
Req: &http.Request{},
},
SignedInUser: &user.SignedInUser{
OrgID: 12,
},
}
request := createAmConfigRequest(t, validConfig)
response := sut.RoutePostAlertingConfig(&rc, request)
require.Equal(t, 404, response.Status())
require.Contains(t, string(response.Body()), "Alertmanager does not exist for this organization")
})
t.Run("assert 202 when config successfully applied", func(t *testing.T) {
rc := contextmodel.ReqContext{
Context: &web.Context{
Req: &http.Request{},
},
SignedInUser: &user.SignedInUser{
OrgID: 1,
},
}
request := createAmConfigRequest(t, validConfig)
response := sut.RoutePostAlertingConfig(&rc, request)
require.Equal(t, 202, response.Status())
})
t.Run("assert 202 when alertmanager to configure is not ready", func(t *testing.T) {
sut := createSut(t)
rc := contextmodel.ReqContext{
Context: &web.Context{
Req: &http.Request{},
},
SignedInUser: &user.SignedInUser{
OrgID: 3, // Org 3 was initialized with broken config.
},
}
request := createAmConfigRequest(t, validConfig)
response := sut.RoutePostAlertingConfig(&rc, request)
require.Equal(t, 202, response.Status())
})
t.Run("assert config hash doesn't change when sending RouteGetAlertingConfig back to RoutePostAlertingConfig", func(t *testing.T) {
rc := contextmodel.ReqContext{
Context: &web.Context{
Req: &http.Request{},
},
SignedInUser: &user.SignedInUser{
OrgID: 1,
},
}
request := createAmConfigRequest(t, validConfigWithSecureSetting)
r := sut.RoutePostAlertingConfig(&rc, request)
require.Equal(t, 202, r.Status())
getResponse := sut.RouteGetAlertingConfig(&rc)
require.Equal(t, 200, getResponse.Status())
body := getResponse.Body()
hash := md5.Sum(body)
postable, err := notifier.Load(body)
require.NoError(t, err)
r = sut.RoutePostAlertingConfig(&rc, *postable)
require.Equal(t, 202, r.Status())
getResponse = sut.RouteGetAlertingConfig(&rc)
require.Equal(t, 200, getResponse.Status())
newHash := md5.Sum(getResponse.Body())
require.Equal(t, hash, newHash)
})
t.Run("when objects are not provisioned", func(t *testing.T) {
t.Run("route from GET config has no provenance", func(t *testing.T) {
sut := createSut(t)
@@ -229,9 +144,8 @@ func TestAlertmanagerConfig(t *testing.T) {
t.Run("contact point from GET config has expected provenance", func(t *testing.T) {
sut := createSut(t)
rc := createRequestCtxInOrg(1)
request := createAmConfigRequest(t, validConfig)
_ = sut.RoutePostAlertingConfig(rc, request)
RoutePostAlertingConfig(t, sut.mam, rc, validConfig)
response := sut.RouteGetAlertingConfig(rc)
body := asGettableUserConfig(t, response)
@@ -347,11 +261,8 @@ func TestGetAlertmanagerConfiguration_NewSecretField(t *testing.T) {
}
}
`
postable := createAmConfigRequest(t, postWithoutChanges)
res = sut.RoutePostAlertingConfig(rc, postable)
require.Equal(t, 202, res.Status())
RoutePostAlertingConfig(t, sut.mam, rc, postWithoutChanges)
// Check that the secret field "integrationKey" is now encrypted in SecureSettings.
savedConfig := &apimodels.PostableUserConfig{}
err = json.Unmarshal([]byte(configs[orgId].AlertmanagerConfiguration), savedConfig)
@@ -400,31 +311,6 @@ func TestAlertmanagerAutogenConfig(t *testing.T) {
}
}
t.Run("route POST config", func(t *testing.T) {
t.Run("does not save autogen routes", func(t *testing.T) {
sut, configs := createSutForAutogen(t)
rc := createRequestCtxInOrg(1)
request := createAmConfigRequest(t, validConfigWithAutogen)
response := sut.RoutePostAlertingConfig(rc, request)
require.Equal(t, 202, response.Status())
compare(t, validConfigWithoutAutogen, configs[1].AlertmanagerConfiguration)
})
t.Run("provenance guard ignores autogen routes", func(t *testing.T) {
sut := createSut(t)
rc := createRequestCtxInOrg(1)
request := createAmConfigRequest(t, validConfigWithoutAutogen)
_ = sut.RoutePostAlertingConfig(rc, request)
setRouteProvenance(t, 1, sut.mam.ProvStore)
request = createAmConfigRequest(t, validConfigWithAutogen)
request.AlertmanagerConfig.Route.Provenance = apimodels.Provenance(ngmodels.ProvenanceAPI)
response := sut.RoutePostAlertingConfig(rc, request)
require.Equal(t, 202, response.Status())
})
})
t.Run("route GET config", func(t *testing.T) {
t.Run("when admin return autogen routes", func(t *testing.T) {
sut, _ := createSutForAutogen(t)
@@ -691,16 +577,6 @@ func createSut(t *testing.T) AlertmanagerSrv {
}
}
func createAmConfigRequest(t *testing.T, config string) apimodels.PostableUserConfig {
t.Helper()
request := apimodels.PostableUserConfig{}
err := request.UnmarshalJSON([]byte(config))
require.NoError(t, err)
return request
}
func createMultiOrgAlertmanager(t *testing.T, configs map[int64]*ngmodels.AlertConfiguration) *notifier.MultiOrgAlertmanager {
t.Helper()
@@ -849,40 +725,6 @@ var validConfigWithAutogen = `{
}
`
var validConfigWithSecureSetting = `{
"template_files": {
"a": "template"
},
"alertmanager_config": {
"route": {
"receiver": "grafana-default-email"
},
"receivers": [{
"name": "grafana-default-email",
"grafana_managed_receiver_configs": [{
"uid": "",
"name": "email receiver",
"type": "email",
"settings": {
"addresses": "<example@email.com>"
}
}]},
{
"name": "slack",
"grafana_managed_receiver_configs": [{
"uid": "",
"name": "slack1",
"type": "slack",
"settings": {"text": "slack text"},
"secureSettings": {
"url": "secure url"
}
}]
}]
}
}
`
var brokenConfig = `
"alertmanager_config": {
"route": {
@@ -947,3 +789,13 @@ func asGettableHistoricUserConfigs(t *testing.T, r response.Response) []apimodel
require.NoError(t, err)
return body
}
// RoutePostAlertingConfig drop-in replacement for removed POST endpoint to make test transition easier.
func RoutePostAlertingConfig(t *testing.T, mam *notifier.MultiOrgAlertmanager, rc *contextmodel.ReqContext, amConfig string) {
t.Helper()
cfg := apimodels.PostableUserConfig{}
err := json.Unmarshal([]byte(amConfig), &cfg)
require.NoError(t, err)
err = mam.SaveAndApplyAlertmanagerConfiguration(rc.Req.Context(), 1, cfg)
require.NoError(t, err)
}
+3 -10
View File
@@ -8,7 +8,6 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/web"
)
@@ -229,9 +228,6 @@ func (api *API) authorize(method, path string) web.Handler {
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
case http.MethodGet + "/api/alertmanager/grafana/api/v2/status":
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/alerts":
// additional authorization is done in the request handler
eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsWrite))
case http.MethodPost + "/api/alertmanager/grafana/config/history/{id}/_activate":
eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsWrite))
case http.MethodGet + "/api/alertmanager/grafana/config/api/v1/receivers":
@@ -296,12 +292,9 @@ func (api *API) authorize(method, path string) web.Handler {
ac.EvalPermission(ac.ActionAlertingProvisioningRead), // organization scope
ac.EvalPermission(ac.ActionAlertingNotificationsProvisioningRead), // organization scope
ac.EvalPermission(ac.ActionAlertingProvisioningReadSecrets), // organization scope
}
if api.FeatureManager.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) {
perms = append(perms,
ac.EvalPermission(ac.ActionAlertingReceiversRead),
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets),
)
ac.EvalPermission(ac.ActionAlertingReceiversRead), // organization scope
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets), // organization scope
}
eval = ac.EvalAny(perms...)
@@ -175,13 +175,6 @@ func (f *AlertmanagerApiHandler) handleRouteGetGrafanaSilences(ctx *contextmodel
return f.GrafanaSvc.RouteGetSilences(ctx)
}
func (f *AlertmanagerApiHandler) handleRoutePostGrafanaAlertingConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableUserConfig) response.Response {
if !conf.AlertmanagerConfig.ReceiverType().Can(apimodels.GrafanaReceiverType) {
return errorToResponse(backendTypeDoesNotMatchPayloadTypeError(apimodels.GrafanaBackend, conf.AlertmanagerConfig.ReceiverType().String()))
}
return f.GrafanaSvc.RoutePostAlertingConfig(ctx, conf)
}
func (f *AlertmanagerApiHandler) handleRouteGetGrafanaReceivers(ctx *contextmodel.ReqContext) response.Response {
return f.GrafanaSvc.RouteGetReceivers(ctx)
}
@@ -42,7 +42,6 @@ type AlertmanagerApi interface {
RouteGetSilences(*contextmodel.ReqContext) response.Response
RoutePostAMAlerts(*contextmodel.ReqContext) response.Response
RoutePostAlertingConfig(*contextmodel.ReqContext) response.Response
RoutePostGrafanaAlertingConfig(*contextmodel.ReqContext) response.Response
RoutePostGrafanaAlertingConfigHistoryActivate(*contextmodel.ReqContext) response.Response
RoutePostTestGrafanaReceivers(*contextmodel.ReqContext) response.Response
RoutePostTestGrafanaTemplates(*contextmodel.ReqContext) response.Response
@@ -162,14 +161,6 @@ func (f *AlertmanagerApiHandler) RoutePostAlertingConfig(ctx *contextmodel.ReqCo
}
return f.handleRoutePostAlertingConfig(ctx, conf, datasourceUIDParam)
}
func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfig(ctx *contextmodel.ReqContext) response.Response {
// Parse Request Body
conf := apimodels.PostableUserConfig{}
if err := web.Bind(ctx.Req, &conf); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return f.handleRoutePostGrafanaAlertingConfig(ctx, conf)
}
func (f *AlertmanagerApiHandler) RoutePostGrafanaAlertingConfigHistoryActivate(ctx *contextmodel.ReqContext) response.Response {
// Parse Path Parameters
idParam := web.Params(ctx.Req)[":id"]
@@ -458,18 +449,6 @@ func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApi, m *metrics
m,
),
)
group.Post(
toMacaronPath("/api/alertmanager/grafana/config/api/v1/alerts"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
api.authorize(http.MethodPost, "/api/alertmanager/grafana/config/api/v1/alerts"),
metrics.Instrument(
http.MethodPost,
"/api/alertmanager/grafana/config/api/v1/alerts",
api.Hooks.Wrap(srv.RoutePostGrafanaAlertingConfig),
m,
),
)
group.Post(
toMacaronPath("/api/alertmanager/grafana/config/history/{id}/_activate"),
requestmeta.SetOwner(requestmeta.TeamAlerting),
@@ -16,17 +16,6 @@ import (
alertingmodels "github.com/grafana/alerting/models"
)
// swagger:route POST /alertmanager/grafana/config/api/v1/alerts alertmanager RoutePostGrafanaAlertingConfig
//
// sets an Alerting config
//
// This API is designated to internal use only and can be removed or changed at any time without prior notice.
//
// Deprecated: true
// Responses:
// 201: Ack
// 400: ValidationError
// swagger:route POST /alertmanager/{DatasourceUID}/config/api/v1/alerts alertmanager RoutePostAlertingConfig
//
// sets an Alerting config
@@ -5696,38 +5696,6 @@
"tags": [
"alertmanager"
]
},
"post": {
"deprecated": true,
"description": "This API is designated to internal use only and can be removed or changed at any time without prior notice.",
"operationId": "RoutePostGrafanaAlertingConfig",
"parameters": [
{
"in": "body",
"name": "Body",
"schema": {
"$ref": "#/definitions/PostableUserConfig"
}
}
],
"responses": {
"201": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
},
"summary": "sets an Alerting config",
"tags": [
"alertmanager"
]
}
},
"/alertmanager/grafana/config/api/v1/receivers": {
@@ -321,38 +321,6 @@
}
}
},
"post": {
"description": "This API is designated to internal use only and can be removed or changed at any time without prior notice.",
"tags": [
"alertmanager"
],
"summary": "sets an Alerting config",
"operationId": "RoutePostGrafanaAlertingConfig",
"deprecated": true,
"parameters": [
{
"name": "Body",
"in": "body",
"schema": {
"$ref": "#/definitions/PostableUserConfig"
}
}
],
"responses": {
"201": {
"description": "Ack",
"schema": {
"$ref": "#/definitions/Ack"
}
},
"400": {
"description": "ValidationError",
"schema": {
"$ref": "#/definitions/ValidationError"
}
}
}
},
"delete": {
"description": "This API is designated to internal use only and can be removed or changed at any time without prior notice.",
"tags": [
+3 -3
View File
@@ -1,5 +1,5 @@
// 🌟 This was machine generated. Do not edit. 🌟
//
//
// Frame[0] {
// "type": "directory-listing",
// "typeVersion": [
@@ -10,7 +10,7 @@
// "HasMore": false
// }
// }
// Name:
// Name:
// Dimensions: 3 Fields by 3 Rows
// +----------------------------+----------------------+---------------+
// | Name: name | Name: mediaType | Name: size |
@@ -87,4 +87,4 @@
}
}
]
}
}