diff --git a/pkg/workflow/dangerous_permissions_validation.go b/pkg/workflow/dangerous_permissions_validation.go index 5a09366151..c38a01f17e 100644 --- a/pkg/workflow/dangerous_permissions_validation.go +++ b/pkg/workflow/dangerous_permissions_validation.go @@ -56,6 +56,7 @@ func validateDangerousPermissions(workflowData *WorkflowData) error { // findWritePermissions returns a list of permission scopes that have write access // Excludes id-token since it's safe (used for OIDC authentication) and doesn't modify repository content +// Excludes metadata since it's a built-in read-only permission func findWritePermissions(permissions *Permissions) []PermissionScope { if permissions == nil { return nil @@ -70,6 +71,11 @@ func findWritePermissions(permissions *Permissions) []PermissionScope { continue } + // Skip metadata as it's a built-in read-only permission + if scope == PermissionMetadata { + continue + } + level, exists := permissions.Get(scope) if exists && level == PermissionWrite { writePerms = append(writePerms, scope) diff --git a/pkg/workflow/permissions.go b/pkg/workflow/permissions.go index 3fffa42d3a..b0557fa180 100644 --- a/pkg/workflow/permissions.go +++ b/pkg/workflow/permissions.go @@ -27,6 +27,8 @@ func convertStringToPermissionScope(key string) PermissionScope { return PermissionIdToken case "issues": return PermissionIssues + case "metadata": + return PermissionMetadata case "models": return PermissionModels case "packages": @@ -93,6 +95,7 @@ const ( PermissionDiscussions PermissionScope = "discussions" PermissionIdToken PermissionScope = "id-token" PermissionIssues PermissionScope = "issues" + PermissionMetadata PermissionScope = "metadata" PermissionModels PermissionScope = "models" PermissionPackages PermissionScope = "packages" PermissionPages PermissionScope = "pages" @@ -114,6 +117,7 @@ func GetAllPermissionScopes() []PermissionScope { PermissionDiscussions, PermissionIdToken, PermissionIssues, + PermissionMetadata, PermissionModels, PermissionPackages, PermissionPages, diff --git a/pkg/workflow/permissions_operations.go b/pkg/workflow/permissions_operations.go index d77e43b8b0..f15cf7ab18 100644 --- a/pkg/workflow/permissions_operations.go +++ b/pkg/workflow/permissions_operations.go @@ -247,6 +247,11 @@ func (p *Permissions) RenderToYAML() string { continue } + // Skip metadata - it's a built-in permission that is always available with read access + if scope == PermissionMetadata { + continue + } + // Add 2 spaces for proper indentation under permissions: // When rendered in a job, the job renderer adds 4 spaces to the first line only, // so we need to pre-indent continuation lines with 4 additional spaces diff --git a/pkg/workflow/permissions_operations_test.go b/pkg/workflow/permissions_operations_test.go index cccb96ebaf..9874d1bad2 100644 --- a/pkg/workflow/permissions_operations_test.go +++ b/pkg/workflow/permissions_operations_test.go @@ -357,6 +357,7 @@ func TestPermissionsMerge(t *testing.T) { PermissionDeployments: PermissionRead, PermissionDiscussions: PermissionRead, PermissionIssues: PermissionRead, + PermissionMetadata: PermissionRead, PermissionPackages: PermissionRead, PermissionPages: PermissionRead, PermissionPullRequests: PermissionRead, @@ -381,6 +382,7 @@ func TestPermissionsMerge(t *testing.T) { PermissionDiscussions: PermissionWrite, PermissionIdToken: PermissionWrite, // id-token supports write PermissionIssues: PermissionWrite, + PermissionMetadata: PermissionWrite, PermissionPackages: PermissionWrite, PermissionPages: PermissionWrite, PermissionPullRequests: PermissionWrite, @@ -403,6 +405,7 @@ func TestPermissionsMerge(t *testing.T) { PermissionDeployments: PermissionRead, PermissionDiscussions: PermissionRead, PermissionIssues: PermissionRead, + PermissionMetadata: PermissionRead, PermissionPackages: PermissionRead, PermissionPages: PermissionRead, PermissionPullRequests: PermissionRead, @@ -427,6 +430,7 @@ func TestPermissionsMerge(t *testing.T) { PermissionDeployments: PermissionWrite, PermissionDiscussions: PermissionWrite, PermissionIdToken: PermissionWrite, // id-token supports write + PermissionMetadata: PermissionWrite, PermissionPackages: PermissionWrite, PermissionPages: PermissionWrite, PermissionPullRequests: PermissionWrite, diff --git a/pkg/workflow/permissions_rendering_test.go b/pkg/workflow/permissions_rendering_test.go index d188a964bb..3f37027c5c 100644 --- a/pkg/workflow/permissions_rendering_test.go +++ b/pkg/workflow/permissions_rendering_test.go @@ -260,3 +260,79 @@ func TestPermissions_AllReadRenderToYAML(t *testing.T) { }) } } + +func TestPermissions_MetadataExcluded(t *testing.T) { + tests := []struct { + name string + perms *Permissions + contains []string + notContains []string + }{ + { + name: "metadata permission should be excluded from YAML output", + perms: NewPermissionsFromMap(map[PermissionScope]PermissionLevel{ + PermissionContents: PermissionRead, + PermissionMetadata: PermissionRead, + PermissionIssues: PermissionWrite, + }), + contains: []string{ + "permissions:", + " contents: read", + " issues: write", + }, + notContains: []string{ + "metadata", + }, + }, + { + name: "all: read should expand without metadata", + perms: NewPermissionsAllRead(), + contains: []string{ + "permissions:", + " contents: read", + " issues: read", + }, + notContains: []string{ + "metadata", + }, + }, + { + name: "metadata: write should also be excluded", + perms: NewPermissionsFromMap(map[PermissionScope]PermissionLevel{ + PermissionContents: PermissionRead, + PermissionMetadata: PermissionWrite, + }), + contains: []string{ + "permissions:", + " contents: read", + }, + notContains: []string{ + "metadata", + }, + }, + { + name: "only metadata permission should render empty permissions", + perms: NewPermissionsFromMap(map[PermissionScope]PermissionLevel{ + PermissionMetadata: PermissionRead, + }), + contains: []string{}, + notContains: []string{"metadata"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.perms.RenderToYAML() + for _, expected := range tt.contains { + if !strings.Contains(result, expected) { + t.Errorf("RenderToYAML() should contain %q, but got:\n%s", expected, result) + } + } + for _, notExpected := range tt.notContains { + if strings.Contains(result, notExpected) { + t.Errorf("RenderToYAML() should NOT contain %q, but got:\n%s", notExpected, result) + } + } + }) + } +}