Improve support for 'exec' credentials. (#774)
This commit is contained in:
52
src/KubernetesClient/Authentication/ExecTokenProvider.cs
Normal file
52
src/KubernetesClient/Authentication/ExecTokenProvider.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user