mirror of
https://github.com/grafana/grafana.git
synced 2024-12-01 13:09:22 -06:00
379 lines
12 KiB
Go
379 lines
12 KiB
Go
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"
|
||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||
"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"
|
||
"github.com/grafana/grafana/pkg/util"
|
||
"github.com/grafana/grafana/pkg/util/errutil/errhttp"
|
||
"github.com/grafana/grafana/pkg/web"
|
||
)
|
||
|
||
// 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) {
|
||
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) {
|
||
cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{}
|
||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||
c.JsonApiErr(http.StatusBadRequest, "bad request data", err)
|
||
return
|
||
}
|
||
|
||
// Do not check permissions when the instance snapshot public mode is enabled
|
||
if !hs.Cfg.SnapshotPublicMode {
|
||
evaluator := ac.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(cmd.Dashboard.GetNestedString("uid")))
|
||
if canSave, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canSave {
|
||
c.JsonApiErr(http.StatusForbidden, "forbidden", err)
|
||
return
|
||
}
|
||
}
|
||
|
||
dashboardsnapshots.CreateDashboardSnapshot(c, dashboardsnapshot.SnapshotSharingOptions{
|
||
SnapshotsEnabled: hs.Cfg.SnapshotEnabled,
|
||
ExternalEnabled: hs.Cfg.ExternalEnabled,
|
||
ExternalSnapshotName: hs.Cfg.ExternalSnapshotName,
|
||
ExternalSnapshotURL: hs.Cfg.ExternalSnapshotUrl,
|
||
}, cmd, 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}
|
||
|
||
queryResult, err := hs.dashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query)
|
||
if err != nil {
|
||
return response.Err(err)
|
||
}
|
||
|
||
snapshot := queryResult
|
||
|
||
// expired snapshots should also be removed from db
|
||
if snapshot.Expires.Before(time.Now()) {
|
||
return response.Error(http.StatusNotFound, "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()
|
||
|
||
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(http.StatusNotFound, "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(http.StatusNotFound, "Snapshot not found", err)
|
||
}
|
||
return response.Error(http.StatusInternalServerError, "Failed to delete dashboard snapshot", err)
|
||
}
|
||
|
||
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 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.
|
||
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}
|
||
|
||
if err := hs.dashboardsnapshotsService.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil {
|
||
return response.Error(http.StatusInternalServerError, "Failed to delete dashboard snapshot", err)
|
||
}
|
||
|
||
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,
|
||
})
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
query := c.Query("query")
|
||
limit := c.QueryInt("limit")
|
||
|
||
if limit == 0 {
|
||
limit = 1000
|
||
}
|
||
|
||
searchQuery := dashboardsnapshots.GetDashboardSnapshotsQuery{
|
||
Name: query,
|
||
Limit: limit,
|
||
OrgID: c.SignedInUser.GetOrgID(),
|
||
SignedInUser: c.SignedInUser,
|
||
}
|
||
|
||
searchQueryResult, err := hs.dashboardsnapshotsService.SearchDashboardSnapshots(c.Req.Context(), &searchQuery)
|
||
if err != nil {
|
||
return response.Error(http.StatusInternalServerError, "Search failed", err)
|
||
}
|
||
|
||
dto := make([]*dashboardsnapshots.DashboardSnapshotDTO, len(searchQueryResult))
|
||
for i, snapshot := range searchQueryResult {
|
||
dto[i] = &dashboardsnapshots.DashboardSnapshotDTO{
|
||
ID: snapshot.ID,
|
||
Name: snapshot.Name,
|
||
Key: snapshot.Key,
|
||
OrgID: snapshot.OrgID,
|
||
UserID: snapshot.UserID,
|
||
External: snapshot.External,
|
||
ExternalURL: snapshot.ExternalURL,
|
||
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"`
|
||
}
|