mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
Alerting: Get grafana-managed alert rule by UID (#86845)
* Add auth checks and test * Check user is authorized to view rule and add tests * Change naming * Update Swagger params * Update auth test and swagger gen * Update swagger gen * Change response to GettableExtendedRuleNode * openapi3-gen * Update tests with refactors models pkg
This commit is contained in:
parent
9e6de035c0
commit
df25e9197e
@ -263,6 +263,29 @@ func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Res
|
||||
return response.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// RouteGetRuleByUID returns the alert rule with the given UID
|
||||
func (srv RulerSrv) RouteGetRuleByUID(c *contextmodel.ReqContext, ruleUID string) response.Response {
|
||||
ctx := c.Req.Context()
|
||||
orgID := c.SignedInUser.GetOrgID()
|
||||
|
||||
rule, err := srv.getAuthorizedRuleByUid(ctx, c, ruleUID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) {
|
||||
return response.Empty(http.StatusNotFound)
|
||||
}
|
||||
return response.ErrOrFallback(http.StatusInternalServerError, "failed to get rule by UID", err)
|
||||
}
|
||||
|
||||
provenance, err := srv.provenanceStore.GetProvenance(ctx, &rule, orgID)
|
||||
if err != nil {
|
||||
return response.ErrOrFallback(http.StatusInternalServerError, "failed to get rule provenance", err)
|
||||
}
|
||||
|
||||
result := toGettableExtendedRuleNode(rule, map[string]ngmodels.Provenance{rule.ResourceID(): provenance})
|
||||
|
||||
return response.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
|
@ -318,6 +318,79 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteGetRuleByUID(t *testing.T) {
|
||||
t.Run("rule is successfully fetched with the correct UID", func(t *testing.T) {
|
||||
orgID := rand.Int63()
|
||||
folder := randFolder()
|
||||
ruleStore := fakes.NewRuleStore(t)
|
||||
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
|
||||
groupKey := models.GenerateGroupKey(orgID)
|
||||
groupKey.NamespaceUID = folder.UID
|
||||
gen := models.RuleGen.With(models.RuleGen.WithGroupKey(groupKey))
|
||||
|
||||
createdRules := gen.With(gen.WithUniqueGroupIndex(), gen.WithUniqueID()).GenerateManyRef(3)
|
||||
require.Len(t, createdRules, 3)
|
||||
ruleStore.PutRule(context.Background(), createdRules...)
|
||||
|
||||
perms := createPermissionsForRules(createdRules, orgID)
|
||||
req := createRequestContextWithPerms(orgID, perms, nil)
|
||||
|
||||
expectedRule := createdRules[1]
|
||||
response := createService(ruleStore).RouteGetRuleByUID(req, expectedRule.UID)
|
||||
|
||||
require.Equal(t, http.StatusOK, response.Status())
|
||||
result := &apimodels.GettableExtendedRuleNode{}
|
||||
require.NoError(t, json.Unmarshal(response.Body(), result))
|
||||
require.NotNil(t, result)
|
||||
|
||||
require.Equal(t, expectedRule.UID, result.GrafanaManagedAlert.UID)
|
||||
require.Equal(t, expectedRule.RuleGroup, result.GrafanaManagedAlert.RuleGroup)
|
||||
require.Equal(t, expectedRule.Title, result.GrafanaManagedAlert.Title)
|
||||
})
|
||||
|
||||
t.Run("error when fetching rule with non-existent UID", func(t *testing.T) {
|
||||
orgID := rand.Int63()
|
||||
folder := randFolder()
|
||||
ruleStore := fakes.NewRuleStore(t)
|
||||
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
|
||||
groupKey := models.GenerateGroupKey(orgID)
|
||||
groupKey.NamespaceUID = folder.UID
|
||||
gen := models.RuleGen.With(models.RuleGen.WithGroupKey(groupKey))
|
||||
|
||||
createdRules := gen.With(gen.WithUniqueGroupIndex(), gen.WithUniqueID()).GenerateManyRef(3)
|
||||
require.Len(t, createdRules, 3)
|
||||
ruleStore.PutRule(context.Background(), createdRules...)
|
||||
|
||||
perms := createPermissionsForRules(createdRules, orgID)
|
||||
req := createRequestContextWithPerms(orgID, perms, nil)
|
||||
response := createService(ruleStore).RouteGetRuleByUID(req, "foobar")
|
||||
|
||||
require.Equal(t, http.StatusNotFound, response.Status())
|
||||
})
|
||||
|
||||
t.Run("error due to user not being authorized to view a rule in the group", func(t *testing.T) {
|
||||
orgID := rand.Int63()
|
||||
folder := randFolder()
|
||||
ruleStore := fakes.NewRuleStore(t)
|
||||
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
|
||||
groupKey := models.GenerateGroupKey(orgID)
|
||||
groupKey.NamespaceUID = folder.UID
|
||||
gen := models.RuleGen.With(models.RuleGen.WithGroupKey(groupKey))
|
||||
|
||||
authorizedRule := gen.With(gen.WithUniqueGroupIndex()).Generate()
|
||||
ruleStore.PutRule(context.Background(), &authorizedRule)
|
||||
|
||||
unauthorizedRule := gen.With(gen.WithUniqueGroupIndex()).Generate()
|
||||
ruleStore.PutRule(context.Background(), &unauthorizedRule)
|
||||
|
||||
perms := createPermissionsForRules([]*models.AlertRule{&authorizedRule}, orgID)
|
||||
req := createRequestContextWithPerms(orgID, perms, nil)
|
||||
response := createService(ruleStore).RouteGetRuleByUID(req, authorizedRule.UID)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, response.Status())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteGetRulesConfig(t *testing.T) {
|
||||
gen := models.RuleGen
|
||||
t.Run("fine-grained access is enabled", func(t *testing.T) {
|
||||
|
@ -40,6 +40,11 @@ func (api *API) authorize(method, path string) web.Handler {
|
||||
case http.MethodGet + "/api/ruler/grafana/api/v1/rules",
|
||||
http.MethodGet + "/api/ruler/grafana/api/v1/export/rules":
|
||||
eval = ac.EvalPermission(ac.ActionAlertingRuleRead)
|
||||
case http.MethodGet + "/api/ruler/grafana/api/v1/rule/{RuleUID}":
|
||||
eval = ac.EvalAll(
|
||||
ac.EvalPermission(ac.ActionAlertingRuleRead),
|
||||
ac.EvalPermission(dashboards.ActionFoldersRead),
|
||||
)
|
||||
case http.MethodPost + "/api/ruler/grafana/api/v1/rules/{Namespace}/export":
|
||||
scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.Parameter(":Namespace"))
|
||||
// more granular permissions are enforced by the handler via "authorizeRuleChanges"
|
||||
|
@ -40,7 +40,7 @@ func TestAuthorize(t *testing.T) {
|
||||
}
|
||||
paths[p] = methods
|
||||
}
|
||||
require.Len(t, paths, 58)
|
||||
require.Len(t, paths, 59)
|
||||
|
||||
ac := acmock.New()
|
||||
api := &API{AccessControl: ac}
|
||||
|
@ -93,6 +93,10 @@ func (f *RulerApiHandler) handleRouteGetGrafanaRulesConfig(ctx *contextmodel.Req
|
||||
return f.GrafanaRuler.RouteGetRulesConfig(ctx)
|
||||
}
|
||||
|
||||
func (f *RulerApiHandler) handleRouteGetRuleByUID(ctx *contextmodel.ReqContext, ruleUID string) response.Response {
|
||||
return f.GrafanaRuler.RouteGetRuleByUID(ctx, ruleUID)
|
||||
}
|
||||
|
||||
func (f *RulerApiHandler) handleRoutePostNameGrafanaRulesConfig(ctx *contextmodel.ReqContext, conf apimodels.PostableRuleGroupConfig, namespace string) response.Response {
|
||||
payloadType := conf.Type()
|
||||
if payloadType != apimodels.GrafanaBackend {
|
||||
|
@ -28,6 +28,7 @@ type RulerApi interface {
|
||||
RouteGetGrafanaRulesConfig(*contextmodel.ReqContext) response.Response
|
||||
RouteGetNamespaceGrafanaRulesConfig(*contextmodel.ReqContext) response.Response
|
||||
RouteGetNamespaceRulesConfig(*contextmodel.ReqContext) response.Response
|
||||
RouteGetRuleByUID(*contextmodel.ReqContext) response.Response
|
||||
RouteGetRulegGroupConfig(*contextmodel.ReqContext) response.Response
|
||||
RouteGetRulesConfig(*contextmodel.ReqContext) response.Response
|
||||
RouteGetRulesForExport(*contextmodel.ReqContext) response.Response
|
||||
@ -80,6 +81,11 @@ func (f *RulerApiHandler) RouteGetNamespaceRulesConfig(ctx *contextmodel.ReqCont
|
||||
namespaceParam := web.Params(ctx.Req)[":Namespace"]
|
||||
return f.handleRouteGetNamespaceRulesConfig(ctx, datasourceUIDParam, namespaceParam)
|
||||
}
|
||||
func (f *RulerApiHandler) RouteGetRuleByUID(ctx *contextmodel.ReqContext) response.Response {
|
||||
// Parse Path Parameters
|
||||
ruleUIDParam := web.Params(ctx.Req)[":RuleUID"]
|
||||
return f.handleRouteGetRuleByUID(ctx, ruleUIDParam)
|
||||
}
|
||||
func (f *RulerApiHandler) RouteGetRulegGroupConfig(ctx *contextmodel.ReqContext) response.Response {
|
||||
// Parse Path Parameters
|
||||
datasourceUIDParam := web.Params(ctx.Req)[":DatasourceUID"]
|
||||
@ -225,6 +231,18 @@ func (api *API) RegisterRulerApiEndpoints(srv RulerApi, m *metrics.API) {
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Get(
|
||||
toMacaronPath("/api/ruler/grafana/api/v1/rule/{RuleUID}"),
|
||||
requestmeta.SetOwner(requestmeta.TeamAlerting),
|
||||
requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow),
|
||||
api.authorize(http.MethodGet, "/api/ruler/grafana/api/v1/rule/{RuleUID}"),
|
||||
metrics.Instrument(
|
||||
http.MethodGet,
|
||||
"/api/ruler/grafana/api/v1/rule/{RuleUID}",
|
||||
api.Hooks.Wrap(srv.RouteGetRuleByUID),
|
||||
m,
|
||||
),
|
||||
)
|
||||
group.Get(
|
||||
toMacaronPath("/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}"),
|
||||
requestmeta.SetOwner(requestmeta.TeamAlerting),
|
||||
|
@ -4546,7 +4546,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlert": {
|
||||
"description": "GettableAlert gettable alert",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"$ref": "#/definitions/labelSet"
|
||||
@ -4608,6 +4607,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"gettableSilence": {
|
||||
"description": "GettableSilence gettable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -4807,7 +4807,6 @@
|
||||
"type": "array"
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
|
@ -8,6 +8,18 @@ import (
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
// swagger:route Get /ruler/grafana/api/v1/rule/{RuleUID} ruler RouteGetRuleByUID
|
||||
//
|
||||
// Get rule by UID
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
//
|
||||
// Responses:
|
||||
// 202: GettableExtendedRuleNode
|
||||
// 403: ForbiddenError
|
||||
// 404: description: Not found.
|
||||
|
||||
// swagger:route Get /ruler/grafana/api/v1/rules ruler RouteGetGrafanaRulesConfig
|
||||
//
|
||||
// List rule groups
|
||||
@ -206,6 +218,12 @@ type PathGetRulesParams struct {
|
||||
PanelID int64
|
||||
}
|
||||
|
||||
// swagger:parameters RouteGetRuleByUID
|
||||
type PathGetRuleByUIDParams struct {
|
||||
// in: path
|
||||
RuleUID string
|
||||
}
|
||||
|
||||
// swagger:model
|
||||
type RuleGroupConfigResponse struct {
|
||||
GettableRuleGroupConfig
|
||||
|
@ -4176,6 +4176,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"URL": {
|
||||
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
|
||||
"properties": {
|
||||
"ForceQuery": {
|
||||
"type": "boolean"
|
||||
@ -4211,7 +4212,7 @@
|
||||
"$ref": "#/definitions/Userinfo"
|
||||
}
|
||||
},
|
||||
"title": "URL is a custom URL type that allows validation at configuration load time.",
|
||||
"title": "A URL represents a parsed URL (technically, a URI reference).",
|
||||
"type": "object"
|
||||
},
|
||||
"UpdateRuleGroupResponse": {
|
||||
@ -4545,6 +4546,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlert": {
|
||||
"description": "GettableAlert gettable alert",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"$ref": "#/definitions/labelSet"
|
||||
@ -4606,7 +4608,6 @@
|
||||
"type": "array"
|
||||
},
|
||||
"gettableSilence": {
|
||||
"description": "GettableSilence gettable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -4662,7 +4663,6 @@
|
||||
"type": "array"
|
||||
},
|
||||
"integration": {
|
||||
"description": "Integration integration",
|
||||
"properties": {
|
||||
"lastNotifyAttempt": {
|
||||
"description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time",
|
||||
@ -4806,6 +4806,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -6218,6 +6219,43 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ruler/grafana/api/v1/rule/{RuleUID}": {
|
||||
"get": {
|
||||
"description": "Get rule by UID",
|
||||
"operationId": "RouteGetRuleByUID",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "RuleUID",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "GettableGrafanaRule",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/GettableGrafanaRule"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "ForbiddenError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ForbiddenError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": " Not found."
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"ruler"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/ruler/grafana/api/v1/rules": {
|
||||
"get": {
|
||||
"description": "List rule groups",
|
||||
|
@ -1272,6 +1272,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ruler/grafana/api/v1/rule/{RuleUID}": {
|
||||
"get": {
|
||||
"description": "Get rule by UID",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"ruler"
|
||||
],
|
||||
"operationId": "RouteGetRuleByUID",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "RuleUID",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "GettableGrafanaRule",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/GettableGrafanaRule"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "ForbiddenError",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ForbiddenError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": " Not found."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ruler/grafana/api/v1/rules": {
|
||||
"get": {
|
||||
"description": "List rule groups",
|
||||
@ -7654,8 +7691,9 @@
|
||||
}
|
||||
},
|
||||
"URL": {
|
||||
"description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use the EscapedPath method, which preserves\nthe original encoding of Path.\n\nThe RawPath field is an optional field which is only set when the default\nencoding of Path is different from the escaped path. See the EscapedPath method\nfor more details.\n\nURL's String method uses the EscapedPath method to obtain the path.",
|
||||
"type": "object",
|
||||
"title": "URL is a custom URL type that allows validation at configuration load time.",
|
||||
"title": "A URL represents a parsed URL (technically, a URI reference).",
|
||||
"properties": {
|
||||
"ForceQuery": {
|
||||
"type": "boolean"
|
||||
@ -8025,6 +8063,7 @@
|
||||
}
|
||||
},
|
||||
"gettableAlert": {
|
||||
"description": "GettableAlert gettable alert",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"labels",
|
||||
@ -8088,7 +8127,6 @@
|
||||
"$ref": "#/definitions/gettableAlerts"
|
||||
},
|
||||
"gettableSilence": {
|
||||
"description": "GettableSilence gettable silence",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"comment",
|
||||
@ -8146,7 +8184,6 @@
|
||||
"$ref": "#/definitions/gettableSilences"
|
||||
},
|
||||
"integration": {
|
||||
"description": "Integration integration",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
@ -8291,6 +8328,7 @@
|
||||
}
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"comment",
|
||||
|
@ -21311,7 +21311,6 @@
|
||||
}
|
||||
},
|
||||
"gettableAlert": {
|
||||
"description": "GettableAlert gettable alert",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"labels",
|
||||
@ -21373,6 +21372,7 @@
|
||||
}
|
||||
},
|
||||
"gettableSilence": {
|
||||
"description": "GettableSilence gettable silence",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"comment",
|
||||
@ -21572,7 +21572,6 @@
|
||||
}
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"comment",
|
||||
|
@ -11947,7 +11947,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlert": {
|
||||
"description": "GettableAlert gettable alert",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"$ref": "#/components/schemas/labelSet"
|
||||
@ -12009,6 +12008,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"gettableSilence": {
|
||||
"description": "GettableSilence gettable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
@ -12208,7 +12208,6 @@
|
||||
"type": "array"
|
||||
},
|
||||
"postableSilence": {
|
||||
"description": "PostableSilence postable silence",
|
||||
"properties": {
|
||||
"comment": {
|
||||
"description": "comment",
|
||||
|
Loading…
Reference in New Issue
Block a user