grafana/pkg/api/dashboard_snapshot.go

363 lines
11 KiB
Go
Raw Normal View History

2015-03-21 07:53:16 -05:00
package api
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
2015-03-21 07:53:16 -05:00
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil/errhttp"
"github.com/grafana/grafana/pkg/web"
2015-03-21 07:53:16 -05: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
}
// swagger:route GET /snapshot/shared-options snapshots getSharingOptions
//
// Get snapshot sharing settings.
//
// Responses:
// 200: getSharingOptionsResponse
// 401: unauthorisedError
func (hs *HTTPServer) GetSharingOptions(c *contextmodel.ReqContext) {
2022-04-15 07:01:58 -05:00
c.JSON(http.StatusOK, util.DynMap{
"snapshotEnabled": hs.Cfg.SnapshotEnabled,
"externalSnapshotURL": hs.Cfg.ExternalSnapshotUrl,
"externalSnapshotName": hs.Cfg.ExternalSnapshotName,
"externalEnabled": hs.Cfg.ExternalEnabled,
})
}
// 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
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)
}
// GET /api/snapshots/:key
// swagger:route GET /snapshots/{key} snapshots getDashboardSnapshot
//
// Get Snapshot by Key.
//
// Responses:
// 200: getDashboardSnapshotResponse
// 400: badRequestError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) GetDashboardSnapshot(c *contextmodel.ReqContext) response.Response {
if !hs.Cfg.SnapshotEnabled {
c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil)
return nil
}
key := web.Params(c.Req)[":key"]
if len(key) == 0 {
return response.Error(http.StatusBadRequest, "Empty snapshot key", nil)
}
query := &dashboardsnapshots.GetDashboardSnapshotQuery{Key: key}
2015-03-21 07:53:16 -05:00
queryResult, err := hs.dashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
2015-03-21 07:53:16 -05:00
if err != nil {
return response.Err(err)
2015-03-21 07:53:16 -05:00
}
snapshot := queryResult
// expired snapshots should also be removed from db
if snapshot.Expires.Before(time.Now()) {
return response.Error(404, "Dashboard snapshot not found", err)
}
dto := dtos.DashboardFullWithMeta{
Dashboard: snapshot.Dashboard,
Meta: dtos.DashboardMeta{
Type: dashboards.DashTypeSnapshot,
IsSnapshot: true,
Created: snapshot.Created,
Expires: snapshot.Expires,
},
}
metrics.MApiDashboardSnapshotGet.Inc()
2015-03-24 10:49:12 -05:00
2022-04-15 07:01:58 -05:00
return response.JSON(http.StatusOK, dto).SetHeader("Cache-Control", "public, max-age=3600")
}
// 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
func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *contextmodel.ReqContext) response.Response {
if !hs.Cfg.SnapshotEnabled {
c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil)
return nil
}
key := web.Params(c.Req)[":deleteKey"]
if len(key) == 0 {
return response.Error(404, "Snapshot not found", nil)
}
err := dashboardsnapshots.DeleteWithKey(c.Req.Context(), key, hs.dashboardsnapshotsService)
if err != nil {
if errors.Is(err, dashboardsnapshots.ErrBaseNotFound) {
return response.Error(404, "Snapshot not found", err)
}
return response.Error(500, "Failed to delete dashboard snapshot", err)
}
2022-04-15 07:01:58 -05:00
return response.JSON(http.StatusOK, util.DynMap{
"message": "Snapshot deleted. It might take an hour before it's cleared from any CDN caches.",
})
}
// swagger:route DELETE /snapshots/{key} snapshots deleteDashboardSnapshot
//
// Delete Snapshot by Key.
//
// Responses:
// 200: okResponse
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) DeleteDashboardSnapshot(c *contextmodel.ReqContext) response.Response {
if !hs.Cfg.SnapshotEnabled {
c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil)
return nil
}
key := web.Params(c.Req)[":key"]
if len(key) == 0 {
return response.Error(http.StatusNotFound, "Snapshot not found", nil)
}
query := &dashboardsnapshots.GetDashboardSnapshotQuery{Key: key}
queryResult, err := hs.dashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
if err != nil {
return response.Err(err)
}
if queryResult == nil {
return response.Error(http.StatusNotFound, "Failed to get dashboard snapshot", nil)
}
if queryResult.OrgID != c.OrgID {
return response.Error(http.StatusUnauthorized, "OrgID mismatch", nil)
}
if queryResult.External {
err := dashboardsnapshots.DeleteExternalDashboardSnapshot(queryResult.ExternalDeleteURL)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to delete external dashboard", err)
}
}
// 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 users 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.
dashboardID := queryResult.Dashboard.Get("id").MustInt64()
if dashboardID != 0 {
g, err := guardian.New(c.Req.Context(), dashboardID, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
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)
}
if !canEdit && queryResult.UserID != c.SignedInUser.UserID && !errors.Is(err, dashboards.ErrDashboardNotFound) {
return response.Error(http.StatusForbidden, "Access denied to this snapshot", nil)
}
}
}
cmd := &dashboardsnapshots.DeleteDashboardSnapshotCommand{DeleteKey: queryResult.DeleteKey}
2015-03-24 10:49:12 -05:00
if err := hs.dashboardsnapshotsService.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil {
return response.Error(http.StatusInternalServerError, "Failed to delete dashboard snapshot", err)
}
2022-04-15 07:01:58 -05:00
return response.JSON(http.StatusOK, util.DynMap{
"message": "Snapshot deleted. It might take an hour before it's cleared from any CDN caches.",
"id": queryResult.ID,
})
2015-03-21 07:53:16 -05:00
}
// swagger:route GET /dashboard/snapshots snapshots searchDashboardSnapshots
//
// List snapshots.
//
// Responses:
// 200: searchDashboardSnapshotsResponse
// 500: internalServerError
func (hs *HTTPServer) SearchDashboardSnapshots(c *contextmodel.ReqContext) response.Response {
if !hs.Cfg.SnapshotEnabled {
c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil)
return nil
}
2016-01-19 07:05:24 -06:00
query := c.Query("query")
limit := c.QueryInt("limit")
2016-01-19 07:05:24 -06:00
if limit == 0 {
limit = 1000
}
searchQuery := dashboardsnapshots.GetDashboardSnapshotsQuery{
Name: query,
Limit: limit,
OrgID: c.SignedInUser.GetOrgID(),
SignedInUser: c.SignedInUser,
2016-01-19 07:05:24 -06:00
}
searchQueryResult, err := hs.dashboardsnapshotsService.SearchDashboardSnapshots(c.Req.Context(), &searchQuery)
2016-01-19 07:05:24 -06:00
if err != nil {
return response.Error(500, "Search failed", err)
2016-01-19 07:05:24 -06:00
}
dto := make([]*dashboardsnapshots.DashboardSnapshotDTO, len(searchQueryResult))
for i, snapshot := range searchQueryResult {
dto[i] = &dashboardsnapshots.DashboardSnapshotDTO{
ID: snapshot.ID,
2016-01-20 01:23:44 -06:00
Name: snapshot.Name,
Key: snapshot.Key,
OrgID: snapshot.OrgID,
UserID: snapshot.UserID,
2016-01-20 01:23:44 -06:00
External: snapshot.External,
ExternalURL: snapshot.ExternalURL,
2016-01-20 01:23:44 -06:00
Expires: snapshot.Expires,
Created: snapshot.Created,
Updated: snapshot.Updated,
}
}
return response.JSON(http.StatusOK, dto)
}
// 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"`
}