Skip to content

Implement Api Key authorisation on requests #3912

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

Merged
merged 5 commits into from
Jul 16, 2019
Merged
Show file tree
Hide file tree
Changes from 3 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
14 changes: 14 additions & 0 deletions src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public abstract class ConnectionConfiguration<T> : IConnectionConfigurationValue
private readonly ElasticsearchUrlFormatter _urlFormatter;

private BasicAuthenticationCredentials _basicAuthCredentials;
private ApiKeyAuthenticationCredentials _apiKeyAuthentication;
private X509CertificateCollection _clientCertificates;
private Action<IApiCallDetails> _completedRequestHandler = DefaultCompletedRequestHandler;
private int _connectionLimit;
Expand Down Expand Up @@ -169,6 +170,7 @@ protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection co

protected IElasticsearchSerializer UseThisRequestResponseSerializer { get; set; }
BasicAuthenticationCredentials IConnectionConfigurationValues.BasicAuthenticationCredentials => _basicAuthCredentials;
ApiKeyAuthenticationCredentials IConnectionConfigurationValues.ApiKeyAuthenticationCredentials => _apiKeyAuthentication;
SemaphoreSlim IConnectionConfigurationValues.BootstrapLock => _semaphore;
X509CertificateCollection IConnectionConfigurationValues.ClientCertificates => _clientCertificates;
IConnection IConnectionConfigurationValues.Connection => _connection;
Expand Down Expand Up @@ -434,6 +436,18 @@ public T BasicAuthentication(string username, string password) =>
public T BasicAuthentication(string username, SecureString password) =>
Assign(new BasicAuthenticationCredentials(username, password), (a, v) => a._basicAuthCredentials = v);

/// <summary>
/// Api Key to send with all requests to Elasticsearch
/// </summary>
public T ApiKeyAuthentication(string id, SecureString apiKey) =>
Assign(new ApiKeyAuthenticationCredentials(id, apiKey), (a, v) => a._apiKeyAuthentication = v);

/// <summary>
/// Api Key to send with all requests to Elasticsearch
/// </summary>
public T ApiKeyAuthentication(string id, string apiKey) =>
Assign(new ApiKeyAuthenticationCredentials(id, apiKey), (a, v) => a._apiKeyAuthentication = v);

/// <summary>
/// Allows for requests to be pipelined. http://en.wikipedia.org/wiki/HTTP_pipelining
/// <para>NOTE: HTTP pipelining must also be enabled in Elasticsearch for this to work properly.</para>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,19 @@ public interface IConnectionConfigurationValues : IDisposable
/// <summary>
/// Basic access authorization credentials to specify with all requests.
/// </summary>
/// <remarks>
/// Cannot be used in conjuction with <see cref="ApiKeyAuthenticationCredentials"/>
/// </remarks>
BasicAuthenticationCredentials BasicAuthenticationCredentials { get; }

/// <summary>
/// Api Key authorization credentials to specify with all requests.
/// </summary>
/// <remarks>
/// Cannot be used in conjuction with <see cref="BasicAuthenticationCredentials"/>
/// </remarks>
ApiKeyAuthenticationCredentials ApiKeyAuthenticationCredentials { get; }

/// <summary> Provides a semaphoreslim to transport implementations that need to limit access to a resource</summary>
SemaphoreSlim BootstrapLock { get; }

Expand Down
40 changes: 40 additions & 0 deletions src/Elasticsearch.Net/Configuration/RequestConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,20 @@ public interface IRequestConfiguration
/// Basic access authorization credentials to specify with this request.
/// Overrides any credentials that are set at the global IConnectionSettings level.
/// </summary>
/// <remarks>
/// Cannot be used in conjunction with <see cref="ApiKeyAuthenticationCredentials"/>
/// </remarks>
BasicAuthenticationCredentials BasicAuthenticationCredentials { get; set; }

/// <summary>
/// An API-key authorization credentials to specify with this request.
/// Overrides any credentials that are set at the global IConnectionSettings level.
/// </summary>
/// <remarks>
/// Cannot be used in conjunction with <see cref="BasicAuthenticationCredentials"/>
/// </remarks>
ApiKeyAuthenticationCredentials ApiKeyAuthenticationCredentials { get; set; }

/// <summary>
/// Use the following client certificates to authenticate this single request
/// </summary>
Expand Down Expand Up @@ -102,6 +114,8 @@ public class RequestConfiguration : IRequestConfiguration
public IReadOnlyCollection<int> AllowedStatusCodes { get; set; }
public BasicAuthenticationCredentials BasicAuthenticationCredentials { get; set; }

