RBAC / Access Control (authorization misconfiguration)

MEDIUM
grafana/grafana
Commit: e1d9d67acf92
Affected: <= 12.4.0
2026-04-07 21:32 UTC

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:"
← Back to Alerts View on GitHub →