The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/live/features/dashboard.go

207 lines
6.9 KiB

package features
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/live/model"
)
type actionType string
const (
ActionSaved actionType = "saved"
ActionDeleted actionType = "deleted"
EditingStarted actionType = "editing-started"
//EditingFinished actionType = "editing-finished"
GitopsChannel = "grafana/dashboard/gitops"
)
type userDisplayDTO struct {
ID int64 `json:"id,omitempty"`
UID string `json:"uid,omitempty"`
Name string `json:"name,omitempty"`
Login string `json:"login,omitempty"`
AvatarURL string `json:"avatarUrl"`
}
// Static function to parse a requester into a userDisplayDTO
func newUserDisplayDTOFromRequester(requester identity.Requester) *userDisplayDTO {
// nolint:staticcheck
userID, _ := requester.GetInternalID()
return &userDisplayDTO{
ID: userID,
UID: requester.GetRawIdentifier(),
Login: requester.GetLogin(),
Name: requester.GetName(),
}
}
// DashboardEvent events related to dashboards
type dashboardEvent struct {
UID string `json:"uid"`
Action actionType `json:"action"` // saved, editing, deleted
User *userDisplayDTO `json:"user,omitempty"`
SessionID string `json:"sessionId,omitempty"`
Message string `json:"message,omitempty"`
Dashboard *dashboards.Dashboard `json:"dashboard,omitempty"`
Error string `json:"error,omitempty"`
}
// DashboardHandler manages all the `grafana/dashboard/*` channels
type DashboardHandler struct {
Publisher model.ChannelPublisher
ClientCount model.ChannelClientCount
Store db.DB
DashboardService dashboards.DashboardService
AccessControl accesscontrol.AccessControl
}
// GetHandlerForPath called on init
func (h *DashboardHandler) GetHandlerForPath(_ string) (model.ChannelHandler, error) {
return h, nil // all dashboards share the same handler
}
// OnSubscribe for now allows anyone to subscribe to any dashboard
func (h *DashboardHandler) OnSubscribe(ctx context.Context, user identity.Requester, e model.SubscribeEvent) (model.SubscribeReply, backend.SubscribeStreamStatus, error) {
parts := strings.Split(e.Path, "/")
// make sure can view this dashboard
if len(parts) == 2 && parts[0] == "uid" {
query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: user.GetOrgID()}
_, err := h.DashboardService.GetDashboard(ctx, &query)
if err != nil {
logger.Error("Error getting dashboard", "query", query, "error", err)
return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil
}
evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(parts[1]))
canView, err := h.AccessControl.Evaluate(ctx, user, evaluator)
if err != nil || !canView {
return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, err
}
return model.SubscribeReply{
Presence: true,
JoinLeave: true,
}, backend.SubscribeStreamStatusOK, nil
}
// Unknown path
logger.Error("Unknown dashboard channel", "path", e.Path)
return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil
}
// OnPublish is called when someone begins to edit a dashboard
func (h *DashboardHandler) OnPublish(ctx context.Context, requester identity.Requester, e model.PublishEvent) (model.PublishReply, backend.PublishStreamStatus, error) {
parts := strings.Split(e.Path, "/")
// make sure can view this dashboard
if len(parts) == 2 && parts[0] == "uid" {
event := dashboardEvent{}
err := json.Unmarshal(e.Data, &event)
if err != nil || event.UID != parts[1] {
return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("bad request")
}
if event.Action != EditingStarted {
// just ignore the event
return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("ignore???")
}
query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: requester.GetOrgID()}
_, err = h.DashboardService.GetDashboard(ctx, &query)
if err != nil {
logger.Error("Unknown dashboard", "query", query)
return model.PublishReply{}, backend.PublishStreamStatusNotFound, nil
}
evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(parts[1]))
canEdit, err := h.AccessControl.Evaluate(ctx, requester, evaluator)
if err != nil {
return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error")
}
// Ignore edit events if the user can not edit
if !canEdit {
return model.PublishReply{}, backend.PublishStreamStatusNotFound, nil // NOOP
}
// Tell everyone who is editing
event.User = newUserDisplayDTOFromRequester(requester)
msg, err := json.Marshal(event)
if err != nil {
return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error")
}
return model.PublishReply{Data: msg}, backend.PublishStreamStatusOK, nil
}
return model.PublishReply{}, backend.PublishStreamStatusNotFound, nil
}
// DashboardSaved should broadcast to the appropriate stream
func (h *DashboardHandler) publish(orgID int64, event dashboardEvent) error {
msg, err := json.Marshal(event)
if err != nil {
return err
}
// Only broadcast non-error events
if event.Error == "" {
err = h.Publisher(orgID, "grafana/dashboard/uid/"+event.UID, msg)
if err != nil {
return err
}
}
// Send everything to the gitops channel
return h.Publisher(orgID, GitopsChannel, msg)
}
// DashboardSaved will broadcast to all connected dashboards
func (h *DashboardHandler) DashboardSaved(orgID int64, requester identity.Requester, message string, dashboard *dashboards.Dashboard, err error) error {
if err != nil && !h.HasGitOpsObserver(orgID) {
return nil // only broadcast if it was OK
}
msg := dashboardEvent{
UID: dashboard.UID,
Action: ActionSaved,
User: newUserDisplayDTOFromRequester(requester),
Message: message,
Dashboard: dashboard,
}
if err != nil {
msg.Error = err.Error()
}
return h.publish(orgID, msg)
}
// DashboardDeleted will broadcast to all connected dashboards
func (h *DashboardHandler) DashboardDeleted(orgID int64, requester identity.Requester, uid string) error {
return h.publish(orgID, dashboardEvent{
UID: uid,
Action: ActionDeleted,
User: newUserDisplayDTOFromRequester(requester),
})
}
// HasGitOpsObserver will return true if anyone is listening to the `gitops` channel
func (h *DashboardHandler) HasGitOpsObserver(orgID int64) bool {
count, err := h.ClientCount(orgID, GitopsChannel)
if err != nil {
logger.Error("Error getting client count", "error", err)
return false
}
return count > 0
}