Folders: Do not allow modifying the folder UID via the API (#74684)

* Folders: Do not allow changing the folder UID via the API

* Update Swagger/OpenAPI docs

* Update HTTP API docs
This commit is contained in:
Sofia Papagiannaki 2023-09-12 14:28:33 +03:00 committed by GitHub
parent 2fac3bd41e
commit 376f9a75db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 162 additions and 76 deletions

View File

@ -235,7 +235,6 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
JSON Body schema: JSON Body schema:
- **uid** Provide another [unique identifier](/http_api/folder/#identifier-id-vs-unique-identifier-uid) than stored to change the unique identifier. Starting with 10.0, this is **deprecated**. It will be removed in a future release. Please avoid using it because it can result in folder losing its permissions.
- **title** The title of the folder. - **title** The title of the folder.
- **version** Provide the current version to be able to update the folder. Not needed if `overwrite=true`. - **version** Provide the current version to be able to update the folder. Not needed if `overwrite=true`.
- **overwrite** Set to true if you want to overwrite existing folder with newer version. - **overwrite** Set to true if you want to overwrite existing folder with newer version.

View File

@ -373,18 +373,9 @@ func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (
return dashFolder, nil return dashFolder, nil
} }
if cmd.NewUID != nil && *cmd.NewUID != "" {
if !util.IsValidShortUID(*cmd.NewUID) {
return nil, dashboards.ErrDashboardInvalidUid
} else if util.IsShortUIDTooLong(*cmd.NewUID) {
return nil, dashboards.ErrDashboardUidTooLong
}
}
foldr, err := s.store.Update(ctx, folder.UpdateFolderCommand{ foldr, err := s.store.Update(ctx, folder.UpdateFolderCommand{
UID: cmd.UID, UID: cmd.UID,
OrgID: cmd.OrgID, OrgID: cmd.OrgID,
NewUID: cmd.NewUID,
NewTitle: cmd.NewTitle, NewTitle: cmd.NewTitle,
NewDescription: cmd.NewDescription, NewDescription: cmd.NewDescription,
SignedInUser: user, SignedInUser: user,
@ -471,10 +462,6 @@ func prepareForUpdate(dashFolder *dashboards.Dashboard, orgId int64, userId int6
dashFolder.Title = strings.TrimSpace(title) dashFolder.Title = strings.TrimSpace(title)
dashFolder.Data.Set("title", dashFolder.Title) dashFolder.Data.Set("title", dashFolder.Title)
if cmd.NewUID != nil && *cmd.NewUID != "" {
dashFolder.SetUID(*cmd.NewUID)
}
dashFolder.SetVersion(cmd.Version) dashFolder.SetVersion(cmd.Version)
dashFolder.IsFolder = true dashFolder.IsFolder = true

View File

@ -92,7 +92,7 @@ func (ss *sqlStore) Update(ctx context.Context, cmd folder.UpdateFolderCommand)
var foldr *folder.Folder var foldr *folder.Folder
if cmd.NewDescription == nil && cmd.NewTitle == nil && cmd.NewUID == nil && cmd.NewParentUID == nil { if cmd.NewDescription == nil && cmd.NewTitle == nil && cmd.NewParentUID == nil {
return nil, folder.ErrBadRequest.Errorf("nothing to update") return nil, folder.ErrBadRequest.Errorf("nothing to update")
} }
err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
@ -110,12 +110,6 @@ func (ss *sqlStore) Update(ctx context.Context, cmd folder.UpdateFolderCommand)
args = append(args, *cmd.NewTitle) args = append(args, *cmd.NewTitle)
} }
if cmd.NewUID != nil {
columnsToUpdate = append(columnsToUpdate, "uid = ?")
uid = *cmd.NewUID
args = append(args, *cmd.NewUID)
}
if cmd.NewParentUID != nil { if cmd.NewParentUID != nil {
if *cmd.NewParentUID == "" { if *cmd.NewParentUID == "" {
columnsToUpdate = append(columnsToUpdate, "parent_uid = NULL") columnsToUpdate = append(columnsToUpdate, "parent_uid = NULL")

View File

@ -286,30 +286,6 @@ func TestIntegrationUpdate(t *testing.T) {
f = updated f = updated
}) })
t.Run("updating folder UID should succeed", func(t *testing.T) {
newUID := "new"
existingTitle := f.Title
existingDesc := f.Description
updated, err := folderStore.Update(context.Background(), folder.UpdateFolderCommand{
UID: f.UID,
OrgID: f.OrgID,
NewUID: &newUID,
})
require.NoError(t, err)
assert.Equal(t, newUID, updated.UID)
updated, err = folderStore.Get(context.Background(), folder.GetFolderQuery{
UID: &updated.UID,
OrgID: orgID,
})
require.NoError(t, err)
assert.Equal(t, newUID, updated.UID)
assert.Equal(t, existingTitle, updated.Title)
assert.Equal(t, existingDesc, updated.Description)
assert.NotEmpty(t, updated.URL)
})
t.Run("updating folder parent UID", func(t *testing.T) { t.Run("updating folder parent UID", func(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string

View File

@ -90,10 +90,6 @@ type CreateFolderCommand struct {
type UpdateFolderCommand struct { type UpdateFolderCommand struct {
UID string `json:"-"` UID string `json:"-"`
OrgID int64 `json:"-"` OrgID int64 `json:"-"`
// NewUID it's an optional parameter used for overriding the existing folder UID
// Starting with 10.0, this is deprecated. It will be removed in a future release.
// Please avoid using it because it can result in folder loosing its permissions.
NewUID *string `json:"uid"` // keep same json tag with the legacy command for not breaking the existing APIs
// NewTitle it's an optional parameter used for overriding the existing folder title // NewTitle it's an optional parameter used for overriding the existing folder title
NewTitle *string `json:"title"` // keep same json tag with the legacy command for not breaking the existing APIs NewTitle *string `json:"title"` // keep same json tag with the legacy command for not breaking the existing APIs
// NewDescription it's an optional parameter used for overriding the existing folder description // NewDescription it's an optional parameter used for overriding the existing folder description

View File

@ -2541,6 +2541,18 @@
"description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.", "description": "Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence.",
"name": "format", "name": "format",
"in": "query" "in": "query"
},
{
"type": "string",
"description": "UID of folder from which export rules",
"name": "folderUid",
"in": "query"
},
{
"type": "string",
"description": "Name of group of rules to export. Must be specified only together with folder UID",
"name": "group",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -8684,7 +8696,7 @@
"saml", "saml",
"enterprise" "enterprise"
], ],
"summary": "It performs assertion Consumer Service (ACS).", "summary": "It performs Assertion Consumer Service (ACS).",
"operationId": "postACS", "operationId": "postACS",
"parameters": [ "parameters": [
{ {
@ -12178,6 +12190,12 @@
"type": "string", "type": "string",
"example": "My Label" "example": "My Label"
}, },
"orgId": {
"description": "OrgID of the data source the correlation originates from",
"type": "integer",
"format": "int64",
"example": 1
},
"sourceUID": { "sourceUID": {
"description": "UID of the data source the correlation originates from", "description": "UID of the data source the correlation originates from",
"type": "string", "type": "string",
@ -12845,9 +12863,6 @@
"provisionedExternalId": { "provisionedExternalId": {
"type": "string" "type": "string"
}, },
"publicDashboardAccessToken": {
"type": "string"
},
"publicDashboardEnabled": { "publicDashboardEnabled": {
"type": "boolean" "type": "boolean"
}, },
@ -14082,6 +14097,12 @@
"$ref": "#/definitions/SNSConfig" "$ref": "#/definitions/SNSConfig"
} }
}, },
"teams_configs": {
"type": "array",
"items": {
"$ref": "#/definitions/MSTeamsConfig"
}
},
"telegram_configs": { "telegram_configs": {
"type": "array", "type": "array",
"items": { "items": {
@ -15162,6 +15183,26 @@
} }
} }
}, },
"MSTeamsConfig": {
"type": "object",
"properties": {
"http_config": {
"$ref": "#/definitions/HTTPClientConfig"
},
"send_resolved": {
"type": "boolean"
},
"text": {
"type": "string"
},
"title": {
"type": "string"
},
"webhook_url": {
"$ref": "#/definitions/SecretURL"
}
}
},
"MassDeleteAnnotationsCmd": { "MassDeleteAnnotationsCmd": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -15239,9 +15280,6 @@
"type": "string", "type": "string",
"example": "now-1h" "example": "now-1h"
}, },
"publicDashboardAccessToken": {
"type": "string"
},
"queries": { "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.", "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.",
"type": "array", "type": "array",
@ -16160,6 +16198,12 @@
"$ref": "#/definitions/SNSConfig" "$ref": "#/definitions/SNSConfig"
} }
}, },
"teams_configs": {
"type": "array",
"items": {
"$ref": "#/definitions/MSTeamsConfig"
}
},
"telegram_configs": { "telegram_configs": {
"type": "array", "type": "array",
"items": { "items": {
@ -16522,7 +16566,10 @@
"example": "Always firing" "example": "Always firing"
}, },
"uid": { "uid": {
"type": "string" "type": "string",
"maxLength": 40,
"minLength": 1,
"pattern": "^[a-zA-Z0-9-_]+$"
}, },
"updated": { "updated": {
"type": "string", "type": "string",
@ -16869,6 +16916,12 @@
"$ref": "#/definitions/SNSConfig" "$ref": "#/definitions/SNSConfig"
} }
}, },
"teams_configs": {
"type": "array",
"items": {
"$ref": "#/definitions/MSTeamsConfig"
}
},
"telegram_configs": { "telegram_configs": {
"type": "array", "type": "array",
"items": { "items": {
@ -18126,10 +18179,18 @@
"type": "object", "type": "object",
"title": "TLSConfig configures the options for TLS connections.", "title": "TLSConfig configures the options for TLS connections.",
"properties": { "properties": {
"ca": {
"description": "Text of the CA cert to use for the targets.",
"type": "string"
},
"ca_file": { "ca_file": {
"description": "The CA cert to use for the targets.", "description": "The CA cert to use for the targets.",
"type": "string" "type": "string"
}, },
"cert": {
"description": "Text of the client cert file for the targets.",
"type": "string"
},
"cert_file": { "cert_file": {
"description": "The client cert file for the targets.", "description": "The client cert file for the targets.",
"type": "string" "type": "string"
@ -18138,6 +18199,9 @@
"description": "Disable target certificate validation.", "description": "Disable target certificate validation.",
"type": "boolean" "type": "boolean"
}, },
"key": {
"$ref": "#/definitions/Secret"
},
"key_file": { "key_file": {
"description": "The client key file for the targets.", "description": "The client key file for the targets.",
"type": "string" "type": "string"
@ -18824,8 +18888,9 @@
"type": "string" "type": "string"
}, },
"URL": { "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", "type": "object",
"title": "URL is a custom URL type that allows validation at configuration load time.", "title": "A URL represents a parsed URL (technically, a URI reference).",
"properties": { "properties": {
"ForceQuery": { "ForceQuery": {
"type": "boolean" "type": "boolean"
@ -19075,10 +19140,6 @@
"description": "NewTitle it's an optional parameter used for overriding the existing folder title", "description": "NewTitle it's an optional parameter used for overriding the existing folder title",
"type": "string" "type": "string"
}, },
"uid": {
"description": "NewUID it's an optional parameter used for overriding the existing folder UID\nStarting with 10.0, this is deprecated. It will be removed in a future release.\nPlease avoid using it because it can result in folder loosing its permissions.",
"type": "string"
},
"version": { "version": {
"description": "Version only used by the legacy folder implementation", "description": "Version only used by the legacy folder implementation",
"type": "integer", "type": "integer",
@ -19381,6 +19442,9 @@
"isGrafanaAdmin": { "isGrafanaAdmin": {
"type": "boolean" "type": "boolean"
}, },
"isGrafanaAdminExternallySynced": {
"type": "boolean"
},
"login": { "login": {
"type": "string" "type": "string"
}, },
@ -19797,7 +19861,6 @@
} }
}, },
"gettableAlert": { "gettableAlert": {
"description": "GettableAlert gettable alert",
"type": "object", "type": "object",
"required": [ "required": [
"labels", "labels",
@ -19915,6 +19978,7 @@
} }
}, },
"integration": { "integration": {
"description": "Integration integration",
"type": "object", "type": "object",
"required": [ "required": [
"name", "name",
@ -20058,6 +20122,7 @@
} }
}, },
"postableSilence": { "postableSilence": {
"description": "PostableSilence postable silence",
"type": "object", "type": "object",
"required": [ "required": [
"comment", "comment",

View File

@ -3191,6 +3191,12 @@
"example": "My Label", "example": "My Label",
"type": "string" "type": "string"
}, },
"orgId": {
"description": "OrgID of the data source the correlation originates from",
"example": 1,
"format": "int64",
"type": "integer"
},
"sourceUID": { "sourceUID": {
"description": "UID of the data source the correlation originates from", "description": "UID of the data source the correlation originates from",
"example": "d0oxYRg4z", "example": "d0oxYRg4z",
@ -3858,9 +3864,6 @@
"provisionedExternalId": { "provisionedExternalId": {
"type": "string" "type": "string"
}, },
"publicDashboardAccessToken": {
"type": "string"
},
"publicDashboardEnabled": { "publicDashboardEnabled": {
"type": "boolean" "type": "boolean"
}, },
@ -5095,6 +5098,12 @@
}, },
"type": "array" "type": "array"
}, },
"teams_configs": {
"items": {
"$ref": "#/components/schemas/MSTeamsConfig"
},
"type": "array"
},
"telegram_configs": { "telegram_configs": {
"items": { "items": {
"$ref": "#/components/schemas/TelegramConfig" "$ref": "#/components/schemas/TelegramConfig"
@ -6176,6 +6185,26 @@
}, },
"type": "object" "type": "object"
}, },
"MSTeamsConfig": {
"properties": {
"http_config": {
"$ref": "#/components/schemas/HTTPClientConfig"
},
"send_resolved": {
"type": "boolean"
},
"text": {
"type": "string"
},
"title": {
"type": "string"
},
"webhook_url": {
"$ref": "#/components/schemas/SecretURL"
}
},
"type": "object"
},
"MassDeleteAnnotationsCmd": { "MassDeleteAnnotationsCmd": {
"properties": { "properties": {
"annotationId": { "annotationId": {
@ -6247,9 +6276,6 @@
"example": "now-1h", "example": "now-1h",
"type": "string" "type": "string"
}, },
"publicDashboardAccessToken": {
"type": "string"
},
"queries": { "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.", "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": [ "example": [
@ -7172,6 +7198,12 @@
}, },
"type": "array" "type": "array"
}, },
"teams_configs": {
"items": {
"$ref": "#/components/schemas/MSTeamsConfig"
},
"type": "array"
},
"telegram_configs": { "telegram_configs": {
"items": { "items": {
"$ref": "#/components/schemas/TelegramConfig" "$ref": "#/components/schemas/TelegramConfig"
@ -7523,6 +7555,9 @@
"type": "string" "type": "string"
}, },
"uid": { "uid": {
"maxLength": 40,
"minLength": 1,
"pattern": "^[a-zA-Z0-9-_]+$",
"type": "string" "type": "string"
}, },
"updated": { "updated": {
@ -7880,6 +7915,12 @@
}, },
"type": "array" "type": "array"
}, },
"teams_configs": {
"items": {
"$ref": "#/components/schemas/MSTeamsConfig"
},
"type": "array"
},
"telegram_configs": { "telegram_configs": {
"items": { "items": {
"$ref": "#/components/schemas/TelegramConfig" "$ref": "#/components/schemas/TelegramConfig"
@ -9136,10 +9177,18 @@
}, },
"TLSConfig": { "TLSConfig": {
"properties": { "properties": {
"ca": {
"description": "Text of the CA cert to use for the targets.",
"type": "string"
},
"ca_file": { "ca_file": {
"description": "The CA cert to use for the targets.", "description": "The CA cert to use for the targets.",
"type": "string" "type": "string"
}, },
"cert": {
"description": "Text of the client cert file for the targets.",
"type": "string"
},
"cert_file": { "cert_file": {
"description": "The client cert file for the targets.", "description": "The client cert file for the targets.",
"type": "string" "type": "string"
@ -9148,6 +9197,9 @@
"description": "Disable target certificate validation.", "description": "Disable target certificate validation.",
"type": "boolean" "type": "boolean"
}, },
"key": {
"$ref": "#/components/schemas/Secret"
},
"key_file": { "key_file": {
"description": "The client key file for the targets.", "description": "The client key file for the targets.",
"type": "string" "type": "string"
@ -9836,6 +9888,7 @@
"type": "string" "type": "string"
}, },
"URL": { "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": { "properties": {
"ForceQuery": { "ForceQuery": {
"type": "boolean" "type": "boolean"
@ -9871,7 +9924,7 @@
"$ref": "#/components/schemas/Userinfo" "$ref": "#/components/schemas/Userinfo"
} }
}, },
"title": "URL is a custom URL type that allows validation at configuration load time.", "title": "A URL represents a parsed URL (technically, a URI reference).",
"type": "object" "type": "object"
}, },
"UpdateAlertNotificationCommand": { "UpdateAlertNotificationCommand": {
@ -10086,10 +10139,6 @@
"description": "NewTitle it's an optional parameter used for overriding the existing folder title", "description": "NewTitle it's an optional parameter used for overriding the existing folder title",
"type": "string" "type": "string"
}, },
"uid": {
"description": "NewUID it's an optional parameter used for overriding the existing folder UID\nStarting with 10.0, this is deprecated. It will be removed in a future release.\nPlease avoid using it because it can result in folder loosing its permissions.",
"type": "string"
},
"version": { "version": {
"description": "Version only used by the legacy folder implementation", "description": "Version only used by the legacy folder implementation",
"format": "int64", "format": "int64",
@ -10392,6 +10441,9 @@
"isGrafanaAdmin": { "isGrafanaAdmin": {
"type": "boolean" "type": "boolean"
}, },
"isGrafanaAdminExternallySynced": {
"type": "boolean"
},
"login": { "login": {
"type": "string" "type": "string"
}, },
@ -10809,7 +10861,6 @@
"type": "object" "type": "object"
}, },
"gettableAlert": { "gettableAlert": {
"description": "GettableAlert gettable alert",
"properties": { "properties": {
"annotations": { "annotations": {
"$ref": "#/components/schemas/labelSet" "$ref": "#/components/schemas/labelSet"
@ -10927,6 +10978,7 @@
"type": "array" "type": "array"
}, },
"integration": { "integration": {
"description": "Integration integration",
"properties": { "properties": {
"lastNotifyAttempt": { "lastNotifyAttempt": {
"description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time", "description": "A timestamp indicating the last attempt to deliver a notification regardless of the outcome.\nFormat: date-time",
@ -11070,6 +11122,7 @@
"type": "array" "type": "array"
}, },
"postableSilence": { "postableSilence": {
"description": "PostableSilence postable silence",
"properties": { "properties": {
"comment": { "comment": {
"description": "comment", "description": "comment",
@ -13958,6 +14011,22 @@
"default": "yaml", "default": "yaml",
"type": "string" "type": "string"
} }
},
{
"description": "UID of folder from which export rules",
"in": "query",
"name": "folderUid",
"schema": {
"type": "string"
}
},
{
"description": "Name of group of rules to export. Must be specified only together with folder UID",
"in": "query",
"name": "group",
"schema": {
"type": "string"
}
} }
], ],
"responses": { "responses": {
@ -20689,7 +20758,7 @@
"$ref": "#/components/responses/internalServerError" "$ref": "#/components/responses/internalServerError"
} }
}, },
"summary": "It performs assertion Consumer Service (ACS).", "summary": "It performs Assertion Consumer Service (ACS).",
"tags": [ "tags": [
"saml", "saml",
"enterprise" "enterprise"