NUC-33: Implement authentication handling for TelemetryPublisherFirestore

This commit is contained in:
Denis-Cosmin Nutiu 2020-04-17 17:04:13 +03:00
parent 29b7b5ca13
commit 9d4135d6d3
3 changed files with 94 additions and 6 deletions

View file

@ -72,6 +72,9 @@ The Telemetry:Publisher must be set to: Firestore
Example connection string:
`ProjectId=nucuhub;CollectionName=sensors-telemetry-test;Timeout=1000`
If you want to use Authentication you can do so by providing the following keys
in the connection string: WebApiEmail, WebApiPassword, WebApiKey.
### Reader
You will need use a firebase client or rest API.

View file

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -19,6 +21,9 @@ namespace NucuCar.Domain.Telemetry
/// The connection string has the following parameters:
/// ProjectId (required) — The string for the Firestore project id.
/// CollectionName (required) — The string for the Firestore collection name.
/// WebApiKey (optional) — The web api key of the firebase project.
/// WebApiEmail (optional) — An email to use when requesting id tokens.
/// WebApiPassword (optional) — The password to use when requesting id tokens.
/// Timeout (optional) — The number in milliseconds in which to timeout if publishing fails. Default: 10000
/// </remarks>
/// </summary>
@ -27,6 +32,13 @@ namespace NucuCar.Domain.Telemetry
private readonly HttpClient _httpClient;
private readonly int _timeout;
private string _idToken;
// Variables used for authentication
private readonly string _webEmail;
private readonly string _webPassword;
private readonly string _webApiKey;
public TelemetryPublisherFirestore(TelemetryPublisherBuilderOptions opts) : base(opts)
{
var options = ConnectionStringParser.Parse(opts.ConnectionString);
@ -44,7 +56,10 @@ namespace NucuCar.Domain.Telemetry
$"Missing CollectionName!");
}
_timeout = int.Parse(options.GetValueOrDefault("Timeout", "10000"));
_timeout = int.Parse(options.GetValueOrDefault("Timeout", "10000") ?? "10000");
_webApiKey = options.GetValueOrDefault("WebApiKey", null);
_webEmail = options.GetValueOrDefault("WebApiEmail", null);
_webPassword = options.GetValueOrDefault("WebApiPassword", null);
var requestUrl = $"https://firestore.googleapis.com/v1/projects/{firestoreProjectId}/" +
$"databases/(default)/documents/{firestoreCollection}/";
@ -53,27 +68,90 @@ namespace NucuCar.Domain.Telemetry
Logger?.LogInformation($"Initialized {nameof(TelemetryPublisherFirestore)}");
}
private async Task SetupAuthenticationHeaders()
{
// Make request
var requestUrl = $"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={_webApiKey}";
var data = new Dictionary<string, object>()
{
["email"] = _webEmail,
["password"] = _webPassword,
["returnSecureToken"] = true
};
// Handle response & setup headers
var cts = new CancellationTokenSource();
try
{
cts.CancelAfter(_timeout);
var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json");
var responseMessage = await _httpClient.PostAsync(requestUrl, content, cts.Token);
var responseJson =
JsonConvert.DeserializeObject<dynamic>(await responseMessage.Content.ReadAsStringAsync());
if (responseMessage.StatusCode == HttpStatusCode.OK)
{
_idToken = responseJson.idToken;
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _idToken);
}
else
{
throw new HttpRequestException(responseJson.message ?? "unknown");
}
}
catch (TaskCanceledException e)
{
Logger.LogWarning($"FireStore authenticate: Timeout or cancellation occured. Message {e.Message}.\n");
}
catch (HttpRequestException e)
{
Logger?.LogError($"Failed to authenticate!\n{e.GetType().FullName}: {e.Message}");
throw;
}
}
public override async Task PublishAsync(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
var cts = new CancellationTokenSource();
cts.CancelAfter(_timeout);
try
{
cts.CancelAfter(_timeout);
var data = FirebaseRestTranslator.Translator.Translate(null, GetTelemetry());
var content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json");
await _httpClient.PostAsync("", content, cts.Token);
Logger?.LogInformation("Published data to Firestore!");
var responseMessage = await _httpClient.PostAsync("", content, cts.Token);
var responseJson =
JsonConvert.DeserializeObject<dynamic>(await responseMessage.Content.ReadAsStringAsync());
switch (responseMessage.StatusCode)
{
case HttpStatusCode.OK:
Logger?.LogInformation("Published data to Firestore!");
break;
case HttpStatusCode.Forbidden:
Logger.LogWarning("Failed to publish telemetry! Forbidden!");
await SetupAuthenticationHeaders();
break;
default:
throw new HttpRequestException(responseJson.message ?? "unknown error");
}
}
catch (Exception e)
catch (TaskCanceledException e)
{
Logger.LogWarning(
$"Firestore publish telemetry: Timeout or cancellation occured. Message {e.Message}.\n");
}
catch (HttpRequestException e)
{
Logger?.LogError($"Failed to publish telemetry data!\n{e.GetType().FullName}: {e.Message}");
throw;
}
finally
{
cts.Dispose();
}
}
public override void Dispose()

View file

@ -0,0 +1,7 @@
namespace NucuCar.UnitTests.NucuCar.Domain.Tests.Telemetry
{
public class TelemetryPublisherFirestoreTest
{
// TODO after refactoring
}
}