Improve support for 'exec' credentials. (#774)

This commit is contained in:
Brendan Burns
2022-02-23 09:32:19 -08:00
committed by GitHub
parent 8a39035055
commit 2ad5b9613c
4 changed files with 83 additions and 18 deletions

View File

@@ -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<AuthenticationHeaderValue> 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);
}
}
}

View File

@@ -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<string, string> Status { get; set; }
public ExecStatus Status { get; set; }
}
}

View File

@@ -444,15 +444,21 @@ namespace k8s
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 @@ namespace k8s
/// <returns>
/// The token, client certificate data, and the client key data received from the external command execution
/// </returns>
public static (string, string, string) ExecuteExternalCommand(ExternalExecution config)
public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config)
{
if (config == null)
{
@@ -562,18 +568,9 @@ namespace k8s
$"external exec failed because api version {responseObject.ApiVersion} does not match {config.ApiVersion}");
}
if (responseObject.Status.ContainsKey("token"))
if (responseObject.Status.IsValid())
{
return (responseObject.Status["token"], null, null);
}
else if (responseObject.Status.ContainsKey("clientCertificateData"))
{
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
{

View File

@@ -24,7 +24,7 @@ namespace k8s
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;
}