-
Notifications
You must be signed in to change notification settings - Fork 305
Out-of-tree client authentication providers (UserCredentials exec option) for asp.net core applications #359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
1269c41
fbe92c1
887fa41
8eb021b
8ac68da
5b69353
93cd4d8
c7bacf6
77e18f2
5b40f28
70026f2
b240d87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
using System.Collections.Generic; | ||
using Newtonsoft.Json; | ||
|
||
namespace k8s.KubeConfigModels | ||
{ | ||
public class ExecCredentialResponse | ||
{ | ||
[JsonProperty("apiVersion")] | ||
public string ApiVersion { get; set; } | ||
[JsonProperty("kind")] | ||
public string Kind { get; set; } | ||
[JsonProperty("status")] | ||
public Dictionary<string, string> Status { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
using System.Collections.Generic; | ||
using YamlDotNet.Serialization; | ||
|
||
namespace k8s.KubeConfigModels | ||
{ | ||
public class ExternalExecution | ||
{ | ||
[YamlMember(Alias = "apiVersion")] | ||
public string ApiVersion { get; set; } | ||
/// <summary> | ||
/// The command to execute. Required. | ||
/// </summary> | ||
[YamlMember(Alias = "command")] | ||
public string Command { get; set; } | ||
/// <summary> | ||
/// Environment variables to set when executing the plugin. Optional. | ||
/// </summary> | ||
[YamlMember(Alias = "env")] | ||
public Dictionary<string, string> EnvironmentVariables { get; set; } | ||
/// <summary> | ||
/// Arguments to pass when executing the plugin. Optional. | ||
/// </summary> | ||
[YamlMember(Alias = "args")] | ||
public List<string> Arguments { get; set; } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IList? |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,9 @@ | ||
using System; | ||
#if NETCOREAPP | ||
using Newtonsoft.Json; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
#endif | ||
using System.IO; | ||
using System.Linq; | ||
using System.Runtime.InteropServices; | ||
|
@@ -8,6 +12,7 @@ | |
using k8s.Exceptions; | ||
using k8s.KubeConfigModels; | ||
|
||
|
||
namespace k8s | ||
{ | ||
public partial class KubernetesClientConfiguration | ||
|
@@ -28,15 +33,19 @@ public partial class KubernetesClientConfiguration | |
/// <summary> | ||
/// Initializes a new instance of the <see cref="KubernetesClientConfiguration" /> from config file | ||
/// </summary> | ||
public static KubernetesClientConfiguration BuildDefaultConfig() { | ||
public static KubernetesClientConfiguration BuildDefaultConfig() | ||
{ | ||
var kubeconfig = Environment.GetEnvironmentVariable("KUBECONFIG"); | ||
if (kubeconfig != null) { | ||
if (kubeconfig != null) | ||
{ | ||
return BuildConfigFromConfigFile(kubeconfigPath: kubeconfig); | ||
} | ||
if (File.Exists(KubeConfigDefaultLocation)) { | ||
if (File.Exists(KubeConfigDefaultLocation)) | ||
{ | ||
return BuildConfigFromConfigFile(kubeconfigPath: KubeConfigDefaultLocation); | ||
} | ||
if (IsInCluster()) { | ||
if (IsInCluster()) | ||
{ | ||
return InClusterConfig(); | ||
} | ||
var config = new KubernetesClientConfiguration(); | ||
|
@@ -150,7 +159,7 @@ private static KubernetesClientConfiguration GetKubernetesClientConfiguration(st | |
var k8SConfiguration = new KubernetesClientConfiguration(); | ||
|
||
currentContext = currentContext ?? k8SConfig.CurrentContext; | ||
// only init context if context if set | ||
// only init context if context is set | ||
if (currentContext != null) | ||
{ | ||
k8SConfiguration.InitializeContext(k8SConfig, currentContext); | ||
|
@@ -214,7 +223,7 @@ private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext | |
Host = clusterDetails.ClusterEndpoint.Server; | ||
SkipTlsVerify = clusterDetails.ClusterEndpoint.SkipTlsVerify; | ||
|
||
if(!Uri.TryCreate(Host, UriKind.Absolute, out Uri uri)) | ||
if (!Uri.TryCreate(Host, UriKind.Absolute, out Uri uri)) | ||
{ | ||
throw new KubeConfigException($"Bad server host URL `{Host}` (cannot be parsed)"); | ||
} | ||
|
@@ -294,65 +303,81 @@ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext) | |
switch (userDetails.UserCredentials.AuthProvider.Name) | ||
{ | ||
case "azure": | ||
{ | ||
var config = userDetails.UserCredentials.AuthProvider.Config; | ||
if (config.ContainsKey("expires-on")) | ||
{ | ||
var expiresOn = Int32.Parse(config["expires-on"]); | ||
DateTimeOffset expires; | ||
#if NET452 | ||
var config = userDetails.UserCredentials.AuthProvider.Config; | ||
if (config.ContainsKey("expires-on")) | ||
{ | ||
var expiresOn = Int32.Parse(config["expires-on"]); | ||
DateTimeOffset expires; | ||
#if NET452 | ||
var epoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); | ||
expires = epoch.AddSeconds(expiresOn); | ||
#else | ||
expires = DateTimeOffset.FromUnixTimeSeconds(expiresOn); | ||
#endif | ||
#else | ||
expires = DateTimeOffset.FromUnixTimeSeconds(expiresOn); | ||
#endif | ||
|
||
if (DateTimeOffset.Compare(expires | ||
, DateTimeOffset.Now) | ||
<= 0) | ||
{ | ||
var tenantId = config["tenant-id"]; | ||
var clientId = config["client-id"]; | ||
var apiServerId = config["apiserver-id"]; | ||
var refresh = config["refresh-token"]; | ||
var newToken = RenewAzureToken(tenantId | ||
, clientId | ||
, apiServerId | ||
, refresh); | ||
config["access-token"] = newToken; | ||
if (DateTimeOffset.Compare(expires | ||
, DateTimeOffset.Now) | ||
<= 0) | ||
{ | ||
var tenantId = config["tenant-id"]; | ||
var clientId = config["client-id"]; | ||
var apiServerId = config["apiserver-id"]; | ||
var refresh = config["refresh-token"]; | ||
var newToken = RenewAzureToken(tenantId | ||
, clientId | ||
, apiServerId | ||
, refresh); | ||
config["access-token"] = newToken; | ||
} | ||
} | ||
} | ||
|
||
AccessToken = config["access-token"]; | ||
userCredentialsFound = true; | ||
break; | ||
} | ||
AccessToken = config["access-token"]; | ||
userCredentialsFound = true; | ||
break; | ||
} | ||
case "gcp": | ||
{ | ||
var config = userDetails.UserCredentials.AuthProvider.Config; | ||
const string keyExpire = "expiry"; | ||
if (config.ContainsKey(keyExpire)) | ||
{ | ||
if (DateTimeOffset.TryParse(config[keyExpire] | ||
, out DateTimeOffset expires)) | ||
var config = userDetails.UserCredentials.AuthProvider.Config; | ||
const string keyExpire = "expiry"; | ||
if (config.ContainsKey(keyExpire)) | ||
{ | ||
if (DateTimeOffset.Compare(expires | ||
, DateTimeOffset.Now) | ||
<= 0) | ||
if (DateTimeOffset.TryParse(config[keyExpire] | ||
, out DateTimeOffset expires)) | ||
{ | ||
throw new KubeConfigException("Refresh not supported."); | ||
if (DateTimeOffset.Compare(expires | ||
, DateTimeOffset.Now) | ||
<= 0) | ||
{ | ||
throw new KubeConfigException("Refresh not supported."); | ||
} | ||
} | ||
} | ||
} | ||
|
||
AccessToken = config["access-token"]; | ||
userCredentialsFound = true; | ||
break; | ||
} | ||
AccessToken = config["access-token"]; | ||
userCredentialsFound = true; | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
|
||
#if NETCOREAPP2_1 | ||
if (userDetails.UserCredentials.ExternalExecution != null) | ||
{ | ||
if (string.IsNullOrWhiteSpace(userDetails.UserCredentials.ExternalExecution.Command)) | ||
throw new KubeConfigException( | ||
"External command execution to receive user credentials must include a command to execute"); | ||
if (string.IsNullOrWhiteSpace(userDetails.UserCredentials.ExternalExecution.ApiVersion)) | ||
throw new KubeConfigException("External command execution missing ApiVersion key"); | ||
|
||
var token = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution); | ||
AccessToken = token; | ||
|
||
userCredentialsFound = true; | ||
} | ||
#endif | ||
|
||
if (!userCredentialsFound) | ||
{ | ||
throw new KubeConfigException( | ||
|
@@ -365,6 +390,83 @@ public static string RenewAzureToken(string tenantId, string clientId, string ap | |
throw new KubeConfigException("Refresh not supported."); | ||
} | ||
|
||
#if NETCOREAPP | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why this is specific to netcore, can it be netstandard? |
||
/// <summary> | ||
/// Implementation of the proposal for out-of-tree client | ||
/// authentication providers as described here -- | ||
/// https://github.com/kubernetes/community/blob/master/contributors/design-proposals/auth/kubectl-exec-plugins.md | ||
/// Took inspiration from python exec_provider.py -- | ||
/// https://github.com/kubernetes-client/python-base/blob/master/config/exec_provider.py | ||
/// </summary> | ||
/// <param name="config">The external command execution configuration</param> | ||
/// <returns>The token received from the external commmand execution</returns> | ||
public static string ExecuteExternalCommand(ExternalExecution config) | ||
{ | ||
var execInfo = new Dictionary<string, dynamic> | ||
{ | ||
{"apiVersion", config.ApiVersion}, | ||
{"kind", "ExecCredentials"}, | ||
{"spec", new Dictionary<string, bool> | ||
{ | ||
{"interactive", Environment.UserInteractive} | ||
}} | ||
}; | ||
|
||
var process = new Process(); | ||
|
||
process.StartInfo.Environment.Add("KUBERNETES_EXEC_INFO", | ||
JsonConvert.SerializeObject(execInfo)); | ||
|
||
if (config.EnvironmentVariables != null) | ||
foreach (var configEnvironmentVariableKey in config.EnvironmentVariables.Keys) | ||
process.StartInfo.Environment.Add(key: configEnvironmentVariableKey, | ||
value: config.EnvironmentVariables[configEnvironmentVariableKey]); | ||
|
||
process.StartInfo.FileName = config.Command; | ||
if (config.Arguments != null) | ||
process.StartInfo.Arguments = string.Join(" ", config.Arguments); | ||
process.StartInfo.RedirectStandardOutput = true; | ||
process.StartInfo.RedirectStandardError = true; | ||
process.StartInfo.UseShellExecute = false; | ||
|
||
try | ||
{ | ||
process.Start(); | ||
} | ||
catch (Exception ex) | ||
{ | ||
throw new KubeConfigException($"external exec failed due to: {ex.Message}"); | ||
} | ||
|
||
var stdout = process.StandardOutput.ReadToEnd(); | ||
var stderr = process.StandardOutput.ReadToEnd(); | ||
if (string.IsNullOrWhiteSpace(stderr) == false) | ||
throw new KubeConfigException($"external exec failed due to: {stderr}"); | ||
|
||
process.WaitForExit(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. timeout? |
||
|
||
try | ||
{ | ||
var responseObject = JsonConvert.DeserializeObject<ExecCredentialResponse>(stdout); | ||
if (responseObject == null || responseObject.ApiVersion != config.ApiVersion) | ||
throw new KubeConfigException( | ||
$"external exec failed because api version {responseObject.ApiVersion} does not match {config.ApiVersion}"); | ||
return responseObject.Status["token"]; | ||
} | ||
catch (JsonSerializationException ex) | ||
{ | ||
throw new KubeConfigException($"external exec failed due to failed deserialization process: {ex}"); | ||
} | ||
catch (Exception ex) | ||
{ | ||
throw new KubeConfigException($"external exec failed due to uncaught exception: {ex}"); | ||
} | ||
|
||
|
||
|
||
} | ||
#endif | ||
|
||
/// <summary> | ||
/// Loads entire Kube Config from default or explicit file path | ||
/// </summary> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IDictionary if possible