Skip to content

Commit

Permalink
Reimplement legacy "exec" credential plugin support as an authenticat…
Browse files Browse the repository at this point in the history
…ion-strategy

#108 #136
  • Loading branch information
tintoy committed Aug 10, 2022
1 parent fc12ce6 commit 89f4549
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 114 deletions.
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,5 @@ csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_indent_case_contents=true
csharp_indent_case_contents_when_block=false
25 changes: 18 additions & 7 deletions src/KubeClient.Extensions.KubeConfig/K8sConfig.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using YamlDotNet.Serialization;

namespace KubeClient
{
using Extensions.KubeConfig.Models;
using KubeClient.Authentication;
using Microsoft.Extensions.Logging;
using System.Security.Cryptography.X509Certificates;

/// <summary>
/// Kubernetes client configuration.
Expand Down Expand Up @@ -65,16 +66,16 @@ public static string Locate()
//Mirror the logic that kubectl and other Kubernetes tools use for homedir resolution: https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/util/homedir/homedir.go
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var home = Environment.GetEnvironmentVariable("HOME");
string home = Environment.GetEnvironmentVariable("HOME");
if (!String.IsNullOrEmpty(home))
return Path.Combine(home, ".kube", "config");

var homeDrive = Environment.GetEnvironmentVariable("HOMEDRIVE");
var homePath = Environment.GetEnvironmentVariable("HOMEPATH");
string homeDrive = Environment.GetEnvironmentVariable("HOMEDRIVE");
string homePath = Environment.GetEnvironmentVariable("HOMEPATH");
if (!String.IsNullOrEmpty(homeDrive) && !String.IsNullOrEmpty(homePath))
return Path.Combine(homeDrive + homePath, ".kube", "config");

var userProfile = Environment.GetEnvironmentVariable("USERPROFILE");
string userProfile = Environment.GetEnvironmentVariable("USERPROFILE");
if (!String.IsNullOrEmpty(userProfile))
return Path.Combine(userProfile, ".kube", "config");
}
Expand Down Expand Up @@ -192,7 +193,6 @@ public KubeClientOptions ConfigureKubeClientOptions(KubeClientOptions kubeClient

kubeClientOptions.ApiEndPoint = new Uri(targetCluster.Config.Server);
kubeClientOptions.KubeNamespace = defaultKubeNamespace;
kubeClientOptions.ClientCertificate = targetUser.Config.GetClientCertificate();
kubeClientOptions.AllowInsecure = targetCluster.Config.AllowInsecure;
kubeClientOptions.CertificationAuthorityCertificate = targetCluster.Config.GetCACertificate();

Expand Down Expand Up @@ -237,6 +237,17 @@ public KubeClientOptions ConfigureKubeClientOptions(KubeClientOptions kubeClient

kubeClientOptions.AuthStrategy = authStrategy;
}
else if (targetUser.Config.Exec != null)
{
CredentialPluginConfig credentialPluginConfig = targetUser.Config.Exec;

kubeClientOptions.AuthStrategy = KubeAuthStrategy.CredentialPlugin(
pluginApiVersion: credentialPluginConfig.ApiVersion,
command: credentialPluginConfig.Command,
arguments: credentialPluginConfig.Arguments,
environmentVariables: credentialPluginConfig.EnvironmentVariables.ToDictionary(variable => variable.Name, variable => variable.Value)
);
}
else
kubeClientOptions.AuthStrategy = KubeAuthStrategy.None;
}
Expand Down
88 changes: 60 additions & 28 deletions src/KubeClient.Extensions.WebSockets/K8sWebSocketOptions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using KubeClient.Authentication;
using System;
using System.Collections.Generic;
using System.Diagnostics;
Expand Down Expand Up @@ -61,7 +62,7 @@ public K8sWebSocketOptions()
/// Default is <see cref="SslProtocols.None"/>, which lets the platform select the most appropriate protocol.
/// </remarks>
public SslProtocols EnabledSslProtocols { get; set; } = SslProtocols.None;

/// <summary>
/// The WebSocket keep-alive interval.
/// </summary>
Expand All @@ -80,44 +81,75 @@ public static K8sWebSocketOptions FromClientOptions(IKubeApiClient client)
{
if (client == null)
throw new ArgumentNullException(nameof(client));

var socketOptions = new K8sWebSocketOptions();

KubeClientOptions clientOptions = client.GetClientOptions();

if (!String.IsNullOrWhiteSpace(clientOptions.AccessToken))
socketOptions.RequestHeaders["Authorization"] = $"Bearer {clientOptions.AccessToken}";

if (clientOptions.ClientCertificate != null)
socketOptions.ClientCertificates.Add(clientOptions.ClientCertificate);
return FromClientOptions(clientOptions);
}

