diff --git a/src/Runner.Common/ConfigurationStore.cs b/src/Runner.Common/ConfigurationStore.cs index 0ae270420d1..0fd40ffcf57 100644 --- a/src/Runner.Common/ConfigurationStore.cs +++ b/src/Runner.Common/ConfigurationStore.cs @@ -1,6 +1,7 @@ using GitHub.Runner.Common.Util; using GitHub.Runner.Sdk; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization; @@ -45,6 +46,9 @@ public sealed class RunnerSettings [DataMember(EmitDefaultValue = false)] public string MonitorSocketAddress { get; set; } + [DataMember(Name = "PullRequestSecurity", EmitDefaultValue = false)] + public PullRequestSecuritySettings PullRequestSecuritySettings { get; set; } + [IgnoreDataMember] public bool IsHostedServer { @@ -98,6 +102,18 @@ private void OnSerializing(StreamingContext context) } } + [DataContract] + public sealed class PullRequestSecuritySettings + { + // pullRequestSecurity is optional in the config -- if the key is + // defined, assume that we only want collaborators to run PRs. + [DataMember(EmitDefaultValue = false)] + public HashSet AllowedAuthors = new HashSet(); + + [DataMember(EmitDefaultValue = false)] + public bool AllowContributors = true; + } + [ServiceLocator(Default = typeof(ConfigurationStore))] public interface IConfigurationStore : IRunnerService { diff --git a/src/Runner.Worker/GitHubContext.cs b/src/Runner.Worker/GitHubContext.cs index cd22773a646..6f71211b472 100644 --- a/src/Runner.Worker/GitHubContext.cs +++ b/src/Runner.Worker/GitHubContext.cs @@ -56,5 +56,16 @@ public GitHubContext ShallowCopy() return copy; } + + public bool IsPullRequest() + { + PipelineContextData data; + if (TryGetValue("event_name", out data)) + { + var eventName = data as StringContextData; + return eventName == "pull_request"; + } + return false; + } } } diff --git a/src/Runner.Worker/JobRunner.cs b/src/Runner.Worker/JobRunner.cs index ca81e51b5db..1f456ffbe11 100644 --- a/src/Runner.Worker/JobRunner.cs +++ b/src/Runner.Worker/JobRunner.cs @@ -12,6 +12,7 @@ using System.Net.Http; using GitHub.Runner.Common; using GitHub.Runner.Sdk; +using GitHub.DistributedTask.Pipelines.ContextData; namespace GitHub.Runner.Worker { @@ -63,6 +64,14 @@ public async Task RunAsync(Pipelines.AgentJobRequestMessage message, jobContext.InitializeJob(message, jobRequestCancellationToken); Trace.Info("Starting the job execution context."); jobContext.Start(); + var githubContext = jobContext.ExpressionValues["github"] as GitHubContext; + + if (!JobPassesSecurityRestrictions(jobContext)) + { + jobContext.Error("Running job on this worker disallowed by security policy"); + return await CompleteJobAsync(jobServer, jobContext, message, TaskResult.Failed); + } + jobContext.Debug($"Starting: {message.JobDisplayName}"); runnerShutdownRegistration = HostContext.RunnerShutdownToken.Register(() => @@ -189,6 +198,81 @@ public async Task RunAsync(Pipelines.AgentJobRequestMessage message, } } + private bool JobPassesSecurityRestrictions(IExecutionContext jobContext) + { + var gitHubContext = jobContext.ExpressionValues["github"] as GitHubContext; + + try { + if (gitHubContext.IsPullRequest()) + { + return OkayToRunPullRequest(gitHubContext); + } + + return true; + } + catch (Exception ex) + { + Trace.Error("Caught exception in JobPassesSecurityRestrictions"); + Trace.Error("As a safety precaution we are not allowing this job to run"); + Trace.Error(ex); + return false; + } + } + + private bool OkayToRunPullRequest(GitHubContext gitHubContext) + { + var configStore = HostContext.GetService(); + var settings = configStore.GetSettings(); + var prSecuritySettings = settings.PullRequestSecuritySettings; + + if (prSecuritySettings is null) { + Trace.Info("No pullRequestSecurity defined in settings, allowing this build"); + return true; + } + + var githubEvent = gitHubContext["event"] as DictionaryContextData; + var prData = githubEvent["pull_request"] as DictionaryContextData; + + var authorAssociation = prData.TryGetValue("author_association", out var value) + ? value as StringContextData : null; + + + // TODO: Allow COLLABORATOR, MEMBER too -- possibly by a config setting + if (authorAssociation == "OWNER") + { + Trace.Info("PR is from the repo owner, always allowed"); + return true; + } + else if (prSecuritySettings.AllowContributors && authorAssociation == "COLLABORATOR") { + Trace.Info("PR is from the repo collaborator, allowing"); + return true; + } + + var prHead = prData["head"] as DictionaryContextData; + var prUser = prHead["user"] as DictionaryContextData; + var prUserLogin = prUser["login"] as StringContextData; + + Trace.Info($"GitHub PR author is {prUserLogin as StringContextData}"); + + if (prUserLogin == null) + { + Trace.Info("Unable to get PR author, not allowing PR to run"); + return false; + } + + if (prSecuritySettings.AllowedAuthors.Contains(prUserLogin)) + { + Trace.Info("Author in PR allowed list"); + return true; + } + else + { + Trace.Info($"Not running job as author ({prUserLogin}) is not in {{{string.Join(", ", prSecuritySettings.AllowedAuthors)}}}"); + + return false; + } + } + private async Task CompleteJobAsync(IJobServer jobServer, IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, TaskResult? taskResult = null) { jobContext.Debug($"Finishing: {message.JobDisplayName}");