From 2be7605794afc7377b1ab4e9ea31ce9f1858884e Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 7 Dec 2023 13:43:58 -0500 Subject: [PATCH] Alerting: Fix fine-grained rule access control to use 403 for authorization error (#79239) * use 403 for authorization error * update silences API * add ForbiddenError to rule API responses --- pkg/services/ngalert/accesscontrol/models.go | 2 +- pkg/services/ngalert/api/api_alertmanager.go | 6 +- .../ngalert/api/api_alertmanager_test.go | 4 +- pkg/services/ngalert/api/api_ruler.go | 4 +- .../ngalert/api/api_ruler_export_test.go | 12 +- pkg/services/ngalert/api/api_ruler_test.go | 12 +- pkg/services/ngalert/api/api_testing_test.go | 8 +- pkg/services/ngalert/api/tooling/api.json | 476 +++--------------- .../api/tooling/definitions/cortex-ruler.go | 14 + .../ngalert/api/tooling/definitions/shared.go | 9 + pkg/services/ngalert/api/tooling/post.json | 172 ++++++- pkg/services/ngalert/api/tooling/spec.json | 172 ++++++- .../api/alerting/api_alertmanager_test.go | 4 +- .../api/alerting/api_backtesting_test.go | 2 +- pkg/tests/api/alerting/api_ruler_test.go | 2 +- public/api-merged.json | 87 +++- public/openapi3.json | 87 +++- 17 files changed, 629 insertions(+), 444 deletions(-) diff --git a/pkg/services/ngalert/accesscontrol/models.go b/pkg/services/ngalert/accesscontrol/models.go index ba8b0e734e2..f3c93aaf2a7 100644 --- a/pkg/services/ngalert/accesscontrol/models.go +++ b/pkg/services/ngalert/accesscontrol/models.go @@ -8,7 +8,7 @@ import ( ) var ( - errAuthorizationGeneric = errutil.Unauthorized("alerting.unauthorized") + errAuthorizationGeneric = errutil.Forbidden("alerting.unauthorized") ) func NewAuthorizationErrorWithPermissions(action string, eval accesscontrol.Evaluator) error { diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index aea9d89382e..9856b1032f6 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -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) diff --git a/pkg/services/ngalert/api/api_alertmanager_test.go b/pkg/services/ngalert/api/api_alertmanager_test.go index 8ab0b168b4a..a2445dcd2fe 100644 --- a/pkg/services/ngalert/api/api_alertmanager_test.go +++ b/pkg/services/ngalert/api/api_alertmanager_test.go @@ -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", diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 2f3d772ef32..ffe8777d2a7 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -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 { diff --git a/pkg/services/ngalert/api/api_ruler_export_test.go b/pkg/services/ngalert/api/api_ruler_export_test.go index 32d254d2dd2..bce90fe1915 100644 --- a/pkg/services/ngalert/api/api_ruler_export_test.go +++ b/pkg/services/ngalert/api/api_ruler_export_test.go @@ -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", diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index 650a20cffe5..2b133ebb227 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -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) { diff --git a/pkg/services/ngalert/api/api_testing_test.go b/pkg/services/ngalert/api/api_testing_test.go index 4cfa5f45e8e..e1efb86b6d1 100644 --- a/pkg/services/ngalert/api/api_testing_test.go +++ b/pkg/services/ngalert/api/api_testing_test.go @@ -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) { diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 59452059108..8d1f829aeac 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -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", diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index 9378f191931..a43c94b2c7c 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -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 diff --git a/pkg/services/ngalert/api/tooling/definitions/shared.go b/pkg/services/ngalert/api/tooling/definitions/shared.go index 9c15a2b8efb..4d7a49b95c6 100644 --- a/pkg/services/ngalert/api/tooling/definitions/shared.go +++ b/pkg/services/ngalert/api/tooling/definitions/shared.go @@ -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"` +} diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index e9b096999a9..7819457c12f 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -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": { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 21a45ace895..8c22d4431e9 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -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", diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index 878362a118b..19af3abe006 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -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 }, diff --git a/pkg/tests/api/alerting/api_backtesting_test.go b/pkg/tests/api/alerting/api_backtesting_test.go index f62b70870ce..de83add0e0f 100644 --- a/pkg/tests/api/alerting/api_backtesting_test.go +++ b/pkg/tests/api/alerting/api_backtesting_test.go @@ -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) }) }) } diff --git a/pkg/tests/api/alerting/api_ruler_test.go b/pkg/tests/api/alerting/api_ruler_test.go index 77ce404e97c..e57eb219211 100644 --- a/pkg/tests/api/alerting/api_ruler_test.go +++ b/pkg/tests/api/alerting/api_ruler_test.go @@ -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) { diff --git a/public/api-merged.json b/public/api-merged.json index e16ec463ff7..c7aa973609b 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -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", diff --git a/public/openapi3.json b/public/openapi3.json index dd65a56f10e..86a0ec2a222 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -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",