/// <summary>
/// Create <see cref="K8sWebSocketOptions"/> using the client's authentication settings.
/// </summary>
/// <param name="clientOptions">
/// The <see cref="KubeClientOptions"/>.
/// </param>
/// <returns>
/// The configured <see cref="K8sWebSocketOptions"/>.
/// </returns>
public static K8sWebSocketOptions FromClientOptions(KubeClientOptions clientOptions)
{
if (clientOptions == null)
throw new ArgumentNullException(nameof(clientOptions));

var socketOptions = new K8sWebSocketOptions();

// TODO: Expose functionality for obtaining access token via KubeAuthStrategy and call it from here, rather than using the special-case credential configuration logic below.

switch (clientOptions.AuthStrategy)
{
case CertificateAuthStrategy certificateAuthStrategy:
{
if (certificateAuthStrategy.Certificate != null)
socketOptions.ClientCertificates.Add(certificateAuthStrategy.Certificate);

break;
}
case BearerTokenAuthStrategy bearerTokenAuthStrategy:
{
if (!String.IsNullOrWhiteSpace(bearerTokenAuthStrategy.Token))
socketOptions.RequestHeaders["Authorization"] = $"Bearer {bearerTokenAuthStrategy.Token}";

break;
}
}

if (clientOptions.CertificationAuthorityCertificate != null)
{
socketOptions.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
if (sslPolicyErrors != SslPolicyErrors.RemoteCertificateChainErrors)
return false;

try
{
using (X509Chain certificateChain = new X509Chain())
{
certificateChain.ChainPolicy.ExtraStore.Add(clientOptions.CertificationAuthorityCertificate);
certificateChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
return certificateChain.Build(
{
if (sslPolicyErrors != SslPolicyErrors.RemoteCertificateChainErrors)
return false;

try
{
using (X509Chain certificateChain = new X509Chain())
{
certificateChain.ChainPolicy.ExtraStore.Add(clientOptions.CertificationAuthorityCertificate);
certificateChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;

return certificateChain.Build(
(X509Certificate2)certificate
);
}
}
catch (Exception chainException)
{
}
}
catch (Exception chainException)
{
Debug.WriteLine(chainException);

return false;
}
};
return false;
}
};
}
else if (clientOptions.AllowInsecure)
socketOptions.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace KubeClient.Authentication
using MessageHandlers;

/// <summary>
/// A Kubernetes API authentication strategy that uses an access token provided by a legacy authentication provider ("auth-provider" in the K8s config).
/// A Kubernetes API authentication strategy that uses an access token provided by a authentication provider ("auth-provider" in the K8s config).
/// </summary>
public class BearerTokenProviderAuthStrategy
: KubeAuthStrategy
Expand Down Expand Up @@ -84,7 +84,7 @@ public override ClientBuilder Configure(ClientBuilder clientBuilder)
Validate();

return clientBuilder.AddHandler(
() => new CommandBearerTokenHandler(Command, Arguments, Selector, ExpirySelector, InitialToken, InitialTokenExpiryUtc)
() => new ExecCommandBearerTokenHandler(Command, Arguments, Selector, ExpirySelector, InitialToken, InitialTokenExpiryUtc)
);
}

Expand Down
10 changes: 3 additions & 7 deletions src/KubeClient/Authentication/CredentialPluginAuthStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,9 @@ public override KubeAuthStrategy Clone()

clonedStrategy.Arguments.AddRange(Arguments);

foreach ( string variableName in EnvironmentVariables.Keys )
{
clonedStrategy.EnvironmentVariables.Add(variableName,
value: EnvironmentVariables[variableName]
);
}

foreach ((string variableName, string variableValue) in EnvironmentVariables)
clonedStrategy.EnvironmentVariables.Add(variableName, variableValue);

return clonedStrategy;
}
}
Expand Down
2 changes: 0 additions & 2 deletions src/KubeClient/KubeApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ public static KubeApiClient Create(string apiEndPoint, string accessToken, X509C
{
ApiEndPoint = new Uri(apiEndPoint),
AuthStrategy = KubeAuthStrategy.BearerToken(accessToken),
AccessToken = accessToken,
CertificationAuthorityCertificate = expectServerCertificate,
LoggerFactory = loggerFactory
});
Expand Down Expand Up @@ -249,7 +248,6 @@ public static KubeApiClient Create(string apiEndPoint, X509Certificate2 clientCe
{
ApiEndPoint = new Uri(apiEndPoint),
AuthStrategy = KubeAuthStrategy.ClientCertificate(clientCertificate),
ClientCertificate = clientCertificate,
CertificationAuthorityCertificate = expectServerCertificate,
LoggerFactory = loggerFactory
});
Expand Down
47 changes: 47 additions & 0 deletions src/KubeClient/KubeAuthStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace KubeClient
{
using Authentication;
using System.Collections.Generic;

/// <summary>
/// The base class for implementations of authentication to the Kubernetes API.
Expand Down Expand Up @@ -123,5 +124,51 @@ public abstract class KubeAuthStrategy
InitialToken = initialToken,
InitialTokenExpiryUtc = initialTokenExpiryUtc
};

