Skip to content

Commit 69ebb3b

Browse files
authored
Add uncached password provider support (#5399)
Closes #5186
1 parent b4cd297 commit 69ebb3b

File tree

8 files changed

+173
-21
lines changed

8 files changed

+173
-21
lines changed

src/Npgsql/NpgsqlDataSource.cs

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System.Data.Common;
44
using System.Diagnostics;
55
using System.Diagnostics.CodeAnalysis;
6-
using System.Linq;
76
using System.Net.Security;
87
using System.Security.Cryptography.X509Certificates;
98
using System.Threading;
@@ -44,6 +43,8 @@ public abstract class NpgsqlDataSource : DbDataSource
4443
internal RemoteCertificateValidationCallback? UserCertificateValidationCallback { get; }
4544
internal Action<X509CertificateCollection>? ClientCertificatesCallback { get; }
4645

46+
readonly Func<NpgsqlConnectionStringBuilder, string>? _passwordProvider;
47+
readonly Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? _passwordProviderAsync;
4748
readonly Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? _periodicPasswordProvider;
4849
readonly TimeSpan _periodicPasswordSuccessRefreshInterval, _periodicPasswordFailureRefreshInterval;
4950

@@ -52,7 +53,7 @@ public abstract class NpgsqlDataSource : DbDataSource
5253
internal Action<NpgsqlConnection>? ConnectionInitializer { get; }
5354
internal Func<NpgsqlConnection, Task>? ConnectionInitializerAsync { get; }
5455

55-
readonly Timer? _passwordProviderTimer;
56+
readonly Timer? _periodicPasswordProviderTimer;
5657
readonly CancellationTokenSource? _timerPasswordProviderCancellationTokenSource;
5758
readonly Task _passwordRefreshTask = null!;
5859
string? _password;
@@ -101,6 +102,8 @@ internal NpgsqlDataSource(
101102
IntegratedSecurityHandler,
102103
UserCertificateValidationCallback,
103104
ClientCertificatesCallback,
105+
_passwordProvider,
106+
_passwordProviderAsync,
104107
_periodicPasswordProvider,
105108
_periodicPasswordSuccessRefreshInterval,
106109
_periodicPasswordFailureRefreshInterval,
@@ -112,6 +115,8 @@ internal NpgsqlDataSource(
112115
= dataSourceConfig;
113116
_connectionLogger = LoggingConfiguration.ConnectionLogger;
114117

118+
Debug.Assert(_passwordProvider is null || _passwordProviderAsync is not null);
119+
115120
// TODO probably want this on the options so it can devirt unconditionally.
116121
_resolver = new TypeInfoResolverChain(resolverChain);
117122
_password = settings.Password;
@@ -123,7 +128,7 @@ internal NpgsqlDataSource(
123128
_timerPasswordProviderCancellationTokenSource = new();
124129

125130
// Create the timer, but don't start it; the manual run below will will schedule the first refresh.
126-
_passwordProviderTimer = new Timer(state => _ = RefreshPassword(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
131+
_periodicPasswordProviderTimer = new Timer(state => _ = RefreshPassword(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
127132
// Trigger the first refresh attempt right now, outside the timer; this allows us to capture the Task so it can be observed
128133
// in GetPasswordAsync.
129134
_passwordRefreshTask = Task.Run(RefreshPassword);
@@ -293,28 +298,48 @@ public string Password
293298
{
294299
set
295300
{
296-
if (_periodicPasswordProvider is not null)
301+
if (_passwordProvider is not null || _periodicPasswordProvider is not null)
297302
throw new NotSupportedException(NpgsqlStrings.CannotSetBothPasswordProviderAndPassword);
298303

299304
_password = value;
300305
}
301306
}
302307

303-
internal async ValueTask<string?> GetPassword(bool async, CancellationToken cancellationToken = default)
308+
internal ValueTask<string?> GetPassword(bool async, CancellationToken cancellationToken = default)
304309
{
310+
if (_passwordProvider is not null)
311+
return GetPassword(async, cancellationToken);
312+
305313
// A periodic password provider is configured, but the first refresh hasn't completed yet (race condition).
306-
// Wait until it completes.
307314
if (_password is null && _periodicPasswordProvider is not null)
315+
return GetInitialPeriodicPassword(async);
316+
317+
return new(_password);
318+
319+
async ValueTask<string?> GetInitialPeriodicPassword(bool async)
308320
{
309321
if (async)
310322
await _passwordRefreshTask.ConfigureAwait(false);
311323
else
312324
_passwordRefreshTask.GetAwaiter().GetResult();
313-
314325
Debug.Assert(_password is not null);
326+
327+
return _password;
315328
}
316329

317-
return _password;
330+
async ValueTask<string?> GetPassword(bool async, CancellationToken cancellationToken)
331+
{
332+
try
333+
{
334+
return async ? await _passwordProviderAsync!(Settings, cancellationToken).ConfigureAwait(false) : _passwordProvider(Settings);
335+
}
336+
catch (Exception e)
337+
{
338+
_connectionLogger.LogError(e, "Password provider threw an exception");
339+
340+
throw new NpgsqlException("An exception was thrown from the password provider", e);
341+
}
342+
}
318343
}
319344

320345
async Task RefreshPassword()
@@ -323,13 +348,13 @@ async Task RefreshPassword()
323348
{
324349
_password = await _periodicPasswordProvider!(Settings, _timerPasswordProviderCancellationTokenSource!.Token).ConfigureAwait(false);
325350

326-
_passwordProviderTimer!.Change(_periodicPasswordSuccessRefreshInterval, Timeout.InfiniteTimeSpan);
351+
_periodicPasswordProviderTimer!.Change(_periodicPasswordSuccessRefreshInterval, Timeout.InfiniteTimeSpan);
327352
}
328353
catch (Exception e)
329354
{
330355
_connectionLogger.LogError(e, "Periodic password provider threw an exception");
331356

332-
_passwordProviderTimer!.Change(_periodicPasswordFailureRefreshInterval, Timeout.InfiniteTimeSpan);
357+
_periodicPasswordProviderTimer!.Change(_periodicPasswordFailureRefreshInterval, Timeout.InfiniteTimeSpan);
333358

334359
throw new NpgsqlException("An exception was thrown from the periodic password provider", e);
335360
}
@@ -448,7 +473,7 @@ protected virtual void DisposeBase()
448473
cancellationTokenSource.Dispose();
449474
}
450475

451-
_passwordProviderTimer?.Dispose();
476+
_periodicPasswordProviderTimer?.Dispose();
452477
_setupMappingsSemaphore.Dispose();
453478
MetricsReporter.Dispose(); // TODO: This is probably too early, dispose only when all connections have been closed?
454479

@@ -475,12 +500,12 @@ protected virtual async ValueTask DisposeAsyncBase()
475500
cancellationTokenSource.Dispose();
476501
}
477502

478-
if (_passwordProviderTimer is not null)
503+
if (_periodicPasswordProviderTimer is not null)
479504
{
480505
#if NET5_0_OR_GREATER
481-
await _passwordProviderTimer.DisposeAsync().ConfigureAwait(false);
506+
await _periodicPasswordProviderTimer.DisposeAsync().ConfigureAwait(false);
482507
#else
483-
_passwordProviderTimer.Dispose();
508+
_periodicPasswordProviderTimer.Dispose();
484509
#endif
485510
}
486511

src/Npgsql/NpgsqlDataSourceBuilder.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,30 @@ public NpgsqlDataSourceBuilder UsePeriodicPasswordProvider(
232232
return this;
233233
}
234234

235+
/// <summary>
236+
/// Configures a password provider, which is called by the data source when opening connections.
237+
/// </summary>
238+
/// <param name="passwordProvider">
239+
/// A callback that may be invoked during <see cref="NpgsqlConnection.Open()" /> which returns the password to be sent to PostgreSQL.
240+
/// </param>
241+
/// <param name="passwordProviderAsync">
242+
/// A callback that may be invoked during <see cref="NpgsqlConnection.OpenAsync(CancellationToken)" /> which returns the password to be sent to PostgreSQL.
243+
/// </param>
244+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
245+
/// <remarks>
246+
/// <para>
247+
/// The provided callback is invoked when opening connections. Therefore its important the callback internally depends on cached
248+
/// data or returns quickly otherwise. Any unnecessary delay will affect connection opening time.
249+
/// </para>
250+
/// </remarks>
251+
public NpgsqlDataSourceBuilder UsePasswordProvider(
252+
Func<NpgsqlConnectionStringBuilder, string>? passwordProvider,
253+
Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? passwordProviderAsync)
254+
{
255+
_internalBuilder.UsePasswordProvider(passwordProvider, passwordProviderAsync);
256+
return this;
257+
}
258+
235259
#endregion Authentication
236260

237261
#region Type mapping

src/Npgsql/NpgsqlDataSourceConfiguration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ sealed record NpgsqlDataSourceConfiguration(string? Name,
1414
IntegratedSecurityHandler userCertificateValidationCallback,
1515
RemoteCertificateValidationCallback? UserCertificateValidationCallback,
1616
Action<X509CertificateCollection>? ClientCertificatesCallback,
17+
Func<NpgsqlConnectionStringBuilder, string>? PasswordProvider,
18+
Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? PasswordProviderAsync,
1719
Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? PeriodicPasswordProvider,
1820
TimeSpan PeriodicPasswordSuccessRefreshInterval,
1921
TimeSpan PeriodicPasswordFailureRefreshInterval,

src/Npgsql/NpgsqlSlimDataSourceBuilder.cs

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,18 @@ public sealed class NpgsqlSlimDataSourceBuilder : INpgsqlTypeMapper
3333

3434
IntegratedSecurityHandler _integratedSecurityHandler = new();
3535

36+
Func<NpgsqlConnectionStringBuilder, string>? _passwordProvider;
37+
Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? _passwordProviderAsync;
38+
3639
Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? _periodicPasswordProvider;
3740
TimeSpan _periodicPasswordSuccessRefreshInterval, _periodicPasswordFailureRefreshInterval;
3841

3942
PgTypeInfoResolverChainBuilder _resolverChainBuilder = new(); // mutable struct, don't make readonly.
4043

4144
readonly UserTypeMapper _userTypeMapper;
4245

43-
Action<NpgsqlConnection>? _syncConnectionInitializer;
44-
Func<NpgsqlConnection, Task>? _asyncConnectionInitializer;
46+
Action<NpgsqlConnection>? _connectionInitializer;
47+
Func<NpgsqlConnection, Task>? _connectionInitializerAsync;
4548

4649
/// <summary>
4750
/// A connection string builder that can be used to configured the connection string on the builder.
@@ -239,6 +242,34 @@ public NpgsqlSlimDataSourceBuilder UsePeriodicPasswordProvider(
239242
return this;
240243
}
241244

245+
/// <summary>
246+
/// Configures a password provider, which is called by the data source when opening connections.
247+
/// </summary>
248+
/// <param name="passwordProvider">
249+
/// A callback that may be invoked during <see cref="NpgsqlConnection.Open()" /> which returns the password to be sent to PostgreSQL.
250+
/// </param>
251+
/// <param name="passwordProviderAsync">
252+
/// A callback that may be invoked during <see cref="NpgsqlConnection.OpenAsync(CancellationToken)" /> which returns the password to be sent to PostgreSQL.
253+
/// </param>
254+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
255+
/// <remarks>
256+
/// <para>
257+
/// The provided callback is invoked when opening connections. Therefore its important the callback internally depends on cached
258+
/// data or returns quickly otherwise. Any unnecessary delay will affect connection opening time.
259+
/// </para>
260+
/// </remarks>
261+
public NpgsqlSlimDataSourceBuilder UsePasswordProvider(
262+
Func<NpgsqlConnectionStringBuilder, string>? passwordProvider,
263+
Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? passwordProviderAsync)
264+
{
265+
if (passwordProvider is null != passwordProviderAsync is null)
266+
throw new ArgumentException(NpgsqlStrings.SyncAndAsyncPasswordProvidersRequired);
267+
268+
_passwordProvider = passwordProvider;
269+
_passwordProviderAsync = passwordProviderAsync;
270+
return this;
271+
}
272+
242273
#endregion Authentication
243274

244275
#region Type mapping
@@ -455,8 +486,8 @@ public NpgsqlSlimDataSourceBuilder UsePhysicalConnectionInitializer(
455486
if (connectionInitializer is null != connectionInitializerAsync is null)
456487
throw new ArgumentException(NpgsqlStrings.SyncAndAsyncConnectionInitializersRequired);
457488

458-
_syncConnectionInitializer = connectionInitializer;
459-
_asyncConnectionInitializer = connectionInitializerAsync;
489+
_connectionInitializer = connectionInitializer;
490+
_connectionInitializerAsync = connectionInitializerAsync;
460491

461492
return this;
462493
}
@@ -504,7 +535,12 @@ NpgsqlDataSourceConfiguration PrepareConfiguration()
504535
throw new InvalidOperationException(NpgsqlStrings.TransportSecurityDisabled);
505536
}
506537

507-
if (_periodicPasswordProvider is not null &&
538+
if (_passwordProvider is not null && _periodicPasswordProvider is not null)
539+
{
540+
throw new NotSupportedException(NpgsqlStrings.CannotSetMultiplePasswordProviderKinds);
541+
}
542+
543+
if ((_passwordProvider is not null || _periodicPasswordProvider is not null) &&
508544
(ConnectionStringBuilder.Password is not null || ConnectionStringBuilder.Passfile is not null))
509545
{
510546
throw new NotSupportedException(NpgsqlStrings.CannotSetBothPasswordProviderAndPassword);
@@ -519,14 +555,16 @@ _loggerFactory is null
519555
_integratedSecurityHandler,
520556
_userCertificateValidationCallback,
521557
_clientCertificatesCallback,
558+
_passwordProvider,
559+
_passwordProviderAsync,
522560
_periodicPasswordProvider,
523561
_periodicPasswordSuccessRefreshInterval,
524562
_periodicPasswordFailureRefreshInterval,
525563
_resolverChainBuilder.Build(ConfigureResolverChain),
526564
HackyEnumMappings(),
527565
DefaultNameTranslator,
528-
_syncConnectionInitializer,
529-
_asyncConnectionInitializer);
566+
_connectionInitializer,
567+
_connectionInitializerAsync);
530568

531569
List<HackyEnumTypeMapping> HackyEnumMappings()
532570
{

src/Npgsql/Properties/NpgsqlStrings.Designer.cs

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Npgsql/Properties/NpgsqlStrings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@
4242
<data name="CannotSetBothPasswordProviderAndPassword" xml:space="preserve">
4343
<value>When registering a password provider, a password or password file may not be set.</value>
4444
</data>
45+
<data name="CannotSetMultiplePasswordProviderKinds" xml:space="preserve">
46+
<value>Multiple kinds of password providers were found, only one kind may be configured per DbDataSource.</value>
47+
</data>
48+
<data name="SyncAndAsyncPasswordProvidersRequired" xml:space="preserve">
49+
<value>Both sync and async password providers must be provided.</value>
50+
</data>
4551
<data name="PasswordProviderMissing" xml:space="preserve">
4652
<value>The right type of password provider (sync or async) was not found.</value>
4753
</data>

src/Npgsql/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Npgsql.NpgsqlDataSourceBuilder.MapEnum(System.Type! clrType, string? pgName = nu
1515
Npgsql.NpgsqlDataSourceBuilder.Name.get -> string?
1616
Npgsql.NpgsqlDataSourceBuilder.Name.set -> void
1717
Npgsql.NpgsqlDataSourceBuilder.UnmapEnum(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
18+
Npgsql.NpgsqlDataSourceBuilder.UsePasswordProvider(System.Func<Npgsql.NpgsqlConnectionStringBuilder!, string!>? passwordProvider, System.Func<Npgsql.NpgsqlConnectionStringBuilder!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<string!>>? passwordProviderAsync) -> Npgsql.NpgsqlDataSourceBuilder!
1819
Npgsql.NpgsqlDataSourceBuilder.UseRootCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2? rootCertificate) -> Npgsql.NpgsqlDataSourceBuilder!
1920
Npgsql.NpgsqlDataSourceBuilder.UseRootCertificateCallback(System.Func<System.Security.Cryptography.X509Certificates.X509Certificate2!>? rootCertificateCallback) -> Npgsql.NpgsqlDataSourceBuilder!
2021
Npgsql.NpgsqlSlimDataSourceBuilder
@@ -50,6 +51,7 @@ Npgsql.NpgsqlSlimDataSourceBuilder.UseClientCertificate(System.Security.Cryptogr
5051
Npgsql.NpgsqlSlimDataSourceBuilder.UseClientCertificates(System.Security.Cryptography.X509Certificates.X509CertificateCollection? clientCertificates) -> Npgsql.NpgsqlSlimDataSourceBuilder!
5152
Npgsql.NpgsqlSlimDataSourceBuilder.UseClientCertificatesCallback(System.Action<System.Security.Cryptography.X509Certificates.X509CertificateCollection!>? clientCertificatesCallback) -> Npgsql.NpgsqlSlimDataSourceBuilder!
5253
Npgsql.NpgsqlSlimDataSourceBuilder.UseLoggerFactory(Microsoft.Extensions.Logging.ILoggerFactory? loggerFactory) -> Npgsql.NpgsqlSlimDataSourceBuilder!
54+
Npgsql.NpgsqlSlimDataSourceBuilder.UsePasswordProvider(System.Func<Npgsql.NpgsqlConnectionStringBuilder!, string!>? passwordProvider, System.Func<Npgsql.NpgsqlConnectionStringBuilder!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<string!>>? passwordProviderAsync) -> Npgsql.NpgsqlSlimDataSourceBuilder!
5355
Npgsql.NpgsqlSlimDataSourceBuilder.UsePeriodicPasswordProvider(System.Func<Npgsql.NpgsqlConnectionStringBuilder!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<string!>>? passwordProvider, System.TimeSpan successRefreshInterval, System.TimeSpan failureRefreshInterval) -> Npgsql.NpgsqlSlimDataSourceBuilder!
5456
Npgsql.NpgsqlSlimDataSourceBuilder.UsePhysicalConnectionInitializer(System.Action<Npgsql.NpgsqlConnection!>? connectionInitializer, System.Func<Npgsql.NpgsqlConnection!, System.Threading.Tasks.Task!>? connectionInitializerAsync) -> Npgsql.NpgsqlSlimDataSourceBuilder!
5557
Npgsql.NpgsqlSlimDataSourceBuilder.UseRootCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2? rootCertificate) -> Npgsql.NpgsqlSlimDataSourceBuilder!

0 commit comments

Comments
 (0)