grafana/pkg/api/annotations.go
Kristin Laemmert 1df340ff28
backend/services: Move GetDashboard from sqlstore to dashboard service (#48971)
* rename folder to match package name
* backend/sqlstore: move GetDashboard into DashboardService

This is a stepping-stone commit which copies the GetDashboard function - which lets us remove the sqlstore from the interfaces in dashboards - without changing any other callers.
* checkpoint: moving GetDashboard calls into dashboard service
* finish refactoring api tests for dashboardService.GetDashboard
2022-05-17 14:52:22 -04:00

537 lines
16 KiB
Go

package api
import (
"context"
"errors"
"net/http"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response {
query := &annotations.ItemQuery{
From: c.QueryInt64("from"),
To: c.QueryInt64("to"),
OrgId: c.OrgId,
UserId: c.QueryInt64("userId"),
AlertId: c.QueryInt64("alertId"),
DashboardId: c.QueryInt64("dashboardId"),
PanelId: c.QueryInt64("panelId"),
Limit: c.QueryInt64("limit"),
Tags: c.QueryStrings("tags"),
Type: c.Query("type"),
MatchAny: c.QueryBool("matchAny"),
SignedInUser: c.SignedInUser,
}
repo := annotations.GetRepository()
items, err := repo.Find(c.Req.Context(), query)
if err != nil {
return response.Error(500, "Failed to get annotations", err)
}
// since there are several annotations per dashboard, we can cache dashboard uid
dashboardCache := make(map[int64]*string)
for _, item := range items {
if item.Email != "" {
item.AvatarUrl = dtos.GetGravatarUrl(item.Email)
}
if item.DashboardId != 0 {
if val, ok := dashboardCache[item.DashboardId]; ok {
item.DashboardUID = val
} else {
query := models.GetDashboardQuery{Id: item.DashboardId, OrgId: c.OrgId}
err := hs.dashboardService.GetDashboard(c.Req.Context(), &query)
if err == nil && query.Result != nil {
item.DashboardUID = &query.Result.Uid
dashboardCache[item.DashboardId] = &query.Result.Uid
}
}
}
}
return response.JSON(http.StatusOK, items)
}
type AnnotationError struct {
message string
}
func (e *AnnotationError) Error() string {
return e.message
}
func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response {
cmd := dtos.PostAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
// overwrite dashboardId when dashboardUID is not empty
if cmd.DashboardUID != "" {
query := models.GetDashboardQuery{OrgId: c.OrgId, Uid: cmd.DashboardUID}
err := hs.dashboardService.GetDashboard(c.Req.Context(), &query)
if err == nil {
cmd.DashboardId = query.Result.Id
}
}
if canSave, err := hs.canCreateAnnotation(c, cmd.DashboardId); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
repo := annotations.GetRepository()
if cmd.Text == "" {
err := &AnnotationError{"text field should not be empty"}
return response.Error(400, "Failed to save annotation", err)
}
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
Epoch: cmd.Time,
EpochEnd: cmd.TimeEnd,
Text: cmd.Text,
Data: cmd.Data,
Tags: cmd.Tags,
}
if err := repo.Save(&item); err != nil {
if errors.Is(err, annotations.ErrTimerangeMissing) {
return response.Error(400, "Failed to save annotation", err)
}
return response.Error(500, "Failed to save annotation", err)
}
startID := item.Id
return response.JSON(http.StatusOK, util.DynMap{
"message": "Annotation added",
"id": startID,
})
}
func formatGraphiteAnnotation(what string, data string) string {
text := what
if data != "" {
text = text + "\n" + data
}
return text
}
func (hs *HTTPServer) PostGraphiteAnnotation(c *models.ReqContext) response.Response {
cmd := dtos.PostGraphiteAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
repo := annotations.GetRepository()
if cmd.What == "" {
err := &AnnotationError{"what field should not be empty"}
return response.Error(400, "Failed to save Graphite annotation", err)
}
text := formatGraphiteAnnotation(cmd.What, cmd.Data)
// Support tags in prior to Graphite 0.10.0 format (string of tags separated by space)
var tagsArray []string
switch tags := cmd.Tags.(type) {
case string:
if tags != "" {
tagsArray = strings.Split(tags, " ")
} else {
tagsArray = []string{}
}
case []interface{}:
for _, t := range tags {
if tagStr, ok := t.(string); ok {
tagsArray = append(tagsArray, tagStr)
} else {
err := &AnnotationError{"tag should be a string"}
return response.Error(400, "Failed to save Graphite annotation", err)
}
}
default:
err := &AnnotationError{"unsupported tags format"}
return response.Error(400, "Failed to save Graphite annotation", err)
}
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
Epoch: cmd.When * 1000,
Text: text,
Tags: tagsArray,
}
if err := repo.Save(&item); err != nil {
return response.Error(500, "Failed to save Graphite annotation", err)
}
return response.JSON(http.StatusOK, util.DynMap{
"message": "Graphite annotation added",
"id": item.Id,
})
}
func (hs *HTTPServer) UpdateAnnotation(c *models.ReqContext) response.Response {
cmd := dtos.UpdateAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "annotationId is invalid", err)
}
repo := annotations.GetRepository()
annotation, resp := findAnnotationByID(c.Req.Context(), repo, annotationID, c.SignedInUser)
if resp != nil {
return resp
}
if canSave, err := hs.canSaveAnnotation(c, annotation); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
Id: annotationID,
Epoch: cmd.Time,
EpochEnd: cmd.TimeEnd,
Text: cmd.Text,
Tags: cmd.Tags,
}
if err := repo.Update(c.Req.Context(), &item); err != nil {
return response.Error(500, "Failed to update annotation", err)
}
return response.Success("Annotation updated")
}
func (hs *HTTPServer) PatchAnnotation(c *models.ReqContext) response.Response {
cmd := dtos.PatchAnnotationsCmd{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "annotationId is invalid", err)
}
repo := annotations.GetRepository()
annotation, resp := findAnnotationByID(c.Req.Context(), repo, annotationID, c.SignedInUser)
if resp != nil {
return resp
}
if canSave, err := hs.canSaveAnnotation(c, annotation); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
existing := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
Id: annotationID,
Epoch: annotation.Time,
EpochEnd: annotation.TimeEnd,
Text: annotation.Text,
Tags: annotation.Tags,
}
if cmd.Tags != nil {
existing.Tags = cmd.Tags
}
if cmd.Text != "" && cmd.Text != existing.Text {
existing.Text = cmd.Text
}
if cmd.Time > 0 && cmd.Time != existing.Epoch {
existing.Epoch = cmd.Time
}
if cmd.TimeEnd > 0 && cmd.TimeEnd != existing.EpochEnd {
existing.EpochEnd = cmd.TimeEnd
}
if err := repo.Update(c.Req.Context(), &existing); err != nil {
return response.Error(500, "Failed to update annotation", err)
}
return response.Success("Annotation patched")
}
func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Response {
cmd := dtos.MassDeleteAnnotationsCmd{}
err := web.Bind(c.Req, &cmd)
if err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
if cmd.DashboardUID != "" {
query := models.GetDashboardQuery{OrgId: c.OrgId, Uid: cmd.DashboardUID}
err := hs.dashboardService.GetDashboard(c.Req.Context(), &query)
if err == nil {
cmd.DashboardId = query.Result.Id
}
}
if (cmd.DashboardId != 0 && cmd.PanelId == 0) || (cmd.PanelId != 0 && cmd.DashboardId == 0) {
err := &AnnotationError{message: "DashboardId and PanelId are both required for mass delete"}
return response.Error(http.StatusBadRequest, "bad request data", err)
}
repo := annotations.GetRepository()
var deleteParams *annotations.DeleteParams
// validations only for RBAC. A user can mass delete all annotations in a (dashboard + panel) or a specific annotation
// if has access to that dashboard.
if !hs.AccessControl.IsDisabled() {
var dashboardId int64
if cmd.AnnotationId != 0 {
annotation, respErr := findAnnotationByID(c.Req.Context(), repo, cmd.AnnotationId, c.SignedInUser)
if respErr != nil {
return respErr
}
dashboardId = annotation.DashboardId
deleteParams = &annotations.DeleteParams{
OrgId: c.OrgId,
Id: cmd.AnnotationId,
}
} else {
dashboardId = cmd.DashboardId
deleteParams = &annotations.DeleteParams{
OrgId: c.OrgId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
}
}
canSave, err := hs.canMassDeleteAnnotations(c, dashboardId)
if err != nil || !canSave {
return dashboardGuardianResponse(err)
}
} else { // legacy permissions
deleteParams = &annotations.DeleteParams{
OrgId: c.OrgId,
Id: cmd.AnnotationId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
}
}
err = repo.Delete(c.Req.Context(), deleteParams)
if err != nil {
return response.Error(500, "Failed to delete annotations", err)
}
return response.Success("Annotations deleted")
}
func (hs *HTTPServer) GetAnnotationByID(c *models.ReqContext) response.Response {
annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "annotationId is invalid", err)
}
repo := annotations.GetRepository()
annotation, resp := findAnnotationByID(c.Req.Context(), repo, annotationID, c.SignedInUser)
if resp != nil {
return resp
}
if annotation.Email != "" {
annotation.AvatarUrl = dtos.GetGravatarUrl(annotation.Email)
}
return response.JSON(200, annotation)
}
func (hs *HTTPServer) DeleteAnnotationByID(c *models.ReqContext) response.Response {
annotationID, err := strconv.ParseInt(web.Params(c.Req)[":annotationId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "annotationId is invalid", err)
}
repo := annotations.GetRepository()
annotation, resp := findAnnotationByID(c.Req.Context(), repo, annotationID, c.SignedInUser)
if resp != nil {
return resp
}
if canSave, err := hs.canSaveAnnotation(c, annotation); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
err = repo.Delete(c.Req.Context(), &annotations.DeleteParams{
OrgId: c.OrgId,
Id: annotationID,
})
if err != nil {
return response.Error(500, "Failed to delete annotation", err)
}
return response.Success("Annotation deleted")
}
func (hs *HTTPServer) canSaveAnnotation(c *models.ReqContext, annotation *annotations.ItemDTO) (bool, error) {
if annotation.GetType() == annotations.Dashboard {
return canEditDashboard(c, annotation.DashboardId)
} else {
if hs.AccessControl.IsDisabled() {
return c.SignedInUser.HasRole(models.ROLE_EDITOR), nil
}
return true, nil
}
}
func canEditDashboard(c *models.ReqContext, dashboardID int64) (bool, error) {
guard := guardian.New(c.Req.Context(), dashboardID, c.OrgId, c.SignedInUser)
if canEdit, err := guard.CanEdit(); err != nil || !canEdit {
return false, err
}
return true, nil
}
func findAnnotationByID(ctx context.Context, repo annotations.Repository, annotationID int64, user *models.SignedInUser) (*annotations.ItemDTO, response.Response) {
query := &annotations.ItemQuery{
AnnotationId: annotationID,
OrgId: user.OrgId,
SignedInUser: user,
}
items, err := repo.Find(ctx, query)
if err != nil {
return nil, response.Error(500, "Failed to find annotation", err)
}
if len(items) == 0 {
return nil, response.Error(404, "Annotation not found", nil)
}
return items[0], nil
}
func (hs *HTTPServer) GetAnnotationTags(c *models.ReqContext) response.Response {
query := &annotations.TagsQuery{
OrgID: c.OrgId,
Tag: c.Query("tag"),
Limit: c.QueryInt64("limit"),
}
repo := annotations.GetRepository()
result, err := repo.FindTags(c.Req.Context(), query)
if err != nil {
return response.Error(500, "Failed to find annotation tags", err)
}
return response.JSON(http.StatusOK, annotations.GetAnnotationTagsResponse{Result: result})
}
// AnnotationTypeScopeResolver provides an ScopeAttributeResolver able to
// resolve annotation types. Scope "annotations:id:<id>" will be translated to "annotations:type:<type>,
// where <type> is the type of annotation with id <id>.
func AnnotationTypeScopeResolver() (string, accesscontrol.ScopeAttributeResolver) {
prefix := accesscontrol.ScopeAnnotationsProvider.GetResourceScope("")
return prefix, accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) {
scopeParts := strings.Split(initialScope, ":")
if scopeParts[0] != accesscontrol.ScopeAnnotationsRoot || len(scopeParts) != 3 {
return nil, accesscontrol.ErrInvalidScope
}
annotationIdStr := scopeParts[2]
annotationId, err := strconv.Atoi(annotationIdStr)
if err != nil {
return nil, accesscontrol.ErrInvalidScope
}
// tempUser is used to resolve annotation type.
// The annotation doesn't get returned to the real user, so real user's permissions don't matter here.
tempUser := &models.SignedInUser{
OrgId: orgID,
Permissions: map[int64]map[string][]string{
orgID: {
dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll},
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsAll},
},
},
}
annotation, resp := findAnnotationByID(ctx, annotations.GetRepository(), int64(annotationId), tempUser)
if resp != nil {
return nil, errors.New("could not resolve annotation type")
}
if annotation.GetType() == annotations.Organization {
return []string{accesscontrol.ScopeAnnotationsTypeOrganization}, nil
} else {
return []string{accesscontrol.ScopeAnnotationsTypeDashboard}, nil
}
})
}
func (hs *HTTPServer) canCreateAnnotation(c *models.ReqContext, dashboardId int64) (bool, error) {
if dashboardId != 0 {
if !hs.AccessControl.IsDisabled() {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeDashboard)
if canSave, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canSave {
return canSave, err
}
}
return canEditDashboard(c, dashboardId)
} else { // organization annotations
if !hs.AccessControl.IsDisabled() {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization)
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
} else {
return c.SignedInUser.HasRole(models.ROLE_EDITOR), nil
}
}
}
func (hs *HTTPServer) canMassDeleteAnnotations(c *models.ReqContext, dashboardID int64) (bool, error) {
if dashboardID == 0 {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeOrganization)
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
} else {
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsDelete, accesscontrol.ScopeAnnotationsTypeDashboard)
canSave, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
if err != nil || !canSave {
return false, err
}
canSave, err = canEditDashboard(c, dashboardID)
if err != nil || !canSave {
return false, err
}
}
return true, nil
}