Skip to content

Commit

Permalink
Improve support for 'exec' credentials. (#774)
Browse files Browse the repository at this point in the history
  • Loading branch information
brendandburns authored Feb 23, 2022
1 parent 8a39035 commit 2ad5b96
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 18 deletions.
52 changes: 52 additions & 0 deletions src/KubernetesClient/Authentication/ExecTokenProvider.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
18 changes: 17 additions & 1 deletion src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
29 changes: 13 additions & 16 deletions src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -525,7 +531,7 @@ public static Process CreateRunnableExternalProcess(ExternalExecution config)
/// <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)
{
Expand Down Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down

0 comments on commit 2ad5b96

Please sign in to comment.