feat: allow editors to access repository refs subresource

Change refs authorization from admin to editor fallback so editors can
view repository branches when pushing changes to dashboards/folders.

- Split refs from other read-only subresources (resources, history, status)
- refs now uses accessWithEditor instead of accessWithAdmin
- Updated documentation comment to reflect authorization levels
- Added integration test TestIntegrationProvisioning_RefsPermissions
  verifying editor access and viewer denial
This commit is contained in:
Roberto Jimenez Sanchez
2025-12-18 08:05:24 +01:00
parent b2d861f01d
commit 1098a65772
2 changed files with 75 additions and 3 deletions
+13 -3
View File
@@ -330,7 +330,7 @@ func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer {
//
// Repositories:
// - CRUD: repositories:create/read/write/delete
// - Subresources (files, refs, resources, history, status): repositories:read
// - Subresources: files (any auth), refs (editor), resources/history/status (admin)
// - Test: repositories:write
// - Jobs subresource: jobs:create/read
//
@@ -413,8 +413,18 @@ func (b *APIBuilder) authorizeRepositorySubresource(ctx context.Context, a autho
case "files":
return authorizer.DecisionAllow, "", nil
// Read-only subresources: refs, resources, history, status
case "refs", "resources", "history", "status":
// refs subresource - editors need to see branches to push changes
case "refs":
return toAuthorizerDecision(b.accessWithEditor.Check(ctx, authlib.CheckRequest{
Verb: apiutils.VerbGet,
Group: provisioning.GROUP,
Resource: provisioning.RepositoryResourceInfo.GetName(),
Name: a.GetName(),
Namespace: a.GetNamespace(),
}, ""))
// Read-only subresources: resources, history, status (admin only)
case "resources", "history", "status":
return toAuthorizerDecision(b.accessWithAdmin.Check(ctx, authlib.CheckRequest{
Verb: apiutils.VerbGet,
Group: provisioning.GROUP,
@@ -950,3 +950,65 @@ func TestIntegrationProvisioning_JobPermissions(t *testing.T) {
require.Equal(t, http.StatusAccepted, statusCode, "should return 202 Accepted")
})
}
func TestIntegrationProvisioning_RefsPermissions(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := runGrafana(t)
ctx := context.Background()
const repo = "refs-permissions-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("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")
// Verify we can parse the refs
refs := &provisioning.RefList{}
err := result.Into(refs)
require.NoError(t, err, "should parse refs response")
})
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")
})
t.Run("admin can GET refs", func(t *testing.T) {
var statusCode int
result := helper.AdminREST.Get().
Namespace("default").
Resource("repositories").
Name(repo).
SubResource("refs").
Do(ctx).StatusCode(&statusCode)
require.NoError(t, result.Error(), "admin should be able to GET refs")
require.Equal(t, http.StatusOK, statusCode, "should return 200 OK")
})
}