Skip to content

Commit 2ad5b96

Browse files
Improve support for 'exec' credentials. (#774)
1 parent 8a39035 commit 2ad5b96

File tree

4 files changed

+83
-18
lines changed

4 files changed

+83
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Diagnostics;
2+
using System.Net.Http.Headers;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using k8s.Exceptions;
6+
using k8s.KubeConfigModels;
7+
using Microsoft.Rest;
8+
9+
namespace k8s.Authentication
10+
{
11+
public class ExecTokenProvider : ITokenProvider
12+
{
13+
private readonly ExternalExecution exec;
14+
private ExecCredentialResponse response;
15+
16+
public ExecTokenProvider(ExternalExecution exec)
17+
{
18+
this.exec = exec;
19+
}
20+
21+
private bool NeedsRefresh()
22+
{
23+
if (response?.Status == null)
24+
{
25+
return true;
26+
}
27+
28+
if (response.Status.Expiry == null)
29+
{
30+
return false;
31+
}
32+
33+
return DateTime.UtcNow.AddSeconds(30) > response.Status.Expiry;
34+
}
35+
36+
public async Task<AuthenticationHeaderValue> GetAuthenticationHeaderAsync(CancellationToken cancellationToken)
37+
{
38+
if (NeedsRefresh())
39+
{
40+
await RefreshToken().ConfigureAwait(false);
41+
}
42+
43+
return new AuthenticationHeaderValue("Bearer", response.Status.Token);
44+
}
45+
46+
private async Task RefreshToken()
47+
{
48+
response =
49+
await Task.Run(() => KubernetesClientConfiguration.ExecuteExternalCommand(this.exec)).ConfigureAwait(false);
50+
}
51+
}
52+
}

src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs

+17-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,27 @@ namespace k8s.KubeConfigModels
22
{
33
public class ExecCredentialResponse
44
{
5+
public class ExecStatus
6+
{
7+
#nullable enable
8+
public DateTime? Expiry { get; set; }
9+
public string? Token { get; set; }
10+
public string? ClientCertificateData { get; set; }
11+
public string? ClientKeyData { get; set; }
12+
#nullable disable
13+
14+
public bool IsValid()
15+
{
16+
return (!string.IsNullOrEmpty(Token) ||
17+
(!string.IsNullOrEmpty(ClientCertificateData) && !string.IsNullOrEmpty(ClientKeyData)));
18+
}
19+
}
20+
521
[JsonPropertyName("apiVersion")]
622
public string ApiVersion { get; set; }
723
[JsonPropertyName("kind")]
824
public string Kind { get; set; }
925
[JsonPropertyName("status")]
10-
public IDictionary<string, string> Status { get; set; }
26+
public ExecStatus Status { get; set; }
1127
}
1228
}

src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs

+13-16
Original file line numberDiff line numberDiff line change
@@ -444,15 +444,21 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
444444
throw new KubeConfigException("External command execution missing ApiVersion key");
445445
}
446446

447-
var (accessToken, clientCertificateData, clientCertificateKeyData) = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution);
448-
AccessToken = accessToken;
447+
var response = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution);
448+
AccessToken = response.Status.Token;
449449
// When reading ClientCertificateData from a config file it will be base64 encoded, and code later in the system (see CertUtils.GeneratePfx)
450450
// expects ClientCertificateData and ClientCertificateKeyData to be base64 encoded because of this. However the string returned by external
451451
// 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.
452-
ClientCertificateData = clientCertificateData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(clientCertificateData));
453-
ClientCertificateKeyData = clientCertificateKeyData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(clientCertificateKeyData));
452+
ClientCertificateData = response.Status.ClientCertificateData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(response.Status.ClientCertificateData));
453+
ClientCertificateKeyData = response.Status.ClientKeyData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(response.Status.ClientKeyData));
454454

455455
userCredentialsFound = true;
456+
457+
// TODO: support client certificates here too.
458+
if (AccessToken != null)
459+
{
460+
TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution);
461+
}
456462
}
457463

458464
if (!userCredentialsFound)
@@ -525,7 +531,7 @@ public static Process CreateRunnableExternalProcess(ExternalExecution config)
525531
/// <returns>
526532
/// The token, client certificate data, and the client key data received from the external command execution
527533
/// </returns>
528-
public static (string, string, string) ExecuteExternalCommand(ExternalExecution config)
534+
public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config)
529535
{
530536
if (config == null)
531537
{
@@ -562,18 +568,9 @@ public static (string, string, string) ExecuteExternalCommand(ExternalExecution
562568
$"external exec failed because api version {responseObject.ApiVersion} does not match {config.ApiVersion}");
563569
}
564570

565-
if (responseObject.Status.ContainsKey("token"))
566-
{
567-
return (responseObject.Status["token"], null, null);
568-
}
569-
else if (responseObject.Status.ContainsKey("clientCertificateData"))
571+
if (responseObject.Status.IsValid())
570572
{
571-
if (!responseObject.Status.ContainsKey("clientKeyData"))
572-
{
573-
throw new KubeConfigException($"external exec failed missing clientKeyData field in plugin output");
574-
}
575-
576-
return (null, responseObject.Status["clientCertificateData"], responseObject.Status["clientKeyData"]);
573+
return responseObject;
577574
}
578575
else
579576
{

src/KubernetesClient/KubernetesClientConfiguration.InCluster.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public static bool IsInCluster()
2424
var host = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST");
2525
var port = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_PORT");
2626

27-
if (String.IsNullOrEmpty(host) || String.IsNullOrEmpty(port))
27+
if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(port))
2828
{
2929
return false;
3030
}

0 commit comments

Comments
 (0)