2021-04-01 03:11:45 -05:00
package api
import (
2022-02-23 10:30:04 -06:00
"context"
2021-04-15 07:54:37 -05:00
"errors"
2021-09-02 11:38:42 -05:00
"fmt"
2021-04-01 03:11:45 -05:00
"net/http"
2024-02-15 11:03:28 -06:00
"slices"
2022-06-15 15:01:14 -05:00
"strings"
2021-04-01 03:11:45 -05:00
"time"
2022-01-11 10:39:34 -06:00
"github.com/prometheus/common/model"
2021-08-25 08:11:22 -05:00
"github.com/grafana/grafana/pkg/api/apierrors"
2021-04-01 03:11:45 -05:00
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
2023-11-14 08:47:34 -06:00
"github.com/grafana/grafana/pkg/services/auth/identity"
2023-01-27 01:50:36 -06:00
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
2023-01-30 02:55:35 -06:00
"github.com/grafana/grafana/pkg/services/dashboards"
2024-02-15 08:45:10 -06:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2023-11-15 10:54:54 -06:00
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
2021-04-19 13:26:04 -05:00
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
2023-01-30 02:55:35 -06:00
"github.com/grafana/grafana/pkg/services/ngalert/eval"
2021-04-01 03:11:45 -05:00
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
2024-02-15 08:45:10 -06:00
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
2023-01-30 02:55:35 -06:00
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/setting"
2021-04-01 03:11:45 -05:00
"github.com/grafana/grafana/pkg/util"
2023-12-01 17:42:11 -06:00
"github.com/grafana/grafana/pkg/util/errutil"
2021-04-01 03:11:45 -05:00
)
2022-09-21 14:14:11 -05:00
type ConditionValidator interface {
// Validate validates that the condition is correct. Returns nil if the condition is correct. Otherwise, error that describes the failure
2022-10-19 14:19:43 -05:00
Validate ( ctx eval . EvaluationContext , condition ngmodels . Condition ) error
2022-09-21 14:14:11 -05:00
}
2024-02-15 08:45:10 -06:00
type AMConfigStore interface {
GetLatestAlertmanagerConfiguration ( ctx context . Context , orgID int64 ) ( * ngmodels . AlertConfiguration , error )
}
type AMRefresher interface {
ApplyConfig ( ctx context . Context , orgId int64 , dbConfig * ngmodels . AlertConfiguration ) error
}
2021-04-01 03:11:45 -05:00
type RulerSrv struct {
2022-09-21 14:14:11 -05:00
xactManager provisioning . TransactionManager
provenanceStore provisioning . ProvisioningStore
2022-09-27 08:56:30 -05:00
store RuleStore
2022-09-21 14:14:11 -05:00
QuotaService quota . Service
log log . Logger
cfg * setting . UnifiedAlertingSettings
conditionValidator ConditionValidator
2023-11-15 10:54:54 -06:00
authz RuleAccessControlService
2024-02-15 08:45:10 -06:00
amConfigStore AMConfigStore
amRefresher AMRefresher
featureManager featuremgmt . FeatureToggles
2021-04-01 03:11:45 -05:00
}
2022-02-23 10:30:04 -06:00
var (
2022-06-15 15:01:14 -05:00
errProvisionedResource = errors . New ( "request affects resources created via provisioning API" )
2022-02-23 10:30:04 -06:00
)
2024-02-15 11:03:28 -06:00
// ignore fields that are not part of the rule definition
var ignoreFieldsForValidate = [ ... ] string { "RuleGroupIndex" }
2022-08-24 14:33:33 -05:00
// RouteDeleteAlertRules deletes all alert rules the user is authorized to access in the given namespace
// or, if non-empty, a specific group of rules in the namespace.
2023-12-07 12:43:58 -06:00
// Returns http.StatusForbidden if user does not have access to any of the rules that match the filter.
2022-08-24 14:33:33 -05:00
// Returns http.StatusBadRequest if all rules that match the filter and the user is authorized to delete are provisioned.
2024-01-17 03:07:39 -06:00
func ( srv RulerSrv ) RouteDeleteAlertRules ( c * contextmodel . ReqContext , namespaceUID string , group string ) response . Response {
namespace , err := srv . store . GetNamespaceByUID ( c . Req . Context ( ) , namespaceUID , c . SignedInUser . GetOrgID ( ) , c . SignedInUser )
2021-04-01 03:11:45 -05:00
if err != nil {
2021-04-15 07:54:37 -05:00
return toNamespaceErrorResponse ( err )
2021-04-01 03:11:45 -05:00
}
2023-09-26 11:45:22 -05:00
2023-11-14 08:47:34 -06:00
userNamespace , id := c . SignedInUser . GetNamespacedID ( )
2023-08-30 10:46:47 -05:00
var loggerCtx = [ ] any {
2023-09-26 11:45:22 -05:00
"userId" ,
2023-11-14 08:47:34 -06:00
id ,
"userNamespace" ,
userNamespace ,
2023-09-26 11:45:22 -05:00
"namespaceUid" ,
namespace . UID ,
2022-03-25 11:39:24 -05:00
}
2022-06-23 15:13:39 -05:00
if group != "" {
2022-03-25 11:39:24 -05:00
loggerCtx = append ( loggerCtx , "group" , group )
2021-04-01 03:11:45 -05:00
}
2022-03-25 11:39:24 -05:00
logger := srv . log . New ( loggerCtx ... )
2021-05-03 13:01:33 -05:00
2023-11-14 08:47:34 -06:00
provenances , err := srv . provenanceStore . GetProvenances ( c . Req . Context ( ) , c . SignedInUser . GetOrgID ( ) , ( & ngmodels . AlertRule { } ) . ResourceType ( ) )
2022-05-06 13:55:27 -05:00
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "failed to fetch provenances of alert rules" )
}
2022-03-25 11:39:24 -05:00
err = srv . xactManager . InTransaction ( c . Req . Context ( ) , func ( ctx context . Context ) error {
2023-09-26 11:45:22 -05:00
deletionCandidates := map [ ngmodels . AlertRuleGroupKey ] ngmodels . RulesGroup { }
if group != "" {
key := ngmodels . AlertRuleGroupKey {
2023-10-09 03:40:19 -05:00
OrgID : c . SignedInUser . GetOrgID ( ) ,
2023-09-26 11:45:22 -05:00
NamespaceUID : namespace . UID ,
RuleGroup : group ,
}
rules , err := srv . getAuthorizedRuleGroup ( ctx , c , key )
if err != nil {
return err
}
deletionCandidates [ key ] = rules
} else {
var totalGroups int
deletionCandidates , totalGroups , err = srv . searchAuthorizedAlertRules ( ctx , c , [ ] string { namespace . UID } , "" , 0 )
if err != nil {
return err
}
if totalGroups > 0 && len ( deletionCandidates ) == 0 {
2023-12-01 17:42:11 -06:00
return accesscontrol . NewAuthorizationErrorGeneric ( "delete any existing rules in the namespace" )
2023-09-26 11:45:22 -05:00
}
2022-03-25 11:39:24 -05:00
}
2023-09-26 11:45:22 -05:00
rulesToDelete := make ( [ ] string , 0 )
provisioned := false
2022-08-24 14:33:33 -05:00
for groupKey , rules := range deletionCandidates {
if containsProvisionedAlerts ( provenances , rules ) {
2023-09-26 11:45:22 -05:00
logger . Debug ( "Alert group cannot be deleted because it is provisioned" , "group" , groupKey . RuleGroup )
2022-08-24 14:33:33 -05:00
provisioned = true
continue
}
uid := make ( [ ] string , 0 , len ( rules ) )
for _ , rule := range rules {
uid = append ( uid , rule . UID )
}
rulesToDelete = append ( rulesToDelete , uid ... )
2022-03-25 11:39:24 -05:00
}
2022-08-24 14:33:33 -05:00
if len ( rulesToDelete ) > 0 {
2023-11-14 08:47:34 -06:00
err := srv . store . DeleteAlertRulesByUID ( ctx , c . SignedInUser . GetOrgID ( ) , rulesToDelete ... )
2023-09-26 11:45:22 -05:00
if err != nil {
return err
}
logger . Info ( "Alert rules were deleted" , "ruleUid" , strings . Join ( rulesToDelete , "," ) )
return nil
2022-05-06 13:55:27 -05:00
}
2022-08-24 14:33:33 -05:00
// if none rules were deleted return an error.
// Check whether provisioned check failed first because if it is true, then all rules that the user can access (actually read via GET API) are provisioned.
if provisioned {
return errProvisionedResource
2022-05-06 13:55:27 -05:00
}
2023-09-26 11:45:22 -05:00
logger . Info ( "No alert rules were deleted" )
2022-08-24 14:33:33 -05:00
return nil
2022-03-25 11:39:24 -05:00
} )
2021-05-03 13:01:33 -05:00
if err != nil {
2023-12-01 17:42:11 -06:00
if errors . As ( err , & errutil . Error { } ) {
return response . Err ( err )
2022-08-24 14:33:33 -05:00
}
if errors . Is ( err , errProvisionedResource ) {
return ErrResp ( http . StatusBadRequest , err , "failed to delete rule group" )
2021-04-16 07:00:07 -05:00
}
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusInternalServerError , err , "failed to delete rule group" )
2021-04-01 03:11:45 -05:00
}
2022-03-25 11:39:24 -05:00
return response . JSON ( http . StatusAccepted , util . DynMap { "message" : "rules deleted" } )
2021-04-01 03:11:45 -05:00
}
2022-06-17 12:55:31 -05:00
// RouteGetNamespaceRulesConfig returns all rules in a specific folder that user has access to
2024-01-17 03:07:39 -06:00
func ( srv RulerSrv ) RouteGetNamespaceRulesConfig ( c * contextmodel . ReqContext , namespaceUID string ) response . Response {
namespace , err := srv . store . GetNamespaceByUID ( c . Req . Context ( ) , namespaceUID , c . SignedInUser . GetOrgID ( ) , c . SignedInUser )
2021-04-01 03:11:45 -05:00
if err != nil {
2021-04-15 07:54:37 -05:00
return toNamespaceErrorResponse ( err )
2021-04-01 03:11:45 -05:00
}
2023-09-26 11:45:22 -05:00
ruleGroups , _ , err := srv . searchAuthorizedAlertRules ( c . Req . Context ( ) , c , [ ] string { namespace . UID } , "" , 0 )
2023-03-28 03:34:35 -05:00
if err != nil {
2023-09-26 11:45:22 -05:00
return errorToResponse ( err )
2022-04-11 16:37:44 -05:00
}
2023-11-14 08:47:34 -06:00
provenanceRecords , err := srv . provenanceStore . GetProvenances ( c . Req . Context ( ) , c . SignedInUser . GetOrgID ( ) , ( & ngmodels . AlertRule { } ) . ResourceType ( ) )
2022-04-28 14:27:34 -05:00
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "failed to get provenance for rule group" )
}
2023-09-26 11:45:22 -05:00
result := apimodels . NamespaceConfigResponse { }
2021-04-01 03:11:45 -05:00
2023-09-26 11:45:22 -05:00
for groupKey , rules := range ruleGroups {
2024-02-06 16:12:13 -06:00
result [ namespace . Fullpath ] = append ( result [ namespace . Fullpath ] , toGettableRuleGroupConfig ( groupKey . RuleGroup , rules , provenanceRecords ) )
2021-04-01 03:11:45 -05:00
}
return response . JSON ( http . StatusAccepted , result )
}
2022-06-17 12:55:31 -05:00
// RouteGetRulesGroupConfig returns rules that belong to a specific group in a specific namespace (folder).
2023-12-07 12:43:58 -06:00
// If user does not have access to at least one of the rule in the group, returns status 403 Forbidden
2024-01-17 03:07:39 -06:00
func ( srv RulerSrv ) RouteGetRulesGroupConfig ( c * contextmodel . ReqContext , namespaceUID string , ruleGroup string ) response . Response {
namespace , err := srv . store . GetNamespaceByUID ( c . Req . Context ( ) , namespaceUID , c . SignedInUser . GetOrgID ( ) , c . SignedInUser )
2021-04-01 03:11:45 -05:00
if err != nil {
2021-04-15 07:54:37 -05:00
return toNamespaceErrorResponse ( err )
2021-04-01 03:11:45 -05:00
}
2023-09-26 11:45:22 -05:00
rules , err := srv . getAuthorizedRuleGroup ( c . Req . Context ( ) , c , ngmodels . AlertRuleGroupKey {
2023-10-09 03:40:19 -05:00
OrgID : c . SignedInUser . GetOrgID ( ) ,
2023-09-26 11:45:22 -05:00
RuleGroup : ruleGroup ,
NamespaceUID : namespace . UID ,
} )
2023-03-28 03:34:35 -05:00
if err != nil {
2023-09-26 11:45:22 -05:00
return errorToResponse ( err )
2022-04-11 16:37:44 -05:00
}
2023-11-14 08:47:34 -06:00
provenanceRecords , err := srv . provenanceStore . GetProvenances ( c . Req . Context ( ) , c . SignedInUser . GetOrgID ( ) , ( & ngmodels . AlertRule { } ) . ResourceType ( ) )
2022-04-28 14:27:34 -05:00
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "failed to get group alert rules" )
}
2021-04-01 03:11:45 -05:00
result := apimodels . RuleGroupConfigResponse {
2023-11-20 14:44:51 -06:00
// nolint:staticcheck
2023-12-15 11:06:53 -06:00
GettableRuleGroupConfig : toGettableRuleGroupConfig ( ruleGroup , rules , provenanceRecords ) ,
2021-04-01 03:11:45 -05:00
}
return response . JSON ( http . StatusAccepted , result )
}
2022-06-17 12:55:31 -05:00
// RouteGetRulesConfig returns all alert rules that are available to the current user
2023-01-27 01:50:36 -06:00
func ( srv RulerSrv ) RouteGetRulesConfig ( c * contextmodel . ReqContext ) response . Response {
2023-10-09 03:40:19 -05:00
namespaceMap , err := srv . store . GetUserVisibleNamespaces ( c . Req . Context ( ) , c . SignedInUser . GetOrgID ( ) , c . SignedInUser )
2021-07-22 01:53:14 -05:00
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "failed to get namespaces visible to the user" )
}
2021-11-08 07:26:08 -06:00
result := apimodels . NamespaceConfigResponse { }
if len ( namespaceMap ) == 0 {
2023-09-04 11:46:34 -05:00
srv . log . Debug ( "User has no access to any namespaces" )
2021-11-08 07:26:08 -06:00
return response . JSON ( http . StatusOK , result )
}
2021-07-22 01:53:14 -05:00
namespaceUIDs := make ( [ ] string , len ( namespaceMap ) )
for k := range namespaceMap {
namespaceUIDs = append ( namespaceUIDs , k )
}
2021-10-04 10:33:55 -05:00
dashboardUID := c . Query ( "dashboard_uid" )
2024-04-23 07:50:26 -05:00
panelID , err := getPanelIDFromQuery ( c . Req . URL . Query ( ) )
2021-10-04 10:33:55 -05:00
if err != nil {
return ErrResp ( http . StatusBadRequest , err , "invalid panel_id" )
}
if dashboardUID == "" && panelID != 0 {
return ErrResp ( http . StatusBadRequest , errors . New ( "panel_id must be set with dashboard_uid" ) , "" )
}
2023-09-26 11:45:22 -05:00
configs , _ , err := srv . searchAuthorizedAlertRules ( c . Req . Context ( ) , c , namespaceUIDs , dashboardUID , panelID )
2023-03-28 03:34:35 -05:00
if err != nil {
2023-09-26 11:45:22 -05:00
return errorToResponse ( err )
2022-04-11 16:37:44 -05:00
}
2023-11-14 08:47:34 -06:00
provenanceRecords , err := srv . provenanceStore . GetProvenances ( c . Req . Context ( ) , c . SignedInUser . GetOrgID ( ) , ( & ngmodels . AlertRule { } ) . ResourceType ( ) )
2022-04-28 14:27:34 -05:00
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "failed to get alert rules" )
}
2022-05-16 14:45:45 -05:00
for groupKey , rules := range configs {
folder , ok := namespaceMap [ groupKey . NamespaceUID ]
2022-05-12 09:42:31 -05:00
if ! ok {
2023-11-14 08:47:34 -06:00
userNamespace , id := c . SignedInUser . GetNamespacedID ( )
srv . log . Error ( "Namespace not visible to the user" , "user" , id , "userNamespace" , userNamespace , "namespace" , groupKey . NamespaceUID )
2022-05-12 09:42:31 -05:00
continue
}
2024-02-06 16:12:13 -06:00
result [ folder . Fullpath ] = append ( result [ folder . Fullpath ] , toGettableRuleGroupConfig ( groupKey . RuleGroup , rules , provenanceRecords ) )
2021-04-01 03:11:45 -05:00
}
2021-10-04 10:33:55 -05:00
return response . JSON ( http . StatusOK , result )
2021-04-01 03:11:45 -05:00
}
2024-01-17 03:07:39 -06:00
func ( srv RulerSrv ) RoutePostNameRulesConfig ( c * contextmodel . ReqContext , ruleGroupConfig apimodels . PostableRuleGroupConfig , namespaceUID string ) response . Response {
namespace , err := srv . store . GetNamespaceByUID ( c . Req . Context ( ) , namespaceUID , c . SignedInUser . GetOrgID ( ) , c . SignedInUser )
2021-04-01 03:11:45 -05:00
if err != nil {
2021-04-15 07:54:37 -05:00
return toNamespaceErrorResponse ( err )
2021-04-01 03:11:45 -05:00
}
2024-02-13 08:29:03 -06:00
if err := srv . checkGroupLimits ( ruleGroupConfig ) ; err != nil {
return ErrResp ( http . StatusBadRequest , err , "" )
}
2024-02-28 14:40:13 -06:00
rules , err := ValidateRuleGroup ( & ruleGroupConfig , c . SignedInUser . GetOrgID ( ) , namespace . UID , RuleLimitsFromConfig ( srv . cfg ) )
2022-02-23 10:30:04 -06:00
if err != nil {
return ErrResp ( http . StatusBadRequest , err , "" )
2021-04-16 07:00:07 -05:00
}
2022-05-16 14:45:45 -05:00
groupKey := ngmodels . AlertRuleGroupKey {
2023-11-14 08:47:34 -06:00
OrgID : c . SignedInUser . GetOrgID ( ) ,
2022-11-11 07:28:24 -06:00
NamespaceUID : namespace . UID ,
2022-05-16 14:45:45 -05:00
RuleGroup : ruleGroupConfig . Name ,
}
return srv . updateAlertRulesInGroup ( c , groupKey , rules )
2022-02-23 10:30:04 -06:00
}
2024-02-13 08:29:03 -06:00
func ( srv RulerSrv ) checkGroupLimits ( group apimodels . PostableRuleGroupConfig ) error {
if srv . cfg . RulesPerRuleGroupLimit > 0 && int64 ( len ( group . Rules ) ) > srv . cfg . RulesPerRuleGroupLimit {
srv . log . Warn ( "Large rule group was edited. Large groups are discouraged and may be rejected in the future." ,
"limit" , srv . cfg . RulesPerRuleGroupLimit ,
"actual" , len ( group . Rules ) ,
"group" , group . Name ,
)
}
return nil
}
2022-03-24 15:53:00 -05:00
// updateAlertRulesInGroup calculates changes (rules to add,update,delete), verifies that the user is authorized to do the calculated changes and updates database.
// All operations are performed in a single transaction
2024-02-15 08:45:10 -06:00
//
//nolint:gocyclo
2023-02-01 06:15:03 -06:00
func ( srv RulerSrv ) updateAlertRulesInGroup ( c * contextmodel . ReqContext , groupKey ngmodels . AlertRuleGroupKey , rules [ ] * ngmodels . AlertRuleWithOptionals ) response . Response {
2022-08-01 23:41:23 -05:00
var finalChanges * store . GroupDelta
2024-02-15 08:45:10 -06:00
var dbConfig * ngmodels . AlertConfiguration
2022-03-15 11:48:42 -05:00
err := srv . xactManager . InTransaction ( c . Req . Context ( ) , func ( tranCtx context . Context ) error {
2023-11-14 08:47:34 -06:00
userNamespace , id := c . SignedInUser . GetNamespacedID ( )
logger := srv . log . New ( "namespace_uid" , groupKey . NamespaceUID , "group" ,
groupKey . RuleGroup , "org_id" , groupKey . OrgID , "user_id" , id , "userNamespace" , userNamespace )
2022-08-01 23:41:23 -05:00
groupChanges , err := store . CalculateChanges ( tranCtx , srv . store , groupKey , rules )
2022-02-23 10:30:04 -06:00
if err != nil {
return err
2021-04-28 03:31:51 -05:00
}
2022-02-23 10:30:04 -06:00
2022-08-01 23:41:23 -05:00
if groupChanges . IsEmpty ( ) {
2022-05-06 13:55:27 -05:00
finalChanges = groupChanges
2023-09-04 11:46:34 -05:00
logger . Info ( "No changes detected in the request. Do nothing" )
2022-03-04 15:16:33 -06:00
return nil
2021-09-02 11:38:42 -05:00
}
2022-02-23 10:30:04 -06:00
2023-11-15 10:54:54 -06:00
err = srv . authz . AuthorizeRuleChanges ( c . Req . Context ( ) , c . SignedInUser , groupChanges )
2023-05-31 03:58:57 -05:00
if err != nil {
return err
2022-03-24 15:53:00 -05:00
}
2023-06-15 12:33:42 -05:00
if err := validateQueries ( c . Req . Context ( ) , groupChanges , srv . conditionValidator , c . SignedInUser ) ; err != nil {
return err
}
2024-02-15 08:45:10 -06:00
newOrUpdatedNotificationSettings := groupChanges . NewOrUpdatedNotificationSettings ( )
if len ( newOrUpdatedNotificationSettings ) > 0 {
dbConfig , err = srv . amConfigStore . GetLatestAlertmanagerConfiguration ( c . Req . Context ( ) , groupChanges . GroupKey . OrgID )
if err != nil {
return fmt . Errorf ( "failed to get latest configuration: %w" , err )
}
cfg , err := notifier . Load ( [ ] byte ( dbConfig . AlertmanagerConfiguration ) )
if err != nil {
return fmt . Errorf ( "failed to parse configuration: %w" , err )
}
validator := notifier . NewNotificationSettingsValidator ( & cfg . AlertmanagerConfig )
for _ , s := range newOrUpdatedNotificationSettings {
if err := validator . Validate ( s ) ; err != nil {
return errors . Join ( ngmodels . ErrAlertRuleFailedValidation , err )
}
}
}
2023-10-09 03:40:19 -05:00
if err := verifyProvisionedRulesNotAffected ( c . Req . Context ( ) , srv . provenanceStore , c . SignedInUser . GetOrgID ( ) , groupChanges ) ; err != nil {
2022-05-06 13:55:27 -05:00
return err
}
2022-08-01 23:41:23 -05:00
finalChanges = store . UpdateCalculatedRuleFields ( groupChanges )
2023-09-04 11:46:34 -05:00
logger . Debug ( "Updating database with the authorized changes" , "add" , len ( finalChanges . New ) , "update" , len ( finalChanges . New ) , "delete" , len ( finalChanges . Delete ) )
2022-03-21 18:20:35 -05:00
2023-06-08 17:51:50 -05:00
// Delete first as this could prevent future unique constraint violations.
if len ( finalChanges . Delete ) > 0 {
UIDs := make ( [ ] string , 0 , len ( finalChanges . Delete ) )
for _ , rule := range finalChanges . Delete {
UIDs = append ( UIDs , rule . UID )
}
2023-11-14 08:47:34 -06:00
if err = srv . store . DeleteAlertRulesByUID ( tranCtx , c . SignedInUser . GetOrgID ( ) , UIDs ... ) ; err != nil {
2023-06-08 17:51:50 -05:00
return fmt . Errorf ( "failed to delete rules: %w" , err )
}
}
if len ( finalChanges . Update ) > 0 {
2022-09-29 15:47:56 -05:00
updates := make ( [ ] ngmodels . UpdateRule , 0 , len ( finalChanges . Update ) )
2022-05-06 13:55:27 -05:00
for _ , update := range finalChanges . Update {
2023-09-04 11:46:34 -05:00
logger . Debug ( "Updating rule" , "rule_uid" , update . New . UID , "diff" , update . Diff . String ( ) )
2022-09-29 15:47:56 -05:00
updates = append ( updates , ngmodels . UpdateRule {
2022-03-04 15:16:33 -06:00
Existing : update . Existing ,
New : * update . New ,
} )
}
2022-04-14 07:21:36 -05:00
err = srv . store . UpdateAlertRules ( tranCtx , updates )
2022-03-04 15:16:33 -06:00
if err != nil {
2022-04-14 07:21:36 -05:00
return fmt . Errorf ( "failed to update rules: %w" , err )
2022-03-04 15:16:33 -06:00
}
}
2023-06-08 17:51:50 -05:00
if len ( finalChanges . New ) > 0 {
inserts := make ( [ ] ngmodels . AlertRule , 0 , len ( finalChanges . New ) )
for _ , rule := range finalChanges . New {
inserts = append ( inserts , * rule )
2022-03-23 15:09:53 -05:00
}
2023-10-06 17:11:24 -05:00
added , err := srv . store . InsertAlertRules ( tranCtx , inserts )
2023-06-08 17:51:50 -05:00
if err != nil {
return fmt . Errorf ( "failed to add rules: %w" , err )
2021-09-02 11:38:42 -05:00
}
2023-10-06 17:11:24 -05:00
if len ( added ) != len ( finalChanges . New ) {
logger . Error ( "Cannot match inserted rules with final changes" , "insertedCount" , len ( added ) , "changes" , len ( finalChanges . New ) )
} else {
for i , newRule := range finalChanges . New {
newRule . ID = added [ i ] . ID
newRule . UID = added [ i ] . UID
}
}
2021-09-02 11:38:42 -05:00
}
2022-05-06 13:55:27 -05:00
if len ( finalChanges . New ) > 0 {
2023-11-14 08:47:34 -06:00
userID , _ := identity . UserIdentifier ( c . SignedInUser . GetNamespacedID ( ) )
2022-11-14 13:08:10 -06:00
limitReached , err := srv . QuotaService . CheckQuotaReached ( tranCtx , ngmodels . QuotaTargetSrv , & quota . ScopeParameters {
2023-10-09 03:40:19 -05:00
OrgID : c . SignedInUser . GetOrgID ( ) ,
2023-11-14 08:47:34 -06:00
UserID : userID ,
2022-02-23 10:30:04 -06:00
} ) // alert rule is table name
if err != nil {
return fmt . Errorf ( "failed to get alert rules quota: %w" , err )
}
if limitReached {
2022-07-13 17:36:17 -05:00
return ngmodels . ErrQuotaReached
2022-02-23 10:30:04 -06:00
}
2021-04-28 03:31:51 -05:00
}
2022-02-23 10:30:04 -06:00
return nil
} )
2021-04-28 03:31:51 -05:00
2022-02-23 10:30:04 -06:00
if err != nil {
2023-12-01 17:42:11 -06:00
if errors . As ( err , & errutil . Error { } ) {
return response . Err ( err )
} else if errors . Is ( err , ngmodels . ErrAlertRuleNotFound ) {
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusNotFound , err , "failed to update rule group" )
2022-06-15 15:01:14 -05:00
} else if errors . Is ( err , ngmodels . ErrAlertRuleFailedValidation ) || errors . Is ( err , errProvisionedResource ) {
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusBadRequest , err , "failed to update rule group" )
2022-07-13 17:36:17 -05:00
} else if errors . Is ( err , ngmodels . ErrQuotaReached ) {
2022-02-23 10:30:04 -06:00
return ErrResp ( http . StatusForbidden , err , "" )
2022-06-13 11:15:28 -05:00
} else if errors . Is ( err , store . ErrOptimisticLock ) {
return ErrResp ( http . StatusConflict , err , "" )
2021-04-15 07:54:37 -05:00
}
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusInternalServerError , err , "failed to update rule group" )
2021-04-01 03:11:45 -05:00
}
2024-02-15 08:45:10 -06:00
if srv . featureManager . IsEnabled ( c . Req . Context ( ) , featuremgmt . FlagAlertingSimplifiedRouting ) && dbConfig != nil {
// This isn't strictly necessary since the alertmanager config is periodically synced.
err := srv . amRefresher . ApplyConfig ( c . Req . Context ( ) , groupKey . OrgID , dbConfig )
if err != nil {
srv . log . Warn ( "Failed to refresh Alertmanager config for org after change in notification settings" , "org" , c . SignedInUser . GetOrgID ( ) , "error" , err )
}
}
2023-10-06 17:11:24 -05:00
return changesToResponse ( finalChanges )
}
2021-04-01 03:11:45 -05:00
2023-10-06 17:11:24 -05:00
func changesToResponse ( finalChanges * store . GroupDelta ) response . Response {
body := apimodels . UpdateRuleGroupResponse {
Message : "rule group updated successfully" ,
Created : make ( [ ] string , 0 , len ( finalChanges . New ) ) ,
Updated : make ( [ ] string , 0 , len ( finalChanges . Update ) ) ,
Deleted : make ( [ ] string , 0 , len ( finalChanges . Delete ) ) ,
}
2022-08-01 23:41:23 -05:00
if finalChanges . IsEmpty ( ) {
2023-10-06 17:11:24 -05:00
body . Message = "no changes detected in the rule group"
} else {
for _ , r := range finalChanges . New {
body . Created = append ( body . Created , r . UID )
}
for _ , r := range finalChanges . Update {
body . Updated = append ( body . Updated , r . Existing . UID )
}
for _ , r := range finalChanges . Delete {
body . Deleted = append ( body . Deleted , r . UID )
}
2022-03-04 15:16:33 -06:00
}
2023-10-06 17:11:24 -05:00
return response . JSON ( http . StatusAccepted , body )
2021-04-01 03:11:45 -05:00
}
2023-12-15 11:06:53 -06:00
func toGettableRuleGroupConfig ( groupName string , rules ngmodels . RulesGroup , provenanceRecords map [ string ] ngmodels . Provenance ) apimodels . GettableRuleGroupConfig {
2022-06-22 09:52:46 -05:00
rules . SortByGroupIndex ( )
2022-05-12 09:42:31 -05:00
ruleNodes := make ( [ ] apimodels . GettableExtendedRuleNode , 0 , len ( rules ) )
var interval time . Duration
if len ( rules ) > 0 {
interval = time . Duration ( rules [ 0 ] . IntervalSeconds ) * time . Second
}
for _ , r := range rules {
2023-12-15 11:06:53 -06:00
ruleNodes = append ( ruleNodes , toGettableExtendedRuleNode ( * r , provenanceRecords ) )
2022-05-12 09:42:31 -05:00
}
return apimodels . GettableRuleGroupConfig {
Name : groupName ,
Interval : model . Duration ( interval ) ,
Rules : ruleNodes ,
}
}
2023-12-15 11:06:53 -06:00
func toGettableExtendedRuleNode ( r ngmodels . AlertRule , provenanceRecords map [ string ] ngmodels . Provenance ) apimodels . GettableExtendedRuleNode {
2022-04-28 14:27:34 -05:00
provenance := ngmodels . ProvenanceNone
if prov , exists := provenanceRecords [ r . ResourceID ( ) ] ; exists {
provenance = prov
}
2024-02-15 08:45:10 -06:00
2021-04-15 07:54:37 -05:00
gettableExtendedRuleNode := apimodels . GettableExtendedRuleNode {
2021-04-01 03:11:45 -05:00
GrafanaManagedAlert : & apimodels . GettableGrafanaRule {
2024-02-15 08:45:10 -06:00
ID : r . ID ,
OrgID : r . OrgID ,
Title : r . Title ,
Condition : r . Condition ,
Data : ApiAlertQueriesFromAlertQueries ( r . Data ) ,
Updated : r . Updated ,
IntervalSeconds : r . IntervalSeconds ,
Version : r . Version ,
UID : r . UID ,
NamespaceUID : r . NamespaceUID ,
RuleGroup : r . RuleGroup ,
NoDataState : apimodels . NoDataState ( r . NoDataState ) ,
ExecErrState : apimodels . ExecutionErrorState ( r . ExecErrState ) ,
Provenance : apimodels . Provenance ( provenance ) ,
IsPaused : r . IsPaused ,
NotificationSettings : AlertRuleNotificationSettingsFromNotificationSettings ( r . NotificationSettings ) ,
2021-04-01 03:11:45 -05:00
} ,
}
2022-06-30 10:46:26 -05:00
forDuration := model . Duration ( r . For )
2021-04-15 07:54:37 -05:00
gettableExtendedRuleNode . ApiRuleNode = & apimodels . ApiRuleNode {
2022-06-30 10:46:26 -05:00
For : & forDuration ,
2021-04-15 07:54:37 -05:00
Annotations : r . Annotations ,
Labels : r . Labels ,
}
return gettableExtendedRuleNode
2021-04-01 03:11:45 -05:00
}
2021-04-07 07:28:06 -05:00
2021-04-15 07:54:37 -05:00
func toNamespaceErrorResponse ( err error ) response . Response {
if errors . Is ( err , ngmodels . ErrCannotEditNamespace ) {
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusForbidden , err , err . Error ( ) )
2021-04-15 07:54:37 -05:00
}
2022-06-30 08:31:54 -05:00
if errors . Is ( err , dashboards . ErrDashboardIdentifierNotSet ) {
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusBadRequest , err , err . Error ( ) )
2021-04-15 07:54:37 -05:00
}
2021-08-25 08:11:22 -05:00
return apierrors . ToFolderErrorResponse ( err )
2021-04-07 07:28:06 -05:00
}
2022-02-23 10:30:04 -06:00
2022-06-15 15:01:14 -05:00
// verifyProvisionedRulesNotAffected check that neither of provisioned alerts are affected by changes.
// Returns errProvisionedResource if there is at least one rule in groups affected by changes that was provisioned.
2022-08-01 23:41:23 -05:00
func verifyProvisionedRulesNotAffected ( ctx context . Context , provenanceStore provisioning . ProvisioningStore , orgID int64 , ch * store . GroupDelta ) error {
2022-06-15 15:01:14 -05:00
provenances , err := provenanceStore . GetProvenances ( ctx , orgID , ( & ngmodels . AlertRule { } ) . ResourceType ( ) )
if err != nil {
return err
}
errorMsg := strings . Builder { }
for group , alertRules := range ch . AffectedGroups {
2022-08-24 14:33:33 -05:00
if ! containsProvisionedAlerts ( provenances , alertRules ) {
continue
}
if errorMsg . Len ( ) > 0 {
errorMsg . WriteRune ( ',' )
2022-06-15 15:01:14 -05:00
}
2022-08-24 14:33:33 -05:00
errorMsg . WriteString ( group . String ( ) )
2022-06-15 15:01:14 -05:00
}
if errorMsg . Len ( ) == 0 {
return nil
}
return fmt . Errorf ( "%w: alert rule group [%s]" , errProvisionedResource , errorMsg . String ( ) )
}
2023-06-15 12:33:42 -05:00
2023-11-14 08:47:34 -06:00
func validateQueries ( ctx context . Context , groupChanges * store . GroupDelta , validator ConditionValidator , user identity . Requester ) error {
2023-06-15 12:33:42 -05:00
if len ( groupChanges . New ) > 0 {
for _ , rule := range groupChanges . New {
err := validator . Validate ( eval . NewContext ( ctx , user ) , rule . GetEvalCondition ( ) )
if err != nil {
return fmt . Errorf ( "%w '%s': %s" , ngmodels . ErrAlertRuleFailedValidation , rule . Title , err . Error ( ) )
}
}
}
if len ( groupChanges . Update ) > 0 {
for _ , upd := range groupChanges . Update {
2024-02-15 11:03:28 -06:00
if ! shouldValidate ( upd ) {
continue
}
2023-06-15 12:33:42 -05:00
err := validator . Validate ( eval . NewContext ( ctx , user ) , upd . New . GetEvalCondition ( ) )
if err != nil {
return fmt . Errorf ( "%w '%s' (UID: %s): %s" , ngmodels . ErrAlertRuleFailedValidation , upd . New . Title , upd . New . UID , err . Error ( ) )
}
}
}
return nil
}
2023-09-26 11:45:22 -05:00
2024-02-15 11:03:28 -06:00
// shouldValidate returns true if the rule is not paused and there are changes in the rule that are not ignored
func shouldValidate ( delta store . RuleDelta ) bool {
for _ , diff := range delta . Diff {
if ! slices . Contains ( ignoreFieldsForValidate [ : ] , diff . Path ) {
return true
}
}
// TODO: consider also checking if rule will be paused after the update
return false
}
2023-10-02 10:47:59 -05:00
// getAuthorizedRuleByUid fetches all rules in group to which the specified rule belongs, and checks whether the user is authorized to access the group.
// A user is authorized to access a group of rules only when it has permission to query all data sources used by all rules in this group.
// Returns rule identified by provided UID or ErrAuthorization if user is not authorized to access the rule.
func ( srv RulerSrv ) getAuthorizedRuleByUid ( ctx context . Context , c * contextmodel . ReqContext , ruleUID string ) ( ngmodels . AlertRule , error ) {
q := ngmodels . GetAlertRulesGroupByRuleUIDQuery {
UID : ruleUID ,
2023-10-09 03:40:19 -05:00
OrgID : c . SignedInUser . GetOrgID ( ) ,
2023-10-02 10:47:59 -05:00
}
var err error
rules , err := srv . store . GetAlertRulesGroupByRuleUID ( ctx , & q )
if err != nil {
return ngmodels . AlertRule { } , err
}
2023-12-01 17:42:11 -06:00
if err := srv . authz . AuthorizeAccessToRuleGroup ( ctx , c . SignedInUser , rules ) ; err != nil {
return ngmodels . AlertRule { } , err
2023-10-02 10:47:59 -05:00
}
for _ , rule := range rules {
if rule . UID == ruleUID {
return * rule , nil
}
}
return ngmodels . AlertRule { } , ngmodels . ErrAlertRuleNotFound
}
2023-09-26 11:45:22 -05:00
// getAuthorizedRuleGroup fetches rules that belong to the specified models.AlertRuleGroupKey and validate user's authorization.
// A user is authorized to access a group of rules only when it has permission to query all data sources used by all rules in this group.
// Returns models.RuleGroup if authorization passed or ErrAuthorization if user is not authorized to access the rule.
func ( srv RulerSrv ) getAuthorizedRuleGroup ( ctx context . Context , c * contextmodel . ReqContext , ruleGroupKey ngmodels . AlertRuleGroupKey ) ( ngmodels . RulesGroup , error ) {
q := ngmodels . ListAlertRulesQuery {
OrgID : ruleGroupKey . OrgID ,
NamespaceUIDs : [ ] string { ruleGroupKey . NamespaceUID } ,
RuleGroup : ruleGroupKey . RuleGroup ,
}
rules , err := srv . store . ListAlertRules ( ctx , & q )
if err != nil {
return nil , err
}
2023-12-01 17:42:11 -06:00
if err := srv . authz . AuthorizeAccessToRuleGroup ( ctx , c . SignedInUser , rules ) ; err != nil {
return nil , err
2023-09-26 11:45:22 -05:00
}
return rules , nil
}
// searchAuthorizedAlertRules fetches rules according to the filters, groups them by models.AlertRuleGroupKey and filters out groups that the current user is not authorized to access.
// A user is authorized to access a group of rules only when it has permission to query all data sources used by all rules in this group.
// Returns groups that user is authorized to access, and total count of groups returned by query
func ( srv RulerSrv ) searchAuthorizedAlertRules ( ctx context . Context , c * contextmodel . ReqContext , folderUIDs [ ] string , dashboardUID string , panelID int64 ) ( map [ ngmodels . AlertRuleGroupKey ] ngmodels . RulesGroup , int , error ) {
query := ngmodels . ListAlertRulesQuery {
2023-10-09 03:40:19 -05:00
OrgID : c . SignedInUser . GetOrgID ( ) ,
2023-09-26 11:45:22 -05:00
NamespaceUIDs : folderUIDs ,
DashboardUID : dashboardUID ,
PanelID : panelID ,
}
rules , err := srv . store . ListAlertRules ( ctx , & query )
if err != nil {
return nil , 0 , err
}
byGroupKey := ngmodels . GroupByAlertRuleGroupKey ( rules )
totalGroups := len ( byGroupKey )
for groupKey , rulesGroup := range byGroupKey {
2023-12-01 17:42:11 -06:00
if ok , err := srv . authz . HasAccessToRuleGroup ( ctx , c . SignedInUser , rulesGroup ) ; ! ok || err != nil {
if err != nil {
return nil , 0 , err
}
2023-09-26 11:45:22 -05:00
delete ( byGroupKey , groupKey )
}
}
return byGroupKey , totalGroups , nil
}