RBAC / Access Control (authorization misconfiguration)
Description
The commit refactors RBAC checks for snapshot operations to use general dashboard actions instead of per-snapshot actions. Specifically, it replaces references to dashboardsnapshots.ActionSnapshotsCreate/Delete/Read with dashboards.ActionSnapshotsCreate/Delete/Read and adjusts evaluators to require both the create/delete/read action and the corresponding dashboards read permission with the correct dashboard scope. This aligns authorization checks across dashboards and snapshots, reducing the risk of misconfigurations allowing unauthorized access to snapshot functionality. In prior versions, mismatch between snapshot-specific actions and roles could permit unintended access or block legitimate access, depending on how roles were defined.
Commit Details
Author: Stephanie Hingtgen
Date: 2026-04-07 20:38 UTC
Message:
Snapshots: Move back to dashboards for now (#122067)
Triage Assessment
Vulnerability Type: Access Control (RBAC)
Confidence: MEDIUM
Reasoning:
The patch switches authorization checks from snapshots-specific actions to general dashboards actions (e.g., ActionSnapshotsCreate/Delete/Read) across multiple files, aligning RBAC permissions used for snapshot operations. This directly affects access control decisions and could fix incorrect permissions gating, thereby addressing authorization vulnerabilities or misconfigurations.
Verification Assessment
Vulnerability Type: RBAC / Access Control (authorization misconfiguration)
Confidence: MEDIUM
Affected Versions: <= 12.4.0
Code Diff
diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go
index ee24219a36cb0..9393d0982eace 100644
--- a/pkg/api/accesscontrol.go
+++ b/pkg/api/accesscontrol.go
@@ -6,7 +6,6 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
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/datasources"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/libraryelements"
@@ -530,7 +529,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Create snapshots",
Group: "Snapshots",
Permissions: []ac.Permission{
- {Action: dashboardsnapshots.ActionSnapshotsCreate},
+ {Action: dashboards.ActionSnapshotsCreate},
},
},
Grants: []string{string(org.RoleEditor)},
@@ -543,7 +542,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Delete snapshots",
Group: "Snapshots",
Permissions: []ac.Permission{
- {Action: dashboardsnapshots.ActionSnapshotsDelete},
+ {Action: dashboards.ActionSnapshotsDelete},
},
},
Grants: []string{string(org.RoleEditor)},
@@ -556,7 +555,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
Description: "Read snapshots",
Group: "Snapshots",
Permissions: []ac.Permission{
- {Action: dashboardsnapshots.ActionSnapshotsRead},
+ {Action: dashboards.ActionSnapshotsRead},
},
},
Grants: []string{string(org.RoleViewer)},
diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go
index bcd2d6ceecbc6..62035a0e7491c 100644
--- a/pkg/api/dashboard_snapshot.go
+++ b/pkg/api/dashboard_snapshot.go
@@ -90,7 +90,7 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *contextmodel.ReqContext) {
}
// Regular mode: check permissions
- evaluator := ac.EvalAll(ac.EvalPermission(dashboardsnapshots.ActionSnapshotsCreate), ac.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(cmd.Dashboard.GetNestedString("uid"))))
+ evaluator := ac.EvalAll(ac.EvalPermission(dashboards.ActionSnapshotsCreate), ac.EvalPermission(dashboards.ActionDashboardsRead, 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
diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go
index e39450924af41..6e0d1e5439c9e 100644
--- a/pkg/middleware/auth.go
+++ b/pkg/middleware/auth.go
@@ -15,7 +15,7 @@ import (
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/authn"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
- "github.com/grafana/grafana/pkg/services/dashboardsnapshots"
+ "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
@@ -246,7 +246,7 @@ func SnapshotPublicModeOrCreate(cfg *setting.Cfg, ac2 ac.AccessControl) web.Hand
return
}
- ac.Middleware(ac2)(ac.EvalPermission(dashboardsnapshots.ActionSnapshotsCreate))
+ ac.Middleware(ac2)(ac.EvalPermission(dashboards.ActionSnapshotsCreate))
}
}
@@ -263,7 +263,7 @@ func SnapshotPublicModeOrDelete(cfg *setting.Cfg, ac2 ac.AccessControl) web.Hand
return
}
- ac.Middleware(ac2)(ac.EvalPermission(dashboardsnapshots.ActionSnapshotsDelete))
+ ac.Middleware(ac2)(ac.EvalPermission(dashboards.ActionSnapshotsDelete))
}
}
diff --git a/pkg/registry/apis/dashboard/snapshot/authorizer.go b/pkg/registry/apis/dashboard/snapshot/authorizer.go
index d873611cbe8c6..a57c8e5695271 100644
--- a/pkg/registry/apis/dashboard/snapshot/authorizer.go
+++ b/pkg/registry/apis/dashboard/snapshot/authorizer.go
@@ -7,7 +7,7 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
- "github.com/grafana/grafana/pkg/services/dashboardsnapshots"
+ "github.com/grafana/grafana/pkg/services/dashboards"
)
// NewSnapshotAuthorizer returns an authorizer that maps k8s verbs to snapshot RBAC actions.
@@ -28,9 +28,9 @@ func NewSnapshotAuthorizer(accessControl ac.AccessControl) authorizer.Authorizer
var action string
switch attr.GetSubresource() {
case "dashboard":
- action = dashboardsnapshots.ActionSnapshotsRead
+ action = dashboards.ActionSnapshotsRead
case "deletekey":
- action = dashboardsnapshots.ActionSnapshotsDelete
+ action = dashboards.ActionSnapshotsDelete
default:
return authorizer.DecisionDeny, "unsupported subresource", nil
}
@@ -45,11 +45,11 @@ func NewSnapshotAuthorizer(accessControl ac.AccessControl) authorizer.Authorizer
var action string
switch attr.GetVerb() {
case "get", "list":
- action = dashboardsnapshots.ActionSnapshotsRead
+ action = dashboards.ActionSnapshotsRead
case "create":
- action = dashboardsnapshots.ActionSnapshotsCreate
+ action = dashboards.ActionSnapshotsCreate
case "delete":
- action = dashboardsnapshots.ActionSnapshotsDelete
+ action = dashboards.ActionSnapshotsDelete
default:
return authorizer.DecisionDeny, "unsupported verb", nil
}
diff --git a/pkg/registry/apis/dashboard/snapshot/routes.go b/pkg/registry/apis/dashboard/snapshot/routes.go
index 4d27c27c68643..420f7a88b25fe 100644
--- a/pkg/registry/apis/dashboard/snapshot/routes.go
+++ b/pkg/registry/apis/dashboard/snapshot/routes.go
@@ -117,7 +117,7 @@ func GetRoutes(service dashboardsnapshots.Service, options dashv0.SnapshotSharin
}
// RBAC check for snapshot creation
- if ok, err := accessControl.Evaluate(ctx, user, ac.EvalPermission(dashboardsnapshots.ActionSnapshotsCreate)); !ok || err != nil {
+ if ok, err := accessControl.Evaluate(ctx, user, ac.EvalPermission(dashboards.ActionSnapshotsCreate)); !ok || err != nil {
wrap.JsonApiErr(http.StatusForbidden, "access denied", err)
return
}
@@ -263,7 +263,7 @@ func GetRoutes(service dashboardsnapshots.Service, options dashv0.SnapshotSharin
errhttp.Write(ctx, err, w)
return
}
- if ok, err := accessControl.Evaluate(ctx, user, ac.EvalPermission(dashboardsnapshots.ActionSnapshotsDelete)); !ok || err != nil {
+ if ok, err := accessControl.Evaluate(ctx, user, ac.EvalPermission(dashboards.ActionSnapshotsDelete)); !ok || err != nil {
w.WriteHeader(http.StatusForbidden)
_ = json.NewEncoder(w).Encode(&util.DynMap{"message": "access denied"})
return
@@ -348,7 +348,7 @@ func GetRoutes(service dashboardsnapshots.Service, options dashv0.SnapshotSharin
}
// RBAC check for reading snapshot settings
- if ok, err := accessControl.Evaluate(ctx, user, ac.EvalPermission(dashboardsnapshots.ActionSnapshotsRead)); !ok || err != nil {
+ if ok, err := accessControl.Evaluate(ctx, user, ac.EvalPermission(dashboards.ActionSnapshotsRead)); !ok || err != nil {
wrap.JsonApiErr(http.StatusForbidden, "access denied", err)
return
}
diff --git a/pkg/registry/apis/dashboard/snapshot/routes_test.go b/pkg/registry/apis/dashboard/snapshot/routes_test.go
index 67bc1955cc92d..686168bb3d838 100644
--- a/pkg/registry/apis/dashboard/snapshot/routes_test.go
+++ b/pkg/registry/apis/dashboard/snapshot/routes_test.go
@@ -124,7 +124,7 @@ func TestCreateSnapshotDashboardValidation(t *testing.T) {
routes := GetRoutes(
snapshotService,
dashv0.SnapshotSharingOptions{SnapshotsEnabled: true},
- acmock.New().WithPermissions([]accesscontrol.Permission{{Action: dashboardsnapshots.ActionSnapshotsCreate}}),
+ acmock.New().WithPermissions([]accesscontrol.Permission{{Action: dashboards.ActionSnapshotsCreate}}),
map[string]common.OpenAPIDefinition{},
tt.setupStorageMock(t),
dashboardService,
diff --git a/pkg/services/dashboards/accesscontrol.go b/pkg/services/dashboards/accesscontrol.go
index edcb66d060a3a..818b3e52dafaf 100644
--- a/pkg/services/dashboards/accesscontrol.go
+++ b/pkg/services/dashboards/accesscontrol.go
@@ -11,6 +11,13 @@ import (
"github.com/grafana/grafana/pkg/services/folder"
)
+// TODO: move to dashboardsnapshots package
+const (
+ ActionSnapshotsCreate = "snapshots:create"
+ ActionSnapshotsDelete = "snapshots:delete"
+ ActionSnapshotsRead = "snapshots:read"
+)
+
const (
ScopeDashboardsRoot = "dashboards"
ScopeDashboardsPrefix = "dashboards:uid:"