Files
2025-05-15 21:36:52 +02:00

302 lines
9.1 KiB
Go

package test
import (
"context"
"fmt"
"net/http"
"testing"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/stretchr/testify/require"
)
// RunStorageServerTest runs the storage server test suite
func RunStorageServerTest(t *testing.T, newBackend NewBackendFunc) {
runTestResourcePermissionScenarios(t, newBackend(context.Background()), GenerateRandomNSPrefix())
}
// func runTestIntegrationBackendHappyPath(t *testing.T, backend resource.StorageBackend, nsPrefix string) {
func runTestResourcePermissionScenarios(t *testing.T, backend resource.StorageBackend, nsPrefix string) {
// Test user
testUser := &identity.StaticRequester{
Type: types.TypeUser,
Login: "testuser",
UserID: 123,
UserUID: "u123",
OrgRole: identity.RoleAdmin,
IsGrafanaAdmin: true,
}
testCases := []struct {
name string
initialFolder string
targetFolder string
permissionMap map[string]bool
expectSuccess bool
expectedChecks int
expectedError int32 // HTTP status code for error, 0 if no error expected
}{
{
name: "Create resource in folder",
initialFolder: "folder1",
targetFolder: "", // No move, just create
permissionMap: map[string]bool{"folder1:create": true},
expectSuccess: true,
expectedChecks: 1,
expectedError: 0,
},
{
name: "Create resource denied",
initialFolder: "folder1",
targetFolder: "", // No move, just create
permissionMap: map[string]bool{"folder1:create": false},
expectSuccess: false,
expectedChecks: 1,
expectedError: http.StatusForbidden,
},
{
name: "Update resource in same folder",
initialFolder: "folder1",
targetFolder: "folder1", // Same folder, just update
permissionMap: map[string]bool{"folder1:update": true},
expectSuccess: true,
expectedChecks: 1,
expectedError: 0,
},
{
name: "Update resource denied",
initialFolder: "folder1",
targetFolder: "folder1", // Same folder
permissionMap: map[string]bool{"folder1:update": false},
expectSuccess: false,
expectedChecks: 1,
expectedError: http.StatusForbidden,
},
{
name: "Move resource to another folder - allowed",
initialFolder: "folder1",
targetFolder: "folder2", // Moving to a different folder
permissionMap: map[string]bool{"folder1:update": true, "folder2:create": true},
expectSuccess: true,
expectedChecks: 2, // Should check both folders
expectedError: 0,
},
{
name: "Move resource - source folder access denied",
initialFolder: "folder1",
targetFolder: "folder2",
permissionMap: map[string]bool{"folder1:update": false, "folder2:create": true},
expectSuccess: false,
expectedChecks: 1, // Should stop at first check
expectedError: http.StatusForbidden,
},
{
name: "Move resource - destination folder access denied",
initialFolder: "folder1",
targetFolder: "folder2",
permissionMap: map[string]bool{"folder1:update": true, "folder2:create": false},
expectSuccess: false,
expectedChecks: 2, // Should do both checks but fail on second
expectedError: http.StatusForbidden,
},
{
name: "Move resource - from empty folder to named folder",
initialFolder: "",
targetFolder: "folder1",
permissionMap: map[string]bool{":update": true, "folder1:create": true},
expectSuccess: true,
expectedChecks: 2,
expectedError: 0,
},
}
for i, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a unique resource name for each test
resourceName := fmt.Sprintf("test-resource-%d", i)
resourceUID := fmt.Sprintf("test123-%d", i)
// Create a mock access client with the test case's permission map
checksPerformed := []types.CheckRequest{}
mockAccess := &mockAccessClient{
allowed: false, // Default to false
allowedMap: tc.permissionMap,
checkFn: func(req types.CheckRequest) {
checksPerformed = append(checksPerformed, req)
},
}
server, err := resource.NewResourceServer(resource.ResourceServerOptions{
Backend: backend,
AccessClient: mockAccess,
})
require.NoError(t, err)
ctx := types.WithAuthInfo(context.Background(), testUser)
key := &resourcepb.ResourceKey{
Group: "test.grafana.app",
Resource: "testresources",
Namespace: nsPrefix + "-ns1",
Name: resourceName,
}
if tc.targetFolder == "" {
// Create resource with unique name
resourceJSON := fmt.Sprintf(`{
"apiVersion": "test.grafana.app/v1",
"kind": "TestResource",
"metadata": {
"name": "%s",
"uid": "%s",
"namespace": "%s",
"annotations": {
"grafana.app/folder": "%s"
}
},
"spec": {
"title": "Test Resource %d"
}
}`, resourceName, resourceUID, nsPrefix+"-ns1", tc.initialFolder, i)
checksPerformed = []types.CheckRequest{}
created, err := server.Create(ctx, &resourcepb.CreateRequest{
Value: []byte(resourceJSON),
Key: key,
})
require.NoError(t, err)
if tc.expectSuccess {
require.Nil(t, created.Error)
require.True(t, created.ResourceVersion > 0)
} else {
require.NotNil(t, created.Error)
require.Equal(t, tc.expectedError, created.Error.Code)
}
require.Len(t, checksPerformed, tc.expectedChecks)
if len(checksPerformed) > 0 {
require.Equal(t, utils.VerbCreate, checksPerformed[0].Verb)
require.Equal(t, tc.initialFolder, checksPerformed[0].Folder)
}
} else {
// Create a resource first, then update/move it
initialResourceJSON := fmt.Sprintf(`{
"apiVersion": "test.grafana.app/v1",
"kind": "TestResource",
"metadata": {
"name": "%s",
"uid": "%s",
"namespace": "%s",
"annotations": {
"grafana.app/folder": "%s"
}
},
"spec": {
"title": "Test Resource %d"
}
}`, resourceName, resourceUID, nsPrefix+"-ns1", tc.initialFolder, i)
// Override permissions for initial creation to always succeed
mockAccess.allowed = true
created, err := server.Create(ctx, &resourcepb.CreateRequest{
Value: []byte(initialResourceJSON),
Key: key,
})
require.NoError(t, err)
require.Nil(t, created.Error)
// Now try the update/move with the configured permissions
targetResourceJSON := fmt.Sprintf(`{
"apiVersion": "test.grafana.app/v1",
"kind": "TestResource",
"metadata": {
"name": "%s",
"uid": "%s",
"namespace": "%s",
"annotations": {
"grafana.app/folder": "%s"
}
},
"spec": {
"title": "Test Resource %d Updated"
}
}`, resourceName, resourceUID, nsPrefix+"-ns1", tc.targetFolder, i)
mockAccess.allowed = false // Reset to use the map
checksPerformed = []types.CheckRequest{}
updated, err := server.Update(ctx, &resourcepb.UpdateRequest{
Key: key,
Value: []byte(targetResourceJSON),
ResourceVersion: created.ResourceVersion,
})
require.NoError(t, err)
if tc.expectSuccess {
require.Nil(t, updated.Error)
require.True(t, updated.ResourceVersion > created.ResourceVersion)
} else {
require.NotNil(t, updated.Error)
require.Equal(t, tc.expectedError, updated.Error.Code)
}
require.Len(t, checksPerformed, tc.expectedChecks)
// Verify the correct permission checks were made
if tc.initialFolder != tc.targetFolder && len(checksPerformed) >= 2 {
// This is a folder move operation
require.Equal(t, utils.VerbUpdate, checksPerformed[0].Verb)
require.Equal(t, tc.initialFolder, checksPerformed[0].Folder)
require.Equal(t, utils.VerbCreate, checksPerformed[1].Verb)
require.Equal(t, tc.targetFolder, checksPerformed[1].Folder)
} else if len(checksPerformed) > 0 {
// Regular update, no folder change
require.Equal(t, utils.VerbUpdate, checksPerformed[0].Verb)
require.Equal(t, tc.initialFolder, checksPerformed[0].Folder)
}
}
})
}
}
// Mock access client for testing
type mockAccessClient struct {
allowed bool
allowedMap map[string]bool
checkFn func(types.CheckRequest)
}
func (m *mockAccessClient) Check(ctx context.Context, user types.AuthInfo, req types.CheckRequest) (types.CheckResponse, error) {
if m.checkFn != nil {
m.checkFn(req)
}
// Check specific folder:verb mappings if provided
if m.allowedMap != nil {
key := fmt.Sprintf("%s:%s", req.Folder, req.Verb)
if allowed, exists := m.allowedMap[key]; exists {
return types.CheckResponse{Allowed: allowed}, nil
}
}
return types.CheckResponse{Allowed: m.allowed}, nil
}
func (m *mockAccessClient) Compile(ctx context.Context, user types.AuthInfo, req types.ListRequest) (types.ItemChecker, error) {
return func(name, folder string) bool {
key := fmt.Sprintf("%s:%s", folder, req.Verb)
if allowed, exists := m.allowedMap[key]; exists {
return allowed
}
return m.allowed
}, nil
}