Merge branch 'main' into plugin-dependency-install

This commit is contained in:
Will Browne
2025-03-24 10:43:01 +00:00
461 changed files with 12976 additions and 12169 deletions
+10 -19
View File
@@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@@ -278,7 +277,7 @@ func (hs *HTTPServer) UpdateAnnotation(c *contextmodel.ReqContext) response.Resp
}
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
if canSave, err := hs.canSaveAnnotation(c, annotation); err != nil || !canSave {
if canSave, err := hs.canSaveAnnotation(c, hs.AccessControl, annotation); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
}
@@ -336,7 +335,7 @@ func (hs *HTTPServer) PatchAnnotation(c *contextmodel.ReqContext) response.Respo
}
if !hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagAnnotationPermissionUpdate) {
if canSave, err := hs.canSaveAnnotation(c, annotation); err != nil || !canSave {
if canSave, err := hs.canSaveAnnotation(c, hs.AccessControl, annotation); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
}
@@ -502,7 +501,7 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *contextmodel.ReqContext) response.
return resp
}
if canSave, err := hs.canSaveAnnotation(c, annotation); err != nil || !canSave {
if canSave, err := hs.canSaveAnnotation(c, hs.AccessControl, annotation); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
}
@@ -518,25 +517,17 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *contextmodel.ReqContext) response.
return response.Success("Annotation deleted")
}
func (hs *HTTPServer) canSaveAnnotation(c *contextmodel.ReqContext, annotation *annotations.ItemDTO) (bool, error) {
func (hs *HTTPServer) canSaveAnnotation(c *contextmodel.ReqContext, ac accesscontrol.AccessControl, annotation *annotations.ItemDTO) (bool, error) {
if annotation.GetType() == annotations.Dashboard {
return canEditDashboard(c, annotation.DashboardID)
return canEditDashboard(c, ac, annotation.DashboardID)
} else {
return true, nil
}
}
func canEditDashboard(c *contextmodel.ReqContext, dashboardID int64) (bool, error) {
guard, err := guardian.New(c.Req.Context(), dashboardID, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return false, err
}
if canEdit, err := guard.CanEdit(); err != nil || !canEdit {
return false, err
}
return true, nil
func canEditDashboard(c *contextmodel.ReqContext, ac accesscontrol.AccessControl, dashboardID int64) (bool, error) {
evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(dashboardID, 10)))
return ac.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
}
func findAnnotationByID(ctx context.Context, repo annotations.Repository, annotationID int64, user *user.SignedInUser) (*annotations.ItemDTO, response.Response) {
@@ -680,7 +671,7 @@ func (hs *HTTPServer) canCreateAnnotation(c *contextmodel.ReqContext, dashboardI
return canSave, err
}
return canEditDashboard(c, dashboardId)
return canEditDashboard(c, hs.AccessControl, dashboardId)
} else { // organization annotations
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization)
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
@@ -708,7 +699,7 @@ func (hs *HTTPServer) canMassDeleteAnnotations(c *contextmodel.ReqContext, dashb
return false, err
}
canSave, err = canEditDashboard(c, dashboardID)
canSave, err = canEditDashboard(c, hs.AccessControl, dashboardID)
if err != nil || !canSave {
return false, err
}
+20 -19
View File
@@ -19,7 +19,6 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
)
@@ -110,7 +109,10 @@ func TestAPI_Annotations(t *testing.T) {
path: "/api/annotations/2",
method: http.MethodPut,
expectedCode: http.StatusOK,
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}},
permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard},
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll},
},
},
{
desc: "should not be able to update dashboard annotation without correct permission",
@@ -162,7 +164,10 @@ func TestAPI_Annotations(t *testing.T) {
path: "/api/annotations/2",
method: http.MethodPatch,
expectedCode: http.StatusOK,
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}},
permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard},
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll},
},
},
{
desc: "should not be able to patch dashboard annotation without correct permission",
@@ -215,7 +220,10 @@ func TestAPI_Annotations(t *testing.T) {
method: http.MethodPost,
body: "{\"dashboardId\": 2,\"text\": \"test\"}",
expectedCode: http.StatusOK,
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}},
permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard},
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll},
},
},
{
desc: "should not be able to create dashboard annotation without correct permission",
@@ -273,7 +281,10 @@ func TestAPI_Annotations(t *testing.T) {
path: "/api/annotations/2",
method: http.MethodDelete,
expectedCode: http.StatusOK,
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}},
permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard},
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll},
},
},
{
desc: "should not be able to delete dashboard annotation without correct permission",
@@ -341,7 +352,10 @@ func TestAPI_Annotations(t *testing.T) {
body: "{\"dashboardId\": 2, \"panelId\": 1}",
method: http.MethodPost,
expectedCode: http.StatusOK,
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}},
permissions: []accesscontrol.Permission{
{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard},
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll},
},
},
{
desc: "should not be able to mass delete dashboard annotations without correct permission",
@@ -382,10 +396,6 @@ func TestAPI_Annotations(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
// Don't need access to dashboards if annotationPermissionUpdate is enabled
if len(tt.featureFlags) == 0 {
setUpRBACGuardian(t)
}
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
repo := annotationstest.NewFakeAnnotationsRepo()
@@ -518,12 +528,3 @@ func TestService_AnnotationTypeScopeResolver(t *testing.T) {
})
}
}
func setUpRBACGuardian(t *testing.T) {
origNewGuardian := guardian.New
t.Cleanup(func() {
guardian.New = origNewGuardian
})
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanEditValue: true, CanViewValue: true})
}
+15 -11
View File
@@ -117,6 +117,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/admin/orgs/edit/:id", authorizeInOrg(ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index)
r.Get("/admin/stats", authorize(ac.EvalPermission(ac.ActionServerStatsRead)), hs.Index)
r.Get("/admin/provisioning", reqOrgAdmin, hs.Index)
r.Get("/admin/provisioning/*", reqOrgAdmin, hs.Index)
if hs.Features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) {
r.Get("/admin/migrate-to-cloud", authorize(cloudmigration.MigrationAssistantAccess), hs.Index)
@@ -462,22 +463,24 @@ func (hs *HTTPServer) registerRoutes() {
// Dashboard
apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) {
dashboardRoute.Get("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsRead)), routing.Wrap(hs.GetDashboard))
dashUIDScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(ac.Parameter(":uid"))
dashboardRoute.Get("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsRead, dashUIDScope)), routing.Wrap(hs.GetDashboard))
if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete)), routing.Wrap(hs.SoftDeleteDashboard))
dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete, dashUIDScope)), routing.Wrap(hs.SoftDeleteDashboard))
} else {
dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete)), routing.Wrap(hs.DeleteDashboardByUID))
dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete, dashUIDScope)), routing.Wrap(hs.DeleteDashboardByUID))
}
dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) {
dashUidRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersions))
dashUidRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.RestoreDashboardVersion))
dashUidRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersion))
dashUidRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.GetDashboardVersions))
dashUidRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.RestoreDashboardVersion))
dashUidRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.GetDashboardVersion))
if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
dashUidRoute.Patch("/trash", reqOrgAdmin, routing.Wrap(hs.RestoreDeletedDashboard))
dashUidRoute.Delete("/trash", reqOrgAdmin, routing.Wrap(hs.HardDeleteDashboardByUID))
dashUidRoute.Patch("/trash", reqOrgAdmin, authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.RestoreDeletedDashboard))
dashUidRoute.Delete("/trash", reqOrgAdmin, authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete, dashUIDScope)), routing.Wrap(hs.HardDeleteDashboardByUID))
}
dashUidRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
@@ -497,9 +500,10 @@ func (hs *HTTPServer) registerRoutes() {
// Deprecated: use /uid/:uid API instead.
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) {
dashIdRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersions))
dashIdRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersion))
dashIdRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.RestoreDashboardVersion))
dashIDScope := dashboards.ScopeDashboardsProvider.GetResourceScope(ac.Parameter(":dashboardId"))
dashIdRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashIDScope)), routing.Wrap(hs.GetDashboardVersions))
dashIdRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashIDScope)), routing.Wrap(hs.GetDashboardVersion))
dashIdRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashIDScope)), routing.Wrap(hs.RestoreDashboardVersion))
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
dashboardPermissionRoute.Get("/", authorize(ac.EvalPermission(dashboards.ActionDashboardsPermissionsRead)), routing.Wrap(hs.GetDashboardPermissionList))
+17 -77
View File
@@ -27,7 +27,6 @@ import (
"github.com/grafana/grafana/pkg/services/dashboardversion/dashverimpl"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/org"
pref "github.com/grafana/grafana/pkg/services/preference"
publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
@@ -141,18 +140,19 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
dash.Data.Set("id", dash.ID)
}
}
guardian, err := guardian.NewByDashboard(ctx, dash, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return response.Err(err)
}
if canView, err := guardian.CanView(); err != nil || !canView {
return dashboardGuardianResponse(err)
dashScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID)
writeEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashScope)
canSave, _ := hs.AccessControl.Evaluate(ctx, c.SignedInUser, writeEvaluator)
canEdit := canSave
//nolint:staticcheck // ViewersCanEdit is deprecated but still used for backward compatibility
if hs.Cfg.ViewersCanEdit {
canEdit = true
}
canEdit, _ := guardian.CanEdit()
canSave, _ := guardian.CanSave()
canAdmin, _ := guardian.CanAdmin()
canDelete, _ := guardian.CanDelete()
deleteEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsDelete, dashScope)
canDelete, _ := hs.AccessControl.Evaluate(ctx, c.SignedInUser, deleteEvaluator)
adminEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsWrite, dashScope)
canAdmin, _ := hs.AccessControl.Evaluate(ctx, c.SignedInUser, adminEvaluator)
isStarred, err := hs.isDashboardStarredByUser(c, dash.ID)
if err != nil {
@@ -369,15 +369,6 @@ func (hs *HTTPServer) RestoreDeletedDashboard(c *contextmodel.ReqContext) respon
return response.Error(http.StatusNotFound, "Dashboard not found", err)
}
guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return response.Err(err)
}
if canRestore, err := guardian.CanSave(); err != nil || !canRestore {
return dashboardGuardianResponse(err)
}
err = hs.DashboardService.RestoreDashboard(c.Req.Context(), dash, c.SignedInUser, cmd.FolderUID)
if err != nil {
var dashboardErr dashboards.DashboardErr
@@ -417,16 +408,7 @@ func (hs *HTTPServer) SoftDeleteDashboard(c *contextmodel.ReqContext) response.R
return rsp
}
guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return response.Err(err)
}
if canDelete, err := guardian.CanDelete(); err != nil || !canDelete {
return dashboardGuardianResponse(err)
}
err = hs.DashboardService.SoftDeleteDashboard(c.Req.Context(), c.SignedInUser.GetOrgID(), uid)
err := hs.DashboardService.SoftDeleteDashboard(c.Req.Context(), c.SignedInUser.GetOrgID(), uid)
if err != nil {
var dashboardErr dashboards.DashboardErr
if ok := errors.As(err, &dashboardErr); ok {
@@ -498,21 +480,12 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo
}
}
guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return response.Err(err)
}
if canDelete, err := guardian.CanDelete(); err != nil || !canDelete {
return dashboardGuardianResponse(err)
}
if dash.IsFolder {
return response.Error(http.StatusBadRequest, "Use folders endpoint for deleting folders.", nil)
}
// disconnect all library elements for this dashboard
err = hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.ID)
err := hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.ID)
if err != nil {
hs.log.Error(
"Failed to disconnect library elements",
@@ -840,14 +813,6 @@ func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response.
return rsp
}
guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return response.Err(err)
}
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
query := dashver.ListDashboardVersionsQuery{
OrgID: c.SignedInUser.GetOrgID(),
DashboardID: dash.ID,
@@ -959,15 +924,6 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R
return rsp
}
guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return response.Err(err)
}
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
version, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
if err != nil {
return response.Err(err)
@@ -1027,22 +983,15 @@ func (hs *HTTPServer) CalculateDashboardDiff(c *contextmodel.ReqContext) respons
if err := web.Bind(c.Req, &apiOptions); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
guardianBase, err := guardian.New(c.Req.Context(), apiOptions.Base.DashboardId, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return response.Err(err)
}
if canSave, err := guardianBase.CanSave(); err != nil || !canSave {
evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(apiOptions.Base.DashboardId, 10)))
if canWrite, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canWrite {
return dashboardGuardianResponse(err)
}
if apiOptions.Base.DashboardId != apiOptions.New.DashboardId {
guardianNew, err := guardian.New(c.Req.Context(), apiOptions.New.DashboardId, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return response.Err(err)
}
if canSave, err := guardianNew.CanSave(); err != nil || !canSave {
evaluator = accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(apiOptions.New.DashboardId, 10)))
if canWrite, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canWrite {
return dashboardGuardianResponse(err)
}
}
@@ -1159,15 +1108,6 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) respon
return rsp
}
guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return response.Err(err)
}
if canSave, err := guardian.CanSave(); err != nil || !canSave {
return dashboardGuardianResponse(err)
}
versionQuery := dashver.GetDashboardVersionQuery{DashboardID: dashID, DashboardUID: dash.UID, Version: apiCmd.Version, OrgID: c.SignedInUser.GetOrgID()}
version, err := hs.dashboardVersionService.Get(c.Req.Context(), &versionQuery)
if err != nil {
+9 -15
View File
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
@@ -17,7 +18,6 @@ import (
"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/errhttp"
"github.com/grafana/grafana/pkg/web"
@@ -226,21 +226,15 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *contextmodel.ReqContext) respon
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)
}
evaluator := ac.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(dashboardID, 10)))
canEdit, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
// 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)
}
if !canEdit && queryResult.UserID != c.SignedInUser.UserID && !errors.Is(err, dashboards.ErrDashboardNotFound) {
return response.Error(http.StatusForbidden, "Access denied to this snapshot", nil)
}
}
+3 -3
View File
@@ -15,13 +15,12 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"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/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@@ -41,7 +40,7 @@ func TestHTTPServer_DeleteDashboardSnapshot(t *testing.T) {
hs.DashboardService = svc
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger())
hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(svc, nil))
})
}
@@ -378,6 +377,7 @@ func buildHttpServer(d dashboardsnapshots.Service, snapshotEnabled bool) *HTTPSe
Cfg: &setting.Cfg{
SnapshotEnabled: snapshotEnabled,
},
AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
}
return hs
}
+53 -97
View File
@@ -1,6 +1,7 @@
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
@@ -22,8 +23,10 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -134,7 +137,10 @@ func newTestLive(t *testing.T, store db.DB) *live.GrafanaLive {
nil,
&usagestats.UsageStatsMock{T: t},
nil,
features, acimpl.ProvideAccessControl(features), &dashboards.FakeDashboardService{}, annotationstest.NewFakeAnnotationsRepo(), nil)
features, acimpl.ProvideAccessControl(features),
&dashboards.FakeDashboardService{},
annotationstest.NewFakeAnnotationsRepo(),
nil, nil)
require.NoError(t, err)
return gLive
}
@@ -327,12 +333,16 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) {
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
hs.starService = startest.NewStarServiceFake()
hs.dashboardVersionService = &dashvertest.FakeDashboardVersionService{
ExpectedListDashboarVersions: []*dashver.DashboardVersionDTO{},
ExpectedDashboardVersion: &dashver.DashboardVersionDTO{},
expectedDashVersions := []*dashver.DashboardVersionDTO{
{Data: simplejson.NewFromAny(map[string]any{"title": "Dash"})},
{Data: simplejson.NewFromAny(map[string]any{"title": "Dash updated"})},
}
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger())
hs.dashboardVersionService = &dashvertest.FakeDashboardVersionService{
ExpectedListDashboarVersions: []*dashver.DashboardVersionDTO{},
ExpectedDashboardVersions: expectedDashVersions,
ExpectedDashboardVersion: &dashver.DashboardVersionDTO{},
}
})
}
@@ -344,6 +354,17 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) {
return server.Send(webtest.RequestWithSignedInUser(server.NewGetRequest("/api/dashboards/uid/1/versions"), userWithPermissions(1, permissions)))
}
calculateDiff := func(server *webtest.Server, permissions []accesscontrol.Permission) (*http.Response, error) {
cmd := &dtos.CalculateDiffOptions{
Base: dtos.CalculateDiffTarget{DashboardId: 1, Version: 1},
New: dtos.CalculateDiffTarget{DashboardId: 1, Version: 2},
DiffType: "json",
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
return server.SendJSON(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/dashboards/calculate-diff", bytes.NewReader(jsonBytes)), userWithPermissions(1, permissions)))
}
t.Run("Should not be able to list dashboard versions without correct permission", func(t *testing.T) {
server := setup()
@@ -363,7 +384,6 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) {
server := setup()
permissions := []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"},
{Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:1"},
}
@@ -378,6 +398,28 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) {
require.NoError(t, res.Body.Close())
})
t.Run("Should be able to diff dashboards with correct permissions", func(t *testing.T) {
server := setup()
permissions := []accesscontrol.Permission{
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll},
}
res, err := calculateDiff(server, permissions)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
t.Run("Should not be able to diff dashboards without permissions", func(t *testing.T) {
server := setup()
res, err := calculateDiff(server, []accesscontrol.Permission{})
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
func TestDashboardAPIEndpoint(t *testing.T) {
@@ -527,39 +569,6 @@ func TestDashboardAPIEndpoint(t *testing.T) {
}),
},
}
sqlmock := dbtest.NewFakeDB()
cmd := dtos.CalculateDiffOptions{
Base: dtos.CalculateDiffTarget{
DashboardId: 1,
Version: 1,
},
New: dtos.CalculateDiffTarget{
DashboardId: 2,
Version: 2,
},
DiffType: "basic",
}
t.Run("when user does not have permission", func(t *testing.T) {
role := org.RoleViewer
postDiffScenario(t, "When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) {
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: false})
callPostDashboard(sc)
assert.Equal(t, http.StatusForbidden, sc.resp.Code)
}, sqlmock, fakeDashboardVersionService)
})
t.Run("when user does have permission", func(t *testing.T) {
role := org.RoleAdmin
postDiffScenario(t, "When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) {
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
// This test shouldn't hit GetDashboardACLInfoList, so no setup needed
sc.dashboardVersionService = fakeDashboardVersionService
callPostDashboard(sc)
assert.Equal(t, http.StatusOK, sc.resp.Code)
}, sqlmock, fakeDashboardVersionService)
})
})
t.Run("Given dashboard in folder being restored should restore to folder", func(t *testing.T) {
@@ -588,11 +597,6 @@ func TestDashboardAPIEndpoint(t *testing.T) {
},
}
mockSQLStore := dbtest.NewFakeDB()
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
t.Cleanup(func() {
guardian.New = origNewGuardian
})
restoreDashboardVersionScenario(t, "When calling POST on", "/api/dashboards/id/1/restore",
"/api/dashboards/id/:dashboardId/restore", dashboardService, fakeDashboardVersionService, cmd, func(sc *scenarioContext) {
@@ -648,7 +652,6 @@ func TestDashboardAPIEndpoint(t *testing.T) {
require.NoError(t, err)
qResult := &dashboards.Dashboard{ID: 1, Data: dataValue}
dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil)
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true})
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", org.RoleEditor, func(sc *scenarioContext) {
fakeProvisioningService := provisioning.NewProvisioningServiceMock(context.Background())
@@ -678,7 +681,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
LibraryElementService: &libraryelementsfake.LibraryElementService{},
dashboardProvisioningService: mockDashboardProvisioningService{},
SQLStore: mockSQLStore,
AccessControl: accesscontrolmock.New(),
AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
DashboardService: dashboardService,
Features: featuremgmt.WithFeatures(),
starService: startest.NewStarServiceFake(),
@@ -710,7 +713,6 @@ func TestDashboardAPIEndpoint(t *testing.T) {
Data: dataValue,
}
dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil)
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true})
loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", org.RoleEditor, func(sc *scenarioContext) {
hs := &HTTPServer{
@@ -718,7 +720,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &libraryelementsfake.LibraryElementService{},
SQLStore: mockSQLStore,
AccessControl: accesscontrolmock.New(),
AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
DashboardService: dashboardService,
Features: featuremgmt.WithFeatures(),
starService: startest.NewStarServiceFake(),
@@ -753,7 +755,7 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) {
Cfg: cfg,
pluginStore: &pluginstore.FakePluginStore{},
SQLStore: mockSQLStore,
AccessControl: accesscontrolmock.New(),
AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
Features: featuremgmt.WithFeatures(),
DashboardService: dashboardService,
dashboardVersionService: fakeDashboardVersionService,
@@ -765,13 +767,8 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) {
}
}
setUp := func() {
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
}
loggedInUserScenarioWithRole(t, "When user exists and calling GET on", "GET", "/api/dashboards/id/2/versions",
"/api/dashboards/id/:dashboardId/versions", org.RoleEditor, func(sc *scenarioContext) {
setUp()
fakeDashboardVersionService.ExpectedListDashboarVersions = []*dashver.DashboardVersionDTO{
{
Version: 1,
@@ -797,7 +794,6 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) {
loggedInUserScenarioWithRole(t, "When user does not exist and calling GET on", "GET", "/api/dashboards/id/2/versions",
"/api/dashboards/id/:dashboardId/versions", org.RoleEditor, func(sc *scenarioContext) {
setUp()
fakeDashboardVersionService.ExpectedListDashboarVersions = []*dashver.DashboardVersionDTO{
{
Version: 1,
@@ -823,7 +819,6 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) {
loggedInUserScenarioWithRole(t, "When failing to get user and calling GET on", "GET", "/api/dashboards/id/2/versions",
"/api/dashboards/id/:dashboardId/versions", org.RoleEditor, func(sc *scenarioContext) {
setUp()
fakeDashboardVersionService.ExpectedListDashboarVersions = []*dashver.DashboardVersionDTO{
{
Version: 1,
@@ -882,6 +877,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
cfg, dashboardStore, folderStore, features, folderPermissions,
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil,
dualwrite.ProvideTestService(), sort.ProvideService(),
serverlock.ProvideService(db, tracing.InitializeTracerForTest()), kvstore.NewFakeKVStore(),
)
require.NoError(t, err)
dashboardService.(dashboards.PermissionsRegistrationService).RegisterDashboardPermissions(dashboardPermissions)
@@ -891,6 +887,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
cfg, dashboardStore, folderStore, features, folderPermissions,
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil,
dualwrite.ProvideTestService(), sort.ProvideService(),
serverlock.ProvideService(db, tracing.InitializeTracerForTest()), kvstore.NewFakeKVStore(),
)
require.NoError(t, err)
@@ -978,47 +975,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
})
}
func postDiffScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions,
role org.RoleType, fn scenarioFunc, sqlmock db.DB, fakeDashboardVersionService *dashvertest.FakeDashboardVersionService,
) {
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
cfg := setting.NewCfg()
dashSvc := dashboards.NewFakeDashboardService(t)
hs := HTTPServer{
Cfg: cfg,
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
Live: newTestLive(t, db.InitTestDB(t)),
QuotaService: quotatest.New(false, nil),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &libraryelementsfake.LibraryElementService{},
SQLStore: sqlmock,
dashboardVersionService: fakeDashboardVersionService,
Features: featuremgmt.WithFeatures(),
DashboardService: dashSvc,
tracer: tracing.InitializeTracerForTest(),
}
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.SignedInUser = &user.SignedInUser{
OrgID: testOrgID,
UserID: testUserID,
}
sc.context.OrgRole = role
return hs.CalculateDashboardDiff(c)
})
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}
func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string,
mock *dashboards.FakeDashboardService, fakeDashboardVersionService *dashvertest.FakeDashboardVersionService,
cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore db.DB,
+15 -14
View File
@@ -154,20 +154,21 @@ type FrontendSettingsSqlConnectionLimitsDTO struct {
}
type FrontendSettingsDTO struct {
DefaultDatasource string `json:"defaultDatasource"`
Datasources map[string]plugins.DataSourceDTO `json:"datasources"`
MinRefreshInterval string `json:"minRefreshInterval"`
Panels map[string]plugins.PanelDTO `json:"panels"`
Apps map[string]*plugins.AppDTO `json:"apps"`
AppUrl string `json:"appUrl"`
AppSubUrl string `json:"appSubUrl"`
AllowOrgCreate bool `json:"allowOrgCreate"`
AuthProxyEnabled bool `json:"authProxyEnabled"`
LdapEnabled bool `json:"ldapEnabled"`
JwtHeaderName string `json:"jwtHeaderName"`
JwtUrlLogin bool `json:"jwtUrlLogin"`
LiveEnabled bool `json:"liveEnabled"`
AutoAssignOrg bool `json:"autoAssignOrg"`
DefaultDatasource string `json:"defaultDatasource"`
Datasources map[string]plugins.DataSourceDTO `json:"datasources"`
MinRefreshInterval string `json:"minRefreshInterval"`
Panels map[string]plugins.PanelDTO `json:"panels"`
Apps map[string]*plugins.AppDTO `json:"apps"`
AppUrl string `json:"appUrl"`
AppSubUrl string `json:"appSubUrl"`
AllowOrgCreate bool `json:"allowOrgCreate"`
AuthProxyEnabled bool `json:"authProxyEnabled"`
LdapEnabled bool `json:"ldapEnabled"`
JwtHeaderName string `json:"jwtHeaderName"`
JwtUrlLogin bool `json:"jwtUrlLogin"`
LiveEnabled bool `json:"liveEnabled"`
LiveMessageSizeLimit int `json:"liveMessageSizeLimit"`
AutoAssignOrg bool `json:"autoAssignOrg"`
VerifyEmailEnabled bool `json:"verifyEmailEnabled"`
SigV4AuthEnabled bool `json:"sigV4AuthEnabled"`
+4
View File
@@ -18,8 +18,10 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
@@ -476,6 +478,8 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
sc.cfg, dashStore, folderStore,
features, folderPermissions, ac,
folderServiceWithFlagOn, fStore, nil, client.MockTestRestConfig{}, nil, quotaSrv, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
serverlock.ProvideService(sc.db, tracing.InitializeTracerForTest()),
kvstore.NewFakeKVStore(),
)
require.NoError(b, err)
+9
View File
@@ -768,3 +768,12 @@ func TestSetDefaultPermissionsWhenCreatingFolder(t *testing.T) {
})
}
}
func setUpRBACGuardian(t *testing.T) {
origNewGuardian := guardian.New
t.Cleanup(func() {
guardian.New = origNewGuardian
})
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanEditValue: true, CanViewValue: true})
}
+1
View File
@@ -194,6 +194,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
JwtHeaderName: hs.Cfg.JWTAuth.HeaderName,
JwtUrlLogin: hs.Cfg.JWTAuth.URLLogin,
LiveEnabled: hs.Cfg.LiveMaxConnections != 0,
LiveMessageSizeLimit: hs.Cfg.LiveMessageSizeLimit,
AutoAssignOrg: hs.Cfg.AutoAssignOrg,
VerifyEmailEnabled: hs.Cfg.VerifyEmailEnabled,
SigV4AuthEnabled: hs.Cfg.SigV4AuthEnabled,