public ApiKeyAuthenticationCredentials ApiKeyAuthenticationCredentials { get; set; }

public X509CertificateCollection ClientCertificates { get; set; }
public string ContentType { get; set; }
public bool? DisableDirectStreaming { get; set; }
Expand Down Expand Up @@ -138,6 +152,7 @@ public RequestConfigurationDescriptor(IRequestConfiguration config)
Self.DisableDirectStreaming = config?.DisableDirectStreaming;
Self.AllowedStatusCodes = config?.AllowedStatusCodes;
Self.BasicAuthenticationCredentials = config?.BasicAuthenticationCredentials;
Self.ApiKeyAuthenticationCredentials = config?.ApiKeyAuthenticationCredentials;
Self.EnableHttpPipelining = config?.EnableHttpPipelining ?? true;
Self.RunAs = config?.RunAs;
Self.ClientCertificates = config?.ClientCertificates;
Expand All @@ -148,6 +163,7 @@ public RequestConfigurationDescriptor(IRequestConfiguration config)
string IRequestConfiguration.Accept { get; set; }
IReadOnlyCollection<int> IRequestConfiguration.AllowedStatusCodes { get; set; }
BasicAuthenticationCredentials IRequestConfiguration.BasicAuthenticationCredentials { get; set; }
ApiKeyAuthenticationCredentials IRequestConfiguration.ApiKeyAuthenticationCredentials { get; set; }
X509CertificateCollection IRequestConfiguration.ClientCertificates { get; set; }
string IRequestConfiguration.ContentType { get; set; }
bool? IRequestConfiguration.DisableDirectStreaming { get; set; }
Expand Down Expand Up @@ -274,6 +290,30 @@ public RequestConfigurationDescriptor BasicAuthentication(string userName, Secur
return this;
}

public RequestConfigurationDescriptor ApiKeyAuthentication(string id, string apiKey)
{
Self.ApiKeyAuthenticationCredentials = new ApiKeyAuthenticationCredentials(id, apiKey);
return this;
}

public RequestConfigurationDescriptor ApiKeyAuthentication(string id, SecureString apiKey)
{
Self.ApiKeyAuthenticationCredentials = new ApiKeyAuthenticationCredentials(id, apiKey);
return this;
}

public RequestConfigurationDescriptor ApiKeyAuthentication(string base64EncodedApiKey)
{
Self.ApiKeyAuthenticationCredentials = new ApiKeyAuthenticationCredentials(base64EncodedApiKey);
return this;
}

public RequestConfigurationDescriptor ApiKeyAuthentication(SecureString base64EncodedApiKey)
{
Self.ApiKeyAuthenticationCredentials = new ApiKeyAuthenticationCredentials(base64EncodedApiKey);
return this;
}

