Skip to content

Commit

Permalink
implement password manager support for connection strings (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
MaceWindu authored Mar 9, 2024
1 parent 96c8a99 commit 4ae27cd
Show file tree
Hide file tree
Showing 15 changed files with 80 additions and 21 deletions.
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 6 additions & 4 deletions Source/Configuration/AppConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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());
Expand All @@ -82,11 +82,13 @@ private sealed class JsonConfig
public IDictionary<string, string>? ConnectionStrings { get; set; }
}

/// <param name="name">Connection name.</param>
/// <param name="connectionString">Must be connection string without password manager tokens.</param>
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; }
}
Expand Down
12 changes: 12 additions & 0 deletions Source/Configuration/ConnectionSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,18 @@ public string? ProviderPath
[JsonIgnore]
public string? ConnectionString { get; set; }

/// <summary>
/// Returns <see cref="ConnectionString"/> property value with resolved LINQPad password manager tokens.
/// </summary>
/// <returns></returns>
public string? GetFullConnectionString () => PasswordManager.ResolvePasswordManagerFields(ConnectionString);

/// <summary>
/// Returns <see cref="SecondaryConnectionString"/> property value with resolved LINQPad password manager tokens.
/// </summary>
/// <returns></returns>
public string? GetFullSecondaryConnectionString() => PasswordManager.ResolvePasswordManagerFields(SecondaryConnectionString);

/// <summary>
/// Stored in <see cref="IDatabaseInfo.Provider"/>.
/// </summary>
Expand Down
8 changes: 5 additions & 3 deletions Source/DatabaseProviders/AccessProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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();
Expand All @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions Source/DatabaseProviders/DatabaseProviderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public virtual void Unload (
public abstract DateTime? GetLastSchemaUpdate(ConnectionSettings settings);
public abstract DbProviderFactory GetProviderFactory (string providerName );

/// <param name="providerName">Provider name.</param>
/// <param name="connectionString">Connection string must be resolved against password manager already.</param>
/// <returns></returns>
public virtual IDataProvider GetDataProvider(string providerName, string connectionString)
{
return DataConnection.GetDataProvider(providerName, connectionString)
Expand Down
13 changes: 11 additions & 2 deletions Source/DatabaseProviders/DatabaseProviders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <param name="providerName">Provider name.</param>
/// <param name="connectionString">Connection string must be already resolved against password manager.</param>
/// <param name="providerPath">Optional path to provider assembly.</param>
public static IDataProvider GetDataProvider(string? providerName, string? connectionString, string? providerPath)
{
if (string.IsNullOrWhiteSpace(providerName))
Expand Down
4 changes: 4 additions & 0 deletions Source/DatabaseProviders/IDatabaseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ internal interface IDatabaseProvider
/// <summary>
/// Tries to infer provider by database connection string.
/// </summary>
/// <param name="connectionString">Connection string could contain password manager tokens.</param>
/// <returns></returns>
ProviderInfo? GetProviderByConnectionString(string connectionString);

/// <summary>
Expand Down Expand Up @@ -95,6 +97,8 @@ internal interface IDatabaseProvider
/// <summary>
/// Returns linq2db data provider.
/// </summary>
/// <param name="providerName">Provider name.</param>
/// <param name="connectionString">Connection string must be already resolved against password manager.</param>
IDataProvider GetDataProvider(string providerName, string connectionString);

#if NETFRAMEWORK
Expand Down
1 change: 0 additions & 1 deletion Source/DatabaseProviders/SqlServerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions Source/Drivers/DriverHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
2 changes: 1 addition & 1 deletion Source/Drivers/DynamicLinqToDBDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ public override List<ExplorerItem> GetSchemaAndBuildAssembly(IConnectionInfo cxI
[
settings.Connection.Provider,
settings.Connection.ProviderPath,
settings.Connection.ConnectionString
settings.Connection.GetFullConnectionString()
];
}
catch (Exception ex)
Expand Down
7 changes: 4 additions & 3 deletions Source/Drivers/DynamicSchemaGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public static (List<ExplorerItem> 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;

Expand All @@ -150,8 +150,9 @@ public static (List<ExplorerItem> 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;
Expand Down
20 changes: 20 additions & 0 deletions Source/Drivers/PasswordManager.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
5 changes: 4 additions & 1 deletion Source/LINQPadDataConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ public class LINQPadDataConnection : DataConnection
/// <summary>
/// Constructor for inherited context.
/// </summary>
/// <param name="providerName">Provider name.</param>
/// <param name="providerPath">Optional provider assembly path.</param>
/// <param name="connectionString">Connection string must have password manager tokens replaced already.</param>
protected LINQPadDataConnection(string? providerName, string? providerPath, string? connectionString)
: base(
DatabaseProviders.GetDataProvider(providerName, connectionString, providerPath),
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion Source/UI/Settings/DynamicConnectionTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<TextBox Padding="5" Text="{Binding ProviderPath}" />
</DockPanel>

<StackPanel Margin="0 5 0 0" ToolTip="Specify database connection string">
<StackPanel Margin="0 5 0 0" ToolTip="Specify database connection string. Connection string could reference values from LINPad Password Manager (see File -> Password Manager) using following format: {pm:name}, e.g. &quot;Server=.;Database=DB;User Id=user;Password={pm:my-password}&quot;">
<Label>Connection string</Label>
<TextBox Padding="5" Text="{Binding ConnectionString}" TextWrapping="Wrap" Height="60" VerticalScrollBarVisibility="Auto" />
</StackPanel>
Expand Down
1 change: 1 addition & 0 deletions release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 4ae27cd

Please sign in to comment.