3
0
mirror of https://github.com/grafana/grafana.git synced 2025-02-25 18:55:37 -06:00

Alerting: Fix fine-grained rule access control to use 403 for authorization error ()

* use 403 for authorization error
* update silences API
* add ForbiddenError to rule API responses
This commit is contained in:
Yuri Tseretyan 2023-12-07 13:43:58 -05:00 committed by GitHub
parent aa12c6c772
commit 2be7605794
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 629 additions and 444 deletions

View File

@ -8,7 +8,7 @@ import (
)
var (
errAuthorizationGeneric = errutil.Unauthorized("alerting.unauthorized")
errAuthorizationGeneric = errutil.Forbidden("alerting.unauthorized")
)
func NewAuthorizationErrorWithPermissions(action string, eval accesscontrol.Evaluator) error {

View File

@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
authz "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/store"
@ -67,12 +68,13 @@ func (srv AlertmanagerSrv) RouteCreateSilence(c *contextmodel.ReqContext, postab
if postableSilence.ID == "" {
action = accesscontrol.ActionAlertingInstanceCreate
}
if !accesscontrol.HasAccess(srv.ac, c)(accesscontrol.EvalPermission(action)) {
evaluator := accesscontrol.EvalPermission(action)
if !accesscontrol.HasAccess(srv.ac, c)(evaluator) {
errAction := "update"
if postableSilence.ID == "" {
errAction = "create"
}
return ErrResp(http.StatusUnauthorized, fmt.Errorf("user is not authorized to %s silences", errAction), "")
return response.Err(authz.NewAuthorizationErrorWithPermissions(fmt.Sprintf("%s silences", errAction), evaluator))
}
silenceID, err := am.CreateSilence(c.Req.Context(), &postableSilence)

View File

@ -571,7 +571,7 @@ func TestRouteCreateSilence(t *testing.T) {
permissions: map[int64]map[string][]string{
1: {},
},
expectedStatus: http.StatusUnauthorized,
expectedStatus: http.StatusForbidden,
},
{
name: "new silence, role-based access control is enabled, authorized",
@ -587,7 +587,7 @@ func TestRouteCreateSilence(t *testing.T) {
permissions: map[int64]map[string][]string{
1: {accesscontrol.ActionAlertingInstanceCreate: {}},
},
expectedStatus: http.StatusUnauthorized,
expectedStatus: http.StatusForbidden,
},
{
name: "update silence, role-based access control is enabled, authorized",

View File

@ -50,7 +50,7 @@ var (
// 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.
// Returns http.StatusUnauthorized if user does not have access to any of the rules that match the filter.
// Returns http.StatusForbidden if user does not have access to any of the rules that match the filter.
// Returns http.StatusBadRequest if all rules that match the filter and the user is authorized to delete are provisioned.
func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser)
@ -170,7 +170,7 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *contextmodel.ReqContext, nam
}
// RouteGetRulesGroupConfig returns rules that belong to a specific group in a specific namespace (folder).
// If user does not have access to at least one of the rule in the group, returns status 401 Unauthorized
// If user does not have access to at least one of the rule in the group, returns status 403 Forbidden
func (srv RulerSrv) RouteGetRulesGroupConfig(c *contextmodel.ReqContext, namespaceTitle string, ruleGroup string) response.Response {
namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {

View File

@ -352,27 +352,27 @@ func TestExportRules(t *testing.T) {
expectedStatus: 400,
},
{
title: "unauthorized if folders are not accessible",
title: "forbidden if folders are not accessible",
params: url.Values{
"folderUid": []string{noAccessByFolder[0].NamespaceUID},
},
expectedStatus: 401,
expectedStatus: http.StatusForbidden,
expectedRules: nil,
},
{
title: "unauthorized if group is not accessible",
title: "forbidden if group is not accessible",
params: url.Values{
"folderUid": []string{noAccessKey1.NamespaceUID},
"group": []string{noAccessKey1.RuleGroup},
},
expectedStatus: 401,
expectedStatus: http.StatusForbidden,
},
{
title: "unauthorized if rule's group is not accessible",
title: "forbidden if rule's group is not accessible",
params: url.Values{
"ruleUid": []string{noAccessRule.UID},
},
expectedStatus: 401,
expectedStatus: http.StatusForbidden,
},
{
title: "return in JSON if header is specified",

View File

@ -73,14 +73,14 @@ func TestRouteDeleteAlertRules(t *testing.T) {
t.Run("when fine-grained access is enabled", func(t *testing.T) {
t.Run("and group argument is empty", func(t *testing.T) {
t.Run("return 401 if user is not authorized to access any group in the folder", func(t *testing.T) {
t.Run("return Forbidden if user is not authorized to access any group in the folder", func(t *testing.T) {
ruleStore := initFakeRuleStore(t)
ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
request := createRequestContextWithPerms(orgID, map[int64]map[string][]string{}, nil)
response := createService(ruleStore).RouteDeleteAlertRules(request, folder.Title, "")
require.Equalf(t, 401, response.Status(), "Expected 401 but got %d: %v", response.Status(), string(response.Body()))
require.Equalf(t, http.StatusForbidden, response.Status(), "Expected 403 but got %d: %v", response.Status(), string(response.Body()))
require.Empty(t, getRecordedCommand(ruleStore))
})
@ -139,7 +139,7 @@ func TestRouteDeleteAlertRules(t *testing.T) {
})
t.Run("and group argument is not empty", func(t *testing.T) {
groupName := util.GenerateShortUID()
t.Run("return 401 if user is not authorized to access the group", func(t *testing.T) {
t.Run("return Forbidden if user is not authorized to access the group", func(t *testing.T) {
ruleStore := initFakeRuleStore(t)
authorizedRulesInGroup := models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(groupName)))
@ -152,7 +152,7 @@ func TestRouteDeleteAlertRules(t *testing.T) {
response := createService(ruleStore).RouteDeleteAlertRules(requestCtx, folder.Title, groupName)
require.Equalf(t, 401, response.Status(), "Expected 401 but got %d: %v", response.Status(), string(response.Body()))
require.Equalf(t, http.StatusForbidden, response.Status(), "Expected 403 but got %d: %v", response.Status(), string(response.Body()))
deleteCommands := getRecordedCommand(ruleStore)
require.Empty(t, deleteCommands)
})
@ -396,14 +396,14 @@ func TestRouteGetRulesGroupConfig(t *testing.T) {
expectedRules := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withGroupKey(groupKey)))
ruleStore.PutRule(context.Background(), expectedRules...)
t.Run("and return 401 if user does not have access one of rules", func(t *testing.T) {
t.Run("and return Forbidden if user does not have access one of rules", func(t *testing.T) {
permissions := createPermissionsForRules(expectedRules[1:], orgID)
request := createRequestContextWithPerms(orgID, permissions, map[string]string{
":Namespace": folder.Title,
":Groupname": groupKey.RuleGroup,
})
response := createService(ruleStore).RouteGetRulesGroupConfig(request, folder.Title, groupKey.RuleGroup)
require.Equal(t, http.StatusUnauthorized, response.Status())
require.Equal(t, http.StatusForbidden, response.Status())
})
t.Run("and return rules if user has access to all of them", func(t *testing.T) {

View File

@ -137,7 +137,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) {
},
}
t.Run("should return 401 if user cannot query a data source", func(t *testing.T) {
t.Run("should return Forbidden if user cannot query a data source", func(t *testing.T) {
data1 := models.GenerateAlertQuery()
data2 := models.GenerateAlertQuery()
@ -156,7 +156,7 @@ func TestRouteTestGrafanaRuleConfig(t *testing.T) {
NamespaceTitle: "test-folder",
})
require.Equal(t, http.StatusUnauthorized, response.Status())
require.Equal(t, http.StatusForbidden, response.Status())
})
t.Run("should return 200 if user can query all data sources", func(t *testing.T) {
@ -208,7 +208,7 @@ func TestRouteEvalQueries(t *testing.T) {
},
}
t.Run("should return 401 if user cannot query a data source", func(t *testing.T) {
t.Run("should return Forbidden if user cannot query a data source", func(t *testing.T) {
data1 := models.GenerateAlertQuery()
data2 := models.GenerateAlertQuery()
@ -224,7 +224,7 @@ func TestRouteEvalQueries(t *testing.T) {
Now: time.Time{},
})
require.Equal(t, http.StatusUnauthorized, response.Status())
require.Equal(t, http.StatusForbidden, response.Status())
})
t.Run("should return 200 if user can query all data sources", func(t *testing.T) {

View File

@ -280,9 +280,6 @@
},
"type": "object"
},
"AlertStateType": {
"type": "string"
},
"AlertingFileExport": {
"properties": {
"apiVersion": {
@ -404,80 +401,6 @@
},
"type": "object"
},
"Annotation": {
"properties": {
"alertId": {
"format": "int64",
"type": "integer"
},
"alertName": {
"type": "string"
},
"avatarUrl": {
"type": "string"
},
"created": {
"format": "int64",
"type": "integer"
},
"dashboardId": {
"format": "int64",
"type": "integer"
},
"dashboardUID": {
"type": "string"
},
"data": {
"$ref": "#/definitions/Json"
},
"email": {
"type": "string"
},
"id": {
"format": "int64",
"type": "integer"
},
"login": {
"type": "string"
},
"newState": {
"type": "string"
},
"panelId": {
"format": "int64",
"type": "integer"
},
"prevState": {
"type": "string"
},
"tags": {
"items": {
"type": "string"
},
"type": "array"
},
"text": {
"type": "string"
},
"time": {
"format": "int64",
"type": "integer"
},
"timeEnd": {
"format": "int64",
"type": "integer"
},
"updated": {
"format": "int64",
"type": "integer"
},
"userId": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
},
"ApiRuleNode": {
"properties": {
"alert": {
@ -657,75 +580,12 @@
},
"type": "array"
},
"CookieType": {
"type": "string"
},
"CounterResetHint": {
"description": "or alternatively that we are dealing with a gauge histogram, where counter resets do not apply.",
"format": "uint8",
"title": "CounterResetHint contains the known information about a counter reset,",
"type": "integer"
},
"CreateLibraryElementCommand": {
"description": "CreateLibraryElementCommand is the command for adding a LibraryElement",
"properties": {
"folderId": {
"description": "ID of the folder where the library element is stored.\n\nDeprecated: use FolderUID instead",
"format": "int64",
"type": "integer"
},
"folderUid": {
"description": "UID of the folder where the library element is stored.",
"type": "string"
},
"kind": {
"description": "Kind of element to create, Use 1 for library panels or 2 for c.\nDescription:\n1 - library panels\n2 - library variables",
"enum": [
1,
2
],
"format": "int64",
"type": "integer"
},
"model": {
"description": "The JSON model for the library element.",
"type": "object"
},
"name": {
"description": "Name of the library element.",
"type": "string"
},
"uid": {
"type": "string"
}
},
"type": "object"
},
"DashboardACLUpdateItem": {
"properties": {
"permission": {
"$ref": "#/definitions/PermissionType"
},
"role": {
"enum": [
"None",
"Viewer",
"Editor",
"Admin"
],
"type": "string"
},
"teamId": {
"format": "int64",
"type": "integer"
},
"userId": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
},
"DataLink": {
"description": "DataLink define what",
"properties": {
@ -988,6 +848,9 @@
},
"EvalQueriesPayload": {
"properties": {
"condition": {
"type": "string"
},
"data": {
"items": {
"$ref": "#/definitions/AlertQuery"
@ -1195,6 +1058,14 @@
"title": "FloatHistogram is similar to Histogram but uses float64 for all\ncounts. Additionally, bucket counts are absolute and not deltas.",
"type": "object"
},
"ForbiddenError": {
"properties": {
"body": {
"$ref": "#/definitions/PublicError"
}
},
"type": "object"
},
"Frame": {
"description": "Each Field is well typed by its FieldType and supports optional Labels.\n\nA Frame is a general data container for Grafana. A Frame can be table data\nor time series data depending on its content and field types.",
"properties": {
@ -1960,82 +1831,6 @@
},
"type": "array"
},
"LegacyAlert": {
"properties": {
"Created": {
"format": "date-time",
"type": "string"
},
"DashboardID": {
"format": "int64",
"type": "integer"
},
"EvalData": {
"$ref": "#/definitions/Json"
},
"ExecutionError": {
"type": "string"
},
"For": {
"$ref": "#/definitions/Duration"
},
"Frequency": {
"format": "int64",
"type": "integer"
},
"Handler": {
"format": "int64",
"type": "integer"
},
"ID": {
"format": "int64",
"type": "integer"
},
"Message": {
"type": "string"
},
"Name": {
"type": "string"
},
"NewStateDate": {
"format": "date-time",
"type": "string"
},
"OrgID": {
"format": "int64",
"type": "integer"
},
"PanelID": {
"format": "int64",
"type": "integer"
},
"Settings": {
"$ref": "#/definitions/Json"
},
"Severity": {
"type": "string"
},
"Silenced": {
"type": "boolean"
},
"State": {
"$ref": "#/definitions/AlertStateType"
},
"StateChanges": {
"format": "int64",
"type": "integer"
},
"Updated": {
"format": "date-time",
"type": "string"
},
"Version": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
},
"LinkTransformationConfig": {
"properties": {
"expression": {
@ -2107,48 +1902,6 @@
},
"type": "array"
},
"MetricRequest": {
"properties": {
"debug": {
"type": "boolean"
},
"from": {
"description": "From Start time in epoch timestamps in milliseconds or relative using Grafana time units.",
"example": "now-1h",
"type": "string"
},
"queries": {
"description": "queries.refId Specifies an identifier of the query. Is optional and default to “A”.\nqueries.datasourceId Specifies the data source to be queried. Each query in the request must have an unique datasourceId.\nqueries.maxDataPoints - Species maximum amount of data points that dashboard panel can render. Is optional and default to 100.\nqueries.intervalMs - Specifies the time interval in milliseconds of time series. Is optional and defaults to 1000.",
"example": [
{
"datasource": {
"uid": "PD8C576611E62080A"
},
"format": "table",
"intervalMs": 86400000,
"maxDataPoints": 1092,
"rawSql": "SELECT 1 as valueOne, 2 as valueTwo",
"refId": "A"
}
],
"items": {
"$ref": "#/definitions/Json"
},
"type": "array"
},
"to": {
"description": "To End time in epoch timestamps in milliseconds or relative using Grafana time units.",
"example": "now",
"type": "string"
}
},
"required": [
"from",
"to",
"queries"
],
"type": "object"
},
"MultiStatus": {
"type": "object"
},
@ -2182,24 +1935,6 @@
},
"type": "object"
},
"NewApiKeyResult": {
"properties": {
"id": {
"example": 1,
"format": "int64",
"type": "integer"
},
"key": {
"example": "glsa_yscW25imSKJIuav8zF37RZmnbiDvB05G_fcaaf58a",
"type": "string"
},
"name": {
"example": "grafana",
"type": "string"
}
},
"type": "object"
},
"NotFound": {
"type": "object"
},
@ -2230,12 +1965,58 @@
},
"NotificationPolicyExport": {
"properties": {
"Policy": {
"$ref": "#/definitions/RouteExport"
"continue": {
"type": "boolean"
},
"group_by": {
"items": {
"type": "string"
},
"type": "array"
},
"group_interval": {
"type": "string"
},
"group_wait": {
"type": "string"
},
"match": {
"additionalProperties": {
"type": "string"
},
"description": "Deprecated. Remove before v1.0 release.",
"type": "object"
},
"match_re": {
"$ref": "#/definitions/MatchRegexps"
},
"matchers": {
"$ref": "#/definitions/Matchers"
},
"mute_time_intervals": {
"items": {
"type": "string"
},
"type": "array"
},
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"orgId": {
"format": "int64",
"type": "integer"
},
"receiver": {
"type": "string"
},
"repeat_interval": {
"type": "string"
},
"routes": {
"items": {
"$ref": "#/definitions/RouteExport"
},
"type": "array"
}
},
"title": "NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.",
@ -2504,56 +2285,9 @@
},
"type": "object"
},
"PatchPrefsCmd": {
"properties": {
"cookies": {
"items": {
"$ref": "#/definitions/CookieType"
},
"type": "array"
},
"homeDashboardId": {
"default": 0,
"description": "The numerical :id of a favorited dashboard",
"format": "int64",
"type": "integer"
},
"homeDashboardUID": {
"type": "string"
},
"language": {
"type": "string"
},
"queryHistory": {
"$ref": "#/definitions/QueryHistoryPreference"
},
"theme": {
"enum": [
"light",
"dark"
],
"type": "string"
},
"timezone": {
"enum": [
"utc",
"browser"
],
"type": "string"
},
"weekStart": {
"type": "string"
}
},
"type": "object"
},
"PermissionDenied": {
"type": "object"
},
"PermissionType": {
"format": "int64",
"type": "integer"
},
"Point": {
"description": "If H is not nil, then this is a histogram point and only (T, H) is valid.\nIf H is nil, then only (T, V) is valid.",
"properties": {
@ -3057,6 +2791,26 @@
},
"type": "object"
},
"PublicError": {
"description": "PublicError is derived from Error and only contains information\navailable to the end user.",
"properties": {
"extra": {
"additionalProperties": {},
"type": "object"
},
"message": {
"type": "string"
},
"messageId": {
"type": "string"
},
"statusCode": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
},
"PushoverConfig": {
"properties": {
"device": {
@ -3113,14 +2867,6 @@
},
"type": "object"
},
"QueryHistoryPreference": {
"properties": {
"homeTab": {
"type": "string"
}
},
"type": "object"
},
"QueryStat": {
"description": "The embedded FieldConfig's display name must be set.\nIt corresponds to the QueryResultMetaStat on the frontend (https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L53).",
"properties": {
@ -4218,7 +3964,6 @@
"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"
@ -4254,62 +3999,7 @@
"$ref": "#/definitions/Userinfo"
}
},
"title": "A URL represents a parsed URL (technically, a URI reference).",
"type": "object"
},
"UpdateDashboardACLCommand": {
"properties": {
"items": {
"items": {
"$ref": "#/definitions/DashboardACLUpdateItem"
},
"type": "array"
}
},
"type": "object"
},
"UpdatePrefsCmd": {
"properties": {
"cookies": {
"items": {
"$ref": "#/definitions/CookieType"
},
"type": "array"
},
"homeDashboardId": {
"default": 0,
"description": "The numerical :id of a favorited dashboard",
"format": "int64",
"type": "integer"
},
"homeDashboardUID": {
"type": "string"
},
"language": {
"type": "string"
},
"queryHistory": {
"$ref": "#/definitions/QueryHistoryPreference"
},
"theme": {
"enum": [
"light",
"dark",
"system"
],
"type": "string"
},
"timezone": {
"enum": [
"utc",
"browser"
],
"type": "string"
},
"weekStart": {
"type": "string"
}
},
"title": "URL is a custom URL type that allows validation at configuration load time.",
"type": "object"
},
"UpdateRuleGroupResponse": {
@ -4515,7 +4205,6 @@
"type": "object"
},
"alertGroup": {
"description": "AlertGroup alert group",
"properties": {
"alerts": {
"description": "alerts",
@ -4644,7 +4333,6 @@
"type": "object"
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"properties": {
"annotations": {
"$ref": "#/definitions/labelSet"
@ -4707,6 +4395,7 @@
"type": "array"
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"properties": {
"comment": {
"description": "comment",
@ -4755,6 +4444,7 @@
"type": "object"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"items": {
"$ref": "#/definitions/gettableSilence"
},
@ -4905,7 +4595,6 @@
"type": "array"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@ -4943,6 +4632,7 @@
"type": "object"
},
"receiver": {
"description": "Receiver receiver",
"properties": {
"active": {
"description": "active",

View File

@ -17,6 +17,7 @@ import (
//
// Responses:
// 202: NamespaceConfigResponse
// 403: ForbiddenError
//
// swagger:route Get /api/ruler/grafana/api/v1/export/rules ruler RouteGetRulesForExport
@ -29,6 +30,7 @@ import (
//
// Responses:
// 200: AlertingFileExport
// 403: ForbiddenError
// 404: description: Not found.
// swagger:route Get /api/ruler/{DatasourceUID}/api/v1/rules ruler RouteGetRulesConfig
@ -40,6 +42,7 @@ import (
//
// Responses:
// 202: NamespaceConfigResponse
// 403: ForbiddenError
// 404: NotFound
// swagger:route POST /api/ruler/grafana/api/v1/rules/{Namespace} ruler RoutePostNameGrafanaRulesConfig
@ -52,6 +55,7 @@ import (
//
// Responses:
// 202: UpdateRuleGroupResponse
// 403: ForbiddenError
//
// swagger:route POST /api/ruler/grafana/api/v1/rules/{Namespace}/export ruler RoutePostRulesGroupForExport
@ -64,6 +68,7 @@ import (
//
// Responses:
// 200: AlertingFileExport
// 403: ForbiddenError
// 404: description: Not found.
// swagger:route POST /api/ruler/{DatasourceUID}/api/v1/rules/{Namespace} ruler RoutePostNameRulesConfig
@ -76,6 +81,7 @@ import (
//
// Responses:
// 202: Ack
// 403: ForbiddenError
// 404: NotFound
// swagger:route Get /api/ruler/grafana/api/v1/rules/{Namespace} ruler RouteGetNamespaceGrafanaRulesConfig
@ -86,6 +92,7 @@ import (
// - application/json
//
// Responses:
// 403: ForbiddenError
// 202: NamespaceConfigResponse
// swagger:route Get /api/ruler/{DatasourceUID}/api/v1/rules/{Namespace} ruler RouteGetNamespaceRulesConfig
@ -97,6 +104,7 @@ import (
//
// Responses:
// 202: NamespaceConfigResponse
// 403: ForbiddenError
// 404: NotFound
// swagger:route Delete /api/ruler/grafana/api/v1/rules/{Namespace} ruler RouteDeleteNamespaceGrafanaRulesConfig
@ -105,6 +113,7 @@ import (
//
// Responses:
// 202: Ack
// 403: ForbiddenError
// swagger:route Delete /api/ruler/{DatasourceUID}/api/v1/rules/{Namespace} ruler RouteDeleteNamespaceRulesConfig
//
@ -112,6 +121,7 @@ import (
//
// Responses:
// 202: Ack
// 403: ForbiddenError
// 404: NotFound
// swagger:route Get /api/ruler/grafana/api/v1/rules/{Namespace}/{Groupname} ruler RouteGetGrafanaRuleGroupConfig
@ -123,6 +133,7 @@ import (
//
// Responses:
// 202: RuleGroupConfigResponse
// 403: ForbiddenError
// swagger:route Get /api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname} ruler RouteGetRulegGroupConfig
//
@ -133,6 +144,7 @@ import (
//
// Responses:
// 202: RuleGroupConfigResponse
// 403: ForbiddenError
// 404: NotFound
// swagger:route Delete /api/ruler/grafana/api/v1/rules/{Namespace}/{Groupname} ruler RouteDeleteGrafanaRuleGroupConfig
@ -141,6 +153,7 @@ import (
//
// Responses:
// 202: Ack
// 403: ForbiddenError
// swagger:route Delete /api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname} ruler RouteDeleteRuleGroupConfig
//
@ -148,6 +161,7 @@ import (
//
// Responses:
// 202: Ack
// 403: ForbiddenError
// 404: NotFound
// swagger:parameters RoutePostNameRulesConfig RoutePostNameGrafanaRulesConfig RoutePostRulesGroupForExport

View File

@ -1,5 +1,7 @@
package definitions
import "github.com/grafana/grafana/pkg/util/errutil"
// swagger:model
type NotFound struct{}
@ -11,3 +13,10 @@ type ValidationError struct {
// example: error message
Msg string `json:"msg"`
}
// swagger:model
type ForbiddenError struct {
// The response message
// in: body
Body errutil.PublicError `json:"body"`
}

View File

@ -848,6 +848,9 @@
},
"EvalQueriesPayload": {
"properties": {
"condition": {
"type": "string"
},
"data": {
"items": {
"$ref": "#/definitions/AlertQuery"
@ -1055,6 +1058,14 @@
"title": "FloatHistogram is similar to Histogram but uses float64 for all\ncounts. Additionally, bucket counts are absolute and not deltas.",
"type": "object"
},
"ForbiddenError": {
"properties": {
"body": {
"$ref": "#/definitions/PublicError"
}
},
"type": "object"
},
"Frame": {
"description": "Each Field is well typed by its FieldType and supports optional Labels.\n\nA Frame is a general data container for Grafana. A Frame can be table data\nor time series data depending on its content and field types.",
"properties": {
@ -1954,12 +1965,58 @@
},
"NotificationPolicyExport": {
"properties": {
"Policy": {
"$ref": "#/definitions/RouteExport"
"continue": {
"type": "boolean"
},
"group_by": {
"items": {
"type": "string"
},
"type": "array"
},
"group_interval": {
"type": "string"
},
"group_wait": {
"type": "string"
},
"match": {
"additionalProperties": {
"type": "string"
},
"description": "Deprecated. Remove before v1.0 release.",
"type": "object"
},
"match_re": {
"$ref": "#/definitions/MatchRegexps"
},
"matchers": {
"$ref": "#/definitions/Matchers"
},
"mute_time_intervals": {
"items": {
"type": "string"
},
"type": "array"
},
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"orgId": {
"format": "int64",
"type": "integer"
},
"receiver": {
"type": "string"
},
"repeat_interval": {
"type": "string"
},
"routes": {
"items": {
"$ref": "#/definitions/RouteExport"
},
"type": "array"
}
},
"title": "NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.",
@ -2734,6 +2791,26 @@
},
"type": "object"
},
"PublicError": {
"description": "PublicError is derived from Error and only contains information\navailable to the end user.",
"properties": {
"extra": {
"additionalProperties": {},
"type": "object"
},
"message": {
"type": "string"
},
"messageId": {
"type": "string"
},
"statusCode": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
},
"PushoverConfig": {
"properties": {
"device": {
@ -3887,7 +3964,6 @@
"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"
@ -3923,7 +3999,7 @@
"$ref": "#/definitions/Userinfo"
}
},
"title": "A URL represents a parsed URL (technically, a URI reference).",
"title": "URL is a custom URL type that allows validation at configuration load time.",
"type": "object"
},
"UpdateRuleGroupResponse": {
@ -4129,7 +4205,6 @@
"type": "object"
},
"alertGroup": {
"description": "AlertGroup alert group",
"properties": {
"alerts": {
"description": "alerts",
@ -4314,13 +4389,13 @@
"type": "object"
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"items": {
"$ref": "#/definitions/gettableAlert"
},
"type": "array"
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"properties": {
"comment": {
"description": "comment",
@ -4520,7 +4595,6 @@
"type": "array"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@ -5910,6 +5984,12 @@
"$ref": "#/definitions/AlertingFileExport"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": " Not found."
}
@ -5945,6 +6025,12 @@
"schema": {
"$ref": "#/definitions/NamespaceConfigResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"tags": [
@ -5970,6 +6056,12 @@
"schema": {
"$ref": "#/definitions/Ack"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"tags": [
@ -5996,6 +6088,12 @@
"schema": {
"$ref": "#/definitions/NamespaceConfigResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"tags": [
@ -6030,6 +6128,12 @@
"schema": {
"$ref": "#/definitions/UpdateRuleGroupResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"tags": [
@ -6081,6 +6185,12 @@
"$ref": "#/definitions/AlertingFileExport"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": " Not found."
}
@ -6114,6 +6224,12 @@
"schema": {
"$ref": "#/definitions/Ack"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"tags": [
@ -6146,6 +6262,12 @@
"schema": {
"$ref": "#/definitions/RuleGroupConfigResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
},
"tags": [
@ -6187,6 +6309,12 @@
"$ref": "#/definitions/NamespaceConfigResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
@ -6225,6 +6353,12 @@
"$ref": "#/definitions/Ack"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
@ -6264,6 +6398,12 @@
"$ref": "#/definitions/NamespaceConfigResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
@ -6311,6 +6451,12 @@
"$ref": "#/definitions/Ack"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
@ -6355,6 +6501,12 @@
"$ref": "#/definitions/Ack"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
@ -6400,6 +6552,12 @@
"$ref": "#/definitions/RuleGroupConfigResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {

View File

@ -1252,6 +1252,12 @@
"$ref": "#/definitions/AlertingFileExport"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": " Not found."
}
@ -1287,6 +1293,12 @@
"schema": {
"$ref": "#/definitions/NamespaceConfigResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
}
@ -1315,6 +1327,12 @@
"schema": {
"$ref": "#/definitions/NamespaceConfigResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
},
@ -1349,6 +1367,12 @@
"schema": {
"$ref": "#/definitions/UpdateRuleGroupResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
},
@ -1372,6 +1396,12 @@
"schema": {
"$ref": "#/definitions/Ack"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
}
@ -1423,6 +1453,12 @@
"$ref": "#/definitions/AlertingFileExport"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": " Not found."
}
@ -1459,6 +1495,12 @@
"schema": {
"$ref": "#/definitions/RuleGroupConfigResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
},
@ -1488,6 +1530,12 @@
"schema": {
"$ref": "#/definitions/Ack"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
}
}
}
@ -1529,6 +1577,12 @@
"$ref": "#/definitions/NamespaceConfigResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
@ -1570,6 +1624,12 @@
"$ref": "#/definitions/NamespaceConfigResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
@ -1617,6 +1677,12 @@
"$ref": "#/definitions/Ack"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
@ -1653,6 +1719,12 @@
"$ref": "#/definitions/Ack"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
@ -1700,6 +1772,12 @@
"$ref": "#/definitions/RuleGroupConfigResponse"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
@ -1742,6 +1820,12 @@
"$ref": "#/definitions/Ack"
}
},
"403": {
"description": "ForbiddenError",
"schema": {
"$ref": "#/definitions/ForbiddenError"
}
},
"404": {
"description": "NotFound",
"schema": {
@ -3828,6 +3912,9 @@
"EvalQueriesPayload": {
"type": "object",
"properties": {
"condition": {
"type": "string"
},
"data": {
"type": "array",
"items": {
@ -4036,6 +4123,14 @@
}
}
},
"ForbiddenError": {
"type": "object",
"properties": {
"body": {
"$ref": "#/definitions/PublicError"
}
}
},
"Frame": {
"description": "Each Field is well typed by its FieldType and supports optional Labels.\n\nA Frame is a general data container for Grafana. A Frame can be table data\nor time series data depending on its content and field types.",
"type": "object",
@ -4938,12 +5033,58 @@
"type": "object",
"title": "NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.",
"properties": {
"Policy": {
"$ref": "#/definitions/RouteExport"
"continue": {
"type": "boolean"
},
"group_by": {
"type": "array",
"items": {
"type": "string"
}
},
"group_interval": {
"type": "string"
},
"group_wait": {
"type": "string"
},
"match": {
"description": "Deprecated. Remove before v1.0 release.",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"match_re": {
"$ref": "#/definitions/MatchRegexps"
},
"matchers": {
"$ref": "#/definitions/Matchers"
},
"mute_time_intervals": {
"type": "array",
"items": {
"type": "string"
}
},
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"orgId": {
"type": "integer",
"format": "int64"
},
"receiver": {
"type": "string"
},
"repeat_interval": {
"type": "string"
},
"routes": {
"type": "array",
"items": {
"$ref": "#/definitions/RouteExport"
}
}
}
},
@ -5716,6 +5857,26 @@
}
}
},
"PublicError": {
"description": "PublicError is derived from Error and only contains information\navailable to the end user.",
"type": "object",
"properties": {
"extra": {
"type": "object",
"additionalProperties": {}
},
"message": {
"type": "string"
},
"messageId": {
"type": "string"
},
"statusCode": {
"type": "integer",
"format": "int64"
}
}
},
"PushoverConfig": {
"type": "object",
"properties": {
@ -6869,9 +7030,8 @@
}
},
"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": "A URL represents a parsed URL (technically, a URI reference).",
"title": "URL is a custom URL type that allows validation at configuration load time.",
"properties": {
"ForceQuery": {
"type": "boolean"
@ -7111,7 +7271,6 @@
}
},
"alertGroup": {
"description": "AlertGroup alert group",
"type": "object",
"required": [
"alerts",
@ -7299,6 +7458,7 @@
"$ref": "#/definitions/gettableAlert"
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"type": "array",
"items": {
"$ref": "#/definitions/gettableAlert"
@ -7306,7 +7466,6 @@
"$ref": "#/definitions/gettableAlerts"
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"type": "object",
"required": [
"comment",
@ -7509,7 +7668,6 @@
}
},
"postableSilence": {
"description": "PostableSilence postable silence",
"type": "object",
"required": [
"comment",

View File

@ -1049,7 +1049,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
},
expectedCode: func() int {
if setting.IsEnterprise {
return http.StatusUnauthorized
return http.StatusForbidden
}
return http.StatusBadRequest
}(),
@ -2285,7 +2285,7 @@ func TestIntegrationEval(t *testing.T) {
expectedResponse: func() string { return "" },
expectedStatusCode: func() int {
if setting.IsEnterprise {
return http.StatusUnauthorized
return http.StatusForbidden
}
return http.StatusBadRequest
},

View File

@ -124,7 +124,7 @@ func TestBacktesting(t *testing.T) {
t.Run("fail if can't query data sources", func(t *testing.T) {
status, body := testUserApiCli.SubmitRuleForBacktesting(t, queryRequest)
require.Contains(t, body, "user is not authorized to access rule group")
require.Equalf(t, http.StatusUnauthorized, status, "Response: %s", body)
require.Equalf(t, http.StatusForbidden, status, "Response: %s", body)
})
})
}

View File

@ -285,7 +285,7 @@ func TestIntegrationAlertRulePermissions(t *testing.T) {
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
FolderUID: []string{"folder2"},
})
assert.Equal(t, http.StatusUnauthorized, status)
assert.Equal(t, http.StatusForbidden, status)
})
t.Run("Export from one group", func(t *testing.T) {

View File

@ -13855,6 +13855,9 @@
"EvalQueriesPayload": {
"type": "object",
"properties": {
"condition": {
"type": "string"
},
"data": {
"type": "array",
"items": {
@ -14198,6 +14201,14 @@
}
}
},
"ForbiddenError": {
"type": "object",
"properties": {
"body": {
"$ref": "#/definitions/PublicError"
}
}
},
"Frame": {
"description": "Each Field is well typed by its FieldType and supports optional Labels.\n\nA Frame is a general data container for Grafana. A Frame can be table data\nor time series data depending on its content and field types.",
"type": "object",
@ -15786,12 +15797,58 @@
"type": "object",
"title": "NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.",
"properties": {
"Policy": {
"$ref": "#/definitions/RouteExport"
"continue": {
"type": "boolean"
},
"group_by": {
"type": "array",
"items": {
"type": "string"
}
},
"group_interval": {
"type": "string"
},
"group_wait": {
"type": "string"
},
"match": {
"description": "Deprecated. Remove before v1.0 release.",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"match_re": {
"$ref": "#/definitions/MatchRegexps"
},
"matchers": {
"$ref": "#/definitions/Matchers"
},
"mute_time_intervals": {
"type": "array",
"items": {
"type": "string"
}
},
"object_matchers": {
"$ref": "#/definitions/ObjectMatchers"
},
"orgId": {
"type": "integer",
"format": "int64"
},
"receiver": {
"type": "string"
},
"repeat_interval": {
"type": "string"
},
"routes": {
"type": "array",
"items": {
"$ref": "#/definitions/RouteExport"
}
}
}
},
@ -17189,6 +17246,26 @@
}
}
},
"PublicError": {
"description": "PublicError is derived from Error and only contains information\navailable to the end user.",
"type": "object",
"properties": {
"extra": {
"type": "object",
"additionalProperties": false
},
"message": {
"type": "string"
},
"messageId": {
"type": "string"
},
"statusCode": {
"type": "integer",
"format": "int64"
}
}
},
"PublicKeyAlgorithm": {
"type": "integer",
"format": "int64"
@ -20459,7 +20536,6 @@
}
},
"alertGroup": {
"description": "AlertGroup alert group",
"type": "object",
"required": [
"alerts",
@ -20616,7 +20692,6 @@
}
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"type": "object",
"required": [
"labels",
@ -20679,6 +20754,7 @@
}
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"type": "object",
"required": [
"comment",
@ -20727,6 +20803,7 @@
}
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"type": "array",
"items": {
"$ref": "#/definitions/gettableSilence"
@ -20877,7 +20954,6 @@
}
},
"postableSilence": {
"description": "PostableSilence postable silence",
"type": "object",
"required": [
"comment",
@ -20943,6 +21019,7 @@
}
},
"receiver": {
"description": "Receiver receiver",
"type": "object",
"required": [
"active",

View File

@ -4872,6 +4872,9 @@
},
"EvalQueriesPayload": {
"properties": {
"condition": {
"type": "string"
},
"data": {
"items": {
"$ref": "#/components/schemas/AlertQuery"
@ -5216,6 +5219,14 @@
},
"type": "object"
},
"ForbiddenError": {
"properties": {
"body": {
"$ref": "#/components/schemas/PublicError"
}
},
"type": "object"
},
"Frame": {
"description": "Each Field is well typed by its FieldType and supports optional Labels.\n\nA Frame is a general data container for Grafana. A Frame can be table data\nor time series data depending on its content and field types.",
"properties": {
@ -6802,12 +6813,58 @@
},
"NotificationPolicyExport": {
"properties": {
"Policy": {
"$ref": "#/components/schemas/RouteExport"
"continue": {
"type": "boolean"
},
"group_by": {
"items": {
"type": "string"
},
"type": "array"
},
"group_interval": {
"type": "string"
},
"group_wait": {
"type": "string"
},
"match": {
"additionalProperties": {
"type": "string"
},
"description": "Deprecated. Remove before v1.0 release.",
"type": "object"
},
"match_re": {
"$ref": "#/components/schemas/MatchRegexps"
},
"matchers": {
"$ref": "#/components/schemas/Matchers"
},
"mute_time_intervals": {
"items": {
"type": "string"
},
"type": "array"
},
"object_matchers": {
"$ref": "#/components/schemas/ObjectMatchers"
},
"orgId": {
"format": "int64",
"type": "integer"
},
"receiver": {
"type": "string"
},
"repeat_interval": {
"type": "string"
},
"routes": {
"items": {
"$ref": "#/components/schemas/RouteExport"
},
"type": "array"
}
},
"title": "NotificationPolicyExport is the provisioned file export of alerting.NotificiationPolicyV1.",
@ -8206,6 +8263,26 @@
},
"type": "object"
},
"PublicError": {
"description": "PublicError is derived from Error and only contains information\navailable to the end user.",
"properties": {
"extra": {
"additionalProperties": false,
"type": "object"
},
"message": {
"type": "string"
},
"messageId": {
"type": "string"
},
"statusCode": {
"format": "int64",
"type": "integer"
}
},
"type": "object"
},
"PublicKeyAlgorithm": {
"format": "int64",
"type": "integer"
@ -11475,7 +11552,6 @@
"type": "object"
},
"alertGroup": {
"description": "AlertGroup alert group",
"properties": {
"alerts": {
"description": "alerts",
@ -11632,7 +11708,6 @@
"type": "object"
},
"gettableAlert": {
"description": "GettableAlert gettable alert",
"properties": {
"annotations": {
"$ref": "#/components/schemas/labelSet"
@ -11695,6 +11770,7 @@
"type": "array"
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"properties": {
"comment": {
"description": "comment",
@ -11743,6 +11819,7 @@
"type": "object"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"items": {
"$ref": "#/components/schemas/gettableSilence"
},
@ -11893,7 +11970,6 @@
"type": "array"
},
"postableSilence": {
"description": "PostableSilence postable silence",
"properties": {
"comment": {
"description": "comment",
@ -11959,6 +12035,7 @@
"type": "object"
},
"receiver": {
"description": "Receiver receiver",
"properties": {
"active": {
"description": "active",