Alerting: Update provisioning to validate user-defined UID on create (#73793)

* add ValidateUID to util
* provisioning to validate UID on rule creation

---------

Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com>
Co-authored-by: Alexander Weaver <weaver.alex.d@gmail.com>
This commit is contained in:
Yuri Tseretyan
2023-09-08 15:09:35 -04:00
committed by GitHub
co-authored by brendamuir Alexander Weaver
parent 9c50296a07
commit 99fd7b8141
14 changed files with 161 additions and 24 deletions
@@ -33,6 +33,7 @@ import (
secrets_fakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -283,7 +284,7 @@ func TestProvisioningApi(t *testing.T) {
t.Run("PUT sets expected fields with no provenance", func(t *testing.T) {
sut := createProvisioningSrvSut(t)
uid := t.Name()
uid := util.GenerateShortUID()
rule := createTestAlertRule("rule", 1)
rule.UID = uid
insertRuleInOrg(t, sut, rule, 3)
@@ -2663,6 +2663,9 @@
"type": "string"
},
"uid": {
"maxLength": 40,
"minLength": 1,
"pattern": "^[a-zA-Z0-9-_]+$",
"type": "string"
},
"updated": {
@@ -108,7 +108,11 @@ type AlertRuleHeaders struct {
type ProvisionedAlertRules []ProvisionedAlertRule
type ProvisionedAlertRule struct {
ID int64 `json:"id"`
ID int64 `json:"id"`
// required: false
// minLength: 1
// maxLength: 40
// pattern: ^[a-zA-Z0-9-_]+$
UID string `json:"uid"`
// required: true
OrgID int64 `json:"orgID"`
@@ -81,6 +81,10 @@ type ContactPoints []EmbeddedContactPoint
type EmbeddedContactPoint struct {
// UID is the unique identifier of the contact point. The UID can be
// set by the user.
// required: false
// minLength: 1
// maxLength: 40
// pattern: ^[a-zA-Z0-9\-\_]+$
// example: my_external_reference
UID string `json:"uid"`
// Name is used as grouping key in the UI. Contact points with the
@@ -764,6 +764,9 @@
"uid": {
"description": "UID is the unique identifier of the contact point. The UID can be\nset by the user.",
"example": "my_external_reference",
"maxLength": 40,
"minLength": 1,
"pattern": "^[a-zA-Z0-9\\-\\_]+$",
"type": "string"
}
},
@@ -2663,6 +2666,9 @@
"type": "string"
},
"uid": {
"maxLength": 40,
"minLength": 1,
"pattern": "^[a-zA-Z0-9-_]+$",
"type": "string"
},
"updated": {
+7 -1
View File
@@ -3625,6 +3625,9 @@
"uid": {
"description": "UID is the unique identifier of the contact point. The UID can be\nset by the user.",
"type": "string",
"maxLength": 40,
"minLength": 1,
"pattern": "^[a-zA-Z0-9\\-\\_]+$",
"example": "my_external_reference"
}
}
@@ -5534,7 +5537,10 @@
"example": "Always firing"
},
"uid": {
"type": "string"
"type": "string",
"maxLength": 40,
"minLength": 1,
"pattern": "^[a-zA-Z0-9-_]+$"
},
"updated": {
"type": "string",
@@ -112,6 +112,8 @@ func (service *AlertRuleService) GetAlertRuleWithFolderTitle(ctx context.Context
func (service *AlertRuleService) CreateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance, userID int64) (models.AlertRule, error) {
if rule.UID == "" {
rule.UID = util.GenerateShortUID()
} else if err := util.ValidateUID(rule.UID); err != nil {
return models.AlertRule{}, errors.Join(models.ErrAlertRuleFailedValidation, fmt.Errorf("cannot create rule with UID '%s': %w", rule.UID, err))
}
interval, err := service.ruleStore.GetRuleGroupInterval(ctx, rule.OrgID, rule.NamespaceUID, rule.RuleGroup)
// if the alert group does not exists we just use the default interval
@@ -4,12 +4,15 @@ import (
"context"
"encoding/json"
"strconv"
"strings"
"testing"
"time"
"github.com/grafana/grafana/pkg/expr"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/models"
@@ -21,21 +24,6 @@ func TestAlertRuleService(t *testing.T) {
ruleService := createAlertRuleService(t)
var orgID int64 = 1
t.Run("alert rule creation should return the created id", func(t *testing.T) {
rule, err := ruleService.CreateAlertRule(context.Background(), dummyRule("test#1", orgID), models.ProvenanceNone, 0)
require.NoError(t, err)
require.NotEqual(t, 0, rule.ID, "expected to get the created id and not the zero value")
})
t.Run("alert rule creation should set the right provenance", func(t *testing.T) {
rule, err := ruleService.CreateAlertRule(context.Background(), dummyRule("test#2", orgID), models.ProvenanceAPI, 0)
require.NoError(t, err)
_, provenance, err := ruleService.GetAlertRule(context.Background(), orgID, rule.UID)
require.NoError(t, err)
require.Equal(t, models.ProvenanceAPI, provenance)
})
t.Run("group creation should set the right provenance", func(t *testing.T) {
group := createDummyGroup("group-test-1", orgID)
err := ruleService.ReplaceRuleGroup(context.Background(), orgID, group, 0, models.ProvenanceAPI)
@@ -531,6 +519,45 @@ func TestAlertRuleService(t *testing.T) {
})
}
func TestCreateAlertRule(t *testing.T) {
ruleService := createAlertRuleService(t)
var orgID int64 = 1
t.Run("should return the created id", func(t *testing.T) {
rule, err := ruleService.CreateAlertRule(context.Background(), dummyRule("test#1", orgID), models.ProvenanceNone, 0)
require.NoError(t, err)
require.NotEqual(t, 0, rule.ID, "expected to get the created id and not the zero value")
})
t.Run("should set the right provenance", func(t *testing.T) {
rule, err := ruleService.CreateAlertRule(context.Background(), dummyRule("test#2", orgID), models.ProvenanceAPI, 0)
require.NoError(t, err)
_, provenance, err := ruleService.GetAlertRule(context.Background(), orgID, rule.UID)
require.NoError(t, err)
require.Equal(t, models.ProvenanceAPI, provenance)
})
t.Run("when UID is specified", func(t *testing.T) {
t.Run("return error if it is not valid UID", func(t *testing.T) {
rule := dummyRule("test#3", orgID)
rule.UID = strings.Repeat("1", util.MaxUIDLength+1)
rule, err := ruleService.CreateAlertRule(context.Background(), rule, models.ProvenanceNone, 0)
require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation)
})
t.Run("should create a new rule with this UID", func(t *testing.T) {
rule := dummyRule("test#3", orgID)
uid := util.GenerateShortUID()
rule.UID = uid
created, err := ruleService.CreateAlertRule(context.Background(), rule, models.ProvenanceNone, 0)
require.NoError(t, err)
require.Equal(t, uid, created.UID)
_, _, err = ruleService.GetAlertRule(context.Background(), orgID, uid)
require.NoError(t, err)
})
})
}
func createAlertRuleService(t *testing.T) AlertRuleService {
t.Helper()
sqlStore := db.InitTestDB(t)
@@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
@@ -190,6 +191,8 @@ func (ecp *ContactPointService) CreateContactPoint(ctx context.Context, orgID in
if contactPoint.UID == "" {
contactPoint.UID = util.GenerateShortUID()
} else if err := util.ValidateUID(contactPoint.UID); err != nil {
return apimodels.EmbeddedContactPoint{}, errors.Join(ErrValidation, fmt.Errorf("cannot create contact point with UID '%s': %w", contactPoint.UID, err))
}
jsonData, err := contactPoint.Settings.MarshalJSON()
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/prometheus/alertmanager/config"
@@ -23,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func TestContactPointService(t *testing.T) {
@@ -84,6 +86,16 @@ func TestContactPointService(t *testing.T) {
require.Equal(t, customUID, cps[1].UID)
})
t.Run("it's not possible to use invalid UID", func(t *testing.T) {
customUID := strings.Repeat("1", util.MaxUIDLength+1)
sut := createContactPointServiceSut(t, secretsService)
newCp := createTestContactPoint()
newCp.UID = customUID
_, err := sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI)
require.ErrorIs(t, err, ErrValidation)
})
t.Run("it's not possible to use the same uid twice", func(t *testing.T) {
customUID := "1337"
sut := createContactPointServiceSut(t, secretsService)