2015-03-21 08:53:16 -04:00
package api
import (
2022-06-16 13:13:58 +02:00
"errors"
2024-02-02 14:19:45 -08:00
"fmt"
2018-12-10 16:36:32 -05:00
"net/http"
2015-03-25 09:04:38 +01:00
"time"
2015-03-21 10:56:26 -04:00
"github.com/grafana/grafana/pkg/api/dtos"
2021-01-15 14:43:20 +01:00
"github.com/grafana/grafana/pkg/api/response"
2024-02-01 22:40:11 -08:00
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
2024-02-02 14:19:45 -08:00
"github.com/grafana/grafana/pkg/infra/appcontext"
2019-02-23 23:35:26 +01:00
"github.com/grafana/grafana/pkg/infra/metrics"
2024-02-02 14:19:45 -08:00
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
2023-01-27 08:50:36 +01:00
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
2022-06-30 09:31:54 -04:00
"github.com/grafana/grafana/pkg/services/dashboards"
2022-06-17 09:09:01 -04:00
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
2024-02-02 14:19:45 -08:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2018-02-20 23:26:08 +01:00
"github.com/grafana/grafana/pkg/services/guardian"
2015-03-21 08:53:16 -04:00
"github.com/grafana/grafana/pkg/util"
2024-02-02 14:19:45 -08:00
"github.com/grafana/grafana/pkg/util/errutil/errhttp"
2021-10-11 14:30:59 +02:00
"github.com/grafana/grafana/pkg/web"
2015-03-21 08:53:16 -04:00
)
2024-02-02 14:19:45 -08:00
// r.Post("/api/snapshots/"
func ( hs * HTTPServer ) getCreatedSnapshotHandler ( ) web . Handler {
if hs . Features . IsEnabledGlobally ( featuremgmt . FlagKubernetesSnapshots ) {
namespaceMapper := request . GetNamespaceMapper ( hs . Cfg )
return func ( w http . ResponseWriter , r * http . Request ) {
user , err := appcontext . User ( r . Context ( ) )
if err != nil || user == nil {
errhttp . Write ( r . Context ( ) , fmt . Errorf ( "no user" ) , w )
return
}
r . URL . Path = "/apis/dashboardsnapshot.grafana.app/v0alpha1/namespaces/" +
namespaceMapper ( user . OrgID ) + "/dashboardsnapshots/create"
hs . clientConfigProvider . DirectlyServeHTTP ( w , r )
}
}
return hs . CreateDashboardSnapshot
}
2022-07-27 16:54:37 +03:00
// swagger:route GET /snapshot/shared-options snapshots getSharingOptions
//
// Get snapshot sharing settings.
//
// Responses:
// 200: getSharingOptionsResponse
// 401: unauthorisedError
2023-01-27 08:50:36 +01:00
func ( hs * HTTPServer ) GetSharingOptions ( c * contextmodel . ReqContext ) {
2022-04-15 14:01:58 +02:00
c . JSON ( http . StatusOK , util . DynMap {
2023-01-26 10:28:11 -03:00
"snapshotEnabled" : hs . Cfg . SnapshotEnabled ,
"externalSnapshotURL" : hs . Cfg . ExternalSnapshotUrl ,
"externalSnapshotName" : hs . Cfg . ExternalSnapshotName ,
"externalEnabled" : hs . Cfg . ExternalEnabled ,
2015-10-14 17:39:57 +03:00
} )
}
2022-07-27 16:54:37 +03:00
// swagger:route POST /snapshots snapshots createDashboardSnapshot
//
// When creating a snapshot using the API, you have to provide the full dashboard payload including the snapshot data. This endpoint is designed for the Grafana UI.
//
// Snapshot public mode should be enabled or authentication is required.
//
// Responses:
// 200: createDashboardSnapshotResponse
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
2024-02-01 22:40:11 -08:00
func ( hs * HTTPServer ) CreateDashboardSnapshot ( c * contextmodel . ReqContext ) {
dashboardsnapshots . CreateDashboardSnapshot ( c , dashboardsnapshot . SnapshotSharingOptions {
SnapshotsEnabled : hs . Cfg . SnapshotEnabled ,
ExternalEnabled : hs . Cfg . ExternalEnabled ,
ExternalSnapshotName : hs . Cfg . ExternalSnapshotName ,
ExternalSnapshotURL : hs . Cfg . ExternalSnapshotUrl ,
} , hs . dashboardsnapshotsService )
2015-03-25 09:04:38 +01:00
}
2018-02-20 23:26:08 +01:00
// GET /api/snapshots/:key
2022-07-27 16:54:37 +03:00
// swagger:route GET /snapshots/{key} snapshots getDashboardSnapshot
//
// Get Snapshot by Key.
//
// Responses:
// 200: getDashboardSnapshotResponse
2022-08-03 17:31:23 +03:00
// 400: badRequestError
2022-07-27 16:54:37 +03:00
// 404: notFoundError
// 500: internalServerError
2023-01-27 08:50:36 +01:00
func ( hs * HTTPServer ) GetDashboardSnapshot ( c * contextmodel . ReqContext ) response . Response {
2023-01-26 10:28:11 -03:00
if ! hs . Cfg . SnapshotEnabled {
c . JsonApiErr ( http . StatusForbidden , "Dashboard Snapshots are disabled" , nil )
return nil
}
2021-10-11 14:30:59 +02:00
key := web . Params ( c . Req ) [ ":key" ]
2021-10-05 19:38:09 +02:00
if len ( key ) == 0 {
2022-08-03 17:31:23 +03:00
return response . Error ( http . StatusBadRequest , "Empty snapshot key" , nil )
2021-10-05 19:38:09 +02:00
}
2022-06-17 09:09:01 -04:00
query := & dashboardsnapshots . GetDashboardSnapshotQuery { Key : key }
2015-03-21 08:53:16 -04:00
2023-01-25 15:09:44 +01:00
queryResult , err := hs . dashboardsnapshotsService . GetDashboardSnapshot ( c . Req . Context ( ) , query )
2015-03-21 08:53:16 -04:00
if err != nil {
2022-08-03 17:31:23 +03:00
return response . Err ( err )
2015-03-21 08:53:16 -04:00
}
2023-01-25 15:09:44 +01:00
snapshot := queryResult
2015-03-26 12:00:52 +01:00
// expired snapshots should also be removed from db
if snapshot . Expires . Before ( time . Now ( ) ) {
2024-02-28 01:39:51 +09:00
return response . Error ( http . StatusNotFound , "Dashboard snapshot not found" , err )
2020-10-13 10:19:42 +02:00
}
2015-05-04 08:36:44 +02:00
dto := dtos . DashboardFullWithMeta {
2021-09-01 13:05:15 +02:00
Dashboard : snapshot . Dashboard ,
2015-03-28 17:53:52 +01:00
Meta : dtos . DashboardMeta {
2023-01-18 13:52:41 +01:00
Type : dashboards . DashTypeSnapshot ,
2015-03-28 17:53:52 +01:00
IsSnapshot : true ,
Created : snapshot . Created ,
Expires : snapshot . Expires ,
} ,
2015-03-21 10:56:26 -04:00
}
2019-07-16 17:58:46 +03:00
metrics . MApiDashboardSnapshotGet . Inc ( )
2015-03-24 16:49:12 +01:00
2022-04-15 14:01:58 +02:00
return response . JSON ( http . StatusOK , dto ) . SetHeader ( "Cache-Control" , "public, max-age=3600" )
2015-03-26 20:34:58 +01:00
}
2015-03-26 12:00:52 +01:00
2022-07-27 16:54:37 +03:00
// swagger:route GET /snapshots-delete/{deleteKey} snapshots deleteDashboardSnapshotByDeleteKey
//
// Delete Snapshot by deleteKey.
//
// Snapshot public mode should be enabled or authentication is required.
//
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
2023-01-27 08:50:36 +01:00
func ( hs * HTTPServer ) DeleteDashboardSnapshotByDeleteKey ( c * contextmodel . ReqContext ) response . Response {
2023-01-26 10:28:11 -03:00
if ! hs . Cfg . SnapshotEnabled {
c . JsonApiErr ( http . StatusForbidden , "Dashboard Snapshots are disabled" , nil )
return nil
}
2021-10-11 14:30:59 +02:00
key := web . Params ( c . Req ) [ ":deleteKey" ]
2021-10-05 19:38:09 +02:00
if len ( key ) == 0 {
2024-02-28 01:39:51 +09:00
return response . Error ( http . StatusNotFound , "Snapshot not found" , nil )
2021-10-05 19:38:09 +02:00
}
2018-05-24 02:55:16 -04:00
2024-02-01 22:40:11 -08:00
err := dashboardsnapshots . DeleteWithKey ( c . Req . Context ( ) , key , hs . dashboardsnapshotsService )
2018-05-24 02:55:16 -04:00
if err != nil {
2024-02-01 22:40:11 -08:00
if errors . Is ( err , dashboardsnapshots . ErrBaseNotFound ) {
2024-02-28 01:39:51 +09:00
return response . Error ( http . StatusNotFound , "Snapshot not found" , err )
2018-12-10 16:40:26 -05:00
}
2024-02-28 01:39:51 +09:00
return response . Error ( http . StatusInternalServerError , "Failed to delete dashboard snapshot" , err )
2018-05-24 02:55:16 -04:00
}
2022-04-15 14:01:58 +02:00
return response . JSON ( http . StatusOK , util . DynMap {
2020-12-04 16:22:58 +01:00
"message" : "Snapshot deleted. It might take an hour before it's cleared from any CDN caches." ,
} )
2018-05-24 02:55:16 -04:00
}
2022-07-27 16:54:37 +03:00
// swagger:route DELETE /snapshots/{key} snapshots deleteDashboardSnapshot
//
// Delete Snapshot by Key.
//
// Responses:
// 200: okResponse
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
2023-01-27 08:50:36 +01:00
func ( hs * HTTPServer ) DeleteDashboardSnapshot ( c * contextmodel . ReqContext ) response . Response {
2023-01-26 10:28:11 -03:00
if ! hs . Cfg . SnapshotEnabled {
c . JsonApiErr ( http . StatusForbidden , "Dashboard Snapshots are disabled" , nil )
return nil
}
2021-10-11 14:30:59 +02:00
key := web . Params ( c . Req ) [ ":key" ]
2021-10-05 19:38:09 +02:00
if len ( key ) == 0 {
2022-08-30 17:19:52 +02:00
return response . Error ( http . StatusNotFound , "Snapshot not found" , nil )
2021-10-05 19:38:09 +02:00
}
2018-02-20 23:26:08 +01:00
2022-06-17 09:09:01 -04:00
query := & dashboardsnapshots . GetDashboardSnapshotQuery { Key : key }
2018-02-20 23:26:08 +01:00
2023-01-25 15:09:44 +01:00
queryResult , err := hs . dashboardsnapshotsService . GetDashboardSnapshot ( c . Req . Context ( ) , query )
2018-02-20 23:26:08 +01:00
if err != nil {
2022-08-03 17:31:23 +03:00
return response . Err ( err )
2018-02-20 23:26:08 +01:00
}
2023-01-25 15:09:44 +01:00
if queryResult == nil {
2022-08-30 17:19:52 +02:00
return response . Error ( http . StatusNotFound , "Failed to get dashboard snapshot" , nil )
2018-02-20 23:26:08 +01:00
}
2020-11-13 09:52:38 +01:00
2024-02-21 08:04:15 -05:00
if queryResult . OrgID != c . OrgID {
return response . Error ( http . StatusUnauthorized , "OrgID mismatch" , nil )
}
2024-02-01 22:40:11 -08:00
2023-01-25 15:09:44 +01:00
if queryResult . External {
2024-02-01 22:40:11 -08:00
err := dashboardsnapshots . DeleteExternalDashboardSnapshot ( queryResult . ExternalDeleteURL )
2018-12-10 16:40:26 -05:00
if err != nil {
2022-08-30 17:19:52 +02:00
return response . Error ( http . StatusInternalServerError , "Failed to delete external dashboard" , err )
2018-12-10 16:40:26 -05:00
}
2022-08-30 17:19:52 +02:00
}
2022-07-07 15:15:39 +02:00
2022-08-30 17:19:52 +02:00
// Dashboard can be empty (creation error or external snapshot). This means that the mustInt here returns a 0,
// which before RBAC would result in a dashboard which has no ACL. A dashboard without an ACL would fallback
// to the user’ s org role, which for editors and admins would essentially always be allowed here. With RBAC,
// all permissions must be explicit, so the lack of a rule for dashboard 0 means the guardian will reject.
2023-01-25 15:09:44 +01:00
dashboardID := queryResult . Dashboard . Get ( "id" ) . MustInt64 ( )
2022-08-30 17:19:52 +02:00
if dashboardID != 0 {
2023-10-06 11:34:36 +02:00
g , err := guardian . New ( c . Req . Context ( ) , dashboardID , c . SignedInUser . GetOrgID ( ) , c . SignedInUser )
2022-12-15 16:34:17 +02:00
if err != nil {
2023-03-08 10:12:02 +02:00
if ! errors . Is ( err , dashboards . ErrDashboardNotFound ) {
return response . Err ( err )
}
} else {
canEdit , err := g . CanEdit ( )
// check for permissions only if the dashboard is found
if err != nil && ! errors . Is ( err , dashboards . ErrDashboardNotFound ) {
return response . Error ( http . StatusInternalServerError , "Error while checking permissions for snapshot" , err )
}
2022-07-07 15:15:39 +02:00
2023-03-08 10:12:02 +02:00
if ! canEdit && queryResult . UserID != c . SignedInUser . UserID && ! errors . Is ( err , dashboards . ErrDashboardNotFound ) {
return response . Error ( http . StatusForbidden , "Access denied to this snapshot" , nil )
}
2022-07-07 15:15:39 +02:00
}
2018-12-10 16:40:26 -05:00
}
2023-01-25 15:09:44 +01:00
cmd := & dashboardsnapshots . DeleteDashboardSnapshotCommand { DeleteKey : queryResult . DeleteKey }
2015-03-24 16:49:12 +01:00
2022-06-14 13:41:29 -04:00
if err := hs . dashboardsnapshotsService . DeleteDashboardSnapshot ( c . Req . Context ( ) , cmd ) ; err != nil {
2022-08-30 17:19:52 +02:00
return response . Error ( http . StatusInternalServerError , "Failed to delete dashboard snapshot" , err )
2015-03-26 20:34:58 +01:00
}
2022-04-15 14:01:58 +02:00
return response . JSON ( http . StatusOK , util . DynMap {
2020-12-04 16:22:58 +01:00
"message" : "Snapshot deleted. It might take an hour before it's cleared from any CDN caches." ,
2023-01-25 15:09:44 +01:00
"id" : queryResult . ID ,
2020-12-04 16:22:58 +01:00
} )
2015-03-21 08:53:16 -04:00
}
2016-01-19 01:37:36 -08:00
2022-07-27 16:54:37 +03:00
// swagger:route GET /dashboard/snapshots snapshots searchDashboardSnapshots
//
// List snapshots.
//
// Responses:
// 200: searchDashboardSnapshotsResponse
// 500: internalServerError
2023-01-27 08:50:36 +01:00
func ( hs * HTTPServer ) SearchDashboardSnapshots ( c * contextmodel . ReqContext ) response . Response {
2023-01-26 10:28:11 -03:00
if ! hs . Cfg . SnapshotEnabled {
c . JsonApiErr ( http . StatusForbidden , "Dashboard Snapshots are disabled" , nil )
return nil
}
2016-01-19 05:05:24 -08:00
query := c . Query ( "query" )
limit := c . QueryInt ( "limit" )
2016-01-19 01:37:36 -08:00
2016-01-19 05:05:24 -08:00
if limit == 0 {
limit = 1000
}
2016-01-19 01:37:36 -08:00
2022-06-17 09:09:01 -04:00
searchQuery := dashboardsnapshots . GetDashboardSnapshotsQuery {
2018-02-20 23:26:08 +01:00
Name : query ,
Limit : limit ,
2023-10-06 11:34:36 +02:00
OrgID : c . SignedInUser . GetOrgID ( ) ,
2018-02-20 23:26:08 +01:00
SignedInUser : c . SignedInUser ,
2016-01-19 05:05:24 -08:00
}
2016-01-19 01:37:36 -08:00
2023-01-25 15:09:44 +01:00
searchQueryResult , err := hs . dashboardsnapshotsService . SearchDashboardSnapshots ( c . Req . Context ( ) , & searchQuery )
2016-01-19 05:05:24 -08:00
if err != nil {
2024-02-28 01:39:51 +09:00
return response . Error ( http . StatusInternalServerError , "Search failed" , err )
2016-01-19 05:05:24 -08:00
}
2016-01-19 01:37:36 -08:00
2023-01-26 10:28:11 -03:00
dto := make ( [ ] * dashboardsnapshots . DashboardSnapshotDTO , len ( searchQueryResult ) )
2023-01-25 15:09:44 +01:00
for i , snapshot := range searchQueryResult {
2023-01-26 10:28:11 -03:00
dto [ i ] = & dashboardsnapshots . DashboardSnapshotDTO {
2023-01-25 15:09:44 +01:00
ID : snapshot . ID ,
2016-01-19 23:23:44 -08:00
Name : snapshot . Name ,
Key : snapshot . Key ,
2023-01-25 15:09:44 +01:00
OrgID : snapshot . OrgID ,
UserID : snapshot . UserID ,
2016-01-19 23:23:44 -08:00
External : snapshot . External ,
2023-01-25 15:09:44 +01:00
ExternalURL : snapshot . ExternalURL ,
2016-01-19 23:23:44 -08:00
Expires : snapshot . Expires ,
Created : snapshot . Created ,
Updated : snapshot . Updated ,
}
}
2023-01-26 10:28:11 -03:00
return response . JSON ( http . StatusOK , dto )
2016-01-19 01:37:36 -08:00
}
2022-07-27 16:54:37 +03:00
// swagger:parameters createDashboardSnapshot
type CreateSnapshotParams struct {
// in:body
// required:true
Body dashboardsnapshots . CreateDashboardSnapshotCommand ` json:"body" `
}
// swagger:parameters searchDashboardSnapshots
type GetSnapshotsParams struct {
// Search Query
// in:query
Query string ` json:"query" `
// Limit the number of returned results
// in:query
// default:1000
Limit int64 ` json:"limit" `
}
// swagger:parameters getDashboardSnapshot
type GetDashboardSnapshotParams struct {
// in:path
Key string ` json:"key" `
}
// swagger:parameters deleteDashboardSnapshot
type DeleteDashboardSnapshotParams struct {
// in:path
Key string ` json:"key" `
}
// swagger:parameters deleteDashboardSnapshotByDeleteKey
type DeleteSnapshotByDeleteKeyParams struct {
// in:path
DeleteKey string ` json:"deleteKey" `
}
// swagger:response createDashboardSnapshotResponse
type CreateSnapshotResponse struct {
// in:body
Body struct {
// Unique key
Key string ` json:"key" `
// Unique key used to delete the snapshot. It is different from the key so that only the creator can delete the snapshot.
DeleteKey string ` json:"deleteKey" `
URL string ` json:"url" `
DeleteUrl string ` json:"deleteUrl" `
// Snapshot id
ID int64 ` json:"id" `
} ` json:"body" `
}
// swagger:response searchDashboardSnapshotsResponse
type SearchDashboardSnapshotsResponse struct {
// in:body
Body [ ] * dashboardsnapshots . DashboardSnapshotDTO ` json:"body" `
}
// swagger:response getDashboardSnapshotResponse
type GetDashboardSnapshotResponse DashboardResponse
// swagger:response getSharingOptionsResponse
type GetSharingOptionsResponse struct {
// in:body
Body struct {
ExternalSnapshotURL string ` json:"externalSnapshotURL" `
ExternalSnapshotName string ` json:"externalSnapshotName" `
ExternalEnabled bool ` json:"externalEnabled" `
} ` json:"body" `
}