Merge remote-tracking branch 'origin/main' into ds-apiserver-with-configs

This commit is contained in:
Ryan McKinley
2025-06-30 13:47:39 -07:00
330 changed files with 8259 additions and 4001 deletions
@@ -2419,68 +2419,6 @@ func runDashboardListTest(t *testing.T, ctx TestContext) {
})
}
// TODO: this only works on mode0-3 right now. In modes 4/5, we need to start returning the connections endpoint
// from retrieving the panel count from search / indexing the dashboard library panels
func TestDashboardWithLibraryPanel(t *testing.T) {
dualWriterModes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3}
for _, dualWriterMode := range dualWriterModes {
t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) {
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
DisableAnonymous: true,
EnableFeatureToggles: []string{
"unifiedStorageSearch",
"kubernetesClientDashboardsFolders",
},
})
ctx := createTestContext(t, helper, helper.Org1, dualWriterMode)
adminClient := getResourceClient(t, ctx.Helper, ctx.AdminUser, getDashboardGVR())
// create the library element first
libraryElement := map[string]interface{}{
"kind": 1,
"name": "Test Library Panel",
"model": map[string]interface{}{
"type": "timeseries",
"title": "Test Library Panel",
},
}
libraryElementURL := "/api/library-elements"
libraryElementData, err := postHelper(t, &ctx, libraryElementURL, libraryElement, ctx.AdminUser)
require.NoError(t, err)
require.NotNil(t, libraryElementData)
data := libraryElementData["result"].(map[string]interface{})
uid := data["uid"].(string)
require.NotEmpty(t, uid)
// then reference the library element in the dashboard
dashboard := createDashboardObject(t, "Library Panel Test", "", 1)
dashboard.Object["spec"].(map[string]interface{})["panels"] = []interface{}{
map[string]interface{}{
"id": 1,
"title": "Library Panel",
"type": "library-panel-ref",
"libraryPanel": map[string]interface{}{
"uid": uid,
"name": "Test Library Panel",
},
},
}
createdDash, err := adminClient.Resource.Create(context.Background(), dashboard, v1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, createdDash)
// should have created a library panel connection
connectionsURL := fmt.Sprintf("/api/library-elements/%s/connections", uid)
connectionsData, err := getDashboardViaHTTP(t, &ctx, connectionsURL, ctx.AdminUser)
require.NoError(t, err)
require.NotNil(t, connectionsData)
connections := connectionsData["result"].([]interface{})
require.Len(t, connections, 1)
})
}
}
func postHelper(t *testing.T, ctx *TestContext, path string, body interface{}, user apis.User) (map[string]interface{}, error) {
bodyJSON, err := json.Marshal(body)
require.NoError(t, err)
@@ -0,0 +1,288 @@
package integration
import (
"context"
"fmt"
"net/http"
"testing"
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
"github.com/stretchr/testify/require"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
)
// this tests the /api path still, but behind the scenes is using search to get the library connections
// as in modes 3+, the connections are found via searching dashboards for the reference of the library panel
//
// it also ensures we create the connection in modes 0-2 if a dashboard v1 is created with a reference
func TestIntegrationLibraryPanelConnections(t *testing.T) {
dualWriterModes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5}
for _, dualWriterMode := range dualWriterModes {
t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) {
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
DisableAnonymous: true,
EnableFeatureToggles: []string{
"unifiedStorageSearch",
"kubernetesClientDashboardsFolders",
"kubernetesLibraryPanels",
},
})
ctx := createTestContext(t, helper, helper.Org1, dualWriterMode)
adminClient := getResourceClient(t, ctx.Helper, ctx.AdminUser, getDashboardGVR())
// create the library element first
libraryElement := map[string]interface{}{
"kind": 1,
"name": "Test Library Panel",
"model": map[string]interface{}{
"type": "timeseries",
"title": "Test Library Panel",
},
}
libraryElementURL := "/api/library-elements"
libraryElementData, err := postHelper(t, &ctx, libraryElementURL, libraryElement, ctx.AdminUser)
require.NoError(t, err)
require.NotNil(t, libraryElementData)
data := libraryElementData["result"].(map[string]interface{})
uid := data["uid"].(string)
require.NotEmpty(t, uid)
// then reference the library element in the dashboard
dashboard := createDashboardObject(t, "Library Panel Test", "", 1)
dashboard.Object["spec"].(map[string]interface{})["panels"] = []interface{}{
map[string]interface{}{
"id": 1,
"title": "Library Panel",
"type": "library-panel-ref",
"libraryPanel": map[string]interface{}{
"uid": uid,
"name": "Test Library Panel",
},
},
}
createdDash, err := adminClient.Resource.Create(context.Background(), dashboard, v1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, createdDash)
// should have created a library panel connection
connectionsURL := fmt.Sprintf("/api/library-elements/%s/connections", uid)
connectionsData, err := getDashboardViaHTTP(t, &ctx, connectionsURL, ctx.AdminUser)
require.NoError(t, err)
require.NotNil(t, connectionsData)
connections := connectionsData["result"].([]interface{})
require.Len(t, connections, 1)
})
}
}
// this tests the /apis path to ensure authorization is being enforced. /api integration tests are within the service package
// only works in modes 0-2 because the library element is created through the /api path
func TestIntegrationLibraryElementPermissions(t *testing.T) {
dualWriterModes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2}
for _, dualWriterMode := range dualWriterModes {
t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) {
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
DisableAnonymous: true,
EnableFeatureToggles: []string{
"unifiedStorageSearch",
"kubernetesClientDashboardsFolders",
"kubernetesLibraryPanels",
"grafanaAPIServerWithExperimentalAPIs", // needed until we move it to v0beta1 at least (currently v0alpha1)
},
})
ctx := createTestContext(t, helper, helper.Org1, dualWriterMode)
t.Run("Library element authorization tests", func(t *testing.T) {
runLibraryElementAuthorizationTests(t, ctx)
})
t.Run("Library element cross-organization tests", func(t *testing.T) {
org2Ctx := createTestContext(t, helper, helper.OrgB, dualWriterMode)
runLibraryElementCrossOrgTests(t, ctx, org2Ctx)
})
})
}
}
func runLibraryElementAuthorizationTests(t *testing.T, ctx TestContext) {
t.Helper()
roles := []string{"Admin", "Editor", "Viewer"}
for _, role := range roles {
t.Run(role, func(t *testing.T) {
var client *apis.K8sResourceClient
switch role {
case "Admin":
client = getResourceClient(t, ctx.Helper, ctx.AdminUser, getLibraryElementGVR())
case "Editor":
client = getResourceClient(t, ctx.Helper, ctx.EditorUser, getLibraryElementGVR())
case "Viewer":
client = getResourceClient(t, ctx.Helper, ctx.ViewerUser, getLibraryElementGVR())
}
t.Run("library element viewing", func(t *testing.T) {
uid, err := createLibraryElement(t, ctx, ctx.AdminUser, "Library Element for "+role+" to view", "", nil)
require.NoError(t, err)
viewedLibElement, err := client.Resource.Get(context.Background(), uid, v1.GetOptions{})
require.NoError(t, err, "All identities should be able to view library elements")
require.NotNil(t, viewedLibElement)
err = deleteLibraryElement(t, ctx, ctx.AdminUser, uid)
require.NoError(t, err)
})
t.Run("library element listing", func(t *testing.T) {
uid, err := createLibraryElement(t, ctx, ctx.AdminUser, "Library Element for "+role+" to list", "", nil)
require.NoError(t, err)
listOpts := v1.ListOptions{}
libElementList, err := client.Resource.List(context.Background(), listOpts)
require.NoError(t, err, "All identities should be able to list library elements")
require.NotNil(t, libElementList)
require.Len(t, libElementList.Items, 1)
require.Equal(t, uid, libElementList.Items[0].GetName())
err = deleteLibraryElement(t, ctx, ctx.AdminUser, uid)
require.NoError(t, err)
})
t.Run("library element in a folder only an admin can see", func(t *testing.T) {
restrictedFolder, err := createFolder(t, ctx.Helper, ctx.AdminUser, "Restricted Folder for "+role)
require.NoError(t, err)
require.NotNil(t, restrictedFolder)
uid, err := createLibraryElement(t, ctx, ctx.AdminUser, "Library Element in restricted folder", restrictedFolder.UID, nil)
require.NoError(t, err)
setResourceUserPermission(t, ctx, ctx.AdminUser, false, restrictedFolder.UID, []ResourcePermissionSetting{})
if role == "Admin" {
_, err = client.Resource.Get(context.Background(), uid, v1.GetOptions{})
require.NoError(t, err, "Admin should be able to access library element in restricted folder")
} else {
_, err = client.Resource.Get(context.Background(), uid, v1.GetOptions{})
require.Error(t, err, "Should not be able to access library element in restricted folder")
}
err = deleteLibraryElement(t, ctx, ctx.AdminUser, uid)
require.NoError(t, err)
})
})
}
}
func runLibraryElementCrossOrgTests(t *testing.T, org1Ctx, org2Ctx TestContext) {
// org1 trying to access org2
org1CrossEditorClient := org2Ctx.Helper.GetResourceClient(apis.ResourceClientArgs{
User: org1Ctx.EditorUser,
Namespace: org2Ctx.Helper.Namespacer(org2Ctx.OrgID),
GVR: getLibraryElementGVR(),
})
org1CrossViewerClient := org2Ctx.Helper.GetResourceClient(apis.ResourceClientArgs{
User: org1Ctx.ViewerUser,
Namespace: org2Ctx.Helper.Namespacer(org2Ctx.OrgID),
GVR: getLibraryElementGVR(),
})
// org2 trying to access org1
org2CrossEditorClient := org1Ctx.Helper.GetResourceClient(apis.ResourceClientArgs{
User: org2Ctx.EditorUser,
Namespace: org1Ctx.Helper.Namespacer(org1Ctx.OrgID),
GVR: getLibraryElementGVR(),
})
org2CrossViewerClient := org1Ctx.Helper.GetResourceClient(apis.ResourceClientArgs{
User: org2Ctx.ViewerUser,
Namespace: org1Ctx.Helper.Namespacer(org1Ctx.OrgID),
GVR: getLibraryElementGVR(),
})
t.Run("Cross-organization access", func(t *testing.T) {
org1LibElementUID, err := createLibraryElement(t, org1Ctx, org1Ctx.AdminUser, "Org1 Library Element", "", nil)
require.NoError(t, err)
org2LibElementUID, err := createLibraryElement(t, org2Ctx, org2Ctx.AdminUser, "Org2 Library Element", "", nil)
require.NoError(t, err)
defer func() {
err = deleteLibraryElement(t, org1Ctx, org1Ctx.AdminUser, org1LibElementUID)
require.NoError(t, err)
err = deleteLibraryElement(t, org2Ctx, org2Ctx.AdminUser, org2LibElementUID)
require.NoError(t, err)
}()
testCrossOrgAccess := func(client *apis.K8sResourceClient, targetLibElementUID string, description string) {
t.Run(description, func(t *testing.T) {
_, err := client.Resource.Get(context.Background(), targetLibElementUID, v1.GetOptions{})
require.Error(t, err, "Should not be able to access library element from another org")
})
}
testCrossOrgAccess(org1CrossEditorClient, org2LibElementUID, "Org1 editor cannot access Org2 library element")
testCrossOrgAccess(org1CrossViewerClient, org2LibElementUID, "Org1 viewer cannot access Org2 library element")
testCrossOrgAccess(org2CrossEditorClient, org1LibElementUID, "Org2 editor cannot access Org1 library element")
testCrossOrgAccess(org2CrossViewerClient, org1LibElementUID, "Org2 viewer cannot access Org1 library element")
})
}
func getLibraryElementGVR() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: dashboardV0.APIGroup,
Version: dashboardV0.VERSION,
Resource: dashboardV0.LIBRARY_PANEL_RESOURCE,
}
}
// currently through /api
func createLibraryElement(t *testing.T, ctx TestContext, user apis.User, title string, folderUID string, uid *string) (string, error) {
t.Helper()
libraryElement := map[string]interface{}{
"kind": 1,
"name": title,
"model": map[string]interface{}{
"type": "text",
"title": title,
},
}
if folderUID != "" {
libraryElement["folderUid"] = folderUID
}
libraryElementURL := "/api/library-elements"
libraryElementData, err := postHelper(t, &ctx, libraryElementURL, libraryElement, user)
if err != nil {
return "", err
}
require.NotNil(t, libraryElementData)
data := libraryElementData["result"].(map[string]interface{})
uidStr := data["uid"].(string)
require.NotEmpty(t, uidStr)
return uidStr, nil
}
// currently through /api
func deleteLibraryElement(t *testing.T, ctx TestContext, user apis.User, uid string) error {
t.Helper()
deleteURL := fmt.Sprintf("/api/library-elements/%s", uid)
resp := apis.DoRequest(ctx.Helper, apis.RequestParams{
User: user,
Method: http.MethodDelete,
Path: deleteURL,
}, &struct{}{})
if resp.Response.StatusCode != http.StatusOK {
return fmt.Errorf("failed to delete library element: %s", resp.Response.Status)
}
return nil
}
+3 -6
View File
@@ -16,7 +16,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/dtos"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
@@ -583,7 +582,6 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Skip("not working yet")
folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}"
folderWithParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\", \"parentUid\": \"parentuid\"}"
@@ -708,7 +706,6 @@ func TestIntegrationFolderGetPermissions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Skip("not yet working")
type testCase struct {
description string
@@ -735,10 +732,10 @@ func TestIntegrationFolderGetPermissions(t *testing.T) {
checkAccessControl: true,
},
{
description: "get folder by UID should return parent folders redacted if nested folder are enabled and user does not have read access to parent folders",
description: "get folder by UID should not return parent folders if nested folder are enabled and user does not have read access to parent folders",
expectedCode: http.StatusOK,
expectedParentUIDs: []string{api.REDACTED},
expectedParentTitles: []string{api.REDACTED},
expectedParentUIDs: []string{},
expectedParentTitles: []string{},
permissions: []resourcepermissions.SetResourcePermissionCommand{
{
Actions: []string{dashboards.ActionFoldersRead},