Skip to content
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

Out-of-tree client authentication providers (UserCredentials exec option) for asp.net core applications #359

Merged
merged 12 commits into from Mar 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/KubernetesClient/KubeConfigModels/ExecCredentialResponse.cs
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 IDictionary<string, string> Status { get; set; }
}
}
26 changes: 26 additions & 0 deletions src/KubernetesClient/KubeConfigModels/ExternalExecution.cs
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 IDictionary<string, string> EnvironmentVariables { get; set; }
/// <summary>
/// Arguments to pass when executing the plugin. Optional.
/// </summary>
[YamlMember(Alias = "args")]
public IList<string> Arguments { get; set; }
}
}
6 changes: 6 additions & 0 deletions src/KubernetesClient/KubeConfigModels/UserCredentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,11 @@ public class UserCredentials
/// </summary>
[YamlMember(Alias = "extensions")]
public IDictionary<string, dynamic> Extensions { get; set; }

/// <summary>
/// Gets or sets external command and its arguments to receive user credentials
/// </summary>
[YamlMember(Alias = "exec")]
public ExternalExecution ExternalExecution { get; set; }
}
}
199 changes: 151 additions & 48 deletions src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using System;
#if NETSTANDARD2_0
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Diagnostics;
#endif
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
Expand All @@ -8,6 +12,7 @@
using k8s.Exceptions;
using k8s.KubeConfigModels;


namespace k8s
{
public partial class KubernetesClientConfiguration
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)");
}
Expand Down Expand Up @@ -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 NETSTANDARD2_0
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(
Expand All @@ -365,6 +390,84 @@ public static string RenewAzureToken(string tenantId, string clientId, string ap
throw new KubeConfigException("Refresh not supported.");
}

#if NETSTANDARD2_0
/// <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}");

// Wait for a maximum of 5 seconds, if a response takes longer probably something went wrong...
process.WaitForExit(5);

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>
Expand Down
Loading