mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add support for fine-grained access to alerting APIs (#46561)
This commit is contained in:
parent
85184ee9dc
commit
c42d2e6f5d
@ -1,11 +1,164 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
//nolint:gocyclo
|
||||
func (api *API) authorize(method, path string) web.Handler {
|
||||
// TODO Add fine-grained authorization for every route
|
||||
return middleware.ReqSignedIn
|
||||
authorize := acmiddleware.Middleware(api.AccessControl)
|
||||
var eval ac.Evaluator = nil
|
||||
|
||||
switch method + path {
|
||||
// Alert Rules
|
||||
|
||||
// Grafana Paths
|
||||
case http.MethodDelete + "/api/ruler/grafana/api/v1/rules/{Namespace}/{Groupname}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace")))
|
||||
case http.MethodDelete + "/api/ruler/grafana/api/v1/rules/{Namespace}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleDelete, dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace")))
|
||||
case http.MethodGet + "/api/ruler/grafana/api/v1/rules/{Namespace}/{Groupname}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleRead, dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace")))
|
||||
case http.MethodGet + "/api/ruler/grafana/api/v1/rules/{Namespace}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleRead, dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace")))
|
||||
case http.MethodGet + "/api/ruler/grafana/api/v1/rules":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
|
||||
case http.MethodPost + "/api/ruler/grafana/api/v1/rules/{Namespace}":
|
||||
scope := dashboards.ScopeFoldersProvider.GetResourceScopeName(ac.Parameter(":Namespace"))
|
||||
// more granular permissions are enforced by the handler via "authorizeRuleChanges"
|
||||
eval = ac.EvalAny(
|
||||
ac.EvalPermission(ac.ActionAlertingRuleUpdate, scope),
|
||||
ac.EvalPermission(ac.ActionAlertingRuleCreate, scope),
|
||||
ac.EvalPermission(ac.ActionAlertingRuleDelete, scope),
|
||||
)
|
||||
|
||||
// Grafana, Prometheus-compatible Paths
|
||||
case http.MethodGet + "/api/prometheus/grafana/api/v1/rules":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
|
||||
|
||||
// Grafana Rules Testing Paths
|
||||
case http.MethodPost + "/api/v1/rule/test/grafana":
|
||||
// additional authorization is done in the request handler
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
|
||||
case http.MethodPost + "/api/v1/eval":
|
||||
// additional authorization is done in the request handler
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
|
||||
|
||||
// Lotex Paths
|
||||
case http.MethodDelete + "/api/ruler/{Recipient}/api/v1/rules/{Namespace}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleExternalWrite, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodDelete + "/api/ruler/{Recipient}/api/v1/rules/{Namespace}/{Groupname}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleExternalWrite, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodGet + "/api/ruler/{Recipient}/api/v1/rules/{Namespace}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodGet + "/api/ruler/{Recipient}/api/v1/rules/{Namespace}/{Groupname}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodGet + "/api/ruler/{Recipient}/api/v1/rules":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodPost + "/api/ruler/{Recipient}/api/v1/rules/{Namespace}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
|
||||
// Lotex Prometheus-compatible Paths
|
||||
case http.MethodGet + "/api/prometheus/{Recipient}/api/v1/rules":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
|
||||
// Lotex Rules testing
|
||||
case http.MethodPost + "/api/v1/rule/test/{Recipient}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
|
||||
// Alert Instances and Silences
|
||||
|
||||
// Silences. Grafana Paths
|
||||
case http.MethodDelete + "/api/alertmanager/grafana/api/v2/silence/{SilenceId}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstanceUpdate) // delete endpoint actually expires silence
|
||||
case http.MethodGet + "/api/alertmanager/grafana/api/v2/silence/{SilenceId}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)
|
||||
case http.MethodGet + "/api/alertmanager/grafana/api/v2/silences":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)
|
||||
case http.MethodPost + "/api/alertmanager/grafana/api/v2/silences":
|
||||
// additional authorization is done in the request handler
|
||||
eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingInstanceCreate), ac.EvalPermission(ac.ActionAlertingInstanceUpdate))
|
||||
|
||||
// Alert Instances. Grafana Paths
|
||||
case http.MethodGet + "/api/alertmanager/grafana/api/v2/alerts/groups":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)
|
||||
case http.MethodGet + "/api/alertmanager/grafana/api/v2/alerts":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)
|
||||
case http.MethodPost + "/api/alertmanager/grafana/api/v2/alerts":
|
||||
eval = ac.EvalAny(ac.EvalPermission(ac.ActionAlertingInstanceCreate), ac.EvalPermission(ac.ActionAlertingInstanceUpdate))
|
||||
|
||||
// Grafana Prometheus-compatible Paths
|
||||
case http.MethodGet + "/api/prometheus/grafana/api/v1/alerts":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstanceRead)
|
||||
|
||||
// Silences. External AM.
|
||||
case http.MethodDelete + "/api/alertmanager/{Recipient}/api/v2/silence/{SilenceId}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodPost + "/api/alertmanager/{Recipient}/api/v2/silences":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodGet + "/api/alertmanager/{Recipient}/api/v2/silence/{SilenceId}":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodGet + "/api/alertmanager/{Recipient}/api/v2/silences":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
|
||||
// Alert instances. External AM.
|
||||
case http.MethodGet + "/api/alertmanager/{Recipient}/api/v2/alerts/groups":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodGet + "/api/alertmanager/{Recipient}/api/v2/alerts":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodPost + "/api/alertmanager/{Recipient}/api/v2/alerts":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
|
||||
// Prometheus-compatible Paths
|
||||
case http.MethodGet + "/api/prometheus/{Recipient}/api/v1/alerts":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
|
||||
// Notification Policies, Contact Points and Templates
|
||||
|
||||
// Grafana Paths
|
||||
case http.MethodDelete + "/api/alertmanager/grafana/config/api/v1/alerts": // reset alertmanager config to the default
|
||||
eval = ac.EvalPermission(ac.ActionAlertingNotificationsDelete)
|
||||
case http.MethodGet + "/api/alertmanager/grafana/config/api/v1/alerts":
|
||||
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.ActionAlertingNotificationsUpdate), ac.EvalPermission(ac.ActionAlertingNotificationsCreate), ac.EvalPermission(ac.ActionAlertingNotificationsDelete))
|
||||
case http.MethodPost + "/api/alertmanager/grafana/config/api/v1/receivers/test":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingNotificationsRead)
|
||||
|
||||
// External Alertmanager Paths
|
||||
case http.MethodDelete + "/api/alertmanager/{Recipient}/config/api/v1/alerts":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingNotificationsDelete, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodGet + "/api/alertmanager/{Recipient}/api/v2/status":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodGet + "/api/alertmanager/{Recipient}/config/api/v1/alerts":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodPost + "/api/alertmanager/{Recipient}/config/api/v1/alerts":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingNotificationsExternalWrite, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
case http.MethodPost + "/api/alertmanager/{Recipient}/config/api/v1/receivers/test":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead, datasources.ScopeDatasourcesProvider.GetResourceScope(ac.Parameter(":Recipient")))
|
||||
|
||||
// Raw Alertmanager Config Paths
|
||||
case http.MethodDelete + "/api/v1/ngalert/admin_config",
|
||||
http.MethodGet + "/api/v1/ngalert/admin_config",
|
||||
http.MethodPost + "/api/v1/ngalert/admin_config",
|
||||
http.MethodGet + "/api/v1/ngalert/alertmanagers":
|
||||
return middleware.ReqOrgAdmin
|
||||
}
|
||||
|
||||
if eval != nil {
|
||||
return authorize(middleware.ReqSignedIn, eval)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("no authorization handler for method [%s] of endpoint [%s]", method, path))
|
||||
}
|
||||
|
63
pkg/services/ngalert/api/authorization_test.go
Normal file
63
pkg/services/ngalert/api/authorization_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/go-openapi/loads"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
)
|
||||
|
||||
func TestAuthorize(t *testing.T) {
|
||||
json, err := os.ReadFile(filepath.Join("tooling", "spec.json"))
|
||||
require.NoError(t, err)
|
||||
swaggerSpec, err := loads.Analyzed(json, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
paths := make(map[string][]string)
|
||||
|
||||
for p, item := range swaggerSpec.Spec().Paths.Paths {
|
||||
var methods []string
|
||||
|
||||
if item.Get != nil {
|
||||
methods = append(methods, http.MethodGet)
|
||||
}
|
||||
if item.Put != nil {
|
||||
methods = append(methods, http.MethodPut)
|
||||
}
|
||||
if item.Post != nil {
|
||||
methods = append(methods, http.MethodPost)
|
||||
}
|
||||
if item.Delete != nil {
|
||||
methods = append(methods, http.MethodDelete)
|
||||
}
|
||||
if item.Patch != nil {
|
||||
methods = append(methods, http.MethodPatch)
|
||||
}
|
||||
paths[p] = methods
|
||||
}
|
||||
require.Len(t, paths, 29)
|
||||
|
||||
ac := acmock.New()
|
||||
api := &API{AccessControl: ac}
|
||||
|
||||
t.Run("should not panic on known routes", func(t *testing.T) {
|
||||
for path, methods := range paths {
|
||||
for _, method := range methods {
|
||||
require.NotPanics(t, func() {
|
||||
api.authorize(method, path)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should panic if route is unknown", func(t *testing.T) {
|
||||
require.Panics(t, func() {
|
||||
api.authorize("test", "test")
|
||||
})
|
||||
})
|
||||
}
|
@ -155,9 +155,6 @@ func (ng *AlertNG) init() error {
|
||||
}
|
||||
api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics())
|
||||
|
||||
if ng.isFgacDisabled() {
|
||||
return nil
|
||||
}
|
||||
return DeclareFixedRoles(ng.accesscontrol)
|
||||
}
|
||||
|
||||
@ -186,9 +183,3 @@ func (ng *AlertNG) IsDisabled() bool {
|
||||
}
|
||||
return !ng.Cfg.UnifiedAlerting.IsEnabled()
|
||||
}
|
||||
|
||||
// TODO temporary. Remove after https://github.com/grafana/grafana/pull/46358 is merged
|
||||
// isFgacDisabled returns true if fine-grained access for Alerting is enabled.
|
||||
func (ng *AlertNG) isFgacDisabled() bool {
|
||||
return ng.Cfg.IsFeatureToggleEnabled == nil || !ng.Cfg.IsFeatureToggleEnabled("alerting_fgac")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user