[Feature] Aggiunto supporto OAuth2 client_credentials per Salesforce
Implementato il flusso OAuth2 grant_type=client_credentials come alternativa al flusso password gia' esistente per l'autenticazione Salesforce server-to-server. La modifica e' completamente retrocompatibile (default rimane Password). ## Dettaglio modifiche ### CredentialManager/Models/CredentialModels.cs - Aggiunto enum SalesforceGrantType con valori Password e ClientCredentials - Aggiunta proprieta' GrantType (default: Password) su RestApiCredential - Aggiunta proprieta' GrantType (default: Password) su SalesforceCredential ### DataConnection/REST/Configuration/RestServiceOptions.cs - Aggiunta proprieta' SalesforceGrantType per passare il tipo di flusso al client ### DataConnection/REST/Implementations/SalesforceServiceClient.cs - Iniettato ILogger<SalesforceServiceClient> con NullLogger come fallback - Sostituiti ~165 Console.WriteLine con chiamate ILogger appropriate (LogDebug per dettagli, LogInformation per eventi, LogWarning/LogError per problemi) - Aggiunto AuthenticateWithPasswordAsync: incapsula il flusso grant_type=password - Aggiunto AuthenticateWithClientCredentialsAsync: implementa grant_type=client_credentials (richiede solo ClientId e ClientSecret, nessun utente, URL My Domain obbligatorio) - Aggiunto SendTokenRequestAsync: helper condiviso per la POST al token endpoint - Aggiornato AuthenticateAsync() override: instrada al flusso corretto in base a GrantType - Rimosso modificatore static da NormalizeNumericValues (usava _logger, causava CS0120) ### Data_Coupler/Services/DataConnectionFactory.cs - Mappatura del campo GrantType dalle opzioni Salesforce a RestServiceOptions - Passaggio dell'ILogger al costruttore di SalesforceServiceClient ### CredentialManager/Services/CredentialService.cs - SaveRestApiCredentialAsync (blocco Salesforce): serializza GrantType in AdditionalParameters - SaveSalesforceCredentialAsync: aggiunto GrantType nel dizionario iniziale - MapToRestApiCredential: deserializza GrantType da AdditionalParameters con Enum.TryParse - MapToSalesforceCredential: idem per il tipo SalesforceCredential ### DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs - TestSalesforceOAuthLogin aggiornato: per ClientCredentials invia solo client_id e client_secret (senza username/password/security_token); per Password comportamento invariato ### Data_Coupler/Pages/CredentialManagement.razor - Aggiunto dropdown 'Tipo di Autenticazione OAuth2' nella sezione Salesforce - I campi Username, Password e Security Token vengono nascosti quando si seleziona il flusso ClientCredentials - Alert contestuale: warning My Domain URL per ClientCredentials, info per Password - GrantType propagato correttamente in EditRestApiCredential e TestRestApiConnectionFromModal ### AGENTS.md - Aggiunta sezione di documentazione per la nuova funzionalita' OAuth2 client_credentials
This commit is contained in:
@@ -13,6 +13,44 @@
|
||||
- **Backup e Ripristino**: Sistema completo di backup/restore per configurazioni e dati
|
||||
- **Amministrazione Avanzata**: Interfaccia unificata per gestione sistema e sicurezza
|
||||
|
||||
## 🚀 **NUOVE FUNZIONALITÀ - Salesforce OAuth2 Client Credentials Flow (2026)**
|
||||
|
||||
### Supporto `grant_type=client_credentials` per autenticazione server-to-server
|
||||
**Data Aggiornamento**: 2026
|
||||
|
||||
#### **Panoramica**
|
||||
Aggiunto supporto per il flusso OAuth2 `client_credentials` come alternativa al flusso `password` già esistente.
|
||||
Completamente retrocompatibile: il default rimane `Password`.
|
||||
|
||||
#### **Enum `SalesforceGrantType`** (in `CredentialManager/Models/CredentialModels.cs`)
|
||||
```csharp
|
||||
public enum SalesforceGrantType
|
||||
{
|
||||
Password, // grant_type=password — richiede Username, Password, SecurityToken (+ClientId/ClientSecret)
|
||||
ClientCredentials // grant_type=client_credentials — server-to-server, nessun utente
|
||||
}
|
||||
```
|
||||
|
||||
#### **Differenze tra i flussi**
|
||||
| Aspetto | `password` | `client_credentials` |
|
||||
|---|---|---|
|
||||
| Richiede Username/Password | ✅ Sì | ❌ No |
|
||||
| Richiede SecurityToken | ✅ Sì (se non Connected App) | ❌ No |
|
||||
| ClientId / ClientSecret | Opzionale | ✅ Obbligatorio |
|
||||
| Base URL | login/test.salesforce.com | **My Domain URL** (es. `https://myorg.my.salesforce.com`) |
|
||||
| Utente Salesforce | Necessario | Integration User (assegnato nella Connected App) |
|
||||
|
||||
#### **File modificati**
|
||||
- `CredentialManager/Models/CredentialModels.cs` — enum `SalesforceGrantType`, proprietà `GrantType` su `RestApiCredential` e `SalesforceCredential`
|
||||
- `DataConnection/REST/Configuration/RestServiceOptions.cs` — proprietà `SalesforceGrantType`
|
||||
- `DataConnection/REST/Implementations/SalesforceServiceClient.cs` — `AuthenticateWithPasswordAsync`, `AuthenticateWithClientCredentialsAsync`, `SendTokenRequestAsync`; `ILogger` iniettato; `NormalizeNumericValues` reso non-static
|
||||
- `Data_Coupler/Services/DataConnectionFactory.cs` — mapping `GrantType`, logger passato al client
|
||||
- `CredentialManager/Services/CredentialService.cs` — `GrantType` serializzato/deserializzato in `AdditionalParameters` JSON
|
||||
- `DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs` — `TestSalesforceOAuthLogin` instrada per `GrantType`
|
||||
- `Data_Coupler/Pages/CredentialManagement.razor` — dropdown "Tipo di Autenticazione OAuth2"; Username/Password/SecurityToken nascosti per `ClientCredentials`; warning My Domain URL
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **NUOVE FUNZIONALITÀ - Salesforce Optimizations (Febbraio 2026)**
|
||||
|
||||
### Salesforce Batch Describe via Composite API
|
||||
|
||||
@@ -22,6 +22,27 @@ public enum RestServiceType
|
||||
Salesforce
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tipo di flusso OAuth2 per Salesforce
|
||||
/// </summary>
|
||||
public enum SalesforceGrantType
|
||||
{
|
||||
/// <summary>
|
||||
/// Flusso Username/Password (grant_type=password).
|
||||
/// Richiede: ClientId, ClientSecret, Username, Password (+SecurityToken se non IP-trusted).
|
||||
/// URL di login: https://login.salesforce.com o https://test.salesforce.com.
|
||||
/// </summary>
|
||||
Password,
|
||||
|
||||
/// <summary>
|
||||
/// Flusso Client Credentials (grant_type=client_credentials) — server-to-server, senza utente.
|
||||
/// Richiede: ClientId, ClientSecret.
|
||||
/// URL obbligatorio: My Domain URL (es. https://myorg.my.salesforce.com).
|
||||
/// La Connected App deve avere "Enable Client Credentials Flow" attivato e un Integration User assegnato.
|
||||
/// </summary>
|
||||
ClientCredentials
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tipi di database supportati (allineato con DataConnection.Enums.DatabaseType)
|
||||
/// </summary>
|
||||
@@ -106,6 +127,7 @@ public class RestApiCredential
|
||||
public string? ApiVersion { get; set; } = "59.0";
|
||||
public bool IsSandbox { get; set; } = false;
|
||||
public bool UseSoapApi { get; set; } = false;
|
||||
public SalesforceGrantType GrantType { get; set; } = SalesforceGrantType.Password;
|
||||
public string? RefreshToken { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
public DateTime? TokenExpiry { get; set; }
|
||||
@@ -145,6 +167,8 @@ public class SalesforceCredential
|
||||
public bool IsSandbox { get; set; } = false; // Se è un ambiente sandbox
|
||||
public int TimeoutSeconds { get; set; } = 120;
|
||||
public bool UseSoapApi { get; set; } = false; // Se usare SOAP invece di REST
|
||||
/// <summary>Tipo di flusso OAuth2 da utilizzare. Default: Password (retrocompatibile).</summary>
|
||||
public SalesforceGrantType GrantType { get; set; } = SalesforceGrantType.Password;
|
||||
public string? RefreshToken { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
public DateTime? TokenExpiry { get; set; }
|
||||
|
||||
@@ -233,6 +233,7 @@ public class CredentialService : ICredentialService
|
||||
additionalParams["ApiVersion"] = credential.ApiVersion;
|
||||
additionalParams["IsSandbox"] = credential.IsSandbox.ToString();
|
||||
additionalParams["UseSoapApi"] = credential.UseSoapApi.ToString();
|
||||
additionalParams["GrantType"] = credential.GrantType.ToString();
|
||||
if (!string.IsNullOrEmpty(credential.RefreshToken))
|
||||
additionalParams["RefreshToken"] = credential.RefreshToken;
|
||||
if (!string.IsNullOrEmpty(credential.AccessToken))
|
||||
@@ -523,7 +524,8 @@ public class CredentialService : ICredentialService
|
||||
["SecurityToken"] = credential.SecurityToken,
|
||||
["ApiVersion"] = credential.ApiVersion,
|
||||
["IsSandbox"] = credential.IsSandbox.ToString(),
|
||||
["UseSoapApi"] = credential.UseSoapApi.ToString()
|
||||
["UseSoapApi"] = credential.UseSoapApi.ToString(),
|
||||
["GrantType"] = credential.GrantType.ToString()
|
||||
};
|
||||
|
||||
// Aggiungi ClientId e ClientSecret se forniti
|
||||
@@ -793,6 +795,8 @@ public class CredentialService : ICredentialService
|
||||
credential.IsSandbox = sandbox;
|
||||
if (additionalParams.TryGetValue("UseSoapApi", out var useSoap) && bool.TryParse(useSoap, out var soap))
|
||||
credential.UseSoapApi = soap;
|
||||
if (additionalParams.TryGetValue("GrantType", out var grantTypeStr) && Enum.TryParse<SalesforceGrantType>(grantTypeStr, out var grantType))
|
||||
credential.GrantType = grantType;
|
||||
if (additionalParams.TryGetValue("RefreshToken", out var refreshToken))
|
||||
credential.RefreshToken = refreshToken;
|
||||
if (additionalParams.TryGetValue("AccessToken", out var accessToken))
|
||||
@@ -915,6 +919,8 @@ public class CredentialService : ICredentialService
|
||||
credential.IsSandbox = sandbox;
|
||||
if (additionalParams.TryGetValue("UseSoapApi", out var useSoap) && bool.TryParse(useSoap, out var soap))
|
||||
credential.UseSoapApi = soap;
|
||||
if (additionalParams.TryGetValue("GrantType", out var grantTypeStr) && Enum.TryParse<SalesforceGrantType>(grantTypeStr, out var grantType))
|
||||
credential.GrantType = grantType;
|
||||
if (additionalParams.TryGetValue("RefreshToken", out var refreshToken))
|
||||
credential.RefreshToken = refreshToken;
|
||||
if (additionalParams.TryGetValue("AccessToken", out var accessToken))
|
||||
|
||||
@@ -863,23 +863,39 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
||||
try
|
||||
{
|
||||
var tokenUrl = credential.LoginUrl.TrimEnd('/') + "/services/oauth2/token";
|
||||
List<KeyValuePair<string, string>> tokenData;
|
||||
|
||||
var tokenData = new List<KeyValuePair<string, string>>
|
||||
if (credential.GrantType == CredentialManager.Models.SalesforceGrantType.ClientCredentials)
|
||||
{
|
||||
new("grant_type", "password"),
|
||||
new("username", credential.Username),
|
||||
new("password", credential.Password + credential.SecurityToken),
|
||||
new("client_id", credential.ClientId ?? ""),
|
||||
new("client_secret", credential.ClientSecret ?? "")
|
||||
};
|
||||
// Client Credentials flow — server-to-server, no user
|
||||
tokenData = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new("grant_type", "client_credentials"),
|
||||
new("client_id", credential.ClientId ?? ""),
|
||||
new("client_secret", credential.ClientSecret ?? "")
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Password flow (default)
|
||||
tokenData = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new("grant_type", "password"),
|
||||
new("username", credential.Username),
|
||||
new("password", credential.Password + credential.SecurityToken),
|
||||
new("client_id", credential.ClientId ?? ""),
|
||||
new("client_secret", credential.ClientSecret ?? "")
|
||||
};
|
||||
}
|
||||
|
||||
var tokenContent = new FormUrlEncodedContent(tokenData);
|
||||
var response = await httpClient.PostAsync(tokenUrl, tokenContent);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
return (true, $"Connessione Salesforce riuscita!\n\nDettagli:\n- Login URL: {credential.LoginUrl}\n- API Version: {credential.ApiVersion}\n- Sandbox: {credential.IsSandbox}\n- Tipo Auth: OAuth2\n- Timeout: {credential.TimeoutSeconds}s");
|
||||
var flowLabel = credential.GrantType == CredentialManager.Models.SalesforceGrantType.ClientCredentials
|
||||
? "client_credentials" : "password";
|
||||
return (true, $"Connessione Salesforce riuscita!\n\nDettagli:\n- Login URL: {credential.LoginUrl}\n- API Version: {credential.ApiVersion}\n- Sandbox: {credential.IsSandbox}\n- Tipo Auth: OAuth2 ({flowLabel})\n- Timeout: {credential.TimeoutSeconds}s");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -41,6 +41,11 @@ namespace DataConnection.REST.Configuration
|
||||
/// </summary>
|
||||
public bool IgnoreSslErrors { get; set; } = false;
|
||||
|
||||
// Add other relevant configuration properties (e.g., OAuth settings, specific headers)
|
||||
/// <summary>
|
||||
/// Salesforce OAuth2 grant type. Default: Password (retrocompatibile).
|
||||
/// ClientCredentials = server-to-server, senza utente.
|
||||
/// </summary>
|
||||
public CredentialManager.Models.SalesforceGrantType SalesforceGrantType { get; set; }
|
||||
= CredentialManager.Models.SalesforceGrantType.Password;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -663,11 +663,30 @@ else
|
||||
} <!-- Campi specifici per Salesforce -->
|
||||
@if (currentRestApiCredential.ServiceType == RestServiceType.Salesforce)
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
<strong>Opzioni di Autenticazione:</strong><br/>
|
||||
• <strong>Username/Password + Security Token:</strong> Autenticazione standard<br/>
|
||||
• <strong>Username/Password + Client ID/Secret:</strong> Autenticazione OAuth<br/>
|
||||
• Il Security Token è richiesto solo se non si configura una Connected App (Client ID/Secret)
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-semibold">Tipo di Autenticazione OAuth2</label>
|
||||
<InputSelect class="form-select" @bind-Value="currentRestApiCredential.GrantType">
|
||||
<option value="Password">Password Flow — Username + Password + Security Token (grant_type=password)</option>
|
||||
<option value="ClientCredentials">Client Credentials — Server-to-Server, nessun utente (grant_type=client_credentials)</option>
|
||||
</InputSelect>
|
||||
@if (currentRestApiCredential.GrantType == CredentialManager.Models.SalesforceGrantType.ClientCredentials)
|
||||
{
|
||||
<div class="alert alert-warning mt-2 py-2">
|
||||
<i class="fa fa-exclamation-triangle me-1"></i>
|
||||
<strong>client_credentials</strong>: il <strong>Base URL</strong> deve essere il <strong>My Domain URL</strong> della tua org
|
||||
(es. <code>https://myorg.my.salesforce.com</code>), <strong>non</strong> login.salesforce.com.<br/>
|
||||
Richiede: Connected App con "Enable Client Credentials Flow" attivato e un Integration User assegnato.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info mt-2 py-2">
|
||||
<i class="fa fa-info-circle me-1"></i>
|
||||
<strong>password flow</strong>: Username, Password + Security Token. Client ID/Secret facoltativi (Connected App).
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@@ -687,11 +706,15 @@ else
|
||||
<div class="form-text">Esempio: 59.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <div class="mb-3">
|
||||
<label class="form-label">@GetFieldLabel("SecurityToken", currentRestApiCredential.ServiceType)</label>
|
||||
<InputText type="password" class="form-control" @bind-Value="currentRestApiCredential.SecurityToken" />
|
||||
<div class="form-text">Token di sicurezza Salesforce (richiesto solo se non si usa OAuth o Connected App)</div>
|
||||
</div>
|
||||
@if (currentRestApiCredential.GrantType != CredentialManager.Models.SalesforceGrantType.ClientCredentials)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label class="form-label">@GetFieldLabel("SecurityToken", currentRestApiCredential.ServiceType)</label>
|
||||
<InputText type="password" class="form-control" @bind-Value="currentRestApiCredential.SecurityToken" />
|
||||
<div class="form-text">Token di sicurezza Salesforce (richiesto solo se non si usa OAuth o Connected App)</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@@ -738,8 +761,9 @@ else
|
||||
}
|
||||
|
||||
<!-- Campi comuni per autenticazione username/password -->
|
||||
@if (currentRestApiCredential.ServiceType != RestServiceType.Generic ||
|
||||
(string.IsNullOrEmpty(currentRestApiCredential.ApiKey) && string.IsNullOrEmpty(currentRestApiCredential.AuthToken)))
|
||||
@if ((currentRestApiCredential.ServiceType != RestServiceType.Generic ||
|
||||
(string.IsNullOrEmpty(currentRestApiCredential.ApiKey) && string.IsNullOrEmpty(currentRestApiCredential.AuthToken))) &&
|
||||
!(currentRestApiCredential.ServiceType == RestServiceType.Salesforce && currentRestApiCredential.GrantType == CredentialManager.Models.SalesforceGrantType.ClientCredentials))
|
||||
{ <div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
@@ -1312,6 +1336,7 @@ else
|
||||
ApiVersion = credential.ApiVersion,
|
||||
IsSandbox = credential.IsSandbox,
|
||||
UseSoapApi = credential.UseSoapApi,
|
||||
GrantType = credential.GrantType,
|
||||
RefreshToken = credential.RefreshToken,
|
||||
AccessToken = credential.AccessToken,
|
||||
TokenExpiry = credential.TokenExpiry
|
||||
@@ -1533,7 +1558,8 @@ else
|
||||
ClientSecret = currentRestApiCredential.ClientSecret,
|
||||
ApiVersion = currentRestApiCredential.ApiVersion,
|
||||
IsSandbox = currentRestApiCredential.IsSandbox,
|
||||
UseSoapApi = currentRestApiCredential.UseSoapApi
|
||||
UseSoapApi = currentRestApiCredential.UseSoapApi,
|
||||
GrantType = currentRestApiCredential.GrantType
|
||||
}; // Salviamo temporaneamente la credenziale per il test
|
||||
await CredentialService.SaveRestApiCredentialAsync(tempCredential);
|
||||
|
||||
|
||||
@@ -133,7 +133,9 @@ namespace Data_Coupler.Services
|
||||
// Per Salesforce usiamo i campi specifici ClientId e ClientSecret
|
||||
options.ApiKey = credential.ClientId; // ClientId -> ApiKey
|
||||
options.AuthToken = credential.ClientSecret; // ClientSecret -> AuthToken
|
||||
_logger.LogInformation("Salesforce mapping - ClientId: '{ClientId}', ClientSecret: {HasSecret}, Username: '{Username}', Password: {HasPassword}",
|
||||
options.SalesforceGrantType = credential.GrantType;
|
||||
_logger.LogInformation("Salesforce mapping - GrantType: {GrantType}, ClientId: '{ClientId}', ClientSecret: {HasSecret}, Username: '{Username}', Password: {HasPassword}",
|
||||
credential.GrantType,
|
||||
credential.ClientId,
|
||||
!string.IsNullOrEmpty(credential.ClientSecret),
|
||||
credential.Username,
|
||||
@@ -215,7 +217,9 @@ namespace Data_Coupler.Services
|
||||
{
|
||||
var httpClientFactory = _serviceProvider.GetRequiredService<IHttpClientFactory>();
|
||||
var httpClient = httpClientFactory.CreateClient();
|
||||
return new SalesforceServiceClient(httpClient, options);
|
||||
var loggerFactory = _serviceProvider.GetService<Microsoft.Extensions.Logging.ILoggerFactory>();
|
||||
var sfLogger = loggerFactory?.CreateLogger<DataConnection.REST.Implementations.SalesforceServiceClient>();
|
||||
return new DataConnection.REST.Implementations.SalesforceServiceClient(httpClient, options, sfLogger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user