NEST, Elasticsearch.Net v7 workaround for passwordless

Версия 7 не умеет кастомные авторизации, доступны только Basic и ApiKey

Апдейт на 8 версию, может быть сложным, а может и нет, зависит от конретно взятого случая

Ниже, пример, как выкрутиться и прикрутить это дело к v7

Baseline

Демо пример делаем на консольном приложении, аля

mkdir es7
cd es7
dotnet new console
dotnet add package NEST
dotnet add package Azure.Identity

За основу берем следующий пример, вокруг которого будем плясать

using Elasticsearch.Net;
using Nest;

var cloudId = "dev:ZXVyb3BlLXdlc3QzLmdjcC5jbG91ZC5lcy5pbzo0NDMkY2IwM2Q1ZDkzMGU5NDFmMWI0MzFjY2I2NjU0MjkwMWMkNjhhOGMxNDFjNmExNDk2NWEyMTI2YWUxOTM1MzkwMGU=";
var client = new ElasticClient(cloudId, new BasicAuthenticationCredentials("elastic", "xxxxxxxx"));

// request example to prove it works
var result = await client.SearchAsync<object>(s => s
  .Index("vacancysearch")
  .Size(2)
  .Query(q => q
    .MatchAll()
  )
);

foreach (var hit in result.Hits)
{
  Console.WriteLine(hit.Id);
}

Запустив это дело, получим пару id вакансий - значит все ок, все работает

Примечание: по сути мы могли бы пользовать Uri, за вместо cloudId, но поскольку он, все равно будет из коробки пользоваться в v8, то сразу пользуем его

How it works?

Выкручиваться будем за счет проперти OnRequestDataCreated позволяющей модифицировать запрос перед тем как он будет отправлен в эластик

Выглядит это дело вот так

using Elasticsearch.Net;
using Nest;

var cloudId = "dev:ZXVyb3BlLXdlc3QzLmdjcC5jbG91ZC5lcy5pbzo0NDMkY2IwM2Q1ZDkzMGU5NDFmMWI0MzFjY2I2NjU0MjkwMWMkNjhhOGMxNDFjNmExNDk2NWEyMTI2YWUxOTM1MzkwMGU=";
var settings = new ConnectionSettings(cloudId, new BasicAuthenticationCredentials("doesnt", "matter"));
settings.OnRequestDataCreated(request =>
{
  request.Headers.Set("Authorization", "Basic xxxxxxxxx"); // echo -n 'elastic:xxxxxx' | base64
});
var client = new ElasticClient(settings);

var result = await client.SearchAsync<object>(s => s
    .Index("vacancysearch")
    .Size(2)
    .Query(q => q
        .MatchAll()
    )
);

foreach (var hit in result.Hits)
{
    Console.WriteLine(hit.Id);
}

Примечания:

  • что важно, в BasicAuthenticationCredentials я вбил какую то абра кадабру, она не важна, т.к. мы будем переопределять ее дальше
  • в OnRequestDataCreated мы подсовываем в заголовок Authentication заранее подготовленую base64 строку с кредами
  • на выходе имеем рабочий сетап, по сути делающий то же самое, что происходило в базовом сетапе

Passwordless

Теперь пазл начал складываться и по сути все что осталось, это за вместо basic, подсунуть bearer, а как его получать мы уже разобрались

На выходе получаем вот такое

using Azure.Core;
using Azure.Identity;
using Elasticsearch.Net;
using Nest;

var cloudId = "dev:ZXVyb3BlLXdlc3QzLmdjcC5jbG91ZC5lcy5pbzo0NDMkY2IwM2Q1ZDkzMGU5NDFmMWI0MzFjY2I2NjU0MjkwMWMkNjhhOGMxNDFjNmExNDk2NWEyMTI2YWUxOTM1MzkwMGU=";
var scope = "api://elastic-dev/.default";
var credentials = new DefaultAzureCredential();
var settings = new ConnectionSettings(cloudId, new BasicAuthenticationCredentials("doesnt", "matter"));
settings.OnRequestDataCreated(request =>
{
    var token = credentials.GetToken(new TokenRequestContext([scope])); // TBD: AuthenticationFailedException
    request.Headers.Set("Authorization", "Bearer " + token.Token); // TBD: null
});
var client = new ElasticClient(settings);

var result = await client.SearchAsync<object>(s => s
    .Index("vacancysearch")
    .Size(2)
    .Query(q => q
        .MatchAll()
    )
);

foreach (var hit in result.Hits)
{
    Console.WriteLine(hit.Id);
}

Примечания:

  • по сути тут меняется только внутрянка OnRequestDataCreated
  • вероятно там еще стоит учесть потенциальное исключение и сдлеать проверку на null, но это out of scope этой заметки
  • в остальном вот так просто можно прикрутить passwordless к старому эластику, даже если нет возомжонсти его обновить

Show me the code

За для истории, содержимое Program.cs целиком

using Azure.Core;
using Azure.Identity;
using Elasticsearch.Net;
using Nest;

// dotnet add package NEST

var cloudId = "dev:ZXVyb3BlLXdlc3QzLmdjcC5jbG91ZC5lcy5pbzo0NDMkY2IwM2Q1ZDkzMGU5NDFmMWI0MzFjY2I2NjU0MjkwMWMkNjhhOGMxNDFjNmExNDk2NWEyMTI2YWUxOTM1MzkwMGU=";

// BEFORE:
// var client = new ElasticClient(cloudId, new BasicAuthenticationCredentials("elastic", "xxxxx"));

// HOW IT WORKS:
// - there is OnRequestDataCreated that allows modify request before it is sent to elastic
// - we will set authorization header there
// - as a proof of concept - showing basic auth example
// var settings = new ConnectionSettings(cloudId, new BasicAuthenticationCredentials("doesnt", "matter"));
// settings.OnRequestDataCreated(request =>
// {
//   request.Headers.Set("Authorization", "Basic ZWxhc3RpYzpaSjlRdXBndHY0cVphaU15cTZqeFYzT2M="); // echo -n 'elastic:xxxxx' | base64
// });
// var client = new ElasticClient(settings);

// AFTER:
var scope = "api://elastic-dev/.default";
var credentials = new DefaultAzureCredential();
var settings = new ConnectionSettings(cloudId, new BasicAuthenticationCredentials("doesnt", "matter")); // note - here, instead of cloudId we may pass Uri, but at the very end in v8 we will use cloudId, that's why we using it
settings.OnRequestDataCreated(request =>
{
  var token = credentials.GetToken(new TokenRequestContext([scope])); // TBD: AuthenticationFailedException
  request.Headers.Set("Authorization", "Bearer " + token.Token); // TBD: null
});
var client = new ElasticClient(settings);


// request example to prove it works
var result = await client.SearchAsync<object>(s => s
  .Index("vacancysearch")
  .Size(2)
  .Query(q => q
    .MatchAll()
  )
);

foreach (var hit in result.Hits)
{
  Console.WriteLine(hit.Id);
}