From a57f90280ff4fc01f63fdc83d9693df2822a80c4 Mon Sep 17 00:00:00 2001 From: Tingluo Huang Date: Fri, 25 Dec 2020 21:54:23 -0500 Subject: [PATCH] config runner via PAT. (#874) --- src/Runner.Common/Constants.cs | 2 + src/Runner.Listener/CommandSettings.cs | 6 + .../Configuration/ConfigurationManager.cs | 106 +++++++++++++++++- .../Configuration/CredentialManager.cs | 10 ++ 4 files changed, 119 insertions(+), 5 deletions(-) diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index f2a12f030..82401bb11 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -99,9 +99,11 @@ namespace GitHub.Runner.Common // Secret args. Must be added to the "Secrets" getter as well. public static readonly string Token = "token"; + public static readonly string PAT = "pat"; public static readonly string WindowsLogonPassword = "windowslogonpassword"; public static string[] Secrets => new[] { + PAT, Token, WindowsLogonPassword, }; diff --git a/src/Runner.Listener/CommandSettings.cs b/src/Runner.Listener/CommandSettings.cs index 364962610..17c12da91 100644 --- a/src/Runner.Listener/CommandSettings.cs +++ b/src/Runner.Listener/CommandSettings.cs @@ -42,6 +42,7 @@ namespace GitHub.Runner.Listener Constants.Runner.CommandLine.Args.Labels, Constants.Runner.CommandLine.Args.MonitorSocketAddress, Constants.Runner.CommandLine.Args.Name, + Constants.Runner.CommandLine.Args.PAT, Constants.Runner.CommandLine.Args.RunnerGroup, Constants.Runner.CommandLine.Args.StartupType, Constants.Runner.CommandLine.Args.Token, @@ -187,6 +188,11 @@ namespace GitHub.Runner.Listener validator: Validators.NonEmptyValidator); } + public string GetGitHubPersonalAccessToken() + { + return GetArg(name: Constants.Runner.CommandLine.Args.PAT); + } + public string GetRunnerRegisterToken() { return GetArgOrPrompt( diff --git a/src/Runner.Listener/Configuration/ConfigurationManager.cs b/src/Runner.Listener/Configuration/ConfigurationManager.cs index 796fa2deb..84996547f 100644 --- a/src/Runner.Listener/Configuration/ConfigurationManager.cs +++ b/src/Runner.Listener/Configuration/ConfigurationManager.cs @@ -4,7 +4,6 @@ using GitHub.Runner.Common.Util; using GitHub.Runner.Sdk; using GitHub.Services.Common; using GitHub.Services.OAuth; -using GitHub.Services.WebApi; using System; using System.Collections.Generic; using System.Linq; @@ -12,6 +11,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.InteropServices; using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; namespace GitHub.Runner.Listener.Configuration @@ -107,8 +107,8 @@ namespace GitHub.Runner.Listener.Configuration else { runnerSettings.GitHubUrl = inputUrl; - var githubToken = command.GetRunnerRegisterToken(); - GitHubAuthResult authResult = await GetTenantCredential(inputUrl, githubToken, Constants.RunnerEvent.Register); + var registerToken = await GetRunnerTokenAsync(command, inputUrl, "registration"); + GitHubAuthResult authResult = await GetTenantCredential(inputUrl, registerToken, Constants.RunnerEvent.Register); runnerSettings.ServerUrl = authResult.TenantUrl; creds = authResult.ToVssCredentials(); Trace.Info("cred retrieved via GitHub auth"); @@ -374,8 +374,8 @@ namespace GitHub.Runner.Listener.Configuration } else { - var githubToken = command.GetRunnerDeletionToken(); - GitHubAuthResult authResult = await GetTenantCredential(settings.GitHubUrl, githubToken, Constants.RunnerEvent.Remove); + var deletionToken = await GetRunnerTokenAsync(command, settings.GitHubUrl, "remove"); + GitHubAuthResult authResult = await GetTenantCredential(settings.GitHubUrl, deletionToken, Constants.RunnerEvent.Remove); creds = authResult.ToVssCredentials(); Trace.Info("cred retrieved via GitHub auth"); } @@ -516,6 +516,102 @@ namespace GitHub.Runner.Listener.Configuration string.Equals(gitHubUrl.Host, "github.localhost", StringComparison.OrdinalIgnoreCase); } + private async Task GetRunnerTokenAsync(CommandSettings command, string githubUrl, string tokenType) + { + var githubPAT = command.GetGitHubPersonalAccessToken(); + var runnerToken = string.Empty; + if (!string.IsNullOrEmpty(githubPAT)) + { + Trace.Info($"Retriving runner {tokenType} token using GitHub PAT."); + var jitToken = await GetJITRunnerTokenAsync(githubUrl, githubPAT, tokenType); + Trace.Info($"Retrived runner {tokenType} token is good to {jitToken.ExpiresAt}."); + HostContext.SecretMasker.AddValue(jitToken.Token); + runnerToken = jitToken.Token; + } + + if (string.IsNullOrEmpty(runnerToken)) + { + if (string.Equals("registration", tokenType, StringComparison.OrdinalIgnoreCase)) + { + runnerToken = command.GetRunnerRegisterToken(); + } + else + { + runnerToken = command.GetRunnerDeletionToken(); + } + } + + return runnerToken; + } + + private async Task GetJITRunnerTokenAsync(string githubUrl, string githubToken, string tokenType) + { + var githubApiUrl = ""; + var gitHubUrlBuilder = new UriBuilder(githubUrl); + var path = gitHubUrlBuilder.Path.Split('/', '\\', StringSplitOptions.RemoveEmptyEntries); + if (path.Length == 1) + { + // org runner + if (IsHostedServer(gitHubUrlBuilder)) + { + githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/orgs/{path[0]}/actions/runners/{tokenType}-token"; + } + else + { + githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/orgs/{path[0]}/actions/runners/{tokenType}-token"; + } + } + else if (path.Length == 2) + { + // repo or enterprise runner. + var repoScope = "repos/"; + if (string.Equals(path[0], "enterprises", StringComparison.OrdinalIgnoreCase)) + { + repoScope = ""; + } + + if (IsHostedServer(gitHubUrlBuilder)) + { + githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/{repoScope}{path[0]}/{path[1]}/actions/runners/{tokenType}-token"; + } + else + { + githubApiUrl = $"{gitHubUrlBuilder.Scheme}://{gitHubUrlBuilder.Host}/api/v3/{repoScope}{path[0]}/{path[1]}/actions/runners/{tokenType}-token"; + } + } + else + { + throw new ArgumentException($"'{githubUrl}' should point to an org or repository."); + } + + using (var httpClientHandler = HostContext.CreateHttpClientHandler()) + using (var httpClient = new HttpClient(httpClientHandler)) + { + var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"github:{githubToken}")); + HostContext.SecretMasker.AddValue(base64EncodingToken); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("basic", base64EncodingToken); + httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents); + httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github.v3+json"); + + var response = await httpClient.PostAsync(githubApiUrl, new StringContent(string.Empty)); + + if (response.IsSuccessStatusCode) + { + Trace.Info($"Http response code: {response.StatusCode} from 'POST {githubApiUrl}'"); + var jsonResponse = await response.Content.ReadAsStringAsync(); + return StringUtil.ConvertFromJson(jsonResponse); + } + else + { + _term.WriteError($"Http response code: {response.StatusCode} from 'POST {githubApiUrl}'"); + var errorResponse = await response.Content.ReadAsStringAsync(); + _term.WriteError(errorResponse); + response.EnsureSuccessStatusCode(); + return null; + } + } + } + private async Task GetTenantCredential(string githubUrl, string githubToken, string runnerEvent) { var githubApiUrl = ""; diff --git a/src/Runner.Listener/Configuration/CredentialManager.cs b/src/Runner.Listener/Configuration/CredentialManager.cs index ee459abe7..e13d29510 100644 --- a/src/Runner.Listener/Configuration/CredentialManager.cs +++ b/src/Runner.Listener/Configuration/CredentialManager.cs @@ -71,6 +71,16 @@ namespace GitHub.Runner.Listener.Configuration } } + [DataContract] + public sealed class GitHubRunnerRegisterToken + { + [DataMember(Name = "token")] + public string Token { get; set; } + + [DataMember(Name = "expires_at")] + public string ExpiresAt { get; set; } + } + [DataContract] public sealed class GitHubAuthResult {