diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 2ca4b6fb535..02ccb18a2ec 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -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)) } diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go new file mode 100644 index 00000000000..641856da079 --- /dev/null +++ b/pkg/services/ngalert/api/authorization_test.go @@ -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") + }) + }) +} diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index e8e419fbe23..3e0eec85e0c 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -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") -}