From d3c68395def5fa81a73fb0339eb0fcf60b615517 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Fri, 4 Feb 2022 11:04:04 -0800 Subject: [PATCH] Improve support for 'exec' credentials. --- .../Authentication/ExecTokenProvider.cs | 52 +++++++++++++++++++ .../ExecCredentialResponse.cs | 18 ++++++- ...ubernetesClientConfiguration.ConfigFile.cs | 29 +++++------ ...KubernetesClientConfiguration.InCluster.cs | 2 +- 4 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 src/KubernetesClient/Authentication/ExecTokenProvider.cs diff --git a/src/KubernetesClient/Authentication/ExecTokenProvider.cs b/src/KubernetesClient/Authentication/ExecTokenProvider.cs new file mode 100644 index 000000000..cfe70af46 --- /dev/null +++ b/src/KubernetesClient/Authentication/ExecTokenProvider.cs @@ -0,0 +1,52 @@ +using System.Diagnostics; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using k8s.Exceptions; +using k8s.KubeConfigModels; +using Microsoft.Rest; + +namespace k8s.Authentication +{ + public class ExecTokenProvider : ITokenProvider + { + private readonly ExternalExecution exec; + private ExecCredentialResponse response; + + public ExecTokenProvider(ExternalExecution exec) + { + this.exec = exec; + } + + private bool NeedsRefresh() + { + if (response?.Status == null) + { + return true; + } + + if (response.Status.Expiry == null) + { + return false; + } + + return DateTime.UtcNow.AddSeconds(30) > response.Status.Expiry; + } + + public async Task GetAuthenticationHeaderAsync(CancellationToken cancellationToken) + { + if (NeedsRefresh()) + { + await RefreshToken().ConfigureAwait(false); + } + + return new AuthenticationHeaderValue("Bearer", response.Status.Token); + } + + private async Task RefreshToken() + { + response = + await Task.Run(() => KubernetesClientConfiguration.ExecuteExternalCommand(this.exec)).ConfigureAwait(false); + } + } +} diff --git a/src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs b/src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs index 95aa5752c..9bce2164c 100644 --- a/src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs +++ b/src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs @@ -2,11 +2,27 @@ namespace k8s.KubeConfigModels { public class ExecCredentialResponse { + public class ExecStatus + { + #nullable enable + public DateTime? Expiry { get; set; } + public string? Token { get; set; } + public string? ClientCertificateData { get; set; } + public string? ClientKeyData { get; set; } + #nullable disable + + public bool IsValid() + { + return (!string.IsNullOrEmpty(Token) || + (!string.IsNullOrEmpty(ClientCertificateData) && !string.IsNullOrEmpty(ClientKeyData))); + } + } + [JsonPropertyName("apiVersion")] public string ApiVersion { get; set; } [JsonPropertyName("kind")] public string Kind { get; set; } [JsonPropertyName("status")] - public IDictionary Status { get; set; } + public ExecStatus Status { get; set; } } } diff --git a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs index 09cd89dde..9f35294a5 100644 --- a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs +++ b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs @@ -444,15 +444,21 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext) throw new KubeConfigException("External command execution missing ApiVersion key"); } - var (accessToken, clientCertificateData, clientCertificateKeyData) = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution); - AccessToken = accessToken; + var response = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution); + AccessToken = response.Status.Token; // When reading ClientCertificateData from a config file it will be base64 encoded, and code later in the system (see CertUtils.GeneratePfx) // expects ClientCertificateData and ClientCertificateKeyData to be base64 encoded because of this. However the string returned by external // auth providers is the raw certificate and key PEM text, so we need to take that and base64 encoded it here so it can be decoded later. - ClientCertificateData = clientCertificateData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(clientCertificateData)); - ClientCertificateKeyData = clientCertificateKeyData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(clientCertificateKeyData)); + ClientCertificateData = response.Status.ClientCertificateData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(response.Status.ClientCertificateData)); + ClientCertificateKeyData = response.Status.ClientKeyData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(response.Status.ClientKeyData)); userCredentialsFound = true; + + // TODO: support client certificates here too. + if (AccessToken != null) + { + TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution); + } } if (!userCredentialsFound) @@ -525,7 +531,7 @@ public static Process CreateRunnableExternalProcess(ExternalExecution config) /// /// The token, client certificate data, and the client key data received from the external command execution /// - public static (string, string, string) ExecuteExternalCommand(ExternalExecution config) + public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config) { if (config == null) { @@ -562,18 +568,9 @@ public static (string, string, string) ExecuteExternalCommand(ExternalExecution $"external exec failed because api version {responseObject.ApiVersion} does not match {config.ApiVersion}"); } - if (responseObject.Status.ContainsKey("token")) - { - return (responseObject.Status["token"], null, null); - } - else if (responseObject.Status.ContainsKey("clientCertificateData")) + if (responseObject.Status.IsValid()) { - if (!responseObject.Status.ContainsKey("clientKeyData")) - { - throw new KubeConfigException($"external exec failed missing clientKeyData field in plugin output"); - } - - return (null, responseObject.Status["clientCertificateData"], responseObject.Status["clientKeyData"]); + return responseObject; } else { diff --git a/src/KubernetesClient/KubernetesClientConfiguration.InCluster.cs b/src/KubernetesClient/KubernetesClientConfiguration.InCluster.cs index a31b496be..e46eb21b9 100644 --- a/src/KubernetesClient/KubernetesClientConfiguration.InCluster.cs +++ b/src/KubernetesClient/KubernetesClientConfiguration.InCluster.cs @@ -24,7 +24,7 @@ public static bool IsInCluster() var host = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST"); var port = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_PORT"); - if (String.IsNullOrEmpty(host) || String.IsNullOrEmpty(port)) + if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(port)) { return false; }