From f7894db99caea6478b151b3756c63ed39646227f Mon Sep 17 00:00:00 2001 From: Yuriy Tseretyan Date: Tue, 8 Mar 2022 09:22:16 -0500 Subject: [PATCH] Alerting: declare authorization actions, scopes and roles for fine-grained access (#45748) * add actions, roles and route mapping for rule permission * add instance\notification actions * do not declare alerting roles if no feature flag is set (temporary) --- pkg/services/ngalert/api/api.go | 2 + pkg/services/ngalert/api/authorization.go | 130 ++++++++++++++++++++++ pkg/services/ngalert/ngalert.go | 12 +- pkg/services/ngalert/tests/util.go | 4 +- 4 files changed, 144 insertions(+), 4 deletions(-) diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 3f513e79ad5..b80f4f469b9 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -70,6 +71,7 @@ type API struct { MultiOrgAlertmanager *notifier.MultiOrgAlertmanager StateManager *state.Manager SecretsService secrets.Service + AccessControl accesscontrol.AccessControl } // RegisterAPIEndpoints registers API handlers diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 2ca4b6fb535..c1a3e163c74 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -2,9 +2,139 @@ package api import ( "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/models" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/web" ) +var ( + // Namespaces (aka folder) scopes + ScopeNamespace = "namespaces" + ScopeNamespaceAll = ac.GetResourceAllScope(ScopeNamespace) + ScopeNamespaceName = ac.Scope(ScopeNamespace, "title", ac.Parameter(":Namespace")) + + ScopeDatasource = "datasources" + ScopeDatasourcesAll = ac.GetResourceAllScope(ScopeDatasource) + ScopeDatasourceID = ac.Scope(ScopeDatasource, "id", ac.Parameter(":Recipient")) + + // Alerting rules actions + ActionAlertingRuleCreate = "alert.rules:create" + ActionAlertingRuleRead = "alert.rules:read" + ActionAlertingRuleUpdate = "alert.rules:update" + ActionAlertingRuleDelete = "alert.rules:delete" + + // Alerting instances (+silences) actions + ActionAlertingInstanceCreate = "alert.instances:create" + ActionAlertingInstanceUpdate = "alert.instances:update" + ActionAlertingInstanceRead = "alert.instances:read" + + // Alerting Notification policies actions + ActionAlertingNotificationsCreate = "alert.notifications:create" + ActionAlertingNotificationsRead = "alert.notifications:read" + ActionAlertingNotificationsUpdate = "alert.notifications:update" + ActionAlertingNotificationsDelete = "alert.notifications:delete" +) + +var ( + alertingReader = ac.FixedRolePrefix + "alerting:reader" + alertingWriter = ac.FixedRolePrefix + "alerting:writer" + + alertingReaderRole = ac.RoleRegistration{ + Role: ac.RoleDTO{ + Name: alertingReader, + DisplayName: "Alerting Rules Reader", + Description: "Read alerting rules", + Group: "Alerting", + Version: 1, + Permissions: []ac.Permission{ + { + Action: ActionAlertingRuleRead, + Scope: ScopeNamespaceAll, + }, + { + Action: ActionAlertingRuleRead, + Scope: ScopeDatasourcesAll, + }, + { + Action: ActionAlertingInstanceRead, // scope is the current organization + }, + { + Action: ActionAlertingNotificationsRead, // scope is the current organization + }, + }, + }, + Grants: []string{string(models.ROLE_VIEWER)}, + } + + alertingWriterRole = ac.RoleRegistration{ + Role: ac.RoleDTO{ + Name: alertingWriter, + DisplayName: "Alerting Rules Writer", + Description: "Read and update alerting rules", + Group: "Alerting", + Version: 1, + Permissions: ac.ConcatPermissions(alertingReaderRole.Role.Permissions, []ac.Permission{ + { + Action: ActionAlertingRuleCreate, + Scope: ScopeNamespaceAll, + }, + { + Action: ActionAlertingRuleUpdate, + Scope: ScopeNamespaceAll, + }, + { + Action: ActionAlertingRuleDelete, + Scope: ScopeNamespaceAll, + }, + { + Action: ActionAlertingRuleCreate, + Scope: ScopeDatasourcesAll, + }, + { + Action: ActionAlertingRuleUpdate, + Scope: ScopeDatasourcesAll, + }, + { + Action: ActionAlertingRuleDelete, + Scope: ScopeDatasourcesAll, + }, + { + Action: ActionAlertingInstanceCreate, // scope is the current organization + }, + { + Action: ActionAlertingInstanceUpdate, // scope is the current organization + }, + { + Action: ActionAlertingNotificationsCreate, // scope is the current organization + }, + { + Action: ActionAlertingNotificationsUpdate, // scope is the current organization + }, + { + Action: ActionAlertingNotificationsDelete, // scope is the current organization + }, + }), + }, + Grants: []string{string(models.ROLE_EDITOR)}, + } +) + +// TODO temporary +func (api *API) isFgacDisabled() bool { + return api.Cfg.IsFeatureToggleEnabled == nil || !api.Cfg.IsFeatureToggleEnabled("alerting_fgac") +} + +// DeclareFixedRoles registers the fixed roles provided by the alerting module +func (api *API) DeclareFixedRoles() error { + // TODO temporary + if api.isFgacDisabled() { + return nil + } + return api.AccessControl.DeclareFixedRoles( + alertingReaderRole, alertingWriterRole, + ) +} + func (api *API) authorize(method, path string) web.Handler { // TODO Add fine-grained authorization for every route return middleware.ReqSignedIn diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 9dd8be60457..156ab04e41d 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -5,10 +5,13 @@ import ( "net/url" "github.com/benbjohnson/clock" + "golang.org/x/sync/errgroup" + "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" @@ -24,12 +27,12 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" - "golang.org/x/sync/errgroup" ) func ProvideService(cfg *setting.Cfg, dataSourceCache datasources.CacheService, routeRegister routing.RouteRegister, sqlStore *sqlstore.SQLStore, kvStore kvstore.KVStore, expressionService *expr.Service, dataProxy *datasourceproxy.DataSourceProxyService, - quotaService *quota.QuotaService, secretsService secrets.Service, notificationService notifications.Service, m *metrics.NGAlert, folderService dashboards.FolderService) (*AlertNG, error) { + quotaService *quota.QuotaService, secretsService secrets.Service, notificationService notifications.Service, m *metrics.NGAlert, + folderService dashboards.FolderService, ac accesscontrol.AccessControl) (*AlertNG, error) { ng := &AlertNG{ Cfg: cfg, DataSourceCache: dataSourceCache, @@ -44,6 +47,7 @@ func ProvideService(cfg *setting.Cfg, dataSourceCache datasources.CacheService, Log: log.New("ngalert"), NotificationService: notificationService, folderService: folderService, + accesscontrol: ac, } if ng.IsDisabled() { @@ -77,6 +81,7 @@ type AlertNG struct { // Alerting notification services MultiOrgAlertmanager *notifier.MultiOrgAlertmanager + accesscontrol accesscontrol.AccessControl } func (ng *AlertNG) init() error { @@ -145,10 +150,11 @@ func (ng *AlertNG) init() error { AdminConfigStore: store, MultiOrgAlertmanager: ng.MultiOrgAlertmanager, StateManager: ng.stateManager, + AccessControl: ng.accesscontrol, } api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) - return nil + return api.DeclareFixedRoles() } // Run starts the scheduler and Alertmanager. diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 826fe6e1f64..f481c1d7548 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol/mock" databasestore "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/services/ngalert" @@ -42,9 +43,10 @@ func SetupTestEnv(t *testing.T, baseInterval time.Duration) (*ngalert.AlertNG, * secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) dashboardStore := databasestore.ProvideDashboardStore(sqlStore) folderService := dashboardservice.ProvideFolderService(dashboardservice.ProvideDashboardService(dashboardStore, nil), dashboardStore, nil) + ac := mock.New() ng, err := ngalert.ProvideService( cfg, nil, routing.NewRouteRegister(), sqlStore, - nil, nil, nil, nil, secretsService, nil, m, folderService, + nil, nil, nil, nil, secretsService, nil, m, folderService, ac, ) require.NoError(t, err) return ng, &store.DBstore{