Merge pull request #4 from krabhishek8260/auth

Add support for connecting/authenticating through kubeconfig
This commit is contained in:
Brendan Burns
2017-06-19 21:42:56 -07:00
committed by GitHub
18 changed files with 709 additions and 24 deletions

6
.gitignore vendored
View File

@@ -2,3 +2,9 @@
.vs
obj/
bin/
# User-specific VS files
*.suo
*.user
*.userosscache
*.sln.docstates

View File

@@ -1,5 +1,6 @@
# csharp
Work In Progress
Currently only supported on Linux
# Generating the client code

View File

@@ -1,18 +1,17 @@
using System;
using k8s;
namespace simple
namespace simple
{
using System;
using System.IO;
using k8s;
class PodList
{
static void Main(string[] args)
{
IKubernetes client = new Kubernetes();
client.BaseUri = new Uri("http://localhost:8001");
var listTask = client.ListNamespacedPodWithHttpMessagesAsync("default");
listTask.Wait();
var list = listTask.Result.Body;
var k8sClientConfig = new KubernetesClientConfiguration();
IKubernetes client = new Kubernetes(k8sClientConfig);
var listTask = client.ListNamespacedPodWithHttpMessagesAsync("default").Result;
var list = listTask.Body;
foreach (var item in list.Items) {
Console.WriteLine(item.Metadata.Name);
}

View File

@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="3.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
<ProjectReference Include="..\..\src\csharp.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="3.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
<ProjectReference Include="..\..\src\KubernetesClient.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,24 @@
namespace k8s.Exceptions
{
using System;
/// <summary>
/// The exception that is thrown when the kube config is invalid
/// </summary>
public class KubeConfigException : Exception
{
public KubeConfigException()
{
}
public KubeConfigException(string message)
: base(message)
{
}
public KubeConfigException(string message, Exception inner)
: base(message, inner)
{
}
}
}

View File

@@ -0,0 +1,24 @@
namespace k8s.Exceptions
{
using System;
/// <summary>
/// The exception that is thrown when there is a client exception
/// </summary>
public class KubernetesClientException : Exception
{
public KubernetesClientException()
{
}
public KubernetesClientException(string message)
: base(message)
{
}
public KubernetesClientException(string message, Exception inner)
: base(message, inner)
{
}
}
}

View File

@@ -0,0 +1,13 @@
namespace k8s.KubeConfigModels
{
using YamlDotNet.Serialization;
public class Cluster
{
[YamlMember(Alias = "cluster")]
public ClusterEndpoint ClusterEndpoint { get; set; }
[YamlMember(Alias = "name")]
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
namespace k8s.KubeConfigModels
{
using YamlDotNet.Serialization;
public class ClusterEndpoint
{
[YamlMember(Alias = "certificate-authority-data")]
public string CertificateAuthorityData { get; set; }
[YamlMember(Alias = "server")]
public string Server { get; set; }
[YamlMember(Alias = "insecure-skip-tls-verify")]
public bool SkipTlsVerify { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
namespace k8s.KubeConfigModels
{
using YamlDotNet.Serialization;
public class Context
{
[YamlMember(Alias = "context")]
public ContextDetails ContextDetails { get; set; }
[YamlMember(Alias = "name")]
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
namespace k8s.KubeConfigModels
{
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;
public class ContextDetails
{
[YamlMember(Alias = "cluster")]
public string Cluster { get; set; }
[YamlMember(Alias = "user")]
public string User { get; set; }
[YamlMember(Alias = "namespace")]
public string Namespace { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
namespace k8s.KubeConfigModels
{
using System.Collections.Generic;
using YamlDotNet.Serialization;
/// <summary>
/// kubeconfig configuration model
/// </summary>
public class K8SConfiguration
{
[YamlMember(Alias = "apiVersion")]
public string ApiVersion { get; set; }
[YamlMember(Alias = "kind")]
public string Kind { get; set; }
[YamlMember(Alias = "current-context")]
public string CurrentContext { get; set; }
[YamlMember(Alias = "contexts")]
public IEnumerable<Context> Contexts { get; set; }
[YamlMember(Alias = "clusters")]
public IEnumerable<Cluster> Clusters { get; set; }
[YamlMember(Alias = "users")]
public IEnumerable<User> Users { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
namespace k8s.KubeConfigModels
{
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;
public class User
{
[YamlMember(Alias = "user")]
public UserCrednetials UserCredentials { get; set; }
[YamlMember(Alias = "name")]
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
namespace k8s.KubeConfigModels
{
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;
public class UserCrednetials
{
[YamlMember(Alias = "client-certificate-data")]
public string ClientCertificateData { get; set; }
[YamlMember(Alias = "client-key-data")]
public string ClientKeyData { get; set; }
[YamlMember(Alias = "token")]
public string Token { get; set; }
[YamlMember(Alias = "userName")]
public string UserName { get; set; }
[YamlMember(Alias = "password")]
public string Password { get; set; }
}
}

125
src/Kubernetes.Auth.cs Normal file
View File

@@ -0,0 +1,125 @@
namespace k8s
{
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using k8s.Exceptions;
using Microsoft.Rest;
public partial class Kubernetes : ServiceClient<Kubernetes>, IKubernetes
{
/// <summary>
/// Initializes a new instance of the <see cref="Kubernetes"/> class.
/// </summary>
/// <param name='config'>
/// Optional. The delegating handlers to add to the http client pipeline.
/// </param>
public Kubernetes(KubernetesClientConfiguration config)
{
this.Initialize();
this.CaCert = Utils.Base64Decode(config.SslCaCert);
this.BaseUri = new Uri(config.Host);
// ssl cert validation
Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> sslCertValidationFunc;
if (config.SkipTlsVerify)
{
sslCertValidationFunc = (sender, certificate, chain, sslPolicyErrors) => true;
}
else
{
sslCertValidationFunc = this.CertificateValidationCallBack;
}
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = sslCertValidationFunc
};
// set credentails for the kubernernet client
this.SetCredentialsAsync(config, handler).Wait();
this.InitializeHttpClient(handler);
}
private string CaCert { get; set; }
/// <summary>
/// Set credentials for the Client
/// </summary>
/// <param name="config">k8s client configuration</param>
/// <param name="handler">http client handler for the rest client</param>
/// <returns>Task</returns>
private async Task SetCredentialsAsync(KubernetesClientConfiguration config, HttpClientHandler handler)
{
// set the Credentails for token based auth
if (!string.IsNullOrWhiteSpace(config.AccessToken))
{
this.Credentials = new KubernetesClientCredentials(config.AccessToken);
}
else if (!string.IsNullOrWhiteSpace(config.Username) && !string.IsNullOrWhiteSpace(config.Password))
{
this.Credentials = new KubernetesClientCredentials(config.Username, config.Password);
}
// othwerwise set handler for clinet cert based auth
else if (!string.IsNullOrWhiteSpace(config.ClientCertificateData) && !string.IsNullOrWhiteSpace(config.ClientCertificateKey))
{
var pfxFilePath = await Utils.GeneratePfxAsync(config).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(pfxFilePath))
{
throw new KubernetesClientException("Failed to generate pfx file");
}
var cert = new X509Certificate2(pfxFilePath, string.Empty, X509KeyStorageFlags.PersistKeySet);
handler.ClientCertificates.Add(cert);
}
else
{
throw new KubeConfigException("Configuration does not have appropriate auth credentials");
}
}
/// <summary>
/// SSl Cert Validation Callback
/// </summary>
/// <param name="sender">sender</param>
/// <param name="certificate">client certificate</param>
/// <param name="chain">chain</param>
/// <param name="sslPolicyErrors">ssl policy errors</param>
/// <returns>true if valid cert</returns>
[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Unused by design")]
private bool CertificateValidationCallBack(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
// If the certificate is a valid, signed certificate, return true.
if (sslPolicyErrors == SslPolicyErrors.None)
{
return true;
}
// If there are errors in the certificate chain, look at each error to determine the cause.
if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) != 0)
{
X509Chain chain0 = new X509Chain();
chain0.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
// add all your extra certificate chain
chain0.ChainPolicy.ExtraStore.Add(new X509Certificate2(System.Text.Encoding.UTF8.GetBytes(this.CaCert)));
chain0.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
var isValid = chain0.Build((X509Certificate2)certificate);
return isValid;
}
else
{
// In all other cases, return false.
return false;
}
}
}
}

5
src/csharp.csproj → src/KubernetesClient.csproj Executable file → Normal file
View File

@@ -3,8 +3,13 @@
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Remove="GlobalSuppressions.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="1.1.0" />
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="3.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
<PackageReference Include="YamlDotNet.NetCore" Version="1.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,220 @@
namespace k8s
{
using System;
using System.IO;
using System.Linq;
using k8s.Exceptions;
using k8s.KubeConfigModels;
using YamlDotNet.Serialization;
/// <summary>
/// Represents a set of kubernetes client configuration settings
/// </summary>
public class KubernetesClientConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="KubernetesClientConfiguration"/> class.
/// Initializes a new instance of the ClientConfiguration class
/// </summary>
/// <param name="kubeconfig">kubeconfig file info</param>
/// <param name="currentContext">Context to use from kube config</param>
public KubernetesClientConfiguration(FileInfo kubeconfig = null, string currentContext = null)
{
if (kubeconfig == null)
{
kubeconfig = new FileInfo(KubeConfigDefaultLocation);
}
var k8SConfig = this.LoadKubeConfig(kubeconfig);
this.Initialize(k8SConfig, currentContext);
}
/// <summary>
/// kubeconfig Default Location
/// </summary>
private static readonly string KubeConfigDefaultLocation = Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".kube/config");
/// <summary>
/// Gets CurrentContext
/// </summary>
public string CurrentContext { get; private set; }
/// <summary>
/// Gets Host
/// </summary>
public string Host { get; private set; }
/// <summary>
/// Gets SslCaCert
/// </summary>
public string SslCaCert { get; private set; }
/// <summary>
/// Gets ClientCertificateData
/// </summary>
public string ClientCertificateData { get; private set; }
/// <summary>
/// Gets ClientCertificate Key
/// </summary>
public string ClientCertificateKey { get; private set; }
/// <summary>
/// Gets a value indicating whether to skip ssl server cert validation
/// </summary>
public bool SkipTlsVerify { get; private set; }
/// <summary>
/// Gets or sets the HTTP user agent.
/// </summary>
/// <value>Http user agent.</value>
public string UserAgent { get; set; }
/// <summary>
/// Gets or sets the username (HTTP basic authentication).
/// </summary>
/// <value>The username.</value>
public string Username { get; set; }
/// <summary>
/// Gets or sets the password (HTTP basic authentication).
/// </summary>
/// <value>The password.</value>
public string Password { get; set; }
/// <summary>
/// Gets or sets the access token for OAuth2 authentication.
/// </summary>
/// <value>The access token.</value>
public string AccessToken { get; set; }
/// <summary>
/// Validates and Intializes Client Configuration
/// </summary>
/// <param name="k8SConfig">Kubernetes Configuration</param>
/// <param name="currentContext">Current Context</param>
private void Initialize(K8SConfiguration k8SConfig, string currentContext = null)
{
Context activeContext;
// set the currentCOntext to passed context if not null
if (!string.IsNullOrWhiteSpace(currentContext))
{
if (k8SConfig.Contexts == null)
{
throw new KubeConfigException("No contexts found in kubeconfig");
}
activeContext = k8SConfig.Contexts.FirstOrDefault(c => c.Name.Equals(currentContext, StringComparison.OrdinalIgnoreCase));
if (activeContext != null)
{
this.CurrentContext = activeContext.Name;
}
else
{
throw new KubeConfigException($"CurrentContext: {0} not found in contexts in kubeconfig");
}
}
// otherwise set current context from kubeconfig
else
{
activeContext = k8SConfig.Contexts.FirstOrDefault(c => c.Name.Equals(k8SConfig.CurrentContext, StringComparison.OrdinalIgnoreCase));
if (activeContext == null)
{
throw new KubeConfigException($"CurrentContext: {currentContext} not found in contexts in kubeconfig");
}
this.CurrentContext = activeContext.Name;
}
var clusterDetails = k8SConfig.Clusters.FirstOrDefault(c => c.Name.Equals(activeContext.ContextDetails.Cluster, StringComparison.OrdinalIgnoreCase));
if (clusterDetails?.ClusterEndpoint != null)
{
if (string.IsNullOrWhiteSpace(clusterDetails.ClusterEndpoint.Server))
{
throw new KubeConfigException($"server not found for current-context :{activeContext} in kubeconfig");
}
if (!clusterDetails.ClusterEndpoint.SkipTlsVerify &&
string.IsNullOrWhiteSpace(clusterDetails.ClusterEndpoint.CertificateAuthorityData))
{
throw new KubeConfigException($"certificate-authority-data not found for current-context :{activeContext} in kubeconfig");
}
this.Host = clusterDetails.ClusterEndpoint.Server;
this.SslCaCert = clusterDetails.ClusterEndpoint.CertificateAuthorityData;
this.SkipTlsVerify = clusterDetails.ClusterEndpoint.SkipTlsVerify;
}
else
{
throw new KubeConfigException($"Cluster details not found for current-context: {activeContext} in kubeconfig");
}
// set user details from kubeconfig
var userDetails = k8SConfig.Users.FirstOrDefault(c => c.Name.Equals(activeContext.ContextDetails.User, StringComparison.OrdinalIgnoreCase));
this.SetUserDetails(userDetails);
}
private void SetUserDetails(User userDetails)
{
if (userDetails == null)
{
throw new KubeConfigException("User not found for the current context in kubeconfig");
}
if (userDetails.UserCredentials == null)
{
throw new KubeConfigException($"User credentials not found for user: {userDetails.Name} in kubeconfig");
}
var userCredentialsFound = false;
// Basic and bearer tokens are mutually exclusive
if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.Token))
{
this.AccessToken = userDetails.UserCredentials.Token;
userCredentialsFound = true;
}
else if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.UserName) &&
!string.IsNullOrWhiteSpace(userDetails.UserCredentials.Password))
{
this.Username = userDetails.UserCredentials.UserName;
this.Password = userDetails.UserCredentials.Password;
userCredentialsFound = true;
}
// Token and cert based auth can co-exist
if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientCertificateData) &&
!string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientKeyData))
{
this.ClientCertificateData = userDetails.UserCredentials.ClientCertificateData;
this.ClientCertificateKey = userDetails.UserCredentials.ClientKeyData;
userCredentialsFound = true;
}
if (!userCredentialsFound)
{
throw new KubeConfigException($"User: {userDetails.Name} does not have appropriate auth credentials in kube config");
}
}
/// <summary>
/// Loads Kube Config
/// </summary>
/// <param name="config">Kube config file contents</param>
/// <returns>Instance of the <see cref="K8SConfiguration"/> class</returns>
private K8SConfiguration LoadKubeConfig(FileInfo kubeconfig)
{
if (!kubeconfig.Exists)
{
throw new KubeConfigException($"kubeconfig file not found at {kubeconfig.FullName}");
}
var kubeconfigContent = File.ReadAllText(kubeconfig.FullName);
var deserializeBuilder = new DeserializerBuilder();
var deserializer = deserializeBuilder.Build();
return deserializer.Deserialize<K8SConfiguration>(kubeconfigContent);
}
}
}

View File

@@ -0,0 +1,70 @@
namespace k8s
{
using System;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using k8s.Exceptions;
using Microsoft.Rest;
/// <summary>
/// Class to set the Kubernetes Client Credentials for token based auth
/// </summary>
public class KubernetesClientCredentials : ServiceClientCredentials
{
public KubernetesClientCredentials(string token)
{
if (string.IsNullOrWhiteSpace(token))
{
throw new ArgumentNullException(nameof(token));
}
this.AuthenticationToken = token;
this.AuthenticationScheme = "Bearer";
}
public KubernetesClientCredentials(string userName, string password)
{
if (string.IsNullOrWhiteSpace(userName))
{
throw new ArgumentNullException(nameof(userName));
}
if (string.IsNullOrWhiteSpace(userName))
{
throw new ArgumentNullException(nameof(password));
}
this.AuthenticationToken = Utils.Base64Encode(string.Format(CultureInfo.InvariantCulture, "{0}:{1}", userName, password));
this.AuthenticationScheme = "Basic";
}
private string AuthenticationToken { get; }
private string AuthenticationScheme { get; }
public override async Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (string.IsNullOrWhiteSpace(this.AuthenticationScheme))
{
throw new KubernetesClientException("AuthenticationScheme cannot be null. Please set the AuthenticationScheme to Basic/Bearer");
}
if (string.IsNullOrWhiteSpace(this.AuthenticationToken))
{
throw new KubernetesClientException("AuthenticationToken cannot be null. Please set the authentication token");
}
request.Headers.Authorization = new AuthenticationHeaderValue(this.AuthenticationScheme, this.AuthenticationToken);
await base.ProcessHttpRequestAsync(request, cancellationToken).ConfigureAwait(false);
}
}
}

86
src/Utils.cs Normal file
View File

@@ -0,0 +1,86 @@
namespace k8s
{
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading.Tasks;
public static class Utils
{
/// <summary>
/// Encode string in base64 format.
/// </summary>
/// <param name="text">string to be encoded.</param>
/// <returns>Encoded string.</returns>
public static string Base64Encode(string text)
{
return Convert.ToBase64String(Encoding.UTF8.GetBytes(text));
}
/// <summary>
/// Encode string in base64 format.
/// </summary>
/// <param name="text">string to be encoded.</param>
/// <returns>Encoded string.</returns>
public static string Base64Decode(string text)
{
return Encoding.UTF8.GetString(Convert.FromBase64String(text));
}
/// <summary>
/// Generates pfx from client configuration
/// </summary>
/// <param name="config">Kuberentes Client Configuration</param>
/// <returns>Generated Pfx Path</returns>
/// TODO: kabhishek8260 Remplace the method with X509 Certificate with private key(in dotnet 2.0)
public static async Task<string> GeneratePfxAsync(KubernetesClientConfiguration config)
{
var userHomeDir = Environment.GetEnvironmentVariable("HOME");
var certDirPath = Path.Combine(userHomeDir, ".k8scerts");
Directory.CreateDirectory(certDirPath);
var filePrefix = config.CurrentContext;
var keyFilePath = Path.Combine(certDirPath, filePrefix + "key");
var certFilePath = Path.Combine(certDirPath, filePrefix + "cert");
var pfxFilePath = Path.Combine(certDirPath, filePrefix + "pfx");
using (FileStream fs = File.Create(keyFilePath))
{
byte[] info = Convert.FromBase64String(config.ClientCertificateKey);
await fs.WriteAsync(info, 0, info.Length).ConfigureAwait(false);
}
using (FileStream fs = File.Create(certFilePath))
{
byte[] info = Convert.FromBase64String(config.ClientCertificateData);
await fs.WriteAsync(info, 0, info.Length).ConfigureAwait(false);
}
var process = new Process();
process.StartInfo = new ProcessStartInfo()
{
FileName = @"/bin/bash",
Arguments = string.Format(
CultureInfo.InvariantCulture,
"-c \"openssl pkcs12 -export -out {0} -inkey {1} -in {2} -passout pass:\"",
pfxFilePath,
keyFilePath,
certFilePath),
CreateNoWindow = true,
RedirectStandardError = true,
RedirectStandardOutput = true
};
process.Start();
process.WaitForExit();
if (process.ExitCode == 0)
{
return pfxFilePath;
}
return null;
}
}
}