Access Control / Authorization

HIGH
grafana/grafana
Commit: 84dad3111d78
Affected: < 12.4.0 (i.e., Grafana 12.x releases prior to 12.4.0)
2026-05-13 13:41 UTC

Description

The commit adds strict validation to admission handling of ResourcePermission objects. Previously the code only checked that the group/resource was registered and enabled, and for duplicates, but did not validate that the requested permission Kind and Verb were actually allowed for the target resource. The fix introduces: - Retrieval and verification of a mapper for the target group/resource and guards against missing mapper after the IsEnabled check. - Per-permission validation that the Kind is allowed by the resource's mapper (AllowsKind) and that the Verb is valid (ActionSet returns no error). - Early rejection with clear BadRequest errors when Kind or Verb are not permitted or when a mapper is missing. This hardens admission validation against misconfigurations that could otherwise grant or imply permissions that are not enforceable by the resource’s mapper, reducing the risk of authorization bypass or privilege escalation via crafted ResourcePermission objects. Tests were added to cover these validation rules. In short: this is a real security fix for access control validation in the IAM/resourcepermission admission flow.

Proof of Concept

Proof-of-concept exploit (pre-fix behavior): 1) Ensure Grafana IAM CRDs are installed in a Kubernetes cluster (ResourcePermission CRD is present). 2) Create a ResourcePermission that attempts to assign a permission with a Kind that is not allowed by the resource's mapper or an invalid Verb. 3) Apply the manifest and observe rejection by the API with a BadRequest error indicating the invalid Kind/Verb for the resource. Example manifest to reproduce the issue (as a PoC of misconfig validation prior to the fix): apiVersion: iam.grafana.app/v0alpha1 kind: ResourcePermission metadata: name: iam.grafana.app-serviceaccounts-sa-abc123 spec: resource: apiGroup: iam.grafana.app resource: serviceaccounts name: sa-abc123 permissions: - kind: BasicRole # invalid for this resource according to the mapper (not allowed) name: Editor verb: edit Expected result on apply (pre-fix would accept; post-fix rejects): - HTTP 400 Bad Request with a message similar to: assignment kind "BasicRole" is not allowed for resource "serviceaccounts": invalid spec If you instead attempt an invalid Verb for a valid Kind (e.g., Verb: view for a resource where only edit/admin are allowed): - HTTP 400 Bad Request with a message like: verb "view" is not valid for resource "serviceaccounts": invalid spec A valid example that should pass after the fix: apiVersion: iam.grafana.app/v0alpha1 kind: ResourcePermission metadata: name: iam.grafana.app-serviceaccounts-sa-abc123 spec: resource: apiGroup: iam.grafana.app resource: serviceaccounts name: sa-abc123 permissions: - kind: User name: "user-uid-xyz" verb: edit This PoC demonstrates that without the fix, malformed Kinds/Verbs could sneak into admission validation; with the fix, such misconfigurations are rejected up front.

Commit Details

Author: linoman

Date: 2026-05-13 13:16 UTC

Message:

