Implementing Encrypted Connection Strings with Telerik Reporting
Environment
| Product | Progress® Telerik® Reporting |
Description
The project's connection strings are stored encrypted in the appsettings.json
configuration file and when the SqlDataSource component attempts to connect using a shared connection resolved from the configuration, it fails due to the connection string being encrypted.
In this article, we will demonstrate how to use encrypted connection strings with SqlDataSource (including those that are used within SharedDataSource components) in both the HTML5 Report Viewers and the Web Report Designer.
Solution
Decrypting Connection Strings for SqlDataSource Components - HTML5 Report Viewers
To assign a decrypted connection string to the ConnectionString property of a SqlDataSource component, it will be necessary to implement a custom IReportSourceResolver in whose's Resolve method, we can unpackage or deserialize our TRDP/TRDX report. This will create a Telerik.Reporting.Report instance upon which we can invoke the GetDataSources() method to get references of all data source components in the report and filter them by the SqlDataSource
type:
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
using System.Linq;
using Telerik.Reporting;
using Telerik.Reporting.Services;
namespace CSharp.Net8.Html5IntegrationDemo
{
public class CustomReportSourceResolver(IConfiguration configuration) : IReportSourceResolver
{
private IConfiguration _configuration = configuration;
public Telerik.Reporting.ReportSource Resolve(string reportId, OperationOrigin operationOrigin, IDictionary<string, object> currentParameterValues)
{
var reportPacker = new ReportPackager();
Report report = null;
using (var sourceStream = System.IO.File.OpenRead(reportId))
{
report = (Report)reportPacker.UnpackageDocument(sourceStream);
}
var sqlDataSources = report.GetDataSources().OfType<SqlDataSource>();
foreach (var sqlDataSource in sqlDataSources)
{
string encryptedConnectionString = _configuration.GetSection($"ConnectionStrings:{sqlDataSource.ConnectionString}").Value; // Get the encrypted connection string from the config
sqlDataSource.ConnectionString = DecryptClass.DecryptConnectionString(encryptedConnectionString); // Decrypt the connection string and assign it to the SqlDataSource component
}
return new InstanceReportSource() { ReportDocument = report };
}
}
}
To use the custom IReportSourceResolver, it must be registered on the ReportSourceResolver property of the ReportServiceConfiguration class in Program.cs
/Startup.cs
:
// Configure dependencies for ReportsController.
builder.Services.TryAddSingleton<IReportServiceConfiguration>(sp =>
new ReportServiceConfiguration
{
HostAppId = "ReportingNet8",
Storage = new FileStorage(),
ReportSourceResolver = new CustomReportSourceResolver(sp.GetService<IConfiguration>())
});
Decrypting Connection Strings for SqlDataSource Components in SubReports/Navigate To Report Action - HTML5 Report Viewers
To use the decrypted connection strings in SqlDataSource components of reports that are used as SubReports or those who are loaded with the Navigate To Report action, it is necessary to also implement the IReportDocumentResolver interface since it handles those cases. We can use the approach from the previous section:
using System.IO;
using System;
using Telerik.Reporting;
using Telerik.Reporting.Services;
using System.Linq;
using Microsoft.Extensions.Configuration;
namespace CSharp.Net8.Html5IntegrationDemo
{
public class CustomDocumentResolver(IConfiguration configuration) : IReportDocumentResolver
{
private IConfiguration _configuration = configuration;
readonly string BaseDir = "C:\\Reports";
public IReportDocument Resolve(ReportSource reportSource)
{
// The main report is wrapped in an InstanceReportSource by CustomReportSourceResolver
if (reportSource is InstanceReportSource)
{
return (reportSource as InstanceReportSource).ReportDocument;
}
// the subreport is resolved in the context of the main report SubReport
else if (reportSource is UriReportSource)
{
var reportPackager = new ReportPackager();
var uri = (reportSource as UriReportSource).Uri.Replace(AppDomain.CurrentDomain.BaseDirectory, string.Empty);
var reportPacker = new ReportPackager();
Report report = null;
using (var sourceStream = System.IO.File.OpenRead(Path.Combine(BaseDir, uri)))
{
report = (Report)reportPacker.UnpackageDocument(sourceStream);
}
var sqlDataSources = report.GetDataSources().OfType<SqlDataSource>();
foreach (var sqlDataSource in sqlDataSources)
{
string encryptedConnectionString = _configuration.GetSection($"ConnectionStrings:{sqlDataSource.ConnectionString}").Value; // Get the encrypted connection string from the config
sqlDataSource.ConnectionString = DecryptClass.DecryptConnectionString(encryptedConnectionString); // Decrypt the connection string and assign it to the SqlDataSource component
}
return report;
}
return null;
}
}
}
To use the custom IReportDocumentResolver, it must be registered on the ReportDocumentResolver property of the ReportServiceConfiguration class in Program.cs
/Startup.cs
:
// Configure dependencies for ReportsController.
builder.Services.TryAddSingleton<IReportServiceConfiguration>(sp =>
new ReportServiceConfiguration
{
HostAppId = "ReportingNet8",
Storage = new FileStorage(),
ReportSourceResolver = new CustomReportSourceResolver(sp.GetService<IConfiguration>()),
ReportDocumentResolver = new CustomReportDocumentResolver(sp.GetService<IConfiguration>())
});
Decrypting Connection Strings for SqlDataSource Components - Web Report Designer
When using the Web Report Designer, if connection strings stored in the configuration file (e.g. appsettings.json
) are encrypted, it will be necessary to use a custom implementation of the ISettingsStorage interface to return decrypted connections.
In the ISettingsStorage.GetConnections() method, we must return a list of the connections with decrypted connection strings. This list will be used by the Web Report Designer on the client to send the connections to the server, and retrieve data.
The ISettingsStorage.AddConnection(ConnectionInfo) method will be executed when an end-user adds a new connection through the Web Report Designer client. The connection string here will be as is and we will need to save it decrypted in the configuration.
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Telerik.Reporting.Data.Schema;
using Telerik.WebReportDesigner.Services;
namespace CSharp.Net8.Html5IntegrationDemo
{
public class CustomSettingsStorage(IConfiguration configuration, string settingsDirectory) : ISettingsStorage
{
readonly FileSettingsManager settingsManager = new FileSettingsManager(settingsDirectory);
readonly IConfiguration _configuration = configuration;
public IEnumerable<ConnectionInfo> GetConnections()
{
var configConnections = _configuration.GetSection("ConnectionStrings").GetChildren();
var settingsConnections = this.settingsManager.GetConnections().ToList();
foreach (var settingConnection in settingsConnections)
{
// Decrypt the saved connections in the WebReportDesigner.json file
settingConnection.ConnectionString = DecryptClass.DecryptConnectionString(settingConnection.ConnectionString)
}
foreach (var connection in configConnections)
{
var connectionInfo = new ConnectionInfo
{
Name = connection.Key,
// Decrypt the connection saved in the .json configuration file loaded by the project itself
ConnectionString = DecryptClass.DecryptConnectionString(connection.GetSection("connectionString").Value),
Provider = connection.GetSection("providerName").Value
};
settingsConnections.Add(connectionInfo);
}
return settingsConnections;
}
public void AddConnection(ConnectionInfo connectionInfo)
{
// Encrypt the connection before saving it
connectionInfo.ConnectionString = EncryptClass.EncryptConnectionString(connectionInfo.ConnectionString);
this.settingsManager.AddConnection(connectionInfo);
}
}
class WebReportDesignerSettings
{
public List<ConnectionInfo> ConnectionStrings { get; set; }
public WebReportDesignerSettings()
{
this.ConnectionStrings = new List<ConnectionInfo>();
}
}
class FileSettingsManager
{
static object padlock = new object();
readonly string settingsDir;
readonly string settingsFilePath;
WebReportDesignerSettings settings;
public FileSettingsManager(string settingsDir)
{
const string SettingsFileName = "WebReportDesignerSettings.json";
this.settingsDir = settingsDir;
this.settingsFilePath = Path.Combine(settingsDir, SettingsFileName);
this.InitSettings();
}
void InitSettings()
{
lock (padlock)
{
if (File.Exists(this.settingsFilePath))
{
var text = File.ReadAllText(this.settingsFilePath);
this.settings = JsonConvert.DeserializeObject<WebReportDesignerSettings>(text);
}
else
{
Directory.CreateDirectory(this.settingsDir);
using (File.Create(this.settingsFilePath))
{
}
}
}
if (this.settings == null)
{
this.settings = new WebReportDesignerSettings();
}
}
public void AddConnection(ConnectionInfo connection)
{
lock (padlock)
{
this.settings.ConnectionStrings.Add(connection);
File.WriteAllText(this.settingsFilePath, JsonConvert.SerializeObject(this.settings));
}
}
public IEnumerable<ConnectionInfo> GetConnections()
{
return this.settings.ConnectionStrings;
}
}
}
Afterward, set the custom implementation in the ReportDesignerServiceConfiguration.
builder.Services.TryAddSingleton<IReportDesignerServiceConfiguration>(sp => new ReportDesignerServiceConfiguration
{
DefinitionStorage = new FileDefinitionStorage(reportsPath, new[] { "Resources", "Shared Data Sources" }),
ResourceStorage = new ResourceStorage(Path.Combine(reportsPath, "Resources")),
SharedDataSourceStorage = new FileSharedDataSourceStorage(Path.Combine(reportsPath, "Shared Data Sources")),
SettingsStorage = new CustomSettingsStorage(sp.GetService<IConfiguration>(), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Telerik Reporting"))
});
Decrypting Connection Strings for SharedDataSource Components
For reports utilizing the SharedDataSource components, the solution is a custom implementation of the ISharedDataSourceResolver interface:
public class CustomSharedDataSourceResolver : ISharedDataSourceResolver
{
readonly string BaseDir = "C:\\Shared Data Sources Directory";
public DataSource Resolve(string sharedDataSourceUri)
{
var path = Path.Combine(BaseDir, sharedDataSourceUri);
using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
var sqlDataSource = (Telerik.Reporting.SqlDataSource)new Telerik.Reporting.XmlSerialization.ReportXmlSerializer()
.Deserialize(fileStream);
sqlDataSource.ConnectionString = DecryptClass.DecryptConnectionString(sqlDataSource.ConnectionString);
return sqlDataSource;
}
}
Register the custom ISharedDataSourceResolver in the project's configuration file (e.g., appsettings.json
) to ensure it is utilized when rendering the reports.
"telerikReporting": {
"processing": {
"sharedDataSourceResolver": {
"provider": "custom",
"parameters": [
{
"name": "typename",
"value": "CSharp.Net8.Html5IntegrationDemo.CustomSharedDataSourceResolver, CSharp.Net8.Html5IntegrationDemo"
}
]
}
}
}
For more detailed instructions on how to register the custom resolver, refer to the Configuring the processing Element article.
Notes
- Ensure the encrypted connection string is correctly decrypted before assigning it to the data source.
- The custom implementations for
ISettingsStorage
andISharedDataSourceResolver
must be adjusted based on the specific project's structure and requirements.