Skip to content

Commit b56c7f1

Browse files
authored
Implement Api Key authorisation on requests (#3912)
Implement Api Key authorisation on requests. In instances where Basic and ApiKey are set, precedence is given to Api Key.
1 parent 8941aaf commit b56c7f1

File tree

11 files changed

+298
-24
lines changed

11 files changed

+298
-24
lines changed

src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public abstract class ConnectionConfiguration<T> : IConnectionConfigurationValue
116116
private readonly ElasticsearchUrlFormatter _urlFormatter;
117117

118118
private BasicAuthenticationCredentials _basicAuthCredentials;
119+
private ApiKeyAuthenticationCredentials _apiKeyAuthCredentials;
119120
private X509CertificateCollection _clientCertificates;
120121
private Action<IApiCallDetails> _completedRequestHandler = DefaultCompletedRequestHandler;
121122
private int _connectionLimit;
@@ -169,6 +170,7 @@ protected ConnectionConfiguration(IConnectionPool connectionPool, IConnection co
169170

170171
protected IElasticsearchSerializer UseThisRequestResponseSerializer { get; set; }
171172
BasicAuthenticationCredentials IConnectionConfigurationValues.BasicAuthenticationCredentials => _basicAuthCredentials;
173+
ApiKeyAuthenticationCredentials IConnectionConfigurationValues.ApiKeyAuthenticationCredentials => _apiKeyAuthCredentials;
172174
SemaphoreSlim IConnectionConfigurationValues.BootstrapLock => _semaphore;
173175
X509CertificateCollection IConnectionConfigurationValues.ClientCertificates => _clientCertificates;
174176
IConnection IConnectionConfigurationValues.Connection => _connection;
@@ -434,6 +436,18 @@ public T BasicAuthentication(string username, string password) =>
434436
public T BasicAuthentication(string username, SecureString password) =>
435437
Assign(new BasicAuthenticationCredentials(username, password), (a, v) => a._basicAuthCredentials = v);
436438

439+
/// <summary>
440+
/// Api Key to send with all requests to Elasticsearch
441+
/// </summary>
442+
public T ApiKeyAuthentication(string id, SecureString apiKey) =>
443+
Assign(new ApiKeyAuthenticationCredentials(id, apiKey), (a, v) => a._apiKeyAuthCredentials = v);
444+
445+
/// <summary>
446+
/// Api Key to send with all requests to Elasticsearch
447+
/// </summary>
448+
public T ApiKeyAuthentication(string id, string apiKey) =>
449+
Assign(new ApiKeyAuthenticationCredentials(id, apiKey), (a, v) => a._apiKeyAuthCredentials = v);
450+
437451
/// <summary>
438452
/// Allows for requests to be pipelined. http://en.wikipedia.org/wiki/HTTP_pipelining
439453
/// <para>NOTE: HTTP pipelining must also be enabled in Elasticsearch for this to work properly.</para>
@@ -523,6 +537,7 @@ protected virtual void DisposeManagedResources()
523537
_semaphore?.Dispose();
524538
_proxyPassword?.Dispose();
525539
_basicAuthCredentials?.Dispose();
540+
_apiKeyAuthCredentials?.Dispose();
526541
}
527542

528543
protected virtual bool HttpStatusCodeClassifier(HttpMethod method, int statusCode) =>

src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,19 @@ public interface IConnectionConfigurationValues : IDisposable
1313
/// <summary>
1414
/// Basic access authorization credentials to specify with all requests.
1515
/// </summary>
16+
/// <remarks>
17+
/// Cannot be used in conjuction with <see cref="ApiKeyAuthenticationCredentials"/>
18+
/// </remarks>
1619
BasicAuthenticationCredentials BasicAuthenticationCredentials { get; }
1720

21+
/// <summary>
22+
/// Api Key authorization credentials to specify with all requests.
23+
/// </summary>
24+
/// <remarks>
25+
/// Cannot be used in conjuction with <see cref="BasicAuthenticationCredentials"/>
26+
/// </remarks>
27+
ApiKeyAuthenticationCredentials ApiKeyAuthenticationCredentials { get; }
28+
1829
/// <summary> Provides a semaphoreslim to transport implementations that need to limit access to a resource</summary>
1930
SemaphoreSlim BootstrapLock { get; }
2031

src/Elasticsearch.Net/Configuration/RequestConfiguration.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,20 @@ public interface IRequestConfiguration
2222
/// Basic access authorization credentials to specify with this request.
2323
/// Overrides any credentials that are set at the global IConnectionSettings level.
2424
/// </summary>
25+
/// <remarks>
26+
/// Cannot be used in conjunction with <see cref="ApiKeyAuthenticationCredentials"/>
27+
/// </remarks>
2528
BasicAuthenticationCredentials BasicAuthenticationCredentials { get; set; }
2629

30+
/// <summary>
31+
/// An API-key authorization credentials to specify with this request.
32+
/// Overrides any credentials that are set at the global IConnectionSettings level.
33+
/// </summary>
34+
/// <remarks>
35+
/// Cannot be used in conjunction with <see cref="BasicAuthenticationCredentials"/>
36+
/// </remarks>
37+
ApiKeyAuthenticationCredentials ApiKeyAuthenticationCredentials { get; set; }
38+
2739
/// <summary>
2840
/// Use the following client certificates to authenticate this single request
2941
/// </summary>
@@ -102,6 +114,8 @@ public class RequestConfiguration : IRequestConfiguration
102114
public IReadOnlyCollection<int> AllowedStatusCodes { get; set; }
103115
public BasicAuthenticationCredentials BasicAuthenticationCredentials { get; set; }
104116

117+
public ApiKeyAuthenticationCredentials ApiKeyAuthenticationCredentials { get; set; }
118+
105119
public X509CertificateCollection ClientCertificates { get; set; }
106120
public string ContentType { get; set; }
107121
public bool? DisableDirectStreaming { get; set; }
@@ -138,6 +152,7 @@ public RequestConfigurationDescriptor(IRequestConfiguration config)
138152
Self.DisableDirectStreaming = config?.DisableDirectStreaming;
139153
Self.AllowedStatusCodes = config?.AllowedStatusCodes;
140154
Self.BasicAuthenticationCredentials = config?.BasicAuthenticationCredentials;
155+
Self.ApiKeyAuthenticationCredentials = config?.ApiKeyAuthenticationCredentials;
141156
Self.EnableHttpPipelining = config?.EnableHttpPipelining ?? true;
142157
Self.RunAs = config?.RunAs;
143158
Self.ClientCertificates = config?.ClientCertificates;
@@ -148,6 +163,7 @@ public RequestConfigurationDescriptor(IRequestConfiguration config)
148163
string IRequestConfiguration.Accept { get; set; }
149164
IReadOnlyCollection<int> IRequestConfiguration.AllowedStatusCodes { get; set; }
150165
BasicAuthenticationCredentials IRequestConfiguration.BasicAuthenticationCredentials { get; set; }
166+
ApiKeyAuthenticationCredentials IRequestConfiguration.ApiKeyAuthenticationCredentials { get; set; }
151167
X509CertificateCollection IRequestConfiguration.ClientCertificates { get; set; }
152168
string IRequestConfiguration.ContentType { get; set; }
153169
bool? IRequestConfiguration.DisableDirectStreaming { get; set; }
@@ -274,6 +290,30 @@ public RequestConfigurationDescriptor BasicAuthentication(string userName, Secur
274290
return this;
275291
}
276292

293+
public RequestConfigurationDescriptor ApiKeyAuthentication(string id, string apiKey)
294+
{
295+
Self.ApiKeyAuthenticationCredentials = new ApiKeyAuthenticationCredentials(id, apiKey);
296+
return this;
297+
}
298+
299+
public RequestConfigurationDescriptor ApiKeyAuthentication(string id, SecureString apiKey)
300+
{
301+
Self.ApiKeyAuthenticationCredentials = new ApiKeyAuthenticationCredentials(id, apiKey);
302+
return this;
303+
}
304+
305+
public RequestConfigurationDescriptor ApiKeyAuthentication(string base64EncodedApiKey)
306+
{
307+
Self.ApiKeyAuthenticationCredentials = new ApiKeyAuthenticationCredentials(base64EncodedApiKey);
308+
return this;
309+
}
310+
311+
public RequestConfigurationDescriptor ApiKeyAuthentication(SecureString base64EncodedApiKey)
312+
{
313+
Self.ApiKeyAuthenticationCredentials = new ApiKeyAuthenticationCredentials(base64EncodedApiKey);
314+
return this;
315+
}
316+
277317
public RequestConfigurationDescriptor EnableHttpPipelining(bool enable = true)
278318
{
279319
Self.EnableHttpPipelining = enable;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Security;
3+
using System.Text;
4+
5+
namespace Elasticsearch.Net
6+
{
7+
/// <summary>
8+
/// Credentials for Api Key Authentication
9+
/// </summary>
10+
public class ApiKeyAuthenticationCredentials : IDisposable
11+
{
12+
public ApiKeyAuthenticationCredentials()
13+
{
14+
}
15+
16+
public ApiKeyAuthenticationCredentials(string id, SecureString apiKey)
17+
{
18+
Base64EncodedApiKey = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{id}:{apiKey.CreateString()}")).CreateSecureString();
19+
}
20+
21+
public ApiKeyAuthenticationCredentials(string id, string apiKey)
22+
{
23+
Base64EncodedApiKey = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{id}:{apiKey}")).CreateSecureString();
24+
}
25+
26+
public ApiKeyAuthenticationCredentials(string base64EncodedApiKey)
27+
{
28+
Base64EncodedApiKey = base64EncodedApiKey.CreateSecureString();
29+
}
30+
31+
public ApiKeyAuthenticationCredentials(SecureString base64EncodedApiKey)
32+
{
33+
Base64EncodedApiKey = base64EncodedApiKey;
34+
}
35+
36+
/// <summary>
37+
/// The Base64 encoded api key with which to authenticate
38+
/// Take the form, id:api_key, which is then base 64 encoded
39+
/// </summary>
40+
public SecureString Base64EncodedApiKey { get; }
41+
42+
public void Dispose() => Base64EncodedApiKey?.Dispose();
43+
}
44+
}

src/Elasticsearch.Net/Connection/HttpConnection.cs

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,59 @@ protected virtual HttpMessageHandler CreateHttpClientHandler(RequestData request
222222
protected virtual HttpRequestMessage CreateHttpRequestMessage(RequestData requestData)
223223
{
224224
var request = CreateRequestMessage(requestData);
225-
SetBasicAuthenticationIfNeeded(request, requestData);
225+
SetAuthenticationIfNeeded(request, requestData);
226226
return request;
227227
}
228228

229+
protected virtual void SetAuthenticationIfNeeded(HttpRequestMessage requestMessage, RequestData requestData)
230+
{
231+
// Api Key authentication takes precedence
232+
var apiKeySet = SetApiKeyAuthenticationIfNeeded(requestMessage, requestData);
233+
234+
if (!apiKeySet)
235+
SetBasicAuthenticationIfNeeded(requestMessage, requestData);
236+
}
237+
238+
// TODO - make private in 8.0 and only expose SetAuthenticationIfNeeded
239+
protected virtual bool SetApiKeyAuthenticationIfNeeded(HttpRequestMessage requestMessage, RequestData requestData)
240+
{
241+
// ApiKey auth credentials take the following precedence (highest -> lowest):
242+
// 1 - Specified on the request (highest precedence)
243+
// 2 - Specified at the global IConnectionSettings level
244+
245+
string apiKey = null;
246+
if (requestData.ApiKeyAuthenticationCredentials != null)
247+
apiKey = requestData.ApiKeyAuthenticationCredentials.Base64EncodedApiKey.CreateString();
248+
249+
if (string.IsNullOrWhiteSpace(apiKey))
250+
return false;
251+
252+
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("ApiKey", apiKey);
253+
return true;
254+
255+
}
256+
257+
// TODO - make private in 8.0 and only expose SetAuthenticationIfNeeded
258+
protected virtual void SetBasicAuthenticationIfNeeded(HttpRequestMessage requestMessage, RequestData requestData)
259+
{
260+
// Basic auth credentials take the following precedence (highest -> lowest):
261+
// 1 - Specified on the request (highest precedence)
262+
// 2 - Specified at the global IConnectionSettings level
263+
// 3 - Specified with the URI (lowest precedence)
264+
265+
string userInfo = null;
266+
if (!requestData.Uri.UserInfo.IsNullOrEmpty())
267+
userInfo = Uri.UnescapeDataString(requestData.Uri.UserInfo);
268+
else if (requestData.BasicAuthorizationCredentials != null)
269+
userInfo =
270+
$"{requestData.BasicAuthorizationCredentials.Username}:{requestData.BasicAuthorizationCredentials.Password.CreateString()}";
271+
if (!userInfo.IsNullOrEmpty())
272+
{
273+
var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes(userInfo));
274+
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
275+
}
276+
}
277+
229278
protected virtual HttpRequestMessage CreateRequestMessage(RequestData requestData)
230279
{
231280
var method = ConvertHttpMethod(requestData.Method);
@@ -256,21 +305,6 @@ protected virtual HttpRequestMessage CreateRequestMessage(RequestData requestDat
256305
private static void SetAsyncContent(HttpRequestMessage message, RequestData requestData, CancellationToken token) =>
257306
message.Content = new RequestDataContent(requestData, token);
258307

259-
protected virtual void SetBasicAuthenticationIfNeeded(HttpRequestMessage requestMessage, RequestData requestData)
260-
{
261-
string userInfo = null;
262-
if (!requestData.Uri.UserInfo.IsNullOrEmpty())
263-
userInfo = Uri.UnescapeDataString(requestData.Uri.UserInfo);
264-
else if (requestData.BasicAuthorizationCredentials != null)
265-
userInfo =
266-
$"{requestData.BasicAuthorizationCredentials.Username}:{requestData.BasicAuthorizationCredentials.Password.CreateString()}";
267-
if (!userInfo.IsNullOrEmpty())
268-
{
269-
var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes(userInfo));
270-
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
271-
}
272-
}
273-
274308
private static System.Net.Http.HttpMethod ConvertHttpMethod(HttpMethod httpMethod)
275309
{
276310
switch (httpMethod)

src/Elasticsearch.Net/Connection/HttpWebRequestConnection.cs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ CancellationToken cancellationToken
142142
protected virtual HttpWebRequest CreateHttpWebRequest(RequestData requestData)
143143
{
144144
var request = CreateWebRequest(requestData);
145-
SetBasicAuthenticationIfNeeded(request, requestData);
145+
SetAuthenticationIfNeeded(requestData, request);
146146
SetProxyIfNeeded(request, requestData);
147147
SetServerCertificateValidationCallBackIfNeeded(request, requestData);
148148
SetClientCertificates(request, requestData);
@@ -239,6 +239,16 @@ protected virtual void SetProxyIfNeeded(HttpWebRequest request, RequestData requ
239239
request.Proxy = null;
240240
}
241241

242+
protected virtual void SetAuthenticationIfNeeded(RequestData requestData, HttpWebRequest request)
243+
{
244+
// Api Key authentication takes precedence
245+
var apiKeySet = SetApiKeyAuthenticationIfNeeded(request, requestData);
246+
247+
if (!apiKeySet)
248+
SetBasicAuthenticationIfNeeded(request, requestData);
249+
}
250+
251+
// TODO - make private in 8.0 and only expose SetAuthenticationIfNeeded
242252
protected virtual void SetBasicAuthenticationIfNeeded(HttpWebRequest request, RequestData requestData)
243253
{
244254
// Basic auth credentials take the following precedence (highest -> lowest):
@@ -253,11 +263,30 @@ protected virtual void SetBasicAuthenticationIfNeeded(HttpWebRequest request, Re
253263
userInfo =
254264
$"{requestData.BasicAuthorizationCredentials.Username}:{requestData.BasicAuthorizationCredentials.Password.CreateString()}";
255265

266+
if (string.IsNullOrWhiteSpace(userInfo))
267+
return;
256268

257-
if (!string.IsNullOrWhiteSpace(userInfo))
258-
request.Headers["Authorization"] = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(userInfo));
269+
request.Headers["Authorization"] = $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes(userInfo))}";
259270
}
260271

272+
// TODO - make private in 8.0 and only expose SetAuthenticationIfNeeded
273+
protected virtual bool SetApiKeyAuthenticationIfNeeded(HttpWebRequest request, RequestData requestData)
274+
{
275+
// ApiKey auth credentials take the following precedence (highest -> lowest):
276+
// 1 - Specified on the request (highest precedence)
277+
// 2 - Specified at the global IConnectionSettings level
278+
279+
string apiKey = null;
280+
if (requestData.ApiKeyAuthenticationCredentials != null)
281+
apiKey = requestData.ApiKeyAuthenticationCredentials.Base64EncodedApiKey.CreateString();
282+
283+
if (string.IsNullOrWhiteSpace(apiKey))
284+
return false;
285+
286+
request.Headers["Authorization"] = $"ApiKey {apiKey}";
287+
return true;
288+
289+
}
261290

262291
/// <summary>
263292
/// Registers an APM async task cancellation on the threadpool

src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ IMemoryStreamFactory memoryStreamFactory
7676
ProxyPassword = global.ProxyPassword;
7777
DisableAutomaticProxyDetection = global.DisableAutomaticProxyDetection;
7878
BasicAuthorizationCredentials = local?.BasicAuthenticationCredentials ?? global.BasicAuthenticationCredentials;
79+
ApiKeyAuthenticationCredentials = local?.ApiKeyAuthenticationCredentials ?? global.ApiKeyAuthenticationCredentials;
7980
AllowedStatusCodes = local?.AllowedStatusCodes ?? EmptyReadOnly<int>.Collection;
8081
ClientCertificates = local?.ClientCertificates ?? global.ClientCertificates;
8182
UserAgent = global.UserAgent;
@@ -86,6 +87,8 @@ IMemoryStreamFactory memoryStreamFactory
8687
public string Accept { get; }
8788
public IReadOnlyCollection<int> AllowedStatusCodes { get; }
8889

90+
public ApiKeyAuthenticationCredentials ApiKeyAuthenticationCredentials { get; }
91+
8992
public BasicAuthenticationCredentials BasicAuthorizationCredentials { get; }
9093

9194
public X509CertificateCollection ClientCertificates { get; }

src/Elasticsearch.Net/Transport/Pipeline/RequestPipeline.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class RequestPipeline : IRequestPipeline
2222
private readonly IDateTimeProvider _dateTimeProvider;
2323
private readonly IMemoryStreamFactory _memoryStreamFactory;
2424
private readonly IConnectionConfigurationValues _settings;
25-
25+
2626
private static DiagnosticSource DiagnosticSource { get; } = new DiagnosticListener(DiagnosticSources.RequestPipeline.SourceName);
2727

2828
public RequestPipeline(
@@ -392,7 +392,7 @@ public void Ping(Node node)
392392
public async Task PingAsync(Node node, CancellationToken cancellationToken)
393393
{
394394
if (PingDisabled(node)) return;
395-
395+
396396
var pingData = CreatePingRequestData(node);
397397
using (var audit = Audit(PingSuccess, node))
398398
using (var d = DiagnosticSource.Diagnose<RequestData, IApiCallDetails>(DiagnosticSources.RequestPipeline.Ping, pingData))
@@ -432,7 +432,7 @@ public void Sniff()
432432
audit.Path = requestData.PathAndQuery;
433433
var response = _connection.Request<SniffResponse>(requestData);
434434
d.EndState = response;
435-
435+
436436
ThrowBadAuthPipelineExceptionWhenNeeded(response);
437437
//sniff should not silently accept bad but valid http responses
438438
if (!response.Success)
@@ -469,7 +469,7 @@ public async Task SniffAsync(CancellationToken cancellationToken)
469469
audit.Path = requestData.PathAndQuery;
470470
var response = await _connection.RequestAsync<SniffResponse>(requestData, cancellationToken).ConfigureAwait(false);
471471
d.EndState = response;
472-
472+
473473
ThrowBadAuthPipelineExceptionWhenNeeded(response);
474474
//sniff should not silently accept bad but valid http responses
475475
if (!response.Success)
@@ -554,6 +554,7 @@ private RequestData CreatePingRequestData(Node node)
554554
PingTimeout = PingTimeout,
555555
RequestTimeout = PingTimeout,
556556
BasicAuthenticationCredentials = _settings.BasicAuthenticationCredentials,
557+
ApiKeyAuthenticationCredentials = _settings.ApiKeyAuthenticationCredentials,
557558
EnableHttpPipelining = RequestConfiguration?.EnableHttpPipelining ?? _settings.HttpPipeliningEnabled,
558559
ForceNode = RequestConfiguration?.ForceNode
559560
};

src/Tests/Tests.Core/Extensions/EphemeralClusterExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ public static IElasticClient GetOrAddClient<TConfig>(
3030
var settings = modifySettings(cluster.CreateConnectionSettings());
3131

3232
var current = (IConnectionConfigurationValues)settings;
33-
var notAlreadyAuthenticated = current.BasicAuthenticationCredentials == null && current.ClientCertificates == null;
33+
var notAlreadyAuthenticated = current.BasicAuthenticationCredentials == null
34+
&& current.ApiKeyAuthenticationCredentials == null
35+
&& current.ClientCertificates == null;
36+
3437
var noCertValidation = current.ServerCertificateValidationCallback == null;
3538

3639
if (cluster.ClusterConfiguration.EnableSecurity && notAlreadyAuthenticated)

0 commit comments

Comments
 (0)