/// <summary>
/// Use a client-go credential plugin ("exec" in the K8s config).
/// </summary>
/// <param name="pluginApiVersion">
/// The plugin API version.
///
/// <para>
/// The API version returned by the plugin MUST match the version specified here.
/// </para>
/// </param>
/// <param name="command">
/// The command used to generate an access token for authenticating to the Kubernetes API.
/// </param>
/// <param name="arguments">
/// The arguments (if any) for the command used to generate an access token for authenticating to the Kubernetes API.
/// </param>
/// <param name="environmentVariables">
/// The environment variables arguments (if any) for the command used to generate an access token for authenticating to the Kubernetes API.
/// </param>
/// <returns>
/// The configured <see cref="CredentialPluginAuthStrategy"/>.
/// </returns>
public static CredentialPluginAuthStrategy CredentialPlugin(string pluginApiVersion, string command, IReadOnlyList<string> arguments = null, IReadOnlyDictionary<string, string> environmentVariables = null)
{
if (arguments == null)
throw new ArgumentNullException(nameof(arguments));

var authStrategy = new CredentialPluginAuthStrategy
{
PluginApiVersion = pluginApiVersion,
Command = command,
};

if (arguments != null)
authStrategy.Arguments.AddRange(arguments);

if (environmentVariables != null)
{
foreach ((string variableName, string variableValue) in authStrategy.EnvironmentVariables)
authStrategy.EnvironmentVariables.Add(variableName, variableValue);
}

return authStrategy;
}

}
}
62 changes: 0 additions & 62 deletions src/KubeClient/KubeClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,61 +43,11 @@ public KubeClientOptions(string apiEndPoint)
/// </summary>
public Uri ApiEndPoint { get; set; }

/// <summary>
/// The access token used to authenticate to the Kubernetes API.
/// </summary>
public string AccessToken { get; set; }

/// <summary>
/// The username used to authenticate to the Kubernetes API.
/// </summary>
public string Username { get; set; }

/// <summary>
/// The password used to authenticate to the Kubernetes API.
/// </summary>
public string Password { get; set; }

/// <summary>
/// The command used to generate an access token for authenticating to the Kubernetes API.
/// </summary>
public string AccessTokenCommand { get; set; }

/// <summary>
/// The command arguments used to generate an access token for authenticating to the Kubernetes API.
/// </summary>
public string AccessTokenCommandArguments { get; set; }

/// <summary>
/// The Go-style selector used to retrieve the access token from the command output.
/// </summary>
public string AccessTokenSelector { get; set; }

/// <summary>
/// The Go-style selector used to retrieve the access token's expiry date/time from the command output.
/// </summary>
public string AccessTokenExpirySelector { get; set; }

/// <summary>
/// The initial access token used to authenticate to the Kubernetes API.
/// </summary>
public string InitialAccessToken { get; set; }

/// <summary>
/// The initial token expiry used to authenticate to the Kubernetes API.
/// </summary>
public DateTime? InitialTokenExpiryUtc { get; set; }

/// <summary>
/// The strategy used for authenticating to the Kubernetes API.
/// </summary>
public KubeAuthStrategy AuthStrategy { get; set; }

/// <summary>
/// The client certificate used to authenticate to the Kubernetes API.
/// </summary>
public X509Certificate2 ClientCertificate { get; set; }

/// <summary>
/// The expected CA certificate used by the Kubernetes API.
/// </summary>
Expand Down Expand Up @@ -141,18 +91,10 @@ public KubeClientOptions Clone()
{
var clonedOptions = new KubeClientOptions
{
AccessToken = AccessToken,
AccessTokenCommand = AccessTokenCommand,
AccessTokenCommandArguments = AccessTokenCommandArguments,
AccessTokenExpirySelector = AccessTokenExpirySelector,
AccessTokenSelector = AccessTokenSelector,
AllowInsecure = AllowInsecure,
ApiEndPoint = ApiEndPoint,
AuthStrategy = AuthStrategy,
CertificationAuthorityCertificate = CertificationAuthorityCertificate,
ClientCertificate = ClientCertificate,
InitialAccessToken = InitialAccessToken,
InitialTokenExpiryUtc = InitialTokenExpiryUtc,
KubeNamespace = KubeNamespace,
LoggerFactory = LoggerFactory,
LogHeaders = LogHeaders,
Expand All @@ -175,9 +117,6 @@ public KubeClientOptions EnsureValid()
if (ApiEndPoint == null || !ApiEndPoint.IsAbsoluteUri)
throw new KubeClientException("Invalid KubeClientOptions: must specify a valid API end-point.");

if (ClientCertificate != null && !ClientCertificate.HasPrivateKey)
throw new KubeClientException("Invalid KubeClientOptions: the private key for the supplied client certificate is not available.");

if (String.IsNullOrWhiteSpace(KubeNamespace))
throw new KubeClientException("Invalid KubeClientOptions: must specify a valid default namespace.");

Expand Down Expand Up @@ -216,7 +155,6 @@ public static KubeClientOptions FromPodServiceAccount(string serviceAccountPath
{
ApiEndPoint = new Uri(apiEndPoint),
AuthStrategy = KubeAuthStrategy.BearerToken(accessToken),
AccessToken = accessToken,
CertificationAuthorityCertificate = kubeCACertificate,
KubeNamespace = defaultNamespace
};
Expand Down
Loading

0 comments on commit 89f4549

Please sign in to comment.