1package features 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "strings" 8 9 "github.com/grafana/grafana-plugin-sdk-go/backend" 10 11 "github.com/grafana/grafana/pkg/bus" 12 "github.com/grafana/grafana/pkg/models" 13 "github.com/grafana/grafana/pkg/services/guardian" 14) 15 16type actionType string 17 18const ( 19 ActionSaved actionType = "saved" 20 ActionDeleted actionType = "deleted" 21 EditingStarted actionType = "editing-started" 22 //EditingFinished actionType = "editing-finished" 23 24 GitopsChannel = "grafana/dashboard/gitops" 25) 26 27// DashboardEvent events related to dashboards 28type dashboardEvent struct { 29 UID string `json:"uid"` 30 Action actionType `json:"action"` // saved, editing, deleted 31 User *models.UserDisplayDTO `json:"user,omitempty"` 32 SessionID string `json:"sessionId,omitempty"` 33 Message string `json:"message,omitempty"` 34 Dashboard *models.Dashboard `json:"dashboard,omitempty"` 35 Error string `json:"error,omitempty"` 36} 37 38// DashboardHandler manages all the `grafana/dashboard/*` channels 39type DashboardHandler struct { 40 Publisher models.ChannelPublisher 41 ClientCount models.ChannelClientCount 42} 43 44// GetHandlerForPath called on init 45func (h *DashboardHandler) GetHandlerForPath(_ string) (models.ChannelHandler, error) { 46 return h, nil // all dashboards share the same handler 47} 48 49// OnSubscribe for now allows anyone to subscribe to any dashboard 50func (h *DashboardHandler) OnSubscribe(ctx context.Context, user *models.SignedInUser, e models.SubscribeEvent) (models.SubscribeReply, backend.SubscribeStreamStatus, error) { 51 parts := strings.Split(e.Path, "/") 52 if parts[0] == "gitops" { 53 // gitops gets all changes for everything, so lets make sure it is an admin user 54 if !user.HasRole(models.ROLE_ADMIN) { 55 return models.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, nil 56 } 57 return models.SubscribeReply{ 58 Presence: true, 59 }, backend.SubscribeStreamStatusOK, nil 60 } 61 62 // make sure can view this dashboard 63 if len(parts) == 2 && parts[0] == "uid" { 64 query := models.GetDashboardQuery{Uid: parts[1], OrgId: user.OrgId} 65 if err := bus.DispatchCtx(ctx, &query); err != nil { 66 logger.Error("Error getting dashboard", "query", query, "error", err) 67 return models.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil 68 } 69 70 dash := query.Result 71 guard := guardian.New(ctx, dash.Id, user.OrgId, user) 72 if canView, err := guard.CanView(); err != nil || !canView { 73 return models.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, nil 74 } 75 76 return models.SubscribeReply{ 77 Presence: true, 78 JoinLeave: true, 79 }, backend.SubscribeStreamStatusOK, nil 80 } 81 82 // Unknown path 83 logger.Error("Unknown dashboard channel", "path", e.Path) 84 return models.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil 85} 86 87// OnPublish is called when someone begins to edit a dashboard 88func (h *DashboardHandler) OnPublish(ctx context.Context, user *models.SignedInUser, e models.PublishEvent) (models.PublishReply, backend.PublishStreamStatus, error) { 89 parts := strings.Split(e.Path, "/") 90 if parts[0] == "gitops" { 91 // gitops gets all changes for everything, so lets make sure it is an admin user 92 if !user.HasRole(models.ROLE_ADMIN) { 93 return models.PublishReply{}, backend.PublishStreamStatusPermissionDenied, nil 94 } 95 96 // Eventually this could broadcast a message back to the dashboard saying a pull request exists 97 return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("not implemented yet") 98 } 99 100 // make sure can view this dashboard 101 if len(parts) == 2 && parts[0] == "uid" { 102 event := dashboardEvent{} 103 err := json.Unmarshal(e.Data, &event) 104 if err != nil || event.UID != parts[1] { 105 return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("bad request") 106 } 107 if event.Action != EditingStarted { 108 // just ignore the event 109 return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("ignore???") 110 } 111 query := models.GetDashboardQuery{Uid: parts[1], OrgId: user.OrgId} 112 if err := bus.DispatchCtx(ctx, &query); err != nil { 113 logger.Error("Unknown dashboard", "query", query) 114 return models.PublishReply{}, backend.PublishStreamStatusNotFound, nil 115 } 116 117 guard := guardian.New(ctx, query.Result.Id, user.OrgId, user) 118 canEdit, err := guard.CanEdit() 119 if err != nil { 120 return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error") 121 } 122 123 // Ignore edit events if the user can not edit 124 if !canEdit { 125 return models.PublishReply{}, backend.PublishStreamStatusNotFound, nil // NOOP 126 } 127 128 // Tell everyone who is editing 129 event.User = user.ToUserDisplayDTO() 130 131 msg, err := json.Marshal(event) 132 if err != nil { 133 return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error") 134 } 135 return models.PublishReply{Data: msg}, backend.PublishStreamStatusOK, nil 136 } 137 138 return models.PublishReply{}, backend.PublishStreamStatusNotFound, nil 139} 140 141// DashboardSaved should broadcast to the appropriate stream 142func (h *DashboardHandler) publish(orgID int64, event dashboardEvent) error { 143 msg, err := json.Marshal(event) 144 if err != nil { 145 return err 146 } 147 148 // Only broadcast non-error events 149 if event.Error == "" { 150 err = h.Publisher(orgID, "grafana/dashboard/uid/"+event.UID, msg) 151 if err != nil { 152 return err 153 } 154 } 155 156 // Send everything to the gitops channel 157 return h.Publisher(orgID, GitopsChannel, msg) 158} 159 160// DashboardSaved will broadcast to all connected dashboards 161func (h *DashboardHandler) DashboardSaved(orgID int64, user *models.UserDisplayDTO, message string, dashboard *models.Dashboard, err error) error { 162 if err != nil && !h.HasGitOpsObserver(orgID) { 163 return nil // only broadcast if it was OK 164 } 165 166 msg := dashboardEvent{ 167 UID: dashboard.Uid, 168 Action: ActionSaved, 169 User: user, 170 Message: message, 171 Dashboard: dashboard, 172 } 173 174 if err != nil { 175 msg.Error = err.Error() 176 } 177 178 return h.publish(orgID, msg) 179} 180 181// DashboardDeleted will broadcast to all connected dashboards 182func (h *DashboardHandler) DashboardDeleted(orgID int64, user *models.UserDisplayDTO, uid string) error { 183 return h.publish(orgID, dashboardEvent{ 184 UID: uid, 185 Action: ActionDeleted, 186 User: user, 187 }) 188} 189 190// HasGitOpsObserver will return true if anyone is listening to the `gitops` channel 191func (h *DashboardHandler) HasGitOpsObserver(orgID int64) bool { 192 count, err := h.ClientCount(orgID, GitopsChannel) 193 if err != nil { 194 logger.Error("error getting client count", "error", err) 195 return false 196 } 197 return count > 0 198} 199