From 273538003ef2cac6147d78a74f6631ae05109c25 Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Fri, 10 Apr 2026 23:33:42 +0100 Subject: [PATCH 1/2] Add workflows permission scope to WorkflowParser Add 'workflows' as a recognized permission scope for GITHUB_TOKEN, gated behind AllowWorkflowsPermission feature flag. Changes: - Permissions.cs: Add Workflows property, copy constructor, comparison key mapping. Excluded from write-all/read-all bulk constructors. - WorkflowTemplateConverter.cs: Parse 'workflows' permission with feature flag guard. Read downgrades to NoAccess (write-only scope). - WorkflowFeatures.cs: Add AllowWorkflowsPermission flag, default false. --- .../Conversion/WorkflowTemplateConverter.cs | 18 ++++++++++++++++ src/Sdk/WorkflowParser/Permissions.cs | 21 ++++++++++++++----- src/Sdk/WorkflowParser/WorkflowFeatures.cs | 10 ++++++++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs b/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs index 0c53f87fcbd..d281a8ba970 100644 --- a/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs +++ b/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs @@ -1957,6 +1957,24 @@ private static Permissions ConvertToPermissions(TemplateContext context, Templat context.Error(key, $"The permission 'models' is not allowed"); } break; + case "workflows": + if (context.GetFeatures().AllowWorkflowsPermission) + { + // Workflows only supports write; downgrade read to none + if (permissionLevel == PermissionLevel.Read) + { + permissions.Workflows = PermissionLevel.NoAccess; + } + else + { + permissions.Workflows = permissionLevel; + } + } + else + { + context.Error(key, $"The permission 'workflows' is not allowed"); + } + break; default: break; } diff --git a/src/Sdk/WorkflowParser/Permissions.cs b/src/Sdk/WorkflowParser/Permissions.cs index 0a211e5b015..eacac7a5b4d 100644 --- a/src/Sdk/WorkflowParser/Permissions.cs +++ b/src/Sdk/WorkflowParser/Permissions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Runtime.Serialization; using GitHub.Actions.WorkflowParser.Conversion; @@ -17,7 +17,7 @@ public Permissions() public Permissions(Permissions copy) { Actions = copy.Actions; - ArtifactMetadata = copy.ArtifactMetadata; + ArtifactMetadata = copy.ArtifactMetadata; Attestations = copy.Attestations; Checks = copy.Checks; Contents = copy.Contents; @@ -32,6 +32,7 @@ public Permissions(Permissions copy) SecurityEvents = copy.SecurityEvents; IdToken = copy.IdToken; Models = copy.Models; + Workflows = copy.Workflows; } public Permissions( @@ -41,7 +42,7 @@ public Permissions( bool includeModels) { Actions = permissionLevel; - ArtifactMetadata = permissionLevel; + ArtifactMetadata = permissionLevel; Attestations = includeAttestations ? permissionLevel : PermissionLevel.NoAccess; Checks = permissionLevel; Contents = permissionLevel; @@ -56,9 +57,11 @@ public Permissions( SecurityEvents = permissionLevel; IdToken = includeIdToken ? permissionLevel : PermissionLevel.NoAccess; // Models must not have higher permissions than Read - Models = includeModels - ? (permissionLevel == PermissionLevel.Write ? PermissionLevel.Read : permissionLevel) + Models = includeModels + ? (permissionLevel == PermissionLevel.Write ? PermissionLevel.Read : permissionLevel) : PermissionLevel.NoAccess; + // Workflows is excluded from write-all / read-all; must be explicitly requested + Workflows = PermissionLevel.NoAccess; } private static KeyValuePair[] ComparisonKeyMapping(Permissions left, Permissions right) @@ -81,6 +84,7 @@ public Permissions( new KeyValuePair("security-events", (left.SecurityEvents, right.SecurityEvents)), new KeyValuePair("id-token", (left.IdToken, right.IdToken)), new KeyValuePair("models", (left.Models, right.Models)), + new KeyValuePair("workflows", (left.Workflows, right.Workflows)), }; } @@ -196,6 +200,13 @@ public PermissionLevel Statuses set; } + [DataMember(Name = "workflows", EmitDefaultValue = false)] + public PermissionLevel Workflows + { + get; + set; + } + public Permissions Clone() { return new Permissions(this); diff --git a/src/Sdk/WorkflowParser/WorkflowFeatures.cs b/src/Sdk/WorkflowParser/WorkflowFeatures.cs index c3fa33af74b..44f760f2935 100644 --- a/src/Sdk/WorkflowParser/WorkflowFeatures.cs +++ b/src/Sdk/WorkflowParser/WorkflowFeatures.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -41,6 +41,13 @@ public class WorkflowFeatures [DataMember(EmitDefaultValue = false)] public bool AllowModelsPermission { get; set; } + /// + /// Gets or sets a value indicating whether users may use the "workflows" permission. + /// Used during parsing only. + /// + [DataMember(EmitDefaultValue = false)] + public bool AllowWorkflowsPermission { get; set; } + /// /// Gets or sets a value indicating whether the expression function fromJson performs strict JSON parsing. /// Used during evaluation only. @@ -67,6 +74,7 @@ public static WorkflowFeatures GetDefaults() Snapshot = false, // Default to false since this feature is still in an experimental phase StrictJsonParsing = false, // Default to false since this is temporary for telemetry purposes only AllowModelsPermission = false, // Default to false since we want this to be disabled for all non-production environments + AllowWorkflowsPermission = false, // Default to false; gated by feature flag for controlled rollout AllowServiceContainerCommand = false, // Default to false since this feature is gated by actions_service_container_command }; } From 18f53d3c9eb6944db4b96a841de541156ce2e59c Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Sat, 11 Apr 2026 08:49:41 +0100 Subject: [PATCH 2/2] Include workflows in write-all when feature flag is enabled The write-all permission level should include workflows:write when the AllowWorkflowsPermission feature flag is enabled, matching the behavior of other gated permissions like copilot-requests. Previously workflows was unconditionally excluded from write-all. This aligns with the ADR decision that write-all means permissive access. --- src/Sdk/WorkflowParser/Conversion/PermissionsHelper.cs | 7 ++++--- .../Conversion/WorkflowTemplateConverter.cs | 2 +- src/Sdk/WorkflowParser/Permissions.cs | 9 ++++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Sdk/WorkflowParser/Conversion/PermissionsHelper.cs b/src/Sdk/WorkflowParser/Conversion/PermissionsHelper.cs index b5340895087..5b0272a2b8c 100644 --- a/src/Sdk/WorkflowParser/Conversion/PermissionsHelper.cs +++ b/src/Sdk/WorkflowParser/Conversion/PermissionsHelper.cs @@ -32,7 +32,7 @@ internal static void ValidateEmbeddedPermissions( return; } - var effectiveMax = explicitMax ?? CreatePermissionsFromPolicy(context, permissionsPolicy, includeIdToken: isTrusted, includeModels: context.GetFeatures().AllowModelsPermission); + var effectiveMax = explicitMax ?? CreatePermissionsFromPolicy(context, permissionsPolicy, includeIdToken: isTrusted, includeModels: context.GetFeatures().AllowModelsPermission, includeWorkflows: context.GetFeatures().AllowWorkflowsPermission); if (requested.ViolatesMaxPermissions(effectiveMax, out var permissionLevelViolations)) { @@ -59,7 +59,8 @@ private static Permissions CreatePermissionsFromPolicy( TemplateContext context, string permissionsPolicy, bool includeIdToken, - bool includeModels) + bool includeModels, + bool includeWorkflows) { switch (permissionsPolicy) { @@ -70,7 +71,7 @@ private static Permissions CreatePermissionsFromPolicy( Packages = PermissionLevel.Read, }; case WorkflowConstants.PermissionsPolicy.Write: - return new Permissions(PermissionLevel.Write, includeIdToken: includeIdToken, includeAttestations: true, includeModels: includeModels); + return new Permissions(PermissionLevel.Write, includeIdToken: includeIdToken, includeAttestations: true, includeModels: includeModels, includeWorkflows: includeWorkflows); default: throw new ArgumentException($"Unexpected permission policy: '{permissionsPolicy}'"); } diff --git a/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs b/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs index d281a8ba970..2cb05ebccfa 100644 --- a/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs +++ b/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs @@ -1877,7 +1877,7 @@ private static Permissions ConvertToPermissions(TemplateContext context, Templat permissionsStr.AssertUnexpectedValue(permissionsStr.Value); break; } - return new Permissions(permissionLevel, includeIdToken: true, includeAttestations: true, includeModels: context.GetFeatures().AllowModelsPermission); + return new Permissions(permissionLevel, includeIdToken: true, includeAttestations: true, includeModels: context.GetFeatures().AllowModelsPermission, includeWorkflows: context.GetFeatures().AllowWorkflowsPermission); } var mapping = token.AssertMapping("permissions"); diff --git a/src/Sdk/WorkflowParser/Permissions.cs b/src/Sdk/WorkflowParser/Permissions.cs index eacac7a5b4d..85a9f2aa048 100644 --- a/src/Sdk/WorkflowParser/Permissions.cs +++ b/src/Sdk/WorkflowParser/Permissions.cs @@ -39,7 +39,8 @@ public Permissions( PermissionLevel permissionLevel, bool includeIdToken, bool includeAttestations, - bool includeModels) + bool includeModels, + bool includeWorkflows = false) { Actions = permissionLevel; ArtifactMetadata = permissionLevel; @@ -60,8 +61,10 @@ public Permissions( Models = includeModels ? (permissionLevel == PermissionLevel.Write ? PermissionLevel.Read : permissionLevel) : PermissionLevel.NoAccess; - // Workflows is excluded from write-all / read-all; must be explicitly requested - Workflows = PermissionLevel.NoAccess; + // Workflows is write-only, so only grant it when permissionLevel is Write + Workflows = includeWorkflows && permissionLevel == PermissionLevel.Write + ? PermissionLevel.Write + : PermissionLevel.NoAccess; } private static KeyValuePair[] ComparisonKeyMapping(Permissions left, Permissions right)