Skip to content

Commit

Permalink
Add runner-level config to enforce PR security
Browse files Browse the repository at this point in the history
This addresses actions#494 at the runner level to give "just enough protection"
to allow using self-hosted runners with public repos.

By default, the current behaviour is unchanged -- all jobs passed to the
runner are executed.

If the `.runner` config file has this block added to it:

```
  "pullRequestSecurity": {}
```

Then by only PRs from "CONTRIBUTORS" (as defined by the field in
https://docs.github.com/en/free-pro-team@latest/graphql/reference/objects#pullrequest
-- nothing for us to have to work out ourselves.)

It is also possible to explicitly list users that are allowed to run
jobs on this worker:

```
  "pullRequestSecurity": {
    "allowedAuthors": ["ashb"]
  }
```

Or to _only_ allow the given users, but not all contributors:

```
  "pullRequestSecurity": {
    "allowContributors": false,
    "allowedAuthors": ["ashb"]
  }
```

Owners of the repo are always allowed to run jobs.
  • Loading branch information
ashb committed Jan 8, 2021
1 parent de95541 commit e7164b9
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 0 deletions.
16 changes: 16 additions & 0 deletions src/Runner.Common/ConfigurationStore.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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<string> AllowedAuthors = new HashSet<string>();

[DataMember(EmitDefaultValue = false)]
public bool AllowContributors = true;
}

[ServiceLocator(Default = typeof(ConfigurationStore))]
public interface IConfigurationStore : IRunnerService
{
Expand Down
11 changes: 11 additions & 0 deletions src/Runner.Worker/GitHubContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
84 changes: 84 additions & 0 deletions src/Runner.Worker/JobRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -63,6 +64,14 @@ public async Task<TaskResult> 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(() =>
Expand Down Expand Up @@ -189,6 +198,81 @@ public async Task<TaskResult> 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<IConfigurationStore>();
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<TaskResult> CompleteJobAsync(IJobServer jobServer, IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, TaskResult? taskResult = null)
{
jobContext.Debug($"Finishing: {message.JobDisplayName}");
Expand Down

0 comments on commit e7164b9

Please sign in to comment.