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 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")]
|
[JsonPropertyName("apiVersion")]
|
||||||
public string ApiVersion { get; set; }
|
public string ApiVersion { get; set; }
|
||||||
[JsonPropertyName("kind")]
|
[JsonPropertyName("kind")]
|
||||||
public string Kind { get; set; }
|
public string Kind { get; set; }
|
||||||
[JsonPropertyName("status")]
|
[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");
|
throw new KubeConfigException("External command execution missing ApiVersion key");
|
||||||
}
|
}
|
||||||
|
|
||||||
var (accessToken, clientCertificateData, clientCertificateKeyData) = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution);
|
var response = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution);
|
||||||
AccessToken = accessToken;
|
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)
|
// 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
|
// 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.
|
// 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));
|
ClientCertificateData = response.Status.ClientCertificateData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(response.Status.ClientCertificateData));
|
||||||
ClientCertificateKeyData = clientCertificateKeyData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(clientCertificateKeyData));
|
ClientCertificateKeyData = response.Status.ClientKeyData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(response.Status.ClientKeyData));
|
||||||
|
|
||||||
userCredentialsFound = true;
|
userCredentialsFound = true;
|
||||||
|
|
||||||
|
// TODO: support client certificates here too.
|
||||||
|
if (AccessToken != null)
|
||||||
|
{
|
||||||
|
TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userCredentialsFound)
|
if (!userCredentialsFound)
|
||||||
@@ -525,7 +531,7 @@ namespace k8s
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// The token, client certificate data, and the client key data received from the external command execution
|
/// The token, client certificate data, and the client key data received from the external command execution
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public static (string, string, string) ExecuteExternalCommand(ExternalExecution config)
|
public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config)
|
||||||
{
|
{
|
||||||
if (config == null)
|
if (config == null)
|
||||||
{
|
{
|
||||||
@@ -562,18 +568,9 @@ namespace k8s
|
|||||||
$"external exec failed because api version {responseObject.ApiVersion} does not match {config.ApiVersion}");
|
$"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);
|
return responseObject;
|
||||||
}
|
|
||||||
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"]);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace k8s
|
|||||||
var host = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST");
|
var host = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST");
|
||||||
var port = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_PORT");
|
var port = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_PORT");
|
||||||
|
|
||||||
if (String.IsNullOrEmpty(host) || String.IsNullOrEmpty(port))
|
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(port))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user