Authorization/Access Control

MEDIUM
grafana/grafana
Commit: 9f7c615a5c16
Affected: < 12.4.0
2026-06-26 22:53 UTC

Description

The commit fixes an authorization bug in Grafana's RBAC translation by aligning org.users:* actions with the global users:* actions for both read and write scopes. Previously, org-level user-management permissions (org.users:read, org.users:write, org.users:remove) were not consistently reflected in the legacy permissions (users:id:*), which could cause org-level grants not to propagate correctly and lead to misconfigurations or privilege inconsistencies. The patch updates isUserRBACAction and userScopeKind to treat org.users:* actions as org-scoped within the users kind and extends the translation table to include org.users:read, org.users:write, and org.users:remove. This ensures proper reflection of org-level grants back to legacy permissions, improving authorization correctness and reducing potential privilege misconfigurations.

Proof of Concept

// Proof of concept illustrating the mapping after the fix // This demonstrates that an org-level org.users:* action maps to the users scope as expected. // In Grafana's authorization resolver, a request that uses org.users:read with All=true should yield // a permission entry of Action: org.users:read, Scope: users:id:* /* Go test-style PoC snippet (conceptual example): package zanzana_poc_test import ( "testing" "github.com/stretchr/testify/require" ac "path/to/pkg/services/accesscontrol/acimpl" // adjust import path as needed authzv1 "path/to/pkg/apis/authz/v1" // adjust import path as needed ) func TestOrgUsersReadMapsToUsersIDStar(t *testing.T) { perms, err := zanzanaResolve(&authzv1.ListResponse{All: true}, "org.users:read", "") require.NoError(t, err) require.Equal(t, []ac.Permission{ { Action: "org.users:read", Scope: "users:id:*" }, }, perms) } func TestOrgUsersWriteMapsToUsersIDStar(t *testing.T) { perms, err := zanzanaResolve(&authzv1.ListResponse{All: true}, "org.users:write", "") require.NoError(t, err) require.Equal(t, []ac.Permission{ { Action: "org.users:write", Scope: "users:id:*" }, }, perms) } func TestOrgUsersRemoveMapsToUsersIDStar(t *testing.T) { perms, err := zanzanaResolve(&authzv1.ListResponse{All: true}, "org.users:remove", "") require.NoError(t, err) require.Equal(t, []ac.Permission{ { Action: "org.users:remove", Scope: "users:id:*" }, }, perms) } */

Commit Details

Author: Cory Forseth

Date: 2026-06-26 21:54 UTC

Message:

