Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b75e57fe31 | |||
| 9fab99112b | |||
| 91dbe9ae11 | |||
| e43b7dc869 | |||
| f1f75d59ac |
@@ -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,8 +863,22 @@ 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)
|
||||
{
|
||||
// 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),
|
||||
@@ -872,14 +886,16 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
||||
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
@@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CredentialManager", "Creden
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components", "Components\Components.csproj", "{B5114CAC-3E03-4150-B93C-652882F66CB7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MachineGuard", "MachineGuard\MachineGuard.csproj", "{AFF3AD52-0356-4879-A0C8-67819611445A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MachineGuardSetup", "MachineGuardSetup\MachineGuardSetup.csproj", "{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -69,6 +73,30 @@ Global
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B5114CAC-3E03-4150-B93C-652882F66CB7}.Release|x86.Build.0 = Release|Any CPU
|
||||
{AFF3AD52-0356-4879-A0C8-67819611445A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AFF3AD52-0356-4879-A0C8-67819611445A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AFF3AD52-0356-4879-A0C8-67819611445A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{AFF3AD52-0356-4879-A0C8-67819611445A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{AFF3AD52-0356-4879-A0C8-67819611445A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{AFF3AD52-0356-4879-A0C8-67819611445A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{AFF3AD52-0356-4879-A0C8-67819611445A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AFF3AD52-0356-4879-A0C8-67819611445A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AFF3AD52-0356-4879-A0C8-67819611445A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{AFF3AD52-0356-4879-A0C8-67819611445A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{AFF3AD52-0356-4879-A0C8-67819611445A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{AFF3AD52-0356-4879-A0C8-67819611445A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{EACF8FA5-EF21-4D7E-8CA3-347C74C4CD0D}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -286,6 +286,8 @@ public class ScheduledJobService : BackgroundService
|
||||
executionHistory.EndTime = DateTime.Now;
|
||||
executionHistory.Status = "failed";
|
||||
executionHistory.Message = $"Errore durante l'esecuzione automatica: {ex.Message}";
|
||||
// Memorizza il dettaglio completo (stack trace) solo per scopi diagnostici;
|
||||
// la UI in produzione ne mostrerà una versione sanitizzata senza percorsi di file.
|
||||
executionHistory.ErrorDetails = ex.ToString();
|
||||
await scheduleService.UpdateExecutionHistoryAsync(executionHistory);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<ProjectReference Include="..\DataConnection\DataConnection.csproj" />
|
||||
<ProjectReference Include="..\CredentialManager\CredentialManager.csproj" />
|
||||
<ProjectReference Include="..\Components\Components.csproj" />
|
||||
<ProjectReference Include="..\MachineGuard\MachineGuard.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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">
|
||||
</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);
|
||||
|
||||
|
||||
@@ -2894,25 +2894,42 @@ public partial class DataCoupler : ComponentBase
|
||||
if (!trimmedQuery.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
// Lista di parole chiave vietate per sicurezza
|
||||
var forbiddenKeywords = new[]
|
||||
// Parole chiave complete: devono essere token SQL isolati (\bKEYWORD\b)
|
||||
// Evita falsi positivi su nomi di colonne come UpdateDate, CreateDate, DeletedAt, ecc.
|
||||
var forbiddenFullKeywords = new[]
|
||||
{
|
||||
"INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", "TRUNCATE",
|
||||
"EXEC", "EXECUTE", "sp_", "xp_", "BULK", "OPENROWSET", "OPENDATASOURCE"
|
||||
"EXEC", "EXECUTE", "BULK", "OPENROWSET", "OPENDATASOURCE"
|
||||
};
|
||||
|
||||
// Prefissi pericolosi: devono iniziare la parola (sp_anything, xp_anything)
|
||||
// Non si usa \bprefix\b perché _ è un word-char e mancherebbe sp_executesql, xp_cmdshell, ecc.
|
||||
var forbiddenPrefixes = new[] { "SP_", "XP_" };
|
||||
|
||||
var upperQuery = trimmedQuery.ToUpperInvariant();
|
||||
|
||||
// Verifica che non contenga parole chiave vietate
|
||||
foreach (var keyword in forbiddenKeywords)
|
||||
// Verifica parole chiave complete con word boundary
|
||||
foreach (var keyword in forbiddenFullKeywords)
|
||||
{
|
||||
if (upperQuery.Contains(keyword))
|
||||
var pattern = $@"\b{System.Text.RegularExpressions.Regex.Escape(keyword)}\b";
|
||||
if (System.Text.RegularExpressions.Regex.IsMatch(upperQuery, pattern))
|
||||
{
|
||||
Logger.LogWarning("Query rifiutata: contiene parola chiave vietata '{Keyword}'", keyword);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verifica prefissi stored procedure: \bSP_ cattura sp_anything, xp_anything
|
||||
foreach (var prefix in forbiddenPrefixes)
|
||||
{
|
||||
var pattern = $@"\b{System.Text.RegularExpressions.Regex.Escape(prefix)}";
|
||||
if (System.Text.RegularExpressions.Regex.IsMatch(upperQuery, pattern))
|
||||
{
|
||||
Logger.LogWarning("Query rifiutata: contiene prefisso stored procedure vietato '{Prefix}'", prefix);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -311,14 +311,21 @@ public partial class Scheduling : ComponentBase
|
||||
|
||||
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, status, message, result.RecordsProcessed);
|
||||
|
||||
// Notifica l'utente (best-effort: la connessione browser potrebbe essere stata interrotta
|
||||
// durante un'esecuzione lunga senza che questo invalidi il risultato già salvato).
|
||||
try
|
||||
{
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
await ShowSuccessMessage($"Schedulazione eseguita con successo! {result.RecordsProcessed} record elaborati in {result.Duration.TotalSeconds:F2} secondi.");
|
||||
}
|
||||
else
|
||||
{
|
||||
await ShowErrorMessage($"Errore durante l'esecuzione: {result.ErrorMessage}");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// La connessione Blazor è stata interrotta durante l'esecuzione: il risultato è
|
||||
// già stato salvato correttamente, la notifica non può essere recapitata.
|
||||
Logger.LogWarning("Notifica UI non inviata per la schedulazione {ScheduleId}: connessione browser interrotta durante l'esecuzione", scheduleId);
|
||||
}
|
||||
|
||||
await LoadSchedules();
|
||||
}
|
||||
@@ -326,7 +333,7 @@ public partial class Scheduling : ComponentBase
|
||||
{
|
||||
Logger.LogError(ex, "Errore nell'esecuzione manuale schedulazione {ScheduleId}", scheduleId);
|
||||
|
||||
// Aggiorna lo storico in caso di eccezione
|
||||
// Aggiorna lo storico in caso di eccezione durante l'esecuzione effettiva
|
||||
if (executionHistory != null)
|
||||
{
|
||||
executionHistory.EndTime = DateTime.Now;
|
||||
@@ -337,8 +344,16 @@ public partial class Scheduling : ComponentBase
|
||||
}
|
||||
|
||||
await ScheduleService.UpdateExecutionStatusAsync(scheduleId, "failed", $"Errore: {ex.Message}");
|
||||
|
||||
try
|
||||
{
|
||||
await ShowErrorMessage("Errore nell'esecuzione: " + ex.Message);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogWarning("Notifica UI non inviata per la schedulazione {ScheduleId}: connessione browser non disponibile", scheduleId);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
isExecuting = false;
|
||||
|
||||
@@ -235,7 +235,15 @@
|
||||
{
|
||||
<h6>Dettagli Errori</h6>
|
||||
<div class="alert alert-danger">
|
||||
@if (IsDevelopment)
|
||||
{
|
||||
<pre style="white-space: pre-wrap; font-size: 0.85em;">@selectedExecution.ErrorDetails</pre>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="mb-1">@GetSanitizedErrorMessage(selectedExecution.ErrorDetails)</p>
|
||||
<small class="text-muted"><i class="fas fa-info-circle"></i> Per i dettagli tecnici completi consultare i log dell'applicazione.</small>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using CredentialManager.Models;
|
||||
using CredentialManager.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
@@ -11,6 +12,29 @@ public partial class SchedulingHistory : ComponentBase
|
||||
[Inject] private IProfileScheduleService ScheduleService { get; set; } = null!;
|
||||
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
||||
[Inject] private ILogger<SchedulingHistory> Logger { get; set; } = null!;
|
||||
[Inject] private IWebHostEnvironment WebHostEnvironment { get; set; } = null!;
|
||||
|
||||
protected bool IsDevelopment => WebHostEnvironment.IsDevelopment();
|
||||
|
||||
/// <summary>
|
||||
/// Restituisce solo il messaggio dell'eccezione (senza stack trace) per la visualizzazione in produzione.
|
||||
/// </summary>
|
||||
protected static string GetSanitizedErrorMessage(string errorDetails)
|
||||
{
|
||||
if (string.IsNullOrEmpty(errorDetails))
|
||||
return string.Empty;
|
||||
|
||||
// Prende solo le righe fino al primo stack frame (riga che inizia con " at")
|
||||
var lines = errorDetails.Split('\n');
|
||||
var messageLines = new System.Collections.Generic.List<string>();
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.TrimStart().StartsWith("at ", StringComparison.Ordinal))
|
||||
break;
|
||||
messageLines.Add(line.TrimEnd());
|
||||
}
|
||||
return string.Join("\n", messageLines).Trim();
|
||||
}
|
||||
|
||||
protected List<ScheduleExecutionHistory>? executionHistory;
|
||||
protected ScheduleExecutionHistory? selectedExecution;
|
||||
|
||||
@@ -10,6 +10,7 @@ using CredentialManager;
|
||||
using Data_Coupler.Services;
|
||||
using Data_Coupler.BackgroundServices;
|
||||
using CredentialManager.Services;
|
||||
using MachineGuard;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -130,6 +131,9 @@ builder.Services.AddScoped<Data_Coupler.Services.IDeletionSyncService, Data_Coup
|
||||
// Register Background Services (solo uno per evitare duplicazioni)
|
||||
builder.Services.AddHostedService<Data_Coupler.BackgroundServices.ScheduledJobService>();
|
||||
|
||||
// Register MachineGuard — protezione machine-binding tramite DPAPI
|
||||
builder.Services.AddMachineGuard(builder.Configuration);
|
||||
|
||||
// Configurazione URL e timeout per servizio Windows
|
||||
var urls = builder.Configuration.GetValue<string>("Urls") ?? "http://*:7550";
|
||||
builder.WebHost.UseUrls(urls);
|
||||
@@ -143,6 +147,38 @@ builder.WebHost.ConfigureKestrel(serverOptions =>
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
#region MachineGuard — verifica autorizzazione macchina
|
||||
// Questa verifica deve avvenire PRIMA di qualsiasi altra inizializzazione.
|
||||
// Se la macchina non è autorizzata, l'applicazione viene arrestata immediatamente.
|
||||
{
|
||||
var machineGuard = app.Services.GetRequiredService<IMachineGuard>();
|
||||
if (!machineGuard.Verify())
|
||||
{
|
||||
var critLogger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||
critLogger.LogCritical(
|
||||
"MachineGuard: questa macchina NON è autorizzata a eseguire Data Coupler. " +
|
||||
"Eseguire MachineGuardSetup.exe come Amministratore per configurare questa macchina. " +
|
||||
"Applicazione arrestata.");
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
try
|
||||
{
|
||||
using var eventLog = new System.Diagnostics.EventLog("Application");
|
||||
eventLog.Source = "DataCouplerService";
|
||||
eventLog.WriteEntry(
|
||||
"MachineGuard: macchina non autorizzata. " +
|
||||
"Eseguire MachineGuardSetup.exe come Amministratore. Applicazione arrestata.",
|
||||
System.Diagnostics.EventLogEntryType.Error);
|
||||
}
|
||||
catch { /* Ignora errori di scrittura EventLog */ }
|
||||
}
|
||||
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
// Initialize database con timeout e retry
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
+2
-2
@@ -25,13 +25,13 @@ COPY ["Components/", "Components/"]
|
||||
|
||||
# Build del progetto principale con output path corto
|
||||
WORKDIR "/s/Data_Coupler"
|
||||
RUN dotnet build "Data_Coupler.csproj" -c Release -o /o --no-restore /p:MinVerVersionOverride=%APP_VERSION%
|
||||
RUN dotnet build "Data_Coupler.csproj" -c Release -o /o --no-restore /p:ContinuousIntegrationBuild=true /p:MinVerVersionOverride=%APP_VERSION%
|
||||
|
||||
# Stage 2: Publish
|
||||
FROM build AS publish
|
||||
# Necessario ridichiarare ARG dopo FROM in multi-stage build
|
||||
ARG APP_VERSION=0.0.0-alpha.0
|
||||
RUN dotnet publish "Data_Coupler.csproj" -c Release -o /p --no-restore -r win-x64 --self-contained false /p:MinVerVersionOverride=%APP_VERSION%
|
||||
RUN dotnet publish "Data_Coupler.csproj" -c Release -o /p --no-restore -r win-x64 --self-contained false /p:ContinuousIntegrationBuild=true /p:MinVerVersionOverride=%APP_VERSION%
|
||||
|
||||
# Stage 3: Runtime
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-nanoserver-ltsc2022 AS final
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace MachineGuard;
|
||||
|
||||
/// <summary>
|
||||
/// Implementazione Windows-only della protezione machine-binding tramite DPAPI.
|
||||
/// Utilizza <see cref="ProtectedData"/> con scope <see cref="DataProtectionScope.LocalMachine"/>:
|
||||
/// il dato cifrato è legato fisicamente alla macchina e non può essere decifrato
|
||||
/// su un'altra macchina, anche con gli stessi account o credenziali.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal sealed class DpapiMachineGuard : IMachineGuard
|
||||
{
|
||||
private readonly MachineGuardOptions _options;
|
||||
private readonly ILogger<DpapiMachineGuard> _logger;
|
||||
|
||||
public DpapiMachineGuard(IOptions<MachineGuardOptions> options, ILogger<DpapiMachineGuard> logger)
|
||||
{
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Verify()
|
||||
{
|
||||
var secretPath = ResolveSecretFilePath();
|
||||
|
||||
if (!File.Exists(secretPath))
|
||||
{
|
||||
_logger.LogError(
|
||||
"MachineGuard: file secret non trovato in '{Path}'. " +
|
||||
"Eseguire MachineGuardSetup.exe su questa macchina per inizializzare l'autorizzazione.",
|
||||
secretPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var encryptedBytes = File.ReadAllBytes(secretPath);
|
||||
var decryptedBytes = ProtectedData.Unprotect(encryptedBytes, null, DataProtectionScope.LocalMachine);
|
||||
var decryptedToken = Encoding.UTF8.GetString(decryptedBytes);
|
||||
|
||||
if (!string.Equals(decryptedToken, MachineGuardToken.ExpectedToken, StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogError(
|
||||
"MachineGuard: il token decifrato non corrisponde al token atteso. " +
|
||||
"Questa macchina non è autorizzata a eseguire questa applicazione.");
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogInformation("MachineGuard: autorizzazione macchina verificata con successo.");
|
||||
return true;
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"MachineGuard: decifrazione fallita. " +
|
||||
"Il file secret potrebbe provenire da un'altra macchina o essere corrotto. " +
|
||||
"Percorso: '{Path}'", secretPath);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"MachineGuard: errore imprevisto durante la verifica. Percorso: '{Path}'", secretPath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string ResolveSecretFilePath()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_options.SecretFilePath))
|
||||
return _options.SecretFilePath;
|
||||
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||
if (string.IsNullOrEmpty(appData))
|
||||
appData = @"C:\ProgramData";
|
||||
|
||||
return Path.Combine(appData, "DataCoupler", "machine.guard");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace MachineGuard;
|
||||
|
||||
/// <summary>
|
||||
/// Interfaccia per il meccanismo di protezione machine-binding tramite DPAPI.
|
||||
/// </summary>
|
||||
public interface IMachineGuard
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifica che l'applicazione sia in esecuzione su una macchina autorizzata.
|
||||
/// Restituisce <c>true</c> se l'autorizzazione è confermata, <c>false</c> altrimenti.
|
||||
/// </summary>
|
||||
bool Verify();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,75 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MachineGuard;
|
||||
|
||||
/// <summary>
|
||||
/// Metodi di estensione per la registrazione di MachineGuard nel container DI.
|
||||
/// </summary>
|
||||
public static class MachineGuardExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registra il servizio <see cref="IMachineGuard"/> nel container DI.
|
||||
/// <para>
|
||||
/// Se <c>MachineGuard:Enabled</c> è <c>false</c> in appsettings, viene registrato
|
||||
/// un guard no-op che approva sempre la verifica (utile per sviluppo/CI).
|
||||
/// Su piattaforme non-Windows, DPAPI non è disponibile e il guard viene disabilitato automaticamente.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static IServiceCollection AddMachineGuard(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
services.Configure<MachineGuardOptions>(
|
||||
configuration.GetSection(MachineGuardOptions.SectionName));
|
||||
|
||||
services.AddSingleton<IMachineGuard>(sp =>
|
||||
{
|
||||
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("MachineGuard.Startup");
|
||||
|
||||
#if DEBUG
|
||||
// In build Debug la protezione è sempre disabilitata — nessuna configurazione richiesta.
|
||||
logger.LogInformation(
|
||||
"MachineGuard: build DEBUG — protezione machine-binding disabilitata automaticamente.");
|
||||
return new NullMachineGuard();
|
||||
#else
|
||||
// In build Release la protezione è sempre attiva.
|
||||
// Può essere disabilitata esplicitamente via MachineGuard:Enabled = false
|
||||
// (utile per ambienti Linux/Docker o casi eccezionali).
|
||||
var options = sp.GetRequiredService<IOptions<MachineGuardOptions>>();
|
||||
|
||||
if (!options.Value.Enabled)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"MachineGuard: protezione machine-binding DISABILITATA via configurazione. " +
|
||||
"Impostare MachineGuard:Enabled = true in produzione.");
|
||||
return new NullMachineGuard();
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
logger.LogWarning(
|
||||
"MachineGuard: DPAPI non è disponibile su piattaforme non-Windows. " +
|
||||
"La protezione machine-binding è bypassata automaticamente.");
|
||||
return new NullMachineGuard();
|
||||
}
|
||||
|
||||
return CreateWindowsGuard(sp, options);
|
||||
#endif
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
||||
private static IMachineGuard CreateWindowsGuard(
|
||||
IServiceProvider sp,
|
||||
IOptions<MachineGuardOptions> options)
|
||||
{
|
||||
var logger = sp.GetRequiredService<ILogger<DpapiMachineGuard>>();
|
||||
return new DpapiMachineGuard(options, logger);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace MachineGuard;
|
||||
|
||||
/// <summary>
|
||||
/// Opzioni di configurazione per MachineGuard.
|
||||
/// Configurabili tramite appsettings.json nella sezione "MachineGuard".
|
||||
/// </summary>
|
||||
public sealed class MachineGuardOptions
|
||||
{
|
||||
/// <summary>Nome della sezione in appsettings.json.</summary>
|
||||
public const string SectionName = "MachineGuard";
|
||||
|
||||
/// <summary>
|
||||
/// Imposta a <c>false</c> per disabilitare completamente la protezione machine-binding.
|
||||
/// Utile in ambienti di sviluppo o CI. Default: <c>true</c>.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Percorso del file secret cifrato.
|
||||
/// Se vuoto, viene usato il percorso predefinito:
|
||||
/// Windows: %ProgramData%\DataCoupler\machine.guard
|
||||
/// Linux: /etc/datacoupler/machine.guard
|
||||
/// </summary>
|
||||
public string? SecretFilePath { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Runtime.Versioning;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace MachineGuard;
|
||||
|
||||
/// <summary>
|
||||
/// Helper pubblico per la scrittura e la verifica del file secret di MachineGuard.
|
||||
/// Usato da MachineGuardSetup e da eventuali script di deployment.
|
||||
/// </summary>
|
||||
public static class MachineGuardSetupHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Cifra il token interno con DPAPI (LocalMachine scope) e lo scrive nel percorso specificato.
|
||||
/// Crea la directory di destinazione se non esiste.
|
||||
/// </summary>
|
||||
/// <param name="secretFilePath">
|
||||
/// Percorso completo del file in cui salvare il secret cifrato.
|
||||
/// Usare <see cref="GetDefaultSecretFilePath"/> per il percorso di default.
|
||||
/// </param>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static void WriteSecret(string secretFilePath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(secretFilePath);
|
||||
|
||||
var tokenBytes = Encoding.UTF8.GetBytes(MachineGuardToken.ExpectedToken);
|
||||
var encryptedBytes = ProtectedData.Protect(tokenBytes, null, DataProtectionScope.LocalMachine);
|
||||
|
||||
var directory = Path.GetDirectoryName(secretFilePath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
File.WriteAllBytes(secretFilePath, encryptedBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica che il file secret nel percorso specificato decifrabile con successo prima del deployment.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static bool VerifySecret(string secretFilePath)
|
||||
{
|
||||
if (!File.Exists(secretFilePath))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var encryptedBytes = File.ReadAllBytes(secretFilePath);
|
||||
var decryptedBytes = ProtectedData.Unprotect(encryptedBytes, null, DataProtectionScope.LocalMachine);
|
||||
var decryptedToken = Encoding.UTF8.GetString(decryptedBytes);
|
||||
return string.Equals(decryptedToken, MachineGuardToken.ExpectedToken, StringComparison.Ordinal);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restituisce il percorso predefinito del file secret in base al sistema operativo corrente.
|
||||
/// </summary>
|
||||
public static string GetDefaultSecretFilePath()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||
if (string.IsNullOrEmpty(appData))
|
||||
appData = @"C:\ProgramData";
|
||||
return Path.Combine(appData, "DataCoupler", "machine.guard");
|
||||
}
|
||||
|
||||
return "/etc/datacoupler/machine.guard";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace MachineGuard;
|
||||
|
||||
/// <summary>
|
||||
/// Contiene il token di machine-binding cablato nel codice.
|
||||
/// Modificare questo valore per ogni distribuzione autorizzata,
|
||||
/// quindi eseguire MachineGuardSetup per scrivere il secret su ogni macchina target.
|
||||
/// </summary>
|
||||
internal static class MachineGuardToken
|
||||
{
|
||||
/// <summary>
|
||||
/// Token atteso. Deve corrispondere esattamente al valore firmato durante il setup.
|
||||
/// </summary>
|
||||
internal const string ExpectedToken = "DC-F47AC10B-58CC-4372-A567-0E02B2C3D479";
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace MachineGuard;
|
||||
|
||||
/// <summary>
|
||||
/// Implementazione no-op di <see cref="IMachineGuard"/>.
|
||||
/// Usata quando la protezione è disabilitata via configurazione
|
||||
/// o quando il sistema operativo non supporta DPAPI (es. Linux, macOS).
|
||||
/// Restituisce sempre <c>true</c> senza eseguire alcuna verifica.
|
||||
/// </summary>
|
||||
internal sealed class NullMachineGuard : IMachineGuard
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public bool Verify() => true;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>MachineGuardSetup</AssemblyName>
|
||||
<RootNamespace>MachineGuardSetup</RootNamespace>
|
||||
<!-- Standalone: publish as single self-contained exe for easy deployment -->
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MachineGuard\MachineGuard.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,198 @@
|
||||
using MachineGuard;
|
||||
|
||||
// ============================================================
|
||||
// MachineGuardSetup — Strumento di configurazione machine-binding
|
||||
// Utilizzo: eseguire come Amministratore su ogni server autorizzato
|
||||
// Indipendente da Data Coupler — nessuna dipendenza dall'applicazione
|
||||
// ============================================================
|
||||
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
|
||||
PrintBanner();
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
PrintError("Questo strumento richiede Windows (DPAPI è un'API esclusiva di Windows).");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!IsRunningAsAdministrator())
|
||||
{
|
||||
PrintWarning("Attenzione: l'applicazione non è in esecuzione come Amministratore.");
|
||||
PrintWarning("La scrittura in C:\\ProgramData potrebbe fallire senza privilegi elevati.");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
return RunSetupWindows();
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Entry point per Windows (isolato per soddisfare l'analizzatore)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
||||
static int RunSetupWindows()
|
||||
{
|
||||
var defaultPath = MachineGuardSetupHelper.GetDefaultSecretFilePath();
|
||||
|
||||
Console.WriteLine("╔══════════════════════════════════════════════════════════╗");
|
||||
Console.WriteLine("║ CONFIGURAZIONE MACHINE-BINDING ║");
|
||||
Console.WriteLine("╚══════════════════════════════════════════════════════════╝");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($" Percorso predefinito secret: {defaultPath}");
|
||||
Console.WriteLine();
|
||||
|
||||
// Chiedi conferma o percorso personalizzato
|
||||
Console.Write(" Usare il percorso predefinito? [S/n]: ");
|
||||
var input = Console.ReadLine()?.Trim().ToUpperInvariant();
|
||||
Console.WriteLine();
|
||||
|
||||
string targetPath;
|
||||
if (string.IsNullOrEmpty(input) || input == "S" || input == "Y")
|
||||
{
|
||||
targetPath = defaultPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Write(" Inserire il percorso completo del file secret: ");
|
||||
var customPath = Console.ReadLine()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(customPath))
|
||||
{
|
||||
PrintError("Percorso non valido. Operazione annullata.");
|
||||
return 1;
|
||||
}
|
||||
targetPath = customPath;
|
||||
}
|
||||
|
||||
// Verifica se esiste già un secret
|
||||
if (File.Exists(targetPath))
|
||||
{
|
||||
Console.WriteLine($" ⚠ Il file secret esiste già: {targetPath}");
|
||||
Console.Write(" Sovrascrivere? [s/N]: ");
|
||||
var overwrite = Console.ReadLine()?.Trim().ToUpperInvariant();
|
||||
Console.WriteLine();
|
||||
|
||||
if (overwrite != "S" && overwrite != "Y")
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine(" Operazione annullata dall'utente.");
|
||||
Console.ResetColor();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Verifica se il secret attuale è già valido
|
||||
Console.WriteLine(" Verifica del secret esistente...");
|
||||
if (MachineGuardSetupHelper.VerifySecret(targetPath))
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine(" ✓ Il secret esistente è GIÀ valido per questa macchina.");
|
||||
Console.ResetColor();
|
||||
Console.Write(" Continuare comunque e riscrivere? [s/N]: ");
|
||||
var rewrite = Console.ReadLine()?.Trim().ToUpperInvariant();
|
||||
Console.WriteLine();
|
||||
if (rewrite != "S" && rewrite != "Y")
|
||||
{
|
||||
Console.WriteLine(" Operazione annullata. Il secret esistente rimane invariato.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrittura del secret
|
||||
Console.WriteLine($" Scrittura del secret cifrato con DPAPI (LocalMachine) in:");
|
||||
Console.WriteLine($" {targetPath}");
|
||||
Console.WriteLine();
|
||||
|
||||
try
|
||||
{
|
||||
MachineGuardSetupHelper.WriteSecret(targetPath);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
PrintError($"Accesso negato: {ex.Message}");
|
||||
PrintError("Riprovare eseguendo il programma come Amministratore (tasto destro → Esegui come amministratore).");
|
||||
return 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PrintError($"Errore durante la scrittura del secret: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Verifica post-scrittura
|
||||
Console.WriteLine(" Verifica del secret appena scritto...");
|
||||
if (MachineGuardSetupHelper.VerifySecret(targetPath))
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine(" ✓ Secret scritto e verificato con successo!");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" Questa macchina è ora autorizzata a eseguire Data Coupler.");
|
||||
Console.WriteLine();
|
||||
PrintFileInfo(targetPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
PrintError("Verifica post-scrittura fallita. Il file potrebbe essere corrotto.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" Premere un tasto per uscire...");
|
||||
if (!Console.IsInputRedirected)
|
||||
Console.ReadKey(intercept: true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Funzioni di utilità
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
static void PrintBanner()
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(" ██████╗ █████╗ ████████╗ █████╗ ██████╗ ██████╗ ██╗ ██╗██████╗ ██╗ ███████╗██████╗ ");
|
||||
Console.WriteLine(" ██╔══██╗██╔══██╗╚══██╔══╝██╔══██╗ ██╔════╝██╔═══██╗██║ ██║██╔══██╗██║ ██╔════╝██╔══██╗");
|
||||
Console.WriteLine(" ██║ ██║███████║ ██║ ███████║ ██║ ██║ ██║██║ ██║██████╔╝██║ █████╗ ██████╔╝");
|
||||
Console.WriteLine(" ██║ ██║██╔══██║ ██║ ██╔══██║ ██║ ██║ ██║██║ ██║██╔═══╝ ██║ ██╔══╝ ██╔══██╗");
|
||||
Console.WriteLine(" ██████╔╝██║ ██║ ██║ ██║ ██║ ╚██████╗╚██████╔╝╚██████╔╝██║ ███████╗███████╗██║ ██║");
|
||||
Console.WriteLine(" ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine();
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.WriteLine(" MachineGuardSetup — Configurazione protezione machine-binding per Data Coupler");
|
||||
Console.WriteLine(" Versione: 1.0 | Tecnologia: DPAPI (DataProtectionScope.LocalMachine)");
|
||||
Console.ResetColor();
|
||||
Console.WriteLine(new string('─', 70));
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
static void PrintError(string message)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($" ✗ ERRORE: {message}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
static void PrintWarning(string message)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($" ⚠ {message}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
static void PrintFileInfo(string path)
|
||||
{
|
||||
var fi = new FileInfo(path);
|
||||
Console.WriteLine(" Dettagli file:");
|
||||
Console.WriteLine($" Percorso : {fi.FullName}");
|
||||
Console.WriteLine($" Dimensione: {fi.Length} byte (cifrati con DPAPI)");
|
||||
Console.WriteLine($" Creato : {fi.CreationTime:dd/MM/yyyy HH:mm:ss}");
|
||||
}
|
||||
|
||||
static bool IsRunningAsAdministrator()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return false;
|
||||
using var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
|
||||
var principal = new System.Security.Principal.WindowsPrincipal(identity);
|
||||
return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
Reference in New Issue
Block a user