Authorization / Access control correctness

HIGH
grafana/grafana
Commit: 5c28fb2e0596
Affected: 12.0.0 - 12.3.x (prior to fix in 12.4.0)
2026-06-22 13:50 UTC

Description

The commit fixes an authorization/batch-check bug in user search. The code previously included access-control checks for verbs that are not defined on the authz model's user type (e.g., org.users:add / VerbCreate and users.permissions:read / VerbGetPermissions). When performing batch access-control checks for user search, evaluating an unsupported verb can cause the batch to fail (e.g., reporting that a relation is not found) and blank out AccessControl metadata for results. This could lead to incorrect authorization decisions or information disclosure during user search. The patch removes unsupported verbs from the batch checks, guards the tests to only use verbs defined for the user type, and introduces a test ensuring only supported verbs are used.

Proof of Concept

PoC (illustrative; shows the pre-fix exploitation path vs post-fix behavior): Pre-fix scenario (vulnerable behavior): - An authenticated user with permissions to list users and read user roles triggers a user search API that internally runs a batch of access-control checks for various verbs, including ones not defined on the authz model's user type (e.g., org.users:add, users.permissions:read). - The authz engine encounters a check mapped to a relation not defined for the user type and returns an error (e.g., "relation 'user#create' not found"), which blanks out all AccessControl metadata for the search results. - As a result, the client receives either an error or a response with missing/blank AccessControl entries, potentially leaking information about what was attempted or denying valid results. Proof-of-concept steps (pseudo-code / curl): 1) Prepare a batch-check request that includes an unsupported verb for the user type: curl -X POST https://grafana.example/api/iam/batch_check \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "checks": [ {"action":"org.users:add","group":"grafana","resource":"users","verb":"CREATE"}, {"action":"org.users:read","group":"grafana","resource":"users","verb":"LIST"}, {"action":"users.permissions:read","group":"grafana","resource":"users","verb":"GET_PERMISSIONS"} ] }' 2) Observe the response: - Likely an error or a response where AccessControl metadata is blank/missing for all hits due to the unsupported verb being evaluated in the batch. 3) Post-fix behavior (after this commit in 12.4.0): - The batch only includes verbs defined on the authz model's user type (get/update/delete). The unsupported checks (org.users:add, users.permissions:read) are omitted, so the batch completes and AccessControl metadata is populated correctly for results the user is allowed to view. - No blanking of metadata occurs, and legitimate access decisions are enforced as expected. Note: The test TestUserAccessControlChecksUseSupportedVerbs in the patch ensures that only supported verbs are included for the user resource, preventing this class of issue from reoccurring.

Commit Details

Author: Georges Chaudy

Date: 2026-06-22 08:18 UTC

Message:

IAM: Fix user search access control batch checks (#126707) * IAM: fix user search access control batch checks Evaluate org.users:add and users.permissions:read at group_resource scope (empty name) so Zanzana batch checks no longer fail on missing typed user relations and accessControl metadata is returned. * IAM: simplify user search access control stamping Use a groupScope flag on checks instead of separate helpers and slices. * Remove org.users:create, and user.permissions:read from the stampAccessControl list --------- Co-authored-by: Mihaly Gyongyosi <mgyongyosi@users.noreply.github.com>

Triage Assessment

Vulnerability Type: Authorization/B access control correctness

Confidence: HIGH

Reasoning:

The commit adjusts IAM access control batch checks for user search so that only verbs defined on the authz model's user type are evaluated. It removes checks for verbs that could cause batch checks to fail or produce incorrect/blanked access control metadata (e.g., org.users:add and users.permissions:read) and guards tests to ensure only supported verbs are considered. This directly addresses authorization behavior and prevents potential information disclosure or incorrect authorization decisions during user search, indicating a security vulnerability fix in access control evaluation.

Verification Assessment

Vulnerability Type: Authorization / Access control correctness

Confidence: HIGH

Affected Versions: 12.0.0 - 12.3.x (prior to fix in 12.4.0)

Code Diff

diff --git a/pkg/registry/apis/iam/user/search.go b/pkg/registry/apis/iam/user/search.go index 4f8b5a499bc65..213de2e2b996d 100644 --- a/pkg/registry/apis/iam/user/search.go +++ b/pkg/registry/apis/iam/user/search.go @@ -49,12 +49,15 @@ type accessControlCheck struct { name string // user UID of the resource being checked } +// Only verbs whose relation is defined on the authz model's "user" type may be +// checked here. A check for a relation the type lacks fails the whole batch +// (e.g. "relation 'user#create' not found"), blanking out all access control +// metadata. The "user" type defines only get/update/delete, so VerbCreate +// (org.users:add) and VerbGetPermissions (users.permissions:read) are omitted. var userAccessControlChecks = []accessControlCheck{ {action: "org.users:read", group: iamv0.GROUP, resource: "users", verb: utils.VerbList}, - {action: "org.users:add", group: iamv0.GROUP, resource: "users", verb: utils.VerbCreate}, {action: "org.users:remove", group: iamv0.GROUP, resource: "users", verb: utils.VerbDelete}, {action: "org.users:write", group: iamv0.GROUP, resource: "users", verb: utils.VerbUpdate}, - {action: "users.permissions:read", group: iamv0.GROUP, resource: "users", verb: utils.VerbGetPermissions}, {action: "users.roles:read", group: iamv0.GROUP, resource: "rolebindings", verb: utils.VerbList}, } diff --git a/pkg/registry/apis/iam/user/search_test.go b/pkg/registry/apis/iam/user/search_test.go index f208b239cadc9..4120792db28e7 100644 --- a/pkg/registry/apis/iam/user/search_test.go +++ b/pkg/registry/apis/iam/user/search_test.go @@ -16,6 +16,7 @@ import ( iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -227,13 +228,34 @@ func TestSearchSort(t *testing.T) { } } +// The authz model's "user" type defines only get/update/delete relations +// (schema_core.fga). A check whose verb maps to any other relation fails the +// whole batch check at the authz server, blanking out all metadata, so guard +// against re-adding one for the "users" resource. +func TestUserAccessControlChecksUseSupportedVerbs(t *testing.T) { + supportedUserVerbs := map[string]bool{ + utils.VerbGet: true, + utils.VerbList: true, + utils.VerbWatch: true, + utils.VerbUpdate: true, + utils.VerbPatch: true, + utils.VerbDelete: true, + } + for _, c := range userAccessControlChecks { + if c.resource != "users" { + continue + } + assert.True(t, supportedUserVerbs[c.verb], + "check %q uses verb %q whose relation is not defined on the authz \"user\" type", c.action, c.verb) + } +} + func TestAccessControl(t *testing.T) { partialClient := &mockAccessClient{ batchCheckFunc: func(_ context.Context, _ authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) { allowed := map[string]bool{ - "org.users:read": true, - "users.permissions:read": true, - "users.roles:read": true, + "org.users:read": true, + "users.roles:read": true, } results := make(map[string]authlib.BatchCheckResult, len(req.Checks)) for _, check := range req.Checks { @@ -335,9 +357,7 @@ func TestAccessControl(t *testing.T) { for _, hit := range hits { require.NotNil(t, hit.AccessControl) assert.True(t, hit.AccessControl["org.users:read"]) - assert.True(t, hit.AccessControl["users.permissions:read"]) assert.True(t, hit.AccessControl["users.roles:read"]) - assert.False(t, hit.AccessControl["org.users:add"]) assert.False(t, hit.AccessControl["org.users:remove"]) assert.False(t, hit.AccessControl["org.users:write"]) }
← Back to Alerts View on GitHub →