RBAC: account for org.users permissions when merging from zanzana (#127345) The org.users:* family (org.users:read/write/remove) gates the same iam.grafana.app/users verbs as the global users:* family on the write side (userManagementMappings in tuple_helpers.go), but the read/merge path did not list them, so org-level user-admin grants were never reflected back as legacy permissions. Add org.users:read/write/remove to the KindUsers translation, match the org.users: prefix in isUserRBACAction, and scope the family to the org-level users kind in userScopeKind (users:id:*), matching roles.go and the docs. org.users:add is omitted, mirroring the write-side mapping. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

Triage Assessment

Vulnerability Type: Authorization/Access Control

Confidence: MEDIUM

Reasoning:

The commit expands RBAC mappings to correctly reflect org-level (org.users) permissions in the access control layer. By aligning org.users:* with global users:* actions and ensuring org-level grants propagate as legacy permissions, it reduces the risk of improper permission grants or revocation due to mis-scoped or missing mappings. This directly affects authorization correctness and helps prevent potential privilege escalation or privilege misconfiguration related to user management actions.

Verification Assessment

Vulnerability Type: Authorization/Access Control

Confidence: MEDIUM

Affected Versions: < 12.4.0

Code Diff

diff --git a/pkg/services/accesscontrol/acimpl/zanzana_resolver.go b/pkg/services/accesscontrol/acimpl/zanzana_resolver.go index 194a7d4907e52..5a85edaaef4ca 100644 --- a/pkg/services/accesscontrol/acimpl/zanzana_resolver.go +++ b/pkg/services/accesscontrol/acimpl/zanzana_resolver.go @@ -273,18 +273,21 @@ func teamWildcardScope() string { return ac.Scope("teams", "id", "*") } -// isUserRBACAction matches user-management actions. +// isUserRBACAction matches user-management actions, including the org.users:* family. func isUserRBACAction(action string) bool { - return strings.HasPrefix(action, "users:") || strings.HasPrefix(action, "users.") + return strings.HasPrefix(action, "users:") || strings.HasPrefix(action, "users.") || + strings.HasPrefix(action, "org.users:") } // userScopeKind returns the legacy RBAC scope kind for a user-management action. // Most user actions are server-level and scoped to global.users (users:read/write/delete and -// users.permissions:write all grant global.users:* in the fixed roles). users.permissions:read is -// the exception: it gates org-level permission listing (fixed:org.users:reader) and is scoped to -// the org-level users kind. +// users.permissions:write all grant global.users:* in the fixed roles). The org-level +// exceptions, scoped to the users kind, are users.permissions:read (fixed:org.users:reader) +// and the org.users:* family (fixed:org.users:reader/writer). func userScopeKind(action string) string { - if action == ac.ActionUsersPermissionsRead { + // users.permissions:read and the org.users:* family are org-scoped (the users kind); + // the remaining user-management actions are server-level (global.users). + if action == ac.ActionUsersPermissionsRead || strings.HasPrefix(action, "org.users:") { return "users" } return "global.users" diff --git a/pkg/services/accesscontrol/acimpl/zanzana_resolver_test.go b/pkg/services/accesscontrol/acimpl/zanzana_resolver_test.go index 66a27de612e5d..475c727e61fce 100644 --- a/pkg/services/accesscontrol/acimpl/zanzana_resolver_test.go +++ b/pkg/services/accesscontrol/acimpl/zanzana_resolver_test.go @@ -886,6 +886,17 @@ func TestListPermissions_UserActions(t *testing.T) { }, perms) }) + // The org.users:* family is org-scoped (users kind), like users.permissions:read. + for _, action := range []string{"org.users:read", "org.users:write", "org.users:remove"} { + t.Run(action+" All=true produces users:id:* wildcard", func(t *testing.T) { + perms, err := zanzanaResolve(&authzv1.ListResponse{All: true}, action, "") + require.NoError(t, err) + require.Equal(t, []ac.Permission{ + {Action: action, Scope: "users:id:*"}, + }, perms) + }) + } + t.Run("items fall back to uid scope when scope resolver is nil", func(t *testing.T) { perms, err := zanzanaResolve(&authzv1.ListResponse{Items: []string{"user-abc"}}, "users:read", "") require.NoError(t, err) diff --git a/pkg/services/authz/zanzana/common/translations.go b/pkg/services/authz/zanzana/common/translations.go index 82246224a48f7..2f12dc30b1917 100644 --- a/pkg/services/authz/zanzana/common/translations.go +++ b/pkg/services/authz/zanzana/common/translations.go @@ -146,6 +146,12 @@ var resourceTranslations = map[string]resourceTranslation{ "users:delete": newMapping(RelationDelete, ""), "users.permissions:read": newMapping(RelationGetPermissions, ""), "users.permissions:write": newMapping(RelationSetPermissions, ""), + // The org.users:* family gates the same iam.grafana.app/users verbs as the + // global users:* family (see userManagementMappings in tuple_helpers.go). + // org.users:add is intentionally omitted, matching the write-side mapping. + "org.users:read": newMapping(RelationGet, ""), + "org.users:write": newMapping(RelationUpdate, ""), + "org.users:remove": newMapping(RelationDelete, ""), }, }, }
← Back to Alerts View on GitHub →