SA: enforce kind and verb restrictions in admission validation (#124378) * AccessControl: enforce kind and verb restrictions in admission validation * SA: guard against nil mapper after IsEnabled check in ValidateCreateAndUpdateInput

Triage Assessment

Vulnerability Type: Access control / Authorization

Confidence: HIGH

Reasoning:

The commit tightens admission validation by enforcing allowed kinds and verbs for permissions, ensuring only permitted actions can be configured. It also validates the presence of a mapper for the group/resource and guards against invalid inputs, addressing potential misconfigurations that could lead to unauthorized access or privilege escalation. Added tests cover these validation rules.

Verification Assessment

Vulnerability Type: Access Control / Authorization

Confidence: HIGH

Affected Versions: < 12.4.0 (i.e., Grafana 12.x releases prior to 12.4.0)

Code Diff

diff --git a/pkg/registry/apis/iam/resourcepermission/validate.go b/pkg/registry/apis/iam/resourcepermission/validate.go index 299adb5b3bc6b..dbf96b6fcc8c0 100644 --- a/pkg/registry/apis/iam/resourcepermission/validate.go +++ b/pkg/registry/apis/iam/resourcepermission/validate.go @@ -36,13 +36,25 @@ func ValidateCreateAndUpdateInput(ctx context.Context, v0ResourcePerm *v0alpha1. } // Check that the group/resource is registered and enabled - if !mappers.IsEnabled(schema.GroupResource{Group: grn.Group, Resource: grn.Resource}) { + groupResource := schema.GroupResource{Group: grn.Group, Resource: grn.Resource} + if !mappers.IsEnabled(groupResource) { return apierrors.NewBadRequest(fmt.Sprintf("unknown or disabled group/resource %s/%s", grn.Group, grn.Resource)) } - // Check for duplicate entities (same kind and name should appear only once) + mapper, ok := mappers.Get(groupResource) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("mapper not found for group/resource %s/%s", grn.Group, grn.Resource)) + } + + // Check for duplicate entities and validate kind/verb per permission seen := make(map[string]bool) for _, perm := range v0ResourcePerm.Spec.Permissions { + if !mapper.AllowsKind(perm.Kind) { + return apierrors.NewBadRequest(fmt.Sprintf("assignment kind %q is not allowed for resource %q: %s", perm.Kind, grn.Resource, errInvalidSpec)) + } + if _, err := mapper.ActionSet(perm.Verb); err != nil { + return apierrors.NewBadRequest(fmt.Sprintf("verb %q is not valid for resource %q: %s", perm.Verb, grn.Resource, errInvalidSpec)) + } key := fmt.Sprintf("%s:%s", perm.Kind, perm.Name) if seen[key] { return apierrors.NewBadRequest(fmt.Sprintf("duplicate entity found: kind=%s, name=%s (each entity can only appear once per resource): %s", perm.Kind, perm.Name, errInvalidSpec)) diff --git a/pkg/registry/apis/iam/resourcepermission/validate_test.go b/pkg/registry/apis/iam/resourcepermission/validate_test.go index 9099897b74a6b..63627ad2781da 100644 --- a/pkg/registry/apis/iam/resourcepermission/validate_test.go +++ b/pkg/registry/apis/iam/resourcepermission/validate_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" ) @@ -192,6 +193,123 @@ func TestValidateOnCreate(t *testing.T) { } } +func TestValidateOnCreate_KindAndVerbRestrictions(t *testing.T) { + mappers := NewMappersRegistry() + mappers.RegisterMapper( + schema.GroupResource{Group: "iam.grafana.app", Resource: "serviceaccounts"}, + NewMapperWithAttribute("serviceaccounts", []string{"Edit", "Admin"}, ScopeAttributeID, + []iamv0alpha1.ResourcePermissionSpecPermissionKind{ + iamv0alpha1.ResourcePermissionSpecPermissionKindUser, + iamv0alpha1.ResourcePermissionSpecPermissionKindServiceAccount, + iamv0alpha1.ResourcePermissionSpecPermissionKindTeam, + }), + nil, + ) + + tests := []struct { + name string + obj *iamv0alpha1.ResourcePermission + wantErr bool + }{ + { + name: "serviceaccount with BasicRole kind - should fail", + obj: &iamv0alpha1.ResourcePermission{ + ObjectMeta: v1.ObjectMeta{Name: "iam.grafana.app-serviceaccounts-sa-abc123"}, + Spec: iamv0alpha1.ResourcePermissionSpec{ + Resource: iamv0alpha1.ResourcePermissionspecResource{ + ApiGroup: "iam.grafana.app", + Resource: "serviceaccounts", + Name: "sa-abc123", + }, + Permissions: []iamv0alpha1.ResourcePermissionspecPermission{ + {Kind: iamv0alpha1.ResourcePermissionSpecPermissionKindBasicRole, Name: "Editor", Verb: "edit"}, + }, + }, + }, + wantErr: true, + }, + { + name: "serviceaccount with view verb - should fail", + obj: &iamv0alpha1.ResourcePermission{ + ObjectMeta: v1.ObjectMeta{Name: "iam.grafana.app-serviceaccounts-sa-abc123"}, + Spec: iamv0alpha1.ResourcePermissionSpec{ + Resource: iamv0alpha1.ResourcePermissionspecResource{ + ApiGroup: "iam.grafana.app", + Resource: "serviceaccounts", + Name: "sa-abc123", + }, + Permissions: []iamv0alpha1.ResourcePermissionspecPermission{ + {Kind: iamv0alpha1.ResourcePermissionSpecPermissionKindUser, Name: "user-uid-xyz", Verb: "view"}, + }, + }, + }, + wantErr: true, + }, + { + name: "serviceaccount with User kind and edit verb - should pass", + obj: &iamv0alpha1.ResourcePermission{ + ObjectMeta: v1.ObjectMeta{Name: "iam.grafana.app-serviceaccounts-sa-abc123"}, + Spec: iamv0alpha1.ResourcePermissionSpec{ + Resource: iamv0alpha1.ResourcePermissionspecResource{ + ApiGroup: "iam.grafana.app", + Resource: "serviceaccounts", + Name: "sa-abc123", + }, + Permissions: []iamv0alpha1.ResourcePermissionspecPermission{ + {Kind: iamv0alpha1.ResourcePermissionSpecPermissionKindUser, Name: "user-uid-xyz", Verb: "edit"}, + }, + }, + }, + wantErr: false, + }, + { + name: "serviceaccount with Team kind and admin verb - should pass", + obj: &iamv0alpha1.ResourcePermission{ + ObjectMeta: v1.ObjectMeta{Name: "iam.grafana.app-serviceaccounts-sa-abc123"}, + Spec: iamv0alpha1.ResourcePermissionSpec{ + Resource: iamv0alpha1.ResourcePermissionspecResource{ + ApiGroup: "iam.grafana.app", + Resource: "serviceaccounts", + Name: "sa-abc123", + }, + Permissions: []iamv0alpha1.ResourcePermissionspecPermission{ + {Kind: iamv0alpha1.ResourcePermissionSpecPermissionKindTeam, Name: "team-uid-abc", Verb: "admin"}, + }, + }, + }, + wantErr: false, + }, + { + name: "folder with BasicRole kind - should still pass (no kind restriction)", + obj: &iamv0alpha1.ResourcePermission{ + ObjectMeta: v1.ObjectMeta{Name: "folder.grafana.app-folders-test_folder"}, + Spec: iamv0alpha1.ResourcePermissionSpec{ + Resource: iamv0alpha1.ResourcePermissionspecResource{ + ApiGroup: "folder.grafana.app", + Resource: "folders", + Name: "test_folder", + }, + Permissions: []iamv0alpha1.ResourcePermissionspecPermission{ + {Kind: iamv0alpha1.ResourcePermissionSpecPermissionKindBasicRole, Name: "Editor", Verb: "edit"}, + }, + }, + }, + wantErr: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := ValidateCreateAndUpdateInput(context.Background(), test.obj, mappers) + if test.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + func TestValidateDeleteInput(t *testing.T) { tests := []struct { name string
← Back to Alerts View on GitHub →