grafana/pkg/services/provisioning/alerting/rules_provisioner.go
Yuri Tseretyan b9abb8cabb
Alerting: Update provisioning API to support regular permissions (#77007)
* allow users with regular actions access provisioning API paths
* update methods that read rules
skip new authorization logic if user CanReadAllRules to avoid performance impact on file-provisioning
update all methods to accept identity.Requester that contains all permissions and is required by access control.

* create deltas for single rul e 

* update modify methods
skip new authorization logic if user CanWriteAllRules to avoid performance impact on file-provisioning
update all methods to accept identity.Requester that contains all permissions and is required by access control.

* implement RuleAccessControlService in provisioning

* update file provisioning user to have all permissions to bypass authz

* update provisioning API to return errutil errors correctly

---------

Co-authored-by: Alexander Weaver <weaver.alex.d@gmail.com>
2024-03-22 15:37:10 -04:00

149 lines
4.8 KiB
Go

package alerting
import (
"context"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder"
alert_models "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/util"
)
type AlertRuleProvisioner interface {
Provision(ctx context.Context, files []*AlertingFile) error
}
func NewAlertRuleProvisioner(
logger log.Logger,
dashboardService dashboards.DashboardService,
dashboardProvService dashboards.DashboardProvisioningService,
ruleService provisioning.AlertRuleService) AlertRuleProvisioner {
return &defaultAlertRuleProvisioner{
logger: logger,
dashboardService: dashboardService,
dashboardProvService: dashboardProvService,
ruleService: ruleService,
}
}
type defaultAlertRuleProvisioner struct {
logger log.Logger
dashboardService dashboards.DashboardService
dashboardProvService dashboards.DashboardProvisioningService
ruleService provisioning.AlertRuleService
}
func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context,
files []*AlertingFile) error {
for _, file := range files {
for _, group := range file.Groups {
u := provisionerUser(group.OrgID)
folderUID, err := prov.getOrCreateFolderUID(ctx, group.FolderTitle, group.OrgID)
if err != nil {
return err
}
prov.logger.Debug("provisioning alert rule group",
"org", group.OrgID,
"folder", group.FolderTitle,
"folderUID", folderUID,
"name", group.Title)
for _, rule := range group.Rules {
rule.NamespaceUID = folderUID
rule.RuleGroup = group.Title
err = prov.provisionRule(ctx, u, rule)
if err != nil {
return err
}
}
err = prov.ruleService.UpdateRuleGroup(ctx, u, folderUID, group.Title, group.Interval)
if err != nil {
return err
}
}
for _, deleteRule := range file.DeleteRules {
err := prov.ruleService.DeleteAlertRule(ctx, provisionerUser(deleteRule.OrgID), deleteRule.UID, alert_models.ProvenanceFile)
if err != nil {
return err
}
}
}
return nil
}
func (prov *defaultAlertRuleProvisioner) provisionRule(
ctx context.Context,
user identity.Requester,
rule alert_models.AlertRule) error {
prov.logger.Debug("provisioning alert rule", "uid", rule.UID, "org", rule.OrgID)
_, _, err := prov.ruleService.GetAlertRule(ctx, user, rule.UID)
if err != nil && !errors.Is(err, alert_models.ErrAlertRuleNotFound) {
return err
} else if err != nil {
prov.logger.Debug("creating rule", "uid", rule.UID, "org", rule.OrgID)
// a nil user is passed in as then the quota logic will only check for
// the organization quota since we don't have any user scope here.
_, err = prov.ruleService.CreateAlertRule(ctx, user, rule, alert_models.ProvenanceFile)
} else {
prov.logger.Debug("updating rule", "uid", rule.UID, "org", rule.OrgID)
_, err = prov.ruleService.UpdateAlertRule(ctx, user, rule, alert_models.ProvenanceFile)
}
return err
}
func (prov *defaultAlertRuleProvisioner) getOrCreateFolderUID(
ctx context.Context, folderName string, orgID int64) (string, error) {
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Provisioning).Inc()
cmd := &dashboards.GetDashboardQuery{
Title: &folderName,
FolderID: util.Pointer(int64(0)), // nolint:staticcheck
OrgID: orgID,
}
cmdResult, err := prov.dashboardService.GetDashboard(ctx, cmd)
if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) {
return "", err
}
// dashboard folder not found. create one.
if errors.Is(err, dashboards.ErrDashboardNotFound) {
createCmd := &folder.CreateFolderCommand{
OrgID: orgID,
UID: util.GenerateShortUID(),
Title: folderName,
}
dbDash, err := prov.dashboardProvService.SaveFolderForProvisionedDashboards(ctx, createCmd)
if err != nil {
return "", err
}
return dbDash.UID, nil
}
if !cmdResult.IsFolder {
return "", fmt.Errorf("got invalid response. expected folder, found dashboard")
}
return cmdResult.UID, nil
}
var provisionerUser = func(orgID int64) identity.Requester {
// this user has 0 ID and therefore, organization wide quota will be applied
return accesscontrol.BackgroundUser(
"alert_provisioner",
orgID,
org.RoleAdmin,
[]accesscontrol.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
{Action: accesscontrol.ActionAlertingProvisioningReadSecrets, Scope: dashboards.ScopeFoldersAll},
{Action: accesscontrol.ActionAlertingProvisioningWrite, Scope: dashboards.ScopeFoldersAll},
},
)
}