public RequestConfigurationDescriptor EnableHttpPipelining(bool enable = true)
{
Self.EnableHttpPipelining = enable;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Security;
using System.Text;

namespace Elasticsearch.Net
{
/// <summary>
/// Credentials for Api Key Authentication
/// </summary>
public class ApiKeyAuthenticationCredentials : IDisposable
{
public ApiKeyAuthenticationCredentials()
{
}

public ApiKeyAuthenticationCredentials(string id, SecureString apiKey)
{
Base64EncodedApiKey = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{id}:{apiKey.CreateString()}")).CreateSecureString();
}

public ApiKeyAuthenticationCredentials(string id, string apiKey)
{
Base64EncodedApiKey = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{id}:{apiKey}")).CreateSecureString();
}

public ApiKeyAuthenticationCredentials(string base64EncodedApiKey)
{
Base64EncodedApiKey = base64EncodedApiKey.CreateSecureString();
}

public ApiKeyAuthenticationCredentials(SecureString base64EncodedApiKey)
{
Base64EncodedApiKey = base64EncodedApiKey;
}

/// <summary>
/// The Base64 encoded api key with which to authenticate
/// Take the form, id:api_key, which is then base 64 encoded
/// </summary>
public SecureString Base64EncodedApiKey { get; }

public void Dispose() => Base64EncodedApiKey?.Dispose();
}
}
38 changes: 34 additions & 4 deletions src/Elasticsearch.Net/Connection/HttpWebRequestConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ CancellationToken cancellationToken
protected virtual HttpWebRequest CreateHttpWebRequest(RequestData requestData)
{
var request = CreateWebRequest(requestData);
SetBasicAuthenticationIfNeeded(request, requestData);
// TODO: Do we need some kind of precedence here?
SetAuthenticationIfNeeded(requestData, request);
SetProxyIfNeeded(request, requestData);
SetServerCertificateValidationCallBackIfNeeded(request, requestData);
SetClientCertificates(request, requestData);
Expand Down Expand Up @@ -239,7 +240,16 @@ protected virtual void SetProxyIfNeeded(HttpWebRequest request, RequestData requ
request.Proxy = null;
}

protected virtual void SetBasicAuthenticationIfNeeded(HttpWebRequest request, RequestData requestData)
private void SetAuthenticationIfNeeded(RequestData requestData, HttpWebRequest request)
{
// Api Key authentication takes precedence
var apiKeySet = SetApiKeyAuthenticationIfNeeded(request, requestData);

if (!apiKeySet)
SetBasicAuthenticationIfNeeded(request, requestData);
}

protected virtual bool SetBasicAuthenticationIfNeeded(HttpWebRequest request, RequestData requestData)
{
// Basic auth credentials take the following precedence (highest -> lowest):
// 1 - Specified on the request (highest precedence)
Expand All @@ -253,11 +263,31 @@ protected virtual void SetBasicAuthenticationIfNeeded(HttpWebRequest request, Re
userInfo =
$"{requestData.BasicAuthorizationCredentials.Username}:{requestData.BasicAuthorizationCredentials.Password.CreateString()}";

if (string.IsNullOrWhiteSpace(userInfo))
return false;

request.Headers["Authorization"] = $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes(userInfo))}";
return true;

if (!string.IsNullOrWhiteSpace(userInfo))
request.Headers["Authorization"] = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(userInfo));
}

protected virtual bool SetApiKeyAuthenticationIfNeeded(HttpWebRequest request, RequestData requestData)
{
// ApiKey auth credentials take the following precedence (highest -> lowest):
// 1 - Specified on the request (highest precedence)
// 2 - Specified at the global IConnectionSettings level

string apiKey = null;
if (requestData.ApiKeyAuthenticationCredentials != null)
apiKey = requestData.ApiKeyAuthenticationCredentials.Base64EncodedApiKey.CreateString();

if (string.IsNullOrWhiteSpace(apiKey))
return false;

request.Headers["Authorization"] = $"ApiKey {apiKey}";
return true;

}

/// <summary>
/// Registers an APM async task cancellation on the threadpool
Expand Down
3 changes: 3 additions & 0 deletions src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ IMemoryStreamFactory memoryStreamFactory
ProxyPassword = global.ProxyPassword;
DisableAutomaticProxyDetection = global.DisableAutomaticProxyDetection;
BasicAuthorizationCredentials = local?.BasicAuthenticationCredentials ?? global.BasicAuthenticationCredentials;
ApiKeyAuthenticationCredentials = local?.ApiKeyAuthenticationCredentials ?? global.ApiKeyAuthenticationCredentials;
AllowedStatusCodes = local?.AllowedStatusCodes ?? EmptyReadOnly<int>.Collection;
ClientCertificates = local?.ClientCertificates ?? global.ClientCertificates;
UserAgent = global.UserAgent;
Expand All @@ -86,6 +87,8 @@ IMemoryStreamFactory memoryStreamFactory
public string Accept { get; }
public IReadOnlyCollection<int> AllowedStatusCodes { get; }

public ApiKeyAuthenticationCredentials ApiKeyAuthenticationCredentials { get; }

public BasicAuthenticationCredentials BasicAuthorizationCredentials { get; }

public X509CertificateCollection ClientCertificates { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using System.Text;
using System.Threading.Tasks;
using Elastic.Xunit.Sdk;
Expand Down Expand Up @@ -59,6 +60,23 @@ [U] public void PasswordIsNotExposedInDebugInformation()

response.DebugInformation.Should().NotContain("pass2");
}
//hide
[U] public void ApiKeyIsNotExposedInDebugInformation()
{
// hide
var client = new ElasticClient(new AlwaysInMemoryConnectionSettings()
.DefaultIndex("index")
.ApiKeyAuthentication("id1", "api_key1")
);

var response = client.Search<Project>(s => s
.Query(q => q
.MatchAll()
)
);

response.DebugInformation.Should().NotContain("api_key1");
}

//hide
[U] public void PasswordIsNotExposedInDebugInformationWhenPartOfUrl()
Expand Down