RBAC / Authorization misconfiguration
Description
This commit implements a real security fix: when creating folders via Grafana's app platform Kubernetes-based folder API, root-level folders are now assigned the grafana.app/grant-permissions annotation with the value 'default'. This ensures that Kubernetes RBAC default permissions are written for those resources. Prior to this change, folders created through the app platform API at root could lack the grant-permissions annotation, potentially leading to misconfigured authorization and either exposure of resources or unintended access due to missing default RBAC grants.
Proof of Concept
Prerequisites: Grafana running with app platform folder API enabled and the folder API accessible at /apis/folder.grafana.app/v1beta1. You must have credentials/tokens with permission to create folders in a namespace (e.g., default). The PoC demonstrates the behavior before the fix by showing the absence of the grant-permissions annotation on a root-level folder created via the app platform API.
1) Create a root-level folder via the app platform API (without grant-permissions annotation)
curl -s -X POST -H "Content-Type: application/json" \
-d '{"apiVersion":"folder.grafana.app/v1beta1","kind":"Folder","metadata":{"name":"root-folder","namespace":"default"},"spec":{"title":"Root Folder"}}' \
https://grafana.example/apis/folder.grafana.app/v1beta1/namespaces/default/folders \
--insecure
# Expect HTTP 200/201 with a response body containing the Folder object.
2) Verify the created folder annotations do not include the grant-permissions key
curl -s -H "Authorization: Bearer <TOKEN>" \
https://grafana.example/apis/folder.grafana.app/v1beta1/namespaces/default/folders/root-folder | jq '.metadata.annotations'
# Expected: Annotations may include grafana.app/folder or grafana.app/grant-permissions is absent.
3) (Optional) Attempt an operation that relies on kubernetesAuthzResourcePermissionApis defaults and observe lack of default RBAC permissions provisioning due to missing annotation.
# This depends on cluster setup; you may observe failures or insufficient permissions when trying to access resources governed by those default permissions.
4) Compare with post-fix behavior (after applying the patch in this PR):
The same POST request should result in the annotation grafana.app/grant-permissions set to 'default' on the root folder, e.g.
curl -s -H "Authorization: Bearer <TOKEN>" \
https://grafana.example/apis/folder.grafana.app/v1beta1/namespaces/default/folders/root-folder | jq '.metadata.annotations'
# Expected: {"grafana.app/grant-permissions":"default"} indicating default RBAC permissions will be applied by the Kubernetes authorization layer.
Commit Details
Author: Georges Chaudy
Date: 2026-07-01 10:47 UTC
Message:
Folders: grant default permissions on app platform folder create (#127631)
When foldersAppPlatformAPI is enabled the UI creates folders via the K8s
folder API instead of POST /api/folders. Root-level creates must include
the ephemeral grafana.app/grant-permissions annotation so default RBAC
permissions are written under kubernetesAuthzResourcePermissionApis.
Fixes grafana/grafana#126964
Triage Assessment
Vulnerability Type: Authorization (RBAC) / Access Control
Confidence: HIGH
Reasoning:
The commit ensures that when creating folders via the app platform API, a grant-permissions annotation is applied to root folders so default Kubernetes RBAC permissions are properly written. This addresses potential misconfigurations in authorization that could expose resources or grant unintended access, effectively hardening access control.
Verification Assessment
Vulnerability Type: RBAC / Authorization misconfiguration
Confidence: HIGH
Affected Versions: 12.4.0 and earlier (pre-fix)
Code Diff
diff --git a/public/app/api/clients/folder/v1beta1/hooks.test.ts b/public/app/api/clients/folder/v1beta1/hooks.test.ts
index d35d27fc69ac9..1c937f4e12b6a 100644
--- a/public/app/api/clients/folder/v1beta1/hooks.test.ts
+++ b/public/app/api/clients/folder/v1beta1/hooks.test.ts
@@ -12,7 +12,7 @@ import {
useMoveFoldersMutation as useMoveFoldersMutationLegacy,
} from 'app/features/browse-dashboards/api/browseDashboardsAPI';
-import { AnnoKeyFolder } from '../../../../features/apiserver/types';
+import { AnnoKeyFolder, AnnoKeyGrantPermissions } from '../../../../features/apiserver/types';
import {
useGetFolderQueryFacade,
@@ -102,6 +102,28 @@ const setupUpdateFolderHandler = (onPatch?: jest.Mock) => {
);
};
+const setupCreateFolderHandler = (onCreate?: jest.Mock) => {
+ folderAPIVersionResolver.set('v1beta1');
+ server.use(
+ http.post('/apis/folder.grafana.app/v1beta1/namespaces/:namespace/folders', async ({ request }) => {
+ const body = await request.json();
+ onCreate?.(body);
+
+ return HttpResponse.json({
+ apiVersion: 'folder.grafana.app/v1beta1',
+ kind: 'Folder',
+ metadata: {
+ name: 'new-folder-uid',
+ generation: 1,
+ },
+ spec: {
+ title: body && typeof body === 'object' && 'spec' in body ? (body.spec?.title ?? 'test') : 'test',
+ },
+ });
+ })
+ );
+};
+
const originalToggles = { ...config.featureToggles };
afterAll(() => {
// Restore the original feature toggle value changed during tests
@@ -312,6 +334,49 @@ describe.each([
expect(await screen.findByText('Folder created')).toBeInTheDocument();
expect(dispatchMockFn).toHaveBeenCalled();
});
+
+ it('sets grant-permissions annotation when creating a root folder via the app platform API', async () => {
+ if (!toggle) {
+ return;
+ }
+
+ const createSpy = jest.fn();
+ setupCreateFolderHandler(createSpy);
+ const { user } = setupCreateFolder();
+
+ await user.click(screen.getByText(/Create Folder at root/));
+
+ await waitFor(() => expect(createSpy).toHaveBeenCalled());
+ expect(createSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ metadata: expect.objectContaining({
+ annotations: { [AnnoKeyGrantPermissions]: 'default' },
+ }),
+ })
+ );
+ });
+
+ it('sets folder annotation when creating a nested folder via the app platform API', async () => {
+ if (!toggle) {
+ return;
+ }
+
+ const createSpy = jest.fn();
+ setupCreateFolderHandler(createSpy);
+ const { user } = setupCreateFolder();
+
+ await user.click(screen.getByText(/Create Folder in nested folder/));
+
+ await waitFor(() => expect(createSpy).toHaveBeenCalled());
+ expect(createSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ metadata: expect.objectContaining({
+ annotations: { [AnnoKeyFolder]: folderA.item.uid },
+ }),
+ })
+ );
+ expect(createSpy.mock.calls[0][0].metadata.annotations[AnnoKeyGrantPermissions]).toBeUndefined();
+ });
});
describe('useUpdateFolder', () => {
diff --git a/public/app/api/clients/folder/v1beta1/hooks.ts b/public/app/api/clients/folder/v1beta1/hooks.ts
index 40ed179fad0af..8e7f3aa692b36 100644
--- a/public/app/api/clients/folder/v1beta1/hooks.ts
+++ b/public/app/api/clients/folder/v1beta1/hooks.ts
@@ -35,6 +35,7 @@ import kbn from '../../../../core/utils/kbn';
import {
AnnoKeyCreatedBy,
AnnoKeyFolder,
+ AnnoKeyGrantPermissions,
AnnoKeyManagerKind,
AnnoKeyUpdatedBy,
AnnoKeyUpdatedTimestamp,
@@ -440,9 +441,10 @@ export function useCreateFolder() {
metadata: {
...partialMetadata,
generateName: 'f',
- annotations: {
- ...(folder.parentUid && { [AnnoKeyFolder]: folder.parentUid }),
- },
+ annotations:
+ folder.parentUid && !isRootFolderUID(folder.parentUid)
+ ? { [AnnoKeyFolder]: folder.parentUid }
+ : { [AnnoKeyGrantPermissions]: 'default' },
},
},
};