feat: implementa cache intelligente per credenziali REST con hash SHA256

- Aggiunge classe CachedRestClient per tracciare client con hash delle credenziali
- Implementa verifica automatica delle credenziali dal database tramite hash SHA256
- Mantiene cache dei client REST autenticati preservando lo stato di autenticazione
- Invalida automaticamente la cache quando le credenziali cambiano nel database
- Aggiunge logging dettagliato per debug dell'autenticazione Salesforce
- Migliora mappatura credenziali Salesforce con validazione Password e ClientSecret

BREAKING CHANGE: Le credenziali REST vengono sempre verificate dal database
mantenendo performance attraverso cache intelligente basata su hash crittografico

Fixes: risolve problemi di "invalid_client_id" intermittenti e _accessToken null
This commit is contained in:
2025-09-10 01:31:14 +02:00
parent bcc936526b
commit ce359a12bc
2 changed files with 63 additions and 20 deletions
@@ -132,13 +132,13 @@ namespace DataConnection.REST.Implementations
if (string.IsNullOrEmpty(_options.Username) || string.IsNullOrEmpty(_options.Password)) if (string.IsNullOrEmpty(_options.Username) || string.IsNullOrEmpty(_options.Password))
{ {
Console.WriteLine("Salesforce authentication requires username and password in options"); Console.WriteLine($"Salesforce authentication requires username and password in options. Username: '{_options.Username}', Password: '{(!string.IsNullOrEmpty(_options.Password) ? "***SET***" : "***NULL***")}'");
return false; return false;
} }
if (string.IsNullOrEmpty(_options.ApiKey) || string.IsNullOrEmpty(_options.AuthToken)) if (string.IsNullOrEmpty(_options.ApiKey) || string.IsNullOrEmpty(_options.AuthToken))
{ {
Console.WriteLine("Salesforce authentication requires ApiKey (ClientId) and AuthToken (ClientSecret) in options"); Console.WriteLine($"Salesforce authentication requires ApiKey (ClientId) and AuthToken (ClientSecret) in options. ApiKey: '{_options.ApiKey}', AuthToken: '{(!string.IsNullOrEmpty(_options.AuthToken) ? "***SET***" : "***NULL***")}'");
return false; return false;
} }
+61 -18
View File
@@ -7,9 +7,21 @@ using DataConnection.REST.Configuration;
using DataConnection.CredentialManagement.Interfaces; using DataConnection.CredentialManagement.Interfaces;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Security.Cryptography;
using System.Text;
namespace Data_Coupler.Services namespace Data_Coupler.Services
{ {
/// <summary>
/// Classe per tracciare i client REST cached con le loro credenziali
/// </summary>
internal class CachedRestClient
{
public IRestServiceClient Client { get; set; } = null!;
public string CredentialsHash { get; set; } = string.Empty;
public DateTime CachedAt { get; set; }
}
/// <summary> /// <summary>
/// Factory service per creare istanze di DatabaseManager e RestClient /// Factory service per creare istanze di DatabaseManager e RestClient
/// </summary> /// </summary>
@@ -46,8 +58,8 @@ namespace Data_Coupler.Services
private readonly ILogger<DataConnectionFactory> _logger; private readonly ILogger<DataConnectionFactory> _logger;
private readonly IDataConnectionCredentialService _credentialService; private readonly IDataConnectionCredentialService _credentialService;
// Cache per mantenere le istanze dei client REST autenticati // Cache intelligente per client REST che mantiene lo stato di autenticazione ma verifica le credenziali
private readonly Dictionary<string, IRestServiceClient> _restClientCache = new(); private readonly Dictionary<string, CachedRestClient> _restClientCache = new();
public DataConnectionFactory(IServiceProvider serviceProvider, ILogger<DataConnectionFactory> logger, IDataConnectionCredentialService credentialService) public DataConnectionFactory(IServiceProvider serviceProvider, ILogger<DataConnectionFactory> logger, IDataConnectionCredentialService credentialService)
{ {
@@ -76,17 +88,29 @@ namespace Data_Coupler.Services
{ {
try try
{ {
// Controlla se abbiamo già un client cached per questa credenziale // SEMPRE recupera le credenziali fresh dal database per assicurare che siano aggiornate
if (_restClientCache.TryGetValue(credentialName, out var cachedClient))
{
_logger.LogInformation("Utilizzando client REST cached per {CredentialName}", credentialName);
return cachedClient;
}
var credential = await _credentialService.GetRestApiCredentialAsync(credentialName); var credential = await _credentialService.GetRestApiCredentialAsync(credentialName);
if (credential == null) if (credential == null)
{ {
throw new ArgumentException($"Credenziale REST API '{credentialName}' non trovata"); throw new ArgumentException($"Credenziale REST API '{credentialName}' non trovata");
}
// Calcola l'hash delle credenziali correnti
var currentCredentialsHash = CalculateCredentialsHash(credential);
// Controlla se abbiamo un client cached con le stesse credenziali
if (_restClientCache.TryGetValue(credentialName, out var cachedClient))
{
if (cachedClient.CredentialsHash == currentCredentialsHash)
{
_logger.LogInformation("Utilizzando client REST cached con credenziali verificate per {CredentialName}", credentialName);
return cachedClient.Client;
}
else
{
_logger.LogInformation("Credenziali cambiate, rimuovendo client cached per {CredentialName}", credentialName);
_restClientCache.Remove(credentialName);
}
} var options = new RestServiceOptions } var options = new RestServiceOptions
{ {
BaseUrl = credential.BaseUrl, BaseUrl = credential.BaseUrl,
@@ -101,8 +125,11 @@ namespace Data_Coupler.Services
// Per Salesforce usiamo i campi specifici ClientId e ClientSecret // Per Salesforce usiamo i campi specifici ClientId e ClientSecret
options.ApiKey = credential.ClientId; // ClientId -> ApiKey options.ApiKey = credential.ClientId; // ClientId -> ApiKey
options.AuthToken = credential.ClientSecret; // ClientSecret -> AuthToken options.AuthToken = credential.ClientSecret; // ClientSecret -> AuthToken
_logger.LogInformation("Salesforce mapping - ClientId: {ClientId}, ClientSecret: {HasSecret}, Username: {Username}", _logger.LogInformation("Salesforce mapping - ClientId: '{ClientId}', ClientSecret: {HasSecret}, Username: '{Username}', Password: {HasPassword}",
credential.ClientId, !string.IsNullOrEmpty(credential.ClientSecret), credential.Username); credential.ClientId,
!string.IsNullOrEmpty(credential.ClientSecret),
credential.Username,
!string.IsNullOrEmpty(credential.Password));
break; break;
case RestServiceType.SapB1ServiceLayer: case RestServiceType.SapB1ServiceLayer:
// Per SAP B1 usiamo il CompanyDatabase come ApiKey // Per SAP B1 usiamo il CompanyDatabase come ApiKey
@@ -128,9 +155,15 @@ namespace Data_Coupler.Services
_ => throw new NotSupportedException($"Tipo di servizio REST non supportato: {credential.ServiceType}") _ => throw new NotSupportedException($"Tipo di servizio REST non supportato: {credential.ServiceType}")
}; };
// Salva il client nella cache // Salva il client nella cache con l'hash delle credenziali
_restClientCache[credentialName] = client; _restClientCache[credentialName] = new CachedRestClient
_logger.LogInformation("Client REST creato e cached per {CredentialName}", credentialName); {
Client = client,
CredentialsHash = currentCredentialsHash,
CachedAt = DateTime.UtcNow
};
_logger.LogInformation("Client REST creato e cached con credenziali aggiornate per {CredentialName}", credentialName);
return client; return client;
} }
@@ -143,13 +176,13 @@ namespace Data_Coupler.Services
{ {
try try
{ {
// Utilizza lo stesso client REST cached (che è già autenticato) // Crea un nuovo client REST con credenziali aggiornate
var restClient = await CreateRestServiceClientAsync(credentialName); var restClient = await CreateRestServiceClientAsync(credentialName);
// I service client già implementano IRestMetadataDiscovery // I service client già implementano IRestMetadataDiscovery
if (restClient is IRestMetadataDiscovery metadataDiscovery) if (restClient is IRestMetadataDiscovery metadataDiscovery)
{ {
_logger.LogInformation("Utilizzando lo stesso client REST per metadata discovery per {CredentialName}", credentialName); _logger.LogInformation("Utilizzando client REST per metadata discovery con credenziali aggiornate per {CredentialName}", credentialName);
return metadataDiscovery; return metadataDiscovery;
} }
@@ -178,7 +211,17 @@ namespace Data_Coupler.Services
} }
/// <summary> /// <summary>
/// Pulisce la cache del client REST per una credenziale specifica /// Calcola un hash delle credenziali per rilevare cambiamenti
/// </summary>
private string CalculateCredentialsHash(RestApiCredential credential)
{
var credentialString = $"{credential.BaseUrl}|{credential.Username}|{credential.Password}|{credential.ApiKey}|{credential.AuthToken}|{credential.ClientId}|{credential.ClientSecret}|{credential.CompanyDatabase}";
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(credentialString));
return Convert.ToBase64String(bytes);
}
/// <summary>
/// Pulisce la cache del client REST per una credenziale specifica.
/// </summary> /// </summary>
public void ClearRestClientCache(string credentialName) public void ClearRestClientCache(string credentialName)
{ {
@@ -189,7 +232,7 @@ namespace Data_Coupler.Services
} }
/// <summary> /// <summary>
/// Pulisce tutta la cache dei client REST /// Pulisce tutta la cache dei client REST.
/// </summary> /// </summary>
public void ClearAllRestClientCache() public void ClearAllRestClientCache()
{ {