diff --git a/.editorconfig b/.editorconfig index 69e9409..8c30844 100644 --- a/.editorconfig +++ b/.editorconfig @@ -249,6 +249,8 @@ dotnet_diagnostic.MA0006.severity = none dotnet_diagnostic.MA0007.severity = none # MA0009: Add regex evaluation timeout dotnet_diagnostic.MA0009.severity = none +# MA0023: Add RegexOptions.ExplicitCapture +dotnet_diagnostic.MA0023.severity = none # MA0026: Fix TODO comment dotnet_diagnostic.MA0026.severity = none # MA0038: Make method static (deprecated, use CA1822 instead) diff --git a/Source/Configuration/AppConfig.cs b/Source/Configuration/AppConfig.cs index 10e5793..0aa5df6 100644 --- a/Source/Configuration/AppConfig.cs +++ b/Source/Configuration/AppConfig.cs @@ -26,7 +26,7 @@ public static ILinqToDBSettings LoadJson(string configPath) if (cn.Key.EndsWith("_ProviderName", StringComparison.InvariantCultureIgnoreCase)) continue; - connections.Add(cn.Key, new ConnectionStringSettings(cn.Key, cn.Value)); + connections.Add(cn.Key, new ConnectionStringSettings(cn.Key, PasswordManager.ResolvePasswordManagerFields(cn.Value))); } foreach (var cn in config.ConnectionStrings) @@ -64,7 +64,7 @@ public static ILinqToDBSettings LoadAppConfig(string configPath) var providerName = node.Attributes["providerName" ]?.Value; if (name != null && connectionString != null) - settings.Add(new ConnectionStringSettings(name, connectionString) { ProviderName = providerName }); + settings.Add(new ConnectionStringSettings(name, PasswordManager.ResolvePasswordManagerFields(connectionString)) { ProviderName = providerName }); } return new AppConfig(settings.ToArray()); @@ -82,11 +82,13 @@ private sealed class JsonConfig public IDictionary? ConnectionStrings { get; set; } } + /// Connection name. + /// Must be connection string without password manager tokens. private sealed class ConnectionStringSettings(string name, string connectionString) : IConnectionStringSettings { string IConnectionStringSettings.ConnectionString => connectionString; - string IConnectionStringSettings.Name => name; - bool IConnectionStringSettings.IsGlobal => false; + string IConnectionStringSettings.Name => name; + bool IConnectionStringSettings.IsGlobal => false; public string? ProviderName { get; set; } } diff --git a/Source/Configuration/ConnectionSettings.cs b/Source/Configuration/ConnectionSettings.cs index e814af1..87884ef 100644 --- a/Source/Configuration/ConnectionSettings.cs +++ b/Source/Configuration/ConnectionSettings.cs @@ -387,6 +387,18 @@ public string? ProviderPath [JsonIgnore] public string? ConnectionString { get; set; } + /// + /// Returns property value with resolved LINQPad password manager tokens. + /// + /// + public string? GetFullConnectionString () => PasswordManager.ResolvePasswordManagerFields(ConnectionString); + + /// + /// Returns property value with resolved LINQPad password manager tokens. + /// + /// + public string? GetFullSecondaryConnectionString() => PasswordManager.ResolvePasswordManagerFields(SecondaryConnectionString); + /// /// Stored in . /// diff --git a/Source/DatabaseProviders/AccessProvider.cs b/Source/DatabaseProviders/AccessProvider.cs index 156846b..e9490ad 100644 --- a/Source/DatabaseProviders/AccessProvider.cs +++ b/Source/DatabaseProviders/AccessProvider.cs @@ -40,9 +40,9 @@ public override void ClearAllPools(string providerName) public override DateTime? GetLastSchemaUpdate(ConnectionSettings settings) { var connectionString = settings.Connection.Provider == ProviderName.Access - ? settings.Connection.ConnectionString + ? settings.Connection.GetFullConnectionString() : settings.Connection.SecondaryProvider == ProviderName.Access - ? settings.Connection.SecondaryConnectionString + ? settings.Connection.GetFullSecondaryConnectionString() : null; if (connectionString == null || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -53,7 +53,7 @@ public override void ClearAllPools(string providerName) if (settings.Connection.Provider == ProviderName.Access) provider = DatabaseProviders.GetDataProvider(settings); else - provider = DatabaseProviders.GetDataProvider(settings.Connection.SecondaryProvider, settings.Connection.SecondaryConnectionString, null); + provider = DatabaseProviders.GetDataProvider(settings.Connection.SecondaryProvider, connectionString, null); using var cn = (OleDbConnection)provider.CreateConnection(connectionString); cn.Open(); @@ -65,6 +65,8 @@ public override void ClearAllPools(string providerName) public override ProviderInfo? GetProviderByConnectionString(string connectionString) { + connectionString = PasswordManager.ResolvePasswordManagerFields(connectionString); + var isOleDb = connectionString.IndexOf("Microsoft.Jet.OLEDB", StringComparison.OrdinalIgnoreCase) != -1 || connectionString.IndexOf("Microsoft.ACE.OLEDB", StringComparison.OrdinalIgnoreCase) != -1; diff --git a/Source/DatabaseProviders/DatabaseProviderBase.cs b/Source/DatabaseProviders/DatabaseProviderBase.cs index e0f1f64..ef5fd42 100644 --- a/Source/DatabaseProviders/DatabaseProviderBase.cs +++ b/Source/DatabaseProviders/DatabaseProviderBase.cs @@ -30,6 +30,9 @@ public virtual void Unload ( public abstract DateTime? GetLastSchemaUpdate(ConnectionSettings settings); public abstract DbProviderFactory GetProviderFactory (string providerName ); + /// Provider name. + /// Connection string must be resolved against password manager already. + /// public virtual IDataProvider GetDataProvider(string providerName, string connectionString) { return DataConnection.GetDataProvider(providerName, connectionString) diff --git a/Source/DatabaseProviders/DatabaseProviders.cs b/Source/DatabaseProviders/DatabaseProviders.cs index 918f4e5..c70c1e8 100644 --- a/Source/DatabaseProviders/DatabaseProviders.cs +++ b/Source/DatabaseProviders/DatabaseProviders.cs @@ -64,12 +64,21 @@ public static void Init() // trigger .cctors } - public static DbConnection CreateConnection (ConnectionSettings settings) => GetDataProvider(settings).CreateConnection(settings.Connection.ConnectionString!); + public static DbConnection CreateConnection(ConnectionSettings settings) + { + return GetDataProvider(settings).CreateConnection(settings.Connection.GetFullConnectionString()!); + } public static DbProviderFactory GetProviderFactory(ConnectionSettings settings) => GetProviderByName(settings.Connection.Provider!).GetProviderFactory(settings.Connection.Provider!); - public static IDataProvider GetDataProvider(ConnectionSettings settings) => GetDataProvider(settings.Connection.Provider, settings.Connection.ConnectionString, settings.Connection.ProviderPath); + public static IDataProvider GetDataProvider(ConnectionSettings settings) + { + return GetDataProvider(settings.Connection.Provider, settings.Connection.GetFullConnectionString(), settings.Connection.ProviderPath); + } + /// Provider name. + /// Connection string must be already resolved against password manager. + /// Optional path to provider assembly. public static IDataProvider GetDataProvider(string? providerName, string? connectionString, string? providerPath) { if (string.IsNullOrWhiteSpace(providerName)) diff --git a/Source/DatabaseProviders/IDatabaseProvider.cs b/Source/DatabaseProviders/IDatabaseProvider.cs index d41d786..d48f969 100644 --- a/Source/DatabaseProviders/IDatabaseProvider.cs +++ b/Source/DatabaseProviders/IDatabaseProvider.cs @@ -53,6 +53,8 @@ internal interface IDatabaseProvider /// /// Tries to infer provider by database connection string. /// + /// Connection string could contain password manager tokens. + /// ProviderInfo? GetProviderByConnectionString(string connectionString); /// @@ -95,6 +97,8 @@ internal interface IDatabaseProvider /// /// Returns linq2db data provider. /// + /// Provider name. + /// Connection string must be already resolved against password manager. IDataProvider GetDataProvider(string providerName, string connectionString); #if NETFRAMEWORK diff --git a/Source/DatabaseProviders/SqlServerProvider.cs b/Source/DatabaseProviders/SqlServerProvider.cs index 77fd676..0007c9b 100644 --- a/Source/DatabaseProviders/SqlServerProvider.cs +++ b/Source/DatabaseProviders/SqlServerProvider.cs @@ -92,7 +92,6 @@ public override DbProviderFactory GetProviderFactory(string providerName) return SqlClientFactory.Instance; } - public override IDataProvider GetDataProvider(string providerName, string connectionString) { // provider detector fails to detect Microsoft.Data.SqlClient diff --git a/Source/Drivers/DriverHelper.cs b/Source/Drivers/DriverHelper.cs index 1433e8f..e504c77 100644 --- a/Source/Drivers/DriverHelper.cs +++ b/Source/Drivers/DriverHelper.cs @@ -266,17 +266,18 @@ public static bool ShowConnectionDialog(IConnectionInfo cxInfo, ConnectionDialog throw new LinqToDBLinqPadException($"Cannot access provider assembly at {model.DynamicConnection.ProviderPath}"); } - var provider = DatabaseProviders.GetDataProvider(model.DynamicConnection.Provider.Name, model.DynamicConnection.ConnectionString, model.DynamicConnection.ProviderPath); - - using (var con = provider.CreateConnection(model.DynamicConnection.ConnectionString)) + var connectionString = PasswordManager.ResolvePasswordManagerFields(model.DynamicConnection.ConnectionString); + var provider = DatabaseProviders.GetDataProvider(model.DynamicConnection.Provider.Name, connectionString, model.DynamicConnection.ProviderPath); + using (var con = provider.CreateConnection(connectionString)) con.Open(); if (model.DynamicConnection.Database.SupportsSecondaryConnection && model.DynamicConnection.SecondaryProvider != null && model.DynamicConnection.SecondaryConnectionString != null) { - var secondaryProvider = DatabaseProviders.GetDataProvider(model.DynamicConnection.SecondaryProvider.Name, model.DynamicConnection.SecondaryConnectionString, null); - using var con = secondaryProvider.CreateConnection(model.DynamicConnection.SecondaryConnectionString); + var secondaryConnectionString = PasswordManager.ResolvePasswordManagerFields(model.DynamicConnection.SecondaryConnectionString); + var secondaryProvider = DatabaseProviders.GetDataProvider(model.DynamicConnection.SecondaryProvider.Name, secondaryConnectionString, null); + using var con = secondaryProvider.CreateConnection(secondaryConnectionString); con.Open(); } diff --git a/Source/Drivers/DynamicLinqToDBDriver.cs b/Source/Drivers/DynamicLinqToDBDriver.cs index de51184..6b42e54 100644 --- a/Source/Drivers/DynamicLinqToDBDriver.cs +++ b/Source/Drivers/DynamicLinqToDBDriver.cs @@ -197,7 +197,7 @@ public override List GetSchemaAndBuildAssembly(IConnectionInfo cxI [ settings.Connection.Provider, settings.Connection.ProviderPath, - settings.Connection.ConnectionString + settings.Connection.GetFullConnectionString() ]; } catch (Exception ex) diff --git a/Source/Drivers/DynamicSchemaGenerator.cs b/Source/Drivers/DynamicSchemaGenerator.cs index bf267b1..61342d8 100644 --- a/Source/Drivers/DynamicSchemaGenerator.cs +++ b/Source/Drivers/DynamicSchemaGenerator.cs @@ -132,7 +132,7 @@ public static (List items, string sourceCode, string providerAssem var provider = DatabaseProviders.GetDataProvider(settings); - using var db = new DataConnection(provider, settings.Connection.ConnectionString!); + using var db = new DataConnection(provider, settings.Connection.GetFullConnectionString()!); if (settings.Connection.CommandTimeout != null) db.CommandTimeout = settings.Connection.CommandTimeout.Value; @@ -150,8 +150,9 @@ public static (List items, string sourceCode, string providerAssem DatabaseModel dataModel; if (settings.Connection.Database == ProviderName.Access && settings.Connection.SecondaryConnectionString != null) { - var secondaryProvider = DatabaseProviders.GetDataProvider(settings.Connection.SecondaryProvider, settings.Connection.SecondaryConnectionString, null); - using var sdc = new DataConnection(secondaryProvider, settings.Connection.SecondaryConnectionString); + var secondaryConnectionString = settings.Connection.GetFullSecondaryConnectionString()!; + var secondaryProvider = DatabaseProviders.GetDataProvider(settings.Connection.SecondaryProvider, secondaryConnectionString, null); + using var sdc = new DataConnection(secondaryProvider, secondaryConnectionString); if (settings.Connection.CommandTimeout != null) sdc.CommandTimeout = settings.Connection.CommandTimeout.Value; diff --git a/Source/Drivers/PasswordManager.cs b/Source/Drivers/PasswordManager.cs new file mode 100644 index 0000000..1de8bd0 --- /dev/null +++ b/Source/Drivers/PasswordManager.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using LINQPad; + +namespace LinqToDB.LINQPad +{ + internal static partial class PasswordManager + { + private static readonly Regex _tokenReplacer = new(@"\{pm:([^\}]+)\}", RegexOptions.Compiled); + + [return: NotNullIfNotNull(nameof(value))] + public static string? ResolvePasswordManagerFields(string? value) + { + if (value == null) + return null; + + return _tokenReplacer.Replace(value, m => Util.GetPassword(m.Groups[1].Value)); + } + } +} diff --git a/Source/LINQPadDataConnection.cs b/Source/LINQPadDataConnection.cs index e113327..967ec19 100644 --- a/Source/LINQPadDataConnection.cs +++ b/Source/LINQPadDataConnection.cs @@ -10,6 +10,9 @@ public class LINQPadDataConnection : DataConnection /// /// Constructor for inherited context. /// + /// Provider name. + /// Optional provider assembly path. + /// Connection string must have password manager tokens replaced already. protected LINQPadDataConnection(string? providerName, string? providerPath, string? connectionString) : base( DatabaseProviders.GetDataProvider(providerName, connectionString, providerPath), @@ -24,7 +27,7 @@ internal LINQPadDataConnection(ConnectionSettings settings) : this( settings.Connection.Provider, settings.Connection.ProviderPath, - settings.Connection.ConnectionString) + settings.Connection.GetFullConnectionString()) { if (settings.Connection.CommandTimeout != null) CommandTimeout = settings.Connection.CommandTimeout.Value; diff --git a/Source/UI/Settings/DynamicConnectionTab.xaml b/Source/UI/Settings/DynamicConnectionTab.xaml index fceb870..5bc68cc 100644 --- a/Source/UI/Settings/DynamicConnectionTab.xaml +++ b/Source/UI/Settings/DynamicConnectionTab.xaml @@ -39,7 +39,7 @@ - + diff --git a/release-notes.md b/release-notes.md index c3f287c..5fb748b 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,6 +2,7 @@ Issues fixed: +- [#88](https://github.com/linq2db/linq2db.LINQPad/issues/88): add support for LINQPad Password Manager in connection strings. To reference value from password manager use `{pm:password-name}` token syntax - [#97](https://github.com/linq2db/linq2db.LINQPad/issues/97), [#103](https://github.com/linq2db/linq2db.LINQPad/issues/103): update dependencies to get rid of vulnerable transient dependencies - [#101](https://github.com/linq2db/linq2db.LINQPad/issues/101): fix snippets generation from object names matching C# keywords - [#102](https://github.com/linq2db/linq2db.LINQPad/pull/102): support custom `IDataContext`-based contexts; don't try to load missing connection configuration files. Thanks to [@cal-tlabwest](https://github.com/cal-tlabwest) for fix