From a276557b0a8a66a1843dc578e23dd79f7f18562a Mon Sep 17 00:00:00 2001 From: Gonzalo Trigueros <242162051+gttrigger@users.noreply.github.com> Date: Wed, 14 Jan 2026 09:17:37 +0100 Subject: [PATCH] provisioning: add integration tests to avoid managed folder permission changes --- .../provisioning/folderpermissions_test.go | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 pkg/tests/apis/provisioning/folderpermissions_test.go diff --git a/pkg/tests/apis/provisioning/folderpermissions_test.go b/pkg/tests/apis/provisioning/folderpermissions_test.go new file mode 100644 index 00000000000..a16bbf15f0e --- /dev/null +++ b/pkg/tests/apis/provisioning/folderpermissions_test.go @@ -0,0 +1,193 @@ +package provisioning + +import ( + "fmt" + "net/http" + "testing" + "time" + + foldersV1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/util/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" +) + +// We currently block permission updates for folders managed by provisioning. +func TestIntegrationFolderPermissions_ProvisionedFolders(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + helper.CreateRepo(t, TestRepo{ + Name: "test-repo", + Target: "folder", + ExpectedFolders: 1, + }) + + t.Run("should fail to update permissions for provisioned folder", func(t *testing.T) { + folders, err := helper.Folders.Resource.List(t.Context(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, folders.Items, 1) + + managedFolderName := folders.Items[0].GetName() + permissionsPayload := map[string]interface{}{ + "items": []map[string]interface{}{ + { + "role": "Viewer", + "permission": 1, // View permission + }, + }, + } + permissionsURL := fmt.Sprintf("/api/folders/%s/permissions", managedFolderName) + permissionsData, code, err := postHelper(t, *helper.K8sTestHelper, permissionsURL, permissionsPayload, helper.Org1.Admin) + require.Error(t, err) + require.Equal(t, http.StatusForbidden, code) + require.NotNil(t, permissionsData) + require.Equal(t, "Cannot update permissions for folders managed by provisioning.", permissionsData["message"]) + }) + + t.Run("should fail to update permissions for nested provisioned folder", func(t *testing.T) { + // Create a new repo with nested folder structure + nestedHelper := runGrafana(t) + nestedHelper.CreateRepo(t, TestRepo{ + Name: "nested-folder-repo", + Target: "instance", + Copies: map[string]string{ + "testdata/all-panels.json": "folder/subfolder/dashboard.json", + }, + SkipResourceAssertions: true, + }) + + folders, err := nestedHelper.Folders.Resource.List(t.Context(), metav1.ListOptions{}) + require.NoError(t, err) + require.GreaterOrEqual(t, len(folders.Items), 2, "should have at least 2 folders (root and nested)") + + // Find the nested folder by checking for sourcePath annotation + // The root folder has sourcePath equal to the repo name, nested folders have paths like "folder" or "folder/subfolder" + var nestedFolder *unstructured.Unstructured + repoName := "nested-folder-repo" + for i := range folders.Items { + sourcePath, found, _ := unstructured.NestedString(folders.Items[i].Object, "metadata", "annotations", utils.AnnoKeySourcePath) + if found && sourcePath != "" && sourcePath != repoName { + // This is a nested folder (not the root folder) + nestedFolder = &folders.Items[i] + break + } + } + require.NotNil(t, nestedFolder, "should find a nested folder") + + nestedFolderName := nestedFolder.GetName() + require.Contains(t, nestedFolder.GetAnnotations(), utils.AnnoKeyManagerKind, "nested folder should be managed") + require.Contains(t, nestedFolder.GetAnnotations(), utils.AnnoKeyManagerIdentity, "nested folder should be managed") + + permissionsPayload := map[string]interface{}{ + "items": []map[string]interface{}{ + { + "role": "Viewer", + "permission": 1, // View permission + }, + }, + } + permissionsURL := fmt.Sprintf("/api/folders/%s/permissions", nestedFolderName) + permissionsData, code, err := postHelper(t, *nestedHelper.K8sTestHelper, permissionsURL, permissionsPayload, nestedHelper.Org1.Admin) + require.Error(t, err) + require.Equal(t, http.StatusForbidden, code) + require.NotNil(t, permissionsData) + require.Equal(t, "Cannot update permissions for folders managed by provisioning.", permissionsData["message"]) + }) +} + +func TestIntegrationFolderPermissions_UnprovisionedFolders(t *testing.T) { + const repo = "test-repo" + helper := runGrafana(t) + helper.CreateRepo(t, TestRepo{ + Name: repo, + Target: "folder", + ExpectedFolders: 1, + }) + + t.Run("should update permissions when folder is released", func(t *testing.T) { + folders, err := helper.Folders.Resource.List(t.Context(), metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, folders.Items, 1) + managedFolderName := folders.Items[0].GetName() + require.Contains(t, folders.Items[0].GetAnnotations(), utils.AnnoKeyManagerKind, "folder should be managed") + require.Contains(t, folders.Items[0].GetAnnotations(), utils.AnnoKeyManagerIdentity, "folder should be managed") + + _, err = helper.Repositories.Resource.Patch(t.Context(), repo, types.JSONPatchType, []byte(`[ + { + "op": "replace", + "path": "/metadata/finalizers", + "value": ["cleanup", "release-orphan-resources"] + } + ]`), metav1.PatchOptions{}) + require.NoError(t, err, "should successfully patch finalizers") + + require.NoError(t, helper.Repositories.Resource.Delete(t.Context(), repo, metav1.DeleteOptions{})) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := helper.Repositories.Resource.Get(t.Context(), repo, metav1.GetOptions{}) + assert.True(collect, apierrors.IsNotFound(err), "repository should be deleted") + }, time.Second*10, time.Millisecond*50, "repository should be deleted") + require.EventuallyWithT(t, func(collect *assert.CollectT) { + foundFolders, err := helper.Folders.Resource.List(t.Context(), metav1.ListOptions{}) + require.NoError(t, err, "can list values") + for _, v := range foundFolders.Items { + assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeyManagerKind) + assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeyManagerIdentity) + assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeySourcePath) + assert.NotContains(t, v.GetAnnotations(), utils.AnnoKeySourceChecksum) + } + }, time.Second*20, time.Millisecond*10, "Expected folders to be released") + + permissionsPayload := map[string]interface{}{ + "items": []map[string]interface{}{ + { + "role": "Viewer", + "permission": 1, // View permission + }, + }, + } + permissionsURL := fmt.Sprintf("/api/folders/%s/permissions", managedFolderName) + permissionsData, code, err := postHelper(t, *helper.K8sTestHelper, permissionsURL, permissionsPayload, helper.Org1.Admin) + require.NoError(t, err) + require.Equal(t, http.StatusOK, code) + require.NotNil(t, permissionsData) + }) + + t.Run("should update permissions for unmanaged folder", func(t *testing.T) { + unmanagedFolder := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": foldersV1.FolderResourceInfo.GroupVersion().String(), + "kind": foldersV1.FolderResourceInfo.GroupVersionKind().Kind, + "metadata": map[string]interface{}{ + "generateName": "test-folder-", + }, + "spec": map[string]interface{}{ + "title": "Unmanaged Folder", + }, + }, + } + createdFolder, err := helper.Folders.Resource.Create(t.Context(), unmanagedFolder, metav1.CreateOptions{}) + require.NoError(t, err) + require.NotNil(t, createdFolder) + + unmanagedFolderName := createdFolder.GetName() + permissionsPayload := map[string]interface{}{ + "items": []map[string]interface{}{ + { + "role": "Editor", + "permission": 2, // Edit permission + }, + }, + } + permissionsURL := fmt.Sprintf("/api/folders/%s/permissions", unmanagedFolderName) + permissionsData, code, err := postHelper(t, *helper.K8sTestHelper, permissionsURL, permissionsPayload, helper.Org1.Admin) + require.NoError(t, err) + require.Equal(t, http.StatusOK, code) + require.NotNil(t, permissionsData) + }) +}