From 7b2ecf95dc28c04b79c48af817b169cda3d1d89d Mon Sep 17 00:00:00 2001 From: Roberto Jimenez Sanchez Date: Thu, 18 Dec 2025 08:20:26 +0100 Subject: [PATCH] tests: add authorization tests for missing provisioning API endpoints Add comprehensive authorization tests for: - Repository subresources (test, resources, history, status) - Connection status subresource - HistoricJobs resource - Settings and Stats resources All authorization paths are now covered by integration tests. --- .../provisioning/AUTHORIZATION_COVERAGE.md | 103 ++++++++ .../connection_status_auth_test.go | 89 +++++++ .../provisioning/historicjobs_auth_test.go | 96 +++++++ .../repository_subresources_auth_test.go | 237 ++++++++++++++++++ .../provisioning/settings_stats_auth_test.go | 105 ++++++++ 5 files changed, 630 insertions(+) create mode 100644 pkg/tests/apis/provisioning/AUTHORIZATION_COVERAGE.md create mode 100644 pkg/tests/apis/provisioning/connection_status_auth_test.go create mode 100644 pkg/tests/apis/provisioning/historicjobs_auth_test.go create mode 100644 pkg/tests/apis/provisioning/repository_subresources_auth_test.go create mode 100644 pkg/tests/apis/provisioning/settings_stats_auth_test.go diff --git a/pkg/tests/apis/provisioning/AUTHORIZATION_COVERAGE.md b/pkg/tests/apis/provisioning/AUTHORIZATION_COVERAGE.md new file mode 100644 index 00000000000..9b3fa5dfc56 --- /dev/null +++ b/pkg/tests/apis/provisioning/AUTHORIZATION_COVERAGE.md @@ -0,0 +1,103 @@ +# Provisioning API Authorization Test Coverage Analysis + +This document analyzes test coverage for all authorization paths in the provisioning API. + +## Authorization Matrix + +| Resource | Subresource | Verb | Fallback Role | Test File | Test Function | Coverage Status | +|----------|-------------|------|---------------|-----------|---------------|-----------------| +| **Repositories** | (CRUD) | create | Admin | `repository_test.go` | `TestIntegrationProvisioning_CreatingAndGetting` | ✅ Tested (viewer denied) | +| Repositories | (CRUD) | read | Admin | `repository_test.go` | `TestIntegrationProvisioning_CreatingAndGetting` | ✅ Tested (viewer denied) | +| Repositories | (CRUD) | update | Admin | `repository_test.go` | Various | ✅ Tested (implicitly) | +| Repositories | (CRUD) | delete | Admin | `repository_test.go` | `TestIntegrationProvisioning_DeleteRepositoryAndReleaseResources` | ✅ Tested | +| Repositories | test | POST | Admin | `repository_subresources_auth_test.go` | `TestIntegrationProvisioning_RepositorySubresourcesAuthorization` | ✅ Tested (admin allowed, editor/viewer denied) | +| Repositories | files | GET | Any auth (route) | `files_test.go` | `TestIntegrationProvisioning_FilesAuthorization` | ✅ Tested (viewer/editor/admin) | +| Repositories | files | POST | Any auth (route) | `files_test.go` | `TestIntegrationProvisioning_FilesAuthorization` | ✅ Tested (viewer/editor/admin) | +| Repositories | files | PUT | Any auth (route) | `files_test.go` | `TestIntegrationProvisioning_FilesAuthorization` | ✅ Tested (viewer/editor/admin) | +| Repositories | files | DELETE | Any auth (route) | `files_test.go` | `TestIntegrationProvisioning_FilesAuthorization` | ✅ Tested (viewer/editor/admin) | +| Repositories | refs | GET | Editor | `repository_test.go` | `TestIntegrationProvisioning_RefsPermissions` | ✅ Tested (editor allowed, viewer denied) | +| Repositories | resources | GET | Admin | `repository_subresources_auth_test.go` | `TestIntegrationProvisioning_RepositorySubresourcesAuthorization` | ✅ Tested (admin allowed, editor/viewer denied) | +| Repositories | history | GET | Admin | `repository_subresources_auth_test.go` | `TestIntegrationProvisioning_RepositorySubresourcesAuthorization` | ✅ Tested (admin allowed, editor/viewer denied) | +| Repositories | status | GET | Admin | `repository_subresources_auth_test.go` | `TestIntegrationProvisioning_RepositorySubresourcesAuthorization` | ✅ Tested (admin allowed, editor/viewer denied) | +| Repositories | jobs | POST | Editor | `repository_test.go` | `TestIntegrationProvisioning_JobPermissions` | ✅ Tested (editor allowed, viewer denied) | +| Repositories | jobs | GET | Editor | `repository_test.go` | `TestIntegrationProvisioning_JobPermissions` | ✅ Tested (implicitly) | +| **Connections** | (CRUD) | create | Admin | `connection_test.go` | `TestIntegrationProvisioning_ConnectionCRUDL` | ✅ Tested (viewer denied) | +| Connections | (CRUD) | read | Admin | `connection_test.go` | `TestIntegrationProvisioning_ConnectionCRUDL` | ✅ Tested (viewer denied) | +| Connections | (CRUD) | update | Admin | `connection_test.go` | `TestIntegrationProvisioning_ConnectionCRUDL` | ✅ Tested | +| Connections | (CRUD) | delete | Admin | `connection_test.go` | `TestIntegrationProvisioning_ConnectionCRUDL` | ✅ Tested | +| Connections | status | GET | Admin | `connection_status_auth_test.go` | `TestIntegrationProvisioning_ConnectionStatusAuthorization` | ✅ Tested (admin allowed, editor/viewer denied) | +| **Jobs** | (CRUD) | create | Editor | `repository_test.go` | `TestIntegrationProvisioning_JobPermissions` | ✅ Tested (editor allowed, viewer denied) | +| Jobs | (CRUD) | read | Editor | Various job tests | Various | ✅ Tested (implicitly) | +| Jobs | (CRUD) | update | Editor | Various job tests | Various | ✅ Tested (implicitly) | +| Jobs | (CRUD) | delete | Editor | `deletejob_test.go` | Various | ✅ Tested (implicitly) | +| **HistoricJobs** | - | GET | Admin | `historicjobs_auth_test.go` | `TestIntegrationProvisioning_HistoricJobsAuthorization` | ✅ Tested (admin allowed, editor/viewer denied) | +| **Settings** | - | GET | Viewer | `settings_stats_auth_test.go` | `TestIntegrationProvisioning_SettingsAuthorization` | ✅ Tested (viewer/editor/admin all allowed) | +| **Stats** | - | GET | Admin | `settings_stats_auth_test.go` | `TestIntegrationProvisioning_StatsAuthorization` | ✅ Tested (admin allowed, editor/viewer denied) | + +## Test Coverage Status + +✅ **All authorization paths are now covered by tests!** + +### Recently Added Tests + +1. **Repositories/test subresource** (POST) - ✅ Added in `repository_subresources_auth_test.go` +2. **Repositories/resources subresource** (GET) - ✅ Added in `repository_subresources_auth_test.go` +3. **Repositories/history subresource** (GET) - ✅ Added in `repository_subresources_auth_test.go` +4. **Repositories/status subresource** (GET) - ✅ Added in `repository_subresources_auth_test.go` +5. **Connections/status subresource** (GET) - ✅ Added in `connection_status_auth_test.go` +6. **HistoricJobs resource** (GET) - ✅ Added in `historicjobs_auth_test.go` +7. **Settings resource** (GET) - ✅ Completed in `settings_stats_auth_test.go` +8. **Stats resource** (GET) - ✅ Completed in `settings_stats_auth_test.go` + +## Test Patterns + +### Successful Authorization Test Pattern +```go +t.Run("editor can GET refs", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("refs"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "editor should be able to GET refs") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") +}) +``` + +### Denied Authorization Test Pattern +```go +t.Run("viewer cannot GET refs", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("refs"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET refs") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") +}) +``` + +## Test Files + +- `repository_subresources_auth_test.go` - Tests for test, resources, history, status subresources +- `connection_status_auth_test.go` - Tests for connection status subresource +- `historicjobs_auth_test.go` - Tests for HistoricJobs resource +- `settings_stats_auth_test.go` - Tests for Settings and Stats resources +- `repository_test.go` - Tests for repository CRUD, refs, jobs +- `connection_test.go` - Tests for connection CRUD +- `files_test.go` - Tests for files subresource authorization + +## Notes + +- Files subresource authorization is tested at the route level (any authenticated user) and at the resource level (via DualReadWriter) +- Jobs subresource is well covered with explicit editor/viewer/admin tests +- Repository CRUD operations are covered with viewer denial tests +- Connection CRUD operations are covered with viewer denial tests + diff --git a/pkg/tests/apis/provisioning/connection_status_auth_test.go b/pkg/tests/apis/provisioning/connection_status_auth_test.go new file mode 100644 index 00000000000..de84b0aead1 --- /dev/null +++ b/pkg/tests/apis/provisioning/connection_status_auth_test.go @@ -0,0 +1,89 @@ +package provisioning + +import ( + "context" + "net/http" + "testing" + + "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" + + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationProvisioning_ConnectionStatusAuthorization(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + createOptions := metav1.CreateOptions{FieldValidation: "Strict"} + + // Create a connection for testing + connection := &unstructured.Unstructured{Object: map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Connection", + "metadata": map[string]any{ + "name": "connection-status-test", + "namespace": "default", + }, + "spec": map[string]any{ + "type": "github", + "github": map[string]any{ + "appID": "123456", + "installationID": "454545", + }, + }, + "secure": map[string]any{ + "privateKey": map[string]any{ + "create": "someSecret", + }, + }, + }} + + _, err := helper.Connections.Resource.Create(ctx, connection, createOptions) + require.NoError(t, err, "failed to create connection") + + t.Run("admin can GET connection status", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("connections"). + Name("connection-status-test"). + SubResource("status"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET connection status") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor cannot GET connection status", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("connections"). + Name("connection-status-test"). + SubResource("status"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to GET connection status") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot GET connection status", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("connections"). + Name("connection-status-test"). + SubResource("status"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET connection status") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) +} + diff --git a/pkg/tests/apis/provisioning/historicjobs_auth_test.go b/pkg/tests/apis/provisioning/historicjobs_auth_test.go new file mode 100644 index 00000000000..a57c17ee1fe --- /dev/null +++ b/pkg/tests/apis/provisioning/historicjobs_auth_test.go @@ -0,0 +1,96 @@ +package provisioning + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + + provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationProvisioning_HistoricJobsAuthorization(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "historicjobs-auth-test" + testRepo := TestRepo{ + Name: repo, + Target: "folder", + Copies: map[string]string{}, // No files needed for this test + ExpectedDashboards: 0, + ExpectedFolders: 1, // Repository creates a folder + } + helper.CreateRepo(t, testRepo) + + // Trigger a job to create a historic job entry + jobSpec := provisioning.JobSpec{ + Action: provisioning.JobActionPull, + Pull: &provisioning.SyncJobOptions{}, + } + body := asJSON(jobSpec) + + // Create a job as admin + var statusCode int + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("jobs"). + Body(body). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + require.NoError(t, result.Error(), "should be able to create job") + require.Equal(t, http.StatusAccepted, statusCode) + + // Wait for job to complete and become historic + helper.AwaitJobs(t, repo) + historicJob := helper.AwaitLatestHistoricJob(t, repo) + require.NotNil(t, historicJob, "should have a historic job") + + historicJobName := historicJob.GetName() + + t.Run("admin can GET historic job", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("historicjobs"). + Name(historicJobName). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET historic job") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor cannot GET historic job", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("historicjobs"). + Name(historicJobName). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to GET historic job") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot GET historic job", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("historicjobs"). + Name(historicJobName). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET historic job") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) +} + diff --git a/pkg/tests/apis/provisioning/repository_subresources_auth_test.go b/pkg/tests/apis/provisioning/repository_subresources_auth_test.go new file mode 100644 index 00000000000..9b18e283876 --- /dev/null +++ b/pkg/tests/apis/provisioning/repository_subresources_auth_test.go @@ -0,0 +1,237 @@ +package provisioning + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationProvisioning_RepositorySubresourcesAuthorization(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "subresources-auth-test" + testRepo := TestRepo{ + Name: repo, + Target: "folder", + Copies: map[string]string{}, // No files needed for this test + ExpectedDashboards: 0, + ExpectedFolders: 1, // Repository creates a folder + } + helper.CreateRepo(t, testRepo) + + t.Run("test subresource", func(t *testing.T) { + newRepoConfig := map[string]any{ + "apiVersion": "provisioning.grafana.app/v0alpha1", + "kind": "Repository", + "spec": map[string]any{ + "title": "Test Configuration", + "type": "local", + "local": map[string]any{ + "path": helper.ProvisioningPath, + }, + "workflows": []string{"write"}, + "sync": map[string]any{ + "enabled": true, + "target": "folder", + "intervalSeconds": 10, + }, + }, + } + configBytes, err := json.Marshal(newRepoConfig) + require.NoError(t, err) + + t.Run("admin can POST test", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name("test-config-auth"). + SubResource("test"). + Body(configBytes). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to POST test") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor cannot POST test", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Post(). + Namespace("default"). + Resource("repositories"). + Name("test-config-auth"). + SubResource("test"). + Body(configBytes). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to POST test") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot POST test", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Post(). + Namespace("default"). + Resource("repositories"). + Name("test-config-auth"). + SubResource("test"). + Body(configBytes). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to POST test") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + }) + + t.Run("resources subresource", func(t *testing.T) { + t.Run("admin can GET resources", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("resources"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET resources") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor cannot GET resources", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("resources"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to GET resources") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot GET resources", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("resources"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET resources") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + }) + + t.Run("history subresource", func(t *testing.T) { + t.Run("admin can GET history (or BadRequest if not supported)", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("history"). + Do(ctx).StatusCode(&statusCode) + + // Admin should pass authorization - may get BadRequest if repo doesn't support history + // but should NOT get Forbidden (which would indicate authorization failure) + if result.Error() != nil { + require.False(t, apierrors.IsForbidden(result.Error()), "admin should not get Forbidden error") + // Local repos don't support history, so BadRequest is expected + require.True(t, apierrors.IsBadRequest(result.Error()) || statusCode == http.StatusBadRequest, + "should get BadRequest if history not supported, not Forbidden") + } else { + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK if history is supported") + } + }) + + t.Run("editor cannot GET history", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("history"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to GET history") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot GET history", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("history"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET history") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + }) + + t.Run("status subresource", func(t *testing.T) { + t.Run("admin can GET status", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("status"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET status") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor cannot GET status", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("status"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to GET status") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot GET status", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("status"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET status") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + }) +} + diff --git a/pkg/tests/apis/provisioning/settings_stats_auth_test.go b/pkg/tests/apis/provisioning/settings_stats_auth_test.go new file mode 100644 index 00000000000..65ae3a46149 --- /dev/null +++ b/pkg/tests/apis/provisioning/settings_stats_auth_test.go @@ -0,0 +1,105 @@ +package provisioning + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + + "github.com/grafana/grafana/pkg/util/testutil" +) + +func TestIntegrationProvisioning_SettingsAuthorization(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + t.Run("viewer can GET settings", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("settings"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "viewer should be able to GET settings") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor can GET settings", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("settings"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "editor should be able to GET settings") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("admin can GET settings", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("settings"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET settings") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) +} + +func TestIntegrationProvisioning_StatsAuthorization(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + helper := runGrafana(t) + ctx := context.Background() + + // Create a repository to ensure stats endpoint has data + const repo = "stats-auth-test" + helper.CreateRepo(t, TestRepo{ + Name: repo, + Target: "folder", + Copies: map[string]string{}, + ExpectedDashboards: 0, + ExpectedFolders: 1, + }) + + t.Run("admin can GET stats", func(t *testing.T) { + var statusCode int + result := helper.AdminREST.Get(). + Namespace("default"). + Resource("stats"). + Do(ctx).StatusCode(&statusCode) + + require.NoError(t, result.Error(), "admin should be able to GET stats") + require.Equal(t, http.StatusOK, statusCode, "should return 200 OK") + }) + + t.Run("editor cannot GET stats", func(t *testing.T) { + var statusCode int + result := helper.EditorREST.Get(). + Namespace("default"). + Resource("stats"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "editor should not be able to GET stats") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) + + t.Run("viewer cannot GET stats", func(t *testing.T) { + var statusCode int + result := helper.ViewerREST.Get(). + Namespace("default"). + Resource("stats"). + Do(ctx).StatusCode(&statusCode) + + require.Error(t, result.Error(), "viewer should not be able to GET stats") + require.Equal(t, http.StatusForbidden, statusCode, "should return 403 Forbidden") + require.True(t, apierrors.IsForbidden(result.Error()), "error should be forbidden") + }) +} +